IT技术博客大学习 共学习 共进步
全部 移动开发 后端 数据库 AI 算法 安全 DevOps 前端 设计 开发者

JAVA序列化和反序列化及漏洞补救

绿盟科技博客 2016-02-11 14:48:09 累计浏览 2,180 次
本机暂存

不久前,网络安全人员再一次在黑产面前遭到重挫,Joomla曝高危0Day漏洞,无需用户登陆就能触发。Joomla漏洞在官方发布升级版和补丁之前,已经在各种地下黑色产业链中流传了一段时间,恐怕并且已经有不少网站被黑客拿下。  这个恶意代码的进入点是用户代理字符串,这是每个浏览器都在广而告之的内容:让浏览器知道用户的技术结构从而为站点提供最佳或最合适的版本。很显然这个字符串存储于Joomla数据库中,但并没有被清洁处理以检测恶意代码。攻击者能够通过能够播报虚假用户代理字符串的方式利用特殊app和脚本拉力轻易编制一个定制化字符串并将恶意代码附着,这个安全隐患是在PHP中session是通过序列化存储的。

想要了解关于PHP相关的远程代码执行漏洞分析,可参考:vBulletin5远程代码执行漏洞分析

JAVA序列化和反序列化是啥?

在现有很多的应用当中,需要对某些对象进行序列化,让它们离开内存空间,入驻物理硬盘,以便可以长期保存,其中最常见的是Web服务器中的Session对象。对象的序列化一般有两种用途:把对象的字节序列永久地保存到硬盘上,通常存放在一个指定文件中;或者在网络上传送对象的字节序列。

而把字节序列恢复为对象的过程称为对象的反序列化。当两个进程在进行远程通信时,彼此可以发送各种类型的数据,而且无论是何种类型的数据,都会以二进制序列的形式在网络上传送。发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象。

1

其实,在不同的计算机语言中,数据结构、对象以及二进制串的表示方式并不相同。对于像Java这种完全面向对象的语言,程序员所操作的一切都是对象,来自于类的实例化。

JAVA序列化和反序列化实例

在Java语言中最接近数据结构的概念,就是 POJO(Plain Old Java Object)或者Javabean。小编更熟悉Java语言,还是以此为例说明一下序列化和反序列化的实现。

public static void main(String[] args) throws Exception {
    SerializeObject();   //序列化Object对象
    Object o = DeserializeObject();  //反序列Object对象
    System.out.println(MessageFormat.format("name={0},age={1},
    sex={2}", o.getName(),o.getSex(),o.getAge(),o.getHobby()));
}

/**
* MethodName: SerializeObject 
* Description: 序列化Object对象
* @author Haom
* @throws FileNotFoundException
* @throws IOException
*/
private static void SerializeObject() throws FileNotFoundException,  
    IOException {  
    Object object = new Object();  
    object.setName("haom");  
    object.setSex("Female");  
    object.setAge(18);  
    object.setHobby("Taekwondo");  
    // 对于ObjectOutputStream 对象输出流,将Object对象存储到M盘的object.txt文件中,完成对Object对象的序列化操作  
    ObjectOutputStream oo = new ObjectOutputStream(new   
        FileOutputStream(new File("M:/object.txt")));  
    oo.writeObject(object);  
    System.out.println("Object Serialization success!");  
    oo.close();  
}  

/**
* MethodName: DeserializeObject 
* Description: 反序列Object对象
* @author Haom
* @throws Exception
* @throws IOException
*/
private static Object DeserializeObject() throws Exception, 
    IOException {
    ObjectInputStream ois = new ObjectInputStream(new FileInputStream(
        new File("M:/object.txt")));
    Object object = (Object) ois.readObject();
    System.out.println("Object deserialization success!");
    return Object;
}

以上代码说明:序列化Object成功后在M盘生成了一个object.txt文件,而反序列化Object是读取M盘的Object.txt后生成了一个Object对象。

当然,并不是一个实现了序列化接口的类的所有字段及属性,都是可以序列化的:

  • 如果该类有父类,则分两种情况来考虑:如果该父类已经实现了可序列化接口,则其父类的相应字段及属性的处理和该类相同;如果该类的父类没有实现可序列化接口,则该类的父类所有的字段属性将不会序列化,并且反序列化时会调用父类的默认构造函数来初始化父类的属性,而子类却不调用默认构造函数,而是直接从流中恢复属性的值。

  • 如果该类的某个属性标识为static类型的,则该属性不能序列化。

  • 如果该类的某个属性采用transient关键字标识,则该属性不能序列化。

那么,在什么情况下,需要自定义序列化的方式? 先举个简单的例子,如下:

public class SeriDemo1 implements Serializable {
    private String name;
    transient private String password; 
    // 瞬态,不可序列化状态,该字段的生命周期仅存于调用者的内存中
    public SeriDemo1() {
    }
    public SeriDemo1(String name, String password) {
        this.name = name;
        this.password = password;
    }
    //模拟对密码进行加密
    private String change(String password) {
        return password + "minna";
    }
    //写入
    private void writeObject(ObjectOutputStream outStream) throws IOException {
        outStream.defaultWriteObject();
        outStream.writeObject(change(password));
    }
    //读取
    private void readObject(ObjectInputStream inStream) throws IOException,
        ClassNotFoundException {
        inStream.defaultReadObject();
        String strPassowrd = (String) inStream.readObject();
        //模拟对密码解密
        password = strPassowrd.substring(0, strPassowrd.length() - 5);
    }
    //返回一个“以文本方式表示”此对象的字符串
    public String toString() {
        return "SeriDemo1 [name=" + name + ", password=" + password + "]";
    }
    //静态的main
    public static void main(String[] args) throws Exception {
        SeriDemo1 demo = new SeriDemo1("haom", "0123");
        ByteArrayOutputStream buf = new ByteArrayOutputStream();
        ObjectOutputStream out = new ObjectOutputStream(buf);
        out.writeObject(demo);
        ObjectInputStream in = new ObjectInputStream(new 
            ByteArrayInputStream(buf.toByteArray()));
        demo = (SeriDemo1) in.readObject();
        System.out.println(demo);
    }
}

如上代码说明,可以得知,以下情况需要自定义序列化的方式:

  • 为了确保序列化的安全性,可以对于一些敏感信息加密;

  • 确保对象的成员变量符合正确的约束条件;

  • 确保需要优化序列化的性能。

  • 在序列化选型的过程中,安全性的考虑往往发生在跨局域网访问的场景。当通讯发生在公司之间或者跨机房的时候,出于安全的考虑,对于跨局域网的访问往往被限制为基于HTTP/HTTPS的80和443端口。如果使用的序列化协议没有兼容而成熟的HTTP传输层框架支持,可能会导致以下几种结果:

  • 因为访问限制而降低服务可用性。

  • 被迫重新实现安全协议而导致实施成本升高。

  • 开放更多的防火墙端口和协议访问,但是是以牺牲安全性为前提。

反序列化漏洞危害

当应用代码从用户接受序列化数据,并试图反序列化改数据进行下一步处理时,会产生反序列化漏洞,其中最有危害性的就是远程代码注入。

这种漏洞产生原因是,java类ObjectInputStream在执行反序列化时,并不会对自身的输入进行检查,这就说明恶意攻击者可能也可以构建特定的输入,在 ObjectInputStream类反序列化之后会产生非正常结果,利用这一方法就可以实现远程执行任意代码。

这个漏洞的严重风险在于,即使你的代码里没有使用到Apache Commons Collections里的类,只要Java应用的Classpath里有Apache Commons Collections的jar包,都可以远程代码执行。

漏洞的根本问题其实并不是Java序列化的问题,而是Apache Commons Collections允许链式的任意的类函数反射调用。攻击者通过允许Java序列化协议的端口,把攻击代码上传到服务器上,再由Apache Commons Collections里的TransformedMap来执行。

反序列化漏洞补救

2

现在,Apache Commons Collections在 3.2.2版本中做了一定的安全处理,对这些不安全的Java类的序列化支持增加了开关,默认为关闭状态。

涉及的类包括:CloneTransformer,ForClosure, InstantiateFactory, InstantiateTransformer, InvokerTransformer, PrototypeCloneFactory,PrototypeSerializationFactory, WhileClosure。

RedHat发布JBoss相关产品的解决方案:https://access.redhat.com/solutions/2045023。

严格意义说起来,Java相对来说安全性问题比较少,出现的一些问题大部分是利用反射,最终用Runtime.exec(String cmd)函数来执行外部命令的。如果可以禁止JVM执行外部命令,未知漏洞的危害性会大大降低,可以大大提高JVM的安全性。

比如:

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);
        }
    };
   System.setSecurityManager(sm);
}

如上所示,只要在Java代码里简单加一段程序,就可以禁止执行外部程序了。

禁止JVM执行外部命令,是一个简单有效的提高JVM安全性的办法。可以考虑在代码安全扫描时,加强对Runtime.exec相关代码的检测。

小结

本文只是初步进行JAVA序列化和反序列化的科普,让大家对此问题及相关补救方式有个直观的印象和简单了解,下一步深入解还请继续关注绿盟技术博客。使用JAVA反序列化增多了数据的种类,但是还需要尽量避免使用反序列化的交互操作,减少风险的增加。目前,绿盟科技蜂巢社区启动应急机制,已经实现远程代码执行漏洞的在线检测。在社区中,大家可以进行网络安全扫描插件的开发及讨论。

其他相关参考资料:

http://blog.nsfocus.net/java-deserialization-vulnerability-overlooked-mass-destruction/

同分类推荐文章

  1. 绿盟科技《APT组织研究年鉴》(2026 版)正式发布 (2026-06-16 20:21:10)
  2. 【已复现】Linux内核Fragnesia权限提升漏洞(CVE-2026-46300) (2026-06-15 10:53:58)
  3. 企业文档安全最佳实践(二):给文档上“身份证”——手动标密与智能自动标密 (2026-06-12 17:18:33)

查看更多 安全 文章 →

建议继续学习

  1. SmartSprites - 命令行形式的CSS Sprites生成器 (累计阅读 123,895)
  2. Java开发岗位面试题归类汇总 (累计阅读 22,157)
  3. android 开发入门 (累计阅读 19,527)
  4. 我的PHP,Python和Ruby之路 (累计阅读 13,147)
  5. HashMap解决hash冲突的方法 (累计阅读 12,654)
  6. 一个大二学生有关如何成为一名软件工程师的疑问及答复 (累计阅读 9,181)
  7. Java程序员应该知道的10个eclipse调试技巧 (累计阅读 8,012)
  8. 如何让员工忠于公司? (累计阅读 7,940)
  9. Java技术路线 (累计阅读 7,726)
  10. 聊聊ThoughtWorks面试 (累计阅读 7,614)