0%

Java的奇怪命令执行

Java的奇怪命令执行

最近在打工,做的java CVE复现,用ysoserialize产生一个Java.Runtime.exec的payload命令执行,发现出现了一定的问题

由于没有回显,因此需要想办法验证命令执行的情况,能执行一些简单的命令,比如touch文件,或者curl出来,确实能成功,但是想写shell的时候就遇到了奇怪的问题,没有回显也不知道哪报错,最后通过百度发现问题的根源在于Java.Runtime.exec的诡异实现

也当头一次试着跟进超级多的java类看Java实现,记录一下一切都是怎么回事

Runtime.getRuntime().exec(cmd)

IDEAnb!!!!!
这调试爱了,整体思路清晰的一逼
直接进Runtime.getRuntime().exec(cmd)这句


看到调用的是exec(cmd, null, null)
再跟,看函数具体实现

用cmd创建了一个StringTokenizer,并且把我们传入的cmd用这个st变成了cmdarray,跟进构造方法

传入参数为str,也就是我们的cmd,还有一个默认参数delim分隔符为 \t\n\r\f(注意开头有一个空格)分别为制表换行回车换页
再进一次构造函数

在这里可以很清楚的看见这个st的内容,str就是我们的cmd,而分隔符就是刚才构造函数默认的这一段
看到第二张图用st构造了一个cmdarray,我们从参数中可以看见再进一个exec时我们的参数从原来的string变成了string数组,看看内容

我们的cmd就变成了一个个通过空格分隔的cmdarray

再跟,进了ProcessBuilder,传入的参数是cmdarray,此时输入的cmd已经和我们无缘了,之后都是对打散之后的array进行操作
在这个start函数里面进行了一系列的判断检查,最后的进入了ProcessImpl.start

最后的最后进了一个ProcessImpl的构造函数

在这个构造函数里面我们可以清楚的看到关键所在

String executablePath = new File(cmd[0]).getPath();
再往后要进入jvm看命令执行的具体过程了。。。不会,看了看网上大哥的结论
jvm最后会使用execvp()这个系统调用去执行命令,把我们的executablePath作为可执行文件名,而后面的所有内容全都被视为execvp的参数传入,这样一来,我们反弹shell语句的操作把所有的内容全都变成了参数
java的exec和我们常见的python,PHP的system这种命令执行并不一致,这两者的命令执行都是直接将输入放到/bin/sh里去跑,而java的exec却是自己把第一个参数当二进制文件去找过来执行,并且还把所有字符串按空格分隔当参数,就导致了各种命令执行的不成功

解决方案

直接传入string数组

如上是exec传入的参数为一个字符串,然后经过一系列处理最后又调用了exec(String[]),如果我们主动出击,直接传入一个Stringarray的话,就能跳过前面几步StringTokenizer,直接到达ProcessBuilder(cmdarray),数组里的字符串不会再被打断,可以直接命令执行
eg:
String[] cmdarray={"bash", "-c", "cmd"}

base64编码

说到底就是输入全都用空格打断变成了参数,有点像上了个escapeshellarg?或者说想办法进行不需要空格的命令执行?
第一个方法看似很棒,但是打java的话基本上都是用ysoserialize去打,而ysoserialize一般直接用Runtime.exec(cmd)这种形式产生payload,看到有一种方法是直接魔改yso创建出cmdarray形式的payload,不过我们这里还是用简单一点的方法
用这种形式的base64使得我们整个命令变成一个参数,不会出现被空格打断的情况
bash -c {echo,base64cmd}|{base64,-d}|{bash,-i}
runtime-exec-payloads
这个也是最佳解决方案了吧

${IFS}

经典Linux命令行代替空格的分隔符,大部分情况下的确可以代替空格,不过好像在偶尔还是会有问题?

朴素方案

远端下载一个shell脚本下来再用bash执行,curl -o下文件,然后任意命令执行

没看懂的方案

sh -c $@|sh . echo cmd $@类似于一个包含所有参数的迭代器,但是这里没有任何参数输入啊?然后再把结果管道符到一个新shell?.搜了一下是source命令(我感觉可能事实上并不是),大概是说保持命令上下文不变?然后执行的也不是脚本是一个echo出来的cmd?忘了这段吧,真完全看不懂呜呜
Shell特殊变量
Linux下source命令详解

并且java的玄幻分隔会把上文分割成[“sh”,”-c”,”$@|sh”, “.”, “echo”, “cmd”]我真觉得跑不起来。。。

其他奇怪的东西

学这个的过程中一些奇怪的边角知识点

bash的引号括号

引号

总觉得引号这段以前学习过
双引号,转义空格,重定向,管道什么的(不转义$)
单引号,转义一切,包括$

括号

shell中各种括号的作用
圆括号()

命令组。括号中的命令将会新开一个子shell顺序执行

大括号{}

代码块,又被称为内部组,这个结构事实上创建了一个匿名函数 。与小括号中的命令不同,大括号内的命令不会新开一个子shell运行,即脚本余下部分仍可使用括号内变量。括号内的命令间用分号隔开,最后一个也必须有分号。{}的第一个命令和左括号之间必须要有一个空格。

但是这里的大括号逗号执行命令好像并不是上面说的内容,测试结果就是{cmd,arg}支持cmd arg这样子的命令运行,无参数可以arg留空,但参数最多只能有一个,也就是说只能绕过一个空格,如果命令存在多个空格,还想用逗号代替就会出错

底层命令执行

分析的时候可以看到命令执行是从Runtime.exec()进入了ProcessBuilder最后进了ProcessImpl,事实上最后的一步执行应该是调用了ProcessImpl的native方法forkAndExec,这个函数就是它的名字这样,进行fork和exec的系统调用进行命令执行。
当目标的防御程度只在ProcessBuilder的时候,就可以直接反射构造ProcessImpl获取其forkAndExec方法进行命令执行

结论

罗里吧嗦的说了一大堆,最后命令执行那段还不是自己调试看到的。。。得到的结论其实也很简单
Java的Runtime.getRuntime().exec()在传入参数为字符串时,会通过空格将字符串分隔成数组,将数组第一个元素作为可执行文件,后面所有元素作为参数进行系统调用execvp完成命令执行
因此导致了奇怪的命令执行失败
最稳定解决方案为base64编码

参考链接

Java Runtime.getRuntime().exec由表及里
Java下奇怪的命令执行
在 Runtime.getRuntime().exec(String cmd) 中执行任意shell命令的几种方法