
Private Fortbridge PoC for the CVE-2026-32740 Next.js/sharp leak-to-memcpy-GOT RCE chain
Working proof of concept that turns CVE-2026-32740 in the
hardcoded Next.js/sharp image stack into a chosen-address write and a
validated /usr/bin/id callback.
This is a private Fortbridge research repository. Use it only against the included lab or another system you are explicitly authorised to test.
The exploit uses only the target's HTTP upload and image-optimization routes:
g_module_open_full, a GModule
routine that wraps dlopen to load a native shared library from a file
path. The exploit calculates its runtime address as the recovered libvips
base plus the hardcoded profile offset 0x3e995e. Loading the library runs
its constructor./usr/bin/id. It sends that ELF through the public
upload route under the image name x.jpg and media type image/jpeg.memcpy@GOT - 16.uploads/x.jpg immediately before the
GOT slot and replaces memcpy@GOT with the derived GModule loader. The next
row calls that loader with the library path already in RDI./usr/bin/id output over TCP. A per-attempt token prevents a stale callback
from being counted as success; the returned uid=... line is the proof.The original libvips-relative validation runs are recorded in
evidence/libvips-gmodule-rce-10x.json.
All ten fresh processes returned valid /usr/bin/id output with ten distinct
randomized libvips bases and ten independently derived loader addresses.
The exact Ubuntu run data are in
evidence/libvips-gmodule-pie-rce-10x.json.
The exact Debian 13 APT run data are in
evidence/debian13-apt-libvips-gmodule-rce-10x.json.
We tested the complete exploit against 10 newly started Ubuntu Node processes
and 10 newly started Debian Node processes. All 20 runs reached command
execution and returned the target's /usr/bin/id output. The exploit also
calculated a different GModule-loader address for every randomized libvips
base.
Before creating the final payload, the exploit may need several ASLR-leak attempts. Each attempt uploads the leak AVIF, sends it through the optimisation route, and checks the returned PNG for four pointers that identify one profile and one libvips base. Ubuntu needed between 3 and 23 attempts. Debian needed between 3 and 15. Incomplete or ambiguous evidence causes another attempt instead of a guessed profile.
The project also has 96 automated regression tests. These are code-level checks, not 96 additional exploit runs. They cover profile validation, returned-pointer classification, heap calibration, address calculation, payload construction, and safe failure when evidence does not match a supported target.
Both x86-64 targets use Next.js 15.5.23, sharp 0.34.4, bundled libvips 8.17.2, bundled libheif 1.20.2, ASLR, and NX. Their native runtimes differ:
| Profile | Node | glibc | libstdc++ | Selector relation |
|---|---|---|---|---|
| Ubuntu | 25.8.1, PIE ET_DYN | 2.43-2ubuntu2.4 | 6.0.35 | 0x6000 - 0x690 = 0x5970 |
| Debian 13 | Debian APT 20.19.2, PIE ET_DYN | 2.41-12+deb13u4 | 6.0.33 | 0x6000 - 0x3a0 = 0x5c60 |
The complete build IDs and SHA-256 values are in
profiles/native_stack_profiles_pie.json.
The Debian package, artifact, route-smoke, and five-lifetime layout measurements
are captured in
evidence/debian13-profile-derivation.json.
The Debian target uses the standard distribution package
nodejs=20.19.2+dfsg-1+deb13u3; Node is not compiled from source.
The chain does not need the randomized Node base. Its control target is the
internal g_module_open_full routine inside libvips, whose randomized base is
recovered from returned pixels. Each versioned profile hardcodes the
build-specific constants required by the exploit:
memcpy@GOT offset and the g_module_open_full offset plus its validating
instruction bytes;The randomized libvips base, the resulting runtime loader address, and the final two-byte selector are not hardcoded. They are derived for each target process from returned pixels and the selected profile.
The exploit fails closed when a profile is malformed or returned pixels do not select exactly one supported profile/base pair. The stock profile also requires its complete returned heap record. A profile is an exact compatibility claim, so the operator should verify the target artifacts offline before using it.
The loader ABI is important. The overwritten call supplies the library path in RDI, an image-row pointer in RSI, and the 58-byte copy length in RDX. This exact profiled GModule routine uses only supported flag bits from ESI and does not dereference RDX on the successful load path. An offline harness validated that entry point and instruction signature before it was used in the measured runs.
ffmpeg with the libaom-av1 encodercc on the attacker machineInstall the only Python dependency:
python3 -m venv .venv
. .venv/bin/activate
python3 -m pip install -r requirements.txt
Confirm AV1 encoding support:
ffmpeg -hide_banner -encoders | grep libaom-av1
The lab intentionally accepts arbitrary uploads and passes selected files to sharp. Do not expose it to an untrusted network.
cd lab
npm ci
npm run build
npm run start
The target is then available at http://127.0.0.1:3000.
To build an Ubuntu lab image around the Node artifact used by the first PIE profile:
./scripts/build_ubuntu2604_container.sh
docker run --rm --network host \
fortbridge/libheif-grid-nextjs-rce:ubuntu2604
The digest-locked Ubuntu 26.04 image builds the exact Node 25.8.1 PIE artifact recorded in the profile, verifies its SHA-256, then copies it into a clean runtime stage. The source build is required because Node's official Linux binary is ET_EXEC, while this profile intentionally tests a PIE executable.
To reproduce the Debian target with Debian's supported APT package:
./scripts/build_debian13_container.sh
docker run --rm --network host \
fortbridge/libheif-grid-nextjs-rce:debian13