前情提要
非Agent型内存马。由于非Agent型内存马注入后,会产生新的类和对象,同时还会产生各种错综复杂的相互引用关系,比如要创建一个恶意Filter内存马,需要先修改已有的FilterMap,然后新增FilterConfig、FilterDef,最后还要修改FilterChain,这一系列操作产生的脏数据过多,不够整洁,而且添加类或者数据的动静太大,很容易被查杀.因此我还是认为Agent型内存马才是更理想的内存马。首先拖出这个雪藏过的大马吧!网上看到分析它的文章好像基本没有…,除了作者自己的一些技术分析
1 | package net.rebeyond.behinder.payload.java; |
捋一下过往的agent🐎注入技术
先膜拜传奇老登: rebeyond
-
最传统的方式:2018年,《利用“进程注入”实现无文件复活 WebShell》一文首次提出memShell(内存马)概念,利用Java Agent技术向JVM内存中植入webshell,并在github上发布memShell项目。项目中对内存马的植入过程比较繁琐,需要三个步骤:
- 上传inject.jar到服务器用来枚举jvm并进行植入;
- 上传agent.jar到服务器用来承载webshell功能;
- 执行系统命令java -jar inject.jar。
-
仍然还是要上传一个agent.jar;2020年,Behinder(冰蝎) v3.0版本更新中内置了Java内存马注入功能,此次更新利用self attach技术,将植入过程由上文中的3个步骤减少为2个步骤:
- 上传agent.jar到服务器用来承载webshell功能;
- 冰蝎服务端调用Java API将agent.jar植入自身进程完成注入。
-
对抗技术,防止查杀agent内存马,不能attach,不过此等方法过于暴力,生产环境需谨慎使用;2021年,Behinder(冰蝎)v3.0 Beta 10版本更新中,实现了内存马防检测(我称他为Anti-Attach技术),可以避免在我们注入内存马之后其他人再注入内存马或者扫描内存马。
-
暴力搜索指针位置,动静太大且容易打崩系统;2021年,《Java内存攻击技术漫谈》一文,提出了无文件agent植入技术,整个Agent注入的过程不需要在目标磁盘上落地文件,这勉强解决了“脏”的问题。但是该文中介绍的方法存在一个缺陷,那就是获取JPLISAgent的过程不够优雅和安静,会概率性的导致Java进程崩溃,这是不能忍的,于是就有了这篇文章.
结合作者的思路:
主要关注windows 平台,linux平台实现较为容易
Windows平台下通过Java向自身进程植入并运行shellcode→调用Native层jvm.dll的JNI_GetCreatedJavaVMs→获取JVMTIEnv指针→构造JPLISAgent对象→具备Java Agent的所有能力
所加载的shellcode的主要流程,主要是为了执行JNI_GetCreatedJavaVMs
:
1 | 先获取到当前进程kernel32.dll的基址; |
X86版本:
1 | 00830000 | 90 | nop | |
X64版本:
1 | 00000000541E0000 | 48:83EC 28 | sub rsp,28 | |
为什么要写一个WindowsVirtualMachine
类
这儿就是冰蝎中WindowsVirtualMachine
类的字节码先看看如何通过反射注入shellcode执行任意代码的POC:
1 | import java.lang.reflect.Method; |
我们可以把如上shellcode封装到WindowsVirtualMachine
中去,当然为了注入agent内存马并不是通过它来直接rce,而是为我们获得agent能力去做铺垫.
可以看看dump出来的形式
为啥包名要是package sun.tools.attach;
并且类名叫public class WindowsVirtualMachine
呢?
可以看到前面的反射调用执行shellcode需要调用sun.tools.attach.VirtualMachineImpl
类,其所在的tools.jar
包并不会在每个JDK环境都存在其实这个方法在冰蝎1.0版本的时候就已经解决了,那就是用一个自定义的classLoader。但是我们都知道classLoader在loadClass的时候采用双亲委托机制,也就是如果系统中已经存在一个类,即使我们用自定义的classLoader去loadClass,也会返回系统内置的那个类。但是如果我们绕过loadClass,直接去defineClass即可从我们指定的字节码数组里创建类,而且类名我们可以任意自定义,重写java.lang.String都没问题:) 然后再用defineClass返回的Class去实例化,然后再调用我们想调用的Native函数即可。因为Native函数在调用的时候只检测发起调用的类限定名,并不检测发起调用类的ClassLoader,这是我们这个方法能成功的原因
我们可以利用这个办法来打破双亲委派,实现自定义类名直接加载字节码然后实例化,这也是为什么冰蝎等工具一众推崇将恶意逻辑放入字节码,采用加载任意字节码的方式来执行恶意代码,确实学到了!
可以动手来实现逻辑:
自定义sun.tools.attach
类:
1 | package sun.tools.attach; |
javac命令编译成class,然后转成base64编码内嵌在代码里
1 | import java.io.*; |
这样就可以绕过loadClass的双亲委派机制和没有tools.jar
依赖的问题来加载系统库函数的Native方法,这其实也是冰蝎的核心思路了可以看看冰蝎4中agent大马的成品实现部分:
1 | inject = (new String(inject, "ISO-8859-1")).replace("&sun/tools/attach/WindowsVirtualMachine", "#sun/tools/attach/VirtualMachineImpl").replace("(Lsun/tools/attach/WindowsVirtualMachine;", "%Lsun/tools/attach/VirtualMachineImpl;").getBytes("ISO-8859-1"); |
inject
里放入恶意shellcode即可,这里的shellcode的作用主要是泄露native_jvmtienv地址来快速,准确,安静的获取到JPLISAgent指针,进而手动实例化sun.instrument.InstrumentationImpl
,不需要再落地agent.jar
这个拖油瓶了,然后就拥有了agent的能力,可完美实现无任何文件落地即可注入agent内存马
使用条件
JDK: 目前来说只研究了JDK 8-11,高于11的话jdk部分关键类出现较大改动,需要结合绕过手段
注入前提: 需要一个能够任意加载字节码的突破口子,需要有动态代码执行上下文的能力.
比如常见的: shiro,fastjson,log4j,部分jdbc,文件上传并可以解析jsp的环境,注(Spring下不能通过上传jsp拿到动态代码上下文,因为spring启动的时候是加载的jar包,需要通过覆写jdk部分文件去解析恶意的文件)等等
效果
因为寄生在jvm里,天然免杀shellcode;具备动态修改的能力,可以选择一些用于处理http请求的关键类,用asm或javassit
技术修改其类字节码,然后加入我们的恶意逻辑即可,可无视框架,项目依赖的版本,实现注入内存马的效果.
影响
目前我还没找到好的方法去杀,查的话可以同样使用agent技术去检测关键类的字节码变动情况;但也只能看有限的容易被利用的类,如果攻击者选择一些偏门的类或者是业务新增的类选择寄生agent🐎,也会很难响应,杀的话貌似要么把源码备份一次,用同样的agent技术复原类,要么用asm或者javasisst剔除恶意代码,但是容易影响到业务…
所以agent内存马很容易使得业务受影响,实战需慎用