ChatGPT解决这个技术问题 Extra ChatGPT

绕过 mysql_real_escape_string() 的 SQL 注入

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

@ThiefMaster - 我不想给出诸如无效用户/无效密码之类的详细错误......它告诉暴力商家他们有一个有效的用户 ID,而这只是他们需要猜测的密码
从可用性的角度来看,这很可怕。有时您无法使用您的主要昵称/用户名/电子邮件地址并在一段时间后忘记这一点,或者该网站因不活动而删除了您的帐户。然后,如果您继续尝试密码,甚至可能会阻止您的 IP,即使只是您的用户名无效,这也是非常烦人的。
Please, don't use mysql_* functions in new code。它们不再被维护,deprecation process 已经开始。看到 red box 了吗?请改为了解 prepared statements,然后使用 PDOMySQLi - this article 将帮助您决定使用哪个。如果您选择 PDO,here is a good tutorial
@machineaddict,从 5.5(最近发布)开始,mysql_* 函数已经产生 E_DEPRECATED 警告。 ext/mysql 扩展已超过 10 年未维护。你真的这么迷信吗?
@machineaddict 他们刚刚删除了 PHP 7.0 上的扩展,现在还不是 2050。

N
Naktibalda

简短的回答是是的,是的,有办法绕过mysql_real_escape_string()。 #对于非常晦涩的边缘案例!!!

长答案并不容易。它基于攻击 demonstrated here

攻击

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

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 即 0x27,又要有某个字符的最后一个字节是 ASCII \ 即 0x5c。事实证明,MySQL 5.6 默认支持 5 种这样的编码:big5、cp932、gb2312、gbk 和 sjis。我们将在这里选择 gbk。现在,注意此处使用 SET NAMES 非常重要。这会在服务器上设置字符集。如果我们使用对 C API 函数 mysql_set_charset() 的调用,我们会很好(在 2006 年以来的 MySQL 版本上)。但是稍后会详细解释为什么... 有效负载 我们将用于此注入的有效负载以字节序列 0xbf27 开头。在 gbk 中,这是一个无效的多字节字符;在 latin1 中,它是字符串 ¿'。请注意,在 latin1 和 gbk 中,0x27 本身就是一个文字 ' 字符。我们选择这个payload是因为,如果我们在它上面调用addslashes(),我们会在'字符之前插入一个ASCII\即0x5c。所以我们最终会得到 0xbf5c27,它在 gbk 中是两个字符序列:0xbf5c 后跟 0x27。或者换句话说,一个有效字符后跟一个未转义的 '。但是我们没有使用addlashes()。继续下一步…… mysql_real_escape_string() 对 mysql_real_escape_string() 的 C API 调用与 addlashes() 的不同之处在于它知道连接字符集。因此它可以对服务器期望的字符集进行正确的转义。然而,到目前为止,客户端认为我们仍在使用 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("\xbf\x27 OR 1=1 /*"));

现在,值得注意的是,您可以通过禁用模拟的准备好的语句来防止这种情况:

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

通常会产生一个真正的准备好的语句(即数据在一个单独的数据包中与查询一起发送)。但是,请注意 PDO 将默默地fallback 模拟 MySQL 无法本地准备的语句:它可以在手册中是 listed,但要注意选择适当的服务器版本)。

丑陋的

我在一开始就说过,如果我们使用 mysql_set_charset('gbk') 而不是 SET NAMES gbk,我们可以阻止这一切。如果您使用的是自 2006 年以来的 MySQL 版本,情况就是如此。

如果您使用的是较早的 MySQL 版本,那么 mysql_real_escape_string() 中的 bug 意味着无效的多字节字符(例如我们的有效负载中的那些字符)被视为单字节以进行转义即使客户端已被正确通知连接编码,所以这个攻击仍然会成功。该错误已在 MySQL 4.1.205.0.225.1.11 中修复。

但最糟糕的是,PDO 直到 5.3.6 才公开 mysql_set_charset() 的 C API,因此在之前的版本中,它无法针对所有可能的命令阻止这种攻击!它现在显示为 DSN parameter

拯救恩典

正如我们一开始所说,要使这种攻击起作用,必须使用易受攻击的字符集对数据库连接进行编码。 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's answer,了解使用此 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 / 等)

你是 100% 安全的。

否则,即使您使用的是 mysql_real_escape_string(),您也很容易受到攻击...


PDO 模拟 MySQL 的准备语句,真的吗?我看不出有什么理由这样做,因为驱动程序本身就支持它。不?
确实如此。他们在文档中说没有。但在源代码中,它清晰可见且易于修复。我把它归结为开发人员的无能。
@TheodoreR.Smith:修复起来并不容易。我一直在努力更改默认设置,但是在切换时它会导致大量测试失败。所以这是一个比看起来更大的变化。我还是希望能在5.5之前完成...
@shadyyx:不,文章描述的漏洞是关于 addslashes。我基于这个漏洞。自己试试。去获取 MySQL 5.0,并运行这个漏洞并亲自看看。至于如何将其放入 PUT/GET/POST,这很简单。输入数据只是字节流。 char(0xBF) 只是一种生成字节的可读方式。我已经在多个会议前现场演示了这个漏洞。相信我......但如果你不这样做,请自己尝试。有用...
@shadyyx:至于在 $_GET... ?var=%BF%27+OR+1=1+%2F%2A 中传递这样的时髦,在 URL 中,$var = $_GET['var']; 在代码中,并且 Bob 是你的叔叔。
J
John

考虑以下查询:

$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_query() 不会执行多个语句,不是吗?
@Pekka,虽然通常的例子是DROP TABLE,但实际上攻击者更有可能SELECT passwd FROM users。在后一种情况下,通常使用 UNION 子句执行第二个查询。
(int)mysql_real_escape_string - 这毫无意义。它与 (int) 完全没有区别。他们将为每个输入产生相同的结果
这更像是对该功能的滥用,而不是其他任何事情。毕竟,它被命名为 mysql_real_escape_string,而不是 mysql_real_escape_integer 。这并不意味着与整数字段一起使用。
@ircmaxell,但答案完全具有误导性。显然,问题是询问引号中的内容。 “行情不存在”不是这个问题的答案。
C
Community

TL;DR mysql_real_escape_string() 在以下情况下将不提供任何保护(并且可能进一步破坏您的数据): MySQL 的 NO_BACKSLASH_ESCAPES SQL 模式已启用(它可能是,除非您在每次连接时明确选择另一个 SQL 模式);并且您的 SQL 字符串文字使用双引号 " 字符引用。这是作为错误 #72458 提交的,并已在 MySQL v5.7.6 中修复(请参阅下面标题为“The Saving Grace”的部分)。

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

为了向 @ircmaxell's excellent answer 致敬(真的,这应该是奉承而不是抄袭!),我将采用他的格式:

攻击

从演示开始...

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"');如字符串文字中所述:有几种方法可以在字符串中包含引号字符:用“'”引用的字符串中的“'”可以写为“''”。用“"”引用的字符串中的“"”可以写成“""”。在引号字符前加上转义字符 (“\”)。用“"”引用的字符串中的“'”不需要特殊处理,也不需要加倍或转义。同样,用“'”引用的字符串中的“"”不需要特殊处理。如果服务器的 SQL 模式包含 NO_BACKSLASH_ESCAPES,则这些选项中的第三个——这是 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: /* 通过将撇号加倍来转义撇号 // [ deletia 839- 845 ] 描述 这通过将字符串包含的任何撇号加倍来转义字符串的内容。当 NO_BACKSLASH_ESCAPES SQL_MODE 在服务器上生效时使用此选项。 // [ 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) { 溢出= TRUE;休息; } *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 specification 的第 5.3 节,即 <quote symbol> ::= <quote><quote> 语法产生式并且缺乏赋予反斜杠)。此外,对于 ircmaxell 的帖子所描述的(早已修复的)bug,它的使用是明确的 recommended as a workaround。谁知道呢,一些 DBA 甚至可能将其配置为默认开启,以阻止使用不正确的转义方法(如 addslashes())。

此外,SQL mode of a new connection 由服务器根据其配置设置(SUPER 用户可以随时更改);因此,为了确定服务器的行为,您必须始终在连接后明确指定所需的模式。

拯救恩典

只要您始终明确将 SQL 模式设置为不包含 NO_BACKSLASH_ESCAPES,或者使用单引号字符引用 MySQL 字符串文字,这个 bug 就无法抬头:分别 escape_quotes_for_mysql() 不会被使用,或者它关于哪些引号字符需要重复的假设将是正确的。

出于这个原因,我建议使用 NO_BACKSLASH_ESCAPES 的任何人也启用 ANSI_QUOTES 模式,因为它会强制习惯使用单引号字符串文字。请注意,如果碰巧使用了双引号文字,这不会阻止 SQL 注入——它只会降低发生这种情况的可能性(因为正常的、非恶意的查询会失败)。

在 PDO 中,它的等效函数 PDO::quote() 和它的预处理语句仿真器都调用 mysql_handle_quoter() — 这正是这样做的:它确保转义的文字用单引号引起来,因此您可以确定 PDO 始终免疫这个错误。

自 MySQL v5.7.6 起,此错误已得到修复。请参阅change log

添加或更改的功能 不兼容的更改:已实现一个新的 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 错误。参考:另见错误 #19211994。

安全示例

结合ircmaxell解释的bug,下面的例子是完全安全的(假设一个是使用晚于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 准备好的语句是安全的。

包起来

因此,如果您:

使用本机准备好的语句

或者

使用 MySQL v5.7.6 或更高版本

或者

除了采用 ircmaxell 总结中的解决方案之一,至少使用以下之一: PDO;单引号字符串文字;或不包含 NO_BACKSLASH_ESCAPES 的显式设置的 SQL 模式

PDO;

单引号字符串文字;或者

不包含 NO_BACKSLASH_ESCAPES 的显式设置的 SQL 模式

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


所以,TL;DR 就像“有一个 NO_BACKSLASH_ESCAPES mysql 服务器模式,如果你不使用单引号,它可能会导致注入。
我无法访问 bugs.mysql.com/bug.php?id=72458;我只是得到一个拒绝访问的页面。由于安全问题,它是否对公众隐藏?另外,我是否从这个答案中正确理解您是漏洞的发现者?如果是这样,那么恭喜。
人们首先不应该将 " 用于字符串。 SQL 说这是针对标识符的。但是,嗯……这只是 MySQL 的另一个例子,它说“螺丝标准,我会做任何我想做的事”。 (幸运的是,您可以在模式中包含 ANSI_QUOTES 来修复引用错误。但是,公开无视标准是一个更大的问题,可能需要更严厉的措施。)
@DanAllen:我的回答有点宽泛,因为您可以通过 PDO 的 quote() 函数避免 这个特定的错误,但准备好的语句是一种更安全、更合适的方式一般避免注射。当然,如果您直接将未转义的变量连接到您的 SQL 中,那么无论您之后使用什么方法,您都肯定容易受到注入的影响。
@eggyall:我们的系统依赖于上面的第二个安全示例。有错误,其中 mysql_real_escape_string 已被省略。在紧急模式下修复那些似乎是谨慎的道路,希望我们不会在更正之前被核爆。我的理由是转换为准备好的陈述将是一个更长的过程,必须经过。准备好的语句更安全的原因是错误不会造成漏洞吗?换句话说,上面的第二个示例是否正确实现与准备好的语句一样安全?
S
Slava

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


+1,但通配符用于 LIKE 子句,而不是简单的相等。
与使用准备好的语句相比,您认为通过什么衡量简单的替换 more efficient? (准备好的语句总是有效的,库可以在受到攻击的情况下快速纠正,不会暴露人为错误[例如错误键入完整的替换字符串],并且如果重复使用语句,则具有显着的性能优势。)
@Slava:您实际上将用户名和密码仅限于单词字符。大多数对安全性一无所知的人会认为这是一个坏主意,因为它会大大缩小搜索空间。当然,他们也认为在数据库中存储明文密码是个坏主意,但我们不需要使问题复杂化。 :)
@cHao,我的建议只涉及登录。显然您不需要过滤密码,对不起,我的回答中没有明确说明。但实际上这可能是个好主意。使用“stone ignorant tree space”而不是难以记住并键入“a4üua3!@v\"ä90;8f" 会更难暴力破解。即使使用字典,比如 3000 个单词来帮助你,知道你正好用了 4 个词——那仍然是大约 3.3*10^12 组合。:)
@Slava:我以前见过这个想法;见xkcd.com/936。问题是,数学并不能完全证明这一点。您的示例 17 字符密码可能有 96^17 种可能性,前提是您忘记了变音符号并将自己限制为可打印的 ASCII。大约是 4.5x10^33。我们实际上是在谈论十亿万亿倍的蛮力工作。即使是 8 字符的 ASCII 密码也有 7.2x10^15 种可能性——多出 3000 倍。