本文将从struts2漏洞出发,研究struts2安全机制从无到有的过程,研究漏洞发生的原因以及修复的方式。
为了不让本文过于冗长,本文将适度对一些细节有所删减,具体详情请自行查找相关文档。
这个版本的struts2没有安全机制,在提交OGNL表达式后,进行了递归查询,导致OGNL表达式的执行。
问题出在TextParseUtil类的translateVariables方法。这个方法是用来做转换对象操作的。
官方给出的修复方案是将altSyntax默认关闭,使用break打断递归查询。
那么这里主要说一下altSyntax,这个功能是将标签内的内容当作OGNL表达式解析,关闭了之后标签内的内容就不会当作OGNL表达式解析了。
S2-003是一个参数名注入,所以我们要重点关注一下ParametersInterceptor拦截器,在XWork2.0.4官方重写了acceptableName方法,增加了敏感字符检测,如 “,” ”#” “:”,如果acceptableName方法检测到了这些字符,则返回false。也就是说安全检测没有通过,则表达式不会执行。
我们注意到网上的POC通过unicode编码绕过了敏感字符的检测。官方针对编码绕过的修复在新版本的struts2中,增加了更为严格的正则表达式检测字符可能出现的问题。
S2-005是一个参数名的注入,官方在这个版本增加了沙箱,并且重写了参数拦截器的正则表达式,但是它忽略了我们依然可以通过OGNL上下文来访问静态方法从而达到任意代码执行。
相关上下文如下:
#context 一个基于'xwork.MethodAccessor.denyMethodExecution'属性值的保护方法执行
#_memberAccess 禁止静态方法执行
#root
#this
#_typeResolver
#_classResolver
#_traceEvaluations
#_lastEvaluation
#_keepLastEvaluation
我们需要将denyMethodExecution的属性设置为false,这样才可以执行方法,通过#_memberAccess上下文设置allowStaticMethodAccess为true
官方的修复是在参数拦截器修改了更为严格的正则表达式
S2-008是参数值的注入,但是需要开启devmode模式,对于debug引起的问题我们需要关注DebuggingInterceptor拦截器,而且沙箱没有改变,官方对于该漏洞的修复也是修改了acceptedParamNames过滤器的ParameterInterceptor和CookieInterceptor的正则表达式。
S2-007是标签内的回显,是因为变量类型报错导致OGNL表达式的执行,同样的沙箱也没有变化。针对该漏洞的修复也是增加了一个方法,对单引号进行转义处理。
与S2-003和S2-005不同的是S2-009是参数值注入,绕过了ParameterInterceptor的限制,安全沙箱没有变化,因为之前官方在针对参数过滤器的修复的时候,只是考虑到了参数名,没有考虑到参数值的安全性,那么这次官方的修复主要有两点,其中之一是在值栈中setParameter方法将不允许参数名称中包含更多的eval表达式,然后又再次重写了正则表达式,那么在struts2 2.3.1.2中 ParameterInterceptor拦截器的正则表达式变得更为严格,同样的,也增加了 “排除参数“这样一个HashMap。
S2-012该漏洞是在struts.xml对action对象做了一个重定向配置,重定向配置的参数以OGNL表达式解析造成OGNL二次注入导致任意代码执行,安全沙箱没有改进。官方的修复拒绝ognlutil类的eval表达式。
S2-013该漏洞跟标签有关,标签属性设置为includeParams=all 会造成OGNL表达式执行,在S2-014后URL将不会把参数名或值传递给OGNL表达式。同样的,沙箱也发生了变化,在Xwork 2.3.14.2 版本,修改了沙箱SecurityMemeberAccess类,删除了setAllowStaticMethodAccess方法,导致无法修改AllowStaticMethodAccess属性值,同时AllowStaticMethodAccess属性被final修饰。
S2-015这个漏洞的POC很有意思,我们失去了setAllowStaticMethodAccess方法导致无法设置AllowStaticMethodAccess属性值从而无法执行静态方法,我们来看看S2-015,这个漏洞是通配符任意映射导致OGNL表达式执行的问题。
这个点是没有检查白名单的,当使用%或$的时候,会在translateVariables方法进行OGNL表达式的二次执行
因为setAllowStaticMethodAccess方法被删掉,但是ognlcontext的上下文属性还在,所以可以通过_memberAccess去获取AllowStaticMethodAccess的值并通过setAccessible方法设置为true,从而达到任意代码执行的目的。
官方针对这个漏洞在DefaultActionMapper类增加了一个cleanupActionName方法去处理actionname的恶意代码,并返回安全的actionname。
这是拦截成功的
S2-016漏洞的主要原因是DefaultActionMapper类支持 “action:”,“redirect:”或“redirectAction:”三种参数,参数后的的信息未得到正确清理,导致OGNL表达式执行,官方的修复是修改了DefaultActionMapper类,删掉了 “redirect:”,“/redirectAction:”保留了method:和action:同时利用cleanupActionName方法进行了过滤。
S2-019漏洞的问题是在Struts 2.0.0 - Struts 2.3.15.1版本的struts.xml开启了动态方法调用
官方的修复是在下一版本将动态方法调用默认为false。
S2-029漏洞的问题是OGNL表达式二次执行,对应struts2版本为Struts 2.0.0 - Struts 2.3.24.1(2.3.20.3除外)官方对这个漏洞的简介是会对分配给某些标记的属性值进行二次执行,因此可以传入一个值,该值在rendered tags attribute会再次执行。
我们可以观察一下struts2是如何处理html的id标签,在看UIBean类之前,先讲一下OGNL二次执行的点,首先是这样的。
我在example.jsp的代码是这样的,接收参数的值用${value}包含了起来,就像这样。
那么我存进去的表达式,就会产生OGNL表达式执行,再看看后端的处理方式。
findString方法执行了一次表达式,随后findStringIfAltSyntax又解析了一次表达式
只要html标签的id值可控,就可以任意代码执行,name属性的值同样。
在struts2 2.3.24版本后官方增加了一些黑名单类列表以及一些包名,在struts-default.xml。
安全管理器也增加了包检查器,类检查器和成员类检查器,关于这些检查器的实现,都是从定义好的排除类名迭代循环匹配
结合POC之所以能够执行命令,是因为OGNL上下文一个安全检查方法的逻辑判断出错了,导致现有安全机制失效。关注一下ObjectPropertyAccessor类的setPossibleProperty方法,这个方法是对表达式完成赋值操作的。
我们主要关注其中一个分支判断
跟进setMethodValue方法
context.getMemberAccess().isAccessible(context, target, m, propertyName)
这个表达式返回true则代表安全检查通过,返回false则代表安全检查没有通过。
这个地方的表达式逻辑写错了,取反则意味着当安全检查没有通过的时候返回值为true。
为true则意味着result 被赋值为 false,继续跟进则发现result返回值为false。
当result返回false的时候,我们表达式为true,绕过了沙箱。
当bypass这个沙箱之后,我们只需要使allowStaticMethodAccess为true 允许执行静态方法。excludedPackageNamePatterns为空集合,允许调用相关的包。excludedClasses为空集合,允许调用任意类。即可完成任意代码执行
S2-032漏洞是一个历史遗留问题,官方给出的是当启用动态方法调用时候,利用method:参数进行远程执行代码。
要求
受影响的版本Struts 2.3.20 - Struts Struts 2.3.28(2.3.20.3和2.3.24.3除外)。
这是因为S2-016的时候,保留了两个参数,分别是method:和 action:
又没有做好过滤导致的。
我们在S2-032漏洞可以看到POC发生了变化,在绕沙箱的语句由一大串变成#_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS,这一句在之前的版本也适用,直接通过DEFAULT_MEMBER_ACCESS覆盖掉SecurityMemeberAccess类,因为SecurityMemeberAccess类有一堆的安全限制属性,想要绕过的话,需要利用ognlcontext的上下文,那么一定会有疑问,为什么#_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS可以绕过安全沙箱呢?
答案非常简单,我们可以看到在ognlcontext类 DEFAULT_MEMBER_ACCESS其实是DefaultMemberAccess的一个实例,随后我们可以注意到一个比较关键的点。
_memberAccess上下文是取值于DEFAULT_MEMBER_ACCESS实例,而DefaultMemberAccess又是SecurityMemeberAccess的父类,所以我们在DefaultMemberAccess的isAccessible方法上下断点
可以看到result返回的值是true,在分支判断语句的地方可以看出有一个取反的操作,所以不会执行分支下面的判断逻辑,也就不会取安全属性的值从而绕过了安全沙箱,DefaultMemeberAccess类的作用是用于设置和获取非public字段的访问程序,以允许访问private, protected, package protected的类成员。
官方修复禁用动态调用方法属性,以及将method:参数做了过滤
S2-033漏洞的问题是REST插件启用了动态方法调用导致的远程代码执行漏洞。并且在setMethod的时候没有做过滤。
“enableEvalexpression的值在showcase找不到,如果有小伙伴找到了可以告诉我,感谢”
同时也与一个关键的属性有关enableEvalexpression,这个属性为true则允许执行表达式,为false则不允许执行ognl表达式,那么struts2官方针对这个漏洞进行的修复方式则是将enableEvalexpression属性设置为false,禁止执行ognl表达式,我们可以根据
这个方法来判断是否能够执行ognl表达式,在struts2 2.3.28版本中enableEvalexpression表达式为true。
可以看到这个分支判断做了一个取反的操作,没有抛出这个错误,所以ognl表达式顺利执行。Btw,在进一步跟进的时候,发现程序在判断语句是否是evalchain的时候,一直返回null,而在修复后的版本,则准确的判断出了evalchain,这个条件语句是进行了两个表达式判断,需要两个表达式均为true才会报错。
最终在invokeAction方法处通过getValue执行表达式。
官方的修复方法则是简单粗暴,将enableEvalexpression表达式设置为false,来看看修复后的版本的处理方式。那么在之后的版本修复则是用cleanactionname方法将actionMethod进行了一个过滤。
这个点也是没搞懂的,反正就抛出错误了。
S2-037这个漏洞也是REST插件造成的问题,不过不需要开启动态方法执行,而且绕过了S2-033的安全机制,绕过的方法是使用三元运算符构造。出问题的类依然是RestActionMapper。
关于这个漏洞会有一个问题,为什么不需要开启动态方法执行也可以执行远程代码。
可以看到这里执行完handleDynamicMethodInvocation方法后在下面有一个
mapping.setMethod,同时没有判断if (allowDynamicMethodCalls)
官方的修复方式是使用cleanactionname方法过滤了actionmethod
S2-045漏洞产生的原因是Jakarta Multipart解析器执行文件上传时造成的远程代码执行,jakarta是struts2的默认解析器。
在包装请求方法中可以看到只有content-type不为null且值为multipart/form-data的时候才会是一个上传请求。
漏洞产生的类还是JakartaMultiPartRequest类,当content-type报错的时候会执行ognl表达式,看看为什么会这样。
然后会看到,在数据处理出错的时候,会走处理错误流程,catch流程,我们可以看到在catch流程调用了buildErrorMessage方法。看看buildErrorMessage方法的实现。
findtext方法执行ognl表达式,跟入
然后我们进入getDefaultMessage方法看看它的内部实现。
报错信息被转换成对象然后调用translateVariables方法的evaluate方法去执行ognl表达式
官方修复的方式是
去掉了e.getmessage方法。
S2-048漏洞问题出现在struts2-struts1-plugin-2.3.32.jar 插件,这个插件的作用是可以让struts2能够兼容struts1的代码
getText方法主要是实现struts的国际化的一个方法,比如说邮件,可能发给多个客户,每个客户的语种不一样,不可能针对每一个语种做一个模版,所以就有了getText方法。
入口点在struts1action的execute方法,struts1action的execute方法是调用savegangsteraction类的execute方法实现的。
这里直接取了原始值,导致了漏洞的产生
execute执行完后,到达第一个sink点,getText取值用户可控
代码最终进入getdefaultmessage的translateVariables方法执行ognl表达式
官方的修复是建议使用资源建传值。
S2-052漏洞是Struts2 REST插件的XStream组件存在反序列化漏洞,使用XStream组件对XML格式的数据包进行反序列化操作时,未对数据内容进行有效验证,存在安全隐患,可被远程攻击。
查看struts2-rest-plugin-2.3.33.jar的struts-plugin.xml发现拦截器ContentTypeInterceptor。
很多不同类型的数据都交给不同的Handler进行处理,我们注意到这里xml的数据是交给XStreamHandler处理的。
contenttypehandler的作用是处理与特定内容类型的对象之间的内容传输。
根据这个漏洞,我们主要查看一下xstreamhandler类,这类主要是是将xml转换成对象,对象转换成xml的,也就是比较专业的词汇叫“marshal“和”“unmarshal”。
我们分别在contenttypeinterceptor拦截器和xstreamhandler处下断点。
它会先获取content-type,这也是我们的poc中要修改content-type为application/xml才可以执行代码的原因,因为只有设置为application/xml,你的数据流才会到xstreamhandler处执行。
这里其实是有一个分发的操作,根据你的content-type类型分发给不同的handler进行处理,你设置application/xml就是分给xstreamhandler处理。
这里是调用fromxml 进行一个对象转换。
关于“marshal“和”“unmarshal” 参考 https://github.com/mbechler/marshalsec/blob/master/marshalsec.pdf
官方的修复引入了一些接口,接口为每个操作类定义类限制。
org.apache.struts2.rest.handler.AllowedClasses
org.apache.struts2.rest.handler.AllowedClassNames
org.apache.struts2.rest.handler.XStreamPermissionProvider
以及升级struts2,我们会看到增加了一些限制类
针对xml序列化做了限制
S2-057这个漏洞的产生原因是alwaysSelectFullNamespace为true导致的,
没什么有意思的地方,唯一有意思的是POC的构造,在利用045的POC测试的时候,会发现这个问题。
你是取不到值栈的,这是为什么呢?
首先是045的取值栈方法,在045包括之前的时候我们在ognlcontext类有几个字段
这就是为什么之前的漏洞poc都会有_memberAccess这样的表示标识存在,_memberAccess这个是一个安全沙箱,关于poc的讲解我会在文章里详细写明,包括置空三个属性的值,这样就可以执行任意代码了,调用任意包了等等
它可以利用ognlcontext的硬编码直接访问上下文,但是在2.3.34版本删除了这三个属性
那么在使用045的poc的时候,会发现值栈返回为空,这是因为无法使用#context,#_memberAccess去获取访问对象了
使用requests域来取值栈,可以看到是可以取到值栈的。
request域下有struts.valueStack对象,可以通过这个获取值栈,达到命令执行的目的。
官方的修复是升级到Apache Struts版本2.3.35或2.5.17。