免责声明本公众号所发布的文章内容仅供学习与交流使用,禁止用于任何非法用途。
众所周知在 Web 安全领域,“使用预处理语句” 几乎是防止SQL注入的一个普遍认知了。开发者们都会认为,我只要使用了 PHP 的 PDO 预处理机制,那么就可以一劳永逸的杜绝sql注入的风险了。

但是之前在 DownUnderCTF 比赛中出现的一道挑战(以及随后的安全研究)披露了一种新的攻击手段:在特定配置和场景下,开发者即便是使用了 PDO 预处理语句,但是依然还可能存在 SQL 注入漏洞。
本文将基于slcyber团队最新的安全研究,剖析这种针对 PDO 预处理语句的新型注入技术,探讨其漏洞成因、利用方式及其防御策略,欢迎各位大佬入座。
如果要理解这个漏洞,我们首先需要理解 PHP PDO 的工作原理。
当下开发环境中许多开发者认为,只要调用 $pdo->prepare() 了,那么PHP 会直接调用数据库(比如说 MySQL)的原生预处理 API。但事实并非总不是如此。一般的默认情况下,PHP PDO 为了兼容性和性能,都会去使用了一种被称为 “模拟预处理”(Emulated Prepared Statements) 的机制
(即 PDO::ATTR_EMULATE_PREPARES 默认为 true)。
那么模拟预处理的工作流程如下:
? 或 :name),并将绑定的参数进行转义后,直接替换到 SQL 字符串当中去。此时,我们会发现问题的根源在于:PDO 的 SQL 解析器与数据库服务端的 SQL 解析器并不完全一致。
PDO 必须要通过解析 SQL 语句来确定哪些 ? 是真正的占位符,而哪些又是字符串或注释当中的字符。
如果说攻击者能构造出一种特殊的输入,让 PDO 的解析器产生了误判,比如,将字符串内部的字符误认为是占位符,或者反之),就会导致参数的替换错位,从而引发注入漏洞。
我们来看下述三个场景:
\0) 欺骗解析器研究发现,PDO 的解析器(尤其是在 PHP 8.4 之前使用通用解析器时)在处理某些特殊字符时存在一些缺陷。最典型的利用场景是在开发者不得不进行“部分动态拼接”的时候。
观看以下代码,开发者为了实现动态列名选择,手动拼接了列名(虽然使用了反引号和替换来防御sql注入漏洞),但是对于 WHERE 条件使用了预处理:
$col = '`' . str_replace('`', '``', $_GET['col']) . '`';// 虽然看起来很安全:列名被转义了,查询条件也是预处理的$stmt = $pdo->prepare("SELECT $col FROM fruit WHERE name = ?");$stmt->execute([$_GET['name']]);但是如果攻击者在 col 参数中注入 ?#\0(URL编码为 ?%23%00),那么会发生什么呢?
我们可以根据上述提到的三个步骤预演下:
PDO 解析阶段: PDO 的解析器在扫描 SQL 字符串时候,遇到了 \0(空字节)。而由于 C 语言字符串处理的特性或者解析逻辑的回溯错误,PDO 可能无法正确识别反引号的闭合,导致它就会认为我们在 col 中注入的 ? 是一个有效的绑定占位符。
参数替换: 既然PDO 认为这个 ? 需要被替换,于是它就将用户绑定的参数(比如说 $_GET['name'] 的值 'apple')填入了这个位置。而那原本用于 WHERE name = ? 的那个真正的占位符,因为参数已经被用掉了,可能会引发“参数数量不匹配”的报错或者直接被忽略掉。
最终 SQL: 最终经过 PDO 错误替换之后,发送给到 MySQL 的语句可能就会变成了类似下面这样:
SELECT `'apple'#\0` FROM fruit WHERE name = ?这里,'apple' 是被注入进去的绑定值。# 在 MySQL 中是注释符。我们可以发现通过这种方式,攻击者就成功地修改了 SQL 的结构,此时预编译并没有阻挡攻击者的步伐QAQ。
虽然这个例子看似只是把绑定值换了位置,但是如果结合一些复杂的 payload,攻击者就可以构造出闭合引号或者是注释掉后续查询sql语句并且联合查询(UNION SELECT)出非预期的一些数据。
这是另一种场景:开发者混合使用了手动转义和预处理。
// 开发者手动转义了 $sku,自认为这样很安全$sku = strtr($_GET['sku'], ["'" => "\\'", '\\' => '\\\\']); $stmt = $pdo->prepare("SELECT * FROM fruit WHERE sku LIKE '%$sku%' AND name = ?");$stmt->execute([$_GET['name']]);那么如果攻击者在 $sku 中传入 ?%00,PDO 的解析器在处理 %?%00% 这一段时,PDO就可能因为空字节的存在误判了单引号的边界,从而会认为字符串内部的 ? 只是一个占位符。
结果就是:PDO 把本该给到 name 字段的值,填入到了 sku 的字符串字面量中,进而会导致查询逻辑被破坏。
在 PHP 8.4 之前,PDO 会使用一个通用的 SQL 解析器来处理所有类型的数据库。但是这就会导致一个非常严重的问题:不同数据库的转义规则不同,但 PDO 却假设它们都一样。
例如说,Postgres 默认是不支持使用反斜杠 \ 来转义单引号(标准 SQL 使用双单引号 '')。但是旧版的 PDO 解析器默认认为 \ 是转义符。
我们看下书攻击向量:
攻击者输入 \'?。
\ 转义了 ',认为引号还没有结束,而接着看到了 ?,其认为这只是字符串的一部分,或者还会在某些复杂的解析状态下产生误判。\ 只是一个普通字符,' 结束了字符串。后续的字符被视为 SQL 命令__QAQ__;这种解析差异是此类注入的核心。
当然了,这个漏洞一般很难利用的,在非白盒的情况下需要一点点的尝试,加上WAF的存在会更难被发现,但是它打破了“预处理即安全”的这个假设。那么为了防御这类攻击,可以采取下列措施:
禁用模拟预处理(最佳实践)这是最根本的解决方案。强制 PDO 使用数据库原生的预处理功能,这样 SQL 解析就完全由数据库服务端完成可,即:不存在客户端解析歧义的问题。
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);升级 PHP 版本PHP 8.4 引入了针对特定驱动的 SQL 解析器(Driver-specific parsers),修复了通用解析器在处理不同数据库语法时的许多歧义问题(打站的时候可以关注下PHP版本)。
拒绝输入当中的空字节在应用层将用户输入进行严格清洗,拒绝包含 \0(NULL byte)的字符串。
避免混合使用不要在同一个查询中既使用手动字符串拼接(即使有转义),又使用参数绑定,越复杂越容易出问题
参考来源:Searchlight Cyber Research - A Novel Technique for SQL Injection in PDO's Prepared Statements