Jawaban singkatnya adalah ya, ya, ada cara untuk menyiasati mysql_real_escape_string()
.#Untuk KASUS EDGE yang Sangat samar!!!
Jawaban panjangnya tidak begitu mudah. Ini didasarkan pada serangan ditunjukkan di sini .
Serangan
Jadi, mari kita mulai dengan menunjukkan serangannya...
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");
Dalam keadaan tertentu, itu akan mengembalikan lebih dari 1 baris. Mari kita bedah apa yang terjadi di sini:
-
Memilih Kumpulan Karakter
mysql_query('SET NAMES gbk');
Agar serangan ini berfungsi, kita memerlukan penyandian yang diharapkan server pada sambungan untuk menyandikan
'
seperti dalam ASCII yaitu0x27
dan untuk memiliki beberapa karakter yang byte terakhirnya adalah\
ASCII yaitu0x5c
. Ternyata, ada 5 pengkodean yang didukung di MySQL 5.6 secara default:big5
,cp932
,gb2312
,gbk
dansjis
. Kami akan memilihgbk
di sini.Sekarang, sangat penting untuk memperhatikan penggunaan
SET NAMES
di sini. Ini menetapkan set karakter ON THE SERVER . Jika kita menggunakan panggilan ke fungsi C APImysql_set_charset()
, kami akan baik-baik saja (pada rilis MySQL sejak 2006). Tapi lebih lanjut tentang mengapa dalam satu menit... -
Muatannya
Payload yang akan kita gunakan untuk injeksi ini dimulai dengan urutan byte
0xbf27
. Digbk
, itu adalah karakter multibyte yang tidak valid; dalamlatin1
, ini adalah string¿'
. Perhatikan bahwa dalamlatin1
dangbk
,0x27
sendiri adalah'
liter literal karakter.Kami memilih payload ini karena, jika kami memanggil
addslashes()
di atasnya, kami akan memasukkan\
ASCII yaitu0x5c
, sebelum'
karakter. Jadi kita akan berakhir dengan0xbf5c27
, yang digbk
adalah urutan dua karakter:0xbf5c
diikuti oleh0x27
. Atau dengan kata lain, valid karakter diikuti oleh'
. yang tidak terhapus . Tapi kami tidak menggunakanaddslashes()
. Jadi ke langkah berikutnya... -
mysql_real_escape_string()
Panggilan API C ke
mysql_real_escape_string()
berbeda dariaddslashes()
karena ia mengetahui set karakter koneksi. Sehingga dapat melakukan pelolosan dengan benar untuk set karakter yang diharapkan server. Namun, hingga saat ini, klien menganggap kami masih menggunakanlatin1
untuk koneksi, karena kami tidak pernah mengatakan sebaliknya. Kami memang memberi tahu server kami menggunakangbk
, tetapi klien masih berpikir itulatin1
.Oleh karena itu panggilan ke
mysql_real_escape_string()
menyisipkan garis miring terbalik, dan kami memiliki'
. gantung gratis karakter dalam konten "lolos" kami! Faktanya, jika kita melihat$var
digbk
set karakter, kita akan melihat:縗' OR 1=1 /*
Yang mana persis apa serangan itu membutuhkan.
-
Permintaan
Bagian ini hanya formalitas, tetapi inilah kueri yang diberikan:
SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1
Selamat, Anda baru saja berhasil menyerang sebuah program menggunakan mysql_real_escape_string()
...
Yang Buruk
Ini menjadi lebih buruk. PDO
default untuk meniru pernyataan yang disiapkan dengan MySQL. Itu berarti bahwa di sisi klien, pada dasarnya melakukan sprintf melalui mysql_real_escape_string()
(di pustaka C), yang berarti berikut ini akan menghasilkan injeksi yang berhasil:
$pdo->query('SET NAMES gbk');
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));
Sekarang, perlu diperhatikan bahwa Anda dapat mencegahnya dengan menonaktifkan pernyataan yang telah disiapkan yang ditiru:
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
Ini akan biasanya menghasilkan pernyataan yang disiapkan dengan benar (yaitu data yang dikirim dalam paket terpisah dari kueri). Namun, perlu diketahui bahwa PDO akan diam-diam fallback untuk meniru pernyataan yang tidak dapat disiapkan MySQL secara asli:pernyataan yang dapat dibuat adalah terdaftar di manual, tetapi berhati-hatilah untuk memilih versi server yang sesuai).
Si Jelek
Saya katakan di awal bahwa kita bisa mencegah semua ini jika kita menggunakan mysql_set_charset('gbk')
bukannya SET NAMES gbk
. Dan itu benar asalkan Anda menggunakan rilis MySQL sejak 2006.
Jika Anda menggunakan rilis MySQL sebelumnya, maka bug
di mysql_real_escape_string()
berarti bahwa karakter multibyte yang tidak valid seperti yang ada di payload kami diperlakukan sebagai byte tunggal untuk menghindari tujuan bahkan jika klien telah diberi tahu dengan benar tentang penyandian koneksi dan serangan ini masih akan berhasil. Bug telah diperbaiki di MySQL 4.1.20
, 5.0.22 dan 5.1.11 .
Tapi bagian terburuknya adalah PDO
tidak mengekspos C API untuk mysql_set_charset()
sampai 5.3.6, jadi di versi sebelumnya tidak bisa mencegah serangan ini untuk setiap perintah yang mungkin! Sekarang ditampilkan sebagai Parameter DSN
.
Kasih Karunia yang Menyelamatkan
Seperti yang kami katakan di awal, agar serangan ini berfungsi, koneksi database harus dikodekan menggunakan rangkaian karakter yang rentan. utf8mb4
tidak rentan namun dapat mendukung setiap Karakter unicode:jadi Anda bisa memilih untuk menggunakannya—tetapi itu hanya tersedia sejak MySQL 5.5.3. Alternatifnya adalah utf8
, yang juga tidak rentan dan dapat mendukung seluruh Unicode Pesawat Multibahasa Dasar
.
Atau, Anda dapat mengaktifkan NO_BACKSLASH_ESCAPES
Mode SQL, yang (antara lain) mengubah operasi mysql_real_escape_string()
. Dengan mengaktifkan mode ini, 0x27
akan diganti dengan 0x2727
daripada 0x5c27
dan dengan demikian proses meloloskan diri tidak bisa buat karakter yang valid di salah satu penyandian rentan yang sebelumnya tidak ada (yaitu 0xbf27
masih 0xbf27
dll.)—sehingga server akan tetap menolak string sebagai tidak valid. Namun, lihat jawaban @eggyal
untuk kerentanan berbeda yang dapat muncul dari penggunaan mode SQL ini.
Contoh Aman
Contoh berikut aman:
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");
Karena server mengharapkan 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");
Karena kami telah mengatur set karakter dengan benar sehingga klien dan server cocok.
$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 /*"));
Karena kami telah menonaktifkan pernyataan siap yang ditiru.
$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 /*"));
Karena kami telah mengatur set karakter dengan benar.
$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();
Karena MySQLi selalu melakukan pernyataan yang disiapkan dengan benar.
Menutup
Jika Anda:
- Gunakan Versi Modern MySQL (akhir 5.1, semua 5.5, 5.6, dll) DAN
mysql_set_charset()
/$mysqli->set_charset()
/ Parameter rangkaian karakter DSN PDO (dalam PHP 5.3.6)
ATAU
- Jangan gunakan rangkaian karakter yang rentan untuk penyandian koneksi (Anda hanya menggunakan
utf8
/latin1
/ascii
/ dll)
Anda 100% aman.
Jika tidak, Anda rentan meskipun Anda menggunakan mysql_real_escape_string()
...