Flink-Memory-Shell
Apache Flink
情况下如何写内存马,本文对这一有趣实践过程做了一个记录。
1. 思路
首先目标机器Flink版本为1.3.2、1.9.0,Flink底层是使用的Netty
作为多功能socket服务器,我们可以有两种解决思路:①注册控制器;②通过JVMTI ATTACH机制Hook关键方法来写内存马。
1.1 应用层
第一个方案就是,类似Tomcat
、Spring
情况下的内存马,从当前或是全局中获取获取到被用于路由类功能的变量,注册自己的路由、处理器。这里拿1.9.0代码来举例,jobmanager的web服务器启动与初始化位于org.apache.flink.runtime.rest.RestServerEndpoint#start
。
这里将自定义的控制器handler
注册到路由器router
,所以我们需要只需要参考Flink
的业务代码,写好自己的Handler
然后注册到该route
变量即可。但很可惜,笔者找了一圈,没有发现相关的静态变量,无法获取到该路由对象。另外jar执行的代码处(invoke main方法)也没有传入啥有用的变量。要不就是想办法添加一个自定义的SocketChannel
,但这个方法更加不现实。
1.2 JVM TI Attach
直接利用JVMTI
的attach机制,hook特定类方法,在其前面插入我们的webshell方法,通过DEBUG相关HTTP处理流程,笔者最终实现了1.3.2、1.9.0版本下的内存马。
本文主要围绕如何使用该方法实现flink内充马进行讲述。
1.3 系统层
在系统层面,通过端口复用实现系统层面的木马,先知上有人提出该种想法利用 Hook 技术打造通用的 Webshell,不过存在一些问题:①执行该操作的权限要求很高;②该hook操作容易被EDR发现;③需要兼容不同平台,且不同linux环境都可能导致不兼容。大佬有说到,通过替换lib库不容易被杀,但需要重启(跑题了)。
2. JVM TI概述
JAVA虚拟机开放了一个叫 JVM Tool Interface (JVM TI) 的接口,通过该接口,我们可以查看和修改在JVM运行中的Java程序代码。
实现了JVM TI接口的程序,称为agent,agent能通过三种方式被执行,①Agent Start-Up (OnLoad phase):在JAVA程序的main函数执行之前执行agent,java命令中需通过-javaagent参数来指定agent,实现方式为premain ②Agent Start-Up (Live phase) :对于正在运行的JAVA程序,通过JVM进程间通信,动态加载agent,实现方式为attatch机制 ③Agent Shutdown:在虚拟机的library 将要被卸载时执行。
如果使用jdk/tools.jar提供的jvm操作类,由于com.sun.tools.attach.VirtualMachine#loadAgent(java.lang.String)的限制,我们的agent需要先落地到系统中,而执行loadAgent这一操作的程序我们被称为starter。
关于agent,最近@rebeyond提出了一种不需要落地的方案,但其实我觉得落地agent这个问题不大(还请大佬们指教):
https://mp.weixin.qq.com/s/JIjBjULjFnKDjEhzVAtxhw
3. 大体框架
首先,我们通过Flink
的JAR上传执行功能,上传我们的starter.jar
,starter被执行后,我们先释放agent到系统临时目录下,之后再加载该agent,并在加载完成之后删除即可。
4.寻找Hook点
由于Netty
是用于支持多协议的socket服务器,对应用层HTTP的解析封装是Flink做的,所以为了简洁高效,我们可以选择在Flink这边Hook对应的方法。
2.1 Flink 1.3.2
通过浏览堆栈信息,查看相关代码,我们可以很容易发现该版本中我们需要的关键类方法在org.apache.flink.runtime.webmonitor.HttpRequestHandler#channelRead0
不过,一个HTTP请求过来,我们在这里并不能一次性拿到整个HTTP报文,在msg instance of HttpRequest
情况下我们拿到的是请求行与请求头(这里简称请求头吧),下一次再来到channelRead0
中,且msg instance of HttpContent
时,我们拿到的是请求体Body,这时需要从this
中拿到currentRequest
请求头、currentDecoder
解码器,然后解析获取到Body中的key-value。
2.2 Flink 1.9.0
起初笔者看到1.9.0版本中存在1.3.2一样的代码,以为web流程没有变化,可以沿用1.3.2的Hook方法,但到实际测试时发现只是旧代码没有删除,而流程发生了变化,导致笔者需要hook新类方法。
笔者使用org.apache.flink.runtime.rest.FileUploadHandler#channelRead0
该类方法作为hook点,这里的代码基础逻辑和1.3.2的一样,也是无法直接拿到整个HTTP请求报文,需要在msg instance HttpContent
情况下使用this.currentHttpPostRequestDecoder
处理BODY拿到KEY-VALUE表单数据,从this.currentHttpRequest
拿到HTTP头。
5. 编写Agent
我们首先编写一个接口类IHook
,声明一个Hook点的要素方法,其中我们可以通过JDK自带的工具获取方法描述符号,如
1 | javap -cp flink-dist_2.11-1.9.0.jar -p -s org.apache.flink.runtime.rest.FileUploadHandler |
5.1 IHook
1 | package com.attach.hook; |
5.2 Flink132
我们在编写目标方法的Hook点时,需要引用相关的类或字段,在本地IDEA测试运行时我们直接引用相关jar包即可,而在打包JAR时,我们可以选择不打包进去,避免获得的jar包过大。
另外,关于实现webshell的业务功能,冰蝎工具就不适用了,因为behind的业务逻辑与HttpServletSession
、HttpServletRequest
、HttpServletResponse
这几个类紧密耦合,修改它的代码的工作量也很大。但笔者还是十分希望有一个图形化界面的工具来辅助我们管理webshell,这样能极大提升我们的工资效率。随后笔者想到要不直接使用比较原始的工具cknife
(JAVA版开源菜刀),稍微改改就能用,但如果要流量免杀,就还得改客户端源码,也费精力。
后面又看到AntSword
的CMDLINUX Shell
功能,服务器只需要提供命令执行功能并回显结果,就能做到文件浏览、修改功能;而且AntSword支持自定义加密,这样一来选择这块工具就很省事了,至于其他重要的功能,如代理,就先放着吧。
另外,在笔者在内存马的代码中添加了内存马删除功能,当用户访问/UNINSTALL
路径时,会触发removeTransformer(..)
,将相关hook点去除。
flink1.3.2中,笔者给出的代码在成功hook后,触发命令执行的HTTP是这样的:
1 | POST /shell HTTP/1.1 |
1 | package com.attach.hook; |
5.3 Flink190
flink1.9.0中,笔者给出的代码在成功hook后,触发命令执行的HTTP是这样的:
1 | POST /shell HTTP/1.1 |
1 | package com.attach.hook; |
5.4 Agent
由于我们使用attach机制去hook方法并插桩,我们的agent客户端被loadAgent
调用时,入口方法为agentmain
,所以我们这里只编写该方法即可。另外,将整个项目打包成JAR后,我们需要在META-INF/MANIFEST
中添加对应的属性。
1 | Agent-Class: com.attach.Agent |
1 | package com.attach; |
5.5 Transformer
我们编写一个自己的Transformer
类,实现ClassFileTransformer
相关接口方法,由于目标类应该已经被加载了,所以我们需要通过retransform
来重新转换已经加载的类。
1 | package com.attach; |
6. 编写Starter
starter这里需要使用到JDK的tools.jar包,用于和JAVA虚拟机进行通信,但不同JDK版本与不同系统架构都会导致jvm或是说tools.jar的差异,为了避免该问题,这里我们可以使用URLClassLoader
优先从本地lib库中找tools.jar包,如果找不到再去使用我们打包的starter.jar中的相关虚拟机操作类。如果是Linux的情况,我们可以直接在JDK/lib下找到tools.jar包,而windows比这复杂多,不过目前不涉及到windows场景,也不必处理。
由于1.3.2与1.9.0的VM Name发生了变化,前者为org.apache.flink.runtime.jobmanager.JobManager
,后者为org.apache.flink.runtime.entrypoint.StandaloneSessionClusterEntrypoint
,这里直接对两种进行了判断。
1 | public class Starter { |
7. libattach.so被占用
起初笔者以为flink的JAR执行是通过java -jar
进行的,后面发现其实就是invoke了main方法。这个情况下,导致了这么一个问题:starter成功执行attach之后,我们通过/UNINSTALL
功能卸载内存马,再一次去执行starter时却发现starter执行失败。原因为,VirtualMachine
在实例化时有个静态代码块加载了libattach.so
,而第二次执行starter会导致在加载该so文件时报java.lang.UnsatisfiedLinkError: Can't load library
异常。
为了避免该问题,我们可以一开始先将starter释放到临时目录下,通过调用系统命令jar -jar
来运行starter。
8. 结语
在路由注册方式行不通的情况下,使用attach进行内存马的写入,不失为一个不错的方法,理论上在任何JAVA代码执行漏洞中,我们都可以使用该方式去写内存马,但关于内存马的业务功能这块,我们可能需要自己费一番功夫去实现。