Java代码审计-命令执行
前言
今天来学一下java代码审计中的命令执行漏洞相关知识,浅浅记录一下。
一、漏洞简介
命令执行漏洞是指应用有时需要调用一些执行系统命令的函数,如果系统命令代码未对用户可控参数做过滤,则当用户能控制这些函数中的参数时,就可以将恶意系统命令拼接到正常命令中,从而造成命令执行攻击。
命令执行攻击主要存在以下几个危害:继承 Web 服务程序的权限去执行系统命令或读/写文件,反弹 shell,控制整个网站甚至控制服务器,进一步实现内网渗透。
在 PHP 开发语言中有 system()、exec()、shell_exec()、eval()、passthru()等函数可以执行系统命令。在 Java 开发语言中可以执行系统命令的函数有 Runtime.getRuntime.exec 和ProcessBuilder.start。
二、命令连接符
Windows与Linux均支持:
-
cmd1 | cmd2 只执行cmd2
-
cmd1 || cmd2 只有当cmd1执行失败后,cmd2才被执行
-
cmd1 & cmd2 先执行cmd1,不管是否成功,都会执行cmd2
-
cmd1 && cmd2 先执行cmd1,cmd1执行成功后才执行cmd2,否则不执行cmd2
Linux单独支持:
- cmd1;cmd2 依次顺序执行命令
三、ProcessBuilder命令执行
1.ProcessBuilder简介
ProcessBuilder类是java.lang包下的基础类,在使用时无需导入,可以直接使用。它主要用于创建和运行各类外部程序。其start() 方法利用这些属性创建一个新的 Process 实例,可以利用 ProcessBuilder 执行命令。
如下示例,ping本地
public static void main(String[] args) throws IOException {
try {
// 创建 ProcessBuilder 实例
ProcessBuilder processBuilder = new ProcessBuilder("cmd","/c","ping", "127.0.0.1");
System.out.println(processBuilder.command());//要执行的指令打印
// 启动进程
Process process = processBuilder.start();
// 获取进程的输入流
InputStream inputStream = process.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String line;
// 读取进程的输出
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
// 等待进程结束
process.waitFor();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
输出如图所示。
2.漏洞利用
如上述代码,如果直接输入的命令为127.0.0.1|dir,则执行效果如下。
四、Runtime exec 命令执行
1.Runtime exec简介
java.lang.Runtime 公共类中的 exec()方法同样也可以执行系统命令,exec()方法的使用 方式有以下 6 种:
//在单独的进程中执行指定的字符串命令
public Process exec(String command)
//在单独的进程中执行指定的命令和参数
public Process exec(String[] cmdarray)
//在具有指定环境的单独进程中执行指定的命令和参数
public Process exec(String[] cmdarray, String[] envp)
//在具有指定环境和工作目录的单独进程中执行指定的命令和参数
public Process exec(String[] cmdarray, String[] envp, File dir)
//在具有指定环境的单独进程中执行指定的字符串命令
public Process exec(String command, String[] envp)
//在具有指定环境和工作目录的单独进程中执行指定的字符串命令
public Process exec(String command, String[] envp, File dir)
比如直接拼接字符串,然后exec()方法执行命令,结果如下
public static void main(String[] args) throws IOException {
// 从用户输入获取要 ping 的目标 IP 地址
System.out.print("请输入要 ping 的 IP 地址或域名: ");
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
String userInput = reader.readLine();
// 不安全的命令执行
String command = "cmd /c"+ "ping " + userInput; // 直接使用用户输入构建命令
Process process = Runtime.getRuntime().exec(command);
// 打印结果
BufferedReader processInput = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = processInput.readLine()) != null) {
System.out.println(line);
}
}
输入如下图所示。
2.漏洞利用
和第二部分一样,输入的参数未经过过滤,随意拼接造成漏洞执行。
五、探索
1.代码底层原理
探索一下,Runtime.getRuntime().exec(command)执行命令的底层原理。
String command = "cmd /c"+ "ping " + userInput; // 直接使用用户输入构建命令
Process process = Runtime.getRuntime().exec(command);
进入exec方法
发现调用了exec(String command, String[] envp, File dir)方法,默认envp和dir为空。
发现这个函数 是借助StringTokenizer工具 对字符串进行分割,存入数组,再执行。
接下来,调试具体看下情况。
然后,再次调用了exec(cmdarry,encp,dir),
原来,在这里最终调用的还是ProcessBuilder()方法 。
2.疑问
还是有个小疑问,在写代码进行测试的时候,命令语句,拼接了cmd /c
。如果没有这个,也能执行ping命令,但是加入|dir
注入并没有成功。那么,为什么会没有cmd /c 注入不会成功呢?另外为什么要加cmd /c 呢?
个人之见解如下:
-
首先对于没有使用cmd /c 的方式,不会造成命令注入,上述看代码原理已经探索过,分割字符串时按照空格分割,所以会造成"ping" “127.0.0.1|dir” 两个字符串。"127.0.0.1|dir"被看成一个整体字符串,而命令不被解析,导致注入失败。
-
对于使用 cmd /c的方式,更加灵活,可以使用管道、重定向等命令,所以对"127.0.0.1|dir"进行了解析,但是也造成了命令注入的风险。
3.小结
-
使用
cmd /c
:-
适合执行包含多个命令、管道、重定向等复杂命令的情况。
-
所有在命令行中可用的功能和语法都可以使用。
-
需要防范命令注入,需对输入参数进行过滤
-
-
不使用
cmd /c
:-
只适合执行简单的原生命令。
-
不能利用命令解释器的特性,如管道和其他命令的结合。
-
经个人简单测试,命令注入失败。
-
六、总结
本文浅显探索了Java中命令执行漏洞的相应代码和产生原理,另发现,看底层代码的具体实现逻辑还是挺有意思的!
参考
- 网络安全:Java代码审计实战
- Java代码审计(入门篇)