文章目录
- 命令执行
- Runtime.exec()
- ProcessBuilder.start()
- ProcessImpl与UNIXProcess类
- Runtime.exec()命令执行漏洞
- Linux测试
- 原因
- Windows测试
- 防御
- 完
命令执行
Runtime.exec()
在传入命令时,可以传入String和String[]
String cmd1 = "/bin/sh -c whoami";
String[] cmd2 = {"/bin/sh", "-c", "whoami"};
这是由于exec()方法自带多种重载方法,其中envp指定环境,dir指定目录
public Process exec(String command)
public Process exec(String command, String[] envp)
public Process exec(String command, String[] envp, File dir)
public Process exec(String cmdarray[])
public Process exec(String[] cmdarray, String[] envp)
public Process exec(String[] cmdarray, String[] envp, File dir)
代码审计一下传入String的exec()
其实都是缺省了后几个参数,追一下完整的public Process exec(String command, String[] envp, File dir)
可以看到String command
经过StringTokenizer类实例处理后转换成了String[] cmdarray
,然后最终调用了public Process exec(String[] cmdarray, String[] envp, File dir)
,因此最终都是要转换成String[]来执行命令的。重点关注一下这里String转String[]的具体操作,首先追一下StringTokenizer的构造器
对于传入单参数,补全了两个参数又调用了三参数的重载构造器,这里加入了delimiter分隔符为空格、tab、回车换行、换行、换页符。
然后下面cmdarray的生成将String command按照默认分隔符进行切片保存,最终获得了String[] cmdarray,起一个动态调试:
得到String[]后,就到了public Process exec(String[] cmdarray, String[] envp, File dir)
,剩下就是ProcessBuilder的工作了
整个Runtime.exec(String command)的调用如下:
java.lang.Runtime.exec(String command)
java.lang.Runtime.exec(String command, String[] envp, File dir)
java.lang.Runtime.exec(String[] cmdarray, String[] envp, File dir)
java.lang.ProcessBuilder.start();
java.lang.ProcessImpl.start();
java.lang.UNIXProcess.<init>;
java.lang.forkAndExec();
然后在java.lang.UNIXProcess.<init>
构造器又调用了forkAndExec,这是一个native方法。
private native int forkAndExec;
接下来就是不同系统下调用C创建shell进程返回PID
ProcessBuilder.start()
调用链下一步是调用了它,因此也能直接用ProcessBuilder.start()
来执行命令
String[] cmd = {"/bin/sh", "-c", "ls"};
InputStream is = new ProcessBuilder(cmd).start().getInputStream();
ProcessImpl与UNIXProcess类
在JDK9以后ProcessImpl与UNIXProcess合并了,因此可以视为一个东西,都是调用了native方法新建进程。
Bb动态调试一下UNIXProcess是如何实例化的:
这里我们传入的指令是/bin/sh -c ls
prog是/bin/sh
加上结束符的c字符串,也就是ascii码串;argBlock是参数串-c ls
,其中空格被视为结束符;argc是参数数量2;其他都是默认。因此,构造反射
package Test;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
public class ReflectionUNIXProcess {
public static void main(String[] args) throws Exception {
Class<?> cls = Class.forName("java.lang.UNIXProcess");
Constructor<?> constructor = cls.getDeclaredConstructors()[0];
constructor.setAccessible(true);
String[] cmd = {"/bin/sh", "-c", "ls"};
byte[] prog = toCString(cmd[0]);
byte[] argBlock = getArgBlock(cmd);
int argc = argBlock.length;
int[] fds = {-1, -1, -1};
Object obj = constructor.newInstance(prog, argBlock, argc, null, 0, null, fds, false);
Method method = cls.getDeclaredMethod("getInputStream");
method.setAccessible(true);
InputStream is = (InputStream) method.invoke(obj);
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
String line = br.readLine();
while (line != null){
System.out.println(line);
line = br.readLine();
}
}
private static byte[] toCString(String str) {
byte[] bytes = str.getBytes();
byte[] result = new byte[bytes.length + 1];
System.arraycopy(bytes, 0, result, 0, bytes.length);
result[result.length - 1] = (byte) 0;
return result;
}
private static byte[] getArgBlock(String[] cmdarray){
byte[][] args = new byte[cmdarray.length-1][];
int size = args.length;
for (int i = 0; i < args.length; i++) {
args[i] = cmdarray[i+1].getBytes();
size += args[i].length;
}
byte[] argBlock = new byte[size];
int i = 0;
for (byte[] arg : args) {
System.arraycopy(arg, 0, argBlock, i, arg.length);
i += arg.length + 1;
}
return argBlock;
}
}
模仿ProcessImpl的操作写了两个自定义函数,运行成功
Runtime.exec()命令执行漏洞
由于String转String[]的过程中是以\t\n\r\f
截断的,只要绕过这五类分隔符就能拼接指令执行:
Linux测试
package Test;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
public class LCE {
public static void main(String[] args) throws Exception {
String hacker = ";cat${IFS}resource/rw.txt";
String cmd = "/bin/sh -c whoami" + hacker;
InputStream is = Runtime.getRuntime().exec(cmd).getInputStream();
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
String line = br.readLine();
while (line != null){
System.out.println(line);
line = br.readLine();
}
}
}
这里使用${IFS}绕过空格,就能执行后面的代码cat resource/rw.txt
原因
究其原因,是因为转换为String[]时候空格将cat的参数拆开了导致无法命令运行,而调用sh的参数-c要求传入一个完整的字符串,动态调试发现已经被拆成两个字符串了
如果用空格就会一直等待运行
Windows测试
Windows平台这边则无需绕过空格,直接拼接命令即可运行
字符串拆开不影响命令的完整性
防御
- 添加钩子函数监测命令执行
- 监测输入命令,过滤非法命令
完