简短的回答是是的,有一种方法可以绕过mysql_real_escape_string()
。
#只适用于非常特殊的情况!!!
详细的回答并不那么简单。它基于一个在这里演示的攻击。
攻击
所以,让我们开始展示攻击...
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");
在某些情况下,这将返回超过1行。让我们分析一下这里发生了什么:
Selecting a Character Set
mysql_query('SET NAMES gbk');
For this attack to work, we need the encoding that the server's expecting on the connection both to encode '
as in ASCII i.e. 0x27
and to have some character whose final byte is an ASCII \
i.e. 0x5c
. As it turns out, there are 5 such encodings supported in MySQL 5.6 by default: big5
, cp932
, gb2312
, gbk
and sjis
. We'll select gbk
here.
Now, it's very important to note the use of SET NAMES
here. This sets the character set ON THE SERVER. If we used the call to the C API function mysql_set_charset()
, we'd be fine (on MySQL releases since 2006). But more on why in a minute...
The Payload
The payload we're going to use for this injection starts with the byte sequence 0xbf27
. In gbk
, that's an invalid multibyte character; in latin1
, it's the string ¿'
. Note that in latin1
and gbk
, 0x27
on its own is a literal '
character.
We have chosen this payload because, if we called addslashes()
on it, we'd insert an ASCII \
i.e. 0x5c
, before the '
character. So we'd wind up with 0xbf5c27
, which in gbk
is a two character sequence: 0xbf5c
followed by 0x27
. Or in other words, a valid character followed by an unescaped '
. But we're not using addslashes()
. So on to the next step...
mysql_real_escape_string()
The C API call to mysql_real_escape_string()
differs from addslashes()
in that it knows the connection character set. So it can perform the escaping properly for the character set that the server is expecting. However, up to this point, the client thinks that we're still using latin1
for the connection, because we never told it otherwise. We did tell the server we're using gbk
, but the client still thinks it's latin1
.
Therefore the call to mysql_real_escape_string()
inserts the backslash, and we have a free hanging '
character in our "escaped" content! In fact, if we were to look at $var
in the gbk
character set, we'd see:
縗' OR 1=1 /*
Which is exactly what the attack requires.
The Query
This part is just a formality, but here's the rendered query:
SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1
恭喜,您刚刚成功地使用mysql_real_escape_string()
攻击了一个程序...
问题
更糟糕的是,PDO
默认使用MySQL模拟预处理语句。这意味着在客户端上,它基本上通过C库中的mysql_real_escape_string()
进行sprintf操作,这意味着以下内容将导致成功注入:
$pdo->query('SET NAMES gbk');
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));
现在值得注意的是,您可以通过禁用模拟预处理语句来防止这种情况:
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
这通常会导致真正的预处理语句(即数据与查询分开发送)。但是,请注意PDO将默默地fallback到模拟MySQL无法本地准备的语句:可以在手册中找到它可以准备的语句listed,但请注意选择适当的服务器版本)。
丑陋的事实
一开始我说过,如果我们使用mysql_set_charset('gbk')
而不是SET NAMES gbk
,我们本可以防止所有这些问题。如果您使用的是自2006年以来的MySQL版本,则是正确的。
如果您正在使用较早版本的MySQL,则mysql_real_escape_string()
中的bug意味着无效的多字节字符(例如我们负载中的字符)将被视为单个字节进行转义,即使客户端已正确地接收到连接编码信息,因此攻击仍将成功。该漏洞已在MySQL 4.1.20, 5.0.22和5.1.11中修复。
但最糟糕的是,PDO
直到5.3.6版本才公开了mysql_set_charset()
的C API,因此在早期版本中,它无法防止每个可能的命令受到攻击!
现在它作为DSN参数暴露出来。
救世之恩
对于这种攻击方式来说,数据库连接必须使用易受攻击的字符集进行编码。utf8mb4
不易受攻击,同时可以支持每个 Unicode 字符:所以您可以选择使用它——但它只能在 MySQL 5.5.3 及以上版本中使用。另一种选择是 utf8
,它也不容易受到攻击,并且可以支持整个 Unicode 基本多文种平面。
或者,您可以启用NO_BACKSLASH_ESCAPES
SQL模式,该模式(除其他外)会更改mysql_real_escape_string()
的操作。启用此模式后,0x27
将被替换为0x2727
而不是0x5c27
,因此转义过程不能在任何原本不存在的易受攻击编码中创建有效字符(即0xbf27
仍然是0xbf27
等)- 因此服务器仍将拒绝无效字符串。但是,请参见@eggyal's answer以了解使用此SQL模式可能出现的不同漏洞。
安全示例
以下示例是安全的:
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");
由于服务器预期使用 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");
因为我们已经正确设置了字符集,使客户端和服务器匹配。
$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 /*"));
因为我们已经关闭了模拟准备语句。
$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 /*"));
因为我们已经正确设置了字符集。
$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();
因为MySQLi始终使用真正的预处理语句。
总结
如果您:
- 使用现代版本的MySQL(5.1后期,全部5.5,5.6等)并且
mysql_set_charset()
/ $mysqli->set_charset()
/ PDO的DSN字符集参数(在PHP≥5.3.6中)
或者
- 不使用易受攻击的连接编码字符集(仅使用
utf8
/ latin1
/ ascii
/等)
那么您就是100%安全的。
否则,即使您使用mysql_real_escape_string()
,您也很容易受到攻击...
mysql_*
函数。它们已不再得到维护,并且已经开始弃用过程。看到红框了吗?学习 预处理语句,并使用PDO或MySQLi,这篇文章将帮助你决定选择哪种方式。如果你选择了PDO,这是一个好的教程。 - tereškomysql_*
函数已经产生了E_DEPRECATED
警告。ext/mysql
扩展已经有超过10年没有维护了。你真的这么妄想吗? - tereško