学习JAVA代码审计,首先需要知道JAVA底层代码的逻辑,以下是相关命令执行的函数,我将以底层逻辑来进行演示。
在 Java 中可用于执行系统命令的方式有三种,分别是:
java.lang.Runtime
java.lang.ProcessBuilder
java.lang.UNIXProcess/ProcessImpl
1. java.lang.Runtime
Runtime 是 java.lang 中的一个类,主要是与操作系统交互执行操作命令。java.lang.Runtime中我们主要关注exec()方法,exec()方法有以下六种重载形式可以传入不同的数据类型的参数。
解释如下:
方法 | 释义 |
exec(String[] cmdarray) | 在单独的进程中执行指定的命令和参数。 |
exec(String command) | 在单独的进程中执行指定的字符串命令。 |
exec(String command, String[] envp, File dir) | 在具有指定环境和工作目录的单独进程中执行指定的字符串命令。 |
exec(String command, String[] envp) | 在具有指定环境的单独进程中执行指定的字符串命令。 |
exec(String[] cmdarray, String[] envp) | 在具有指定环境的单独进程中执行指定的命令和参数。 |
exec(String[] cmdarray, String[] envp, File dir) | 在具有指定环境和工作目录的单独进程中执行指定的命令和参数。 |
我们主要关注exec(String command)和exec(String[] cmdarray)这两种执行方式。
exec(String command)
单独的进程中执行指定的字符串命令,也就是直接执行字符串命令。
如下示例,我们输入命令后,就直接执行command的命令。
//简易示例代码
String command = "whoami";
Runtime.getRuntime().exec(command)
演示示例exec(String cmdarray)
我们创建一个JAVAWEB的项目
public void execRuntimeString(String command, HttpServletResponse response) throws IOException {
String line = null;
//使用 Runtime 类的 exec 方法执行给定的命令,并返回一个表示进程的 Process 对象。
Process process = Runtime.getRuntime().exec(command);
//创建一个 BufferedReader 对象,用于读取进程的标准输出流。这里通过 InputStreamReader 将字节流转换为字符流,并指定字符集为 GBK,以正确处理中文字符。
BufferedReader bufferedReader =
new BufferedReader(new InputStreamReader(process.getInputStream(), Charset.forName("GBK")));
//获取 HttpServletResponse 对象的输出流,用于向客户端发送响应。
PrintWriter out = response.getWriter();
//使用 while 循环逐行读取进程的输出,直到读取到末尾为止。
while ((line = bufferedReader.readLine()) != null) {
//逐行读取
System.out.println(line);
out.print(line);
}
//闭 BufferedReader 对象,释放资源。
bufferedReader.close();
}
启动之后访问相关地址:
http://127.0.0.1:8080/execRuntimeString?command=echo%20123
这里是执行输出123的命令,可以看到命令执行成功
exec(String[] cmdarray)
在单独的进程中执行指定的命令和参数。以数组的形式接收多个字符串然后执行。
示例:
String[] command = { "cmd", "/c", "whoami" };
Runtime.getRuntime().exec(command)
演示示例exec(String[] cmdarray)
我们创建一个java web的项目,示例代码如下
@RequestMapping("/execRuntimeArray")
public void execRuntimeArray(String command, HttpServletResponse response) throws IOException {
//定义一个字符串变量 line,用于逐行读取命令执行的结果。
String line = null;
//创建一个字符串数组 commandarray,其中包含了要执行的命令及其参数。这里使用了 Windows 的命令提示符(cmd.exe),通过 /c 参数告诉命令提示符执行完命令后自动退出,command 参数是从请求中传入的。
String[] commandarray ={"cmd","/c",command};
//用 Java 的 Runtime 类的 exec 方法执行命令数组,生成一个新的进程。
Process process = Runtime.getRuntime().exec(commandarray);
//创建一个 BufferedReader 对象,用于读取命令执行后产生的输出流。这里使用了指定的字符集 "GBK" 来读取输出流,通常是因为命令行默认使用GBK编码。
BufferedReader bufferedReader =
new BufferedReader(new InputStreamReader(process.getInputStream(), Charset.forName("GBK")));
//获取 HttpServletResponse 的 PrintWriter 对象,用于向客户端发送响应。
PrintWriter out = response.getWriter();
while ((line = bufferedReader.readLine()) != null) {
//逐行读取
System.out.println(line);
out.print(line);
}
bufferedReader.close();
}
之后访问
http://127.0.0.1:8080/execRuntimeArray?command=ipconfig
可以看到,命令执行成功
2. java.lang.ProcessBuilder
上面我们介绍了java.lang.Runtime,实际上Runtime是java.lang中的一个API,现在介绍的ProcessBuilder也是java.lang的一个API,它们的不同之处在于ProcessBulider主要用于创建操作系统进程。
在java.lang.ProcessBuilder中我们要关注command()方法,可通过该方法设置要执行的命令参数。以及start()方法,简单来说使用该方法可以执行命令。
command()方法主要用于设置要执行的命令。使用start()方法可以创建一个新的具有命令,或环境,或工作目录,或输入来源,或标准输出和标准错误输出的目标,或redirectErrorStream属性的进程。新进程中调用的命令和参数有command()方法设置,工作目录将由directory()方法设置,进程环境将由environment()设置。在使用command()方法设置执行命令参数后,然后由start()方法创建一个新的进程进而在系统中执行了我们设置的命令。
我们创建一个Java web进程
@RequestMapping("/ProcceBuilder")
public void ProcceBuilder(String command, HttpServletResponse response) throws IOException {
//定义一个字符串变量 line,用于逐行读取命令执行的结果。
String line = null;
//创建一个 ProcessBuilder 对象,用于构建新的进程。
ProcessBuilder processBuilder = new ProcessBuilder();
//设置命令及其参数。这里也是使用 Windows 的命令提示符(cmd.exe),通过 /c 参数告诉命令提示符执行完命令后自动退出,command 参数是从请求中传入的。
processBuilder.command("cmd.exe", "/c", command);
//使用 ProcessBuilder 对象的 start 方法启动进程。
Process process = processBuilder.start();
//创建一个 BufferedReader 对象,用于读取命令执行后产生的输出流。这里同样使用了指定的字符集 "GBK" 来读取输出流,通常是因为命令行默认使用GBK编码。
BufferedReader bufferedReader =
new BufferedReader(new InputStreamReader(process.getInputStream(), Charset.forName("GBK")));
//获取 HttpServletResponse 的 PrintWriter 对象,用于向客户端发送响应。
PrintWriter out = response.getWriter();
while ((line = bufferedReader.readLine()) != null) {
//逐行读取
System.out.println(line);
out.print(line);
}
bufferedReader.close();
}
接着访问以下接口
http://127.0.0.1:8080/ProcceBuilder?command=ipconfig
可以看到命令执行成功
3. java.lang.UNIXProcess/ProcessImpl
JDK9 的时候把UNIXProcess合并到了ProcessImpl当中,因此UNIXProcess和ProcessImpl可以理解本就是一个东西。
UNIXProcess和ProcessImpl最终都是调用native执行系统命令的类,这个类提供了一个叫forkAndExec的native方法,如方法名所述主要是通过fork&exec来执行本地系统命令。
UNIXProcess类是*nix系统在 java 程序中的体现,可以使用该类创建新进程,实现与”fork”类似的功能(对于Windows系统,使用的是java.lang.ProcessImpl类)
ProcessImpl 是更为底层的实现,Runtime和ProcessBuilder执行命令实际上也是调用了ProcessImpl这个类。
对于 ProcessImpl 类,我们不能直接调用需要配合使用反射。因为 java.lang.ProcessImpl 代码都被 private 封装起来了。并没有设置公共的 API 接口。
创建一个java web项目,演示代码如下
@RequestMapping("/ProcessImpl")
public void ProcessImpl(String command, HttpServletResponse response) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
//定义一个字符串变量 line,用于逐行读取命令执行的结果。
String line = null;
//将传入的命令封装到一个字符串数组 cmds 中。
String[] cmds = new String[]{command};
//使用反射机制获取 java.lang.ProcessImpl 类的 Class 对象。ProcessImpl 是一个内部实现类,在正常使用中不应该直接访问。
Class clazz = Class.forName("java.lang.ProcessImpl");
//获取 ProcessImpl 类中的 start 方法,这个方法有五个参数:一个 String[] 数组,一个 Map,一个 String,一个 ProcessBuilder.Redirect[] 数组和一个 boolean。
Method method = clazz.getDeclaredMethod("start", String[].class, Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class);
//设置方法的可访问性为 true,即使它是私有的,这样可以通过反射调用它。
method.setAccessible(true);
//通过反射调用 start 方法来启动进程。传入参数分别是命令数组 cmds,null (表示没有环境变量),当前目录 ".",null (表示没有重定向) 和 true (表示要使用 ProcessBuilder 的特性)。method.invoke 的第一个参数为 null,因为 start 是静态方法。
Process process = (Process) method.invoke(null, cmds, null, ".", null, true);
//创建一个 BufferedReader 对象,用于读取命令执行后产生的输出流。这里同样使用了指定的字符集 "GBK" 来读取输出流,通常是因为命令行默认使用 GBK 编码。
BufferedReader bufferedReader =
new BufferedReader(new InputStreamReader(process.getInputStream(), Charset.forName("GBK")));
//获取 HttpServletResponse 的 PrintWriter 对象,用于向客户端发送响应。
PrintWriter out = response.getWriter();
while ((line = bufferedReader.readLine()) != null) {
//逐行读取
System.out.println(line);
out.print(line);
}
bufferedReader.close();
}
接着访问
http://127.0.0.1:8080/ProcessImpl?command=ipconfig
可以看到命令执行成功