Attacker Value
High
(2 users assessed)
Exploitability
High
(2 users assessed)
User Interaction
Unknown
Privileges Required
Unknown
Attack Vector
Unknown
1

CVE-2019-0232

Disclosure Date: April 15, 2019 Last updated February 13, 2020
Add MITRE ATT&CK tactics and techniques that apply to this CVE.

Description

When running on Windows with enableCmdLineArguments enabled, the CGI Servlet in Apache Tomcat 9.0.0.M1 to 9.0.17, 8.5.0 to 8.5.39 and 7.0.0 to 7.0.93 is vulnerable to Remote Code Execution due to a bug in the way the JRE passes command line arguments to Windows. The CGI Servlet is disabled by default. The CGI option enableCmdLineArguments is disable by default in Tomcat 9.0.x (and will be disabled by default in all versions in response to this vulnerability). For a detailed explanation of the JRE behaviour, see Markus Wulftange’s blog (https://codewhitesec.blogspot.com/2016/02/java-and-command-line-injections-in-windows.html) and this archived MSDN blog (https://web.archive.org/web/20161228144344/https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/).

Add Assessment

1
Technical Analysis

Background

Apache Tomcat is an open source implementation of the Java Servlet, JavaServer Pages, Java Expression Language and Java WebSocket technologies. It powers numerous large-scale, mission-critical web applications across a diverse range of industries and organizations.

The Common Gateway Interface (CGI) defines a way for a web server to interact with external content-generating programs, which are often referred to as CGI programs. Within Tomcat, CGI support is disabled by default, but can be manually added in the configuration file.

One of the configurations for the CGI servlet is enableCmdLineArguments, which allows command line arguments from the query string, but can be abused to inject system commands in order to gain remote code execution.

Vulnerable Setup

The following versions of Apache Tomcat on Windows are effected:

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

Since enableCmdLineArguments isn’t enabled by default, the following code needs to be added in the conf/web.xml file:

<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>
<init-param>
  <param-name>executable</param-name>
  <param-value></param-value>
</init-param>
<init-param>
  <param-name>enableCmdLineArguments</param-name>
  <param-value>true</param-value>
</init-param>
<load-on-startup>5</load-on-startup>
</servlet>

Also:

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

Finally, a script should be added in the webapps\ROOT\WEB-INF\cgi directory, which is the trigger for the CGI servlet:

@echo off
echo Content-Type: text/plain
echo.
echo Hello, World!

For convenience, a vulnerable setup of Apache Tomcat can be downloaded here. It was also used during the analysis on a x64 Windows 10 with JDK 8 installed.

Vulnerability Analysis

Static Code Analysis

In Apache Tomcat, CGI support is handled by the CGIServlet class, which can be found in the lib/catalina.jar file. You can easily find this by grepping for CGISeverlet as the keyword.

CGIServlet is a subclass of the abstract HttpServlet class, which is within the Java API. The way you want to start reading this code is by looking at the service method, which will tell you to go down to doGet, so let’s start with doGet:

protected void doGet(HttpServletRequest req, HttpServletResponse res)
        throws ServletException, IOException {

    CGIEnvironment cgiEnv = new CGIEnvironment(req, getServletContext());

    if (cgiEnv.isValid()) {
        CGIRunner cgi = new CGIRunner(cgiEnv.getCommand(),
                                      cgiEnv.getEnvironment(),
                                      cgiEnv.getWorkingDirectory(),
                                      cgiEnv.getParameters());

        if ("POST".equals(req.getMethod())) {
            cgi.setInput(req.getInputStream());
        }
        cgi.setResponse(res);
        cgi.run();
    } else {
        res.sendError(404);
    }

    if (log.isTraceEnabled()) {
        String[] cgiEnvLines = cgiEnv.toString().split(System.lineSeparator());
        for (String cgiEnvLine : cgiEnvLines) {
            log.trace(cgiEnvLine);
        }

        printServletEnvironment(req);
    }
}

The first thing that happens in the doGet method is creating a new instance of CGIEnvironment. The constructor of the class looks like this:

protected CGIEnvironment(HttpServletRequest req,
                         ServletContext context) throws IOException {
    setupFromContext(context);
    setupFromRequest(req);

    this.valid = setCGIEnvironment(req);

    if (this.valid) {
        workingDirectory = new File(command.substring(0,
              command.lastIndexOf(File.separator)));
    } else {
        workingDirectory = null;
    }
}

We see the first argument is an HttpServletRequest, which is extra interesting because since it’s an HTTP request, it implies there are user-controlled inputs. There are two methods that need the HTTP request: setupFromRequest, and setCGIEnvironment. Let’s take a look at the first:

protected void setupFromRequest(HttpServletRequest req)
        throws UnsupportedEncodingException {

    boolean isIncluded = false;

    // Look to see if this request is an include
    if (req.getAttribute(
            RequestDispatcher.INCLUDE_REQUEST_URI) != null) {
        isIncluded = true;
    }
    if (isIncluded) {
        this.contextPath = (String) req.getAttribute(
                RequestDispatcher.INCLUDE_CONTEXT_PATH);
        this.servletPath = (String) req.getAttribute(
                RequestDispatcher.INCLUDE_SERVLET_PATH);
        this.pathInfo = (String) req.getAttribute(
                RequestDispatcher.INCLUDE_PATH_INFO);
    } else {
        this.contextPath = req.getContextPath();
        this.servletPath = req.getServletPath();
        this.pathInfo = req.getPathInfo();
    }
    // If getPathInfo() returns null, must be using extension mapping
    // In this case, pathInfo should be same as servletPath
    if (this.pathInfo == null) {
        this.pathInfo = this.servletPath;
    }

    // If the request method is GET, POST or HEAD and the query string
    // does not contain an unencoded "=" this is an indexed query.
    // The parsed query string becomes the command line parameters
    // for the cgi command.
    if (enableCmdLineArguments && (req.getMethod().equals("GET")
        || req.getMethod().equals("POST") || req.getMethod().equals("HEAD"))) {
        String qs;
        if (isIncluded) {
            qs = (String) req.getAttribute(
                    RequestDispatcher.INCLUDE_QUERY_STRING);
        } else {
            qs = req.getQueryString();
        }
        if (qs != null && qs.indexOf('=') == -1) {
            StringTokenizer qsTokens = new StringTokenizer(qs, "+");
            while ( qsTokens.hasMoreTokens() ) {
                cmdLineParameters.add(URLDecoder.decode(qsTokens.nextToken(),
                                      parameterEncoding));
            }
        }
    }
}

The first block of the code about “include” can be skipped because that’s not what our request would be, so we want to focus on the second block:

if (enableCmdLineArguments && (req.getMethod().equals("GET")
                               || req.getMethod().equals("POST") || req.getMethod().equals("HEAD"))) {
  String qs;
  if (isIncluded) {
    qs = (String) req.getAttribute(
      RequestDispatcher.INCLUDE_QUERY_STRING);
  } else {
    qs = req.getQueryString();
  }
  if (qs != null && qs.indexOf('=') == -1) {
    StringTokenizer qsTokens = new StringTokenizer(qs, "+");
    while ( qsTokens.hasMoreTokens() ) {
      cmdLineParameters.add(URLDecoder.decode(qsTokens.nextToken(),
                                              parameterEncoding));
    }
  }
}

The first thing that gets checked in the if condition is the enableCmdLineArguments variable, which is also mentioned in the advisory. By default, this option is set to false:

private boolean enableCmdLineArguments = false;

In the init function of CGIServlet, it is also clear for us that the setting needs to be configured in the config file in order to get loaded in the code:

if (getServletConfig().getInitParameter("enableCmdLineArguments") != null) {
    enableCmdLineArguments =
            Boolean.parseBoolean(config.getInitParameter("enableCmdLineArguments"));
}

As the advisory and the code describe, when enableCmdLineArguments is enabled and the request is either GET, POST, or HEAD, we basically reach this part of the code:

qs = req.getQueryString();

Once the query string is loaded, that is normalized into command line parameters:

if (qs != null && qs.indexOf('=') == -1) {
  StringTokenizer qsTokens = new StringTokenizer(qs, "+");
  while ( qsTokens.hasMoreTokens() ) {
    cmdLineParameters.add(URLDecoder.decode(qsTokens.nextToken(),
                                            parameterEncoding));
  }
}

Back to the doGet method, after the command line parameters are loaded. We are at this block of code:

CGIRunner cgi = new CGIRunner(cgiEnv.getCommand(),
                              cgiEnv.getEnvironment(),
                              cgiEnv.getWorkingDirectory(),
                              cgiEnv.getParameters());

if ("POST".equals(req.getMethod())) {
  cgi.setInput(req.getInputStream());
}
cgi.setResponse(res);
cgi.run();

CGIRunner is a class that actually executes the CGI program, which occurs in the run method. The run method is really big, so it would be too much code to post but basically this is where code execution is gained:

rt = Runtime.getRuntime();
proc = rt.exec(
  cmdAndArgs.toArray(new String[cmdAndArgs.size()]),
  hashToStringArray(env), wd);

The run method also handles the program output that is returned to the HTTP client, which is another bonus for the attacker.

Dynamic Analysis

To verify this execution flow, we can use IntelliJ to set up some breakpoints to observe. To do this, first add the following to the bin/startup.bat file (at around line 21):

set JAVA_OPTS=-Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,address=4000,server=y,suspend=n

Next, start that file. There will be two important ports running: 4000 is the debugging port, and 8080 is the Tomcat server where the CGI program is served. Connect port 4000 with IntelliJ.

There are many possible methods we can add breakpoints for, but since we have seen that the Java’s Runtime class is used to execute the CGI program, we can add a breakpoint right there:

java.lang.Runtime.exec

When we visit the CGI program, the breakpoint should trigger with the following backtrace:

exec:617, Runtime (java.lang)
run:1579, CGIServlet$CGIRunner (org.apache.catalina.servlets)
doGet:575, CGIServlet (org.apache.catalina.servlets)
service:538, CGIServlet (org.apache.catalina.servlets)
service:741, HttpServlet (javax.servlet.http)
internalDoFilter:231, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilter:53, WsFilter (org.apache.tomcat.websocket.server)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
invoke:200, StandardWrapperValve (org.apache.catalina.core)
invoke:96, StandardContextValve (org.apache.catalina.core)
invoke:490, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:139, StandardHostValve (org.apache.catalina.core)
invoke:92, ErrorReportValve (org.apache.catalina.valves)
invoke:678, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:74, StandardEngineValve (org.apache.catalina.core)
service:343, CoyoteAdapter (org.apache.catalina.connector)
service:408, Http11Processor (org.apache.coyote.http11)
process:66, AbstractProcessorLight (org.apache.coyote)
process:834, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1415, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)

For the extra file, we can also attach WinDBG to java.exe. You may have multiple java.exe for whatever reason, just make sure you’re attaching the one that listens on port 8080. In WinDBG, add this breakpoint:

bu KERNELBASE!CreateProcessW

Again, when you visit the CGI program, you should see that Java is using CreateProcessW to launch the CGI program:

0:021> r
rax=0000000000000001 rbx=0000000008000400 rcx=0000000000000000
rdx=0000000017bbdd80 rsi=0000000017f869f8 rdi=ffffffffffffff00
rip=00007ffd8d423dd0 rsp=0000000019d7dee8 rbp=0000000019d7e050
 r8=0000000000000000  r9=0000000000000000 r10=0000000000000000
r11=0000000019d7df48 r12=0000000000000000 r13=0000000000000064
r14=00000000176d8980 r15=0000000000000060
iopl=0         nv up ei pl nz na po nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
KERNELBASE!CreateProcessW:
00007ffd`8d423dd0 4c8bdc          mov     r11,rsp
0:021> du rdx
00000000`17bbdd80  "C:\Users\sinn3r\Desktop\apache-t"
00000000`17bbddc0  "omcat-9.0.17\webapps\ROOT\WEB-IN"
00000000`17bbde00  "F\cgi\test.bat &echo CQdjFeaVZh"

It looks we are calling test.bat (the CGI program) with an arbitrary echo command. At this point, we have proven the exploitability of the vulnerability.

1
Ratings
  • Attacker Value
    High
  • Exploitability
    High
Technical Analysis

Given that the enableCmdLineArguments setting is configured, it looks fairly easy to get code execution. This should definitely be patched.

General Information

References

Advisory

Additional Info

Technical Analysis