绕过 mysql_real_escape_string() 的 SQL 注入

发布于 2024-11-02 19:18:39 字数 528 浏览 2 评论 0 原文

即使使用 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 代码吗?

Is there an SQL injection possibility even when using mysql_real_escape_string() function?

Consider this sample situation. SQL is constructed in PHP like this:

$login = mysql_real_escape_string(GetFromPost('login'));
$password = mysql_real_escape_string(GetFromPost('password'));

$sql = "SELECT * FROM table WHERE login='$login' AND password='$password'";

I have heard numerous people say to me that code like that is still dangerous and possible to hack even with mysql_real_escape_string() function used. But I cannot think of any possible exploit?

Classic injections like this:

aaa' OR 1=1 --

do not work.

Do you know of any possible injection that would get through the PHP code above?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

月隐月明月朦胧 2024-11-09 19:18:39

简短的回答是是的,是的,有一种方法可以绕过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('设置名称 gbk');
    

    要使此攻击发挥作用,我们需要服务器期望连接上的编码将 ' 编码为 ASCII,即 0x27 得到一些最终字节为 ASCII \ 的字符,即 0x5c。事实证明,MySQL 5.6 默认支持 5 种这样的编码:big5cp932gb2312gbk和<代码>sjis。我们将在这里选择gbk

    现在,请注意此处 SET NAMES 的使用,这一点非常重要。这将字符集设置在服务器上。如果我们使用对 C API 函数 mysql_set_charset() 的调用,那就没问题(自 2006 年以来的 MySQL 版本)。但稍后会详细说明原因...

  2. 有效负载

    我们将用于此注入的有效负载以字节序列 0xbf27 开头。在 gbk 中,这是一个无效的多字节字符;在 latin1 中,它是字符串 ¿'。请注意,在 latin1 gbk 中,0x27 本身就是一个文字 '性格。

    我们选择此有效负载是因为,如果我们对其调用 addslashes(),我们将插入一个 ASCII \0x5c,在 ' 字符之前。因此,我们最终会得到 0xbf5c27,它在 gbk 中是一个两个字符序列:0xbf5c 后跟 0x27 。或者换句话说,一个有效字符后跟一个未转义的'。但我们没有使用 addslashes()。那么继续下一步...

  3. mysql_real_escape_string()

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

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

    缞' 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() 中的“noreferrer">bug 意味着无效的多字节字符(例如我们的有效负载中的字符)出于转义目的被视为单个字节,即使客户端已正确处理得知连接编码,因此此攻击仍然会成功。该错误已在 MySQL 4.1.205.0.225.1.11

但最糟糕的是 PDO 直到 5.3.6 才公开 mysql_set_charset() 的 C API,因此在之前的版本中它不能针对每个可能的命令防止这种攻击!
它现在公开为 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(),您也很容易受到攻击...

The short answer is yes, yes there is a way to get around mysql_real_escape_string().
#For Very OBSCURE EDGE CASES!!!

The long answer isn't so easy. It's based off an attack demonstrated here.

The Attack

So, let's start off by showing the attack...

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

In certain circumstances, that will return more than 1 row. Let's dissect what's going on here:

  1. Selecting a Character Set

    mysql_query('SET NAMES gbk');
    

    For this attack to work, we need the encoding that the server's expecting on the connection both to encode ' as in ASCII i.e. 0x27 and to have some character whose final byte is an ASCII \ i.e. 0x5c. As it turns out, there are 5 such encodings supported in MySQL 5.6 by default: big5, cp932, gb2312, gbk and sjis. We'll select gbk here.

    Now, it's very important to note the use of SET NAMES here. This sets the character set ON THE SERVER. If we used the call to the C API function mysql_set_charset(), we'd be fine (on MySQL releases since 2006). But more on why in a minute...

  2. The Payload

    The payload we're going to use for this injection starts with the byte sequence 0xbf27. In gbk, that's an invalid multibyte character; in latin1, it's the string ¿'. Note that in latin1 and gbk, 0x27 on its own is a literal ' character.

    We have chosen this payload because, if we called addslashes() on it, we'd insert an ASCII \ i.e. 0x5c, before the ' character. So we'd wind up with 0xbf5c27, which in gbk is a two character sequence: 0xbf5c followed by 0x27. Or in other words, a valid character followed by an unescaped '. But we're not using addslashes(). So on to the next step...

  3. mysql_real_escape_string()

    The C API call to mysql_real_escape_string() differs from addslashes() in that it knows the connection character set. So it can perform the escaping properly for the character set that the server is expecting. However, up to this point, the client thinks that we're still using latin1 for the connection, because we never told it otherwise. We did tell the server we're using gbk, but the client still thinks it's latin1.

    Therefore the call to mysql_real_escape_string() inserts the backslash, and we have a free hanging ' character in our "escaped" content! In fact, if we were to look at $var in the gbk character set, we'd see:

    縗' OR 1=1 /*

    Which is exactly what the attack requires.

  4. The Query

    This part is just a formality, but here's the rendered query:

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

Congratulations, you just successfully attacked a program using mysql_real_escape_string()...

The Bad

It gets worse. PDO defaults to emulating prepared statements with MySQL. That means that on the client side, it basically does a sprintf through mysql_real_escape_string() (in the C library), which means the following will result in a successful injection:

$pdo->query('SET NAMES gbk');
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));

Now, it's worth noting that you can prevent this by disabling emulated prepared statements:

$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

This will usually result in a true prepared statement (i.e. the data being sent over in a separate packet from the query). However, be aware that PDO will silently fallback to emulating statements that MySQL can't prepare natively: those that it can are listed in the manual, but beware to select the appropriate server version).

The Ugly

I said at the very beginning that we could have prevented all of this if we had used mysql_set_charset('gbk') instead of SET NAMES gbk. And that's true provided you are using a MySQL release since 2006.

If you're using an earlier MySQL release, then a bug in mysql_real_escape_string() meant that invalid multibyte characters such as those in our payload were treated as single bytes for escaping purposes even if the client had been correctly informed of the connection encoding and so this attack would still succeed. The bug was fixed in MySQL 4.1.20, 5.0.22 and 5.1.11.

But the worst part is that PDO didn't expose the C API for mysql_set_charset() until 5.3.6, so in prior versions it cannot prevent this attack for every possible command!
It's now exposed as a DSN parameter.

The Saving Grace

As we said at the outset, for this attack to work the database connection must be encoded using a vulnerable character set. utf8mb4 is not vulnerable and yet can support every Unicode character: so you could elect to use that instead—but it has only been available since MySQL 5.5.3. An alternative is utf8, which is also not vulnerable and can support the whole of the Unicode Basic Multilingual Plane.

Alternatively, you can enable the NO_BACKSLASH_ESCAPES SQL mode, which (amongst other things) alters the operation of mysql_real_escape_string(). With this mode enabled, 0x27 will be replaced with 0x2727 rather than 0x5c27 and thus the escaping process cannot create valid characters in any of the vulnerable encodings where they did not exist previously (i.e. 0xbf27 is still 0xbf27 etc.)—so the server will still reject the string as invalid. However, see @eggyal's answer for a different vulnerability that can arise from using this SQL mode.

Safe Examples

The following examples are safe:

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

Because the server's expecting 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");

Because we've properly set the character set so the client and the server match.

$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 /*"));

Because we've turned off emulated prepared statements.

$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 /*"));

Because we've set the character set properly.

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

Because MySQLi does true prepared statements all the time.

Wrapping Up

If you:

  • Use Modern Versions of MySQL (late 5.1, all 5.5, 5.6, etc) AND mysql_set_charset() / $mysqli->set_charset() / PDO's DSN charset parameter (in PHP ≥ 5.3.6)

OR

  • Don't use a vulnerable character set for connection encoding (you only use utf8 / latin1 / ascii / etc)

You're 100% safe.

Otherwise, you're vulnerable even though you're using mysql_real_escape_string()...

吾家有女初长成 2024-11-09 19:18:39

考虑以下查询:

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

Consider the following query:

$iId = mysql_real_escape_string("1 OR 1=1");    
$sSql = "SELECT * FROM table WHERE id = $iId";

mysql_real_escape_string() will not protect you against this.
The fact that you use single quotes (' ') around your variables inside your query is what protects you against this. The following is also an option:

$iId = (int)"1 OR 1=1";
$sSql = "SELECT * FROM table WHERE id = $iId";
墟烟 2024-11-09 19:18:39

TL;DR

mysql_real_escape_string()不提供任何保护(并且可能进一步破坏您的数据),如果:

这已被归档为 bug #72458 并已在 MySQL v5 中修复.7.6(参见下面标题为“拯救恩典”的部分)。

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

为了向 @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 ,那么这些选项中的第三个(这是 mysql_real_escape_string() 采用的常用方法)不可用:必须使用前两个选项之一。请注意,第四个项目符号的效果是,必须知道将用于引用文字的字符,以避免修改数据。

  2. 有效负载

    " 或 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,
                 超长长度)
    {
      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 SQL 模式,则会调用不同的底层函数 escape_quotes_for_mysql()。如上所述,这样的函数需要知道将使用哪个字符来引用文字,以便重复它,而不会导致其他引用字符被文字重复。

    但是,此函数任意假设使用单引号'字符来引用字符串。请参阅 charset.c

    <代码>/*
      通过将撇号加倍来转义撇号
    
    // [删除839-845]
    
      描述
        这通过将任何撇号加倍来转义字符串的内容
        它包含。当 NO_BACKSLASH_ESCAPES SQL_MODE 处于
        对服务器的影响。
    
    // [删除852-858]
    */
    
    size_t escape_quotes_for_mysql(CHARSET_INFO *charset_info,
                                   char *to,size_t to_length,
                                   const char *from, size_t 长度)
    {
    // [删除865-892]
    
        if (*from == '\'')
        {
          if (to + 2 > to_end)
          {
            溢出=真;
            休息;
          }
          *到++='\'';
          *到++='\'';
        }
    

    因此,无论用于引用的实际字符如何,它都会保持双引号 " 字符不变(并将所有单引号 ' 字符加倍)在我们的例子中,$var 与提供给 mysql_real_escape_string() 的参数完全相同 - 就好像没有发生转义< em>完全没有

  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()<成功攻击了一个程序/code>...

不好的

mysql_set_charset()< /code> 没有帮助,因为这与字符集无关;也不能 mysqli::real_escape_string() ,因为这只是同一函数的不同包装。

问题(如果还不是很明显的话)是对 mysql_real_escape_string() 的调用无法知道将用哪个字符引用文字,因为这由开发人员决定稍后。因此,在 NO_BACKSLASH_ESCAPES 模式下,该函数实际上不可能能够安全地转义每个输入以用于任意引用(至少,不加倍不需要的字符)加倍并因此修改你的数据)。

丑陋的

事情变得更糟。 NO_BACKSLASH_ESCAPES 在野外可能并不那么罕见,因为它需要与标准 SQL 兼容(例如,请参阅 SQL-92 规范,即 ::= 语法产生并且反斜杠没有任何特殊含义)。此外,它的使用被明确推荐作为ircmaxell 的帖子描述的(早已修复)bug 的解决方法 。谁知道呢,一些 DBA 甚至可能将其配置为默认打开,以阻止使用不正确的转义方法,例如 addslashes()

另外,新连接的 SQL 模式由服务器根据其配置进行设置(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 开始,此错误已得到修复。请参阅更改日志

添加或更改的功能


安全示例

结合 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;
    • 单引号字符串文字;或
    • 显式设置的 SQL 模式,不包含 NO_BACKSLASH_ESCAPES

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

TL;DR

mysql_real_escape_string() will provide no protection whatsoever (and could furthermore munge your data) if:

  • MySQL's NO_BACKSLASH_ESCAPES SQL mode is enabled (which it might be, unless you explicitly select another SQL mode every time you connect); and

  • your SQL string literals are quoted using double-quote " characters.

This was filed as bug #72458 and has been fixed in MySQL v5.7.6 (see the section headed "The Saving Grace", below).

This is another, (perhaps less?) obscure EDGE CASE!!!

In homage to @ircmaxell's excellent answer (really, this is supposed to be flattery and not plagiarism!), I will adopt his format:

The Attack

Starting off with a demonstration...

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:

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

  2. The Payload

    " OR 1=1 -- 
    

    The payload initiates this injection quite literally with the " character. No particular encoding. No special characters. No weird bytes.

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

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

The Bad

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

The Ugly

It gets worse. NO_BACKSLASH_ESCAPES may not be all that uncommon in the wild owing to the necessity of its use for compatibility with standard SQL (e.g. see section 5.3 of the SQL-92 specification, namely the <quote symbol> ::= <quote><quote> grammar production and lack of any special meaning given to backslash). Furthermore, its use was explicitly recommended as a workaround to the (long since fixed) bug that ircmaxell's post describes. Who knows, some DBAs might even configure it to be on by default as means of discouraging use of incorrect escaping methods like addslashes().

Also, the SQL mode of a new connection is set by the server according to its configuration (which a SUPER user can change at any time); thus, to be certain of the server's behaviour, you must always explicitly specify your desired mode after connecting.

The Saving Grace

So long as you always explicitly set the SQL mode not to include NO_BACKSLASH_ESCAPES, or quote MySQL string literals using the single-quote character, this bug cannot rear its ugly head: respectively escape_quotes_for_mysql() will not be used, or its assumption about which quote characters require repeating will be correct.

For this reason, I recommend that anyone using NO_BACKSLASH_ESCAPES also enables ANSI_QUOTES mode, as it will force habitual use of single-quoted string literals. Note that this does not prevent SQL injection in the event that double-quoted literals happen to be used—it merely reduces the likelihood of that happening (because normal, non-malicious queries would fail).

In PDO, both its equivalent function PDO::quote() and its prepared statement emulator call upon mysql_handle_quoter()—which does exactly this: it ensures that the escaped literal is quoted in single-quotes, so you can be certain that PDO is always immune from this bug.

As of MySQL v5.7.6, this bug has been fixed. See change log:

Functionality Added or Changed

Safe Examples

Taken together with the bug explained by ircmaxell, the following examples are entirely safe (assuming that one is either using MySQL later than 4.1.20, 5.0.22, 5.1.11; or that one is not using a GBK/Big5 connection encoding):

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

...because we've explicitly selected an SQL mode that doesn't include 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");

...because we're quoting our string literal with single-quotes.

$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(["' OR 1=1 /*"]);

...because PDO prepared statements are immune from this vulnerability (and ircmaxell's too, provided either that you're using PHP≥5.3.6 and the character set has been correctly set in the DSN; or that prepared statement emulation has been disabled).

$var  = $pdo->quote("' OR 1=1 /*");
$stmt = $pdo->query("SELECT * FROM test WHERE name = $var LIMIT 1");

...because PDO's quote() function not only escapes the literal, but also quotes it (in single-quote ' characters); note that to avoid ircmaxell's bug in this case, you must be using PHP≥5.3.6 and have correctly set the character set in the DSN.

$stmt = $mysqli->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$param = "' OR 1=1 /*";
$stmt->bind_param('s', $param);
$stmt->execute();

...because MySQLi prepared statements are safe.

Wrapping Up

Thus, if you:

  • use native prepared statements

OR

  • use MySQL v5.7.6 or later

OR

  • in addition to employing one of the solutions in ircmaxell's summary, use at least one of:

    • PDO;
    • single-quoted string literals; or
    • an explicitly set SQL mode that does not include NO_BACKSLASH_ESCAPES

...then you should be completely safe (vulnerabilities outside the scope of string escaping aside).

瑶笙 2024-11-09 19:18:39

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

Well, there's nothing really that can pass through that, other than % wildcard. It could be dangerous if you were using LIKE statement as attacker could put just % as login if you don't filter that out, and would have to just bruteforce a password of any of your users.
People often suggest using prepared statements to make it 100% safe, as data can't interfere with the query itself that way.
But for such simple queries it probably would be more efficient to do something like $login = preg_replace('/[^a-zA-Z0-9_]/', '', $login);

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文