Mysql_real_escape_string () etrafında dolaşan SQL enjeksiyonu
mysql_real_escape_string()
İşlev kullanılırken bile SQL enjeksiyon olasılığı var mı ?
Bu örnek durumu düşünün. SQL, PHP'de şu şekilde oluşturulur:
$login = mysql_real_escape_string(GetFromPost('login')); $password = mysql_real_escape_string(GetFromPost('password'));
$sql = "SELECT * FROM table WHERE login='$login' AND password='$password'";
Çok sayıda insanın bana bunun gibi bir kodun hala tehlikeli olduğunu ve mysql_real_escape_string()
kullanılan işlevle bile kırılmasının mümkün olduğunu söylediğini duydum . Ama olası bir istismar düşünemiyorum?
Bunun gibi klasik enjeksiyonlar:
aaa' OR 1=1 --
çalışma.
Yukarıdaki PHP kodundan geçebilecek olası bir enjeksiyondan haberiniz var mı?
Yanıtlar
Aşağıdaki sorguyu düşünün:
$iId = mysql_real_escape_string("1 OR 1=1"); $sSql = "SELECT * FROM table WHERE id = $iId";
mysql_real_escape_string()
sizi buna karşı korumaz. Sorgunuzda değişkenlerinizin etrafında tek tırnak ( ' '
) kullanmanız sizi buna karşı koruyan şeydir. Aşağıdakiler de bir seçenektir:
$iId = (int)"1 OR 1=1";
$sSql = "SELECT * FROM table WHERE id = $iId";
Kısa cevap evet, evet dolaşmanın bir yolu varmysql_real_escape_string()
. # Çok OBSCURE EDGE VAKALARI İÇİN !!!
Uzun cevap o kadar kolay değil. Burada gösterilen bir saldırıya dayanıyor .
Saldırı
Öyleyse, saldırıyı göstererek başlayalım ...
mysql_query('SET NAMES gbk');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*"); mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");
Belirli durumlarda, bu 1'den fazla satır döndürür. Burada neler olduğunu inceleyelim:
Bir Karakter Seti Seçme
mysql_query('SET NAMES gbk');
İşe bu saldırı için, sunucunun hem kodlamak için bağlantıda bekliyor o kodlamayı ihtiyaç
'
ASCII ie olarak0x27
ve kimin nihai bayt bir ASCII bazı karakter var\
yani0x5c
. Sonradan anlaşıldı ki, varsayılan olarak MySQL 5.6 desteklenen 5 tür kodlamalar vardır:big5
,cp932
,gb2312
,gbk
vesjis
. Burada seçeceğizgbk
.Şimdi, buranın kullanımına dikkat etmek çok önemli
SET NAMES
. Bu, SUNUCU ÜZERİNDEKİ karakter kümesini ayarlar . C API işlevine yapılan çağrıyı kullanırsakmysql_set_charset()
sorun olmaz (2006'dan beri MySQL sürümlerinde). Ama neden bir dakika içinde daha fazlası ...Yük
Bu enjeksiyon için kullanacağımız yük, bayt dizisi ile başlar
0xbf27
. İçindegbk
, bu geçersiz bir çok baytlı karakterdir; içindelatin1
, bu dize¿'
. İçinde olduğunu unutmayınlatin1
vegbk
,0x27
kendi hazır bilgi üzerine'
karakteri.Biz denilen eğer, çünkü bu yükü seçmiş
addslashes()
üzerine, biz bir ASCII eklemek istiyorum\
ie0x5c
önce,'
karakteri. Böylece0xbf5c27
,gbk
iki karakterlik bir dizi olan:0xbf5c
ardından gelen ile sonuçlanırdık0x27
. Ya da başka bir deyişle, geçerli bir karakter ve ardından gelen bir kaçış karakteri'
. Ama kullanmıyoruzaddslashes()
. Yani bir sonraki adıma geçelim ...mysql_real_escape_string ()
C API çağrısı , bağlantı karakter kümesini bilmesinden
mysql_real_escape_string()
farklıdıraddslashes()
. Böylece sunucunun beklediği karakter seti için kaçış işlemini düzgün bir şekilde gerçekleştirebilir. Ancak, bu noktaya kadar müşterilatin1
bağlantı için hala kullandığımızı düşünüyor çünkü aksini asla söylemedik. Biz söyledin sunucu Kullandığımızgbk
ancak istemci hala sanıyorlatin1
.Bu nedenle çağrı
mysql_real_escape_string()
ters eğik çizgiyi ekler ve'
"kaçmış" içeriğimizde serbest asılı bir karakter var ! Biz bakmak için olsaydı Aslında,$var
içindegbk
karakter kümesi, biz görürdük:縗 'VEYA 1 = 1 / *
Saldırının gerektirdiği de tam olarak bu.
Sorgu
Bu kısım sadece bir formalitedir, ancak işlenen sorgu şu şekildedir:
SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1
Tebrikler, az önce bir programa başarıyla saldırdınız mysql_real_escape_string()
...
Kötü
Daha da kötüleşiyor. MySQL ile hazırlanmış ifadeleri taklit etmekPDO
için varsayılan . Bu, istemci tarafında (C kütüphanesinde) temelde bir sprintf yaptığı anlamına gelir, bu da aşağıdakilerin başarılı bir enjeksiyonla sonuçlanacağı anlamına gelir:mysql_real_escape_string()
$pdo->query('SET NAMES gbk');
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));
Şimdi, öykünülmüş hazırlanmış ifadeleri devre dışı bırakarak bunu önleyebileceğinizi belirtmek gerekir:
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
Bu genellikle gerçek bir hazırlanmış ifadeyle sonuçlanacaktır (yani veriler, sorgudan ayrı bir pakette gönderilir). Bununla birlikte, PDO'nun MySQL'in yerel olarak hazırlayamayacağı öykünme ifadelerine sessizce geri döneceğini unutmayın: kılavuzda listelenenler , ancak uygun sunucu sürümünü seçmeye dikkat edin).
Çirkin
En başta söyledim, mysql_set_charset('gbk')
yerine kullansaydık bunların hepsini önleyebilirdik SET NAMES gbk
. Ve 2006'dan beri bir MySQL sürümü kullanıyorsanız, bu doğrudur.
Daha eski bir MySQL salınımını, ardından kullanıyorsanız hata içinde mysql_real_escape_string()
böyle bizim yükü içinde olanlar gibi geçersiz baytlı karakterler amaçları kaçmak için tek bayt olarak tedavi edildi anlamına geliyordu istemci doğru bağlantı kodlama haberdar olmuştu bile bu yüzden bu saldırı olur ve hala başarılı. Hata MySQL giderilmiştir 4.1.20 , 5.0.22 ve 5.1.11 .
Ama en kötüsü olduğunu PDO
C API maruz vermedi mysql_set_charset()
böylece önceki sürümlerinde bu 5.3.6 kadar olamaz , mümkün olan her komut için bu saldırıyı önlemek! Artık bir DSN parametresi olarak gösteriliyor .
Kurtarıcı Zarafet
Başlangıçta söylediğimiz gibi, bu saldırının işe yaraması için veritabanı bağlantısının savunmasız bir karakter seti kullanılarak kodlanması gerekir. utf8mb4olduğunu savunmasız değil destekleyebilir henüz ve her MySQL 5.5.3 beri yerine-ama sadece olmuştur kullanılabilir kullanmayı seçebilir böylece: Unicode karakter. Bir alternatif, utf8aynı zamanda savunmasız olmayan ve Unicode Temel Çok Dilli Düzlemin tamamını destekleyebilen bir alternatiftir .
Alternatif olarak, NO_BACKSLASH_ESCAPES(diğer şeylerin yanı sıra) çalışmasını değiştiren SQL modunu etkinleştirebilirsiniz mysql_real_escape_string()
. Bu mod etkinleştirildiğinde, 0x27
değiştirilecek 0x2727
ziyade 0x5c27
kaçan süreci dolayısıyla ve olamaz , daha önce var olmayan savunmasız kodlamalarla geçerli karakterleri oluşturmak (yani 0xbf27
hala 0xbf27
vb.) - Sunucu yüzden hala geçersiz olarak dize reddedecektir . Bununla birlikte, bu SQL modunu kullanmaktan kaynaklanabilecek farklı bir güvenlik açığı için @ eggyal'ın cevabına bakın .
Güvenli Örnekler
Aşağıdaki örnekler güvenlidir:
mysql_query('SET NAMES utf8');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*"); mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");
Çünkü sunucu bekliyor utf8
...
mysql_set_charset('gbk');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*"); mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");
Çünkü karakter setini, istemci ve sunucu eşleşecek şekilde doğru bir şekilde ayarladık.
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); $pdo->query('SET NAMES gbk');
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));
Çünkü öykünmüş hazırlanmış ifadeleri kapattık.
$pdo = new PDO('mysql:host=localhost;dbname=testdb;charset=gbk', $user, $password);
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));
Çünkü karakter setini doğru bir şekilde ayarladık.
$mysqli->query('SET NAMES gbk');
$stmt = $mysqli->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$param = "\xbf\x27 OR 1=1 /*"; $stmt->bind_param('s', $param); $stmt->execute();
Çünkü MySQLi her zaman doğru hazırlanmış ifadeler yapar.
Sarma
Eğer sen:
- MySQL'in Modern Sürümlerini (5.1'in sonları, tümü 5.5, 5.6, vb.) VE
mysql_set_charset()
/$mysqli->set_charset()
/ PDO'nun DSN karakter kümesi parametresini (PHP ≥ 5.3.6'da) kullanın
VEYA
- Bağlantı kodlaması için savunmasız bir karakter kümesi kullanmayın (yalnızca
utf8
/latin1
/ascii
/ vb. Kullanırsınız)
% 100 güvendesin.
Aksi takdirde, kullanıyor olsanız bilemysql_real_escape_string()
savunmasızsınız ...
TL; DR
mysql_real_escape_string()
aşağıdaki durumlarda hiçbir koruma sağlamaz (ve ayrıca verilerinizi kırabilir):
MySQL'in NO_BACKSLASH_ESCAPESSQL modu etkindir ( her bağlandığınızda açıkça başka bir SQL modu seçmediğiniz sürece bu olabilir ); ve
SQL dize değişmezleriniz çift tırnak
"
karakterleri kullanılarak tırnak içine alınır .Bu hata # 72458 olarak dosyalanmış ve MySQL v5.7.6'da düzeltilmiştir (aşağıdaki " The Saving Grace " başlıklı bölüme bakın).
Bu başka, (belki daha az mı?) Belirsiz EDGE CASE !!!
@ İrcmaxell'in mükemmel cevabına saygı göstererek (gerçekten, bunun intihal değil, övgü olması gerekiyor!), Onun formatını benimseyeceğim:
Saldırı
Bir gösteri ile başlamak ...
mysql_query('SET SQL_MODE="NO_BACKSLASH_ESCAPES"'); // could already be set
$var = mysql_real_escape_string('" OR 1=1 -- '); mysql_query('SELECT * FROM test WHERE name = "'.$var.'" LIMIT 1');
Bu test
tablodaki tüm kayıtları döndürecektir . Bir diseksiyon:
Bir SQL Modu Seçme
mysql_query('SET SQL_MODE="NO_BACKSLASH_ESCAPES"');
String Literals altında belgelendiği gibi :
Bir dizeye tırnak işareti eklemenin birkaç yolu vardır:
"
'
" İle tırnak içine alınmış bir dizge içindeki "'
", " " olarak yazılabilir''
."
"
" İle tırnak içine alınmış bir dizge içindeki ""
", " " olarak yazılabilir""
.Alıntı karakterinin önüne bir çıkış karakteri ("
\
") koyun ."
'
" İle alıntılanan bir dizenin içindeki ""
" özel bir işleme gerek yoktur ve iki katına çıkarılması veya kaçılması gerekmez. Aynı şekilde, ""
" ile alıntılanan bir dizenin içindeki "'
" özel bir işleme ihtiyaç duymaz.
Sunucunun SQL modu içeriyorsa NO_BACKSLASH_ESCAPES, bu seçeneklerden üçüncüsü (benimsediği genel yaklaşım)
mysql_real_escape_string()
mevcut değildir: onun yerine ilk iki seçenekten biri kullanılmalıdır. Dördüncü madde işaretinin etkisinin, kişinin verilerini parçalamaktan kaçınmak için değişmezi alıntılamak için kullanılacak karakteri mutlaka bilmesi gerektiğine dikkat edin.Yük
" OR 1=1 --
Yük, bu enjeksiyonu
"
karakterle tam anlamıyla başlatır . Belirli bir kodlama yok. Özel karakter yok. Garip bayt yok.mysql_real_escape_string ()
$var = mysql_real_escape_string('" OR 1=1 -- ');
Neyse ki,
mysql_real_escape_string()
SQL modunu kontrol ediyor ve davranışını buna göre ayarlıyor. Bakınız libmysql.c:ulong STDCALL mysql_real_escape_string(MYSQL *mysql, char *to,const char *from, ulong length) { if (mysql->server_status & SERVER_STATUS_NO_BACKSLASH_ESCAPES) return escape_quotes_for_mysql(mysql->charset, to, 0, from, length); return escape_string_for_mysql(mysql->charset, to, 0, from, length); }
Bu nedenle
escape_quotes_for_mysql()
,NO_BACKSLASH_ESCAPES
SQL modu kullanımdaysa , farklı bir temel işlev çağrılır . Yukarıda belirtildiği gibi, böyle bir işlevin, diğer tırnak karakterinin harfi harfine tekrarlanmasına neden olmadan onu tekrarlamak için harfi harfine alıntı yapmak için hangi karakterin kullanılacağını bilmesi gerekir.Bununla birlikte, bu işlev keyfi olarak dizenin tek tırnak karakteri kullanılarak alıntılanacağını varsayar
'
. Bakınız charset.c:/* Escape apostrophes by doubling them up // [ deletia 839-845 ] DESCRIPTION This escapes the contents of a string by doubling up any apostrophes that it contains. This is used when the NO_BACKSLASH_ESCAPES SQL_MODE is in effect on the server. // [ deletia 852-858 ] */ size_t escape_quotes_for_mysql(CHARSET_INFO *charset_info, char *to, size_t to_length, const char *from, size_t length) { // [ deletia 865-892 ] if (*from == '\'') { if (to + 2 > to_end) { overflow= TRUE; break; } *to++= '\''; *to++= '\''; }
Bu nedenle, çift tırnak
"
karakterlerini değiştirmeden bırakır (ve tüm tek tırnaklı'
karakterleri ikiye katlar ) , gerçek karakterden bağımsız olarak alıntı yapmak için kullanılan gerçek karakterden bağımsızdır ! Bizim durumda$var
sağlandığı argüman olarak tamamen aynı kalıntılarmysql_real_escape_string()
kaçış yok yerini almıştır sanki -Bu hiç .Sorgu
mysql_query('SELECT * FROM test WHERE name = "'.$var.'" LIMIT 1');
Bir formalite, işlenen sorgu şudur:
SELECT * FROM test WHERE name = "" OR 1=1 -- " LIMIT 1
Bilgili arkadaşımın dediği gibi: tebrikler, bir programa başarıyla saldırdın mysql_real_escape_string()
...
Kötü
mysql_set_charset()yardımcı olamaz, çünkü bunun karakter kümeleriyle hiçbir ilgisi yoktur; ne de yapamaz mysqli::real_escape_string(), çünkü bu aynı işlevin etrafındaki farklı bir sarmalayıcıdır.
Sorun, halihazırda açık mysql_real_escape_string()
değilse de, daha sonra karar vermek için geliştiriciye bırakıldığından , çağrının değişmezin hangi karakterle alıntılanacağını bilememesidir . Bu nedenle, NO_BACKSLASH_ESCAPES
modda, bu işlevin keyfi alıntılarla kullanmak için her girdiden güvenli bir şekilde kaçabilmesinin hiçbir yolu yoktur (en azından, iki katına çıkarılmasını gerektirmeyen karakterleri ikiye katlamadan ve böylece verilerinizi parçalamadan).
Çirkin
Daha da kötüleşiyor. NO_BACKSLASH_ESCAPES
standart SQL ile uyumluluk için kullanılması gerekliliği nedeniyle vahşi doğada o kadar da nadir olmayabilir (örneğin, SQL-92 belirtiminin 5.3 bölümüne bakın , yani <quote symbol> ::= <quote><quote>
dilbilgisi üretimi ve ters eğik çizgiye verilen herhangi bir özel anlamın olmaması). Ayrıca, ircmaxell'in gönderisinin açıkladığı (uzun zamandan beri düzeltilen) hataya geçici bir çözüm olarak kullanımı açıkça önerildi . Kim bilir, bazı DBA'lar, gibi yanlış kaçış yöntemlerinin kullanılmasını caydırmak için varsayılan olarak açık olacak şekilde bile yapılandırabilir .addslashes()
Ayrıca, yeni bir bağlantının SQL modu , yapılandırmasına göre sunucu tarafından ayarlanır (bir SUPER
kullanıcı herhangi bir zamanda değiştirebilir); bu nedenle, sunucunun davranışından emin olmak için, bağlandıktan sonra istediğiniz modu her zaman açıkça belirtmeniz gerekir .
Kurtarıcı Zarafet
SQL modunu her zaman açıkça dahil etmeyecek şekilde ayarladığınız NO_BACKSLASH_ESCAPES
veya tek tırnak karakterini kullanarak MySQL dizesi değişmezlerini alıntıladığınız sürece, bu hata çirkin başını escape_quotes_for_mysql()
geri alamaz : sırasıyla kullanılmayacak veya hangi tırnak karakterlerinin tekrarlanmasını gerektirdiği varsayımı doğru ol.
Bu nedenle, tek tırnaklı dize değişmezlerinin alışılmış kullanımını zorlayacağından, kullanan herkesin modu NO_BACKSLASH_ESCAPES
etkinleştirmesini tavsiye ederim ANSI_QUOTES. Bunun, çift tırnaklı değişmez değerlerin kullanılması durumunda SQL enjeksiyonunu engellemediğini unutmayın; yalnızca bunun gerçekleşme olasılığını azaltır (çünkü normal, kötü niyetli olmayan sorgular başarısız olur).
PDO'da, hem eşdeğer işlevi PDO::quote()hem de hazırlanmış deyim öykünücüsü çağırır mysql_handle_quoter()- tam olarak bunu yapar: kaçan değişmezin tek tırnak içinde alıntılanmasını sağlar, böylece PDO'nun bu hataya karşı her zaman bağışık olduğundan emin olabilirsiniz.
MySQL v5.7.6'dan itibaren bu hata düzeltildi. Değişiklik günlüğüne bakın :
İşlev Eklendi veya Değiştirildi
Uyumsuz Değişiklik: Yeni bir C API işlevimysql_real_escape_string_quote()yerine getirilmiştir,mysql_real_escape_string()çünkü ikinci işlevNO_BACKSLASH_ESCAPESSQL modu etkinleştirildiğindekarakterleri doğru şekilde kodlayamayabilir. Bu durumda,mysql_real_escape_string()tırnak karakterlerini iki katına çıkarmak dışında kaçamaz ve bunu doğru bir şekilde yapmak için, alıntı bağlamı hakkında mevcut olandan daha fazla bilgi bilmesi gerekir. mysql_real_escape_string_quote()alıntı bağlamını belirtmek için fazladan bir argüman alır. Kullanım detayları için mysql_real_escape_string_quote () 'ye bakınız.
Not
Uygulamalar , artık başarısız olan ve etkinleştirilirse bir hata üreten mysql_real_escape_string_quote()yerine kullanılmak üzere değiştirilmelidir .mysql_real_escape_string()CR_INSECURE_API_ERRNO_BACKSLASH_ESCAPES
Referanslar: Ayrıca bkz. Hata # 19211994.
Güvenli Örnekler
İrcmaxell tarafından açıklanan hatayla birlikte ele alındığında, aşağıdaki örnekler tamamen güvenlidir (MySQL'in 4.1.20, 5.0.22, 5.1.11'den sonra kullanıldığı veya GBK / Big5 bağlantı kodlamasının kullanılmadığı varsayılırsa) :
mysql_set_charset($charset);
mysql_query("SET SQL_MODE=''");
$var = mysql_real_escape_string('" OR 1=1 /*'); mysql_query('SELECT * FROM test WHERE name = "'.$var.'" LIMIT 1');
... çünkü açıkça içermeyen bir SQL modu seçtik NO_BACKSLASH_ESCAPES
.
mysql_set_charset($charset); $var = mysql_real_escape_string("' OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");
... çünkü dize değişmezimizi tek tırnaklarla alıntılıyoruz.
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1'); $stmt->execute(["' OR 1=1 /*"]);
... çünkü PDO tarafından hazırlanan ifadeler bu güvenlik açığından muaftır (ve ircmaxell de PHP≥5.3.6 kullanıyor olmanız ve karakter setinin DSN'de doğru şekilde ayarlanmış olması koşuluyla; veya hazırlanmış ifade öykünmesinin devre dışı bırakılmış olması koşuluyla) .
$var = $pdo->quote("' OR 1=1 /*");
$stmt = $pdo->query("SELECT * FROM test WHERE name = $var LIMIT 1");
... çünkü PDO'nun quote()
işlevi sadece değişmezden kaçmakla kalmaz, aynı zamanda onu (tek tırnaklı '
karakterlerle) tırnak içine alır ; bu durumda ircmaxell'in hatasından kaçınmak için PHP≥5.3.6 kullanıyor olmanız ve karakter kümesini DSN'de doğru şekilde ayarlamış olmanız gerektiğini unutmayın .
$stmt = $mysqli->prepare('SELECT * FROM test WHERE name = ? LIMIT 1'); $param = "' OR 1=1 /*";
$stmt->bind_param('s', $param);
$stmt->execute();
... çünkü MySQLi tarafından hazırlanan ifadeler güvenlidir.
Sarma
Dolayısıyla, eğer:
- yerel hazırlanmış ifadeler kullanın
VEYA
- MySQL v5.7.6 veya üzerini kullanın
VEYA
içinde ilave en az biri ircmaxell özetinde çözümlerden bir, kullanım istihdam etmek:
- PDO;
- tek tırnaklı dize değişmezleri; veya
- içermeyen açıkça ayarlanmış bir SQL modu
NO_BACKSLASH_ESCAPES
... o zaman tamamen güvende olmalısınız (bir kenara kaçan dizge kapsamı dışındaki güvenlik açıkları).
%
Joker karakter dışında bundan geçebilecek hiçbir şey yok . Eğer bunu filtrelemezseniz LIKE
saldırganın %
giriş olarak koyabileceği bir ifade kullanıyorsanız tehlikeli olabilir ve kullanıcılarınızdan herhangi birinin şifresini zorla kullanmanız gerekir. Veriler, sorgunun kendisine bu şekilde müdahale edemeyeceğinden, insanlar genellikle% 100 güvenli hale getirmek için hazırlanmış ifadelerin kullanılmasını önerir. Ancak bu kadar basit sorgular için, büyük olasılıkla daha verimli olacaktır.$login = preg_replace('/[^a-zA-Z0-9_]/', '', $login);