绕过mysql_real_escape_string()的SQL注入攻击
绕过mysql_real_escape_string()的SQL注入攻击
即使使用了mysql_real_escape_string()
函数,是否仍存在SQL注入可能性?
考虑这个样本情况,SQL在PHP中是这样构建的:
$login = mysql_real_escape_string(GetFromPost('login')); $password = mysql_real_escape_string(GetFromPost('password')); $sql = "SELECT * FROM table WHERE login='$login' AND password='$password'";
我听到很多人告诉我,即使使用mysql_real_escape_string()
函数,像这样的代码仍然是危险的,有可能被黑客攻击。但是我无法想到任何可能的漏洞?
像这样的经典注入:
aaa' OR 1=1 --
不能生效。
你知道任何可能通过上面的PHP代码进行注入的漏洞吗?
简短的回答是是的,确实有一种方法可以绕过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");
在某些情况下,会返回多个结果。让我们分析一下这里发生了什么:
-
选择字符集
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()
插入了反斜杠,我们在“已转义”的内容中有一个自由悬挂的'
字符!实际上,如果我们在gbk
字符集中查看$var
,我们会看到:縗' OR 1=1 /*
这正是攻击所需要的。
-
查询
这只是一种形式,但以下是渲染后的查询:
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 会默默地 回退 到 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 中得到修复。
但最糟糕的是,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 的答案以了解使用此 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 /*"));
因为我们关闭了模拟的准备语句(prepared statements)。
$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()
/ PHP ≥ 5.3.6中的PDO的DSN字符集参数
或者
- 连接编码不使用易受攻击的字符集(只使用
utf8
/latin1
/ascii
/ 等)
那么您可以100%安全。
否则,即使使用了mysql_real_escape_string()
,您也容易受到攻击...