绕过 mysql_real_escape_string() 的 SQL 注入

SQL injection that gets around mysql_real_escape_string()

提问人:Richard Knop 提问时间:4/21/2011 最后编辑:RahulRichard Knop 更新时间:9/30/2020 访问量:307944

问:

即使使用函数,也有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代码吗?

php mysql sql 安全性 sql 注入

评论

42赞 Mark Baker 4/21/2011
@ThiefMaster - 我不想给出冗长的错误,例如无效用户/无效密码...它告诉蛮力商家他们有一个有效的用户ID,而这只是他们需要猜测的密码
24赞 ThiefMaster 4/21/2011
不过,从可用性的角度来看,这很可怕。有时您无法使用您的主要昵称/用户名/电子邮件地址,并在一段时间后忘记这一点,或者网站删除了您的帐户以使其不活动。然后,如果您继续尝试密码,甚至可能阻止您的IP,即使只是您的用户名无效,这也会非常烦人。
66赞 tereško 12/4/2012
请不要在新代码中使用 mysql_* 函数。它们不再维护,弃用过程已经开始。看到红框了吗?请改用预准备语句,并使用 PDOMySQLi - 本文将帮助您确定哪个语句。如果你选择PDO,这里有一个很好的教程
18赞 tereško 7/5/2013
@machineaddict,从 5.5(最近发布)开始,这些函数已经产生了警告。延期已经超过10年没有维持了。你真的有那么妄想吗?mysql_*E_DEPRECATEDext/mysql
20赞 GGG 3/15/2017
@machineaddict 他们刚刚在 PHP 7.0 上删除了该扩展,现在还不是 2050 年。

答:

22赞 Slava 4/21/2011 #1

好吧,除了通配符之外,没有什么东西可以通过它。如果您使用语句,可能会很危险,因为如果您不将其过滤掉,攻击者可能会将其作为登录名,并且必须暴力破解任何用户的密码。 人们通常建议使用准备好的语句来使其 100% 安全,因为数据不会以这种方式干扰查询本身。 但是对于这种简单的查询,执行类似操作可能会更有效%LIKE%$login = preg_replace('/[^a-zA-Z0-9_]/', '', $login);

评论

2赞 Dor 4/21/2011
+1,但通配符是 LIKE 子句,而不是简单的相等。
8赞 MatBailie 4/21/2011
您认为比使用准备好的语句更简单的替代标准是什么?(准备好的语句始终有效,在发生攻击时可以快速更正库,不会暴露人为错误[例如错误键入完整的替换字符串],并且如果重用语句,则具有显着的性能优势。more efficient
8赞 cHao 1/27/2013
@Slava:您实际上将用户名和密码限制为仅字字符。大多数对安全性有所了解的人会认为这是一个坏主意,因为它大大缩小了搜索空间。当然,他们也会认为在数据库中存储明文密码是个坏主意,但我们不需要使问题复杂化。:)
2赞 Slava 1/28/2013
@cHao,我的建议只涉及登录。显然,您不需要过滤密码,对不起,我的回答中没有明确说明。但实际上这可能是个好主意。使用“石头无知的树空间”,而不是难以记住和输入的“a4üua3!@v\”ä90;8f“将更难暴力破解。即使使用字典,比如说 3000 个单词来帮助你,知道你只使用了 4 个单词——这仍然是大约 3.3*10^12 个组合。:)
2赞 cHao 1/28/2013
@Slava:我以前见过这个想法;请参见 xkcd.com/936 。问题是,数学并不能完全证明这一点。您的示例 17 字符密码将有 96^17 种可能性,前提是您忘记了变音符号并将自己限制为可打印的 ASCII。这大约是 4.5x10^33。我们谈论的是十亿万亿倍的蛮力工作。即使是 8 个字符的 ASCII 密码也有 7.2x10^15 种可能性——3000 倍以上。
421赞 Wesley van Opdorp 4/21/2011 #2

请考虑以下查询:

$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";

评论

10赞 Pekka 10/8/2011
但这不是一个真正的问题,因为不会执行多个语句,不是吗?mysql_query()
13赞 Jacco 5/21/2012
@Pekka,虽然通常的例子是,在实践中攻击者更有可能。在后一种情况下,第二个查询通常通过使用子句来执行。DROP TABLESELECT passwd FROM usersUNION
62赞 zerkms 7/25/2012
(int)mysql_real_escape_string- 这毫无意义。它与完全没有区别。它们将为每个输入产生相同的结果(int)
30赞 NullUserException 10/10/2012
这更像是对函数的误用。毕竟,它是命名的,而不是.这并不意味着要与整数字段一起使用。mysql_real_escape_stringmysql_real_escape_integer
12赞 Pacerier 4/11/2015
@ircmaxell,然而答案完全具有误导性。显然,问题是询问引号中的内容。“引号不存在”不是这个问题的答案。
746赞 ircmaxell 8/25/2012 #3

简短的回答是肯定的,是的,有一种方法可以绕过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 行以上。让我们剖析一下这里发生了什么:

  1. 选择字符集

    mysql_query('SET NAMES gbk');
    

    为了使这种攻击起作用,我们需要服务器在连接上期望的编码,以ASCII格式进行编码,即 具有一些最终字节为 ASCII 的字符,即 .事实证明,MySQL 5.6 默认支持 5 种这样的编码:、、和 。我们将在这里选择。'0x27\0x5cbig5cp932gb2312gbksjisgbk

    现在,注意这里的用法非常重要。这将在 SERVER 上设置字符集。如果我们使用对 C API 函数的调用,我们会没问题(自 2006 年以来的 MySQL 版本)。但更多关于为什么在一分钟内......SET NAMESmysql_set_charset()

  2. 有效载荷

    我们将用于此注入的有效载荷从 字节序列 .在 中,这是一个无效的多字节字符;中,它是字符串。请注意,in 本身就是一个文字字符。0xbf27gbklatin1¿'latin1gbk0x27'

    我们之所以选择这个有效载荷,是因为如果我们调用它,我们会在字符之前插入一个 ASCII,即 。因此,我们最终会得到 ,这是一个两个字符序列:后跟 。或者换句话说,一个有效的字符后跟一个未转义的 .但我们没有使用 .依此类推,进入下一步......addslashes()\0x5c'0xbf5c27gbk0xbf5c0x27'addslashes()

  3. mysql_real_escape_string()

    对 C API 的调用不同之处在于它知道连接字符集。因此,它可以对服务器期望的字符集正确执行转义。但是,到目前为止,客户端认为我们仍在使用连接,因为我们从未告诉过它。我们确实告诉了我们正在使用的服务器,但客户端仍然认为它是 。mysql_real_escape_string()addslashes()latin1gbklatin1

    因此,插入反斜杠的调用,我们在“转义”内容中有一个自由悬挂的字符!事实上,如果我们看一下字符集,我们会看到:mysql_real_escape_string()'$vargbk

    縗' OR 1=1 /*

    这正是攻击所需要的。

  4. 查询

    这部分只是一个形式,但下面是呈现的查询:

    SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1
    

恭喜,您刚刚使用...mysql_real_escape_string()

坏处

情况变得更糟。 默认使用 MySQL 模拟准备好的语句。这意味着在客户端,它基本上执行 sprintf through(在 C 库中),这意味着以下操作将导致成功注入:PDOmysql_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 参数公开。PDOmysql_set_charset()

救赎的恩典

正如我们在一开始所说,要使这种攻击起作用,必须使用易受攻击的字符集对数据库连接进行编码。utf8mb4 不容易受到攻击,但可以支持所有 Unicode 字符:因此您可以选择使用它——但它仅在 MySQL 5.5.3 之后才可用。另一种选择是 utf8,它也不容易受到攻击,并且可以支持整个 Unicode 基本多语言平面

或者,您可以启用 NO_BACKSLASH_ESCAPES SQL 模式,该模式(除其他外)会更改 的操作。启用此模式后,将替换为 而不是 ,因此转义过程无法在任何以前不存在的易受攻击的编码中创建有效字符(即 是静止的等)—因此,服务器仍将拒绝该字符串,因为该字符串无效。但是,请参阅@eggyal的答案,了解使用此 SQL 模式可能产生的其他漏洞。mysql_real_escape_string()0x270x27270x5c270xbf270xbf27

安全示例

以下示例是安全的:

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()

  • 不要使用易受攻击的字符集进行连接编码(您只使用 / / / 等)utf8latin1ascii

你是100%安全的。

否则,即使你正在使用 mysql_real_escape_string()...

评论

4赞 netcoder 8/25/2012
PDO模拟MySQL的准备语句,真的吗?我看不出它这样做的任何理由,因为驱动程序本身支持它。不?
18赞 Theodore R. Smith 8/26/2012
确实如此。他们在文档中说它没有。但在源代码中,它清晰可见且易于修复。我将其归咎于开发人员的无能。
7赞 ircmaxell 8/26/2012
@TheodoreR.Smith:这并不容易解决。我一直在努力更改默认值,但在切换时它无法进行大量测试。所以这是一个比看起来更大的变化。我仍然希望在 5.5 之前完成它......
15赞 ircmaxell 11/21/2012
@shadyyx:不,文章描述的漏洞是关于的。我基于这个漏洞。自己试试吧。获取MySQL 5.0,然后运行此漏洞并亲自查看。至于如何将其放入 PUT/GET/POST,这是微不足道的。输入数据只是字节流。 只是生成字节的一种可读方式。我已经在多个会议面前现场演示了这个漏洞。相信我......但如果你不这样做,请自己尝试一下。它有效...addslasheschar(0xBF)
5赞 cHao 12/27/2012
@shadyyx:至于以 _GET 美元的价格传递这种时髦...... 在 URL 中,在代码中,Bob 是你的叔叔。?var=%BF%27+OR+1=1+%2F%2A$var = $_GET['var'];
208赞 eggyal 4/25/2014 #4

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

  1. 选择SQL模式

    mysql_query('SET SQL_MODE="NO_BACKSLASH_ESCAPES"');
    

    字符串文本下所述:

    有几种方法可以在字符串中包含引号字符:

    • 用 “” 引出的字符串中的 “” 可以写成 “”。''''

    • 用 “” 引出的字符串中的 “” 可以写成 “”。""""

    • 在引号字符前面加上转义字符 (“”)。\

    • 用 “” 引号括起来的字符串中的 “” 不需要特殊处理,也不需要加倍或转义。同样,用 “” 引号括起来的字符串内的 “” 不需要特殊处理。'""'

    如果服务器的 SQL 模式包括 NO_BACKSLASH_ESCAPES,则这些选项中的第三个选项(这是 SQL 采用的常用方法)不可用:必须改用前两个选项之一。请注意,第四个项目符号的作用是,人们必须知道将用于引用字面意思的字符,以避免咀嚼一个人的数据。mysql_real_escape_string()

  2. 有效载荷

    " OR 1=1 -- 
    

    有效载荷从字面上启动了这种注入。没有特别的编码。没有特殊字符。没有奇怪的字节。"

  3. mysql_real_escape_string()

    $var = mysql_real_escape_string('" OR 1=1 -- ');
    

    幸运的是,确实检查了 SQL 模式并相应地调整其行为。参见 libmysql.cmysql_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++= '\'';
        }
    

    因此,无论用于引用文字的实际字符如何,它都不会触及双引号字符(并使所有单引号字符加倍)!在我们的案例中,与提供给的论点完全相同——就好像根本没有发生任何逃避一样。"'$varmysql_real_escape_string()

  4. 查询

    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_ESCAPESescape_quotes_for_mysql()

出于这个原因,我建议任何使用的人都同时启用ANSI_QUOTES模式,因为它会强制习惯性地使用单引号字符串文字。请注意,这并不能阻止在碰巧使用双引号文字时进行 SQL 注入,它只是降低了发生这种情况的可能性(因为正常的非恶意查询会失败)。NO_BACKSLASH_ESCAPES

在 PDO 中,它的等效函数 PDO::quote() 和它准备好的语句仿真器都调用 mysql_handle_quoter()——这正是这样做的:它确保转义的文字用单引号引用,因此您可以确定 PDO 始终不受此错误的影响。

从 MySQL v5.7.6 开始,此错误已修复。查看更改日志

添加或更改的功能

安全示例

结合 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

...那么你应该是完全安全的(撇开字符串转义范围之外的漏洞)。

评论

11赞 Your Common Sense 4/25/2014
所以,TL;DR 就像“有一个NO_BACKSLASH_ESCAPES mysql 服务器模式,如果您不使用单引号,可能会导致注入。
1赞 cHao 8/28/2014
人们不应该首先使用字符串。SQL说这是针对标识符的。但是呃......这是MySQL说“搞砸标准,我会做任何我想做的事”的另一个例子。(幸运的是,您可以在模式中包含以修复引用中断。然而,公然无视标准是一个更大的问题,可能需要采取更严厉的措施。"ANSI_QUOTES
2赞 eggyal 4/6/2017
@DanAllen:我的回答更宽泛一些,因为你可以通过 PDO 的函数来避免这个特定的错误——但准备好的语句是避免注入的一种更安全、更合适的方法。当然,如果你直接将未转义的变量连接到你的 SQL 中,那么无论你之后使用什么方法,你肯定都容易受到注入的影响。quote()
1赞 DanAllen 4/6/2017
@eggyall:我们的系统依赖于上面的第二个安全示例。存在错误,其中省略了mysql_real_escape_string。在紧急模式下修复这些问题似乎是谨慎的做法,希望我们在修正之前不会被核弹击中。我的理由是,转换为准备好的陈述将是一个更长的过程,之后必须进行。准备好的语句更安全的原因是否是错误不会造成漏洞?换句话说,正确实现上面的第二个示例是否与准备好的语句一样安全?
1赞 ToolmakerSteve 10/24/2019
@kittygirl - 你已经把它弄倒了。这个答案说“NO_BACKSLASH_ESCAPES”是危险的(除非你做其他建议之一,以避免危险)。您显示包含“NO_BACKSLASH_ESCAPES”的sql_mode。你只是承担了不必要的风险 - 删除它。