SQL注入绕过mysql_real_escape_string()


645

即使使用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代码会进行任何可能的注入?


34
@ThiefMaster-我不希望给出冗长的错误,例如无效的用户名/无效的密码...它告诉暴力商人他们具有有效的用户ID,而这只是他们需要猜测的密码
Mark Ba​​ker

18
从可用性的角度来看,这太可怕了。有时,您无法使用您的主要昵称/用户名/电子邮件地址,过了一段时间便忘记了这一点,或者站点因不活动而删除了您的帐户。如果您继续尝试输入密码,甚至是无效的IP地址,那将是非常烦人的事情。
ThiefMaster

50
请不要mysql_*在新代码中使用函数。它们已不再维护,并且已开始弃用过程。看到红色框了吗?了解准备的语句来代替,并使用 PDO库MySQLi -本文将帮助你决定哪些。如果您选择PDO,这是一个很好的教程
tereško

13
@machineaddict,自5.5(最近发布)以来,这些mysql_*功能已经产生E_DEPRECATED警告。该ext/mysql扩展还没有被保持超过10年。你真的那么妄想吗?
tereško

13
@machineaddict他们只是在PHP 7.0上删除了该扩展名,但还没有到2050年。
GGG

Answers:


379

考虑以下查询:

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

9
但这不是真正的问题,因为mysql_query()不执行多条语句,不是吗?
Pekka

11
@Pekka,尽管通常的例子是DROP TABLE,实际上,攻击者更有可能这样做SELECT passwd FROM users。在后一种情况下,通常使用UNION子句执行第二个查询。
雅科

58
(int)mysql_real_escape_string-这没有道理。一点也不不同(int)。他们将为每个输入产生相同的结果
zerkms 2012年

28
与其他任何事情相比,这更是对该功能的滥用。毕竟,它被命名为mysql_real_escape_string,而不是mysql_real_escape_integer。这并不是要与整数字段一起使用。
NullUserException 2012年

11
@ircmaxell,但是答案完全是误导的。显然,问题是询问引号中的内容。“行情不存在” 不是这个问题的答案。
Pacerier,2015年

629

简短的答案是,是的,有一种解决方法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行以上。让我们剖析这里发生的事情:

  1. 选择字符集

    mysql_query('SET NAMES gbk');

    为了使这种攻击起作用,我们需要服务器在连接上期望的编码既要'以ASCII码编码0x27 也要具有某些字符的最终字节为ASCII码,\0x5c。事实证明,会默认在MySQL 5.6支持5个这样的编码:big5cp932gb2312gbksjis。我们将gbk在此处选择。

    现在,注意SET NAMES这里的用法非常重要。这将在服务器上设置字符集。如果我们使用对C API函数的调用mysql_set_charset(),那会很好的(自2006年以来的MySQL版本)。但是更多关于为什么在一分钟...

  2. 有效载荷

    我们将用于此注入的有效负载从字节序列开始0xbf27。在中gbk,这是一个无效的多字节字符;在latin1,这是字符串¿'。请注意,在latin1 gbk0x27对自己是一个文字'字符。

    我们选择此有效负载是因为,如果调用addslashes()它,我们会在字符之前插入一个ASCII码,\即。因此,我们将以结束,其中有两个字符序列:后跟。换句话说,就是一个有效字符,后跟一个未转义的。但是我们没有使用。继续下一步...0x5c'0xbf5c27gbk0xbf5c0x27'addslashes()

  3. mysql_real_escape_string()

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

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

    OR'OR 1 = 1 / *

    正是攻击所需要的。

  4. 查询

    这只是一个形式,但这是呈现的查询:

    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.205.0.225.1.11

但是最糟糕的是,直到5.3.6 PDO才公开C API mysql_set_charset(),因此在以前的版本中,它无法防止所有可能的命令遭受这种攻击!现在,它作为DSN参数公开。

拯救恩典

正如我们在一开始所说的那样,要使这种攻击起作用,必须使用易受攻击的字符集对数据库连接进行编码。 utf8mb4不容易,但可以支持所有的 Unicode字符:所以你可以选择使用的是代替,但它只是可利用从MySQL 5.5.3。另一种选择是utf8,它也不易受到攻击,并且可以支持整个Unicode Basic Multilingual Plane

或者,您可以启用NO_BACKSLASH_ESCAPESSQL模式,该模式(除其他外)会更改的操作mysql_real_escape_string()。启用该模式,0x27将被替换0x2727,而不是0x5c27从而逃逸过程不能在任何地方,他们以前不存在的脆弱编码的创建有效的字符(即0xbf270xbf27等) -这样的服务器仍然会拒绝的字符串为无效。但是,有关使用此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的(晚5.1,所有5.5,5.6,等)使用的现代版本 mysql_set_charset() / $mysqli->set_charset()/ PDO的DSN charset参数(在PHP 5.3.6≥)

要么

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

您是100%安全的。

否则,即使您正在使用mysql_real_escape_string() ... ,也很容易受到攻击。


3
PDO是为MySQL模拟Prepare语句,真的吗?由于驱动程序本身支持它,因此我看不出为什么要这样做。没有?
netcoder

16
是的 他们在文档中说没有。但是在源代码中,它是显而易见的,并且易于修复。我将其归因于开发人员的无能。
西奥多·史密斯

5
@ TheodoreR.Smith:修复起来并不容易。我一直在尝试更改默认值,但是切换默认设置后,它无法进行大量测试。因此,这是一个比看起来更大的变化。我仍然希望能在5.5之前完成...
ircmaxell,2012年

14
@shadyyx:不,本文描述的漏洞与有关addslashes。我基于此漏洞。自己尝试。去获取MySQL 5.0,并运行此漏洞并亲自查看。至于如何将其放入PUT / GET / POST,则是TRIVIAL。输入数据只是字节流。char(0xBF)只是生成字节的一种可读方式。我已经在多个会议上现场演示了此漏洞。相信我...但是,如果您不尝试,请自己尝试。它有效...
ircmaxell 2012年

5
@shadyyx:至于在$ _GET ... ?var=%BF%27+OR+1=1+%2F%2A中通过URL,$var = $_GET['var'];代码和Bob的叔叔传递这种时髦。
cHao 2012年

183

TL; DR

mysql_real_escape_string()如果发生以下情况,将不会提供任何保护(并且可能会进一步破坏您的数据):

  • MySQL的NO_BACKSLASH_ESCAPESSQL模式已启用(除非每次连接都显式选择另一个SQL模式,否则可能会启用);和

  • 您的SQL字符串文字使用双引号引起来"

它被记录为bug#72458,并已在MySQL v5.7.6中修复(请参见下面的“ The Saving Grace ” 部分)。

这是另一个(也许更少?)晦涩的EDGE案例!!!

为了向@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"');

    字符串文字中所述

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

    • A“ '”加引号的字符串内“ '”可以写为“ ''”。

    • A“ "”加引号的字符串内“ "”可以写为“ ""”。

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

    • A“ '”加引号的字符串内“ "”也不需要特殊对待而且不必增加一倍或逃脱。同样,用“ "”引号的字符串中的“ '”不需要特殊处理。

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

  2. 有效载荷

    " OR 1=1 -- 

    有效负载从字面上完全按照"字符启动此注入。没有特定的编码。没有特殊字符。没有奇怪的字节。

  3. 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);
    }

    因此escape_quotes_for_mysql(),如果使用NO_BACKSLASH_ESCAPESSQL模式,则调用另一个基础函数。如上所述,这样的函数需要知道将使用哪个字符来引用文字,以便重复它而又不会导致另一个引用字符在文字上被重复。

    但是,此函数任意假定将使用单引号'字符将字符串引起来。见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(),因为这只是围绕同一功能的不同包装。

问题(如果不是很明显的话)是对的调用mysql_real_escape_string() 不知道文字将用哪个字符加引号,因为这留给开发人员在以后决定。因此,在NO_BACKSLASH_ESCAPES模式下,实际上没有办法上讲,该函数无法安全地转义每个输入以供任意引用使用(至少,不加倍加倍不需要字符的字符,这样就不会浪费数据)。

丑陋的

情况变得更糟。 NO_BACKSLASH_ESCAPES由于有必要使用它来与标准SQL兼容,因此在野外可能并不太常见(例如,请参见SQL-92规范的 5.3节,即<quote symbol> ::= <quote><quote>语法生成,并且对反斜杠没有任何特殊含义)。此外,明确建议使用它作为 ircmaxell帖子所描述的(早已修复)错误的解决方法。谁知道,某些DBA甚至可能会将其配置为默认情况下处于启用状态,以阻止使用不正确的转义方法,例如addslashes()

此外,服务器根据其配置(用户可以随时更改)来设置新连接SQL模式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开始,此错误已修复。请参阅更改日志

添加或更改功能

安全的例子

结合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');

...因为我们明确选择了不包含的SQL模式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预备语句不受此漏洞的影响(如果您使用的是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准备好的语句是安全的。

包起来

因此,如果您:

  • 使用本机准备的语句

要么

  • 使用MySQL v5.7.6或更高版本

要么

  • 于使用在ircmaxell的总结的解决方案之一,使用,并在至少一个:

    • PDO;
    • 单引号字符串文字;要么
    • 显式设置的SQL模式,不包括 NO_BACKSLASH_ESCAPES

...那么您应该是完全安全的(漏洞不在字符串转义范围之内)。


10
因此,TL; DR将会像“有一个NO_BACKSLASH_ESCAPES MySQL服务器模式下,如果你不使用单引号这可能会导致注射。
你的常识,

我无法访问bugs.mysql.com/bug.php?id=72458 ; 我刚得到一个拒绝访问页面。是否由于安全问题而对公众隐藏?另外,我是否从此答案中正确理解您是漏洞的发现者?如果是这样,那么恭喜您。
Mark Amery 2014年

1
人们首先不应该使用"字符串。SQL说这是用于标识符。但是……只是MySQL的另一个例子,它说“螺钉标准,我会做我想做的事”。(幸运的是,您可以ANSI_QUOTES在此模式中修复报价损坏的问题。但是,公开无视标准是一个更大的问题,可能需要更严格的措施。)
cHao 2014年

2
@DanAllen:我的回答要广一些,因为您可以通过PDO的功能避免这个特定的错误quote()-但是准备好的语句是一种通常更安全的避免注入的方式。当然,如果您直接将未转义的变量连接到SQL中,那么无论随后使用哪种方法,您无疑都很容易注入。
eggyal

1
@eggyall:我们的系统依赖于上面的第二个安全示例。有错误,其中省略了mysql_real_escape_string。在紧急情况下解决这些问题似乎是一种谨慎的方法,希望我们在进行更正之前不会感到无所适从。我的基本原理是,转换为准备好的语句将需要更长的过程。错误不会造成漏洞的事实是准备语句更安全的原因吗?换句话说,上面第二个示例是否正确实施与准备好的语句一样安全?
DanAllen '17

18

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


2
+1,但通配符用于LI​​KE子句,而不是简单的相等性。
多尔

7
more efficient与采用预备语句相比,您认为通过什么措施进行简单替换?(预准备的语句始终有效,可以在遭受攻击的情况下快速纠正该库,不会暴露人为错误(例如,错误键入完整的替换字符串),并且如果重新使用该语句,则可以显着提高性能。)
MatBailie 2011年

7
@Slava:您实际上是将用户名和密码限制为仅单词字符。大多数对安全性一无所知的人会认为这是个坏主意,因为它会大大缩小搜索空间。当然,他们也认为将明文密码存储在数据库中是个坏主意,但是我们不必为此加重麻烦。:)
cHao 2013年

2
@cHao,我的建议仅涉及登录。显然,您不需要过滤密码,抱歉,我的回答中并未明确指出。但是实际上,这可能是个好主意。使用“石头无知树空间”代替难以记忆和键入的“a4üua3!@v \”ä90; 8f”将更加困难,即使使用字典(比如说3000个单词)来帮助您,您只使用了4个字-仍然大约是3.3 * 10 ^ 12个组合。:)
Slava 2013年

2
@Slava:我以前见过这个主意;参见xkcd.com/936。问题是,数学并不能完全证明这一点。您的示例17个字符的密码可能有96 ^ 17个可能性,那就是如果您忘记了变音符号并将自己限制为可打印的ASCII。大约是4.5x10 ^ 33。从字面上看,我们正在谈论十亿亿倍的蛮力工作。即使是8个字符的ASCII密码也有7.2x10 ^ 15的可能性-超过三千倍。
cHao
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.