「预编译了就 100% 防住注入?」DEF CON 32 议题《SQL Injection Isn’t Dead: Smuggling Queries at the Protocol Level》(Paul Gerste)复现:CVE-2024-27304,pgx(Go 最流行的 PostgreSQL 驱动)协议消息长度溢出——预编译防不住协议层走私。
0x01 场景:预编译下的一切都”没戏”
靶场是 Go + PostgreSQL 登录应用(pgx v5.5.3,受影响版本):
// 用户名密码全部走 placeholder,传统注入语法层面无懈可击
SELECT COUNT(*) FROM users WHERE name = $1 AND password = $2
攻击者想往 users 表插一行(注册个”管理员”),但输入只当成字符串参数——教科书结论”预编译 = 安全”。
0x02 原理:协议层的两条消息路径
PostgreSQL wire protocol 有两种参数化消息序列:
- 扩展模式:先
P(Parse)编译、再B(Bind)传参——参数独立成块,进不了语法; - 简单模式:一条
Q(Query),内容是完整 SQL 文本。
pgx 默认走 P+B,但 Bind 的长度字段是 int32:参数长度构造到总长溢出((1<<32) - adjust_size 填充)时,消息切分器错位,剩余字节被当成一条全新的 Q 消息解析——走私的第二条 SQL 就此”越狱”:
query = b'INSERT INTO users(name, password) VALUES($$attacker$$, $$pass$$)\x00'
query_message = b'Q' + (len(query) + 4).to_bytes(4, 'big') + query
# 用名字参数携带伪造消息 + 4GB 填充顶爆长度字段
malicious = urllib.parse.quote(query_message) + 'A' * ((1<<32) - adjust_size - len(query_message))
payload = f'name={malicious}&password=abc' # POST /login
两个细节:
$$...$$美元引用免去引号转义;adjust_size要卡准”消息头之后剩余字符数”偏移,差一字节就解析不成——三种 exploit 脚本(B 简单版/Q 简单版/Q + NOP sled)的区别就在这。
本质还是混淆数据与代码:长度溢出让解析器把数据段解释成命令消息。
0x03 这类漏洞的通用套路
抽象成:
应用向协议 A 发”结构 + 数据”,数据里嵌套协议 B 的消息——两侧对”边界”判定不一致(长度溢出、转义不彻底、编码歧义)时,数据就能升格为结构。
同议题还有 HTTP 降级走私(CL/TE 差分)。防御共同答案:驱动/协议库保持最新(pgx v5.5.4 修复长度校验)。
0x04 复现要点
- 靶场:PoC 仓库自带 compose(Go webapp + PostgreSQL + init.sql),
docker compose up起; - 4GB payload 发送前先算 URL 编码后长度(会膨胀
%XX),adjust_size按编码后字节校准; - 成功标志:日志/数据库出现
attacker用户行——走私的 INSERT 在预编译应用代码下执行成功。
相关旧文
- SQL 注入深挖、SQL 注入绕过手册
- SSRF 深入利用(同为”协议层思考”:gopher/Redis RESP 也是协议级武器化)