Share
## https://sploitus.com/exploit?id=CF93D2B1-B69E-5458-A2B0-6C8E2B4E3E94
# KindaRails2Shell: CVE-2026-66066

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.

- Affected: Rails 7.2.0โ€“7.2.3.1, 8.0.0โ€“8.0.5, and 8.1.0โ€“8.1.3
- Fixed: Rails 7.2.3.2, 8.0.5.1, and 8.1.3.1
- Runtime: Python 3.11+, standard library only
- Scope: authorized testing and local research

Primary references:
[Rails advisory](https://github.com/rails/rails/security/advisories/GHSA-xr9x-r78c-5hrm),
[Rails forensic analysis](https://github.com/rails/rails-forensics-CVE-2026-66066),
the [Ethiack technical write-up](https://ethiack.com/info-hub/research/kindarails2shell-how-a-matlab-file-reads-your-secrets-and-pops-a-shell-on-ruby-on-rails),
and the [8.1.3.1 release](https://github.com/rails/rails/releases/tag/v8.1.3.1).


## Demo





## Why this PoC is useful

`kr2s.py` contains the complete client in one file:

- an embedded, patchable HDF5/MAT payload with no `h5py` or `numpy`;
- direct-upload delivery and automatic CSRF handling;
- variation-key harvesting from an existing Active Storage thumbnail;
- a strict, standard-library PNG decoder for byte-exact recovery;
- arbitrary offsets and reads up to 16 MiB per request;
- secret discovery and a pure-Python Rails variation signer;
- command-output read-back with a random marker and best-effort cleanup;
- threaded passive or explicitly active scanning with JSON reports;
- proxy, custom path, TLS, timeout, and user-agent controls.

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.

## Quick start

Start the vulnerable lab:

```bash
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:

```bash
# 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:

```bash
cd lab
docker compose down
```

## What the exploit does

1. The client reserves an Active Storage direct upload declared as `image/png`.
2. It uploads a hybrid MATLAB/HDF5 file. The HDF5 dataset uses external storage
   whose filename and offset point to a file in the Rails process.
3. A server-signed variation key is harvested from an existing thumbnail and
   reused with the malicious blob's signed ID.
4. Vulnerable Rails passes the data to libvips `matload`; target-file bytes
   become image pixels and return in a generated PNG.
5. The client validates and decodes the PNG, then reverses matio's
   column-major layout to recover the original bytes.

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`.

## Conditional RCE

`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](https://github.com/rails/rails/security/advisories/GHSA-r4mg-4433-c7g3).
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.

## Read behavior and limits

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.

## Version matrix

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:

```bash
./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:

| 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 |

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 utilities

`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:

```bash
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:

```bash
python3 -m unittest discover -s tests -v
```

## Public-work provenance

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](https://github.com/Zer0SumGam3/CVE-2026-66066-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](https://behradtaher.dev/Hunting-for-Deserialization-Gadgets/).
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](https://ethiack.com/info-hub/research/kindarails2shell-how-a-matlab-file-reads-your-secrets-and-pops-a-shell-on-ruby-on-rails),
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.

## Responsible use and license

Use only on systems you own or are explicitly authorized to test. The authors
accept no liability for misuse. See [SECURITY.md](SECURITY.md) for reporting
issues in this research artifact.

Released under the [MIT License](LICENSE).