Struts2-001 漏洞分析

漏洞概要

可参考官方安全公告:https://cwiki.apache.org/confluence/display/WW/S2-001

漏洞分析

在HTTP请求被Struts2处理时,首先读取web.xml文件,这个是网站配置文件,里面有个过滤器,叫:org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter然后这个过滤器执行完之后,会经过一系列的拦截器,这些拦截器可以是默认的,也是可以用户自定义的。

Struts2请求处理流程(来自攻击JavaWeb应用[5]):

image-20201225113205710

1
2
3
4
5
6
7
8
9
这里科普几个概念
拦截器概念
拦截器(Interceptor)是Struts2框架的核心功能之一,Struts 2是一个基于MVC设计模式的开源框架, [3] 主要完成请求参数的解析、将页面表单参数赋给值栈中相应属性、执行功能检验、程序异常调试等工作。Struts2拦截器是一种可插拔策略,实现了面向切面的组件开发,当需要扩展功能时,只需要提供对应拦截器,并将它配置在Struts2容器中即可,如果不需要该功能时,也只需要在配置文件取消该拦截器的设置,整个过程不需要用户添加额外的代码。拦截器中更为重要的概念即拦截器栈(Interceptor Stack),拦截器栈就是Struts2中的拦截器按一定的顺序组成的一个线性链,页面发出请求,访问Action对象或方法时,栈中被设置好的拦截器就会根据堆栈的原理顺序的被调用。

说人话:struts2是框架,封装的功能都是在拦截器里面,封装很多功能,有很多拦截器,不是每次这些拦截器都执行,每次执行默认的拦截器,默认拦截器位置struts2-core-2.0.8.jar!\struts-default.xml,在执行拦截器,执行过程使用aop思想,在action没有直接调用拦截器方法,而是使用配置文件进行操作,在执行拦截器时候,执行很多的拦截器,这个过程使用责任链模式,例如:执行三个拦截器,执行拦截器1->执行完放行->执行拦截器2->执行完放行->执行拦截器3->执行完放行->执行action方法。


拦截器什么时候执行呢?
在action对象之后,action方法执行之前

例如下图struts.xml中的package 继承了struts默认的拦截器(struts-default),具体可以查看struts-default.xml文件。

image-20201218102840514

image-20201218104357386

这里我们要关注params这个拦截器,代码位置:xwork-2.0.3.jar!\com\opensymphony\xwork2\interceptor\ParametersInterceptor.class

image-20201218111703265

image-20201218113626884

经过一系列的拦截器处理后,数据会成功进入实际业务 Action。程序会根据Action 处理的结果,选择对应的 JSP视图进行展示,并对视图中的 Struts2 标签进行处理。

在本实例中Action处理用户登录是返回error

image-20201218144456659

根据返回结果以及先前在struts.xml中定义的视图,程序将开始处理 index.jsp

image-20201218144537239

image-20201218144810408

从代码里我们可以看得到,struts2使用了自定义标签库,也就是/struts-tags, 通过阅读 struts2-core-2.0.8.jar!/META-INF/struts-tags.tld文件,我们得知这个textfield标签实现类是org.apache.struts2.views.jsp.ui.TextFieldTag

image-20201220153156591

image-20201220153226282

了解jsp自定义标签的同学应该知道,这时候我们需要找的是doStartTag方法,因为解析标签是从这个方法开始,具体可以参考 TagSupport详解, 通过在TextFieldTag类的ComponentTagSupport父类我们找到doStartTag方法

当在JSP 文件中遇到 Struts2标签 时,由于s2的标签库都是集成与ComponentTagSupport类,程序会先调用 doStartTag ,并将标签中的属性设置到 TextFieldTag对象相应属性中。

image-20201218154513302

最后,在遇到 />结束标签的时候调用 doEndTag 方法。

image-20201218152120932

1
2
3
4
5
public int doEndTag() throws JspException {
this.component.end(this.pageContext.getOut(), this.getBody());
this.component = null;
return 6;
}

我们跟进this.component.end方法,该方法调用了 this.evaluateParams();方法来填充JSP中的动态数据。

image-20201218152528873

跟进this.evaluateParams方法,发现如果开启OGNL表达式支持(this.altSyntax()),会进行属性字段添加OGNL表达式字符(%{name})

image-20201218152851979

然后使用findValue方法从值栈中获得该表达式所对应的值,跟进findValue方法

image-20201218153309872

findValue在开启了altSyntaxtoTypeclass.java.lang.string时调用TextParseUtil.translateVariables方法

image-20201218153519551

跟进该方法

image-20201218153626893

发现该方法重名加载

image-20201218153722178

我们传入translateVariables 方法的表达式 expression%{password} ,经过 OGNL表达式解析,程序会获得其值 %{1+1}(这里就是我们传入的payload)。由于此处使用的是 while循环来解析OGNL ,所以获得的%{1+1}又会被再次循环解析,最终也就造成了任意代码执行。

image-20201218154234294

关键代码:

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
public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, TextParseUtil.ParsedValueEvaluator evaluator) {
Object result = expression;

while(true) {
int start = expression.indexOf(open + "{");
int length = expression.length();
int x = start + 2;
int count = 1;

while(start != -1 && x < length && count != 0) {
char c = expression.charAt(x++);
if (c == '{') {
++count;
} else if (c == '}') {
--count;
}
}

int end = x - 1;
if (start == -1 || end == -1 || count != 0) {
return XWorkConverter.getInstance().convertValue(stack.getContext(), result, asType);
}

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 {
result = left + right;
expression = left + right;
}
}
}

因此究其原因,在于在translateVariables中,递归解析了表达式,在处理完%{password}后将password的值直接取出并继续在while循环中解析,若用户输入的password是恶意的OGNL表达式,比如%{1+1},则得以解析执行。

POC:

1
%{1+1}

修复

增加了了递归解析的判断

image-20201224171538096

参考

https://xz.aliyun.com/t/7915

https://xz.aliyun.com/t/2044

https://dean2021.github.io/posts/s2-001/

https://cwiki.apache.org/confluence/display/WW/S2-001

-本文结束感谢您的阅读-