行业新闻

一只网安小白对Struts2远程命令执行的尝试梳理

一只网安小白对Struts2远程命令执行的尝试梳理

 

【写在最前】

本文创作的初衷在于分享和总结,作为一只网安新人小白,在RCE方向上的求知经高人指点落脚在了Struts2上。自21年7月中旬开始,基于Vulhub、Vulfocus逐个对S2系列公告涉及的RCE展开摸索,遍历所有RCE过程中,几乎将网上相关且公开的复现和分析全部文章都进行了参考,个人觉得素十八凭借两个未公开细节的RCE并自建Demo展现了大佬的深度。遍历完所有RCE之后又觉得不总结一下就留不下什么特别的收获,又经过medi0cr1ty Lucifaer等大佬总结性的启发,才有本文的梳理雏形。由于参考文献太多,对于不能逐一列举原文出处表达歉意,同时也欢迎各位大师傅批评指正。

 

一、Struts2基础背景

Apache Struts2是一个用于开发JavaEE网络应用程序开放源代码网页应用程序框架。单词Struts在编程语言中译为结构体,而在高级编程语言中结构体正是面向对象的类(Class)的前身。

Struts2是一个基于Client/Server模式、HTTP协议的Web应用程序的MVC模式框架,本质上相当于一个Servlet(容器)。MVC设计模式即模型(Model)、视图(View)和控制器(Control)。Struts2作为控制器(Controller)来建立模型与视图之间的数据交互,是Struts的下一代产品,以Struts1和WebWork的技术基础进行合并升级的Java Web框架。

从Struts1到Struts2变化非常大,但Struts2相对于WebWork的变化很小。简单理解:以WebWork的核心技术框架为血肉披上Struts的知名度表皮经过包装整合成为Struts2。

1.1 Struts2背景和现状

自2000年5月发展至今,Struts1市场占有率超过20%,开发人群丰富,稳定性和可靠性都得到广泛的证明,已然成为一个高度成熟的框架,隐有事实行业标准的趋势。随着时间推移技术不断进步,视图效果要求越来越高,Struts1的局限性也越来越多地暴露出来,并且制约Struts1继续发展。

由于Struts1框架与JSP/Servlet关联非常紧密导致一些严重问题:
—Struts1支持的视图技术单一。由于Struts1出现年代较早,那时尚无Free Marker、Velocity等技术,因此它不能与视图层的模版技术进行整合;
—Struts1与Servlet API严重耦合,应用难于测试;
—Struts1代码严重依赖于Struts1 API。

技术层面上,出现许多与Struts1竞争的视图层框架,比如JSF、Tapestry和spring MVC等。这些框架由于出现年代比较近,并应用最新设计理念,同时也从Struts1中吸取经验、克服不足。竞争的同时也促进Struts的发展。

Struts1已经分化成为两个框架:
—第一个是在传统的Struts1的基础上,融合另外的一个视图层技术优势的Web框架WebWork的Struts2。Struts2虽是在Struts1的基础上发展而来,但是实质上是以WebWork为核心。Struts2为传统Struts1注入WebWork先进设计理念,融合统一Struts1和WebWork两个框架;
—第二个分化框架是Shale。该框架远远超出Struts1原有设计思想,关联很少。Shale更像一个新的框架而不是Struts1的分化升级款。

Struts2的体系结构与Struts1的体系结构差别巨大,但Struts2向后兼容Strust1。Struts2以WebWork设计思想为核心,采用拦截器的机制处理用户请求,这样的设计也使得业务逻辑控制器能够与Servlet API完全脱离,所以Struts2也可以理解为WebWork的更新升级产品。

自2007年官方发布S2-001公告第一个远程代码执行(Remote Code Execution,RCE)至梳理本篇文章时,安全公告已经发布至S2-061,其中31个安全公告涉及RCE,且每个安全公告至少涉及一个CVE,当之无愧Java安全领域的“漏洞之王”。

1.2 Struts2工作原理

Web应用程序与传统网站的不同之处在于Web应用程序可以创建动态响应,传统网站只提供静态页面。Web应用程序可以与数据库和业务逻辑引擎以自定义响应的方式产生交互。

基于 Java Server Pages(JSP)的Web应用程序有时会混合数据库代码、页面设计代码和控制流代码。应用实践中,除非将这些代码关注点分开,否则大型应用程序的维护将变得很困难。

Struts2在软件应用程序中分离关注点的方法就是使用MVC模式,即模型(Model)、视图(View)和控制器(Control)。Model映射业务或数据库代码,View映射页面设计代码,Controller映射导航定位的控制流代码,Struts可以很好地与传统的 REST 应用程序以及 SOAP 和 AJAX 等技术配合使用。

Struts2框架为Web应用程序开发提供三个关键组件:
— 一个由应用程序开发人员提供的映射到标准 URI 的“请求”处理程序。
— 一个控制转移请求到另一个可完成响应的资源的“响应”处理程序。
— 一个标签库,可帮助应用程序开发人员使用服务器页面创建基于表单的交互式应用程序。

Struts2框架在MVC基本思想模式下的构成:
—M模型部分:一系列Interceptors(拦截器)、Action组件和ActionContext组件;
—V视图组件:Result组件;
—C控制部分:DispatchFilter(调度过滤器)、StrutsPrepareAndExecuteFilter。

1.2.1 针对框架组件给出名词解析

—Servlet:
服务程序的容器,如常见tomcat weblogic websphere jboss resin glassfish等;
—FilterDispatcher:
过滤器调度,控制部分的核心,即MVC中C控制层的核心,FilterDispatcher进行初始化并启用核心doFilter(在早期的 Struts2 开发中使用,它从 Struts 2.1.3 开始被弃用);
—StrutsPrepareAndExecuteFilter:
直译为Struts的预处理和执行过滤器,StrutsPrepareAndExecuteFilter类似Struts2.1.3以前版本中的FilterDispatcher,是Struts2框架的核心控制器。该控制器作为一个Filter运行在Web应用中,它负责过滤所有的HttpServletRequest请求。当HttpServletRequest请求到达时,该Filter会过滤用户请求,如果用户请求以.action结尾,该请求就会被转入Struts2框架处理;
—ActionMapper:
动作映射器,HttpServletRequest请求被运行在Web应用中的、作为一个Filter的控制器StrutsPrepareAndExecuteFilter过滤处理,如该请求以.action结尾,就会被转入Struts2框架的ActionMapper记录处理,获取*.action请求后,将根据*.action请求的前面部分决定调用那个业务逻辑组件。例如:ActionMapper对于login.action请求,调用名为login的Action来处理该请求;
—ActionProxy:
动作类代理,用户实现的Action是Struts2的ActionProxy的代理目标。用户实现的业务控制器Action则包含了对用户请求的处理,用户的请求数据被封装在HttpServletRequest中,而用户Action Class无需访问HttpServletRequest对象,过滤器会将请求数据解析出来并传给业务逻辑组件Action实例;
—ConfigurationManager
配置管理器,读取框架内用户的配置文件struts.xml,配置文件内是用户编写的Action Class;
—Action Invocation
动作调用;
—Tag Subsystem
标签子系统,如HTML、Dojo、forms等;
—Interceptor
拦截器;
—Template
模板,指JSP、FreeMarker、Velocity等。

Struts2框架由三部分构成:核心控制器、业务控制器和用户实现的业务逻辑组件。三部分中,Struts2框架提供核心控制器StrutsPrepareAndExecuteFilter,而用户需要实现业务控制层和业务逻辑层。Struts2官方给出的框架如图1.2.1,现如今的Struts2经过一系列的修修补补已经和当时的组织框架有一定的差别。

官方图

图1.2.1 Struts2官方框架图

1.2.2 Struts2中正常请求的响应处理流程

—用户请求数据被封装在一个HttpServletRequest请求中并由客户端提交,指向一个Servlet容器,Servlet容器初步解析该请求,并确定处理该请求的Web程序;

—Struts2参考该Web程序下的web.xml配置文件(Struts2.5时过滤器移到top文件)并确定过滤器Filter,注意顺序是先ActionContextCleanUp,再其他过滤器(SiteMesh、Plugin等),最后是核心控制器FilterDispatcher(Struts2.1.3时核心控制器变更为StrutsPrepareAndExecuteFilter);

—核心控制器FilterDispatcher(StrutsPrepareAndExecuteFilter)调用ActionMapper以确定是否需要调用某个Action来处理该HttpServletRequest,ActionMapper确定需要调用的Action;

—ActionMapper确定需要调用的Action后,核心控制器则把请求的处理交给ActionProxy,ActionProxy通过Configuration Manager询问Struts2框架的配置文件struts.xml,确定需要调用的Action Class;

—ActionProxy创建一个ActionInvocation实例,同时ActionInvocation通过命名模式调用Action。但在调用之前,ActionInvocation会根据配置加载Action相关的所有Interceptor(拦截器);

—调用执行Action完毕后,ActionInvocation负责根据struts.xml中的配置确定对应的返回结果Result,该过程需要ActionMapper参与;

—Result通常对应的是Template里面需被表示的JSP、Free Marker和Velocity等视图模板,也可能是另一个Action链;

—Result视图结果信息传递给ActionInvocation,此过程会反向加载对应调用Action时加载的所有Interceptor;

—最终处理结果被封装到HttpServletResponse中,通过核心控制器反馈客户端并呈现页面给用户。

1.3 Struts2安全机制

从严格意义来讲,Struts2框架并不提供独立的安全机制,黑名单或白名单方式构建沙箱的基础也是基于框架核心的拦截器(Interceptor)在一次次攻击防御重修修补补所实现的。换而言之,就是Struts2的默认表达式语言OGNL存在只允许其范围内的,阻止访问或者只允许访问特定类SecurityMemberAccess和Java包(Jar),其内部安全机制是随着版本修复更新一步一步添加进去的。

阻止访问或者只允许访问特定类具体体现在MemberAccess接口中规定了OGNL的对象方法或属性的访问策略。实现MemberAccess接口的有两类:一个是在OGNL中实现的DefaultMemberAccess,默认禁止访问private、protected、package protected修饰的属性方法。另一个是xwork中对对象方法访问策略进行了扩展的SecurityMemberAccess,指定是否支持静态访问方法,默认设置为false。

由于安全机制的问题和Struts2的版本修复有伴生关系,简单三言两语也无法直接展现整个形成过程,故而计划单开一个章节梳理一下漏洞验证利用催化安全修复所形成的安全机制。

 

二、OGNL语法介绍

OGNL的全称是对象图导航语言( Object-Graph Navigation Language),它是一种用于获取和设置 Java对象属性的开源表达式语言,以及其他附加功能,是Struts2的默认表达式语言。使用这种表达式语言,可以利用表达式语法树规则,存储Java对象的任意属性,调用Java对象的方法,同时能够自动实现期望的类型转换。OGNL最初是作为一种使用属性名称在UI组件和控制器之间建立关联的方法。如果将表达式看作是一种带有语义的字符串,那么OGNL就是这个语义字符串与Java对象之间的沟通桥梁。

简要概括OGNL的功能就是双向转换语义字符串与Java对象数据即转换String和Object(参考序列与反序列化),原因是视图层表示的控制源于语义字符串,而业务控制逻辑处理过程涉及的数据不仅限于语义字符串,其中就包含Java对象数据。

2.1 OGNL基本语法树

Java实现的Web实际就是面向对象的编程,OGNL表达式必然也有面向对象的Action。为便于理解,由访问方式简单划分三类对象:
—静态类静态对象
即访问静态类或其成员的方法为@,语法规则为根据@开始,后面接上完整的类名。如@java.lang.Math@max(10,20)相当于java中直接调用Math.max(10,20)
—基本对象
即基本数据类型的对象访问,不加任何前缀,直接使用即可。基本数据类型包括String、Int、Boolean和List等。
—非基本对象
即一个实例对象的访问方法为#,语法规则为根据#开头,后面为对象名,该对象需要在OgnlContext类中,且可以根据对象名可以唯一定位。如#demo。

OGNL中有两个常用的类:
—ognl.Ognl类
主要用来解析和解释执行OGNL表达式。
—ognl.OgnlContext类
该类为OGNL表达式提供一个执行环境,该类也实现Map接口,允许通过put(key,object)方法向OgnlContext环境中添加各种类型的对象。

OgnlContext中对象分为两种:
—Root
根对象,可以存入任何对象通过调用OgnlContext.setRoot(obj)设置为根对象,整个OgnlContext中有且最多只能有一个根对象。
— Context
上下文对象,即Map结构,可以往该Map存入任意key-value键值对。该类非基本对象个数类型不受限制,但只能通过制定上下文容器的“#对象名.name”的方式去获取属性值,且在对象的类型中要提供getName方法。

OGNL表达式目标对象的方法调用:
—非基本对象的方法访问,#开头,对象与方法之间用.连接;
\#obj.method( 参数 )
—静态对象的方法访问,@开头,对象与方法之间用@连接;
@xxx@method( 参数 )
—基本对象的方法访问,和非基本对象方法方式一致。
"name".length()
OGNL表达式目标对象的成员访问:
—非基本对象的成员访问,#开头,对象与成员之间用.连接;
\#obj.field
—静态对象的成员访问,@开头,对象与成员之间用@连接;
@xxx@field
—基本对象的成员访问,和非基本对象成员方式一致。
"name".hash

2.2 OGNL执行操作

2.2.1 OGNL的三个重要操作符号

OGNL中的三个重要符号:#、%、$。这三者在Struts2的开发使用和RCE漏洞中都作用不可忽视。

#符号的用途一般有三种:
—访问非根对象属性,例如#session.msg。由于Struts2中值栈被视为根对象,所以访问其他非根对象时需要加#前缀。实际上,#相当于调用ActionContext.getContext()#session.msg表达式相当于
ActionContext.getContext().getSession().getAttribute("msg");
—加在this指针之前表示对this指针的引用;
—以#{}格式构造Map的键值对,例如:\#{'foo':'foo value', 'bar':'bar value'}

%符号的用途是在标志属性为字符串类型时,计算OGNL表达式的值,类似JS中的函数eval()。例如:\<s:url value ="%{items.{title}[0]}"/\>

$符号主要有两个方面的用途:
—在国际化资源文件中,引用OGNL表达式,例如国际化资源文件中的代码:reg.agerange=国际化资源信息:年龄必须在{min}{max}之间;
—在Struts 2框架的配置文件中引用OGNL表达式。

2.2.2 OGNL的执行操作三要素

包含getValue()setValue()两个最基本的操作,OGNL的所有操作实际上都是围绕着三个参数而进行,这三个参数姑且称之为OGNL的执行操作三要素。

—表达式(Expression)
表达式是整个OGNL的核心,表达式会确定此次OGNL执行操作到底要干什么,所有OGNL的执行操作都是在针对表达式的解析后进行的。因此,表达式本质上是一个带有语法含义的字符串,该字符串将规定执行操作的类型和执行操作的内容。
OGNL支持大量的表达式语法,不仅支持“链式”描述对象访问路径,还支持在表达式中进行简单的计算,甚至还能够支持复杂的Lambda表达式等。

—Root对象(Root Object)
OGNL的Root对象可以理解为OGNL的执行操作对象。当OGNL表达式确定“干什么”以后,尚需要指定对谁实施该动作。OGNL的Root对象实际上是一个Java对象,是所有OGNL操作的实际载体。这就意味着,对于一个OGNL的表达式,实际上需要针对Root对象去进行OGNL表达式的评估求值并返回结果。

—上下文环境(Context)
OGNL确定表达式和Root对象的要素后,就可以使用OGNL的基本功能。例如,根据表达式针对OGNL中的Root对象进行“取值”或者“写值”操作。
事实执行过程中,在OGNL的内部,所有的操作都会在一个特定的数据环境中运行,该数据环境就是OGNL的上下文环境(Context)。该上下文环境确定OGNL的执行操作在哪里实施。OGNL的上下文环境是Map结构,称之为OgnlContext。之前所提到的Root对象,事实上也会被添加到上下文环境中去,并且将被作为一个特殊的变量进行处理。

 

三、Struts2 RCE漏洞梳理

自2007年至2020年,官方发布S2-001至S2-061安全公告,61份安全公告中有31份涉及RCE。而这31份中抛开脆弱性细节未直接披露的,仍有相当部分威胁系数高、利用复杂度低的脆弱点。由于Struts2本身不提供安全机制,修修补补地正则匹配也难以根除,Bypass类型RCE也不可忽视。

3.1 Struts2 RCE的概述

—S2-001:表单验证错误OGNL 循环解析导致RCE。
—S2-003:XWork ParameterInterceptors 绕过允许OGNL语句执行。
—S2-005:XWork ParameterInterceptors 绕过导致RCE。
—S2-007:当出现转换错误时,用户输入被评估为OGNL表达式。
—S2-008:Struts2 中的多个严重漏洞。
—S2-009:ParameterInterceptor 漏洞导致RCE。
—S2-012:Showcase 应用程序漏洞导致RCE。
—S2-013:存在于URL和定位标记的includeParams属性导致RCE。
—S2-014:通过强制的URL和锚标签参数包括引入导致RCE,会话访问和操纵和跨站脚本攻击。
—S2-015:由通配符匹配机制或OGNL表达的双重评价引入的漏洞导致RCE。
—S2-016:通过操纵前缀为“action:”/“redirect:”/“redirectAction:”的参数引入的漏洞允许远程命令执行。
—S2-018:ActionMapper机制支持特殊参数前缀动作的访问控制漏洞。
—S2-019:动态方法调用的默认开启和默认禁止。
—S2-020:将Commons FileUpload升级1.3.1版(DoS)并添加“class”以排除ParametersInterceptor 中的参数避免 ClassLoader 操作。
—S2-021:改进了ParametersInterceptor和CookieInterceptor中排除的参数以避免
ClassLoader 操作。
—S2-022:扩展CookieInterceptor中排除的参数以避免操纵Struts的内部结构。
—S2-026:特殊的顶部对象可用于访问Struts的内部结构。
—S2-029:强制双重OGNL评估求值,当对标签属性中的原始用户输入进行评估求值时,可能会导致RCE。
—S2-032:启用动态方法调用时,可以通过 method: 前缀导致RCE。
—S2-033:启用动态方法调用时,使用REST插件则运算符!可执行远程代码。
—S2-036:强制双重OGNL评估求值,当对标签属性中的原始用户输入进行评估求值时,可能会导致RCE。(S2-029的延申)
—S2-037:使用REST插件时可以执行远程代码执行。
—S2-045:基于Jakarta Multipart解析器执行文件上传时可能的RCE。
—S2-046:基于Jakarta Multipart解析器执行文件上传时可能的RCE。(S2-045的异构)

—S2-048:Struts 2.3.x 系列中Struts 1插件示例中的Struts Showcase应用程序中可能存在的RCE。
—S2-052:使用带有XStream 处理程序的Struts REST插件来处理XML负载时可能存在的RCE。
—S2-053:在Freemarker标签中使用无意表达式而不是字符串文字时可能导致RCE。
—S2-055:Jackson JSON库中的RCE漏洞
—S2-057:alwaysSelectFullNamespace的RCE漏洞触发条件:定义XML配置时namespace值未设置且上层动作配置(Action Configuration)中未设置或用通配符namespace;url标签未设置value和action值且上层动作未设置或用通配符namespace。
—S2-059:强制双重OGNL评估,当对标签属性中的原始用户输入进行评估时,可能会导致远程代码执行。
—S2-061:强制双重OGNL评估,当对标签属性中的原始用户输入进行评估时,可能会导致远程代码执行。(S2-059的延申)

3.2 Struts2 RCE的位置

初步熟知Struts2的工作原理后,再去理解其RCE漏洞触发的位置及漏洞成因,有利于建立整体性的脆弱点感知梳理。如图3.2.1将RCE触发的隶属处理环节归类表示,细节和位置点都已经比较完善,但是仍然不能完美体现在工作流程中。意欲自己梳理并动手制作一个更好的位置成因架构图,当遇见图3.2.2时,觉得在Struts2 RCE的位置梳理在整个工作流程生命周期中的体现无人能出其右,但其中S2-008也并非如此单一。

漏洞位置图

图3.2.1 Struts2的RCE在框架中的位置和原因

漏洞位置

图3.2.2 Struts2的RCE在生命周期中的位置

3.3 Struts2 RCE的成因

Struts2 RCE的漏洞成因并不能一概而论地归结到OGNL上,其成因原理涉及角度和利用复杂度均不一致。除全新漏洞触发点还涉及老漏洞的新打开方式,结合很多在Struts2安全上有贡献前辈的梳理和整合,粗略梳理Struts2 RCE的漏洞成因和类型。
类型可以概括为四类:用户可控的不可信数据源解析执行、逻辑处理上的OGNL二次解析、支持使用的第三方组件包含脆弱性和正则防御和沙箱的Bypass。下面详谈的RCE成因都能归属到这四类中。

—S2-001
S2-001=CVE-2007-4556=CNVD-2007-5360。
WebWork2.1+和Struts2的altSyntax功能允许将OGNL表达式插入到文本字符串中并进行递归处理。用户提交表单数据并且验证失败时,后端会将用户提交的参数值使用OGNL表达式%{value}进行解析,然后重新填充到对应的表单数据中。例如注册或登录页面,提交失败时后端会默认返回之前提交的数据,由于后端使用%{value}对提交的数据执行了一次OGNL表达式解析,所以可以直接构造
Payload 进行命令执行。

—S2-003、S2-005
S2-003=CVE-2008-6504;
S2-005=CVE-2010-1870=CNVD-2010-1318。
ParametersInterceptor拦截器对参数名正则过滤不完全导致的OGNL表达式通过#来访问Struts2的对象,框架正则过滤#字符防止安全问题,但是通过Unicode编码(u0023)或8进制(43)即可绕过(Bypass)安全限制。漏洞执行前提需要将denyMethodExecution置为false,即acceptableName为true,原因是正则表达式[\\p{Graph}&&[\^,\#:=]]\*检测特殊字符,防止传入恶意特殊字符开头如#等。
官方通过增加安全配置禁止静态方法调用(allowStaticMethodAcces)和否认方法执行(MethodAccessor.denyMethodExecution)等来修补S2-003。S2-005则是Bypass安全配置,设置allowStaticMethodAccess为true和denyMethodExecution为false(允许静态方法调用和否认方法执行)再次造成漏洞。

—S2-007
S2-007=CVE-2012-0838=CNVD-2012-8979。
在数据类型转换错误情况下,拦截器会将用户输入数据取出并插入到当前值栈(ValueStack)中,之后OGNL强制评估会对标签进行解析导致表达式执行造成表达式注入,合理构造OGNL即可导致RCE。

—S2-008
S2-008=CVE-2012-0392。
S2-008涉及多个漏洞,为了防止在参数内调用任意方法,默认情况下将标志否认方法执行xwork.MethodAccessor.denyMethodExecution设置为true,并将SecurityMemberAccess字段allowStaticMethodAccess设置为false。此外,为防止访问上下文变量,自Struts2.2.1.1开始,在ParameterInterceptor中应用针对参数名称的改进字符白名单:acceptedParamNames= "[a-zA-Z0-9\\.][()_']+"。在某些情况下,可以绕过这些限制以执行恶意Java代码。
Struts2<= 2.2.3中的远程命令执行(ExceptionDelegator)(S2-007)
当将参数值应用到属性时发生异常时,该值被评估为 OGNL表达式。例如,将字符串值设置为整数属性时会发生这种情况。由于这些值没有被过滤,攻击者可以滥用OGNL语言的能力来执行导致远程命令执行的任意Java代码。此问题已被报告并在Struts2.2.3.1中得到修复。然而执行任意Java代码的能力却被忽视。
Struts2<= 2.3.1中的远程命令执行(CookieInterceptor)
参数名称的字符白名单不适用于CookieInterceptor。当Struts2被配置为处理cookie名称时,攻击者可以对Java函数的调用静态方法访问来执行任意系统命令。即allowStaticMethodAccess标志可以在请求中被设置为true,漏洞触发和S2-003及S2-005的ParameterInterceptor机制类似。
Struts2<= 2.3.1中的任意文件覆盖(ParameterInterceptor)
虽然allowStaticMethodAccess从Struts2.2.3.1开始禁止访问参数中的标志,但攻击者仍然可以仅使用一个String类型的参数访问公共构造函数来创建新的Java对象并访问其setter一个字符串类型的参数。该方法存在滥用创建和覆盖任意文件,可以使用未初始化的字符串属性将禁止字符注入文件名。
Struts<= 2.3.17中的远程命令执行( DebuggingInterceptor)
devMode模式本身不是安全漏洞,但在devMode模式下运行的应用程序拦截器DebuggingInterceptor也容易远程执行命令。虽然devMode模式在生产环境中几乎不存在,但该模式下的安全影响仍值得注意。

—S2-009、S2-026
S2-009=CVE-2011-3923=CNVD-2012-0369;
S2-026=CVE-2015-5209=CNVD-2016-01256。
ParametersInterceptor中的正则表达式将top['foo'](0)作为有效的表达式匹配,OGNL将其作为(top['foo'])(0)处理,并将“foo”操作参数的值作为OGNL表达式求值。这使得恶意用户将任意的OGNL语句放入操作公开的任何String变量中,即可将其评估为OGNL表达式。由于OGNL语句利用点在HTTP参数中,攻击者可以使用黑名单字符(如#)设置禁用方法执行denyMethodExecution并执行任意方法,绕过ParametersInterceptor和OGNL类的保护。
Struts2版本2.3.24前存在安全漏洞,特定的top对象可用于访问strtus’
internals,允许攻击者利用该漏洞绕过安全限制执行未授权操作。ValueStack定义了top代表执行上下文Root的特殊对象。它可用于操作Struts2的内部结构或可用于影响容器的设置。S2-009公告中,使用top['foo'](0)的形式用来对foo进行二次OGNL解析,实际上是也使用了top可以访问Root中的第一个对象的特性。S2-026公告中,官方禁止了通过参数名使用top访问上下文中的内容。

—S2-012
S2-012=CVE-2013-1965=CNVD-2013-06101。
OGNL评估已经在S2-003、S2-005和S2-009中得到修复,由于只涉及参数名称,修复方法是基于将可接受的参数名称列入白名单并拒绝对参数中包含的表达式进行评估,部分修复只解决参数名称问题。
包含特制请求参数的请求可用于将任意OGNL代码注入到属性中,然后用于重定向地址的请求参数,导致在重定向结果从栈中读取并使用先前注入的代码作为重定向参数时发生OGNL评估,在S2-003、S2-005、S2-009进行的参数过滤未对重定向值起到限制过滤作用。

—S2-013、S2-014
S2-013=CVE-2013-1966=CNVD-2013-05924;
S2-014=CVE-2013-2115=CNVD-2013-06279。
Struts2的标签中<s:a\><s:url\>都有一个includeParams属性,该属性的主要功能是了解是否包含HTTP请求参数且仅允许三个取值及意义如下:
-none-URL中不包含任何参数(默认);
-get-仅包含URL中的GET参数;
-all-在URL中包含GET和POST参数。
includeParams=all的时候,标签<s:a\><s:url\>会将本次请求的GET和POST参数都放在URL的GET参数上。包含特制参数的请求可用于将任意OGNL注入堆栈(Stack),然后用作<s:a\><s:url\>标签的请求参数。当<s:a\><s:url\>尝试去解析原始请求参数时,将其参数评估为OGNL表达式,由此启用方法执行和执行任意方法,绕过Struts和OGNL类的保护。
该问题最初由Struts2.3.14.1和安全公告S2-013修复,S2-014是S2-013的不完全修复的利用。S2-014的修复方法是URL呈现子系统已更改为不向OGNL评估传递任何参数名称或值,且MemberAccess组件的allowStaticMethodAccess属性现在不可变。

—S2-015
S2-015=CVE-2013-2135=CNVD-2013-06834;
S2-015=CVE-2013-2134=CNVD-2013-06787。
Struts2\*通配符匹配机制可导致OGNL表达式两次评估求值。Struts2允许定义基于通配符\*的Action映射,如:

<action name="\*" class="example.ExampleSupport"\>
<result\>/example/{1}.jsp\</result\>
</action\>

如果请求不匹配任何所定义的Action就会匹配通配符\*,且请求的Action名会用于加载基于Action名的JSP文件,即把{1+1}值作为OGNL表达式处理,结果允许在服务端执行任意Java代码。此漏洞包含两个问题的组合:
-请求的Action名没有转义或针对白名单进行检查;
-当组合使用$和%开放字符时对TextParseUtil.translateVariables中的OGNL表达式进行两次求值。

—S2-016、S2-018、S2-019
S2-016=CVE-2013-2251=CNVD-2013-09777;
S2-018=CVE-2013-4310=CNVD-2014-00665;
S2-019=CVE-2013-4316=CNVD-2013-13318。
Struts2 DefaultActionMapper通过在参数前加上action:redirect:前缀,后跟所需的导航目标表达式来支持短路重定向的方法。在2.3.15.1之前的Struts2中, action:redirect:redirectAction:之后的信息没有被正确清理,且所述信息将作为针对ValueStack的OGNL表达式进行评估求值,导致利用OGNL表达式调用Java静态方法执行任意系统命令。
S2-016公告中,DefaultActionMapper已更改为正确清理action:前缀信息。S2-017公告中,与redirect:/redirectAction: 前缀参数相关的功能被完全删除。
S2-018公告中,Struts2.3.15.3中动作映射机制已更改。为避免规避安全约束引入两个额外常量来引导DefaultActionMapper的行为:
struts.mapper.action.prefix.enabled当设置为false时action:前缀被禁用,默认设置为false;struts.mapper.action.prefix.crossNamespaces当设置为false时,用action:前缀定义的动作必须与当前动作在同一个命名空间中。
S2-019公告中,描述漏洞影响为Dynamic method executions,修复方案是在Struts2版本2.3.15.2之后,将struts2-core包中的default.properties(默认属性)配置中的struts.enable.DynamicMethodInvocation默认值设置为false,其他版本也可以在struts.xml中使用如下配置:

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

漏洞和S2-008类似,均是当devMode开启debug模式下会出现漏洞,参数DynamicMethodInvocation是动态方法调用的flag也就是对应着action:method:两个前缀。
有关S2-018和S2-019漏洞复现及其细节的资料网上较少,可能性原因或许是因为对action:method:两个前缀挖掘出漏洞利用的方式,还是存在一定的限制性。

—S2-020、S2-021、S2-022
S2-020=CVE-2014-0094=CNVD-2014-01552;
S2-021=CVE-2014-0112=CNVD-2014-02702;
S2-021=CVE-2014-0113=CNVD-2014-02705;
S2-022=CVE-2014-0116=CNVD-2014-02855。
struts2中Root对象是当次访问的Action对象,而ClassLoader通常由运行环境所提供,例如在Tomcat下,这个ClassLoader应该为当前应用所使用的:org.apache.catalina.loader.WebappClassLoader。由于环境不同,class.classLoader
对应存放的容器结果也不同,因此漏洞利用不具有完全通用性。ClassLoader中存放很多容器运行时上下文(Context)中的所需要的一些属性值。如果这些值被修改,可能会影响到应用程序的运行方式。能被我们修改的属性需要有以下几个条件:
-有set方法,或者是可以使用set方法改变的值;
-修饰符应该是 public;
-属性的返回值应该是通过用户的输入可以被OGNL解析成为相应的对象;
-修改后能够对应用程序造成影响,导致安全风险。
S2-020公告ApacheStruts2存在安全漏洞,由于ParametersInterceptor允许访问直接映射到getClass()方法的class参数,允许攻击者利用漏洞通过请求参数操作ClassLoader。
S2-021公告Apache Struts2.3.16.1版本中S2-020修复引入的用于阻止对getClass()方法的访问的排除参数模式是不完全修复。可通过特别设计的请求进行绕过。同理,当CookieInterceptor被配置为接受所有Cookie时,即当使用\*来配置CookieName参数时,也容易受到相同类型的攻击。
S2-022公告Apache Struts2.3.16.2中S2-021修复引入的用于阻止访问 getClass()方法的排除参数模式也是不完全修复,当使用\*配置CookiesName参数时,可以更改会话、请求等的状态,通过CookieInterceptor实现对Struts2的内部操作。

—S2-029、S2-036、S2-059、S2-061
S2-029=CVE-2016-0785=CNVD-2016-01714;
S2-036=CVE-2016-4461=CNNVD-201710-559。
该漏洞源于程序对属性值执行双重判断。远程攻击者可利用该漏洞执行任意代码。代码执行过程大致为先尝试获取Value的值,如果Value为空,那么就二次解释执行了Name,并且在执行前给name加上了%{}。最终造成OGNL二次执行。因此需要的条件极为苛刻:特殊的代码、Value值为空、可以传参到Value、控制Name。
主要有两类标签属性存在问题,一类是Id,一类是Name,现知Struts2的i18n,text标签的Name属性处理的时候会经过两次OGNL执行,在最新版本里面要想成功利用该漏洞执行任意代码,需要绕过Struts2的安全管理器,该漏洞也是标签语言的OGNL表达式二次执行,归根于Bypass安全管理器。
S2-036是S2-029的不完全修复,即借助tag属性中的%{}序列利用该漏洞执行任意代码。
S2-059公告中,Struts2 会对某些标签属性(比如id,其他属性有待寻找)的属性值进行二次表达式解析,因此当这些标签属性中使用了%{x}且x的值用户可控时,用户再传入一个%{payload} 即可造成OGNL表达式执行。
S2-061是S2-059的不完全修复,S2-059的安全管理修复可以被S2-061完全Bypass。

—S2-032、S2-033、S2-037
S2-032=CVE-2016-3081=CNVD-2016-02506;
S2-033=CVE-2016-3087 =CNVD-2016-03754;
S2-037=CVE-2016-4438=CNVD-2016-04040。
当程序启用Dynamic Method Invocation时,远程攻击者可借助method:前缀利用该漏洞在服务器端执行任意代码。即当启用动态方法调用时,可以传递可用于在服务器端执行任意代码的恶意表达式。method:\<name\>Action前缀去调用声明为public的函数。低版本中Strtus2不会对name方法值做OGNL计算,而在高版本中会。
S2-033漏洞依附于S2-032漏洞,Struts官方发布S2-033安全公告只是针对性地对REST插件功能存在的安全问题进行补充,其漏洞技术成因与S2-032相同,利用前提均为配置文件struts.xml的struts.enable.DynamicMethodInvocation设置为true。根据官方漏洞描述,当开启动态方法调用,并且同时使用了Strut2 REST Plugin插件时,使用“!”操作符调用动态方法可能执行OGNL表达式,导致代码执行。
S2-037是S2-033不完全修复的利用方式,S2-033需要开启动态函数调用,且S2-037不需要开启动态函数调用。

—S2-045、S2-046
S2-045=CVE-2017-5638=CNVD-2017-02474
S2-046=CVE-2017-5638=CNVD-2017-02474
S2-045、S2-046涉及同一个CVE,不同点在于漏洞的利用方式。Struts2上传默认使用的是org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest类对上传数据进行解析,只不过最终调用了第三方组件common upload完成上传操作。
S2-045安全公告中,可以使用恶意Content-Type 值执行RCE攻击 。如果该Content-Type值无效,则会引发异常,然后用于向用户显示错误消息。该漏洞源于程序没有正确处理文件上传。远程攻击者可借助带有#cmd=字符串的特制Content-TypeHTTP头利用该漏洞执行任意命令。
S2-046安全公告中,可以使用恶意Content-Disposition 值或不正确的Content-Length标头尝试执行RCE攻击 。如果Content-DispositionContent-Length值无效,则会引发异常,然后用于向用户显示错误消息。这是S2-045中描述的相同漏洞的不同利用方式 。

— S2-048
S2-048=CVE-2017-9791=CNVD-2017-13259
漏洞主要问题出在struts2-struts1-plugin这个插件包上。这个库的主要作用就是将struts1的Action封装成struts2的Action以便它能在strut2上运行使用。而由于struts2-struts1-plugin包中的 Struts1Action.java中的execute()函数可以调用getText()函数,这个函数刚好又能执行OGNL表达式,同时这个getText()的参数输入点,又可以被用户直接进行控制。如果这个点被恶意攻击者所控制,就可以构造恶意执行代码,从而实现一个RCE攻击。

—S2-052
S2-052=CVE-2017-9805=CNVD-2017-25267。
Struts2 REST插件的XStream组件存在反序列化漏洞,使用XStream组件对XML格式的数据包进行反序列化操作时,未对数据内容进行有效验证,可直接在数据包中插入恶意代码,导致服务器被攻陷。
S2-033和S2-037也是涉及REST插件,只不过属于利用操作符号“!”实现OGNL的执行。而S2-052通过XML实体的反序列化实现OGNL的执行。OGNL本身就是转换Object和String,两者有一定的相似原理。

— S2-053
S2-053=CVE-2017-12611=CNVD-2017-25632。
S2-053公告中,在Freemarker标签中使用错误的表达式而不是字符串文字时,导致攻击者远程执行代码攻击。
当在Freemarker标签中使用表达式文本或强制表达式时,使用以下请求值可能会导致远程代码执行:

<@s.hidden name=&quot;redirectUri&quot; value=redirectUri /><@s.hidden name=&quot;redirectUri&quot; value=&quot;\${redirectUri}&quot; />

这两种情况下,值属性都使用可写属性,都会受到Freemarker表达式影响。

—S2-055
S2-055涉及CVE-2017-7525=CNVD-2017-27693
FasterXML Jackson是美国FasterXML公司的一款用于Java的数据处理工具,Jackson-databind是其中的一个具有数据绑定功能的组件。FasterXML Jackson-databind中存在Java反序列化远程代码执行漏洞。未经过身份验证的攻击者通过向ObjectMapper的readValue方法的发送精心制作的序列化数据执行恶意代码。
从官方公告的描述来看,该漏洞不涉及Struts2本身的框架和代码,就是插件Jackson的反序列化漏洞。由于Jackson在处理反序列的时候需要支持多态,所以在反序列的时候通过指定特定的类来达到实现多态的目的。这个特性默认关闭,所以在Struts2中影响也是有限。

—S2-057
S2-057=CVE-2018-11776=CNVD-2018-15894。
S2-057公告中,漏洞产生于网站配置XML时如果没有设置namespace的值,并且上层动作配置中并没有设置或使用通配符namespace时,可能会导致远程代码执行漏洞的发生。同样也可能因为url标签没有设置value和action的值,并且上层动作并没有设置或使用通配符namespace,从而导致远程代码执行漏洞的发生。当访问动作类型为目标(redirect action,chain,postback)时,会根据URL生成的namespace生成一个跳转地址location,location会进行OGNL评估求值。可以解析为两部分:
-alwaysSelectFullNamespace被设置为true,此时namespace的值是从URL中获取的。URL是可控的,所以namespace也是可控的;
-action元素没有名称空间属性集,或者使用通配符。该名称空间将由用户从URL传递并解析为OGNL表达式,最终导致远程代码执行的脆弱性。

 

四、 RCE攻击催化的防御安全机制

梳理整合Struts2历经十多年安全漏洞的修修补补,从每个RCE导致版本的修复方案确实是一条可行的线索。纵观各路大牛剖析RCE的验证利用,网文路西法er的触发点解析安全修复的安全机制产生思路,使人眼前一亮。

4.1 Struts2 安全机制阶段划分

安全修复姑且划分三个阶段。从最初的知攻知防地定点打卡方式去正则过滤用户可控的不可信数据源的最初阶段,到进入Struts2依赖OGNL调用路径下的数据包com.opensymphony.xwork2.ognl中,自定义一个DefaultMemberAccess的子类SecurityMemberAccess进行安全验证,阻止访问或者只允许访问特定类和Java包的黑名单沙箱机制阶段,最后到直接删除DefaultMemberAccess,并且具体到调用的xwork2.ognl包中,在调用方法之前把黑名单类直接写死在判断代码里,并且屏蔽所有中间件调用包的终极阶段。

4.2 安全机制对应的版本和PoC

关于Struts2 RCE系列的漏洞复现和验证利用方向上,除少数几个利用难度较为复杂,网上公开细节较少的漏洞,几乎已经没有什么新鲜内容了。原计划该章节对应全部RCE梳理一下其验证利用PoC在对应官方修复后的获取OGNL执行操作三要素上的变化,但发现有俩位前辈已经析出了技术总结。考虑单独针对每一个RCE解释一下PoC和修复角度的情况又发现了另一位的技术总结。读下来几乎全部对应本人对Struts2RCE梳理的构想,本着分享的基本思想取其精华,抛开冗长的源码分析整理到此处并记录关键的PoC构成。

—Struts 2.0.0-Struts 2.0.8(S2-001)
该版本的Struts2严格意义上没有安全机制且altSyntax默认为true,所以表单提交标签内容直接被递归查询执行OGNL进一步导致RCE。
官方修复方案是altSyntax默认为false,使得标签内容不再解析为OGNL并添加判断使用break限制递归查询。

—Struts 2.0.0-Struts 2.1.8.1(S2-003、S2-005)
Struts 2.0.0-Struts2.0.11.2的ParametersInterceptor拦截器对参数名过滤在XWork2.0.4官方重写acceptableName方法,才增加敏感字符检测,S2-003仅仅通过一招敏感字符编码即可Bypass。
官方修复方案通过增加沙箱机制禁止静态方法调用(allowStaticMethodAcces)和否认方法执行MethodAccessor.denyMethodExecution等安全配置并且重写参数拦截器的正则表达式来修补S2-003。至此官方修复之路开始进入第二阶段。
第二阶段的安全机制在OGNL上的体现在MemberAccess接口中规定OGNL的对象方法或属性访问策略。实现MemberAccess接口的有两类:一个是在OGNL中实现的DefaultMemberAccess,默认禁止访问private、protected、package protected修饰的属性方法;另一个是xwork中对对象方法访问策略进行扩展SecurityMemberAccess,指定是否支持访问静态方法,默认设置为false 。
但此时context对象依旧可以访问,两个比较关键的就有:
#context-OgnlContext,一种基于 xwork.MethodAccessor.denyMethodExecution属性值的保护方法执行;
#_memberAccess-SecurityMemberAccess,其allowStaticAccess阻止静态方法执行。故而绕过这一版本的防护的 PoC开始具有明显的结构特点:

(\#_memberAccess['allowStaticMethodAccess']=true). (@java.lang.Runtime@getRuntime().exec('calc'))

验证利用方法为通过#_memberAccess获取SecurityMemberAccess实例,通过 setAllowStaticMethodAccess方法设置其值为true,允许执行静态方法。官方修复方案是在参数拦截器修改为更加严格的正则表达式,将忽略简单属性导航路径以外的参数。至此Struts2进入2.2.1版本。

—Struts 2.2.1-Struts 2.3.3.1(S2-007、S2-008、S2-009)
该版本区间沙箱没有改变,但其安全机制上针对每个RCE都有修复点。
S2-007是标签内的回显,是因为数据变量类型报错导致OGNL表达式的执行。官方针对该漏洞的修复增加对单引号进行转义处理。
S2-008是参数值的过滤问题,但是需要开启devmode模式,对于debug需要关注DebuggingInterceptor拦截器。官方针对该漏洞的修复也是修改为更加严格acceptedParamNames过滤器的ParameterInterceptor和CookieInterceptor的正则表达式。
S2-009是top ['foo'](0)作为有效的表达式匹配,OGNL将其作为(top
['foo'])(0)
处理绕过了ParameterInterceptor的限制。官方针对该漏洞的修复主要有两点,其一是在值栈(ValueStack)中setParameter方法将不允许参数名称中包含更多的eval表达式,其二是在struts2.3.1.2中 ParameterInterceptor拦截器的正则表达式变得更为严格。

—Struts2.3.14.2及之后版本(S2-012、S2-013、S2-014、S2-015)
Struts2.3.14.2及之后版本沙箱发生改变,修改SecurityMemeberAccess类,删除setAllowStaticMethodAccess方法,导致无法修改AllowStaticMethodAccess属性值,同时AllowStaticMethodAccess属性被final修饰。
S2-012漏洞是在struts.xml对action对象进行重定向配置,重定向配置的参数以OGNL表达式解析造成OGNL二次注入导致任意代码执行,安全沙箱没有改进。官方针对该漏洞的修复是OGNLUtil类已更改为默认拒绝eval表达式。
S2-013漏洞跟标签有关,标签属性设置为includeParams=all会造成OGNL表达式执行,在S2-014后URL将不会把参数名或值传递给OGNL表达式。沙箱就是在S2-014时发生改变。
S2-015漏洞是通配符问题,此时沙箱机制已经改变。因为setAllowStaticMethodAccess方法被删掉,但是ognlcontext的上下文属性还在。所以阶段性的PoC特征又出现了:通过反射将allowStaticMethodAccess的值改变

#f=\#_memberAccess.getClass().getDeclaredField("allowStaticMethodAccess")`
#f.setAccessible(true)
#f.set(\#_memberAccess,true)

新建一个 ProcessBuilder 实例,调用start方法来执行命令
(\#p=new java.lang.ProcessBuilder('calc')).(\#p.start())
官方针对性修复方案在新版本引入操作名称白名单,默认情况下设置为接受与以下正则表达式匹配的操作:[a-z]\*[A-Z]\*[0-9]\*[.\\-_!/]\*
用户可以通过在 struts.xml 中设置一个新常量来更改定义,如下所示:
<constant name="struts.allowed.action.names" value="[a-zA-Z]\*"/\>删除对导致执行表达式的双重评估OgnlTextParser其中 TextParseUtil.translateVariables,在DefaultActionMapper类增加了一个cleanupActionName方法去处理actionname的恶意代码,并返回安全的actionname。

—Struts2.3.20+(S2-032)
准确来说是S2-029时,Struts2.3.20+版本的沙箱又一次发生变化。SecurityMemberAccess中增加了excludedClasses,excludedPackageNames以及
excludedPackageNamePatterns三个黑名单属性。这三个属性在
SecurityMemberAccess#isAccessible方法中遍历判断了当前操作类是否在黑名单类中,而在OGNL表达式执行时OgnlRuntime类中callConstructor、getMethodValue、setMethodValue、getFieldValue、isFieldAccessible、isMethodAccessible、invokeMethod调用此方法。也即是在OGNL表达式在执行以上操作时,判断当前操作类是否在黑名单中,配置文件struts-default.xml定义黑名单属性。
此时的PoC特征是通过 DefaultMemberAccess 替换 SecurityMemberAccess来完成:#_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS
这样OGNL计算时的规则就被替换成为DefaultMemberAccess中的规则,也就没有了黑名单的限制以及静态方法的限制。此PoC方法获取类的静态属性通过 ognl.OgnlRuntime#getStaticField获得,而该方法中没有调用isAccessible方法,故通过@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS可以获取到DefaultMemberAccess对象,赋值给上下文环境中的_memberAccess,从而Bypass黑名单限制。

—Struts2.3.30+及struts2.5.2+(S2-045)
该版本下的沙箱机制又有改变,进一步增加SecurityMemberAccess
中的黑名单的限制。其中将ognl.DefaultMemberAccess以及ognl.MemberAccess加入黑名单,同时在Struts2.3.30使用的ognl-3.0.19.jar包、struts2.5.2使用的ognl-3.1.10.jar包中的OgnlContext不再支持使用#_memberAccess获得MemberAccess实例。
此时PoC又展现另外的利用角度。通过ognl.OgnlContext#setMemberAccess方法将DefaultMemberAccess设为OGNL表达式评估求值的规则:

(\#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).   (\#context.setMemberAccess(\#dm))

由于黑名单限制是不再支持通过#_memberAccess的形式获取实例,利用方式变成直接改变OgnlContext中的_memberAccess属性,从而实现非期望动作。但是调用setMemberAccess方法会触发检查黑名单,ognl.OgnlContext俨然在黑名单中。清空黑名单的方式首次出现,通过容器获取OgnlUtil实例,由于OgnlUtil是单例模式实现的对象,所以获取到的实例唯一,接着调用get方法获取黑名单集合,clear()方法清空集合。即属性值。

(\#container=\#context[‘com.opensymphony.xwork2.ActionContext.container’]).
(\#ognlUtil=
\#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).
(\#ognlUtil.getExcludedPackageNames().clear()).
(\#ognlUtil.getExcludedClasses().clear())

OgnlUtil能置空SecurityMemberAccess黑名单的原因是作为过滤器起核心控制功能的StrutsPrepareAndExecuteFilter#doFilter初始化OgnlValueStack中
SecurityMemberAccess的黑名单集合时,是通过ognlUtil中的黑名单集合进行赋值的,即共享同一个黑名单地址,具体的执行命令请参考S2-045的PoC,这里简述思路就是将DefaultMemberAccess存入OgnlContext上下文环境中,接着使用三目运算符主要为了适配低版本中可以直接取到_memberAccess对象,取不到就按前面绕过的形式将黑名单置空并将DefaultMemberAccess设为默认安全策略。

—Struts2.5.13+(S2-057)
此版本时沙箱的excludedClasses等黑名单集合设为不可变集合(从struts
2.5.12开始就不再可变)通过前面S2-045的PoC思路中的clear()函数来置空黑名单不再可行。同时struts2.5.13使用的ognl-3.1.15.jar包中OgnlContext不再支持使用#context获取上下文环境。
针对上述获取上下文环境context的方式是通过上下文环境中其他属性(比如attr)来获得context:\#attr['struts.valueStack'].context
至此置空黑名单的新打开方式又可以通过setExcludedXXX('')方法实现出现:(\#ognlUtil.setExcludedClasses('')).(\#ognlUtil.setExcludedPackageNames(''))
但是执行setExcludedXXX('')的方法中的调用Collections.unmodifiableSet(classes)其实是返回一个新的空集合,并不是之前那个_memberAccess和ognlUtil共享的那个黑名单地址的集合,这里通过再次发送请求即可。原因是提到过OgnlUtil是单例模式实现,应用从始至终都用的同一个OgnlUtil,而_memberAccess的作用域是在一次请求范围内的,与此同时OgnlUtil中的黑名单集合已经置空,那么重新发一次请求,_memberAccess将重新初始化,通过OgnlUtil中为空的黑名单进行赋值。
直接参考S2-057 PoC的两次请求包:

/${(#context=#attr['struts.valueStack'].context).(#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.setExcludedClasses('')).(#ognlUtil.setExcludedPackageNames(''))}/login.action

/${(#context=#attr['struts.valueStack'].context).(#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)).(@java.lang.Runtime@getRuntime().exec('calc'))}/login

—Struts2.5.22+(S2-061)
之前Struts2.5.20中使用的ognl-3.1.21.jar包ognl.OgnlRuntime#getStaticField
中调用了isAccessible方法,同时OgnlUtil中set黑名单集合等修饰符由 public 变成
protected。
在Struts2.5.22+中,ognl.OgnlRuntime#invokeMethod方法调用时屏蔽常用的类,即是就算将黑名单Bypass方法调用时仍会判断是否是这些常用的类。到这才算正正式进入第三阶段的安全机制。同时struts-default.xml中定义的黑名单再次增加。此时的黑名单导致之前的Bypass方式,通过容器创建实例; @ognl.OgnlContext@DEFAULT_MEMBER_ACCESS获得DefaultMemberAccess实例;使用#attr['struts.valueStack'].context获得上下文环境等全部失效。
到S2-059的修复完成的第三阶段安全机制又被S2-061的Bypass方式突破,其中涉及两个初次引用类:
org.apache.tomcat.InstanceManager
使用其默认实现类DefaultInstanceManager的newInstance方法来创建实例。
org.apache.commons.collections.BeanMap
-通过BeanMap调用setBean方法可以将类实例存入BeanMap中,存入同时进行初始化将其set、get方法存入当前的writeMethod、readMethod集合中;
-通过BeanMap调用get方法可以在当前bean的readMethod集合中找到对应get方法,再反射调用该方法返回一个对象;
-通过BeanMap调用put方法可以在当前bean的writeMethod集合中找到对应set方法,再反射调用该方法。
网上关于S2-061Bypass的复现和剖析几乎详尽到无可附加,但还是有必要记录一下这个最年轻的PoC。

%25{(\#im=\#application['org.apache.tomcat.InstanceManager']).
(\#bm=\#im.newInstance('org.apache.commons.collections.BeanMap')).
(\#vs=\#request['struts.valueStack']).
(\#bm.setBean(\#vs)).(\#context=\#bm.get('context')).
(\#bm.setBean(\#context)).(\#access=\#bm.get('memberAccess')).
(\#bm.setBean(\#access)).
(\#empty=\#im.newInstance('java.util.HashSet')).
(\#bm.put('excludedClasses',\#empty)).(\#bm.put('excludedPackageNames',\#empty)).
(\#cmdout=\#im.newInstance('freemarker.template.utility.Execute').exec({'whoami'}))}

首先从application中获得DefaultInstanceManager实例,调用newInstance方法获得BeanMap实例。接着先将OgnlValueStack存入BeanMap中,通过get方法可以获得OgnlContext实例,获得OgnlContext实例就可以通过其获得MemberAccess实例,接着可以通过put方法调用set方法,将其黑名单置空,黑名单置空后就可以创建一个黑名单中的类实例来执行命令。

—Struts2.5.26
Struts2.5.26版本中再一次增加黑名单,中间件调用包也被屏蔽。至此,Struts2的版本修复催化的安全机制已经梳理完毕,其中针对阶段性的PoC也只说明当时的一种可行性方式,并不没有绝对意味,同时也不意味着最新PoC就能适用低版本。PoC对应的攻击和官方修复给出的防御一步一步演变为今天的安全机制,将来Struts2还会不会再出RCE,较为客观评价就是有对应价值驱动,突破当前安全机制的投入产出比可能是个客观因素。
【参考文献】
(文献太多给出部分参考,余下后续再整理) https://lucifaer.com/2019/01/16/%E6%B5%85%E6%9E%90OGNL%E7%9A%84%E6%94%BB%E9%98%B2%E5%8F%B2/
https://cloud.tencent.com/developer/article/1024093
https://mp.weixin.qq.com/s?__biz=MzU5NDgxODU1MQ==&mid=2247493528&idx=1&sn=a312582f4797cdc357c08f7bee69dfb5&chksm=fe79c300c90e4a164c8e85d8d4b917f5c1d5bac0acda0b50efdd5b882689fbb04c815c6c6b34&scene=178&cur_album_id=1791937637906219012#rd https://si1ent.xyz/2021/02/26/Struts2%E6%BC%8F%E6%B4%9E%E6%B1%87%E6%80%BB/
https://zhuanlan.zhihu.com/p/50896816