Skip to Content
OpenVAPTAlpha
Pre-release. Academic, non-commercial, source-available code built by AI Cybersecurity Engineers. Free for your own use, 100% at your own risk. This website collects no data. Read MoreRead Less
Pre-release. OpenVAPT has no Stable release yet. Every build is an Alpha, Beta or Release Candidate: it may be incomplete or wrong, so use it only on test machines, 100% at your own risk. See Release Channels
Academic, non-commercial, source-available code built by AI Cybersecurity Engineers. These are AI systems, not people or registered engineers. Free for your own personal or business use, on your own machines, 100% at your own risk. Not for sale. Not for providing services. This website collects no data. Read the Acknowledgements
Pre-release. OpenVAPT has no Stable release yet. Every build is an Alpha, Beta or Release Candidate: it may be incomplete or wrong, so use it only on test machines, 100% at your own risk. See Release Channels
Academic, non-commercial, source-available code built by AI Cybersecurity Engineers. These are AI systems, not people or registered engineers. Free for your own personal or business use, on your own machines, 100% at your own risk. Not for sale. Not for providing services. This website collects no data. Read the Acknowledgements
CriticalWEB-TOM-004Web Application SecurityWeb Apps

Tomcat AJP Connector Unprotected (Ghostcat)

Checks for an active AJP connector that has no shared secret and is not bound to loopback, or that disables the secret requirement.

Why It Matters
AJP is a trusted, unauthenticated protocol between a web server and Tomcat. CVE-2020-1938 (Ghostcat) showed that a reachable AJP port lets an attacker read any file in the web applications and, where uploads exist, execute code. It is listed in the CISA Known Exploited Vulnerabilities catalogue.
How It Detects
Reads the <Connector> elements of conf/server.xml (comments ignored) and, for those with protocol AJP/1.3 or an AJP protocol class, fails when secretRequired="false" or when there is no secret (or requiredSecret) and the address is not a loopback address.
How to Fix
If you do not use AJP (most sites proxy over HTTP), delete or comment out the AJP <Connector>. Otherwise set address="127.0.0.1", a long random secret="..." with secretRequired="true", and the same secret in the proxy (mod_proxy_ajp: ProxyPass ... secret=...). Upgrade Tomcat to a supported release.
How to Verify
grep -i ajp <conf>/server.xml and ss -tlnp | grep 8009 shows nothing or 127.0.0.1 only.
CVSS v3.1 Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Control Category
Web Application Security: CIS Controls v8 16; NIST CSF 2.0 PR.PS, PR.DS
Fix Effort
Low
CIS Controls v8 Safeguard
16.7
NIST CSF 2.0 Subcategory
PR.PS-01
CWE
CWE-306, CWE-1327
OWASP Top 10
A05:2021, A06:2021
NIST SP 800-53 Rev. 5
SC-7, AC-3, CM-7
References
Apache Tomcat 10.1, Security Considerations
Apache Tomcat 10.1 Configuration Reference, The AJP Connector
NVD, CVE-2020-1938
Learn the theory behind this check: P13 Application Servers: Tomcat, Node.js and Python.

Source Code

The complete file that defines this check. Run it with openvapt scan --only WEB-TOM-004, or read it offline with openvapt explain WEB-TOM-004.

internal/checks/web/tomcat.go · Lines 1-217View on GitHub at This Commit
1package web
2
3import (
4 "context"
5 "fmt"
6 "net"
7 "regexp"
8 "strings"
9
10 "github.com/openvapt/openvapt/internal/check"
11 "github.com/openvapt/openvapt/internal/checks/collect"
12 "github.com/openvapt/openvapt/internal/sys"
13)
14
15// Apache Tomcat checks. They read server.xml, tomcat-users.xml and the
16// deployed application folders; no request is sent to Tomcat, and passwords
17// read from tomcat-users.xml are compared in memory and never reported.
18
19var refTomcatSec = check.Ref{Title: "Apache Tomcat 10.1, Security Considerations", URL: "https://tomcat.apache.org/tomcat-10.1-doc/security-howto.html"}
20
21type tomcatRule struct {
22 id, title, summary, msg, rationale, detection, fix, verify, cvss string
23 sev check.Severity
24 cat check.Category
25 refs []check.Ref
26 std check.Standards
27 eval func(context.Context, sys.System, collect.TomcatInstance) []string
28}
29
30func registerTomcat(r tomcatRule) {
31 check.Register(&check.Check{ID: r.id, Title: r.title, Domain: check.DomainWeb, Severity: r.sev, Category: r.cat, Effort: check.EffortLow,
32 CVSS: r.cvss, Summary: r.summary, Rationale: r.rationale, Detection: r.detection, Fix: r.fix, Verify: r.verify, Refs: r.refs, Standards: r.std,
33 Run: func(ctx context.Context, s sys.System) []check.Result {
34 ts := collect.Tomcats(s)
35 if len(ts) == 0 {
36 return []check.Result{check.SkipR("tomcat", "No Apache Tomcat installation found")}
37 }
38 var out []check.Result
39 for _, t := range ts {
40 if bad := r.eval(ctx, s, t); len(bad) > 0 {
41 out = append(out, check.FailR(t.Conf, r.msg, bad...))
42 } else {
43 out = append(out, check.PassR(t.Conf, "No issue found"))
44 }
45 }
46 return out
47 },
48 })
49}
50
51func readText(ctx context.Context, s sys.System, path string) string {
52 r, err := s.ReadFile(ctx, path)
53 if err != nil {
54 return ""
55 }
56 return string(r.Data)
57}
58
59func loopbackAddr(a string) bool {
60 if a == "localhost" {
61 return true
62 }
63 ip := net.ParseIP(a)
64 return ip != nil && ip.IsLoopback()
65}
66
67// Passwords Tomcat documentation, tutorials and scanners use as examples.
68var tomcatDefaults = map[string]bool{"": true, "tomcat": true, "admin": true, "password": true, "s3cret": true, "secret": true,
69 "changeit": true, "manager": true, "root": true, "123456": true, "role1": true, "both": true, "<must-be-changed>": true}
70
71func init() {
72 registerTomcat(tomcatRule{
73 id: "WEB-TOM-001", title: "Tomcat Manager Reachable Without Address Restriction", sev: check.High, cat: check.CatWebApp, msg: "Tomcat Manager is deployed without an address restriction",
74 cvss: "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H",
75 summary: "Checks whether the Tomcat Manager or Host Manager application is deployed without an address filter limiting it to localhost.",
76 rationale: "The Manager deploys WAR files, which means it runs any code it is given. Exposed to the network it is one stolen or guessed password away from full compromise, and it is a standard target of automated attacks.",
77 detection: "Looks for the manager and host-manager applications in each Tomcat installation's webapps folder (and /usr/share/tomcatN-admin on Debian and Ubuntu), then reads their META-INF/context.xml and conf/Catalina/localhost/<app>.xml. Fails when no active RemoteAddrValve or RemoteCIDRValve restricts access.",
78 fix: "If you do not use them, delete the manager and host-manager folders (or apt remove tomcatN-admin). Otherwise keep the default valve in META-INF/context.xml: <Valve className=\"org.apache.catalina.valves.RemoteAddrValve\" allow=\"127\\.\\d+\\.\\d+\\.\\d+|::1|0:0:0:0:0:0:0:1\" /> and reach the Manager through an SSH tunnel.",
79 verify: "grep -A2 RemoteAddrValve <webapps>/manager/META-INF/context.xml",
80 refs: []check.Ref{refTomcatSec, {Title: "Apache Tomcat 10.1, Manager App How-To", URL: "https://tomcat.apache.org/tomcat-10.1-doc/manager-howto.html"}},
81 std: check.Standards{CWE: []string{"CWE-1188", "CWE-923"}, OWASP: []string{"A01:2021", "A05:2021"}, WSTG: []string{"WSTG-CONF-05"}, NIST: []string{"AC-3", "CM-7"}},
82 eval: func(ctx context.Context, s sys.System, t collect.TomcatInstance) []string {
83 var bad []string
84 for _, app := range []string{"manager", "host-manager"} {
85 var dirs []string
86 for _, w := range t.Webapps {
87 if collect.Exists(s, w+"/"+app+"/META-INF/context.xml") || collect.Exists(s, w+"/"+app+"/WEB-INF/web.xml") {
88 dirs = append(dirs, w+"/"+app)
89 }
90 }
91 if len(dirs) == 0 {
92 continue
93 }
94 restricted := false
95 for _, ctxFile := range []string{dirs[0] + "/META-INF/context.xml", t.Conf + "/Catalina/localhost/" + app + ".xml"} {
96 for _, v := range collect.XMLElements(readText(ctx, s, ctxFile), "Valve") {
97 cls := v["classname"]
98 if (strings.HasSuffix(cls, "RemoteAddrValve") || strings.HasSuffix(cls, "RemoteCIDRValve")) && v["allow"] != "" && v["allow"] != ".*" {
99 restricted = true
100 }
101 }
102 }
103 if !restricted {
104 bad = append(bad, dirs[0]+" is deployed with no RemoteAddrValve or RemoteCIDRValve restriction")
105 }
106 }
107 return bad
108 },
109 })
110
111 registerTomcat(tomcatRule{
112 id: "WEB-TOM-002", title: "Tomcat Administrative Users With Default or Weak Passwords", sev: check.Critical, cat: check.CatIdentity, msg: "Tomcat administrative users have weak credentials",
113 cvss: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
114 summary: "Checks tomcat-users.xml for manager or admin accounts with well-known default passwords, passwords equal to the user name, or short passwords.",
115 rationale: "Default Tomcat credentials (tomcat/tomcat, admin/admin, tomcat/s3cret) are in every attack tool's word list. A Manager account with such a password lets anyone deploy code on the server.",
116 detection: "Parses the <user> elements of conf/tomcat-users.xml (comments ignored). For users with manager-gui, manager-script, manager-jmx, admin-gui or admin-script roles, compares the password in memory against a short list of documented defaults, the user name and a 12-character minimum. Passwords stored as digests are not judged. No password is ever written to the report.",
117 fix: "Remove accounts you do not need. Give the remaining ones long random passwords and store them as digests: configure a CredentialHandler (for example SecretKeyCredentialHandler with PBKDF2) in the Realm and generate hashes with bin/digest.sh. Restart Tomcat.",
118 verify: "Re-run this check; no administrative user should be listed.",
119 refs: []check.Ref{refTomcatSec, {Title: "Apache Tomcat 10.1, Realm Configuration How-To (digested passwords)", URL: "https://tomcat.apache.org/tomcat-10.1-doc/realm-howto.html"}},
120 std: check.Standards{CWE: []string{"CWE-1392", "CWE-521", "CWE-256"}, OWASP: []string{"A07:2021"}, ASVS: []string{"2.1.1", "2.5.4"}, WSTG: []string{"WSTG-ATHN-02"}, NIST: []string{"IA-5"}},
121 eval: func(ctx context.Context, s sys.System, t collect.TomcatInstance) []string {
122 doc := readText(ctx, s, t.Conf+"/tomcat-users.xml")
123 digest := regexp.MustCompile(`(?i)CredentialHandler`).MatchString(readText(ctx, s, t.Conf+"/server.xml"))
124 var bad []string
125 for _, u := range collect.XMLElements(doc, "user") {
126 roles := strings.ToLower(u["roles"])
127 if !strings.Contains(roles, "manager-") && !strings.Contains(roles, "admin-") {
128 continue
129 }
130 pw, name := u["password"], u["username"]
131 if digest && len(pw) >= 32 {
132 continue
133 }
134 switch {
135 case tomcatDefaults[strings.ToLower(pw)]:
136 bad = append(bad, fmt.Sprintf("user %q (roles %s) uses a documented default password", name, u["roles"]))
137 case strings.EqualFold(pw, name):
138 bad = append(bad, fmt.Sprintf("user %q (roles %s) uses the user name as password", name, u["roles"]))
139 case len(pw) < 12:
140 bad = append(bad, fmt.Sprintf("user %q (roles %s): fewer than 12 characters in the password", name, u["roles"]))
141 }
142 }
143 return bad
144 },
145 })
146
147 registerTomcat(tomcatRule{
148 id: "WEB-TOM-003", title: "Tomcat Shutdown Port Uses the Default Command", sev: check.Medium, cat: check.CatConfig, msg: "Tomcat shutdown port uses the default command",
149 cvss: "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H",
150 summary: "Checks whether Tomcat's shutdown port is enabled with the well-known default command SHUTDOWN.",
151 rationale: "Anyone who can connect to the shutdown port (8005 by default) and send the shutdown string stops Tomcat. With the default string, every local user and every compromised process on the machine can take the applications offline.",
152 detection: "Reads the <Server> element of conf/server.xml and fails when port is not -1 and the shutdown attribute is SHUTDOWN.",
153 fix: "Disable the port with <Server port=\"-1\" ...> and stop Tomcat through systemd, or set shutdown to a long random string. Restart Tomcat.",
154 verify: "grep '<Server' <conf>/server.xml shows port=\"-1\".",
155 refs: []check.Ref{refTomcatSec, {Title: "Apache Tomcat 10.1 Configuration Reference, The Server Component", URL: "https://tomcat.apache.org/tomcat-10.1-doc/config/server.html"}},
156 std: check.Standards{CWE: []string{"CWE-1188"}, OWASP: []string{"A05:2021"}, NIST: []string{"CM-6", "CM-7"}},
157 eval: func(ctx context.Context, s sys.System, t collect.TomcatInstance) []string {
158 for _, srv := range collect.XMLElements(readText(ctx, s, t.Conf+"/server.xml"), "Server") {
159 if srv["port"] != "-1" && srv["shutdown"] == "SHUTDOWN" {
160 return []string{fmt.Sprintf("<Server port=%q shutdown=\"SHUTDOWN\">", srv["port"])}
161 }
162 }
163 return nil
164 },
165 })
166
167 registerTomcat(tomcatRule{
168 id: "WEB-TOM-004", title: "Tomcat AJP Connector Unprotected (Ghostcat)", sev: check.Critical, cat: check.CatWebApp, msg: "AJP connector is reachable without protection",
169 cvss: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
170 summary: "Checks for an active AJP connector that has no shared secret and is not bound to loopback, or that disables the secret requirement.",
171 rationale: "AJP is a trusted, unauthenticated protocol between a web server and Tomcat. CVE-2020-1938 (Ghostcat) showed that a reachable AJP port lets an attacker read any file in the web applications and, where uploads exist, execute code. It is listed in the CISA Known Exploited Vulnerabilities catalogue.",
172 detection: "Reads the <Connector> elements of conf/server.xml (comments ignored) and, for those with protocol AJP/1.3 or an AJP protocol class, fails when secretRequired=\"false\" or when there is no secret (or requiredSecret) and the address is not a loopback address.",
173 fix: "If you do not use AJP (most sites proxy over HTTP), delete or comment out the AJP <Connector>. Otherwise set address=\"127.0.0.1\", a long random secret=\"...\" with secretRequired=\"true\", and the same secret in the proxy (mod_proxy_ajp: ProxyPass ... secret=...). Upgrade Tomcat to a supported release.",
174 verify: "grep -i ajp <conf>/server.xml and ss -tlnp | grep 8009 shows nothing or 127.0.0.1 only.",
175 refs: []check.Ref{refTomcatSec, {Title: "Apache Tomcat 10.1 Configuration Reference, The AJP Connector", URL: "https://tomcat.apache.org/tomcat-10.1-doc/config/ajp.html"}, {Title: "NVD, CVE-2020-1938", URL: "https://nvd.nist.gov/vuln/detail/cve-2020-1938"}},
176 std: check.Standards{CWE: []string{"CWE-306", "CWE-1327"}, OWASP: []string{"A05:2021", "A06:2021"}, NIST: []string{"SC-7", "AC-3", "CM-7"}},
177 eval: func(ctx context.Context, s sys.System, t collect.TomcatInstance) []string {
178 var bad []string
179 for _, c := range collect.XMLElements(readText(ctx, s, t.Conf+"/server.xml"), "Connector") {
180 if !strings.Contains(strings.ToUpper(c["protocol"]), "AJP") {
181 continue
182 }
183 port := c["port"]
184 switch {
185 case strings.EqualFold(c["secretrequired"], "false") && !loopbackAddr(c["address"]):
186 bad = append(bad, "AJP connector on port "+port+" is not bound to loopback and does not require a secret")
187 case c["secret"] == "" && c["requiredsecret"] == "" && !loopbackAddr(c["address"]):
188 bad = append(bad, "AJP connector on port "+port+" is not bound to loopback and has no secret")
189 }
190 }
191 return bad
192 },
193 })
194
195 registerTomcat(tomcatRule{
196 id: "WEB-TOM-005", title: "Tomcat Example and Documentation Applications Deployed", sev: check.Medium, cat: check.CatExposure, msg: "Tomcat sample applications are deployed",
197 cvss: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N",
198 summary: "Checks whether the examples or docs web applications that ship with Tomcat are deployed.",
199 rationale: "The sample applications are written to demonstrate features, not to be safe. Some (such as the session example) let visitors change server-side session data, and all of them reveal the exact Tomcat version.",
200 detection: "Looks for examples/ and docs/ application folders (WEB-INF/web.xml or index.html) in each Tomcat installation's webapps folders, including the Debian and Ubuntu tomcatN-examples and tomcatN-docs packages.",
201 fix: "Delete the examples and docs folders from webapps (or apt remove tomcatN-examples tomcatN-docs) and restart Tomcat.",
202 verify: "ls <webapps> lists only your own applications (and ROOT).",
203 refs: []check.Ref{refTomcatSec, {Title: "OWASP WSTG-CONF-02, Test Application Platform Configuration", URL: "https://wstg.owasp.org/v4.2/4-Web_Application_Security_Testing/02-Configuration_and_Deployment_Management_Testing/02-Test_Application_Platform_Configuration/"}},
204 std: check.Standards{CWE: []string{"CWE-1188"}, OWASP: []string{"A05:2021"}, WSTG: []string{"WSTG-CONF-02"}, NIST: []string{"CM-7"}},
205 eval: func(ctx context.Context, s sys.System, t collect.TomcatInstance) []string {
206 var bad []string
207 for _, w := range t.Webapps {
208 for _, app := range []string{"examples", "docs"} {
209 if collect.Exists(s, w+"/"+app+"/WEB-INF/web.xml") || collect.Exists(s, w+"/"+app+"/index.html") {
210 bad = append(bad, w+"/"+app+" is deployed")
211 }
212 }
213 }
214 return bad
215 },
216 })
217}

Also available as plain Markdown for notes, scripts and AI tools. Same facts, same source, no styling.

Cite This Page

If this page helped your work, please cite it. The version and web address are filled in; the access date is today's date in your browser. Every other style, and when credit is required, is on How to Cite OpenVAPT.

APA 7
OpenVAPT. (2026). WEB-TOM-004 Tomcat AJP Connector Unprotected (Ghostcat) (Version 0.1.0-alpha.6). OpenVAPT Check Catalogue. Retrieved October 16, 2026, from https://openvapt.com/checks/WEB-TOM-004.html

In text: (OpenVAPT, 2026). Accessed: the date shown is today's date from your own browser (the site's build date without JavaScript); change it to the day you read the page.

BibTeX
@misc{openvapt_web_tom_004,
  author       = {{OpenVAPT}},
  title        = {{WEB-TOM-004 Tomcat AJP Connector Unprotected (Ghostcat)}},
  year         = {2026},
  howpublished = {\url{https://openvapt.com/checks/WEB-TOM-004.html}},
  note         = {OpenVAPT Check Catalogue, version 0.1.0-alpha.6. Accessed 16 October 2026}
}

Accessed: the date shown is today's date from your own browser (the site's build date without JavaScript); change it to the day you read the page.

Citing OpenVAPT does not mean OpenVAPT endorses, approves or has reviewed your work.