简短的答案是,是的,有一种解决方法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行以上。让我们剖析这里发生的事情:
选择字符集
mysql_query('SET NAMES gbk');
为了使这种攻击起作用,我们需要服务器在连接上期望的编码既要'
以ASCII码编码0x27
,也要具有某些字符的最终字节为ASCII码,\
即0x5c
。事实证明,会默认在MySQL 5.6支持5个这样的编码:big5
,cp932
,gb2312
,gbk
和sjis
。我们将gbk
在此处选择。
现在,注意SET NAMES
这里的用法非常重要。这将在服务器上设置字符集。如果我们使用对C API函数的调用mysql_set_charset()
,那会很好的(自2006年以来的MySQL版本)。但是更多关于为什么在一分钟...
有效载荷
我们将用于此注入的有效负载从字节序列开始0xbf27
。在中gbk
,这是一个无效的多字节字符;在latin1
,这是字符串¿'
。请注意,在latin1
和 gbk
,0x27
对自己是一个文字'
字符。
我们选择此有效负载是因为,如果调用addslashes()
它,我们会在字符之前插入一个ASCII码,\
即。因此,我们将以结束,其中有两个字符序列:后跟。换句话说,就是一个有效字符,后跟一个未转义的。但是我们没有使用。继续下一步...0x5c
'
0xbf5c27
gbk
0xbf5c
0x27
'
addslashes()
mysql_real_escape_string()
对C API的调用的mysql_real_escape_string()
不同之处addslashes()
在于它知道连接字符集。因此,它可以为服务器期望的字符集正确执行转义。但是,到目前为止,客户端认为我们仍在使用latin1
该连接,因为我们从未告诉过它。我们确实告诉我们正在使用的服务器gbk
,但是客户端仍然认为是latin1
。
因此,调用会mysql_real_escape_string()
插入反斜杠,并且我们'
的“转义”内容中有一个自由悬挂的字符!事实上,如果我们看一下$var
在gbk
字符集,我们会看到:
OR'OR 1 = 1 / *
这正是攻击所需要的。
查询
这只是一个形式,但这是呈现的查询:
SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1
恭喜,您刚刚成功使用mysql_real_escape_string()
... 攻击了一个程序
坏人
情况变得更糟。PDO
默认情况下使用MySQL 模拟准备好的语句。这意味着在客户端,它基本上通过mysql_real_escape_string()
(在C库中)执行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会悄悄地退回到仿真陈述,MySQL不能原生准备:那些可被列在手册中,但要注意选择合适的服务器版本)。
丑陋的
我在一开始就说过,如果我们使用mysql_set_charset('gbk')
代替,我们可以避免所有这些情况SET NAMES gbk
。前提是您自2006年以来一直在使用MySQL版本。
如果您使用的是较早的MySQL版本,则存在一个错误,mysql_real_escape_string()
即出于逃避目的,无效的多字节字符(例如我们的有效负载中的字符)被视为单个字节,即使已正确告知客户端连接编码,因此该攻击也可能仍然成功。该错误是固定在MySQL 4.1.20,5.0.22和5.1.11。
但是最糟糕的是,直到5.3.6 PDO
才公开C API mysql_set_charset()
,因此在以前的版本中,它无法防止所有可能的命令遭受这种攻击!现在,它作为DSN参数公开。
拯救恩典
正如我们在一开始所说的那样,要使这种攻击起作用,必须使用易受攻击的字符集对数据库连接进行编码。 utf8mb4
是不容易,但可以支持所有的 Unicode字符:所以你可以选择使用的是代替,但它只是可利用从MySQL 5.5.3。另一种选择是utf8
,它也不易受到攻击,并且可以支持整个Unicode Basic Multilingual Plane。
或者,您可以启用NO_BACKSLASH_ESCAPES
SQL模式,该模式(除其他外)会更改的操作mysql_real_escape_string()
。启用该模式,0x27
将被替换0x2727
,而不是0x5c27
从而逃逸过程不能在任何地方,他们以前不存在的脆弱编码的创建有效的字符(即0xbf27
尚0xbf27
等) -这样的服务器仍然会拒绝的字符串为无效。但是,有关使用此SQL模式可能引起的其他漏洞,请参阅@eggyal的答案。
安全的例子
以下示例是安全的:
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 charset参数(在PHP 5.3.6≥)
要么
- 不要使用易受攻击的字符集进行连接编码(您只能使用
utf8
/ latin1
/ ascii
/等)
您是100%安全的。
否则,即使您正在使用mysql_real_escape_string()
... ,也很容易受到攻击。