文章目录

  • 命令执行
  • Runtime.exec()
  • ProcessBuilder.start()
  • ProcessImpl与UNIXProcess类
  • Runtime.exec()命令执行漏洞
  • Linux测试
  • 原因
  • Windows测试
  • 防御


命令执行

Runtime.exec()

在传入命令时,可以传入String和String[]

String cmd1 = "/bin/sh -c whoami";
String[] cmd2 = {"/bin/sh", "-c", "whoami"};

这是由于exec()方法自带多种重载方法,其中envp指定环境,dir指定目录

public Process exec(String command)
public Process exec(String command, String[] envp)
public Process exec(String command, String[] envp, File dir)
public Process exec(String cmdarray[])
public Process exec(String[] cmdarray, String[] envp)
public Process exec(String[] cmdarray, String[] envp, File dir)

代码审计一下传入String的exec()

shell 执行maven命令 shell执行java命令_命令执行


其实都是缺省了后几个参数,追一下完整的public Process exec(String command, String[] envp, File dir)

shell 执行maven命令 shell执行java命令_java_02


可以看到String command经过StringTokenizer类实例处理后转换成了String[] cmdarray,然后最终调用了public Process exec(String[] cmdarray, String[] envp, File dir)因此最终都是要转换成String[]来执行命令的。重点关注一下这里String转String[]的具体操作,首先追一下StringTokenizer的构造器

shell 执行maven命令 shell执行java命令_java_03

对于传入单参数,补全了两个参数又调用了三参数的重载构造器,这里加入了delimiter分隔符为空格、tab、回车换行、换行、换页符。

shell 执行maven命令 shell执行java命令_java_04


然后下面cmdarray的生成将String command按照默认分隔符进行切片保存,最终获得了String[] cmdarray,起一个动态调试:

shell 执行maven命令 shell执行java命令_java_05


得到String[]后,就到了public Process exec(String[] cmdarray, String[] envp, File dir),剩下就是ProcessBuilder的工作了

shell 执行maven命令 shell执行java命令_shell 执行maven命令_06


整个Runtime.exec(String command)的调用如下:

java.lang.Runtime.exec(String command)
java.lang.Runtime.exec(String command, String[] envp, File dir)
java.lang.Runtime.exec(String[] cmdarray, String[] envp, File dir)
java.lang.ProcessBuilder.start();
java.lang.ProcessImpl.start();
java.lang.UNIXProcess.<init>;
java.lang.forkAndExec();

然后在java.lang.UNIXProcess.<init>构造器又调用了forkAndExec,这是一个native方法。

private native int forkAndExec;

接下来就是不同系统下调用C创建shell进程返回PID

ProcessBuilder.start()

调用链下一步是调用了它,因此也能直接用ProcessBuilder.start()来执行命令

String[] cmd = {"/bin/sh", "-c", "ls"};
InputStream is = new ProcessBuilder(cmd).start().getInputStream();

ProcessImpl与UNIXProcess类

在JDK9以后ProcessImpl与UNIXProcess合并了,因此可以视为一个东西,都是调用了native方法新建进程。

Bb动态调试一下UNIXProcess是如何实例化的:

shell 执行maven命令 shell执行java命令_java_07


这里我们传入的指令是/bin/sh -c ls

shell 执行maven命令 shell执行java命令_java_08


prog是/bin/sh加上结束符的c字符串,也就是ascii码串;argBlock是参数串-c ls,其中空格被视为结束符;argc是参数数量2;其他都是默认。因此,构造反射

package Test;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

public class ReflectionUNIXProcess {
    public static void main(String[] args) throws Exception {
        Class<?> cls = Class.forName("java.lang.UNIXProcess");

        Constructor<?> constructor = cls.getDeclaredConstructors()[0];
        constructor.setAccessible(true);

        String[] cmd = {"/bin/sh", "-c", "ls"};

        byte[] prog = toCString(cmd[0]);
        byte[] argBlock = getArgBlock(cmd);
        int argc = argBlock.length;
        int[] fds = {-1, -1, -1};

        Object obj = constructor.newInstance(prog, argBlock, argc, null, 0, null, fds, false);

        Method method = cls.getDeclaredMethod("getInputStream");
        method.setAccessible(true);

        InputStream is = (InputStream) method.invoke(obj);
        InputStreamReader isr = new InputStreamReader(is);
        BufferedReader br = new BufferedReader(isr);
        String line = br.readLine();
        while (line != null){
            System.out.println(line);
            line = br.readLine();
        }
    }

    private static byte[] toCString(String str) {
        byte[] bytes  = str.getBytes();
        byte[] result = new byte[bytes.length + 1];
        System.arraycopy(bytes, 0, result, 0, bytes.length);
        result[result.length - 1] = (byte) 0;
        return result;
    }

    private static byte[] getArgBlock(String[] cmdarray){
        byte[][] args = new byte[cmdarray.length-1][];
        int size = args.length;
        for (int i = 0; i < args.length; i++) {
            args[i] = cmdarray[i+1].getBytes();
            size += args[i].length;
        }
        byte[] argBlock = new byte[size];
        int i = 0;
        for (byte[] arg : args) {
            System.arraycopy(arg, 0, argBlock, i, arg.length);
            i += arg.length + 1;
        }
        return argBlock;
    }
}

模仿ProcessImpl的操作写了两个自定义函数,运行成功

shell 执行maven命令 shell执行java命令_java_09

Runtime.exec()命令执行漏洞

由于String转String[]的过程中是以\t\n\r\f截断的,只要绕过这五类分隔符就能拼接指令执行:

Linux测试

package Test;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;

public class LCE {
    public static void main(String[] args) throws Exception {
        String hacker = ";cat${IFS}resource/rw.txt";
        String cmd = "/bin/sh -c whoami" + hacker;
        InputStream is = Runtime.getRuntime().exec(cmd).getInputStream();
        InputStreamReader isr = new InputStreamReader(is);
        BufferedReader br = new BufferedReader(isr);
        String line = br.readLine();
        while (line != null){
            System.out.println(line);
            line = br.readLine();
        }
    }
}

这里使用${IFS}绕过空格,就能执行后面的代码cat resource/rw.txt

原因

究其原因,是因为转换为String[]时候空格将cat的参数拆开了导致无法命令运行,而调用sh的参数-c要求传入一个完整的字符串,动态调试发现已经被拆成两个字符串了

shell 执行maven命令 shell执行java命令_后端_10


如果用空格就会一直等待运行

shell 执行maven命令 shell执行java命令_java_11

Windows测试

Windows平台这边则无需绕过空格,直接拼接命令即可运行

shell 执行maven命令 shell执行java命令_后端_12


字符串拆开不影响命令的完整性

shell 执行maven命令 shell执行java命令_java_13

防御

  • 添加钩子函数监测命令执行
  • 监测输入命令,过滤非法命令