Security News

Cybersecurity news aggregator

🔓
CRITICAL Vulnerabilities Reddit r/netsec

Full Rails RCE technical writeup... KindaRails2Shell: How a MATLAB file reads your secrets and pops a shell on Ruby on Rails | Ethiack

This vulnerability, CVE-2026-66066 (KindaRails2Shell), is an arbitrary file read to remote code execution chain in Ruby on Rails applications using the default libvips image processor. The attack vector involves uploading a malicious MATLAB (.mat) file, which libvips parses, exploiting HDF5 external dataset functionality to read arbitrary files and ultimately achieve RCE. The article notes that applications configured to use ImageMagick instead of libvips are not affected by this specific vector.
Read Full Article →

A technical deep-dive into an arbitrary file read to RCE chain in Ruby on Rails applications. Ruby on Rails runs a large slice of the modern web: GitHub, Shopify, Basecamp, and a very long tail of self-hosted products and open-source projects. When we published the initial advisory for KindaRails2Shell (CVE-2026-66066), we withheld the technical details. Some PoCs are already landing, and Rails just published a repository with technical details and forensic tooling, so we are now also publishing our analysis: the root cause, the file-format trick that makes it work, the ActiveStorage behaviors that route attacker bytes into it, and the read primitive that turns a thumbnail into an oracle for any file, and then into remote code execution. This is also the story of how we found it. Inspired by wp2shell , we were seeking a Rails2Shell and we started from an old idea from previous research: libvips, used by Ruby on Rails by default, loads a lot of weird image formats, including Matlab files. Here’s an overview of the chain: a single malformed upload escalates from arbitrary file read to remote code execution as root: five steps, no authentication required Ruby on Rails processes far more than regular images If a Rails app lets a user upload an avatar and shows a thumbnail back, it’s almost certainly running libvips . Since Rails 7, the default ActiveStorage variant processor is vips, and the official Rails Docker images do apt install libvips on a Debian base by default. Alternatively, Rails may be configured to use ImageMagick instead, which is not affected by this specific vector. The thing is how many formats libvips can open, and it's not just regular images (PNG/JPEG/WEBP/GIF). Depending on how it was built, libvips will also load PDFs (via poppler), SVG (via librsvg), FITS astronomy images, NIfTI medical scans, OpenSlide pathology slides, Radiance HDR, and MATLAB .mat files. Libvips picks its loader by sniffing the bytes , and not by trusting a filename or a declared MIME type. If Vips::Image.new_from_file processes some bytes, it will run each loader's detector until one says "hey, that's mine." However, libvips flags a bunch of formats as untrusted , which is a good security control - one that ActiveStorage did not use by default. So we pointed Claude Opus 4.8 at these loaders to dig deep on arbitrary file read (AFR) and arbitrary file write (AFW) primitives (btw for more information on some neat techniques on AFWs, check out our upcoming DEFCON Bug Bounty Village talk : Write Once, Shell Everywhere: Turning Arbitrary File Writes into RCE). The root cause: HDF5 external datasets This is the behavior that makes everything else work. A modern MATLAB file, MAT v7.3 , is not a specific format at all. It is an HDF5 file, and HDF5 has a feature called external storage . When you create an HDF5 dataset, you can tell that the dataset's raw bytes don't live inside the file, they live in another file, at an arbitrary path, at some offset . This is a known, legitimate HDF5 feature (H5Pset_external) meant for datasets too big to inline. The dataset's creation property list carries a list of (filename, offset, size) entries, and when you read the dataset, HDF5 transparently opens those files and reads from them. Libmatio, the Matlab library used by Libvips, reads MAT v7.3 variable data with a H5Dread call. In src/mat73.c: The only storage validation libmatio does is a check for chunked datasets (to detect truncation). It never calls H5Pget_external_count() , so it never checks if a dataset comes from an arbitrary file like /etc/passwd. H5Dread opens the external file, and copies its bytes into the variable's data buffer. That's the primitive: any application that opens an attacker-supplied .mat, and then reads a variable, performs an intended arbitrary file read. It's possible to craft one in a few lines of Python with h5py: This class of bug, HDF5 external storage as an intended arbitrary file read primitive, has surfaced recently in ML model loaders (Keras and TensorFlow both had variants). We found no prior report of it in libmatio, whose existing CVEs are all memory-corruption and DoS, and, at the same time, this could even be intended behavior, given the Matlab format. But a file read in a C library almost nobody calls directly isn't that interesting. The question is: can we trigger it from a web request? The MATLAB 5.0 / 7.3 confusion Here's the first issue, a detail that got us mindblown. Libvips decides "this is a MAT file, send it to libmatio" using a content sniffer, vips__mat_ismat. We tested every version string, and it turns out that sniffer only accepts files whose header begins with the exact literal ASCII string MATLAB 5.0 . MATLAB 7.3, MATLAB 7.0, MATLAB 6.0 are all rejected.But our arbitrary file read primitive needs the file to be HDF5 (MAT v7.3), because external datasets are an HDF5 feature. MAT v5 is an entirely different, non-HDF5 binary format with no such capability. So we have a disagreemen...

Share this article