Câu trả lời ngắn gọn là có, có một cách để đi lạimysql_real_escape_string()
.
Đối với các TRƯỜNG HỢP EDGE Rất HẤP DẪN !!!
Câu trả lời dài không quá dễ. Nó dựa trên một cuộc tấn công được chứng minh ở đây .
Cuộc tấn công
Vì vậy, hãy bắt đầu bằng cách hiển thị cuộc tấn công ...
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");
Trong một số trường hợp nhất định, điều đó sẽ trả về hơn 1 hàng. Hãy phân tích những gì đang diễn ra ở đây:
Chọn một bộ ký tự
mysql_query('SET NAMES gbk');
Để cuộc tấn công này hoạt động, chúng ta cần mã hóa mà máy chủ mong đợi trên cả kết nối để mã hóa '
như trong ASCII tức là 0x27
và để có một số ký tự có byte cuối cùng là ASCII \
tức là 0x5c
. Khi nó quay ra, có 5 mã hóa như hỗ trợ trong MySQL 5.6 theo mặc định: big5
, cp932
, gb2312
, gbk
và sjis
. Chúng tôi sẽ chọn gbk
ở đây.
Bây giờ, rất quan trọng để lưu ý việc sử dụng SET NAMES
ở đây. Cái này đặt bộ ký tự TRÊN MÁY CHỦ . Nếu chúng tôi sử dụng lệnh gọi hàm C API mysql_set_charset()
, chúng tôi sẽ ổn (trên các bản phát hành MySQL từ năm 2006). Nhưng nhiều hơn về lý do tại sao trong một phút ...
Tải trọng
Tải trọng mà chúng ta sẽ sử dụng cho phép tiêm này bắt đầu bằng chuỗi byte 0xbf27
. Trong gbk
đó, đó là một nhân vật đa bào không hợp lệ; trong latin1
, đó là chuỗi ¿'
. Lưu ý rằng trong latin1
và gbk
, 0x27
trên chính nó là một '
nhân vật theo nghĩa đen .
Chúng tôi đã chọn tải trọng này bởi vì, nếu chúng tôi gọi addslashes()
nó, chúng tôi sẽ chèn ASCII \
tức là 0x5c
trước '
ký tự. Vì vậy, chúng tôi sẽ kết thúc 0xbf5c27
, trong đó gbk
là một chuỗi hai ký tự: 0xbf5c
theo sau 0x27
. Hay nói cách khác, một nhân vật hợp lệ theo sau là một người không được giải thoát '
. Nhưng chúng tôi không sử dụng addslashes()
. Vì vậy, bước tiếp theo ...
mysql_real_escape_opes ()
Lệnh gọi API C mysql_real_escape_string()
khác với addslashes()
ở chỗ nó biết bộ ký tự kết nối. Vì vậy, nó có thể thực hiện thoát đúng cho bộ ký tự mà máy chủ đang mong đợi. Tuy nhiên, cho đến thời điểm này, khách hàng nghĩ rằng chúng tôi vẫn đang sử dụng latin1
cho kết nối, bởi vì chúng tôi không bao giờ nói với nó khác. Chúng tôi đã nói với máy chủ chúng tôi đang sử dụng gbk
, nhưng khách hàng vẫn nghĩ rằng nó latin1
.
Do đó, lệnh gọi mysql_real_escape_string()
chèn dấu gạch chéo ngược và chúng tôi có một '
ký tự treo miễn phí trong nội dung "thoát" của chúng tôi! Thực tế, nếu chúng ta nhìn vào $var
bộ gbk
ký tự, chúng ta sẽ thấy:
縗 'HOẶC 1 = 1 / *
Đó chính xác là những gì cuộc tấn công yêu cầu.
Truy vấn
Phần này chỉ là một hình thức, nhưng đây là truy vấn được kết xuất:
SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1
Xin chúc mừng, bạn vừa tấn công thành công một chương trình bằng mysql_real_escape_string()
...
Những người xấu
Nó trở nên tồi tệ hơn. PDO
mặc định để mô phỏng các câu lệnh đã chuẩn bị với MySQL. Điều đó có nghĩa là về phía khách hàng, về cơ bản, nó thực hiện chạy nước rút mysql_real_escape_string()
(trong thư viện C), điều đó có nghĩa là sau đây sẽ dẫn đến việc tiêm thành công:
$pdo->query('SET NAMES gbk');
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));
Bây giờ, đáng chú ý là bạn có thể ngăn chặn điều này bằng cách vô hiệu hóa các câu lệnh được chuẩn bị mô phỏng:
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
Điều này thường sẽ dẫn đến một tuyên bố được chuẩn bị thực sự (tức là dữ liệu được gửi trong một gói riêng biệt từ truy vấn). Tuy nhiên, lưu ý rằng PDO sẽ âm thầm dự phòng các giả lập mà MySQL không thể chuẩn bị một cách tự nhiên: những thứ mà nó có thể được liệt kê trong hướng dẫn, nhưng hãy cẩn thận để chọn phiên bản máy chủ phù hợp).
Xấu xí
Tôi đã nói ngay từ đầu rằng chúng ta có thể đã ngăn chặn tất cả những điều này nếu chúng ta đã sử dụng mysql_set_charset('gbk')
thay vì SET NAMES gbk
. Và điều đó đúng với điều kiện bạn đang sử dụng bản phát hành MySQL từ năm 2006.
Nếu bạn đang sử dụng một phiên bản MySQL trước, sau đó là một lỗi trong mysql_real_escape_string()
có nghĩa là ký tự nhiều byte không hợp lệ như những người trong Payload của chúng tôi bị đối xử như byte duy nhất cho mục đích thoát ngay cả khi khách hàng đã được thông báo một cách chính xác của mã hóa kết nối và do đó cuộc tấn công này sẽ vẫn thành công Lỗi này đã được cố định trong MySQL 4.1.20 , 5.0.22 và 5.1.11 .
Nhưng điều tồi tệ nhất là PDO
đã không để lộ API C cho mysql_set_charset()
đến ngày 5.3.6, vì vậy trong các phiên bản trước, nó không thể ngăn chặn cuộc tấn công này cho mọi lệnh có thể! Bây giờ nó được hiển thị dưới dạng tham số DSN .
Ân điển cứu rỗi
Như chúng ta đã nói ngay từ đầu, để cuộc tấn công này hoạt động, kết nối cơ sở dữ liệu phải được mã hóa bằng bộ ký tự dễ bị tổn thương. utf8mb4
là không dễ bị tổn thương và chưa thể hỗ trợ tất cả các ký tự Unicode: vì vậy bạn có thể chọn để sử dụng mà thay vào đó, nhưng nó đã chỉ được đưa ra từ MySQL 5.5.3. Một cách khác là utf8
, nó cũng không dễ bị tổn thương và có thể hỗ trợ toàn bộ Mặt phẳng đa ngôn ngữ Unicode Basic .
Ngoài ra, bạn có thể kích hoạt NO_BACKSLASH_ESCAPES
chế độ SQL, trong đó (trong số những thứ khác) làm thay đổi hoạt động của mysql_real_escape_string()
. Khi bật chế độ này, 0x27
sẽ được thay thế bằng 0x2727
thay vì 0x5c27
và do đó, quá trình thoát không thể tạo các ký tự hợp lệ trong bất kỳ mã hóa dễ bị tổn thương nào mà chúng không tồn tại trước đó (nghĩa 0xbf27
là vẫn 0xbf27
v.v.) - vì vậy máy chủ vẫn sẽ từ chối chuỗi là không hợp lệ . Tuy nhiên, hãy xem câu trả lời của @ eggyal để biết một lỗ hổng khác có thể phát sinh từ việc sử dụng chế độ SQL này.
Ví dụ an toàn
Các ví dụ sau đây là an toàn:
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");
Bởi vì máy chủ đang mong đợi 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");
Bởi vì chúng tôi đã đặt đúng bộ ký tự sao cho máy khách và máy chủ khớp.
$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 /*"));
Bởi vì chúng tôi đã tắt các tuyên bố chuẩn bị mô phỏng.
$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 /*"));
Bởi vì chúng tôi đã thiết lập đúng ký tự.
$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();
Bởi vì MySQLi luôn luôn chuẩn bị các tuyên bố chuẩn bị.
Kết thúc
Nếu bạn:
- Sử dụng các phiên bản hiện đại của MySQL (cuối 5.1, tất cả 5.5, 5.6, v.v.) Tham số bộ ký tự DSN của AND
mysql_set_charset()
/ $mysqli->set_charset()
/ PDO (trong PHP ≥ 5.3.6)
HOẶC LÀ
- Không sử dụng bộ ký tự dễ bị tổn thương để mã hóa kết nối (bạn chỉ sử dụng
utf8
/ latin1
/ ascii
/ etc)
Bạn an toàn 100%.
Mặt khác, bạn dễ bị tổn thương mặc dù bạn đang sử dụngmysql_real_escape_string()
...