目录

S2-001

S2-001

漏洞介绍

Struts2对OGNL表达式解析使用了开源组件 opensymphony.xwork 组件,OGNL解析代码实际上位于XWork中,而不是 WebWork2 或Struts2 中

该漏洞是由于WebWork 2.1+ 和 Struts 2 的"altSyntax"特性引起的。altSyntax特性允许将OGNL表达式插入文本字符串并进行递归处理。这允许恶意用户通过HTML文本字段提交一个包含OGNL表达式的字符串,如果表单验证失败,服务器将执行该表达式。

影响版本

WebWork 2.1 (with altSyntax enabled), WebWork 2.2.0 - WebWork 2.2.5, Struts 2.0.0 - Struts 2.0.8

环境搭建

这里学习 nivia 师傅的环境搭建方法,下载 vulhub 靶场,然后参考 war 包运行:Tomcat 服务搭建

但是这个教程只是能运行 war 包,还不能调试,如果直接搜索调试的话是类似 IDEA 调试 jar 包这个这种调试,没什么用,这里可以直接把解压后的文件配置依次复制过来,然后选择部署 facet,这样就行了。

还可以直接配置远程调试。

漏洞复现

访问漏洞环境,

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250123015406818.png

POST:username=%{1+1}&password=1

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250123015632125.png

漏洞分析

调用栈,

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250123020824146.png

在相应的漏洞版本下,核心Filter为org.apache.struts2.dispatcher.FilterDispatcher,从web.xml中也可以看出

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250123021448146.png

将 Action 类的相关属性放到 ValueStack

那么在 org.apache.struts2.dispatcher.FilterDispatcher#dofilter 处下断点,先获得 Actionmapping,

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250123022112925.png https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250123022211815.png

然后 mapping 不为 null,调用 dispatcher#serviceAction 方法,

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250123022230386.png

该方法始化extraContext,封装了session、HttpServletRequest、HttpServletResponse等信息。获取ValueStack,调试时为null。

初始化ActionProxy。proxy.getInvocation().getStack() 初始化 ValueStack,通过 setAttribute 方法封装进 request

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250123022321798.png

调用 ActionProxy#execute,这里对应会调用到 ActionInvocation#invoke,这里开始执行拦截器

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250123023110229.png

调用拦截链的拦截器

其中到 ParametersInterceptor 拦截器时跟进 doIntercept 方法,是获取对应Action,获取ActionInvocationparameters,parameters对应请求中的参数。由于parameters不为null,这里会获取StackValue,调用setParameters方法

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250123023303212.png

ParametersInterceptor#setParameters 方法中,会遍历这个 parameters,将键值对存进Stack中,对应 OgnlValueStack#setValue 方法,这个过滤器就是单纯存个参数值,(这里也可以注意一下就是后面的 s2-003 漏洞)

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250123023634949.png

然后调用到 DefaultWorkflowInterceptor#doIntercept 方法,在这个方法最后调用了ActionInvocation#invoke

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250123024345665.png

调用完所有拦截器后调用 Action 方法

跟进然后一路调用到了 invokeActionOnly() 方法,这里开始调用对应Action#execute方法了,

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250123024420042.png https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250123024852691.png

来到了 LoginAction#execute,也就是自己写的逻辑,这里会执行失败返回 error

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250123024651206.png

表单执行失败处理

看到 struts.xml 配置中如果返回 error 就会回到 index.jsp 页面

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250123024809915.png

返回结果以后就开始调用 ActionInvocation#executeResult 方法,调用Result实现类里的execute方法开始处理请求结果,

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250123025053837.png

然后一路调用,处理结果

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250123025926663.png

如果返回结果是一个 jsp 文件,则会调用 JspServlet 来处理请求,调用栈如下(用一下nivia师傅的图)

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250123030114775.png

最后交由 Struts 来处理解析相关的标签,

例如在解析Struts标签如<s:textfield name="username" label="username" />,在标签的开始和结束位置,会分别调用对应实现类中的doStartTagdoEndTag方法:

  • doStartTag:获取一些组件信息和属性赋值,总之是些初始化的工作
  • doEndTag:在标签解析结束后需要做的事,如调用组件的end

这里会调用到ComponentTagSupport这个实现类,漏洞触发点位于ComponentTagSupport#doEndTag

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250123030258565.png https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250123030422294.png

当解析 <s:textfield name="username" label="username" />doEndTag 方法会getBean方法获取到TextField对象赋值给 component,调用其end方法

跟进 evaluateParams 方法,判断是否开启altSyntax,若开启会套上一层%{}

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250123030602987.png

跟进 findValue 方法,满足条件调用 TextParseUtil.translateVariables 方法

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250123030721766.png

public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, ParsedValueEvaluator evaluator) {
    Object result = expression;

    while (true) {
        int start = expression.indexOf(open + "{");
        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);
            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;
            }
        } else {
            break;
        }
    }
    return XWorkConverter.getInstance().convertValue(stack.getContext(), result, asType);
}

方法使用while true的方式递归执行ognl表达式,执行逻辑位于 Object o = stack.findValue(var, asType);,解析过后将解析结果替换回原来的表达式中,继续第一步,直到不出现 %{}

比如第一次 %{username}

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250123032505859.png

解析得到,

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250123032539697.png

然后回替换回原来表达式,因为还有 %{},所以会继续解析,造成二次解析,再次调用 stack.findValue(var, asType) 方法,

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250123032806256.png

最后执行命令。