JAVA Security | In-depth analysis of the underlying mechanism of Runtime.exec command execution
In Java,Runtime.getRuntime().exec
This is a classic method for executing commands. This article will analyze the call chain of this method and write various command execution methods during the analysis of the underlying source code.
Since Java is a cross-platform language, the calling methods of both are different in the JDK source code, and the author has prepared them hereWindows && Linux
Exploring the system environment to investigate the differences between the Runtime of these two systems.
Runtime ProcessBuilder API

To study the relationship between Runtime and ProcessBuilder, we need to list their common methods and constructors first, so that we have a concept in mind.
# Multiple rewrites of the exec methods in Runtime.exec
public Process exec(String command) throws IOException { // Calls exec(String command, String[] envp, File dir) for processing
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 { // Ultimately calls to exec(String[] cmdarray, String[] envp, File dir)
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) // Finally call this method
throws IOException {
return new ProcessBuilder(cmdarray)
.environment(envp)
.directory(dir)
.start();
}
As can be seen, ourRuntime.exec
Many methods are overridden, and we focus onexec(String command) && exec(String[] cmdarray, String[] envp, File dir)
on the method.
whileProcessBuilder
The constructor only allows passingan array of String type
, orExisting ArrayList
Enter, as follows:
public ProcessBuilder(List<String> command) {
if (command == null)
throw new NullPointerException();
this.command = command;
}
public ProcessBuilder(String... command) {
this.command = new ArrayList<>(command.length);
for (String arg : command)
this.command.add(arg);
}
So let's analyze what they actually do.
Runtime command execution
Because Runtime actually callsProcessBuilder
So, the author prepares a command execution DEMO here first, and then analyzes and explores new command execution methods step by step.
public class MyCmdTester {
@Test
public void t1() {
try {
InputStream is = Runtime.getRuntime().exec("whoami").getInputStream(); // Obtain InputStream
ByteArrayOutputStream resData = new ByteArrayOutputStream(); // Prepare to place the command execution result
byte[] buffer = new byte[1024]; // Prepare a 1024-byte buffer
int len;
while ((len = is.read(buffer)) > 0) { // Read 1024 bytes into buffer
resData.write(buffer, 0, len); // Assign the value of buffer to ByteArrayOutputStream
}
System.out.println("Command execution result: " + new String(resData.toByteArray())); // heihubook\administrator
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
call chain analysis [Windows]
Runtime.getRuntime().exec() did what
From the above code we can see that,public Process exec(String command)
the method ultimately calledpublic Process exec(String command, String[] envp, File dir)
, here comes into playStringTokenizer
class, we do not know what it is used for, so we can check the official API documentation description:
It is very concise and clear, splitting the string by spaces to get the values in turn, so here we usewhoami
the command test does not show any effect, here we useping www.baidu.com
to do a test:
we can see that it has been successfully split into a character array, hereStringTokenizer
atin Linux
will cause a command execution failure issue, which we will mention again later.
The code then calledexec
method, let's follow up and see.
Here we can see that,Runtime.getRuntime().exec()
the method is justProcessBuilder
the class encapsulation call. Then we can locally callProcessBuilder
to perform the operation of executing commands.
new ProcessBuilder command execution - direct instantiation
then we can directlyinstance the ProcessBuilder class
to call system commands, here we do a test:
public class T1 {
public static void main(String[] args) {
try {
Process process = new ProcessBuilder(new String[]{"whoami"}).start(); // Directly call the start method
InputStream inputStream = process.getInputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = inputStream.read(buffer)) > 0) {
System.out.println(new String(buffer, 0, len)); // heihubook\administrator
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
What we can see is that we can directlyProcessBuilder
to perform command execution operations.
Analysis of the ProcessBuilder call process
due toProcessImpl
the class property modifier is notpublic
, so we cannot call the method of this class under any package, but here we canReflection|Unsafe
to call the class.
ProcessImpl.start command execution - reflection
Based on the logic of the above imitation, we can write the following command execution code:
public class T2 {
public static void main(String[] args) {
try {
Class<?> clazz = Class.forName("java.lang.ProcessImpl");
/**
static Process start(String[] cmdarray,
java.util.Map<String,String> environment,
String dir,
ProcessBuilder.Redirect[] redirects,
boolean redirectErrorStream)
*/
Method start = clazz.getDeclaredMethod("start",
String[].class, Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class);
start.setAccessible(true);
Process process = (Process) start.invoke(null,
new String[]{"whoami"}, null, null, null, false);
InputStream inputStream = process.getInputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = inputStream.read(buffer)) > 0) {
System.out.println(new String(buffer, 0, len)); // heihubook\administrator
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
Finally, by reflection, it was successfully calledProcessImpl::start
method.
ProcessImpl::start process analysis
new ProcessImpl command execution - reflection
We can see that,ProcessImpl::start
ultimately only callednew ProcessImpl
operation, due toProcessImpl
the access modifier is notpublic
, so here I use reflection to execute commands. The mimic of the underlying code is as follows:
public class T4 {
public static void main(String[] args) {
try {
Class<?> clazz = Class.forName("java.lang.ProcessImpl");
/*
private ProcessImpl(String cmd[],
final String envblock,
final String path,
final long[] stdHandles,
final boolean redirectErrorStream)
*/
Constructor<?> constructor = clazz.getDeclaredConstructor(String[].class, String.class, String.class, long[].class, boolean.class);
constructor.setAccessible(true);
Process process = (Process) constructor.newInstance(new String[]{"whoami"}, null, null, new long[]{-1, -1, -1}, false);
InputStream inputStream = process.getInputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = inputStream.read(buffer)) > 0) {
System.out.println(new String(buffer, 0, len)); // heihubook\administrator
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
new ProcessImpl execution flow
String executablePath = new File(cmd[0]).getPath();
pass in the command line, usingnew File().getPath()
The reason for this is also to convertC:/Windows/System32/cmd.exe
path conversion toWindows
recognizable pathC:\\Windows\\System32\\cmd.exe
needsEscaping
function analysis
It's simple in two sentences: either the sides you pass have double quotes, or the middle of what you pass cannot havespace, \t
characters. Otherwise, I will add double quotes around both sides.
In fact, this approach can also be understood, because if you want to executeC:\\Program Files\\MyApp
if so, this will be executedC:\\Program.exe
, the meaning has changed, and adding double quotes here is also to allowC:\\Program Files\\MyApp.exe
file executed correctly.
createCommandLine
function analysis
Finally, a series of parameter concatenation operations are performed. The cmd[] array is reassembled into the execution command string. After these operations are completed, the final string is passed intocreate
method to execute the command, as follows:
Finally, it will call thenative
method, fromopenjdk
中翻出来它的函数定义如下:
// https://github.com/bpupadhyaya/openjdk-8/blob/master/jdk/src/windows/native/java/lang/ProcessImpl_md.c
JNIEXPORT jlong JNICALL
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;
const jchar *pdir = (dir != NULL)
? (*env)->GetStringChars(env, dir, NULL)
: NULL;
jlong *handles = (*env)->GetLongArrayElements(env, stdHandles, NULL);
if (handles != NULL) {
ret = processCreate( // Use processCreate for command execution call
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};
{
/* Extraction of current process standard IOE handles */
DWORD idsIOE[3] = {STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, STD_ERROR_HANDLE};
int i;
for (i = 0; i < 3; ++i)
/* Should not be closed by CloseHandle! */
stdIOE[i] = GetStdHandle(idsIOE[i]);
}
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;
}
success = initHolder(env, &handles[2], &holderErr, &si.hStdError);
}
if (success) {
PROCESS_INFORMATION pi;
DWORD processFlag = CREATE_UNICODE_ENVIRONMENT;
/* Suppress popping-up of a console window for non-console applications */
if (GetConsoleWindow() == NULL)
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");
}
closeSafely(pi.hThread);
ret = (jlong)pi.hProcess;
}
}
releaseHolder(ret == 0, &holderErr);
releaseHolder(ret == 0, &holderOut);
}
releaseHolder(ret == 0, &holderIn);
}
}
restoreIOEHandleState(stdIOE, inherit);
return ret;
}
the underlying call isCreateProcessW
thisWindows API
:
#include <windows.h>
#include <stdio.h>
int main() {
STARTUPINFOA si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
si.dwFlags = STARTF_USESHOWWINDOW;
si.wShowWindow = SW_SHOW; // Let the new window be visible
LPSTR cmdLine = "C:\\Windows\\System32\\cmd.exe /c notepad & calc";
if (!CreateProcessA(NULL, cmdLine, NULL, NULL, FALSE, CREATE_NEW_CONSOLE , NULL, NULL, &si, &pi)) {
DWORD errorCode = GetLastError();
printf("CreateProcessA failed (%d).\n", errorCode);
return 1;
}
WaitForSingleObject(pi.hProcess, INFINITE);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return 0;
}
This API cannot be executedwhoami & calc
The command, if you want to execute this command, you must callC:\\Windows\\System32\\cmd.exe /c whoami & calc
Execute.
Summary: new ProcessImpl() ultimately calls the ProcessImpl.create method
ProcessImpl.create uses WindowsAPI - CreateProcessW for command execution. CreateProcessW can only start one SHELL. If we want to execute batch commands, we must call them through cmd.exe /c.
Execution of ProcessImpl.create command - Reflection
public class T5 {
public static void main(String[] args) {
try {
Class<?> clazz = Class.forName("java.lang.ProcessImpl");
/*
private static synchronized native long create(String cmdstr,
String envblock,
String dir,
long[] stdHandles,
boolean redirectErrorStream)
*/
Method createMethod = clazz.getDeclaredMethod("create", String.class, String.class, String.class, long[].class, boolean.class);
createMethod.setAccessible(true);
long pid = (long) createMethod.invoke(null, "cmd /c calc",
null, null, new long[]{-1, -1, -1}, false); // Return PID
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
After running, the calculator will pop up.
Call chain analysis [Linux]
Why this article also writes Linux, the core reason is that there is such code, the result of the call in Linux is different from that in Windows.
Windows && Linux write file issues
In Windows:
public static void main(String[] args) {
try {
Runtime.getRuntime().exec("cmd.exe /c \"echo 123 > D:/a.txt\"");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
After running successfully, the D disk will have morea.txt
The file, contains123
The command can run normally.
In Linux:
public static void main(String[] args) throws IOException {
Process process = Runtime.getRuntime().exec("/bin/sh -c \"echo 123 > /tmp/1.txt\"");
InputStream errorStream = process.getErrorStream();
byte[] buffer = new byte[1024];
int len;
while((len = errorStream.read(buffer)) > 0){
System.out.println(new String(buffer, 0, len)); // 123: 1: Syntax error: Unterminated quoted string
}
}
Not only did it fail to run, but error information can also be obtained from the error stream. Below, we will analyze what exactly was done in the source code.
Runtime.getRuntime().exec() did what
Through what we just didWindows analysis
the calling process, theoretically, goes throughStringTokenizer
The processing is irrelevant, because inWindows
After the processing is completed, it will be converted back to a string, but there will be some problems in Linux, so let's point it out here first, and then we will see what problems have occurred later.
new ProcessBuilder command execution - direct instantiation
Because the processing process here is basically consistent with Windows, so we can still go throughnew ProcessBuilder
Command execution:
public static void main(String[] args) {
try {
Process process = new ProcessBuilder(new String[]{"whoami"}).start(); // Directly call the start method
InputStream inputStream = process.getInputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = inputStream.read(buffer)) > 0) {
System.out.println(new String(buffer, 0, len)); // root
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
Analysis of the ProcessBuilder call process
Let's keep following upProcessBuilder
Let's take a look at what the constructor does.
As can be seen here, there is little difference from Windows, so of course command execution can also be performed here.
ProcessImpl.start command execution - reflection
Yet it still uses the previous Windows method, and it can still execute commands:
try {
Class<?> clazz = Class.forName("java.lang.ProcessImpl");
/**
static Process start(String[] cmdarray,
java.util.Map<String,String> environment,
String dir,
ProcessBuilder.Redirect[] redirects,
boolean redirectErrorStream)
*/
Method start = clazz.getDeclaredMethod("start",
String[].class, Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class);
start.setAccessible(true);
Process process = (Process) start.invoke(null,
new String[]{"whoami"}, null, null, null, false);
InputStream inputStream = process.getInputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = inputStream.read(buffer)) > 0) {
System.out.println(new String(buffer, 0, len)); // root
}
} catch (Exception e) {
throw new RuntimeException(e);
}
ProcessImpl::start process analysis
It can be clearly seen that becauseStringTokenizer
The processing changes from string to array. AndLinux-ProcessImpl::start method in
The processing process is different fromWindows-ProcessImpl::start method
The processing process is different,Windows-ProcessImpl::start method
the original array is passed tonew ProcessImpl
constructor for processing, whilenew ProcessImpl
is finally converted back to the array passed inthe string
, soWindows
is almost not affected byStringTokenizer
butLinux
will be affected byStringTokenizer
The space split of-c
,"echo
,123"
are all treated as command line arguments, and each argument is placed intoargs
There will be ambiguity here, causing the command execution to fail!
Of course, if you want to execute the command correctly,-c
,echo 123
is considered as a parameter is correct. Before discussing the solution, let's look attoCString
does in the call:
This code is relatively easy to understand, and we will discuss later what the methodnew UNIXProcess
When executing commands, write code by imitating its logic.
Linux, the solution affected by StringTokenizer
As we know, inRuntime.getRuntime().exec
method overrides manyexec
method, afterStringTokenizer
The model being processed is also receivedString
Type method:
public Process exec(String command) throws IOException {
return exec(command, null, null);
}
// Subsequent method calls are as follows
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);
}
// Clearly it has passed through StringTokenizer, so ambiguity has occurred.
While notStringTokenizer
The class processing method model is as follows:
public Process exec(String cmdarray[]) throws IOException {
return exec(cmdarray, null, null);
}
// Finally call the following method
public Process exec(String[] cmdarray, String[] envp, File dir) // Finally call this method
throws IOException {
return new ProcessBuilder(cmdarray)
.environment(envp)
.directory(dir)
.start();
}
So we willPOC
Can avoid ambiguity by changing to the following situation:
public static void main(String[] args) throws IOException {
Process process = Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", "echo 123 > /tmp/1.txt"});
InputStream errorStream = process.getErrorStream();
byte[] buffer = new byte[1024];
int len;
while((len = errorStream.read(buffer)) > 0){
System.out.println(new String(buffer, 0, len));
}
}
And becauseStringTokenizer
It will affectSpace, /t
Perform splitting, after splittingcmdarray[0]
Handled as the main running program,cmdarray[n]
Handled as parameters, so when our actual parameters do not contain spaces, it can be avoidedStringTokenizer
Split.
public class T3 {
public static void main(String[] args) throws IOException {
Process process = Runtime.getRuntime().exec("bash -c echo${IFS}heihu577");
InputStream inputStream = process.getInputStream();
byte[] buffer = new byte[1024];
int len;
while((len = inputStream.read(buffer)) > 0){
System.out.println(new String(buffer, 0, len)); // heihu577
}
}
}
Of course,bash -c echo${IFS}666
This string in actualLinux
will not run in the environment, the reason isecho${IFS}666
both sides are not enclosed in double quotes.
whileJava
runs successfully due toStringTokenizer
The split has already regarded it as a parameter, so no quotes are needed.
new UNIXProcess command execution - reflection
andWindows-ProcessImpl::start
The method differences are also in the lastLinux
to invokenew UNIXProcess
in the constructor, so we cannot call it throughnew ProcessImpl
It calls reflection, notnew UNIXProcess
, In this case, we can mimic the underlying logic:
public class T4 {
public static void main(String[] args) {
try {
Class<?> clazz = Class.forName("java.lang.UNIXProcess");
/**
* UNIXProcess(final byte[] prog,
* final byte[] argBlock, final int argc,
* final byte[] envBlock, final int envc,
* final byte[] dir,
* final int[] fds,
* final boolean redirectErrorStream)
*/
Constructor<?> declaredConstructor = clazz.getDeclaredConstructor(byte[].class, byte[].class,
int.class, byte[].class, int.class, byte[].class, int[].class, boolean.class);
declaredConstructor.setAccessible(true);
Object result = declaredConstructor.newInstance("/bin/sh ".replace(" ", "\0").getBytes(),
"-c whoami ".replace(" ", "\0").getBytes(), 2,
null, 0, null, new int[]{-1, -1, -1}, false);
// Parameter 1: /bin/sh+space, imitating the running logic of toCString
// Parameter 2: -c+space+whoami+space, imitating the running logic of toCString
// Parameter 3: -c and whoami are two parameters, so pass 2
Method inputStreamMethod = clazz.getDeclaredMethod("getInputStream");
inputStreamMethod.setAccessible(true);
InputStream inputStream = (InputStream) inputStreamMethod.invoke(result);
byte[] buffer = new byte[1024];
int len;
while((len = inputStream.read(buffer)) > 0){
System.out.println(new String(buffer, 0, len)); // root
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
new UNIXProcess execution process
This constructor is quite direct, it directly callsforkAndExec
The method, this method isnative
The method, its source code is as follows:
// https://github.com/bpupadhyaya/openjdk-8/blob/master/jdk/src/solaris/native/java/lang/UNIXProcess_md.c
JNIEXPORT jint JNICALL
Java_java_lang_UNIXProcess_forkAndExec(JNIEnv *env,
jobject process
jint mode,
jbyteArray helperpath,
jbyteArray prog,
jbyteArray argBlock, jint argc,
jbyteArray envBlock, jint envc,
jbyteArray dir,
jintArray std_fds,
jboolean redirectErrorStream)
{
int errnum;
int resultPid = -1;
int in[2], out[2], err[2], fail[2], childenv[2];
jint *fds = NULL;
const char *phelperpath = NULL;
const char *pprog = NULL;
const char *pargBlock = NULL;
const char *penvBlock = NULL;
ChildStuff *c;
in[0] = in[1] = out[0] = out[1] = err[0] = err[1] = fail[0] = fail[1] = -1;
childenv[0] = childenv[1] = -1;
if ((c = NEW(ChildStuff, 1)) == NULL) return -1;
c->argv = NULL;
c->envv = NULL;
c->pdir = NULL;
c->clone_stack = NULL;
/* Convert prog + argBlock into a char ** argv.
Add one word room for expansion of argv for use by
execve_as_traditional_shell_script.
This word is also used when using spawn mode
*/
assert(prog != NULL && argBlock != NULL);
if ((phelperpath = getBytes(env, helperpath)) == NULL) goto Catch;
if ((pprog = getBytes(env, prog)) == NULL) goto Catch;
if ((pargBlock = getBytes(env, argBlock)) == NULL) goto Catch;
if ((c->argv = NEW(const char *, argc + 3)) == NULL) goto Catch;
c->argv[0] = pprog;
c->argc = argc + 2;
initVectorFromBlock(c->argv+1, pargBlock, argc);
if (envBlock != NULL) {
/* Convert envBlock into a char ** envv */
if ((penvBlock = getBytes(env, envBlock)) == NULL) goto Catch;
if ((c->envv = NEW(const char *, envc + 1)) == NULL) goto Catch;
initVectorFromBlock(c->envv, penvBlock, envc);
}
if (dir != NULL) {
if ((c->pdir = getBytes(env, dir)) == NULL) goto Catch;
}
assert(std_fds != NULL);
fds = (*env)->GetIntArrayElements(env, std_fds, NULL);
if (fds == NULL) goto Catch;
if ((fds[0] == -1 && pipe(in) < 0) ||
(fds[1] == -1 && pipe(out) < 0) ||
(fds[2] == -1 && pipe(err) < 0) ||
(pipe(childenv) < 0) ||
(pipe(fail) < 0) {
throwIOException(env, errno, "Bad file descriptor");
goto Catch;
}
c->fds[0] = fds[0];
c->fds[1] = fds[1];
c->fds[2] = fds[2];
copyPipe(in, c->in);
copyPipe(out, c->out);
copyPipe(err, c->err);
copyPipe(fail, c->fail);
copyPipe(childenv, c->childenv);
c->redirectErrorStream = redirectErrorStream;
c->mode = mode;
resultPid = startChild(env, process, c, phelperpath);
assert(resultPid != 0);
if (resultPid < 0) {
switch (c->mode) {
case MODE_VFORK:
throwIOException(env, errno, "vfork failed");
break;
case MODE_FORK:
throwIOException(env, errno, "fork failed");
break;
case MODE_POSIX_SPAWN:
throwIOException(env, errno, "spawn failed");
break;
}
goto Catch;
}
close(fail[1]); fail[1] = -1; /* See: WhyCantJohnnyExec (childproc.c) */
switch (readFully(fail[0], &errnum, sizeof(errnum))) {
case 0: break; /* Exec succeeded */
case sizeof(errnum):
waitpid(resultPid, NULL, 0);
throwIOException(env, errnum, "Exec failed");
goto Catch;
default:
throwIOException(env, errno, "Read failed");
goto Catch;
}
fds[0] = (in [1] != -1) ? in [1] : -1;
fds[1] = (out[0] != -1) ? out[0] : -1;
fds[2] = (err[0] != -1) ? err[0] : -1;
Finally:
free(c->clone_stack);
/* Always clean up the child's side of the pipes */
closeSafely(in [0]);
closeSafely(out[1]);
closeSafely(err[1]);
/* Always clean up fail and childEnv descriptors */
closeSafely(fail[0]);
closeSafely(fail[1]);
closeSafely(childenv[0]);
closeSafely(childenv[1]);
releaseBytes(env, prog, pprog);
releaseBytes(env, argBlock, pargBlock);
releaseBytes(env, envBlock, penvBlock);
releaseBytes(env, dir, c->pdir);
free(c->argv);
free(c->envv);
free(c);
if (fds != NULL)
(*env)->ReleaseIntArrayElements(env, std_fds, fds, 0);
return resultPid;
Catch:
/* Clean up the parent's side of the pipes in case of failure only */
closeSafely(in [1]); in[1] = -1;
closeSafely(out[0]); out[0] = -1;
closeSafely(err[0]); err[0] = -1;
goto Finally;
}
atLinux
,Runtime.getRuntime().exec
all the call processes inUNIXProcess.forkAndExec
member method. We can also constructUNIXProcess.forkAndExec
command execution code segment, but it makes no sense, because it is a member method, and naturally it also needsUNIXProcess
An instance of this class.
atWindows
,Runtime.getRuntime().exec
All the calls in, call the underlyingProcessImpl.create
In the static method, it is still meaningful for us to construct the command execution code segment, because we only need to make a static call and do not depend on any object.
These two methods are different due to different underlying code, so the process in the source code is also different. Windows receives a string command string, and Linux receives a command array, which leads to unexpected issues when executing commands in the Linux environment if there are spaces. This article explains these situations and reasons one by one.
Ending...
ExplorationRuntime.getRuntime().exec
The underlying mechanism is still very interesting, and there are some masters who have written it on the Internet already:Runtime
tools to avoid in practical combat:Runtime
Here are some issues that cannot be executed, let's share them, the tool is very good:
<!DOCTYPE html>
<html lang="en">
<head>
<title>java.lang.Runtime.exec() Payload Workarounds - @Jackson_T</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- <link rel="stylesheet" href="https://www.freebuf.com/articles/es/css/main.css" type="text/css" /> -->
<style>
body {
margin: 0;
padding: 10px 0;
text-align: center;
font-family: 'Ubuntu Condensed', sans-serif;
color: #585858;
background-color: #fff;
font-size: 13px;
line-height: 1.4
}
::selection {
background: #fff2a8;
}
pre, code {
font-family: 'Ubuntu Mono', 'Consolas', Monospace;
font-size: 13px;
background-color: #E5F5E5;
color: #585858;
padding-left: 0.25em;
padding-right: 0.25em;
/*display: block;*/
}
#wrap {
margin-left: 1em;
margin-right: 1em;
text-align: left;
font-size: 13px;
line-height: 1.4
}
#wrap {
width: 820px;
}
#container {
float: right;
width: 610px;
}
.entry {
font-size: 14px;
line-height: 20px;
hyphens: auto;
font-family: 'Roboto', sans-serif, 'Inconsolata', Monospace;
}
</style>
</head>
<body>
<div id="wrap">
<div id="container">
<div class="entry">
<article>
<p>Occasionally, command execution fails with <code>Runtime.getRuntime().exec()</code>. This may happen when using web shells, deserialization vulnerabilities, or other vectors.</p>
<p>Occasionally, the use of redirection and pipe characters may be meaningless in the context of the process being started. For example, <code>ls > dir_listing</code> when executed in the shell should output the list of the current directory to a file named <code>dir_listing</code>. However, in the context of the <code>exec()</code> function, the command is interpreted as getting <code>></code> and the directory <code>dir_listing</code>.</p>
<p>At other times, parameters containing spaces are destroyed by the StringTokenizer class. This class splits spaces into command strings. Such things as <code>ls "My Directory"</code> are interpreted as <code>ls '"My' 'Directory"'</code>.</p>
<p>With the help of Base64 encoding, the following converter can help reduce these issues. It can make pipelines and redirections better by calling Bash or PowerShell again, and also ensures that there are no spaces in the parameters.</p>
<p>Input type: <input type="radio" id="bash" name="option" value="bash"} <p onclick="processInput();" checked="" /><label for="bash">Bash</label> <input type="radio" id="powershell" name="option" value="powershell" onclick="processInput();" /><label for="powershell">PowerShell</label> <input type="radio" id="python" name="option" value="python" onclick="processInput();" /><label for="python">Python</label> <input type="radio" id="perl" name="option" value="perl" onclick="processInput();" /><label for="perl">Perl</label></p>
<p><textarea rows="10" style="width: 100%; box-sizing: border-box;" id="input" placeholder="Type input here..."></textarea> <textarea rows="5" style="width: 100%; box-sizing: border-box;" id="output" onclick="this.focus(); this.select();" readonly=""></textarea></p>
<script>
var taInput = document.querySelector('textarea#input');
var taOutput = document.querySelector('textarea#output');
function processInput() {
var option = document.querySelector('input[name="option"]:checked').value;
switch (option) {
case 'bash':
taInput.placeholder = 'Type Bash here...'
taOutput.value = 'bash -c {echo,' + btoa(taInput.value) + '}|{base64,-d}|{bash,-i}';
break;
case 'powershell':
taInput.placeholder = 'Type PowerShell here...'
poshInput = ''
for (var i = 0; i < taInput.value.length; i++) { poshInput += taInput.value[i] + unescape("%00"); }
taOutput.value = 'powershell.exe -NonI -W Hidden -NoP -Exec Bypass -Enc ' + btoa(poshInput);
break;
case 'python':
taInput.placeholder = 'Type Python here...'
taOutput.value = "python -c exec('" + btoa(taInput.value) + "'.decode('base64'))";
break;
case 'perl':
taInput.placeholder = 'Type Perl here...'
taOutput.value = "perl -MMIME::Base64 -e eval(decode_base64('" + btoa(taInput.value) + "'))";
break;
default:
taOutput.value = ''
}
if (!taInput.value) taOutput.value = '';
}
taInput.addEventListener('input', processInput, false);
</script>
</article>
</div>
</div>
</div>
</body>
</html>

评论已关闭