SQL注射,得到周围mysql_real_escape_string()

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

296236 次浏览

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

这样的操作可能会更有效

考虑以下查询:

$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_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中编码',即0x270x270有一些字符,其最后一个字节是ASCII\,即0x5c。事实证明,MySQL 5.6默认支持5种这样的编码:big5cp932gb2312gbksjis。我们将在这里选择gbk

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

  2. 有效载荷

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

    我们选择这个有效负载是因为,如果我们在其上调用addslashes(),我们将在'字符之前插入一个ASCII\,即0x5c。所以我们最终得到0xbf5c27,在gbk中是一个两个字符的序列:0xbf5c后跟0x27。或者换句话说,一个\0字符后跟一个未转义的'。但我们没有使用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 /*

    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
    

恭喜,您刚刚使用mysql_real_escape_string()成功攻击了一个程序…

坏人

它变得更糟。PDO默认为模仿使用MySQL准备的语句。这意味着在客户端,它基本上通过mysql_real_escape_string()(在C库中)执行冲刺,这意味着以下操作将导致成功注入:

$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()中的bug意味着无效的多字节字符(例如我们有效负载中的字符)被视为单字节用于转义目的即使客户端已被正确告知连接编码,因此此攻击仍然会成功。MySQL4.1.205.0.225.1.11中修复了bug。

但最糟糕的是PDO直到5.3.6才公开mysql_set_charset()的C API,所以在以前的版本中,不能阻止了所有可能的命令的攻击! 它现在暴露为DSN参数

拯救恩典

正如我们一开始所说,要使这种攻击奏效,数据库连接必须使用易受攻击的字符集进行编码。utf8mb4不脆弱,但可以支持 Unicode字符:所以你可以选择使用它——但它只在MySQL 5.5.3之后才可用。另一种选择是utf8,它也是不脆弱,可以支持整个Unicode基本多语种平面

或者,您可以启用NO_BACKSLASH_ESCAPESSQL模式,该模式(除其他外)会更改mysql_real_escape_string()的操作。启用此模式后,0x27将被替换为0x2727而不是0x5c27,因此转义过程不能会在任何易受攻击的编码中创建以前不存在的有效字符(即0xbf27仍然是0xbf27等)-因此服务器仍然会将字符串视为无效而拒绝。但是,请参阅@李安的回答了解使用此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等)mysql_set_charset()/$mysqli->set_charset()/PDO的DSN字符集参数(在PHP≥5.3.6中)

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

你是100%安全的。

否则,你很脆弱即使您使用的是mysql_real_escape_string()

太长别读

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. 有效载荷

    " OR 1=1 --
    

    有效负载以"字符开始此注入。没有特定的编码。没有特殊字符。没有奇怪的字节。

  3. mysql_real_escape_string()

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

    幸运的是,mysql_real_escape_string()确实检查了SQL模式并相应地调整了其行为。

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

    因此,如果使用NO_BACKSLASH_ESCAPESSQL模式,则调用不同的底层函数escape_quotes_for_mysql()。如上所述,这样的函数需要知道将使用哪个字符来引用文字,以便重复它,而不会导致其他引用字符按字面意思重复。

    但是,此函数任意假设,字符串将使用单引号'字符引用。请参阅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()的参数完全相同-就好像没有发生转义在所有

  4. 的查询

    mysql_query('SELECT * FROM test WHERE name = "'.$var.'" LIMIT 1');
    

    某种形式,呈现的查询是:

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

…因为我们明确选择了不包括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的bug在这种情况下,您必须使用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

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