Java 命令执行的几种方式

 

Java 中几种命令执行的方式

Runtime

public class RuntimeExec {

    public static void main(String[] args) throws Exception {
        InputStream in = Runtime.getRuntime().exec("whoami").getInputStream();
        byte[] bcache = new byte[1024];
        int readSize = 0;   //每次读取的字节长度
        ByteArrayOutputStream infoStream = new ByteArrayOutputStream();
        while ((readSize = in.read(bcache)) > 0) {
            infoStream.write(bcache, 0, readSize);
        }
        System.out.println(infoStream.toString());
    }
}

使用 Runtime.getRuntime 来执行系统命令时,无法直接使用管道符或重定向符, 具体分析可见: Java下多种执行命令的姿势及问题 - Y4er的博客, 但可以使用 base64 编码绕过这个限制.

    public static void case1_base64() throws Exception{
        String encodeString = Base64.getEncoder().encodeToString(cmd.getBytes());
        String cmd = "bash -c {echo," + encodeString + "}|{base64,-d}|{bash,-i}";
        Runtime.getRuntime().exec(cmd);
    }

如果使用 sh, 还可以使用如下的形式. 参考博客工具: Runtime.exec Payload Generater - AresX’s Blog

    public static void case1_sh() throws Exception{
        cmd = "ls | cat";
        cmd = "sh -c $@|sh . echo " + cmd;
        Process exec = Runtime.getRuntime().exec(cmd);
        getOutput(exec);
    }

同样的, bash 也可以使用这种形式, 其中 x 的内容可以随便写.

    public static void case1_bash() throws Exception{
        cmd = "ls | cat";
        cmd = "bash -c $@|bash x echo " + cmd;
        Process exec = Runtime.getRuntime().exec(cmd);
        getOutput(exec);
    }

ProcessBuilder

public class ProcessExec {
    public static void main(String[] args) {
        try {
            InputStream in = new ProcessBuilder("whoami").start().getInputStream();
            byte[] bs = new byte[2048];
            int readSize = 0;   //每次读取的字节长度
            ByteArrayOutputStream infoStream = new ByteArrayOutputStream();
            while ((readSize = in.read(bs)) > 0) {
                infoStream.write(bs, 0, readSize);
            }
            System.out.println(infoStream.toString());
        } catch (Exception e) {
            System.out.println(e.toString());
        }
    }
}

ProcessImpl

ProcessImpl 是更为底层的实现,Runtime 和 ProcessBuilder 执行命令实际上也是调用了 ProcessImpl 这个类,对于 ProcessImpl 类我们不能直接调用,但是可以通过反射来间接调用 ProcessImpl 来达到执行命令的目的。

public class ProcessImplExec {
    public static void main(String[] args) throws Exception {
        String[] cmds = new String[]{"whoami"};
        Class clazz = Class.forName("java.lang.ProcessImpl");
        Method method = clazz.getDeclaredMethod("start", String[].class, Map.class, String.class, Redirect[].class, boolean.class);
        method.setAccessible(true);
        Process e = (Process) method.invoke(null, cmds, null, ".", null, true);
        byte[] bs = new byte[2048];
        int readSize = 0;
        ByteArrayOutputStream infoStream = new ByteArrayOutputStream();
        while ((readSize = e.getInputStream().read(bs)) > 0) {
            infoStream.write(bs, 0, readSize);
        }
        System.out.println(infoStream.toString());
    }
}

UNIXProcess

UNIXProcess 类的构造函数会调用 native forkAndExec 函数,最终由 C 实现的 Java_java_lang_ProcessImpl_forkAndExec 函数来执行系统命令。

private native int forkAndExec(int mode, byte[] helperpath,
                                byte[] prog,
                                byte[] argBlock, int argc,
                                byte[] envBlock, int envc,
                                byte[] dir,
                                int[] fds,
                                boolean redirectErrorStream)
    throws IOException;

测试代码如下:

    Class<?> UnixProcess = Class.forName("java.lang.UNIXProcess");
    Constructor<?> constructor = UnixProcess.getDeclaredConstructors()[0];
    constructor.setAccessible(true);
    
    byte[] argBlock = String.format("-c\00%s",cmd).getBytes();
    Object o = constructor.newInstance("/bin/sh".getBytes(),
            argBlock, argBlock.length,
            null, 0,
            null,
            new int[]{-1, -1, -1},
            false
    );

UnixPrintService

UnixPrintService 这个类中存在很多 getter 方法可以执行系统命令

getPrinterIsAcceptingJobsAIX

例如 getPrinterIsAcceptingJobsAIX 函数,代码如下所示:

    String var1 = "/usr/bin/lpstat -R " + this.printer;
    String[] var2 = UnixPrintServiceLookup.execCmd(var1);

类似命令注入,只需要将 printer 设置为自己要执行的命令,然后拼接执行即可。

测试代码:

    Constructor<?> constructor = ReflectUtils.getFirstCtor(UnixPrintService.class);
    constructor.setAccessible(true);
    UnixPrintService exp = (UnixPrintService) constructor.newInstance(";"+cmd);

    Method getPrinterIsAcceptingJobsAIXMethod = UnixPrintService.class.getDeclaredMethod("getPrinterIsAcceptingJobsAIX",null);
    getPrinterIsAcceptingJobsAIXMethod.setAccessible(true);
    getPrinterIsAcceptingJobsAIXMethod.invoke(exp,null);

getDefaultPrintService

getDefaultPrintService 这条利用链如下:

<sun.print.UnixPrintServiceLookup: javax.print.PrintService getDefaultPrintService()>
<sun.print.UnixPrintServiceLookup: java.lang.String getDefaultPrinterNameBSD()>
<sun.print.UnixPrintServiceLookup: java.lang.String[] execCmd(java.lang.String)>
<sun.print.UnixPrintServiceLookup$1: java.lang.Object run()>
<java.lang.Runtime: java.lang.Process exec(java.lang.String[])>

UnixPrintServiceLookup

在利用 UnixPrintService 执行系统命令时最终调用的就是 UnixPrintServiceLookup.execCmd 函数,所以直接调用这个函数也可以实现命令执行。

    Method execCommand = UnixPrintServiceLookup.class.getDeclaredMethod("execCmd", String.class);
    execCommand.setAccessible(true);
    execCommand.invoke(null,cmd);

execCmd 底层也是 Runtime.getRuntime().exec()

注意事项

Windows 环境

Runtime 和 ProcessBuilder 的底层实际上都是 ProcessImpl 。而在 Windows 下不能执行 echo 命令的原因是因为 java 找不到环境变量。所以执行命令时需要加上cmd /c

参考