Java下奇怪的命令执行

2019-10-15 约 322 字 预计阅读 2 分钟

声明:本文 【Java下奇怪的命令执行】 由作者 l1nk3r 于 2019-10-13 10:52:11 首发 先知社区 曾经 浏览数 2947 次

感谢 l1nk3r 的辛苦付出!

0x01 前言

首先Java下的命令执行大家都知道常见的两种方式:

1.使用ProcessBuilder

ProcessBuilder pb=new ProcessBuilder(cmd); 
pb.start();

2.使用Runtime

Runtime.getRuntime().exec(cmd)

也就是说上面cmd参数可控的情况下,均存在命令执行的问题。但是话题回来,不太清楚大家是否遇到过java命令执行的时候,无论是windows还是linux环境下,带有|,<,>等符号的命令没办法正常执行。所以今天就进入底层看看这两个东西。

0x02 差别

先选择跟进Runtime.getRuntime().exec(cmd),样例代码如下所示:

import java.io.*;

public class Main {
    public static void main(String[] arg) throws IOException {
        String command="/bin/sh -c echo 111 > 3.txt";
        Process proc = Runtime.getRuntime().exec(command);
        InputStream in = proc.getInputStream();
        BufferedReader br = new BufferedReader(new InputStreamReader(in, "UTF8"));
        String line = null;
        while((line=br.readLine())!=null) {
            System.out.println(line);
        }
    }
}

跟进 java.lang.Runtime#exec 的构造方法,下面话题回来,exec的构造方法有以下几种情况,其实根据传入的变量我们大概可以区分的了,一个是根据String command,也就是直接传入一个字符串。另一个是根据String cmdarray[],也就是传入一个数组。

public Process exec(String command) throws IOException {
        return exec(command, null, null);
    }

public Process exec(String command, String[] envp) throws IOException {
        return exec(command, envp, null);
    }

public Process exec(String cmdarray[]) throws IOException {
        return exec(cmdarray, null, null);
    }

public Process exec(String[] cmdarray, String[] envp) throws IOException {
        return exec(cmdarray, envp, null);
    }

而根据前面代码中,我们传入的命令是如下所示:

String command="/bin/sh -c echo 111 > 3.txt";

所以会进入Process exec(String command)这个构造方法进行处理,跟进这个方法,发现最后返回exec(command, null, null)

public Process exec(String command) throws IOException {
        return exec(command, null, null);
    }

继续跟进这个exec方法,看看这个方法的实现。这里代码实例化了StringTokenizer类,并且传入了我们要执行的command命令,简单翻译一下注释:为指定的字符串构造一个字符串标记器。也就是说StringTokenizer这个类会为特殊字符打上tag之类的东西。

我们继续往下看,经过StringTokenizer类处理之后会返回一个cmdarray[],而这里的处理实际上是根据空格针对命令进行了分割,至于为什么结果要是一个array数组,我们下面会接着说。

我们发现经过一系列的处理,最后又有一个return exec 的处理。继续跟进这个exec的处理,我们可以看到这里最后实例化ProcessBuilder来处理我们传入的cmdarray。到这里实际上可以清楚了Runtime.getRuntime().exec()的底层实际上也是ProcessBuilder

public Process exec(String[] cmdarray, String[] envp, File dir)
        throws IOException {
        return new ProcessBuilder(cmdarray)
            .environment(envp)
            .directory(dir)
            .start();
    }

我们知道ProcessBuilder.start方法是命令执行,那么跟进这个start我们发现,首先prog获取cmdarray[0]也就是我们的/bin/sh,然后判断security是否为null,如果不为null就会校验checkExec

然后继续往下走,这里调用java.lang.ProcessImpl.start

进入之后我们就可以看到最后是调用java.lang.UnixProcess这个类来执行命令,而且我们发现执行命令的时候实际上是根据cmdarray[0]来判断用什么命令。而在java.lang.UnixProcess这个类里面是调用forkAndExec来为命令创建环境等操作。我们看到当前断点pid2653,而这里确实起了一个sh的进程。

这样看可能还不够明显,因为我们知道/bin/sh -c echo 111 > 3.txt在bash命令行下也不会正常执行成功,命令行下需要/bin/sh -c "echo 111 > 3.txt",看这两段代码的命令执行的效果。

String[] command = { "/bin/sh", "-c", "echo 111 > 3.txt" };
String command="/bin/sh -c \"echo 111 > 3.txt\"";

首先先看String command="/bin/sh -c \"echo 111 > 3.txt\"";,按照前面的分析,经过StringTokenizer这个类进行拆分之后变成了{"/bin/sh","-c",""echo","111"、">","3.txt""}

而当前内存开辟一个12473进程,并且确实12473执行sh命令。

但是我们发现,经过StringTokenizer这类拆分之后,命令完全变了一个味道,语义完全变了,并不是我们想要的结果,那我们再看看String[] command = { "/bin/sh", "-c", "echo 111 > 3.txt" };的结果。因为我们传入的是array数组类型,这里直接将命令直接带入了ProcessBuilder进行处理,前面完全没有经过StringTokenizer这个类的拆分。也就是他完整的保存了我想要的语义。

也就是说getRuntime().exec()如果直接传入字符串会经过StringTokenizer的分割,进而破坏其原本想要表达的意思。

下面这段代码是否存在命令执行的问题,要是在PHP下,我会斩钉截铁的说是,但是回到java环境下,我们发现|等一些特殊符号没办法使用,而且传入的是字符串,遇到空格会被StringTokenizer进行切割,所以实际上下面这段代码是没办法使用的。

String str = request.getParameter("url");
String cmdstr = "ping "+ url;
Runtime.getRuntime().exec(cmdstr)

再来一段代码,能够执行命令,但是很受限,我们知道命令根据cmdarray[0]来确认以什么命令环境启动,这里确实以/bin/sh启动了,但是后面的命令执行的时候存在问题,它仅能执行单条命令,拼接不了相关参数。

String str = request.getParameter("cmd");
String cmdstr = "/bin/sh -c "+ cmd;
Runtime.getRuntime().exec(cmdstr)

最后再来一段代码,下面这段代码才会是java下命令执行的完全体。

String str = request.getParameter("cmd");
String[] cmdstr = { "/bin/sh", "-c", str };
Runtime.getRuntime().exec(cmdstr)

后面我翻到一篇文章,实际上也是差不多这个情况,实际上也是这个StringTokenizer这个类针对命令进行处理可能会造成非预期的结果。

最后还有一个问题,为什么一定要将命令切割成为数组,原因是因为ProcessBuilder,看看他的构造方法。

public ProcessBuilder(String... command) {
        this.command = new ArrayList<>(command.length);
        for (String arg : command)
            this.command.add(arg);
    }

    public ProcessBuilder(List<String> command) {
        if (command == null)
            throw new NullPointerException();
        this.command = command;
    }

实际上它是要求 Array 类型或者 List 类型,如果我们要执行下图中的代码是不行的。

原因在于我们传入的类型不对,我们前面说过命令执行是根据cmdarray[0],确认命令启动环境,这里自然找不到我们要启动的命令。

所以Java下的命令稍微改造一下代码就好。

还有一种方式就是用编码,linux下可以用bash的base64编码来解决这个特殊字符的问题。

这里在小提一下如果遇到命令执行过滤了ProcessBuildergetRuntime,可以考虑一下java.lang.ProcessImpl.start

0x03 小结

其实java已经尽量规避命令执行的安全问题,JDK沙盒机制会进行checkExec,执行命令的机制就是仅仅检查并执行命令数组中的第一个,而分隔符后面的所有东西都是默认为被执行程序的参数,而分隔符后面的所有东西都是默认为被执行程序的参数,这也是我们前文一直聊的内容。所以getRuntime().exec()通过传入字符串执行命令的时候,应该尽量避免使用空格,用了空格可能会改变这条命令本身想要表达的意思。

所以在java下如果遇到复杂的命令执行,且参数只能如下所示,且只有一个位置可以控制的话,建议使用base64的编码方式,windows下可以使用powershellbase64

java的反序列化框架利用框架yso,以及一些shiro这类反序列化导致的命令执行实际上很多是用了getRuntime来达到命令执行的目的,且就像我们上面说的,可控位置比较固定,执行复杂命令会出现执行不了,以上只是复习一下之前和人聊的一个问题。

Reference

sh-or-getting-shell-environment-from

关键词:[‘安全技术’, ‘WEB安全’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now