Web Uygulama Güvenliğinde adını sıkça duyuran, OWASP Top 10'da 1. kategoride yer alan SQL Injection zaafiyetine dair detaylı bir incelemede bulunmak istedim.
2. Merhabalar,
Bu yazımızda internet dünyasında adını sıkça
duyduğumuz ve Owasp Top 10 Security
Risks1
sıralamasında bulunan SQL Injection
(SQLi) zaafiyetini anlamaya çalışacağız.
Konunun ayrıntılı olarak daha iyi bir biçimde
anlaşılması için öncelikle SQL nedir sorusuna
deyinip, ardından Injection mantığını anlamaya
ve ilerleyen aşamalarda ise türlere göre
farklılıklarını örnekler ile açıklamaya çalışıp
inceleyeceğiz.
SQL Nedir?
En genel tanımı ile SQL nedir sorusuna cevap verecek olursak;
İngilizce açılımı Structured Queery Language olan ve Türkçe’ye Yapılandırılmış
Sorgu Dili olarak çevirebileceğimiz veritabanı yönetim sistemidir.2
Açıkçası
tanımı daha da genişletecek olursak belirli bir amaca yönelik bilgileri
(Örneğin bir web sitesine kayıt olduğunuzdaki kullanıcı adı ve parolanız) bir
düzen içerisinde tutan veritabanı adını verdiğimiz yapı üzerinde işlem
yapmamızı sağlayan bir alt dildir.Bilinenin aksine bir programlama dili
değildir.
Bu örnekte ele alacağımız seneryoda Bilgiler
tablosunun Tablo1.1’de ki gibi verildiğini
düşünelim. Şayet Bilgiler tablosunda bulunan
OgrenciAd sütununu listelemek istersek
yazacağımız SQL sorgusu;
SELECT OgrenciAd FROM Bilgiler
şeklinde olacaktır. Yani SELECT komutu ile listelemek istediğimiz sütunu
seçerken, FROM ile de hangi tabloya bakmamız gerektiğini belirtiyoruz.
Bu sorgudan beklediğimiz çıktı Sevinc, Cihan, Servet, Bahar, Mehmet
olacaktır.
1 https://www.owasp.org/index.php/Top_10-2017_Top_10
2 https://tr.wikipedia.org/wiki/SQL
Tablo 1.1 Bilgiler
3. SQL sorgularında sıkça kullandığımız belirli komutlar
vardır. Bunlara örnek olarak;
• FROM
• SELECT
• WHERE
Bu komut ile belirli bir ön koşulu sağlayan verileri isteyebiliriz. Örneğin
Bilgiler tablosundan spesifik olarak Numarası 98 değerine sahip olan
öğrencinin bilgilerini istediğimizi düşünelim. (Basit halde programlama
dillerindeki if koşulu gibi düşünebiliriz)
SELECT*FROM Bilgiler WHERE Numara=98
• ORDER BY
Varsayılan olarak artan düzende sıralama.
SELECT* FROM Bilgiler ORDER BY Numara
şeklinde yapacağımız sorguda çıktı;
Cihan, Mehmet, Servet, Sevinç, Bahar
• AND
Örneğin Bilgiler tablosunda Numarası“98” ve ID değeri“4” olanları listelemek
istiyoruz.
SELECT* FROM Bilgiler WHERE Numara=98 AND ID=4
• UPDATE
• DELETE
bu komutları verebiliriz.İlerleyen süreçte yapacağımız örnekleri anlamanız
açısından yukarıdaki bilgileri yeterli buldum ancak buradaki3
bağlantı ile daha
fazla bilgiye buradaki4
bağlantı ile de alıştırma sayfasına gidebilirsiniz.
3 https://www.w3schools.com/sql/default.asp
4 https://www.w3schools.com/sql/trysql.asp?filename=trysql_op_in
4. Injection Mantığı
İnjection mantığını anlamak için öncelikle bir kullanıcının isteğini ve istek
doğrultusunda arka planda dönen olayları inceleyerek başlayalım.
Parametre olarak vereceğimiz id değeri
doğrultusunda öğrencinin numarasını
istiyoruz. Kod parçacığında görüldüğü üzere
hiçbir kontrol olmaksızın parametre
değerimiz sorguya katılıyor. (Burada sanırım
bir ampül yandı)
Parametre olarak 5 değerini gönderdik (request) ve sunucumuz bu istek
sonucu bize id değeri 5 olan Mehmet isimli öğrencinin numarasını verdi
(response).
Id değerimiz hiçbir kontrolden geçmeden sorgumuza aktarılıyordu, bu
sorguyu nasıl manipüle edebiliriz diye düşündüğümüzde;
Burada 5' or '1' = '1 ifadesini biraz daha açalım;
i. 5' değeri ile normal sorgumuzu bitirdi
ii. or '1' = '1 ifadesi ile mantıksal açıdan sürekli TRUE değer döndürecek
bir ifadeyi sorgumuza ekledik.
5. iii. Sonuç olarak sorgumuz id değeri 5 olan yada 1 = 1 koşulunun
sağlandığı süre zarfı boyunca bize satırların numara değerini getirecek,
totalde bu tüm sütuna eşit olacak.
Yukarıdaki örnek ile id parametresi üzerinden id sütununu çekmeyi başardık.
Bu zaafiyetin adında neden Injection geçtiğini anladık umarım!
SQL Injection
Veri tabanlarına dayalı uygulamalarda en yaygın web hacking tekniği
SQLi’dir diyebiliriz. Uygulamanın kullanıcıdan herhangi bir giriş beklediği
alana SQL ifadeleri gömülür. Eğer bu input alanında doğru bir filtreleme
yoksa girdiğimiz SQL sorguları hata vermeden çalışır. Bu açıktan
yararlanılarak saldırgan örnek olarak uygulamanın authentication5
ve
authorization6
mekanizmasını bypass edebilir bunun ötesinde veri tabanını
tamamen yok edebilir.
Yaygın düşüncenin aksine sadece web sitelerinde değil, çoğu sistemde eğer
gerekli ve doğru bir filtrelenme yapılmamış ise SQL Injection tehtidinden
bahsedebiliriz.
Kullanılan teknik ve uygulamadan alacağımız yanıta göre değişen farklı
türleri bulunmaktadır.
• Genel SQLi
• Union Based SQLi
• Blind Based SQLi
• Time Based SQLi
Bu türlerin hepsini ilerleyen sayfalarda örnekler dahilinde açıklamaya
çalışacağız. Şimdi buraya kadar geldiğimize göre yeterli düzeyde SQL bilgimiz
var, Injection mantığını biliyoruz, uygun bir test ortamımız var. Eee ne
duruyoruz örnek yapsak ya!
SQLi spesifik olarak türlerinden bahsetmek yerine öncelikle Genel SQLi
hakkında bir örnek yapalım.Yararlanacağımız ortam DVWA7
test ortamı
olacaktır.
5 Bir web uygulamasında kimlik doğrulama aşaması. Genellikle kullanıcı adı – parola şeklindedir.
6 Yetkilendirme, giriş yapan kullanıcının erişim kısıtlamaları.
7 http://www.dvwa.co.uk/ (İçinde zaafiyet barındıran bir test ortamıdır.)
6. Yukarıda da bahsettiiğimiz gibi
görüldüğü üzere bir input alanı
verilmiş. Kullanıcıdan bir User ID
girilmesi bekleniyor. Örnek bir sorgu
ve yanıtını görmekteyiz.
Sorgumuzu bölecek ve hata
verdirecek bir ifadeyi bulmamız
gerekiyor. Bunun için deneme
yanılma yolu kullanılabilir ancak
genellikle ' veya " işareti sorguyu
bölmemize yardımcı olacaktır. Yanda
verilen sorgu çalıştırıldığında çıktısı;
olarak karşımıza çıkar. Buradan anlıyoruz ki ' işareti bu uygulama için
sorgumuzu başarılı bir şekilde bölmek için yeterli. Geri de yazacağımız
sorgular ve elde etmek istediğimiz veriler SQL bilgilerimiz ile doğru orantılı.
Görüldüğü gibi 4' or'1' ='1 sorgusu bize
veri tabanında olan tüm kullanıcıların
bilgilerini getirdi. Peki bu nasıl oldu?
Daha iyi anlamak adına sayfa kaynak
kodlarına bir göz gezdirelim.
7. $query ="SELECT first_name, last_name FROM users WHERE user_id ='$id';"
Sorgumuz input alanından aldığı $id isimli değişkeni herhangi bir filtreleme
işlemine tabi tutmadan çalıştırıyor.
Biz ise $id değerine 4' or'1' ='1 ifadesini vererek 4' ile ilk sorgumuzu
sonlandırıp or'1' ='1 ifadesi ile sorguya ek yapıp istediğimiz bilgilere
erişiyoruz. Son halde çalışan sorgu şu şekilde olmaktadır;
$query ="SELECT first_name, last_name FROM users WHERE user_id ='4' or'1' ='1';"
Union Based SQLi
SQL Injection zaafiyetinin bu türünde spesifik olarak istediğimiz en önemli
gereklilik sorgu sonuçlarımızı (Result – Set ) görebilmemizdir.
Konuya girmeden önce SQL Sorgu dilinde UNION operatörüne değinelim.
UNION operatörünün kullanım amacı herhangi 2 sorguyu Result-Set’e
yansıtmasıdır.
SELECT dbname FROM table1 UNION SELECT dbname FROM table2
Şeklinde yazacağımız bir sorguda dbname adında
veritabanında bulunan table1 ve table2 degerlerini
birlikte döndürür.
dunya adında ulkeler ve sehirler tablolarına sahip
olan bir veritabanımızın olduğunu düşünelim.
SELECT* FROM ulkeler sorgusunda sonucumuz Result-
Set 1 şeklinde olacaktır.
Result-Set 1
8. Hem ulkeler hem de sehirler tablosundaki verileri tek bir sorguda
birleştirmek istersek devreye UNION komutu giriyor.
SELECT* FROM ulkeler UNION SELECT* FROM sehirler
sorgusunun çıktısı Result-Set 2’de gösterilmektedir.
Burada önemli noktalardan biri eğer ulkeler tablomuzun veri tipi string ve
diyelim ki sehirler tablomuzunda veri tipi int türünden olsun. UNION
komutunu bu koşullarda işletmeye çalıştığımızda bir hata ile karşılaşırız(Bu
hata tabii ki de bypass edilemeyecek bir hata değil). Ayrıca UNION
komutunun başarılı bir şekilde işlenmesi için alan sayısının (kolonlar) her iki
sorguda da aynı olması şartı vardır.
UNION komutu ile ilgili temel bir bilgi sağladığımıza göre zaafiyet kısmına
geçelim. (Web For Pentester’dan yararlanacağız.)
Yukarıda gösterildiği gibi bir yapımızın olduğunu düşünelim. name değerine
parametre olarak verdiğimiz değerler sonucu uygulamanın girilen değere ait
id,name,age çıktılarını verdiğini görüyoruz. Büyük ihtimalle arka taraftaa
SELECT id,name,age FROM users where name='user1'
şeklinde bir sorgu çalıştığını tahmin edebiliriz.
Injection Mantığı başlığı altında anlattiıklarımdan yola çıkarak ilk denemizi
yapıyoruz.
Result-Set 2
9. Result-Set’imizde gördüğümüz değerlere göre sorgumuzun;
SELECT id,name,age FROM users where name='user1' or'deneme' ='deneme'
şekline dönüştüğünü söyleyebiliriz. Bu nedenle or’dan sonraki ifade daima
TRUE değer döndürdüğü için tüm kayıtlar listelendi.SQL sorgularını bölerek
kendi kodumuzu çalıştırabildiğimizi gördük.
Ekran alıntısında gösterildiği gibi Result – Set’de göreceğimiz değerleri
sıralamak istiyoruz. Bu sorgular sonucunda sonucumuz
şeklinde olacaktır.
Peki ya sorgusunu çalıştırmak
istersek?
Ekran Alıntısı
10. Burada ekrana herhangi bir değer dönmeyecektir. Sebebi 6. kolon değerinin
bulunmamasıdır. Böylece kolon sayımızı tespit etmiş olduk. UNION
komutunun kullanımı açısından (yukarıda belirttiiğim gibi kolon sayılarının
eşit olması gerekiyordu) önemli bir bilgi.
Gelelim en nihayetinde UNION komutumuzu kullanmaya;
Sorgumuzu
user1’in bilgilerini çek ve 2. kolona denk gelen alana test stringini yaz
diye düşünebiliriz.
Uygulamamız istediğimiz bir şekilde sonuç döndürdü.
Bu başlığa ilk girişimde‘spesifik olarak istediğimiz en önemli gereklilik sorgu
sonuçlarımızı (Result – Set ) görebilmemizdir’ dememin sebebini şimdi daha da
iyi kavramışızdır diye düşünüyorum. Zaafiyetimizi bulduk, işlettiik. İlerleyen
süreçte yazacağımız sorgular hayal gücümüze ve veritabanı bilgimize kalmış.
Örnek olarak veritabanın şuan hangi versiyonda olduğunu öğrenmek
istiyoruz.
Bunun için kullancağımız fonksiyon version()’dır. 'test' kısmına version()
fonksiyonunu yazmamız yeterli olacaktır.
Aynı şekilde örnek olarak INFORMATION_SCHEMA ile veritabanına ait
datalara ulaşabiliriz.
11. Sorgusu ile veritabanında bulunan tüm tablo isimlerini çekmiş olduk.
Blind Based SQLi
Hatırlarsanız UNION Based SQLi için mutlaka Result-Set değerlerini
görmemiz gerekir demiştik, bu söz konusu zaafiyet için spesifik bir özellikti.
Blind Based türü için de öncelikle diyeceğimiz özellik ekrana asla verilerin
yansımayacağı olur. Adından da anlaşılacağı gibi Blind (kör).
Görüldüğü gibi vereceğimiz id değerine göre kullanıcı bilgilerini bize getiriyor.
Şimdi sorgumuz üstünde denemeler yapmaya başlıyoruz.
Sorgumuza and 1 = 1 gibi TRUE değer döndürecek bir ifade eklediğimizde
Result – Set değerinde bir değişiklik olmadığını görüyoruz.
12. Ancak and 1 = 2 gibi bir FALSE değer döndürecek ifade eklediğimizde
herhangi bir çıktı alamıyoruz.
Buradan anlıyoruz ki and ifademizin sağına yazdığımız mantıksal ifadeler
sorgumuza ekleniyor ve sorgudan dönen değerlerimiz mantıksal ifadelerin
sonuçlarına göre şekilleniyor.
Peki burada ekleyeceğimiz sorguyu nasıl şekillendirebiliriz?
Örneğin veritabanının verisyonunu öğrenmek istiyoruz
eğer substring(version(),1,1) = 4 ifadesi doğru olsaydı web sayfasında id
değeri 1 olan kullanıcı için bilgileri görebilecektik. Herhangi bir çıktı
alamadağımız için versiyonumuzun 3 veya 4 olmadığı çıkarımını yapıyoruz.
substring(version(),1,1) = 5 ifadesi ile Result-Set’i görebiliyoruz ve buradan
anlıyoruz ki versiyon değerimiz 5.
13. Time Based SQLi
4 örnek ile Injection mantığını anladığımızı düşünerek burada yanlızca örnek
sorgular ile açıklamaya çalışacağım.
Yukarıda gösterilen kod parçacığını ilk örnekten hatırlıyoruz. Burada zaman
tabanlı bir SQL injection yapmak istersek
' UNION SELECT SLEEP(5), 2
parametresini yollamamız gerekecek.
Farklı veritabanı uygulamaları için tabi ki farklı bekleme komutları mevcut.
MySQL ve SQL Server için şekildeki komutlar geçerli.
Çoğu zaman Time Based SQLi ile Blind Based SQLi birlikte kullanılır
diyebiliriz. Bir örnekle daha iyi anlayalım;
Arama yapacağımız bir sayfamız olduğunu düşünelim. Yukarıda SLEEP
fonksiyonun zaafiyeti tespit etmemize fayda sağlayacığını görmüştük.
sqlinjection.net/time-based/
14. deneme' SLEEP(5)#
Şeklinde bir arama yaptığımızda sayfanın bize geç cevap verdiğini görüyoruz
Blind based türünde deneme – yanılma ile istediğimiz bilgiyi elde etmeye
çalışıyorduk. Blind Based başlığı altında yaptığımız versiyon bulma
denemesini bu örneğimize uyarlayacak olursak kullanacağımız payload
deneme' IF(SUBSTRING(VERSION(),1,1) = '5', SLEEP(5), 0)#
Burada HTTP response yazdığımız sorgunun doğruluna göre;
1. Bekleyerek dönecek
2. Doğrudan dönecek
Korunma Yöntemleri
SQL Injection zaafiyetinin türlerine göre incelemesini ve istediğimiz verileri
nasıl elde edebileceğimize dair örnekler yaptık. Peki bu kadar tehlikeli olan bu
zaafiyet türünden nasıl korunacağız?
Bir çok kişi için akla gelen ilk yöntem Regular expression oluyor ancak bu
yöntem her ne kadar filtreye göre fark göstersede bypass edilebilecek bir
yöntem. Örnek olması açısıdan;
Yanda verilen kaynak kodlar
incelendiğinde name parametresi boşluk
karakterini içeriyorsa die fonksiyonunu
çalıştırır.
Adından da anlaşılacağı gibi die
fonksiyonu sağlıklı bir bağlantı sağlamaz ve
aynı zamanda istenen hata mesajını verir
15. Web Geliştiricisinin buna benzer bir önlem aldığını düşünelim
Öngördüğümüz gibi hata ile karşılaştık boşluk karakteri kullanmadan
sorgumuzu manipüle etmemiz gerek. Farklı yöntemler de mevcut ancak akla
ilk gelen yöntem yorum satırı kullanmak
Böylece bize bağlantı imkanı vermeyecek if kontrolünden kaçmayı başardık.
Dinamik olan SQL sorgularından kaçınmamız, kullanıcının herhangi bir input
değerini sorgumuza eklemememiz gerek. Buraya OWASP’ın SQL Injection
Prevention Cheat Sheet makalesinden bir alınıt bırakayım;
16. Aynı şekilde PHP için Prepared Statement örneği verecek olursak;
Evet şimdi aklınızda bir soru var neden Güvenli yazmak yerine Güvenli?!
yazdım? Sebebi basit herhangi bir yöntem %100 başarı sağlıyacak, bizi
tamamen SQL Injection saldırılarına karşı koruyacak diyemeyiz.
Herhangi bir sızıntı dahilinde hasarımızı azaltmak açısından verilerimizi
veritabanımıza şifrelenmiş bir şekilde kayıt etmemiz fayda sağlayacaktır.
Güvensiz Kullanım Örneği
Güvenli?!
Güvensiz
Güvenli?!