JAVA Security | In-depth analysis of the underlying mechanism of Runtime.exec command execution

0 28
JAVA Security | In-depth analysis of the underlying mechanism of Runtime.exec co...

JAVA Security | In-depth analysis of the underlying mechanism of Runtime.exec command execution

In Java,Runtime.getRuntime().execThis 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 && LinuxExploring the system environment to investigate the differences between the Runtime of these two systems.

Runtime ProcessBuilder API

JAVA Security | In-depth analysis of the underlying mechanism of Runtime.exec command execution

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.execMany methods are overridden, and we focus onexec(String command) && exec(String[] cmdarray, String[] envp, File dir) on the method.

whileProcessBuilderThe constructor only allows passingan array of String type, orExisting ArrayListEnter, 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 callsProcessBuilderSo, 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

image

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 playStringTokenizerclass, we do not know what it is used for, so we can check the official API documentation description:

image

It is very concise and clear, splitting the string by spaces to get the values in turn, so here we usewhoamithe command test does not show any effect, here we useping www.baidu.comto do a test:

image

we can see that it has been successfully split into a character array, hereStringTokenizeratin Linuxwill cause a command execution failure issue, which we will mention again later.

The code then calledexecmethod, let's follow up and see.

image

Here we can see that,Runtime.getRuntime().exec()the method is justProcessBuilderthe class encapsulation call. Then we can locally callProcessBuilderto perform the operation of executing commands.

new ProcessBuilder command execution - direct instantiation

then we can directlyinstance the ProcessBuilder classto 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 directlyProcessBuilderto perform command execution operations.

Analysis of the ProcessBuilder call process

image

due toProcessImplthe class property modifier is notpublic, so we cannot call the method of this class under any package, but here we canReflection|Unsafeto 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::startmethod.

ProcessImpl::start process analysis

image

new ProcessImpl command execution - reflection

We can see that,ProcessImpl::startultimately only callednew ProcessImploperation, due toProcessImplthe 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

image

  • 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.exepath conversion toWindowsrecognizable pathC:\\Windows\\System32\\cmd.exe

  • needsEscapingfunction analysis

image

It's simple in two sentences: either the sides you pass have double quotes, or the middle of what you pass cannot havespace, \tcharacters. 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\\MyAppif so, this will be executedC:\\Program.exe, the meaning has changed, and adding double quotes here is also to allowC:\\Program Files\\MyApp.exefile executed correctly.

  • createCommandLinefunction analysis

image

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 intocreatemethod to execute the command, as follows:

image

Finally, it will call thenativemethod, 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 isCreateProcessWthisWindows 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 & calcThe command, if you want to execute this command, you must callC:\\Windows\\System32\\cmd.exe /c whoami & calcExecute.

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.txtThe file, contains123The 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

image

Through what we just didWindows analysisthe calling process, theoretically, goes throughStringTokenizerThe processing is irrelevant, because inWindowsAfter 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 ProcessBuilderCommand 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 upProcessBuilderLet's take a look at what the constructor does.

image

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

image

It can be clearly seen that becauseStringTokenizerThe processing changes from string to array. AndLinux-ProcessImpl::start method inThe processing process is different fromWindows-ProcessImpl::start methodThe processing process is different,Windows-ProcessImpl::start methodthe original array is passed tonew ProcessImplconstructor for processing, whilenew ProcessImplis finally converted back to the array passed inthe string, soWindowsis almost not affected byStringTokenizerbutLinuxwill be affected byStringTokenizerThe space split of-c,"echo,123"are all treated as command line arguments, and each argument is placed intoargsThere will be ambiguity here, causing the command execution to fail!

Of course, if you want to execute the command correctly,-c,echo 123is considered as a parameter is correct. Before discussing the solution, let's look attoCStringdoes in the call:

image

This code is relatively easy to understand, and we will discuss later what the methodnew UNIXProcessWhen executing commands, write code by imitating its logic.

Linux, the solution affected by StringTokenizer

As we know, inRuntime.getRuntime().execmethod overrides manyexecmethod, afterStringTokenizerThe model being processed is also receivedStringType 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 notStringTokenizerThe 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 willPOCCan 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 becauseStringTokenizerIt will affectSpace, /tPerform 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 avoidedStringTokenizerSplit.

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}666This string in actualLinuxwill not run in the environment, the reason isecho${IFS}666both sides are not enclosed in double quotes.

whileJavaruns successfully due toStringTokenizerThe split has already regarded it as a parameter, so no quotes are needed.

new UNIXProcess command execution - reflection

andWindows-ProcessImpl::startThe method differences are also in the lastLinuxto invokenew UNIXProcessin the constructor, so we cannot call it throughnew ProcessImplIt 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

image

This constructor is quite direct, it directly callsforkAndExecThe method, this method isnativeThe 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().execall the call processes inUNIXProcess.forkAndExecmember method. We can also constructUNIXProcess.forkAndExeccommand execution code segment, but it makes no sense, because it is a member method, and naturally it also needsUNIXProcessAn instance of this class.

atWindows,Runtime.getRuntime().execAll the calls in, call the underlyingProcessImpl.createIn 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().execThe underlying mechanism is still very interesting, and there are some masters who have written it on the Internet already:Runtimetools to avoid in practical combat:RuntimeHere 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>
你可能想看:

In-depth Analysis and Practice: Analysis of Apache Commons SCXML Remote Code Execution Vulnerability and POC EXP Construction

(3) Is the national secret OTP simply replacing the SHA series hash algorithms with the SM3 algorithm, and becoming the national secret version of HOTP and TOTP according to the adopted dynamic factor

5. Collect exercise results The main person in charge reviews the exercise results, sorts out the separated exercise issues, and allows the red and blue sides to improve as soon as possible. The main

2021-Digital China Innovation Competition-Huifu Cybersecurity Track-Final-Web-hatenum and source code analysis and payload script analysis

b) It should have a login failure handling function, and should configure and enable measures such as ending the session, limiting the number of illegal login attempts, and automatically logging out w

b) It should have the login failure handling function, and should configure and enable measures such as ending the session, limiting the number of illegal logins, and automatically exiting when the lo

In-depth analysis of the Atom CMS 2.0 vulnerability: revealing the truth behind remote code execution

Ensure that the ID can be accessed even if it is guessed or cannot be tampered with; the scenario is common in resource convenience and unauthorized vulnerability scenarios. I have found many vulnerab

In-depth Analysis: Mining Trojan Analysis and Emergency Response Disposal Under a Complete Attack Chain

As announced today, Glupteba is a multi-component botnet targeting Windows computers. Google has taken action to disrupt the operation of Glupteba, and we believe this action will have a significant i

最后修改时间:
admin
上一篇 2025年03月30日 15:27
下一篇 2025年03月30日 15:50

评论已关闭