很多人以为SQL注入只发生在URL参数或表单输入框里,却忽略了HTTP请求中一个极其危险的载体:Cookie。攻击者将恶意SQL语句编码后植入Cookie,利用后端对Cookie值的盲目信任和不当解码,绕过层层防御。这不是理论上的攻击路径,而是真实渗透测试中屡试不爽的高危手法。问题的核心在于,开发人员往往对GET和POST参数保持高度警惕,做了严格的过滤和参数化查询,但面对Cookie时却松懈了——要么直接拼接Cookie值到SQL语句,要么解码逻辑存在缺陷,让编码后的攻击载荷长驱直入。

Cookie注入的常见场景

Cookie注入的高发地带是那些根据Cookie值进行数据库操作的页面。典型场景包括:用户登录后,后端根据Cookie中的用户ID查询个人信息;电商网站根据Cookie中的购物车标识查询商品详情;内容管理系统根据Cookie中的语言偏好拼接SQL查询条件。这些操作如果采用字符串拼接方式构建SQL,且Cookie值未经有效过滤,攻击者就能通过修改Cookie值注入恶意代码。更隐蔽的情况是,某些框架会自动将Cookie值解码后存入全局变量,开发者在后续数据库操作中直接引用,完全意识不到数据来源是客户端可控的。

一个容易被忽视的细节是,Cookie在HTTP头中传输时,特殊字符必须经过URL编码。例如单引号会被编码为%27,空格编码为%20或+号。后端脚本语言如PHP会自动对Cookie值进行URL解码,这意味着攻击者提交的%27到了SQL语句中就变回了单引号。但问题在于,很多安全过滤机制是在解码之前执行的——WAF看到的是%27这个字符串,没有识别出危险,放行后却被后端解码成了真实的攻击字符。这就是编码绕过的核心原理。

编码绕过的技术细节

攻击者利用多层编码来混淆WAF和后端过滤规则。最基础的是URL编码,将单引号编码为%27,将空格编码为%20。当WAF的规则只匹配原始的单引号字符时,%27就能轻松穿透。更高级的做法是双重URL编码:先将单引号编码为%27,再将%符号本身编码为%25,最终得到%2527。如果后端只做一次URL解码,%2527会变成%27,仍然是编码状态,不会触发SQL注入。但如果后端进行了两次解码,%2527先变成%27,再变成单引号,注入就发生了。这种双重解码的漏洞常见于有反向代理或多层架构的系统,每层都可能对Cookie值进行一次解码操作。

除了URL编码,十六进制编码和Unicode编码也是绕过利器。MySQL支持使用0x开头加十六进制值来表示字符串,例如0x61646d696e等同于字符串"admin"。攻击者可以将整个SQL语句转换为十六进制,再放入Cookie中。如果后端直接将Cookie值拼接到SQL语句且数据库支持这种语法,攻击就能成功。类似地,SQL Server支持用CHAR()函数拼接字符,攻击者可以用CHAR(39)来替代单引号,绕过基于单引号的过滤规则。

还有一种基于字符集特性的绕过方式。当后端使用GBK等宽字节字符集时,攻击者可以利用宽字节注入。具体做法是在单引号前加上一个GBK编码范围的前缀字节,例如%df。如果后端使用GBK编码且开启了addslashes之类的转义函数,%df%27会被转义为%df%5c%27,其中%5c是反斜杠。但在GBK编码下,%df%5c是一个合法的汉字字符,反斜杠被吃掉了,后面的单引号就逃逸出来了。这种攻击在Cookie注入中同样有效,因为Cookie值经过URL解码后,字符集处理逻辑与GET参数一致。

实战中的编码组合拳

真实的攻击场景中,攻击者往往组合使用多种编码技术。假设目标系统前端有WAF检测Cookie中的SQL关键字,后端PHP使用GBK编码且自动URL解码Cookie值。攻击者可以这样构造载荷:先将完整的SQL注入语句进行十六进制编码,再对十六进制字符串进行URL编码,最后在特定位置插入宽字节前缀。这样WAF看到的是经过URL编码的十六进制字符串,完全没有SQL关键字的特征,而到了后端,经过URL解码和GBK字符集处理后,注入代码被精确还原。

下面是一个具体的代码示例,展示一个存在漏洞的PHP后端如何处理Cookie:

// 漏洞代码示例
$user_id = $_COOKIE['user_id'];
$sql = "SELECT * FROM users WHERE id = $user_id";
$result = mysql_query($sql);

攻击者可以修改Cookie中的user_id值为:1 UNION SELECT username,password FROM admin。由于Cookie中不能直接包含空格和特殊字符,攻击者将其URL编码后通过Burp Suite或浏览器开发者工具发送。如果后端没有过滤,这个注入就能直接执行。更隐蔽的做法是使用十六进制编码,将整个UNION SELECT语句转换为十六进制,MySQL会直接解析执行。

宽字节注入在Cookie中的具体利用

宽字节注入在Cookie场景下的利用需要满足两个条件:后端使用GBK编码,且对Cookie值进行了addslashes或类似的转义处理。攻击者构造的Cookie值类似:%df%27 OR 1=1 %23。经过URL解码后得到0xdf加上单引号,addslashes会在单引号前加反斜杠,变成0xdf、0x5c、0x27三个字节。GBK编码下0xdf5c对应汉字"運",0x27作为单引号独立存在,成功闭合了SQL语句中的引号。

这种攻击的威力在于,很多使用了addslashes函数的旧系统,开发人员以为只要转义了单引号就万事大吉,完全不知道宽字节能让转义失效。而且Cookie注入点往往不像URL参数那样经过严格的输入校验,攻击成功率更高。

Base64编码的Cookie注入

不少Web应用会将Cookie值进行Base64编码存储,后端读取时先解码再使用。开发人员容易产生一种错觉:Base64编码过的数据是安全的,因为它看起来像乱码。实际上Base64只是一种编码方式,不是加密,攻击者可以轻易地将恶意载荷进行Base64编码后替换原始Cookie值。如果后端在Base64解码后直接拼接SQL语句,注入就能成功。更危险的是,有些系统为了兼容性,会在Base64解码后再做一次URL解码,这就给双重编码攻击提供了机会。

一个典型的漏洞代码模式如下:

// 危险:Base64解码后直接拼接SQL
$cookie_value = base64_decode($_COOKIE['data']);
$query = "SELECT * FROM products WHERE category = '$cookie_value'";

攻击者只需将注入载荷进行Base64编码,例如' OR '1'='1的Base64编码是JyBPUiAnMSc9JzE=,然后替换Cookie中的data字段值,就能实现注入。

防御Cookie注入的硬核措施

最根本的防御手段是使用参数化查询和预编译语句,无论数据来自URL参数、表单输入还是Cookie,都不应该直接拼接到SQL语句中。PDO和MySQLi都支持占位符绑定,这是杜绝SQL注入的银弹。如果项目遗留代码无法快速改造,至少要对Cookie值进行严格的类型校验和过滤。对于预期是数字类型的Cookie,使用intval()强制转换;对于字符串类型,使用白名单校验,只允许合法字符通过。

WAF规则方面,不能只检测原始的攻击特征,需要加入对编码后载荷的识别。针对URL编码、双重URL编码、十六进制编码、Unicode编码的常见攻击模式建立规则库。同时要注意解码顺序的问题,确保检测点在最终解码之后。对于使用GBK编码的系统,要么升级到UTF-8,要么在转义时使用mysql_real_escape_string并正确设置连接字符集,它能识别宽字节并正确转义。

另一个重要的防御层是数据库权限最小化。即使注入成功,如果数据库用户只有SELECT权限,攻击者就无法执行INSERT、UPDATE或DROP操作。Web应用连接数据库的账号应该只拥有业务所需的最小权限,避免使用root或sa账号。此外,在代码层面设置Cookie时,启用HttpOnly和Secure属性,虽然这不能直接防御注入,但能降低XSS窃取Cookie后配合注入攻击的风险。

日志监控和异常检测也不可或缺。对Cookie值的长度、字符分布建立基线模型,当出现异常的长Cookie或包含大量编码字符的Cookie时触发告警。SQL注入攻击往往伴随着大量的试探性请求,通过分析请求频率和Cookie值的变化模式,可以在攻击早期发现并阻断。

绕过与防御的持续对抗

安全研究人员不断发现新的绕过技巧。例如利用SQL注释符//替代空格绕过空格过滤,利用浮点数科学计数法如1e0绕过数字校验,利用JSON格式的Cookie值嵌入注入载荷等。防御方需要持续跟踪这些新型攻击手法,更新检测规则。但万变不离其宗,只要坚持参数化查询这一原则,无论攻击者如何变换编码和绕过技巧,都无法撼动数据库安全。Cookie注入的编码与绕过本质上是对输入验证不彻底的惩罚,将安全防线建立在数据来源的不可信假设之上,才是解决问题的根本思路。