当前位置:   article > 正文

『Java安全』shell命令执行的几种方式与Runtime.exec()本地命令执行漏洞调用分析_runtime.exec无法调用,需加上 processbuilder.start()

runtime.exec无法调用,需加上 processbuilder.start()

命令执行

Runtime.exec()

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

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

这是由于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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

代码审计一下传入String的exec()
在这里插入图片描述
其实都是缺省了后几个参数,追一下完整的public Process exec(String command, String[] envp, File dir)
在这里插入图片描述
可以看到String command经过StringTokenizer类实例处理后转换成了String[] cmdarray,然后最终调用了public Process exec(String[] cmdarray, String[] envp, File dir)因此最终都是要转换成String[]来执行命令的。重点关注一下这里String转String[]的具体操作,首先追一下StringTokenizer的构造器
在这里插入图片描述

对于传入单参数,补全了两个参数又调用了三参数的重载构造器,这里加入了delimiter分隔符为空格、tab、回车换行、换行、换页符。
在这里插入图片描述
然后下面cmdarray的生成将String command按照默认分隔符进行切片保存,最终获得了String[] cmdarray,起一个动态调试:
在这里插入图片描述
得到String[]后,就到了public Process exec(String[] cmdarray, String[] envp, File dir),剩下就是ProcessBuilder的工作了
在这里插入图片描述
整个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();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

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

private native int forkAndExec;
  • 1

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

ProcessBuilder.start()

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

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

ProcessImpl与UNIXProcess类

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

Bb动态调试一下UNIXProcess是如何实例化的:
在这里插入图片描述
这里我们传入的指令是/bin/sh -c ls
在这里插入图片描述
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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61

模仿ProcessImpl的操作写了两个自定义函数,运行成功
在这里插入图片描述

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();
        }
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

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

原因

究其原因,是因为转换为String[]时候空格将cat的参数拆开了导致无法命令运行,而调用sh的参数-c要求传入一个完整的字符串,动态调试发现已经被拆成两个字符串了
在这里插入图片描述
如果用空格就会一直等待运行
在这里插入图片描述

Windows测试

Windows平台这边则无需绕过空格,直接拼接命令即可运行
在这里插入图片描述
字符串拆开不影响命令的完整性
在这里插入图片描述

防御

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

欢迎在评论区留言,欢迎关注我的CSDN @Ho1aAs

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/309201
推荐阅读
相关标签
  

闽ICP备14008679号