『Java安全』shell命令执行的几种方式与Runtime.exec()本地命令执行漏洞调用分析

命令执行

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()
在这里插入图片描述
其实都是缺省了后几个参数,追一下完整的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();

然后在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是如何实例化的:
在这里插入图片描述
这里我们传入的指令是/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;
    }
}

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

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

原因

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

Windows测试

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

防御

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

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

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值