Struts2漏洞系列

前言

春节假期结束,回来不知道该干些什么,就找点事情做。之前没有怎么接触和了解过Struts2这个框架,只知道它好多洞,于是就趁现在学习一下。下面调试的是几个可以rce的漏洞。

各个漏洞详情查看:https://cwiki.apache.org/confluence/display/WW/Security+Bulletins

Struts 2 概述

Struts2是一个基于MVC(Models、View、Controller)设计模式的Web应用框架,它本质上相当于一个servlet,在MVC设计模式中,Struts2作为控制器(Controller)来建立模型与视图的数据交互。Struts 2是Struts的下一代产品,是在 Struts 1和WebWork的技术基础上进行了合并的全新的Struts 2框架。其全新的Struts 2的体系结构与Struts 1的体系结构差别巨大。Struts 2以WebWork为核心,采用拦截器的机制来处理用户的请求,这样的设计也使得业务逻辑控制器能够与Servlet API完全脱离开,所以Struts 2可以理解为WebWork的更新产品。虽然从Struts 1到Struts 2有着太大的变化,但是相对于WebWork,Struts 2的变化很小。

S2-001

环境搭建

环境没有使用现成的而是自己搭建一遍

这里参考了https://mengsec.com/2019/10/29/Java-Web-Struts2-Env-Build/

漏洞简介

WebWork 2.1+ 和 Struts 2 的“altSyntax”功能允许将 OGNL 表达式插入到文本字符串中并进行递归处理。这允许恶意用户通常通过 HTML 文本字段提交一个字符串,其中包含 OGNL 表达式,如果表单验证失败,服务器将执行该表达式。例如,假设我们有一个表单,要求“phoneNumber”字段不能为空:

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

用户可以将“phoneNumber”字段留空以触发验证错误,然后使用 %{1+1} 填充“name”字段。当表单重新显示给用户时,“名称”字段的值将为“2”。原因是默认情况下,值字段被处理为 %{name},并且由于 OGNL 表达式是递归求值的, 因此它的求值就像下面表达式 一样。

%{ %{1+1}}

OGNL解析代码实际上在XWork中,而不是在WebWork 2或Struts 2中。

影响范围

WebWork 2.1(启用 altSyntax)、WebWork 2.2.0 - WebWork 2.2.5、Struts 2.0.0 - Struts 2.0.8

漏洞分析

先跟一下整个流程

在web.xml中配置了/*即全路径的filter,为org.apache.struts2.dispatcher.FilterDispatcher

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

在这个类的doFilter下断点开始分析

根据漏洞描述传入%{1+1}进行跟进分析

image-20240218175131951

org.apache.struts2.dispatcher.FilterDispatcher#doFilter中获取到本次请求的上下文等信息

然后调用this.dispatcher.serviceAction(request, response, servletContext, mapping)

image-20240218175819316

跟进serviceAction()

image-20240219093548722

这里的proxy获取到的是struts.xml中是配置信息,因为mapping中的result属性为空进入了else分支执行proxy.execute()

跟进execute()

image-20240219094354808

调用了this.invocation.invoke()

image-20240219095035747

在invoke()中会遍历struts-default.xml配置的所有的拦截器,struts-default.xml是Struts 2 框架的基础配置文件,为框架提供默认设置,这个文件包含在Struts2-core.jar中,由框架自动加载。

漏洞在ParametersInterceptor中,ParametersInterceptor拦截器会将接收到的请求数据设置到valueStack里,后面在jsp中会从valueStack中获取数据

image-20240219095329367

然后进入ParametersInterceptor中的doIntercept方法

image-20240219095421180

这里获取和设置一些参数等,然后调用this.setParameters()

image-20240219100155264

在这里会通过setValue给值栈中的action参数赋值

遍历完所有的拦截器后往下会调用this.invokeActionOnly()

image-20240219100411157

跟进里面会发现其反射调用了action中的execute方法,这里就不进去看了

往下

检测这个代理的action是否有执行结果,如果没有进入到executeResult()

image-20240219100819832

这里会根据struts.xml中给出的配置,根据返回结果跳转到对应的jsp执行

image-20240219101644974

executeResult()如下

image-20240219101721507

这里调用了execute(),往下

image-20240219103001349

跟进 this.conditionalParse

image-20240219103242784

前面的条件为true,进入到TextParseUtil.translateVariables()

往下跟进

image-20240219103800129

继续跟进

image-20240219104140813

这里就是解析表达式的地方,即漏洞触发的地方。但是现在还没解析到传入的参数%{1+1},先在这下个断点,跟着调试返回到execute(),并跟进doExecute()

image-20240219104412166

image-20240219104755712

这里调用dispatcher.forward(request, response);进行转发到index.jsp

image-20240219105554026

在index.jsp中使用了struts标签

image-20240219110058970

解析标签使用的是org.apache.struts2.views.jsp.ComponentTagSupport这个类

在标签的开始和结束位置,会分别调用对应实现类如org.apache.struts2.views.jsp.ComponentTagSupport 中的 doStartTag()doEndTag() 方法:

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

这里直接快进到解析username的地方,漏洞的触发点,就从 doEndTag() 开始

image-20240219111944946

跟进end(),来到了org.apache.struts2.components.UIBean

image-20240219112041461

跟进调用的evaluateParams()

image-20240219112642836

在这个方法中判断了 altSyntax 是否开启,并调用 findValue() 方法寻找参数值,其实就是通过ogln表达式%{name}来获取值

跟进findValue()

image-20240219112925300

这里调用了刚刚调试到达过的 translateVariables() ,跟进

image-20240219113108788

注意这里的while(true)

stack.findValue()是在值栈中查找username的值,即%{1+1}

往下将得到的%{1+1}当中表达式继续解析

image-20240219114024877

image-20240219114123009

这次跟进findValue()

image-20240219114308593

image-20240219114403385

看到value = OgnlUtil.getValue(expr, this.context, this.root, asType); 这里就解析了表达式

整个漏洞触发过程就是酱紫

总的来说就是:

用户通过使用 %{} 包裹恶意表达式的方式,将参数传递给应用程序,应用程序由于处理逻辑失误while(true),导致了二次解析,造成了漏洞

S2-003

漏洞简介

OGNL(Object-Graph Navigation Language)提供了广泛的表达式评估功能,其中包括(http://www.ognl.org/2.6.9/Documentation/html/LanguageGuide/expressionEvaluation.html)。该漏洞允许恶意用户绕过ParametersInterceptor内置的“#”使用保护,从而能够操纵服务器端上下文对象。

例如,要将#session.user设置为’0wn3d’,可以使用以下参数名称:

(’\u0023’ + ‘session’user’’)(unused)=0wn3d

在进行URL编码后,将如下所示:

(’\u0023’%20%2b%20’session’user’’)(unused)=0wn3d

影响范围

Struts 2.0.0 - Struts 2.1.8.1

环境搭建

搭个der,直接用S2-001的进行分析

漏洞分析

根据官方漏洞介绍,漏洞出现在com.opensymphony.xwork2.interceptor.ParametersInterceptor中,并且是在对参数名称的处理中出的问题

com.opensymphony.xwork2.interceptor.ParametersInterceptordoIntercept方法下断点分析

image-20240219164246126

doIntercept方法中获取到请求中的参数和对应的值栈,然后调用setParameters,根进

image-20240219164918865

这个方法中会将参数Parameters进行遍历,然后将name传入acceptableName检查其合法性

image-20240219165141304

大概的意思是查name中是否包含=,#: 这几个字符,还有isExcluded()检测是不是被排除的name

protected boolean isExcluded(String paramName) {
    if (!this.excludeParams.isEmpty()) {
        Iterator i$ = this.excludeParams.iterator();

        while(i$.hasNext()) {
            Pattern pattern = (Pattern)i$.next();
            Matcher matcher = pattern.matcher(paramName);
            if (matcher.matches()) {
                return true;
            }
        }
    }

    return false;
}

image-20240219165757665

然后返回true ,跳出循环

执行到getValue方法,获取name对应的值,然后通过调用setValue(),对值栈中的参数赋值

image-20240219170248586

在setValue中将name当作表达式进行解析了,如果能控制name就能进行OGNL注入了

image-20240219170712654

这个name是参数名称,可以通过请求来构造即可

这个是2.0.8版本的,在高版本中这个过滤条件使用的是正则

image-20240219174835784

image-20240219175113276

这个this.acceptedPattern赋值是

image-20240219175148902

image-20240219175204520

payload需要绕过这个正则匹配 ,如上面漏洞简介所说的,通过unicode编码(u0023)或8进制(43)即可绕过安全限制

Tomcat-7.0.78本身限定了特殊字符;要验证此漏洞,需要安装Tomcat 6.0

加载中…