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命令的几种方法