“预编译了是不是就 100% 防住了 SQL 注入?“——课程开篇就用这个反问引出了 DEF CON 32 的议题《SQL Injection Isn’t Dead: Smuggling Queries at the Protocol Level》(Paul Gerste)。这篇记录其中的代表漏洞 CVE-2024-27304:Go 生态最流行的 PostgreSQL 驱动 pgx 的协议消息长度溢出,附完整靶场复现。
0x01 场景:预编译下的一切都”没戏”
靶场是个 Go + PostgreSQL 的登录应用(pgx v5.5.3,受影响版本):
// 用户名密码全部走 placeholder,传统注入语法层面无懈可击
SELECT COUNT(*) FROM users WHERE name = $1 AND password = $2
攻击者试图向 users 表恶意插一行记录(注册个”管理员”),但输入只会被当成字符串参数——教科书结论是”预编译 = 安全”。
0x02 原理:协议层的两条消息路径
PostgreSQL 前后端协议(wire protocol)里,执行一条参数化 SQL 有两种消息序列:
- 扩展模式:先发
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
注意两个细节:
- PostgreSQL 的
$$...$$美元引用避免了引号转义问题; 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 用 HTTP 发送前要先算好 URL 编码后的长度(编码会膨胀
%XX),adjust_size要按编码后的消息字节数校准; - 成功标志:靶场日志/数据库里出现
attacker用户行——走私的 INSERT 在预编译”毫发无损”的应用代码下执行成功。
相关旧文
- SQL 注入课堂深挖、SQL 注入绕过手册
- SSRF 深入利用(同为”协议层思考”:gopher/Redis RESP 也是协议级武器化)