漏洞分析
由上图工作流程我们可以看到,当一个 HTTP 请求被 Struts2 处理时,会经过一系列的 拦截器(Interceptor) ,这些拦截器可以是 Struts2 自带的,也可以是用户自定义的。例如下图 struts.xml 中的 package 继承自 struts-default ,而 struts-default 就使用了 Struts2 自带的拦截器。
找到默认使用的拦截器栈
在拦截器栈 defaultStack 中,我们需要关注 params 这个拦截器。其中, params拦截器 会将客户端请求数据设置到 值栈(valueStack) 中,后续 JSP 页面中所有的动态数据都将从值栈中取出。
在经过一系列的拦截器处理后,数据会成功进入实际业务 Action 。程序会根据 Action 处理的结果,选择对应的 JSP 视图进行展示,并对视图中的 Struts2 标签进行处理。如下图,在本例中 Action 处理用户登录失败时会返回 error 。
然后
/com/opensymphony/xwork2/DefaultActionInvocation.class:253
继续往下,主要问题在translateVariables这个函数里
/** * Converted object from variable translation. * * @param open * @param expression * @param stack * @param asType * @param evaluator * @return Converted object from variable translation. */ 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); }
第一次执行的时候 会取出%{username}的值,即%{1+1}
通过if ((start != -1) && (end != -1) && (count == 0))的判断,跳过return
通过Object o = stack.findValue(var, asType);把值赋给o
然后赋值给expression,进行下一次循环
第二次循环会执行我们构造的OGNL表达式
可以看到执行后结果为2
然后再次循环,经过if判断过后return
后面经过处理后返回index.jsp
漏洞成因呢就是在translateVariables函数中递归来验证OGNL表达式,造成了OGNL表达式的执行
漏洞修复
官方修复代码
public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, ParsedValueEvaluator evaluator, int maxLoopCount) { // deal with the "pure" expressions first! //expression = expression.trim(); Object result = expression; int loopCount = 1; int pos = 0; while (true) { int start = expression.indexOf(open + "{", pos); if (start == -1) { pos = 0; loopCount++; start = expression.indexOf(open + "{"); } if (loopCount > maxLoopCount) { // translateVariables prevent infinite loop / expression recursive evaluation break; } 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); String middle = null; if (o != null) { middle = o.toString(); if (!TextUtils.stringSet(left)) { result = o; } else { result = left + middle; } if (TextUtils.stringSet(right)) { result = result + right; } expression = left + middle + right; } else { // the variable doesn't exist, so don't display anything result = left + right; expression = left + right; } pos = (left != null && left.length() > 0 ? left.length() - 1: 0) + (middle != null && middle.length() > 0 ? middle.length() - 1: 0) + 1; pos = Math.max(pos, 1); } else { break; } } return XWorkConverter.getInstance().convertValue(stack.getContext(), result, asType); }
可以看到增加了对OGNL递归解析次数的判断,默认情况下只会解析第一层
if (loopCount > maxLoopCount) { // translateVariables prevent infinite loop / expression recursive evaluation break; }
总结
入门找了S2-001跟着师傅们的文章学习了一下,原理还是很简单,就是调试java过程很费时间。
最后弹个计算器收尾吧,(不知道为什么mac上
弹/System/Application/Calculator.app没弹成功
%{(new java.lang.ProcessBuilder(new java.lang.String[]{"calc.exe"})).start()}
参考
https://mochazz.github.io/2020/06/16/Java代码审计之Struts2-001/#漏洞分析