Java Web安全入门——S2-001漏洞分析
Java-Web入门第一篇,S2-001漏洞复现&原理分析
1. 漏洞简介
官方公告:https://cwiki.apache.org/confluence/display/WW/S2-001
漏洞影响范围:WebWork 2.2.0-WebWork 2.2.5,Struts 2.0.0-Struts 2.0.8
2. Struts2 架构&请求处理流程

在该图中,一共给出了四种颜色的标识,其对应的意义如下。
- Servlet Filters(橙色):过滤器,所有的请求都要经过过滤器的处理。
- Struts Core(浅蓝色):Struts2的核心部分。
- Interceptors(浅绿色):Struts2的拦截器。
- User created(浅黄色):需要开发人员创建的部分。
图中的一些组件的作用如下:
- FilterDispatcher:是整个Struts2的调度中心,也就是整个MVC架构中的C,它根据ActionMapper的结果来决定是否处理请求。
- ActionMapper:用来判断传入的请求是否被Struts2处理,如果需要处理的话,ActionMapper就会返回一个对象来描述请求对应的ActionInvocation的信息。
- ActionProxy:用来创建一个ActionInvocation代理实例,它位于Action和xwork之间。
- ConfigurationManager:是xwork配置的管理中心,可以把它当做已经读取到内存中的
struts.xml配置文件。 - struts.xml:是Stuts2的应用配置文件,负责诸如URL与Action之间映射的配置、以及执行后页面跳转的Result配置等。
- ActionInvocation:用来真正的调用并执行Action、拦截器和对应的Result,作用类似于一个调度器。
- Interceptor:拦截器,可以自动拦截Action,主要在Action运行之前或者Result运行之后来进行执行,开发者可以自定义。
- Action:是Struts2中的动作执行单元。用来处理用户请求,并封装业务所需要的数据。
- Result:是不同视图类型的抽象封装模型,不同的视图类型会对应不同的Result实现,Struts2中支持多种视图类型,比如Jsp,FreeMarker等。
- Templates:各种视图类型的页面模板,比如JSP就是一种模板页面技术。
- Tag Subsystem:Struts2的标签库,它抽象了三种不同的视图技术JSP、velocity、freemarker,可以在不同的视图技术中,几乎没有差别的使用这些标签。
接下来我们可以结合上图,来了解下Struts2框架是如何处理一个HTTP请求的。
当HTTP请求发送个Web服务器之后,Web服务器根据用户的请求以及web.xml中的配置文件,将请求转发给Struts2框架进行处理。
- HTTP请求经过一系列的过滤器,最后到达
FilterDispatcher过滤器。 FilterDispatcher将请求转发给ActionMapper,判断该请求是否需要处理。- 如果该请求需要处理,
FilterDispatcher会创建一个ActionProxy来进行后续的处理。 ActionProxy拿着HTTP请求,询问struts.xml该调用哪一个Action进行处理。- 当知道目标
Action之后,实例化一个ActionInvocation来进行调用。 - 然后运行在
Action之前的拦截器,图中就是拦截器1、2、3。 - 运行
Action,生成一个Result。 Result根据页面模板和标签库,生成要响应的内容。- 根据响应逆序调用拦截器,然后生成最终的响应并返回给Web服务器。
3. 漏洞复现
常用Payload
获取tomcat路径
1 | |

获取Web路径
1 | |

执行命令
1 | |

4. 漏洞分析
我们就以%{1+1}作为Payload,来分析一下漏洞产生的原因。
首先给我们自定义的Action上下一个断点,然后发送一个请求。
从调用栈中,我们可以知道,在DefaultActionInvocation类中反射调用了我们自定义的类LoginAction。
路径:xwork-2.0.3-sources.jar!/com/opensymphony/xwork2/DefaultActionInvocation.java

此时到达自定义类LoginAction的username的值为%{1+1}。

从官方公布的漏洞详情中,我们可以知道,漏洞是出现在Struts2重新渲染jsp时,对ognl表达式进行了递归解析,导致了恶意的表达式被执行。
让我们继续往下跟,直到进入了TextParseUtil中。
路径:xwork-2.0.3-sources.jar!/com/opensymphony/xwork2/util/TextParseUtil.java
1 | |
在这里下了断点之后,程序进入了该方法好几次,而且每次的expression的值也不一样。
在手册中,我们也可以查到该方法的作用是将变量转换为对象。

https://struts.apache.org/maven/struts2-core/apidocs/index.html
当expression为username时,从调用栈中我们可以看到整个调用过程。

读取index.jsp的标签

通过UIBean将标签解析出来

然后将其传入到了translateVariables方法中。
经过两次调用之后,
1 | |
传入的expression的值变为了%{username}。

在后面的findValue方法中获得了我们传入的Payload%{1+1},然后将其存入到了o中。
继续往下走,%{1+1}还是满足ognl表达式的规则,于是又进行了一次调用。

此时的expression的值为%{1+1},在后面使用findValue对表达式进行了解析,返回的值2。

在这之后,2这个值是不满足表达式的规则,于是直接将值进行了返回。

由于没有进行验证,导致我们的输入%{1+1}被程序错误的当做了表达式进行了解析,返回了值2,最终显示在响应的表单中。
5. 补丁分析
通过对比xwork2.0.3和2.0.4版本的源码,我们很容易可以发现区别。

添加了一个maxLoopCount属性,限制了递归解析的最大数目。
1 | |
从而在解析到%{1+1}时,不会继续向下递归了,这样就修复了该漏洞。
6. Reference
- https://www.kingkk.com/2018/08/%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B%E5%AD%A6%E4%B9%A0struts2-S2-001/
- https://chybeta.github.io/2018/02/06/%E3%80%90struts2-%E5%91%BD%E4%BB%A4-%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90%E7%B3%BB%E5%88%97%E3%80%91S2-001/
- https://seaii-blog.com/index.php/2019/09/20/90.html
- https://struts.apache.org/core-developers/big-picture
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!