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种编码: big5
, cp932
, gb2312
, gbk
和sjis
。 我们在这里选择gbk
。
现在,在这里注意使用SET NAMES
非常重要。 这将字符集设置为ON THE SERVER 。 如果我们使用C API函数mysql_set_charset()
的调用,那么我们会很好(从2006年开始MySQL发布)。 但更多的是为什么在一分钟内...
有效载荷
我们将用于此注入的有效负载从字节序列0xbf27
。 在gbk
,这是一个无效的多字节字符; 在latin1
,它是字符串¿'
。 请注意,在latin1
和 gbk
, 0x27
本身就是一个文字'
字符。
我们选择了这个有效载荷,因为如果我们在它上面调用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一直都在准备真正的语句。
包起来
如果你:
$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准备的语句是安全的。
包起来
因此,如果您:
要么
要么
除了在ircmaxell的摘要中使用其中一种解决方案外,还应至少使用以下一种方法:
NO_BACKSLASH_ESCAPES
...那么你应该完全安全(漏掉字符串范围之外的漏洞)。
链接地址: http://www.djcxy.com/p/2727.html