在Trend Micro Vulnerability Research Service近期公布的一份漏洞报告中,Trend Micro研究团队成员Kc Udonsi和John Simpson详细介绍了Apache Struts框架中最近曝出的一个代码执行漏洞。这个安全漏洞最初是由苹果信息安全部的Matthias Kaiser发现并报告的。下文节选自他们撰写的CVE-2019-0230报告,其中做了部分修改。
最近,Apache Struts框架被曝出了一个远程代码执行漏洞。该漏洞是由于输入验证不足导致在计算原始用户输入时强制进行两次Object Graph Navigation Library(OGNL)计算所致。攻击者可以通过向目标服务器发送精心制作的请求来利用该漏洞。一旦攻击得手,他们就能够以服务器的权限执行任意代码。
漏洞详情
Apache Struts是一个用于构建基于Java的web应用程序的模型-视图-控制器(MVC)框架。该MVC框架用于将信息的表示与用户与信息的交互隔离开来。其中,模型由负责与数据库通信等后端工作的业务和应用程序逻辑组成。视图是提供给用户的数据的表示形式,而控制器负责协调模型和视图之间的通信。
Apache Struts将通过Java Servlet API提供一个名为ActionServlet的控制器。而来自客户端的请求则以XML配置文件中定义的“Action(操作)”形式发送到控制器。这时,控制器会调用模型代码中相应的Action类,这些类会使用内部逻辑来验证和执行这些操作,并以新视图的形式返回结果,而控制器则负责更新这些结果。Apache Struts允许通过Java ServerPages(JSP)对视图进行编码,这些视图将编译成HTML页面,以便通过浏览器进行查看。
我们可以使用如下所示的URI来访问相关的操作:
actionname>.action
其中< action name >被替换为操作名。后缀“.action”只是一个惯例。基于Struts的Web应用程序可以使用任何后缀,如“.do”。
HTTP是RFC 7230-7237和其他RFC中描述的请求/响应协议。客户端将请求发送到服务器,经过相应的处理后,服务器会再将响应发送回客户端。通常情况下,HTTP请求是由一个请求行、各种标头、一个空行和一个可选的消息正文组成的,具体如下所示:
其中,CRLF表示回车换行符(CR),后跟换行符(LF),而SP则表示空格字符。参数可以在请求URI或消息正文中作为“名称-值”对从客户端传递到服务器端,具体取决于所使用的方法和Content-Type标头。例如,当使用GET方法传送值为“1”、名为“param”的参数时,相应的HTTP请求将如下所示:
如果使用POST方法的话,则相应HTTP请求将如下所示:
如果有多个参数/值对的话,它们将被编码为以&为分隔符的“名称=值”对,具体如下所示:
var1=value1&var2=value2...
此外,Apache Struts还支持使用对象图导航语言(Object Graph Navigational Language,OGNL)表达式通过模板.jsp文件动态生成网页内容。OGNL是一种具有简洁语法的动态表达式语言(EL),用于获取和设置Java对象、lambda表达式等属性。OGNL表达式可以包含组成导航链的字符串。这些字符串可以是属性名、方法调用、数组索引等。OGNL表达式是根据以OGNL上下文形式提供给求值器的初始或根上下文对象进行计算的。
Apache Struts使用名为ActionContext的线程本地容器对象来存储执行Action所需的各种对象。这些对象通常会包括会话标识符、请求参数、区域设置等。此外,ActionContext还公开了一个ValueStack接口,用于推送和存储处理动态表达式语言(EL)所需的对象。当EL编译器需要解析表达式时,它会从推送到堆栈中的最新对象开始向下搜索堆栈。OGNL是Struts使用的主要EL,它具有以下特殊语法:
- –objectName:ValueStack中的对象(OGNL上下文中的默认/根对象),如Action属性。
- –#objectName:位于ActionContext之内,同时位于ValueStack之外的对象。例如,#parameters.objectName用于请求参数,#session.objectName用于会话作用域属性,等等。
- –%{ognlexp}:强制对通常为字符串的属性进行OGNL评估。
- –@full.class.name@Static_Field:用于表示类的静态字段(变量和方法)。
与此报告相关的功能是强制使用语法%{ognlexp}对通常为字符串的属性进行OGNL计算。该语法称为alt语法,是默认启用的。如果ognlexp是从用户输入中获得的,且没有进行任何消毒处理,那么很可能会发生双重计算。如果在标签属性中进行强制求值的话,这种情况就非常可能发生。例如(以锚点标签为例):
如果用户提供的fileNam,使得原始OGNL表达式未经进一步验证即进行了传递,则当该标签作为请求的结果渲染时,将会计算走私的OGNL表达式。也就是说,如果filename是OGNL表达式,如%{2+2},则渲染的标签为:
在渲染响应时,对于每个要渲染的标签,都会调用在org.apache.struts2.views.jsp.ComponentTagSupport类中定义的doStartTag()方法。然后,这个方法会进一步调用populateParams()方法。然而,这个方法的实际调用版本,则取决于被处理的标签。对于锚标签来说,对应的方法被定义为org.apache.struts2.views.jsp.ui.AnchorTag类中的populateParams()方法。实际上,id属性的第一次计算发生在populateParams()的基类实现中,即调用在org.apache.struts2.component.UIBean类中定义的setId()。
在调用populateParams()方法之后,doStartTag()方法将开始计算标签并渲染模板。在org.apache.struts2.component.ClosingUIBean类中定义的start()方法中,将会执行函数evaluateParams(),而这个函数会利用populateComponentHtmlId()函数来填充标签的id属性。至于这个方法的调用版本,具体取决于被渲染的标签。对于一个锚标签来说,将调用org.apache.struts2.component.UIBean类中定义的populateComponentHtmlId()方法。这个实现会在findStringIfAltSyntax()函数中再次将id属性作为OGNL表达式进行处理。
在Apache Struts 2框架存在一个远程代码执行漏洞。该漏洞是由于对OGNL表达式可用的类和包限制不足所致,特别是java.io.、java.nio.、java.net.、sun.misc.包。虽然Apache Struts团队多次指出重复的OGNL计算是一个潜在的安全风险,并指出开发人员应该只使用强制计算功能来处理受信任的数据,并已经添加了相应缓解控制措施,以最大限度地降低被利用的可能性。这些缓解控制措施包括:
- — struts.excludedClasses:以逗号分隔的由排除的类组成的列表。
- — struts.excludedPackageNamePatterns:用于根据Regex来排除包的模式。
- — struts.excludedPackageNames: 用逗号分隔的由被排除的包组成的列表。
此外,Struts默认限制了对Class构造函数以及静态方法的访问。SetOgnlUtil()方法会修改ValueStack的SecurityMemberAccess属性,以包含上述列表中的排除项。但是,如果遇到不在排除项中的类或包,则仍然允许其执行不安全操作,因为SecurityMemberAccess.isAccessible()方法允许对那些访问或操作这些包中定义的对象的表达式进行计算。
Apache Struts所使用的FreeMarker Java模板引擎在包freemarker.ext.jsp中提供了一组实用工具类,以方便FreeMarker-JSP的双向集成。在这些类中,有一个名为TaglibFactory的类。这个类提供了一个与servlet上下文相关联的哈希模型,可以加载相关的JSP标签库。类TaglibFactory能够通过ObjectWrapper类的实例将Java对象映射到FreeMarker模板语言的类型系统。ObjectWrapper类决定了Java对象的哪些部分可以从模板中访问,以及如何访问。此外,类TaglibFactory还公开了一个获取支持ObjectWrapper实例的getter。同时,这个ObjectWrapper实例还提供了一种创建任意Java对象的方法,以便在需要时分别使用newInstance()和wrap()两个公共方法对其进行封装。因此,攻击者可以通过首先使用wrap()方法对参数进行封装,然后调用newInstance()指定要实例化的类和封装的构造函数参数来创建一个具有公共构造函数的任意类实例。这个gadget有效地帮助Struts摆脱了对Class构造函数的默认限制。因此,TaglibFactory的实例可用于OGNL ValueStack中的OGNL表达式。
Guice轻量级依赖注入容器在包com.openymphony.xwork2.inject中提供了一组实用工具类,用于提供构造函数、方法、静态方法、字段和静态字段注入。其中,有一个名为ContainerBuilder的类。这个类提供了构建依赖注入容器的工厂方法。更重要的是,它还提供了一个公共方法,可以用来创建Container实例。而Container实例不仅存有依赖关系映射,而且还提供了一个公共方法injection(),该方法可用于创建任意实例并将其注入依赖项映射中。容器实例有效地提供了一种检索静态工厂方法保护的实例的方法,否则OGNL表达式将无法使用这些方法。通过调用inject()并指定所需的类,我们就可以获得没有公共构造函数,只有返回单例实例的静态工厂方法的任意类实例。我们可以使用前述的FreeMarker TagLibFactory小工具来获得ContainerBuilder类的实例,因为它实现了一个公共构造函数。
借助于这些小工具(gadget),我们可以通过获取受限类sun.misc.Unsafe的实例,从java.net.包中实例化一个类,打开与远程机器的连接并从远程机器接收类字节,最后使用sun.misc.Unsafe.defineAnonymousClass()方法以及从远程服务器接收到的字节来定义一个任意类,从而绕过当前的安全限制。然后,我们就可以使用sun.misc.Unsafe.allocateInstance()方法初始化任意类了。尽管allocateInstance()方法无法调用构造函数,却可以在创建的实例上调用为该类定义的其他公共方法。或者,我们也可以使用来自java.io或java.nio包的类的实例可以用来创建任意文件(如JSP文件),并将其写入到任意位置(如Web应用程序根目录),因为文件路径引用可用于OGNL ValueStack的OGNL表达式。
远程攻击者可以通过向受攻击的服务器发送包含恶意参数的HTTP请求来利用该漏洞。一旦成功利用该漏洞,他们就能够执行具有服务器权限的任意代码。
小结
Apache Struts团队已经在2020年8月修复了这个安全漏洞。根据他们的说法,相关的补丁是通过确保对传入的每个值进行正确检查,并验证其是否用于标签的属性来修复该漏洞的。此外,他们还建议,除非无法避免,否则最好不要使用%{…}或${…}语法对值以外的属性进行强制求值。最后,需要补充一点,Struts的2.5.22和更高版本不受该漏洞影响。
特别感谢趋势科技研究团队的Kc Udonsi和John Simpson对该漏洞提供了如此详尽的分析。有关趋势科技研究服务的简介,请访问http://go.trendmicro.com/tis/。
本文翻
发表评论