Java 反序列化工具 gadgetinspector 初窥

作者:Longofo@知道创宇404实验室
时间:2019年9月4日
原文链接:https://paper.seebug.org/1034/

起因

一开始是听@Badcode师傅说的这个工具,在Black Hat 2018的一个议题提出来的。这是一个基于字节码静态分析的、利用已知技巧自动查找从source到sink的反序列化利用链工具。看了几遍作者在Black Hat上的演讲视频与PPT,想从作者的演讲与PPT中获取更多关于这个工具的原理性的东西,可是有些地方真的很费解。不过作者开源了这个工具,但没有给出详细的说明文档,对这个工具的分析文章也很少,看到一篇平安集团对这个工具的分析,从文中描述来看,他们对这个工具应该有一定的认识并做了一些改进,但是在文章中对某些细节没有做过多的阐释。后面尝试了调试这个工具,大致理清了这个工具的工作原理,下面是对这个工具的分析过程,以及对未来工作与改进的设想。

关于这个工具

这个工具不是用来寻找漏洞,而是利用已知的source->…->sink链或其相似特征发现分支利用链或新的利用链。
这个工具是在整个应用的classpath中寻找利用链。
这个工具进行了一些合理的预估风险判断(污点判断、污点传递等)。
这个工具会产生误报不是漏报(其实这里还是会漏报,这是作者使用的策略决定的,在后面的分析中可以看到)。
这个工具是基于字节码分析的,对于Java应用来说,很多时候我们并没有源码,而只有War包、Jar包或class文件。
这个工具不会生成能直接利用的Payload,具体的利用构造还需要人工参与。

序列化与反序列化

序列化(Serialization)是将对象的状态信息转化为可以存储或者传输形式的过程,转化后的信息可以存储在磁盘上,在网络传输过程中,可以是字节、XML、JSON等格式;而将字节、XML、JSON等格式的信息还原成对象这个相反的过程称为反序列化。

在JAVA中,对象的序列化和反序列化被广泛的应用到RMI(远程方法调用)及网络传输中。

Java中的序列化与反序列化库

JDK(ObjectInputStream)

XStream(XML,JSON)

Jackson(XML,JSON)

Genson(JSON)

JSON-IO(JSON)

FlexSON(JSON)

Fastjson(JSON)

不同的反序列化库在反序列化不同的类时有不同的行为、被反序列化类的不同"魔术方法"会被自动调用,这些被自动调用的方法就能够作为反序列化的入口点(source)。如果这些被自动调用的方法又调用了其他子方法,那么在调用链中某一个子方法也可以作为source,就相当于已知了调用链的前部分,从某个子方法开始寻找不同的分支。通过方法的层层调用,可能到达某些危险的方法(sink)。

ObjectInputStream

例如某个类实现了Serializable接口,ObjectInputStream.readobject在反序列化类得到其对象时会自动查找这个类的readObject、readResolve等方法并调用。

例如某个类实现了Externalizable接口,ObjectInputStream.readobject在反序列化类得到其对象时会自动查找这个类的readExternal等方法并调用。

Jackson

ObjectMapper.readValue在反序列化类得到其对象时,会自动查找反序列化类的无参构造方法、包含一个基础类型参数的构造方法、属性的setter、属性的getter等方法并调用。

在后面的分析中,都使用JDK自带的ObjectInputStream作为样例。

控制数据类型=>控制代码
作者说,在反序列化漏洞中,如果控制了数据类型,我们就控制了代码。这是什么意思呢?按我的理解,写了下面的一个例子:

public class TestDeserialization {

interface Animal {
    public void eat();
}

public static class Cat implements Animal,Serializable {
    @Override
    public void eat() {
        System.out.println("cat eat fish");
    }
}

public static class Dog implements Animal,Serializable {
    @Override
    public void eat() {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("dog eat bone");
    }
}

public static class Person implements Serializable {
    private Animal pet;

    public Person(Animal pet){
        this.pet = pet;
    }

    private void readObject(java.io.ObjectInputStream stream)
            throws IOException, ClassNotFoundException {
        pet = (Animal) stream.readObject();
        pet.eat();
    }
}

public static void GeneratePayload(Object instance, String file)
        throws Exception {
    //将构造好的payload序列化后写入文件中
    File f = new File(file);
    ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f));
    out.writeObject(instance);
    out.flush();
    out.close();
}

public static void payloadTest(String file) throws Exception {
    //读取写入的payload,并进行反序列化
    ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
    Object obj = in.readObject();
    System.out.println(obj);
    in.close();
}

public static void main(String[] args) throws Exception {
    Animal animal = new Dog();
    Person person = new Person(animal);
    GeneratePayload(person,"test.ser");
    payloadTest("test.ser");

//        Animal animal = new Cat();
//        Person person = new Person(animal);
//        GeneratePayload(person,"test.ser");
//        payloadTest("test.ser");
    }
}

为了方便我把所有类写在一个类中进行测试。在Person类中,有一个Animal类的属性pet,它是Cat和Dog的接口。在序列化时,我们能够控制Person的pet具体是Cat对象或者Dog对象,因此在反序列化时,在readObject中pet.eat()具体的走向就不一样了。如果是pet是Cat类对象,就不会走到执行有害代码
Runtime.getRuntime().exec(“calc”);这一步,但是如果pet是Dog类的对象,就会走到有害代码。

即使有时候类属性在声明时已经为它赋值了某个具体的对象,但是在Java中通过反射等方式依然能修改。如下:

public class TestDeserialization {

interface Animal {
    public void eat();
}

public static class Cat implements Animal, Serializable {
    @Override
    public void eat() {
        System.out.println("cat eat fish");
    }                           
}

public static class Dog implements Animal, Serializable {
    @Override
    public void eat() {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("dog eat bone");
    }
}

public static class Person implements Serializable {
    private Animal pet = new Cat();

    private void readObject(java.io.ObjectInputStream stream)
            throws IOException, ClassNotFoundException {
        pet = (Animal) stream.readObject();
        pet.eat();
    }
}

public static void GeneratePayload(Object instance, String file)
        throws Exception {
    //将构造好的payload序列化后写入文件中
    File f = new File(file);
    ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f));
    out.writeObject(instance);
    out.flush();
    out.close();
}

public static void payloadTest(String file) throws Exception {
    //读取写入的payload,并进行反序列化
    ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
    Object obj = in.readObject();
    System.out.println(obj);
    in.close();
}

public static void main(String[] args) throws Exception {
    Animal animal = new Dog();
    Person person = new Person();

    //通过反射修改私有属性
    Field field = person.getClass().getDeclaredField("pet");
    field.setAccessible(true);
    field.set(person, animal);

    GeneratePayload(person, "test.ser");
    payloadTest("test.ser");
}

}
在Person类中,不能通过构造器或setter方法或其他方式对pet赋值,属性在声明时已经被定义为Cat类的对象,但是通过反射能将pet修改为Dog类的对象,因此在反序列化时依然会走到有害代码处。

这只是我自己对作者"控制了数据类型,就控制了代码"的理解,在Java反序列化漏洞中,很多时候是利用到了Java的多态特性来控制代码走向最后达到恶意执行目的。

魔术方法

在上面的例子中,能看到在反序列化时没有调用Person的readobject方法,它是ObjectInputStream在反序列化对象时自动调用的。作者将在反序列化中会自动调用的方法称为"魔术方法"。

使用ObjectInputStream反序列化时几个常见的魔术方法:

Object.readObject()
Object.readResolve()
Object.finalize()

一些可序列化的JDK类实现了上面这些方法并且还自动调用了其他方法(可以作为已知的入口点):

· HashMap
Object.hashCode()
Object.equals()
· PriorityQueue
Comparator.compare()
Comparable.CompareTo()
· …
一些sink:

Runtime.exec(),这种最为简单直接,即直接在目标环境中执行命令
Method.invoke(),这种需要适当地选择方法和参数,通过反射执行Java方法
RMI/JNDI/JRMP等,通过引用远程对象,间接实现任意代码执行的效果

作者给出了一个从Magic Methods(source)->Gadget Chains->Runtime.exec(sink)的例子:

在这里插入图片描述

上面的HashMap实现了readObject这个"魔术方法",并且调用了hashCode方法。某些类为了比较对象之间是否相等会实现equals方法(一般是equals和hashCode方法同时实现)。从图中可以看到AbstractTableModelKaTeX parse error: Expected group after '_' at position 58: …是IFn对象,并且f能通过属性_̲_clojureFnMap获取…ff19274a.hashcode中的f.invoke具体选择IFn的哪个实现类,根据后面对这个工具的测试以及对决策原理的分析,广度优先会选择短的路径,也就是选择了FnEval.invoke,所以这也是为什么要人为参与,在后面的样例分析中也可以看到)。

有了这条链,只需要找到触发这个链的漏洞点就行了。Payload使用JSON格式表示如下:

{
    "@class":"java.util.HashMap",
    "members":[
        2,
        {
            "@class":"AbstractTableModel$ff19274a",
            "__clojureFnMap":{
                "hashcode":{
                    "@class":"FnCompose",
                    "f1":{"@class","FnConstant",value:"calc"},
                    "f2":{"@class":"FnEval"}
                }
            }
        }
    ]
}

gadgetinspector工作流程
如作者所说,正好使用了五个步骤:

    // 枚举全部类以及类的所有方法
    if (!Files.exists(Paths.get("classes.dat")) || !Files.exists(Paths.get("methods.dat"))
            || !Files.exists(Paths.get("inheritanceMap.dat"))) {
        LOGGER.info("Running method discovery...");
        MethodDiscovery methodDiscovery = new MethodDiscovery();
        methodDiscovery.discover(classResourceEnumerator);
        methodDiscovery.save();
    }
    //生成passthrough数据流
    if (!Files.exists(Paths.get("passthrough.dat"))) {
        LOGGER.info("Analyzing methods for passthrough dataflow...");
        PassthroughDiscovery passthroughDiscovery = new PassthroughDiscovery();
        passthroughDiscovery.discover(classResourceEnumerator, config);
        passthroughDiscovery.save();
    }
    //生成passthrough调用图
    if (!Files.exists(Paths.get("callgraph.dat"))) {
        LOGGER.info("Analyzing methods in order to build a call graph...");
        CallGraphDiscovery callGraphDiscovery = new CallGraphDiscovery();
        callGraphDiscovery.discover(classResourceEnumerator, config);
        callGraphDiscovery.save();
    }
    //搜索可用的source
    if (!Files.exists(Paths.get("sources.dat"))) {
        LOGGER.info("Discovering gadget chain source methods...");
        SourceDiscovery sourceDiscovery = config.getSourceDiscovery();
        sourceDiscovery.discover();
        sourceDiscovery.save();
    }
    //搜索生成调用链
    {
        LOGGER.info("Searching call graph for gadget chains...");
        GadgetChainDiscovery gadgetChainDiscovery = new GadgetChainDiscovery(config);
        gadgetChainDiscovery.discover();
    }

Step1 枚举全部类以及每个类的所有方法

要进行调用链的搜索,首先得有所有类及所有类方法的相关信息:

public class MethodDiscovery {

private static final Logger LOGGER = LoggerFactory.getLogger(MethodDiscovery.class);

private final List<ClassReference> discoveredClasses = new ArrayList<>();//保存所有类信息
private final List<MethodReference> discoveredMethods = new ArrayList<>();//保存所有方法信息
...
...
public void discover(final ClassResourceEnumerator classResourceEnumerator) throws Exception {
    //classResourceEnumerator.getAllClasses()获取了运行时的所有类(JDK rt.jar)以及要搜索应用中的所有类
    for (ClassResourceEnumerator.ClassResource classResource : classResourceEnumerator.getAllClasses()) {
        try (InputStream in = classResource.getInputStream()) {
            ClassReader cr = new ClassReader(in);
            try {
                cr.accept(new MethodDiscoveryClassVisitor(), ClassReader.EXPAND_FRAMES);//通过ASM框架操作字节码并将类信息保存到this.discoveredClasses,将方法信息保存到discoveredMethods
            } catch (Exception e) {
                LOGGER.error("Exception analyzing: " + classResource.getName(), e);
            }
        }
    }
}
...
...
public void save() throws IOException {
    DataLoader.saveData(Paths.get("classes.dat"), new ClassReference.Factory(), discoveredClasses);//将类信息保存到classes.dat
    DataLoader.saveData(Paths.get("methods.dat"), new MethodReference.Factory(), discoveredMethods);//将方法信息保存到methods.dat

    Map<ClassReference.Handle, ClassReference> classMap = new HashMap<>();
    for (ClassReference clazz : discoveredClasses) {
        classMap.put(clazz.getHandle(), clazz);
    }
    InheritanceDeriver.derive(classMap).save();//查找所有继承关系并保存
}

}
来看下classes.dat、methods.dat分别长什么样子:

classes.dat

找了两个比较有特征的

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值