即使使用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代码可以进行任何注入吗?
考虑以下查询:
$iId = mysql_real_escape_string("1 OR 1=1");
$sSql = "SELECT * FROM table WHERE id = $iId";
mysql_real_escape_string()
不会阻止您这样做。 您在查询中的变量周围使用单引号(''
)的事实是保护您免受此伤害的事实。以下也是一种选择:
$iId = (int)"1 OR 1=1";
$sSql = "SELECT * FROM table WHERE id = $iId";
简短的回答是是的,是的,有一种方法可以解决mysql_real_escape_string()
。
长答案并不容易。它基于此处演示的攻击。 / p>
所以,让我们开始展示攻击...
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
。
现在,在此处注意使用SETNAMES
十分重要。这将在服务器上上设置字符集。如果我们使用对C API函数mysql_set_charset()
的调用,我们会很好的(自2006年以来的MySQL版本)。但是更多关于为什么的一分钟...
有效负载
我们将用于此注入的有效负载以字节序列0xbf27
开始。在gbk
中,这是一个无效的多字节字符;在latin1
中,它是字符串¿'
。请注意,在latin1
和 gbk
中,0x27
本身就是文字'
字符。
我们选择此有效负载是因为,如果在其上调用addslashes()
,则会插入ASCII \
即0x5c
,在
'
字符之前。因此,我们将以0xbf5c27
结尾,在gbk
中它是两个字符序列:0xbf5c
后跟0x27
。换句话说,是 valid 字符,后跟未转义的'
。但是我们没有使用addslashes()
。继续下一步...
-
mysql_real_escape_string()
对mysql_real_escape_string()
的C API调用与addslashes()
不同因为它知道连接字符集。因此,它可以为服务器期望的字符集正确执行转义。但是,到目前为止,客户端认为我们仍在使用latin1
进行连接,因为我们从未告诉过它。我们确实告诉服务器我们正在使用gbk
,但是 client 仍然认为它是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的 emulation 预备语句。这意味着在客户端,它基本上是通过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')
而不是SETNAMESgbk
,我们可以避免所有这些情况。前提是您自2006年以来一直使用MySQL版本。
如果您使用的是较早的MySQL版本,请在其中输入 bug mysql_real_escape_string()
意味着无效的多字节字符(例如有效负载中的字符)出于转义目的被视为单个字节,即使已正确告知客户端连接编码,依此类推这次攻击仍然会成功。该错误已在MySQL 4.1.20 中修复。 a>, 5.0.22 和 5.1.11
但是最糟糕的是,PDO
直到5.3.6才公开用于mysql_set_charset()
的C API,因此在以前的版本中,不能< / strong>防止所有可能的命令发出这种攻击!现在,它已显示为 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 /*"));
因为我们已经关闭了模拟的准备好的语句。
$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等) AND
mysql_set_charset()
/ $mysqli->set_charset()
/ PDO的DSN字符集参数(在PHP≥5.3.6中)
OR
- 请勿使用易受攻击的字符集进行连接编码(您只能使用
utf8
/ latin1
/ ascii
/等)
您100%安全。
否则,即使您正在使用mysql_real_escape_string()
...
,您也容易受到攻击
TL; DR
mysql_real_escape_string()
在以下情况下将不提供任何保护(并且可能会进一步破坏您的数据)
为了向 @ircmaxell的出色回答致敬(实际上,这应该是奉承,而不是窃!)他的格式:
从演示开始...
mysql_query('SET SQL_MODE="NO_BACKSLASH_ESCAPES"'); // could already be set
$var = mysql_real_escape_string('" OR 1=1 -- ');
mysql_query('SELECT * FROM test WHERE name = "'.$var.'" LIMIT 1');
这将返回test
表中的所有记录。解剖:
选择SQL模式
mysql_query('SET SQL_MODE="NO_BACKSLASH_ESCAPES"');
如字符串文字:
有几种方法可以在字符串中包含引号字符:
用"
'
"引用的字符串中的"'
"可以写为""
"。用"
"
"引用的字符串中的""
"可以写为"""
"。在引号字符前加上转义字符("
\
")。用"
"
"引用的字符串中的"'
"不需要特殊处理,不需要加倍或转义。引号中带有"'
"的字符串中的"
"不需要特殊处理。
如果服务器的SQL模式包括 NO_BACKSLASH_ESCAPES
,则这些选项的第三个(这是mysql_real_escape_string()
所采用的常用方法)不可用:必须使用前两个选项之一。请注意,第四个项目符号的作用是,必须一定知道用于引用文字的字符,以免混淆自己的数据。
有效负载
" OR 1=1 --
有效负载实际上是使用"
字符启动注入的。编码,没有特殊字符,没有奇怪的字节。
mysql_real_escape_string()
$var = mysql_real_escape_string('" OR 1=1 -- ');
幸运的是,mysql_real_escape_string()
确实检查了SQL模式并对其进行了调整。行为相应。参见 libmysql.c
:
ulong STDCALL
mysql_real_escape_string(MYSQL *mysql, char *to,const char *from,
ulong length)
{
if (mysql->server_status & SERVER_STATUS_NO_BACKSLASH_ESCAPES)
return escape_quotes_for_mysql(mysql->charset, to, 0, from, length);
return escape_string_for_mysql(mysql->charset, to, 0, from, length);
}
因此,如果NO_BACKSLASH_ESCAPES
,则调用另一个基础函数escape_quotes_for_mysql()
正在使用SQL模式。如上所述,这样的函数需要知道将使用哪个字符来引用文字,以便在不引起另一个引号字符被字面重复的情况下重复该文字。
但是,该函数任意地假设使用单引号'
字符对该字符串进行引号。参见 charset.c
:
/*
Escape apostrophes by doubling them up
// [ deletia 839-845 ]
DESCRIPTION
This escapes the contents of a string by doubling up any apostrophes that
it contains. This is used when the NO_BACKSLASH_ESCAPES SQL_MODE is in
effect on the server.
// [ deletia 852-858 ]
*/
size_t escape_quotes_for_mysql(CHARSET_INFO *charset_info,
char *to, size_t to_length,
const char *from, size_t length)
{
// [ deletia 865-892 ]
if (*from == '\'')
{
if (to + 2 > to_end)
{
overflow= TRUE;
break;
}
*to++= '\'';
*to++= '\'';
}
因此,它使双引号"
字符保持不变(并将所有单引号'
个字符),而与用于引用文字的实际字符无关!在我们的例子中,$var
与提供给
mysql_real_escape_string()
-好像根本没有转义 。
-
查询
mysql_query('SELECT * FROM test WHERE name = "'.$var.'" LIMIT 1');
某种形式的查询,呈现的查询为:
SELECT * FROM test WHERE name = "" OR 1=1 -- " LIMIT 1
正如我的博学朋友所说:恭喜,您刚刚使用mysql_real_escape_string()
...
成功地攻击了一个程序 坏人
mysql_set_charset()
无济于事,因为这与字符集无关; mysqli::real_escape_string()
也不能,因为这只是围绕同一功能的不同包装器。
问题(如果不是很明显的话)是对mysql_real_escape_string()
的调用不知道,因为该文字留给了开发人员稍后再决定。因此,在NO_BACKSLASH_ESCAPES
模式下,实际上没有办法该函数可以安全地转义每个输入以供使用任意引号使用(至少,不对不需要的字符加倍)翻倍,从而破坏您的数据。
丑陋
情况变得更糟。由于必须使用NO_BACKSLASH_ESCAPES
与标准SQL兼容(例如,请参见 SQL-92规范,即
::=
语法生成,并且对反斜杠没有任何特殊含义)。此外,明确使用解决方法(已修复很久了),ircmaxell的帖子描述了 bug 。谁知道,某些DBA甚至可能会将其配置为默认情况下处于启用状态,以阻止使用不正确的转义方法,例如 addslashes()
。
此外,新连接的SQL模式< / a>由服务器根据其配置设置(SUPER
用户可以随时更改);因此,为了确定服务器的行为,必须始终在连接后明确指定所需的模式。
拯救之恩
只要您总是明确地将SQL模式设置为不包含NO_BACKSLASH_ESCAPES
,或使用单引号字符引用MySQL字符串文字,此错误就不会使其难看head:将分别不使用escape_quotes_for_mysql()
,否则其关于需要重复使用引号字符的假设将是正确的。
由于这个原因,我建议使用NO_BACKSLASH_ESCAPES
的任何人也启用 ANSI_QUOTES
模式,因为它将强制习惯使用单引号引起来的字符串文字。请注意,这不会阻止在使用双引号文字的情况下进行SQL注入—只是减少了发生这种情况的可能性(因为正常的,非恶意的查询将失败)。
在PDO中,其两个等效功能 PDO::quote()
及其准备好的语句模拟器调用 mysql_handle_quoter()
—正是这样做的:它确保转义的文字用单引号引起来,因此可以确定PDO始终不受此错误影响
自MySQL v5.7.6起,此错误已修复。参见更改日志:< / p>
已添加或更改的功能
-
不兼容的更改: :一个新的C API函数, mysql_real_escape_string_quote()
已实现,替代了 mysql_real_escape_string()
,因为后者可以当 NO_BACKSLASH_ESCAPES
启用SQL模式。在这种情况下, mysql_real_escape_string()
只能通过将引号字符加倍来对其进行转义,并且要正确执行此操作,它必须了解有关引号上下文的信息。 mysql_real_escape_string_quote()
使用一个额外的参数来指定引用上下文。有关用法的详细信息,请参见 mysql_real_escape_string_quote()< / a>。
注意
应将应用程序修改为使用 mysql_real_escape_string_quote()
,而不是 mysql_real_escape_string()
,现在失败并生成 CR_INSECURE_API_ERR
如果 NO_BACKSLASH_ESCAPES
启用。
参考文献:另请参见Bug#19211994。
安全示例
结合ircmaxell解释的错误,以下示例是完全安全的(假定一个示例正在使用晚于4.1.20、5.0.22、5.1.11的MySQL;或者一个示例未使用GBK / Big5连接编码):
mysql_set_charset($charset);
mysql_query("SET SQL_MODE=''");
$var = mysql_real_escape_string('" OR 1=1 /*');
mysql_query('SELECT * FROM test WHERE name = "'.$var.'" LIMIT 1');
...因为我们已明确选择不包含NO_BACKSLASH_ESCAPES
的SQL模式。
mysql_set_charset($charset);
$var = mysql_real_escape_string("' OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");
...因为我们用单引号引起来了字符串文字。
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(["' OR 1=1 /*"]);
...因为PDO预备语句不受此漏洞的影响(并且ircmaxell也是如此),前提是您使用的是PHP≥5.3.6,并且在DSN中已正确设置了字符集;或者预备语句模拟已被禁用)。
$var = $pdo->quote("' OR 1=1 /*");
$stmt = $pdo->query("SELECT * FROM test WHERE name = $var LIMIT 1");
...因为PDO的quote()
函数不仅转义了文字,而且还对其进行了引号(用单引号'
字符);请注意,为避免在这种情况下ircmaxell的错误,您必须使用PHP≥5.3.6,并且已在DSN中正确设置了字符集。
$stmt = $mysqli->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$param = "' OR 1=1 /*";
$stmt->bind_param('s', $param);
$stmt->execute();
...因为MySQLi准备好的语句是安全的。
总结
因此,如果您:
- 使用本地准备的语句
OR
- 使用MySQL v5.7.6或更高版本
OR
要使用ircmaxell摘要中的一种解决方案,请
-
中的至少一种,
- PDO;
- 单引号字符串文字;或
- 不包含
NO_BACKSLASH_ESCAPES
的显式设置的SQL模式
...那么您应该是完全安全的(漏洞不在字符串转义范围之内)。
好吧,除了%
通配符之外,没有什么可以通过的。如果您使用LIKE
语句,可能会很危险,因为如果您不过滤掉它,攻击者可能只将%
用作登录名,而只需要暴力破解密码您的任何用户。人们经常建议使用准备好的语句以使其100%安全,因为数据不会以这种方式干扰查询本身。但是对于这样简单的查询,执行类似$login=preg_replace('/[^a-zA-Z0-9_]/','',$login);
< / p>