欢迎访问Sunbet,Sunbet是欧博Allbet的官方网站!

首页Sunbet_安全防护正文

Java Runtime.getRuntime().exec由表及里

b9e08c31ae1faa592020-01-0736Web安全安全技术

这篇文章主要目的在于学习前人文章,并从深入一点的角度探讨为什么Runtime.getRuntime().exec某些时候会失效这个问题

问题复现

测试代码如下

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;

public class linux_cmd1 {
    public static void main(String[] args) throws IOException {
        String cmd = "cmd which you want to exec";
        InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] b = new byte[1024];
        int a = -1;

        while ((a = in.read(b)) != -1) {
            baos.write(b, 0, a);
        }

        System.out.println(new String(baos.toByteArray()));
    }
}

先看看可以成功的情况
Java Runtime.getRuntime().exec由表及里  Web安全 安全技术 第1张

再来看看不能成功的情况
Java Runtime.getRuntime().exec由表及里  Web安全 安全技术 第2张

这里 && 并没有达到bash中的效果
Java Runtime.getRuntime().exec由表及里  Web安全 安全技术 第3张

如果以前有人问我为什么会出现这种,我会毫不犹豫的回答:因为 Runtime.getRuntime().exec 执行命令的时候并没有shell上下文环境所以无法把类似于 & | 这样的符号进行特殊处理。

解决方法

解决这种问题的方法有两种
第一种就是对执行命令进行编码,编码地址在这

Java Runtime.getRuntime().exec由表及里  Web安全 安全技术 第4张

Java Runtime.getRuntime().exec由表及里  Web安全 安全技术 第5张

第二种是使用数组的形式命令执行

String[] command = { "/bin/sh", "-c", "echo 2333 2333 2333 && echo 2333 2333 2333" };
InputStream in = Runtime.getRuntime().exec(command).getInputStream();

Java Runtime.getRuntime().exec由表及里  Web安全 安全技术 第6张

至此从实战应用的角度这个问题已经解决了。

不过我们可以看到其实这第二种方法用到了 & 上面 Runtime.getRuntime().exec执行命令的时候并没有shell上下文环境所以无法把类似于 & | __` _这样的符号特殊处理。_这一结论似乎看起来并站不住脚?

下面来跟踪一下源码,看看到底发生了什么。

源码分析

当传入Runtime.getRuntime().exec的是字符串

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;

public class linux_cmd1 {
    public static void main(String[] args) throws IOException {
        String cmd = "echo 2333 && echo 2333";
        InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] b = new byte[1024];
        int a = -1;

        while ((a = in.read(b)) != -1) {
            baos.write(b, 0, a);
        }

        System.out.println(new String(baos.toByteArray()));
    }
}

因为传入的命令是String类型,所以进入 java.lang.Runtime#exec(java.lang.String, java.lang.String[], java.io.File)这里是第一个非常关键的点, StringTokenizer 会把传入的conmmand字符串按 \t \n \r \f 中的任意一个分割成数组cmdarray。

Java Runtime.getRuntime().exec由表及里  Web安全 安全技术 第7张
Java Runtime.getRuntime().exec由表及里  Web安全 安全技术 第8张

代码来到exec的多态实现 java.lang.Runtime#exec(java.lang.String[], java.lang.String[], java.io.File) ,exec内部调用了ProcessBuilder的start。
Java Runtime.getRuntime().exec由表及里  Web安全 安全技术 第9张

ProcessBuilder.start内部又调用了ProcessImpl.start。
Java Runtime.getRuntime().exec由表及里  Web安全 安全技术 第10张

在ProcessImpl.start中有第二个非常关键的点我们可以看到程序把cmdarray第一个参数(cmdarray[0])当成要执行的命令,把其后的部分(cmdarray[1:])作为命令的参数转换成byte 数组 argBlock(具体规则是以\x00进行implode)。
Java Runtime.getRuntime().exec由表及里  Web安全 安全技术 第11张

ProcessImpl.start最后又会把处理好的参数传入UNIXProcess
Java Runtime.getRuntime().exec由表及里  Web安全 安全技术 第12张

UNIXProcess内部又调用了forkAndExec方法
Java Runtime.getRuntime().exec由表及里  Web安全 安全技术 第13张

这里的是forkAndExec是一个native方法。
Java Runtime.getRuntime().exec由表及里  Web安全 安全技术 第14张

从变量的命名来看,在开发者的眼中prog是要执行的命令即 echo ,argBlock都是传给 echo 的参数即2333\x00&&\x002333且传给 echo 的参数个数argc是4。
可见经过StringTokenizer对字符串中空格类的处理其实是一种java对命令执行的保护机制,他可以防御以下这种命令注入,其效果相当于php中的escapeshellcmd。

String cmd = "echo " + 可控点;
Runtime.getRuntime().exec(cmd)

补一个完整的调用栈。
Java Runtime.getRuntime().exec由表及里  Web安全 安全技术 第15张

当传入Runtime.getRuntime().exec的是字符串数组

我们再来看看给Runtime传入数组的时候是什么情况。

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;

public class linux_cmd1 {
    public static void main(String[] args) throws IOException {
        String[] command = { "/bin/sh", "-c", "echo 2333 && echo 2333" };
        InputStream in = Runtime.getRuntime().exec(command).getInputStream();

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] b = new byte[1024];
        int a = -1;

        while ((a = in.read(b)) != -1) {
            baos.write(b, 0, a);
        }

        System.out.println(new String(baos.toByteArray()));
    }
}

因为这里传入的数组,所以并没有经StringTokenizer对字符串的分割处理这一步而是直接进入了。java.lang.Runtime#exec(java.lang.String[])

深入研究Pass-the-Hash攻击与防御

概述 这篇Paper深入研究了 Windows 10下的Pass-the-Hash攻击: 本文分析了PtH攻击在windows 10 v1903环境下的可行性。 本文展示了几种hash提取技术。 本文演示了在哪些情况下攻击者可以使用这些hashes进行身份验证(各种协议下执行PtH攻击的条件和方法)。 本文说明了一个企业可以考虑使用的"安全管控"(security controls)措施,可以最大程度降低PtH攻击的风险。 最终,做了的这些测试证明了PtH攻击仍然是一个真正的威胁,每个企业都需要直面这种风险。 意义 研究Pass-the-Hash攻击的意义: 1.有助于已授权的渗透测试 2.有助于企业防御PtH攻击、横向移动 3.有助于安全研究人员继续探索 ... 意义较大,故逐字翻译

Java Runtime.getRuntime().exec由表及里  Web安全 安全技术 第16张

后面的流程和字符串的情形是一致的,最后来到forkAndExec
Java Runtime.getRuntime().exec由表及里  Web安全 安全技术 第17张

按照上面的说法这里 /bin/bash 是要执行的命令, -c\x00"echo 2333 && echo 23333" 是传给的 /bin/bash 的参数。

补一个调用栈
Java Runtime.getRuntime().exec由表及里  Web安全 安全技术 第18张

一个错误的想法

看到这里不知道你是不是有点晕,心底生出了疑问,在执行字符串的时候加上 /bin/bash 不就好了。像下面这样。

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;

public class linux_cmd1 {
    public static void main(String[] args) throws IOException {
        String cmd = "/bin/bash -c 'echo 2333 && echo 2333'";
        InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] b = new byte[1024];
        int a = -1;

        while ((a = in.read(b)) != -1) {
            baos.write(b, 0, a);
        }

        System.out.println(new String(baos.toByteArray()));
    }
}

运行试试看,发现什么结果都没有,推测应该是shell执行命令失败了。
Java Runtime.getRuntime().exec由表及里  Web安全 安全技术 第19张

为什么会失败呢?我们来diff一下和数组执行最后进native的层的区别。
Java Runtime.getRuntime().exec由表及里  Web安全 安全技术 第20张

可以看到prog都是 /bin/bash 但是字符串模式下执行的参数变成了 -c\x00'echo\x002333\x00&&\x00echo\x002333' ,对比数组模式 -c\x00"echo 2333 && echo 23333" 。可以发现字符串模式下因为StringTokenizer对字符串空格类字符的处理破坏了命令执行的语义

如果再仔细看看会发现字符串模式argc为6而数组模式只有2。写到这里其实我还想钻以下牛角尖,凭什么6个参数最后就不能执行?

进入jvm看看

带着这样的疑问,我自不量力的编译了java源码并现学了一下怎么调试jvm(调试的环境是ubuntu14.04+jdk8)下面是学习成果。

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;

public class Test {
    public static void main(String[] args) throws IOException {
        String[] command = { "/bin/bash", "-c", "echo 2333 && echo 2333" };
        InputStream in = Runtime.getRuntime().exec(command).getInputStream();

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] b = new byte[1024];
        int a = -1;

        while ((a = in.read(b)) != -1) {
            baos.write(b, 0, a);
        }

        System.out.println(new String(baos.toByteArray()));
    }
}

根据java native函数命名规则可以知道forkAndExec对应的c函数是 Java_java_lang_UNIXProcess_forkAndExec
Java Runtime.getRuntime().exec由表及里  Web安全 安全技术 第21张

这个函数初始化执行命令所需要一些变量(如输入输出错误流)以及提取并处理java传入进来的参数,最后调用startChild函数开启子进程。
Java Runtime.getRuntime().exec由表及里  Web安全 安全技术 第22张

startChild会根据是mode的数值不同进入不同的分支,mode由操作系统、libc版本决定。
Java Runtime.getRuntime().exec由表及里  Web安全 安全技术 第23张

我这里进入了vforkChild,vforkChild会使用vfork开启一个子进程,并且在子进程内部调用了childProcess,在clion中为了调试进入子进程需要在进入之前在gdb调试框输入 set follow-fork-mode childset detach-on-fork off
Java Runtime.getRuntime().exec由表及里  Web安全 安全技术 第24张

childProcess中调用JDK_execvpe。
Java Runtime.getRuntime().exec由表及里  Web安全 安全技术 第25张

JDK_execvpe最后调用系统execvp函数,我们来细一看传参情况。
Java Runtime.getRuntime().exec由表及里  Web安全 安全技术 第26张

Java Runtime.getRuntime().exec由表及里  Web安全 安全技术 第27张

Java Runtime.getRuntime().exec由表及里  Web安全 安全技术 第28张

Java Runtime.getRuntime().exec由表及里  Web安全 安全技术 第29张

故数组情况下等价于
Java Runtime.getRuntime().exec由表及里  Web安全 安全技术 第30张

那么我们再来考察一下,字符串的情况的情况。
Java Runtime.getRuntime().exec由表及里  Web安全 安全技术 第31张
Java Runtime.getRuntime().exec由表及里  Web安全 安全技术 第32张
Java Runtime.getRuntime().exec由表及里  Web安全 安全技术 第33张
Java Runtime.getRuntime().exec由表及里  Web安全 安全技术 第34张
Java Runtime.getRuntime().exec由表及里  Web安全 安全技术 第35张
Java Runtime.getRuntime().exec由表及里  Web安全 安全技术 第36张
Java Runtime.getRuntime().exec由表及里  Web安全 安全技术 第37张

故字符串模式等价于
Java Runtime.getRuntime().exec由表及里  Web安全 安全技术 第38张

所以整个调用链如下

java.lang.Runtime.exec(cmd);
->java.lang.ProcessBuilder.start();
-->java.lang.ProcessImpl.start();
--->Java_java_lang_UNIXProcess_forkAndExec() in j2se/src/solaris/native/java/lang/UNIXProcess_md.c
---->fork或VFORK或POSIX_SPAWN
----->execvp();

结论

字符串形式下Runtime.getRuntime().exec执行命令的时候无法解释&等特殊字符的本质是execvp特殊符号。而之所以数组情况能成是因为execvp调用了 /bin/bash/bin/bash 解释了 & , | 和execvp没关系。

参考

Java下奇怪的命令执行
在 Runtime.getRuntime().exec(String cmd) 中执行任意shell命令的几种方法
Java JVM、JNI、Native Function Interface、Create New Process Native Function API Analysis
How to debug a forked child process using CLion


网友评论