提问人:Richard Knop 提问时间:4/21/2011 最后编辑:RahulRichard Knop 更新时间:9/30/2020 访问量:307976
绕过 mysql_real_escape_string() 的 SQL 注入
SQL injection that gets around mysql_real_escape_string()
问:
即使使用函数,也有SQL注入的可能性吗?mysql_real_escape_string()
请考虑此示例情况。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代码吗?
答:
好吧,除了通配符之外,没有什么东西可以通过它。如果您使用语句,可能会很危险,因为如果您不将其过滤掉,攻击者可能会将其作为登录名,并且必须暴力破解任何用户的密码。
人们通常建议使用准备好的语句来使其 100% 安全,因为数据不会以这种方式干扰查询本身。
但是对于这种简单的查询,执行类似操作可能会更有效%
LIKE
%
$login = preg_replace('/[^a-zA-Z0-9_]/', '', $login);
评论
more efficient
请考虑以下查询:
$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_query()
DROP TABLE
SELECT passwd FROM users
UNION
(int)mysql_real_escape_string
- 这毫无意义。它与完全没有区别。它们将为每个输入产生相同的结果(int)
mysql_real_escape_string
mysql_real_escape_integer
简短的回答是肯定的,是的,有一种方法可以绕过mysql_real_escape_string()。
#For 非常晦涩的边缘情况!!
长答案并不那么容易。它基于此处演示的攻击。
攻击
所以,让我们从展示攻击开始......
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格式进行编码,即 并具有一些最终字节为 ASCII 的字符,即 .事实证明,MySQL 5.6 默认支持 5 种这样的编码:、、和 。我们将在这里选择。
'
0x27
\
0x5c
big5
cp932
gb2312
gbk
sjis
gbk
现在,注意这里的用法非常重要。这将在 SERVER 上设置字符集。如果我们使用对 C API 函数的调用,我们会没问题(自 2006 年以来的 MySQL 版本)。但更多关于为什么在一分钟内......
SET NAMES
mysql_set_charset()
有效载荷
我们将用于此注入的有效载荷从 字节序列 .在 中,这是一个无效的多字节字符;中,它是字符串。请注意,in 和 本身就是一个文字字符。
0xbf27
gbk
latin1
¿'
latin1
gbk
0x27
'
我们之所以选择这个有效载荷,是因为如果我们调用它,我们会在字符之前插入一个 ASCII,即 。因此,我们最终会得到 ,这是一个两个字符序列:后跟 。或者换句话说,一个有效的字符后跟一个未转义的 .但我们没有使用 .依此类推,进入下一步......
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()
'
$var
gbk
縗' OR 1=1 /*
这正是攻击所需要的。
查询
这部分只是一个形式,但下面是呈现的查询:
SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1
恭喜,您刚刚使用...mysql_real_escape_string()
坏处
情况变得更糟。 默认使用 MySQL 模拟准备好的语句。这意味着在客户端,它基本上执行 sprintf through(在 C 库中),这意味着以下操作将导致成功注入:PDO
mysql_real_escape_string()
$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无法本地准备的语句:手册中列出了它可以准备的语句,但请注意选择适当的服务器版本)。
丑陋的
我一开始就说过,如果我们用 代替 .如果您使用的是自 2006 年以来的 MySQL 版本,则情况确实如此。mysql_set_charset('gbk')
SET NAMES gbk
如果您使用的是较早的MySQL版本,那么错误意味着无效的多字节字符(例如我们的有效负载中的字符)被视为单个字节以进行转义,即使客户端已正确通知连接编码,此攻击仍会成功。该错误已在 MySQL 4.1.20、5.0.22 和 5.1.11 中修复。mysql_real_escape_string()
但最糟糕的是,直到 5.3.6 才公开 C API,因此在以前的版本中,它无法阻止每个可能的命令的这种攻击!
它现在作为 DSN 参数公开。PDO
mysql_set_charset()
救赎的恩典
正如我们在一开始所说,要使这种攻击起作用,必须使用易受攻击的字符集对数据库连接进行编码。utf8mb4
不容易受到攻击,但可以支持所有 Unicode 字符:因此您可以选择使用它——但它仅在 MySQL 5.5.3 之后才可用。另一种选择是 utf8
,它也不容易受到攻击,并且可以支持整个 Unicode 基本多语言平面。
或者,您可以启用 NO_BACKSLASH_ESCAPES
SQL 模式,该模式(除其他外)会更改 的操作。启用此模式后,将替换为 而不是 ,因此转义过程无法在任何以前不存在的易受攻击的编码中创建有效字符(即 是静止的等)—因此,服务器仍将拒绝该字符串,因为该字符串无效。但是,请参阅@eggyal的答案,了解使用此 SQL 模式可能产生的其他漏洞。mysql_real_escape_string()
0x27
0x2727
0x5c27
0xbf27
0xbf27
安全示例
以下示例是安全的:
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 等)和 / / PDO 的 DSN 字符集参数(在 PHP ≥ 5.3.6 中)
mysql_set_charset()
$mysqli->set_charset()
或
- 不要使用易受攻击的字符集进行连接编码(您只使用 / / / 等)
utf8
latin1
ascii
你是100%安全的。
否则,即使你正在使用 mysql_real_escape_string()
...
评论
addslashes
char(0xBF)
?var=%BF%27+OR+1=1+%2F%2A
$var = $_GET['var'];
TL;博士
mysql_real_escape_string()
如果出现以下情况,将不提供任何保护(并可能进一步破坏您的数据):
MySQL的
NO_BACKSLASH_ESCAPES
SQL模式已启用(可能已启用,除非每次连接时都明确选择另一种SQL模式);和SQL 字符串文字使用双引号字符引用。
"
这被提交为错误 #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');
这将返回表中的所有记录。解剖:test
选择SQL模式
mysql_query('SET SQL_MODE="NO_BACKSLASH_ESCAPES"');
如字符串文本下所述:
有几种方法可以在字符串中包含引号字符:
用 “” 引出的字符串中的 “” 可以写成 “”。
'
'
''
用 “” 引出的字符串中的 “” 可以写成 “”。
"
"
""
在引号字符前面加上转义字符 (“”)。
\
用 “” 引号括起来的字符串中的 “” 不需要特殊处理,也不需要加倍或转义。同样,用 “” 引号括起来的字符串内的 “” 不需要特殊处理。
'
"
"
'
如果服务器的 SQL 模式包括
NO_BACKSLASH_ESCAPES
,则这些选项中的第三个选项(这是 SQL 采用的常用方法)不可用:必须改用前两个选项之一。请注意,第四个项目符号的作用是,人们必须知道将用于引用字面意思的字符,以避免咀嚼一个人的数据。mysql_real_escape_string()
有效载荷
" OR 1=1 --
有效载荷从字面上启动了这种注入。没有特别的编码。没有特殊字符。没有奇怪的字节。
"
mysql_real_escape_string()
$var = mysql_real_escape_string('" OR 1=1 -- ');
幸运的是,确实检查了 SQL 模式并相应地调整其行为。参见
libmysql.c
:mysql_real_escape_string()
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); }
因此,如果正在使用 SQL 模式,则会调用不同的基础函数 。如上所述,这样的函数需要知道将使用哪个字符来引用文本,以便重复它,而不会导致另一个引用字符在字面上重复。
escape_quotes_for_mysql()
NO_BACKSLASH_ESCAPES
但是,此函数任意假定字符串将使用单引号字符引用。参见
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()
也不能,因为这只是同一函数的不同包装器。
问题(如果不是很明显的话)是调用 to 无法知道将引用文本的字符,因为这留给开发人员在以后决定。因此,在模式下,这个函数实际上不可能安全地转义每个输入以用于任意引号(至少,如果没有不需要加倍的字符加倍,从而清理你的数据)。mysql_real_escape_string()
NO_BACKSLASH_ESCAPES
丑陋的
情况变得更糟。 由于有必要使用它来与标准 SQL 兼容(例如,参见 SQL-92 规范的第 5.3 节,即语法生成和缺乏对反斜杠的任何特殊含义),因此在野外可能并不少见。此外,明确建议将其用作 ircmaxell 帖子中描述的(早已修复的)错误的解决方法。谁知道呢,一些 DBA 甚至可能将其配置为默认打开,以阻止使用不正确的转义方法,如 addslashes()。
NO_BACKSLASH_ESCAPES
<quote symbol> ::= <quote><quote>
此外,新连接的 SQL 模式由服务器根据其配置(用户可以随时更改)进行设置;因此,为了确定服务器的行为,您必须始终在连接后明确指定所需的模式。SUPER
救赎的恩典
只要您始终将 SQL 模式显式设置为不包含 ,或者使用单引号字符引用 MySQL 字符串文字,这个错误就不会抬起丑陋的头:分别不会被使用,或者它关于哪些引号字符需要重复的假设将是正确的。NO_BACKSLASH_ESCAPES
escape_quotes_for_mysql()
出于这个原因,我建议任何使用的人都同时启用ANSI_QUOTES
模式,因为它会强制习惯性地使用单引号字符串文字。请注意,这并不能阻止在碰巧使用双引号文字时进行 SQL 注入,它只是降低了发生这种情况的可能性(因为正常的非恶意查询会失败)。NO_BACKSLASH_ESCAPES
在 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(),
如果启用NO_BACKSLASH_ESCAPES
,后者现在会失败并产生CR_INSECURE_API_ERR
错误。参考资料:参见 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
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 的函数不仅转义了文字,而且还引用了它(用单引号字符);请注意,为了避免在这种情况下出现 ircmaxell 的错误,您必须使用 PHP≥5.3.6 并在 DSN 中正确设置字符集。quote()
'
$stmt = $mysqli->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$param = "' OR 1=1 /*";
$stmt->bind_param('s', $param);
$stmt->execute();
...因为 MySQLi 准备好的语句是安全的。
结束语
因此,如果您:
- 使用本机预准备语句
或
- 使用 MySQL v5.7.6 或更高版本
或
除了使用 IRCMAXELL 摘要中的解决方案之一外,还要使用以下至少一种解决方案:
- PDO;
- 单引号字符串文字;或
- 显式设置的 SQL 模式,不包括
NO_BACKSLASH_ESCAPES
...那么你应该是完全安全的(撇开字符串转义范围之外的漏洞)。
评论
"
ANSI_QUOTES
quote()
评论
mysql_*
函数。它们不再维护,弃用过程已经开始。看到红框了吗?请改用预准备语句,并使用 PDO 或 MySQLi - 本文将帮助您确定哪个语句。如果你选择PDO,这里有一个很好的教程。mysql_*
E_DEPRECATED
ext/mysql