
CVE-2026-111497 RCE PoC for one pinned Docker Official Image for WordPress
This source-only PoC reproduces unauthenticated command execution against one pinned Docker Official Image for WordPress.
One pingback.ping request makes WordPress fetch an attacker URL twice during the same PHP request. The first response corrupts a PHP handler through Brotli. The retry header starts Bash and returns the lab flag.
The profile is target-specific. It makes no claim about current WordPress images, other digests, or a memory error in WordPress Core.
WARNING: Run this PoC only in the included lab on a system that you own. It contains a working RCE payload.
/flag.txt, and the flag returns in the XML-RPC response.sh to bash to sleep process chain.| Item | Value |
|---|---|
| Target | docker.io/library/wordpress@sha256:806680ba62232c4c300fb9dcc9f89d08e9e8c2bfc59837a1f58716f8648306d5 |
| Image ID | c64dd89c00079939f18d70489e7c3e30cb071e8581824ab2de688f9f4d70d076 |
| Platform | Linux amd64, Debian 13.7 |
| Application | WordPress 7.1.3, Apache 2.4.69 prefork |
| Runtime | PHP 8.3.36 NTS |
| Decoder | libbrotli1:amd64 1.1.0-2+b7 |
This Linux/amd64 manifest was selected by wordpress:latest when published. The current latest tag no longer selects it.
The lab runs this unmodified Docker Official Image with rootless Podman. It does not inject a Brotli library.
CVE-2026-111497 supplies the decoder corruption. WordPress supplies the unauthenticated fetch path through pingback.ping.
The malformed stream copies six bytes from the live system handler pointer into the strtolower handler. Both pointers use the same ASLR-loaded libphp.so base. Therefore, the profile does not need an absolute address leak.
The profile still depends on the exact Brotli, PHP, glibc, cURL, Requests, Apache, allocator, and request history. The first response uses a 161-byte header and a 23,069,816-byte body. The origin sends 23,069,752 bytes, waits one second, and sends the final 64 bytes.
The decoder returns an error after the corruption. Requests retries without content decoding after CURLE_BAD_CONTENT_ENCODING. Its first header name reaches the modified handler and starts the fixed Bash command.
The application path requires these conditions:
/xmlrpc.php is reachable, and pingback.ping is enabled.The lab creates these conditions without a plugin or victim-side instrumentation.
generate_payloads.py is the authoritative payload source. The repository tracks no prebuilt payload binaries.
The generator uses only the Python standard library. It works offline and does not send traffic or execute commands. It defines the profile constants and serialization stages from RFC 7932.
verify.sh regenerates all fragments. It verifies their sizes, hashes, intermediate bit offsets, and one-bit selector relationship.
The tested host used Ubuntu 24.04.4 under WSL2 on amd64. It used rootless Podman 4.9.3, Bash 5.2.21, and Python 3.12.3.
The minimum requirements are Linux amd64, rootless Podman, Bash, Python 3.8 or later, ASLR mode 2, and standard GNU user tools. Keep port 18170 and subnet 93.184.218.0/24 free, or select alternatives. An enforcing SELinux host can reject the bind mounts.
Before you run the lab, pull the immutable images:
podman pull docker.io/library/wordpress@sha256:806680ba62232c4c300fb9dcc9f89d08e9e8c2bfc59837a1f58716f8648306d5
podman pull docker.io/library/wordpress@sha256:65d5f185a6f539803a177ea9cdd5ad632ae24427bea114447e67a6fcd5d8bfe8
podman pull docker.io/library/mariadb@sha256:33a6653ab666774f48818120045f9367a81cd4c54c027412fe294a7d1274e2dc
Make the scripts executable, then verify the source package:
chmod +x generate_payloads.py verify.sh run.sh
./verify.sh
Run each mode with a new tag:
| Mode | Command | Required result |
|---|---|---|
| Positive | ./run.sh standard | Returns the flag and prints PoC positive: |
| Observable | ./run.sh observable | Also records visible bash and sleep processes |
| Selector | ./run.sh selector | Retries but keeps the flag out of the response |
The selector changes one bit at byte offset 1066. It is a same-image causal control, not a fixed-decoder control.
The optional arguments are the mode, tag, host port, and third network octet:
./run.sh standard trial2 18171 219
The runner writes evidence to runs/<tag>/. The main files are:
VERDICT.txt: result and request countsclient-request.raw and client-response.raw: inbound wire dataorigin-evidence/origin-events.json: both outbound requests and retry headersoracle-match.txt: the flag match in the responsepayloads/identity.json: generated profile metadataMANIFEST.sha256: hashes for all evidence filesThe runner uses an internal Podman network and publishes WordPress only on 127.0.0.1. The client accepts no target address. The origin accepts no command input. The fixed command reads only the synthetic flag.
The runner removes the flag value from the Apache environment before Apache starts. It also creates fresh credentials and removes only resources from the current run.
The response split and process history are exploit inputs. If a run fails, repeat the unchanged profile with a new tag. Do not tune addresses or payload bytes after a failed run.
This package does not establish portability across image digests, PHP builds, architectures, allocators, or response schedules. A different target can stop execution or cause only a crash.
Upstream fixed the maintained Brotli branches in 1.0.10, 1.1.1, and 1.2.1. Distribution vendors can backport the fix. Use the vendor advisory and package changelog, not only the displayed version.