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");
在某些情况下,这将返回 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()
,则会在'
字符\
即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 模拟准备好的语句。这意味着在客户端,它基本上通过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 。
但是最糟糕的是PDO
mysql_set_charset()
的 C API,因此在以前的版本中,它无法针对所有可能的命令阻止这种攻击!现在,它作为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_set_charset()
/ $mysqli->set_charset()
/ PDO 的 DSN 字符集参数(在 PHP≥5.3.6 中)或者
utf8
/ latin1
/ ascii
等)您是 100%安全的。
否则,即使您正在使用mysql_real_escape_string()
,您也很容易受到攻击。
考虑以下查询:
$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";
TL; DR
mysql_real_escape_string()
将不提供任何保护(并且可能会进一步破坏您的数据):
MySQL 的
NO_BACKSLASH_ESCAPES
SQL 模式已启用(除非每次连接时明确选择另一个 SQL 模式,否则可能会启用);和您的 SQL 字符串文字使用双引号
"
字符引用。这被作为Bug#72458提交,并已在 MySQL v5.7.6 中修复(请参见下面标题为 “ The Saving Grace ” 的部分)。
为了向@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');
This will return all records from the test
table. A dissection:
Selecting an SQL Mode
mysql_query('SET SQL_MODE="NO_BACKSLASH_ESCAPES"');
As documented under String Literals:
There are several ways to include quote characters within a string:
A “
'
” inside a string quoted with “'
” may be written as “''
”.A “
"
” inside a string quoted with “"
” may be written as “""
”.Precede the quote character by an escape character (“
\
”).A “
'
” inside a string quoted with “"
” needs no special treatment and need not be doubled or escaped. In the same way, “"
” inside a string quoted with “'
” needs no special treatment.
If the server's SQL mode includes NO_BACKSLASH_ESCAPES
, then the third of these options—which is the usual approach adopted by mysql_real_escape_string()
—is not available: one of the first two options must be used instead. Note that the effect of the fourth bullet is that one must necessarily know the character that will be used to quote the literal in order to avoid munging one's data.
The Payload
" OR 1=1 --
The payload initiates this injection quite literally with the "
character. No particular encoding. No special characters. No weird bytes.
mysql_real_escape_string()
$var = mysql_real_escape_string('" OR 1=1 -- ');
Fortunately, mysql_real_escape_string()
does check the SQL mode and adjust its behaviour accordingly. See 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);
}
Thus a different underlying function, escape_quotes_for_mysql()
, is invoked if the NO_BACKSLASH_ESCAPES
SQL mode is in use. As mentioned above, such a function needs to know which character will be used to quote the literal in order to repeat it without causing the other quotation character from being repeated literally.
However, this function arbitrarily assumes that the string will be quoted using the single-quote '
character. See 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++= '\'';
}
So, it leaves double-quote "
characters untouched (and doubles all single-quote '
characters) irrespective of the actual character that is used to quote the literal! In our case $var
remains exactly the same as the argument that was provided to mysql_real_escape_string()
—it's as though no escaping has taken place at all.
The Query
mysql_query('SELECT * FROM test WHERE name = "'.$var.'" LIMIT 1');
Something of a formality, the rendered query is:
SELECT * FROM test WHERE name = "" OR 1=1 -- " LIMIT 1
As my learned friend put it: congratulations, you just successfully attacked a program using mysql_real_escape_string()
...
mysql_set_charset()
cannot help, as this has nothing to do with character sets; nor can mysqli::real_escape_string()
, since that's just a different wrapper around this same function.
The problem, if not already obvious, is that the call to mysql_real_escape_string()
cannot know with which character the literal will be quoted, as that's left to the developer to decide at a later time. So, in NO_BACKSLASH_ESCAPES
mode, there is literally no way that this function can safely escape every input for use with arbitrary quoting (at least, not without doubling characters that do not require doubling and thus munging your data).
情况变得更糟。 NO_BACKSLASH_ESCAPES
与标准 SQL 兼容(例如,请参见SQL-92 规范的5.3 节,即<quote symbol> ::= <quote><quote>
语法)的生产,并且对反斜杠没有任何特殊含义)。此外,明确建议使用它作为 ircmaxell 帖子所描述的(早已修复)错误的解决方法。谁知道,一些 DBA 甚至可能会将其配置为默认情况下处于启用状态,以阻止使用诸如addslashes()
类的不正确的转义方法。
另外,服务器根据其配置来设置新连接SUPER
用户可以随时更改该模式)。因此,为了确定服务器的行为,必须在连接后始终明确指定所需的模式。
只要您始终明确地将 SQL 模式设置为不包含NO_BACKSLASH_ESCAPES
或使用单引号字符来引用 MySQL 字符串文字,此错误就不会变得丑陋:分别escape_quotes_for_mysql()
或假定使用哪个引号需要重复的字符将是正确的。
因此,我建议使用NO_BACKSLASH_ESCAPES
任何人也启用ANSI_QUOTES
模式,因为它将强制习惯性使用单引号字符串文字。请注意,这不会阻止在使用双引号字面量的情况下进行 SQL 注入—只是减少了发生这种情况的可能性(因为正常的,非恶意的查询将失败)。
在 PDO 中,它的等效函数PDO::quote()
及其准备好的语句模拟器都调用mysql_handle_quoter()
-正是这样做的:它确保转义的文字用单引号引起来,因此可以确定 PDO 是始终不受此错误影响。
从 MySQL v5.7.6 开始,此错误已修复。请参阅更改日志:
添加或更改功能
不兼容的更改:已实现一个新的 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() 。笔记
应修改应用程序使用
mysql_real_escape_string_quote()
而不是mysql_real_escape_string()
它现在将失败并产生一个CR_INSECURE_API_ERR
如果错误NO_BACKSLASH_ESCAPES
被启用。参考:另请参见 Bug#19211994。
结合 ircmaxell 解释的错误,以下示例是完全安全的(假设一个示例使用的 MySQL 版本晚于 4.1.20、5.0.22、5.1.11;或者一个示例未使用 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 预备语句不受此漏洞的影响(如果您使用的是 PHP≥5.3.6,并且已在 DSN 中正确设置字符集;或者已禁用预备语句仿真,则 ircmaxell 也是如此) 。
$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 准备好的语句是安全的。
因此,如果您:
或者
或者
除了采用 ircmaxell 的摘要中的一种解决方案外,还应至少使用以下一种:
NO_BACKSLASH_ESCAPES
的显式设置的 SQL 模式... 那么您应该是完全安全的(漏洞不在字符串转义范围之内)。