Java Runtime.getRuntime().exec由表及里
本文发在先知社区,转载到自己博客上。
问题复现
测试代码如下
1 | import java.io.ByteArrayOutputStream; |
先看看可以成功的情况
再来看看不能成功的情况
这里 &&
并没有达到bash中的效果
如果以前有人问我为什么会出现这种,我会毫不犹豫的回答:因为 Runtime.getRuntime().exec
执行命令的时候并没有shell上下文环境所以无法把类似于 &
|
这样的符号进行特殊处理。
解决方法
解决这种问题的方法有两种
第一种就是对执行命令进行编码,编码地址在这
第二种就是使用数组的形式命令执行
1 | String[] command = { "/bin/sh", "-c", "echo 2333 2333 2333 && echo 2333 2333 2333" }; |
至此从实战应用的角度这个问题已经解决了。
不过我们可以看到其实这第二种方法用到了 &
上面 _Runtime.getRuntime().exec执行命令的时候并没有shell上下文环境所以无法把类似于 &
|
_
` _这样的符号特殊处理。_这一结论似乎看起来并站不住脚?
下面来跟踪一下源码,看看到底发生了什么。
源码分析
当传入Runtime.getRuntime().exec的是字符串
1 | import java.io.ByteArrayOutputStream; |
因为传入的命令是String类型,所以进入 java.lang.Runtime#exec(java.lang.String, java.lang.String[], java.io.File)
。这里是第一个非常关键的点, StringTokenizer
会把传入的conmmand字符串按 \t \n \r \f
中的任意一个分割成数组cmdarray。
代码来到exec的多态实现 java.lang.Runtime#exec(java.lang.String[], java.lang.String[], java.io.File)
,exec内部调用了ProcessBuilder的start。
ProcessBuilder.start内部又调用了ProcessImpl.start。
在ProcessImpl.start中有第二个非常关键的点我们可以看到程序把cmdarray第一个参数(cmdarray[0])当成要执行的命令,把其后的部分(cmdarray[1:])作为命令的参数转换成byte 数组 argBlock(具体规则是以\x00进行implode)。
ProcessImpl.start最后又会把处理好的参数传入UNIXProcess
UNIXProcess内部又调用了forkAndExec方法
这里的是forkAndExec是一个native方法。
从变量的命名来看,在开发者的眼中prog是要执行的命令即 echo
,argBlock都是传给 echo
的参数即2333\x00&&\x002333
且传给 echo
的参数个数argc是4
可见经过StringTokenizer对字符串中空格类的处理其实是一种java对命令执行的保护机制,他可以防御以下这种命令注入。
1 | String cmd = "ping " + 可控点; |
补一个完整的调用栈。
当传入Runtime.getRuntime().exec的是字符串数组
我们再来看看给Runtime传入数组的时候是什么情况。
1 | import java.io.ByteArrayOutputStream; |
因为这里传入的数组,所以并没有经StringTokenizer对字符串的分割处理这一步而是直接进入了。java.lang.Runtime#exec(java.lang.String[])
。
后面的流程和字符串的情形是一致的,最后来到forkAndExec
按照上面的说法这里 /bin/bash
是要执行的命令, -c\x00"echo 2333 && echo 23333"
是传给的 /bin/bash
的参数。
补一个调用栈
一个错误的想法
看到这里不知道你是不是有点晕,心底生出了疑问,在执行字符串的时候加上 /bin/bash
不就好了。像下面这样。
1 | import java.io.ByteArrayOutputStream; |
运行试试看,发现什么结果都没有,推测应该是shell执行命令失败了。
为什么会失败呢?我们来diff一下和数组执行最后进native的层的区别。
可以看到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)下面是学习成果。
1 | import java.io.ByteArrayOutputStream; |
根据java native函数命名规则可以知道forkAndExec对应的c函数是 Java_java_lang_UNIXProcess_forkAndExec
。
这个函数初始化执行命令所需要一些变量(如输入输出错误流)以及提取并处理java传入进来的参数,最后调用startChild函数开启子进程。
startChild会根据是mode的数值不同进入不同的分支,mode由操作系统、libc版本决定。
我这里进入了vforkChild,vforkChild会使用vfork开启一个子进程,并且在子进程内部调用了childProcess,在clion中为了调试进入子进程需要在进入之前在gdb调试框输入 set follow-fork-mode child
和 set detach-on-fork off
childProcess中调用JDK_execvpe。
JDK_execvpe最后调用系统execvp函数,我们来细一看传参情况。
故数组情况下等价于
那么我们再来考察一下,字符串的情况的情况。
故字符串模式等价于
1 | int main() { |
所以整个调用链如下
1 | java.lang.Runtime.exec(cmd); |
结论
_Runtime.getRuntime().exec执行命令的时候并没有shell上下文环境所以无法把类似于 &
|
_
_这样的符号特殊处理_的本质是execvp也确实不支持shell中的特殊符号。而之所以数组情况能成是因为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
Java Runtime.getRuntime().exec由表及里
https://cl0und.github.io/2020/01/11/Java-Runtime-getRuntime-exec由表及里/