SQL注入绕过MySQL

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


考虑以下查询:

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

对于非常OBSCURE边缘案件!

长的回答并不容易。 它基于在这里演示的攻击。

攻击

所以,让我们以显示攻击开始......

mysql_query('SET NAMES gbk');
$var = mysql_real_escape_string("xbfx27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");

在某些情况下,这将返回超过1行。 让我们来分析一下这里发生了什么:

  • 选择一个字符集

    mysql_query('SET NAMES gbk');
    

    为了使这种攻击起作用,我们需要服务器对连接进行编码的编码'如ASCII码0x27并且有一些字符的最终字节为ASCII ie 0x5c 。 事实证明,默认情况下MySQL 5.6支持5种编码: big5cp932gb2312gbksjis 。 我们在这里选择gbk

    现在,在这里注意使用SET NAMES非常重要。 这将字符集设置为ON THE SERVER 。 如果我们使用C API函数mysql_set_charset()的调用,那么我们会很好(从2006年开始MySQL发布)。 但更多的是为什么在一分钟内...

  • 有效载荷

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

    我们选择了这个有效载荷,因为如果我们在它上面调用addslashes() ,我们会在'字符之前插入一个ASCII ie 0x5c 。 因此,我们最终会得到0xbf5c27 ,它在gbk是一个两字符序列: 0xbf5c后面是0x27 。 或者换句话说,一个有效的字符后跟一个非转义的' 。 但是我们没有使用addslashes() 。 所以下一步...

  • mysql_real_escape_string()

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

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

    縗' OR 1=1 /*

    这正是攻击所需要的。

  • 查询

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

    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("xbfx27 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.20,5.0.22和5.1.11中修复。

    但最糟糕的部分是, PDO在5.3.6之前没有将mysql_set_charset()的C API公开,所以在以前的版本中,它无法阻止这种攻击的每一个可能的命令! 它现在作为DSN参数公开。

    节省的恩典

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

    或者,您可以启用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("xbfx27 OR 1=1 /*");
    mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");
    

    因为服务器期待utf8 ...

    mysql_set_charset('gbk');
    $var = mysql_real_escape_string("xbfx27 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("xbfx27 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("xbfx27 OR 1=1 /*"));
    

    因为我们已经正确设置了字符集。

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

    因为MySQLi一直都在准备真正的语句。

    包起来

    如果你:

  • 使用MySQL的现代版本(5.1后,全部$mysqli->set_charset()等) mysql_set_charset() / $mysqli->set_charset() / PDO的DSN字符集参数(PHP中的$mysqli->set_charset()
  • 要么

  • 不要使用易受攻击的字符集进行连接编码(您只能使用utf8 / latin1 / ascii / etc)
  • 你100%安全。

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


    TL; DR

    mysql_real_escape_string()将不会提供任何保护 (并且可能会涉及您的数据):

  • MySQL的NO_BACKSLASH_ESCAPES SQL模式已启用(可能是这样,除非您每次连接时都显式选择另一个SQL模式); 和

  • 你的SQL字符串文字用双引号引用"字符。

  • 这是作为bug#72458提交的,并已在MySQL v5.7.6中修复(请参阅下面的“ The Saving Grace ”一节)。

    这是另一个,(可能更少?)掩盖EDGE CASE!

    对@ 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"');
    

    正如在字符串文字下记录:

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

  • A“ ' ”加引号的字符串里面的“ ' ”可以写成‘ '' ’。

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

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

  • A“ ' ”与“援引一个字符串内" ”不需要特殊对待而且不必增加一倍或逃脱。以同样的方式" ’ ”在字符串中引述“ ' ”也不需要特殊治疗。

  • 如果服务器的SQL模式包含NO_BACKSLASH_ESCAPES ,那么这些选项中的第三个(这是mysql_real_escape_string()采用的常用方法mysql_real_escape_string()不可用:必须使用前两个选项之一。 请注意,第四个项目符号的效果是,必须知道将用于引用文字的字符,以避免传送数据。

  • 有效载荷

    " OR 1=1 -- 
    

    有效载荷启动此注射毫不夸张地用"字,没有特定的编码,没有特殊字符,没有怪异的字节。

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

    因此,如果正在使用NO_BACKSLASH_ESCAPES SQL模式,则会调用不同的基础函数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() - 就好像没有逃跑一样。

  • 查询

    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() :分别不使用escape_quotes_for_mysql() ,或者不使用escape_quotes_for_mysql()字符需要重复才会正确。

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

    在PDO中,它的等效函数PDO::quote()及其准备好的语句模拟器调用mysql_handle_quoter() ,它确实如此:它确保转义文字用单引号括起来,因此您可以确定PDO是总是免受这个bug的影响。

    从MySQL v5.7.6开始,这个bug已经修复。 请参阅更改日志:

    功能添加或更改

  • 不兼容的更改:新的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.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准备的语句是安全的。

    包起来

    因此,如果您:

  • 使用本地准备的语句
  • 要么

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

  • 除了在ircmaxell的摘要中使用其中一种解决方案外,还应至少使用以下一种方法:

  • PDO;
  • 单引号字符串文字; 要么
  • 一个明确设置的SQL模式,不包括NO_BACKSLASH_ESCAPES
  • ...那么你应该完全安全(漏掉字符串范围之外的漏洞)。

    链接地址: http://www.djcxy.com/p/2727.html

    上一篇: SQL injection that gets around mysql

    下一篇: Close/hide the Android Soft Keyboard with Kotlin