A detection script for the exploit precondition of CVE-2026-11374, a predictable SSO-ticket
flaw that leads to unauthenticated account takeover across the ManageEngine AD360 suite. The
four in-scope products share the ManageEngineADSFramework:
| Product | Affected build | Fixed build |
|---|---|---|
| ADSelfService Plus | ≤ 6528 | 6529 |
| RecoveryManager Plus | ≤ 6320 | 6321 |
| M365 Manager Plus | ≤ 4816 | 4817 |
| ADAudit Plus | ≤ 8702 | 8703 |
In affected builds the SSO ticket is just System.currentTimeMillis() (a predictable timestamp) sampled at the victim's
login, so it can be replayed through the CUSTOM_SSO_TICKET cookie to hijack that session. The
fix replaces the ticket with UUID.randomUUID() (a sufficiently random identifier). The replay path is only reachable when the
product is AD360-integrated: ADSFilter gates it on isProductIntegrated(). That gate is the
precondition this tool checks for.
NOTE: this detector confirms the precondition, not the vulnerability itself. A
POTENTIALLY_AFFECTEDresult is not a confirmed-vulnerable verdict — see below for more info.
Yes. It's built for production and assessment use.
- Nothing is exploited. The probe sends an invalid SSO ticket (
1700000000000, a millisecond value far enough in the past that it can never be cache-resident), so no session is ever recovered. The server just tells us to clear the cookies we sent. - No target state changes. Every request is a plain
GET, and the only cookies affected are the throwaway ones the tool sends. - No brute forcing or session resolution. The tool does not attempt the active timestamp-ticket resolution that would prove exploitability; that's exploitation rather than detection, and it's out of scope here (see Limitations).
CVE-2026-11374's patch changed only how the ticket is generated (milliseconds to UUID); it did not change the cookie-replay path this probe exercises. As a result, a patched install responds byte-for-byte identically to a vulnerable one for any unauthenticated request.
What the tool can do, unauthenticated and non-destructively:
- Confirm that a reachable in-scope product has the CustomSSO cookie-replay path active (that is, it is AD360-integrated), which is the CVE-2026-11374 exploit precondition.
- Identify which of the four products it is, from the per-product session cookie.
- Make a best-effort read of an asset build number as a hint. This is available on ADSelfService Plus, ADAudit Plus, and M365 Manager Plus; unavailable on RecoveryManager Plus.
What it can't do:
- Tell vulnerable from patched. There is no safe, passive, unauthenticated signal for it. A
POTENTIALLY_AFFECTEDresult means the precondition is met and you should go verify the patch level, not that the host is confirmed vulnerable. - Confirm exploitability. Proving a host is actually exploitable requires observing a minted
ticket's format (a 13-digit number is vulnerable, a UUID is patched, both of which need
authenticated or on-host visibility), reading
conf/product.conflocally, or actively resolving a live ticket (real exploitation, intentionally not implemented here). - Fully trust a below-fixed ADSelfService Plus build number. ADSSP's
?build=is sometimes the real build and sometimes a frozen placeholder that reads below the fixed build, so a below-fixed value is ambiguous and the tool marks it inconclusive. A value at or above the fixed build is still a trustworthy patched signal, since the placeholder is always too low to reach it.
- Python 3.7+ and the
requestslibrary. Install withpip install requests.
# single host (prefer a URL or host:port; ports differ per product)
./cve_2026_11374_check.py https://adssp.example.com:8888
# multiple hosts (scheme optional: https is tried first, then http)
./cve_2026_11374_check.py host-a:8081 host-b:8365
# scan a list, one target per line ('#' comments allowed), compact output
./cve_2026_11374_check.py -f targets.txt --brief
# machine-readable output for pipelines
./cve_2026_11374_check.py -f targets.txt --json > results.jsonDefault ports differ per product (ADSelfService Plus 8888, ADAudit Plus 8081, M365 Manager Plus
8365, RecoveryManager Plus 8090), so pass a URL or host:port. A bare host defaults to 8888.
| Flag | Description |
|---|---|
targets |
One or more host, host:port, or https://host:port |
-f, --targets-file FILE |
Read targets from a file (one per line; # comments) |
--brief |
Single aligned line per target, good for scanning many hosts |
--json |
Emit structured JSON results |
--timeout SECS |
Per-request timeout (default: 15) |
--no-build |
Skip the extra build-number request on a positive finding |
--no-color |
Disable coloured output (also honours NO_COLOR and non-TTY) |
An AD360-integrated console (verbose, the default). The second line spells out that this is the precondition and not a vulnerable verdict; the third is the best-effort build hint:
$ ./cve_2026_11374_check.py https://adssp.example.com:8888
[!] https://adssp.example.com:8888: POTENTIALLY_AFFECTED
ADSelfService Plus: AD360-integrated, CustomSSO replay path active - precondition met. Not confirmed vulnerable; verify patch level (fixed build 6529).
build: 6519 (below fixed 6529 - inconclusive: ?build= may be a stale placeholder or a hotfix)A standalone install of the same product, where the replay path isn't active:
$ ./cve_2026_11374_check.py https://adssp.example.com:8888
[+] https://adssp.example.com:8888: UNAFFECTED
ADSelfService Plus: standalone / not AD360-integrated (no cleanup), so the replay path isn't reachable here. Verify build >= 6529 regardless.Scanning a list with one aligned line per host (--brief). Exit status is 1 if any host is
POTENTIALLY_AFFECTED, otherwise 0, which is handy in scripts. The trailing note shows the
identified product, plus the build number on a finding:
$ ./cve_2026_11374_check.py -f targets.txt --brief; echo "exit: $?"
POTENTIALLY_AFFECTED https://host-a:8888 ADSelfService Plus 6519
POTENTIALLY_AFFECTED http://host-b:8081 ADAudit Plus 8530
UNAFFECTED http://host-c:8365 M365 Manager Plus
UNAFFECTED https://host-d:443
INCONCLUSIVE http://host-e:8888 ADSelfService Plus
ERROR host-f:8888 timeout
exit: 1Machine-readable output (--json). Each result carries the verdict, the state and detail
behind it, the identified product, and — on a finding — a build object: build is the number
found, fixed_build the threshold for that product, patch_hint the directional call
(likely_patched at or above the fixed build, otherwise inconclusive), and note a short
explanation:
$ ./cve_2026_11374_check.py https://host-b:8081 --json
[
{
"target": "https://host-b:8081",
"state": "potentially_affected",
"detail": "ADAudit Plus: AD360-integrated, CustomSSO replay path active - precondition met ...",
"product": "ADAudit Plus",
"build": { "build": "8530", "fixed_build": "8703", "patch_hint": "inconclusive", "note": "below fixed 8703 - inconclusive: may be a hotfix" },
"verdict": "POTENTIALLY_AFFECTED"
}
]| Verdict | Meaning |
|---|---|
POTENTIALLY_AFFECTED |
AD360-integrated in-scope product; the CustomSSO cookie-replay path is active, so the CVE-2026-11374 exploit precondition is met. This is not a confirmed-vulnerable verdict; verify the patch level (see next steps). |
UNAFFECTED |
The replay path is not reachable. Any of: a standalone in-scope product (not AD360-integrated — though it would become reachable if integrated later, so verify the build anyway); a ManageEngine ADS-framework product that isn't one of the four in scope; or no in-scope product on the response at all (not one of the four CVE-2026-11374 products, or a proxy that strips the session cookie). |
INCONCLUSIVE |
An HTTP 400 — likely the IAM rate-limit lock masking the signal; retry after ~60 s. |
ERROR |
Connection/timeout/TLS failure. |
| Code | Meaning |
|---|---|
0 |
No target returned POTENTIALLY_AFFECTED |
1 |
At least one target is POTENTIALLY_AFFECTED |
2 |
Usage error (bad arguments / unreadable targets file) |
Send an invalid ticket plus an app tag to any CustomSSO URL (*.do):
GET /AppsHome.do
Cookie: CUSTOM_SSO_TICKET=1700000000000; CUSTOM_SSO_APP_TAG_NAME=AD360
On an AD360-integrated install, the chain ADSFilter → CustomSSOFilter → CookieSSOImpl runs its
resolution branch (the tag names a different product, so appName != prodName), fails to resolve
the bogus ticket, and emits cookie-cleanup response headers:
Set-Cookie: CUSTOM_SSO_TICKET=removed; Max-Age=0; ...
Set-Cookie: CUSTOM_SSO_APP_NAME=removed; Max-Age=0; ...
Set-Cookie: CUSTOM_SSO_APP_TAG_NAME=removed; Max-Age=0; ...
A standalone install never runs that path and emits no cleanup. The product is identified from
its fixed per-product Tomcat session cookie (JSESSIONIDADSSP for ADSelfService Plus, and so
on), which is set on every response. The check keys on the headers rather than the status code,
since an integrated ADSSP returns a 302 while ADAudit, M365, and RMP return 200 to the same
probe.
| Probe response | Verdict |
|---|---|
| cleanup headers and a known in-scope product cookie | POTENTIALLY_AFFECTED |
| product cookie, no cleanup, status ≠ 400 | UNAFFECTED (standalone / not integrated) |
| no in-scope product cookie, status ≠ 400 | UNAFFECTED (not one of the four products) |
| cleanup headers but an unrecognized product cookie | UNAFFECTED (ManageEngine, not in scope) |
HTTP 400 |
INCONCLUSIVE (likely the IAM rate-limit lock; see Limitations) |
POTENTIALLY_AFFECTEDis not "confirmed vulnerable." It is the exploit precondition. Because the patch did not touch this code path, a patched build responds identically, so you have to confirm the patch level out of band (see next steps). That is the ceiling on what an unauthenticated observer can know.- No active confirmation. The tool never tries to resolve a real, current-time ticket. That technique can positively prove a host vulnerable, but it is active exploitation (it hijacks a live session), it is throttle-limited and probabilistic, and it needs an active victim session, so it is excluded from this detector.
HTTP 400can be a rate-limit lock rather than a real answer. Zoho IAM applies a URL rolling throttle that caps a source IP at about 40 requests per 60 s, then locks that path for ~60 s. During a lock, requests returnHTTP 400and the integration signal is masked. The tool reports these asINCONCLUSIVE; retry from that IP after about a minute.- Behind a reverse proxy, a console that strips or rewrites the session cookie can be misreported
as
UNAFFECTED. Confirm directly if you expect a ManageEngine product there. - Build numbers are hints, not verdicts. ADAudit (
?v=) and M365 (?bN=) always expose the real build; ADSSP's?build=is sometimes real and sometimes a frozen placeholder that reads below the fixed build, so a below-fixed ADSSP value is ambiguous; RMP exposes none. In every case a build at or above the fixed build strongly indicates patched, and below it is inconclusive.
If a target comes back POTENTIALLY_AFFECTED, treat it as exposed and worth confirming, then:
-
Confirm the patch level directly (the tool can't). Read
conf/product.conf(build_number) on the host and compare it against the fixed build for that product (ADSelfService Plus 6529, RecoveryManager Plus 6321, M365 Manager Plus 4817, ADAudit Plus 8703). Interpret it directionally: a build at or above the fixed build is a strong indicator the host is patched, since the UUID fix is in that tree. A build below the fixed build is inconclusive rather than proof of vulnerability, because out-of-band hotfixes and backports can apply the UUID fix without bumping the build number; in that case fall back to the ticket-format check below. -
Check the minted ticket format if you can log in. The
CUSTOM_SSO_TICKETcookie isn'tHttpOnly, so after authenticating you can read its value: a 13-digit number means vulnerable, a UUID means patched. This is the clearest positive check. -
Update to the fixed build (or later) on any host that is vulnerable or that you can't confirm is patched. The fix replaces the predictable ticket with a random UUID.
-
Hunt for exploitation attempts. On the host,
serverOut_<date>.txtrecords the throttle as bursts of:IAMSecurityException ErrorCode: URL_ROLLING_THROTTLES_LIMIT_EXCEEDED, RequestURI: "/showLogin.cc", RemoteAddr: <source IP>A spike of
URL_ROLLING_THROTTLES_LIMIT_EXCEEDEDon SSO/login dispatch paths from a single source IP is a strong indicator of ticket brute forcing. Also review recent successful logins and session activity for signs of takeover. -
Reduce exposure in the meantime. Restrict network access to the AD360 suite (it shouldn't be internet-facing), and if the replay path isn't needed, disabling the CustomSSO integration removes the precondition entirely.
Update the affected product to at least the fixed build (ADSelfService Plus 6529, RecoveryManager
Plus 6321, M365 Manager Plus 4817, ADAudit Plus 8703) or later. The fix replaces the predictable
System.currentTimeMillis() SSO ticket with UUID.randomUUID().
This code is distributed under an MIT license.
Usage of this tool for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state, and federal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program.
- NVD: CVE-2026-11374
- ManageEngine service packs: ADSelfService Plus · RecoveryManager Plus · M365 Manager Plus · ADAudit Plus
- Bishop Fox Blog: Full Technical Analysis