TL;DR
CVE-2026-11374 is a critical, unauthenticated account takeover that hands an attacker any session, administrators included, on ManageEngine’s Active Directory management platform. Because the SSO ticket is just the millisecond wall-clock time at login, an attacker who predicts that instant can replay it as a cookie and inherit the victim’s session. Blind exploitation is still impractical, though. Several layered controls box the attack into a narrow window, so the realistic threat is a targeted attacker already on the victim’s network, not opportunistic scanning. The bug affects ADSelfService Plus, RecoveryManager Plus, M365 Manager Plus, and ADAudit Plus under AD360, so patch to the latest build. But the patch only goes halfway. It removes the ticket’s predictability while leaving it a non-HttpOnly, long-lived bearer token, so a stolen ticket still works on a patched host, and hardening the SSO cookie yourself is the other half of the fix. Our detection tool is on GitHub.
Summary
ManageEngine published a single advisory for CVE-2026-11374 covering four products, with per-product fixes rolling out between June 3 and June 12, 2026. The vendor scores it CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H, which computes to 9.0 (Critical). (NVD has not published a CVSS vector at the time of writing, so we use the vendor’s here.) The high attack complexity in that vector is the same story this post tells at length: the takeover is real, but landing it blind is hard. The vendor describes it as an SSO ticket that “could be predicted by an unauthenticated attacker, leading to account takeover” when the product is integrated under AD360. Credit for the original report goes to 0xmanhnv through the Zoho BugBounty program.
Bishop Fox analyzed the patch by diffing the last vulnerable build of each affected product against the fix, confirmed the root cause in source, traced the runtime ticket-issuance and replay path, and validated an end-to-end unauthenticated administrator takeover against an instrumented lab install (ADSelfService Plus 6528, AD360-integrated). We then built a detection method that runs safely against production deployments. This post takes a deliberately defender-first angle: the validated impact is severe where the conditions line up, but those conditions are narrow, and the realistic threat model is a targeted attacker with a specific vantage point rather than opportunistic mass exploitation.
One point on scope: this is a single CVE that spans four products, not a bundle of siblings. The vulnerable code lives in shared and per-product ManageEngine components that each make the same mistake, so everything below applies to all four. We did the bulk of our hands-on validation against ADSelfService Plus and confirmed the identical root cause in the other three from extracted installs.
Our detection tool is available at https://github.com/BishopFox/CVE-2026-11374-check.
Defender Action Items
- Patch. Update to the fixed build for each product you run: ADSelfService Plus 6529, RecoveryManager Plus 6321, M365 Manager Plus 4817, or ADAudit Plus 8703 (or later). The fix replaces the predictable ticket with a random UUID and is the real remediation.
- Audit your AD360 integration. The vulnerable cookie-replay path is active only when the product is integrated under AD360. A standalone install does not expose it. If you do not run these products under AD360, you are not exposed to this bug today, but you should still patch, because integrating later turns the path on.
- Shrink the blast radius of a stolen ticket. Patching removes ticket prediction, but a stolen ticket can still be replayed, so tighten the controls you own. Shorten the session timeout so a ticket tied to an active session stops resolving sooner. If you front ManageEngine with a reverse proxy, scope its trust tightly, since a trusted proxy effectively bypasses client IP binding.
- Watch for the brute-force signature. A blind ticket search hammers the SSO and login dispatch paths and trips the built-in request throttle. Bursts of
URL_ROLLING_THROTTLES_LIMIT_EXCEEDEDagainst/showLogin.ccor SSO endpoints inserverOut_<date>.txt, from a single source IP, are a high-signal indicator of an in-progress attack. - Sweep your fleet. Run our detection tool against every reachable instance, including from inside the network, to find which ones have the AD360 cookie-replay path active, then confirm their patch levels.
Background
ADSelfService Plus, RecoveryManager Plus, M365 Manager Plus, and ADAudit Plus are ManageEngine’s Active Directory and Microsoft 365 management tools: password self-service, backup and recovery, M365 reporting, and AD change auditing respectively. AD360 is the umbrella product that ties them together with a shared console and single sign-on, so that an operator who logs into one can jump to the others without re-authenticating. That cross-product SSO jump is the feature this bug lives in.
When a user signs into one integrated product and moves to another, the source product mints an SSO ticket: a short-lived secret that the destination resolves back to the user’s identity and roles. The ticket travels in a cookie named CUSTOM_SSO_TICKET, and the destination caches the user’s identity server-side, keyed by the ticket string. The ticket is the only secret binding that cookie to an authenticated identity, so its unpredictability is load-bearing.
We worked from the vendor advisory and a binary diff of each product’s last vulnerable build against its fix. The diff was small and pointed: the SSO ticket generator changed, and nothing else in the authentication path did. That made the root cause easy to confirm once we knew where to look.
The Vulnerability: An SSO Ticket That’s Just a Timestamp
Each product wires up its own SSO ticket generator, and in the affected builds that generator builds the ticket from the clock:
public synchronized Ticket generateTicket(HttpServletRequest request, Credential cred) {
Long time = Long.valueOf(System.currentTimeMillis());
String ticketId = time.toString(); // the SSO ticket IS the millisecond timestamp
...
ticketObj.ticket = ticketId;
}
The ticket is System.currentTimeMillis() at the moment of login, rendered as a 13-digit decimal string. That string is written verbatim into the CUSTOM_SSO_TICKET cookie with no hashing or encoding, and the server caches the resolved Ticket object (the user’s ID, login name, roles, domain, and source IP) under that exact string as the key. So the cookie value an attacker needs to forge is not derived from the timestamp. It is the timestamp.
The framework’s own base generator already used a random UUID. Only the per-product subclasses reached for the clock, and the patch simply brings them in line:
public synchronized Ticket generateTicket(HttpServletRequest request, Credential cred) {
String ticketId = UUID.randomUUID().toString(); // 122 bits of SecureRandom entropy
...
ticketObj.ticket = ticketId;
}Reaching the Takeover: Two-Stage Cookie Replay
With a candidate ticket in hand, the attack is a cookie replay in two stages:
- The attacker sends a GET to any protected
*.docarrying two cookies:CUSTOM_SSO_TICKET=<candidate>andCUSTOM_SSO_APP_TAG_NAMEnaming a product other than the one being hit. That mismatched tag matters because the cookie-SSO handler (CookieSSOImpl) only enters its resolution branch when the tagged app differs from the one it is running as, so a tag naming the same product is ignored. In that branch it callsTicketHandler.getTicket(), which looks the candidate up in the local ticket cache (the same cache the victim's login populated), then checks that the ticket is valid and that the request's source address matches the IP it was issued to. This validates the ticket but does not log the attacker in. On success, the handler stashes the resolved identity in a new server-side session and, rather than serving the protected page, returns an auto-submitting login form (/adsf/jsp/common/LoginFormSubmit.jsp) pre-filled with the victim's username and the raw ticket as the password. - The browser POSTs that form to
/j_security_check, the servlet container's authentication endpoint and the only place a login principal is established. TheAUTHRULE_NAMEfield routes the login to the cookie-SSO authenticator (CustomSSOAuthHandler), which reads the identity the GET stashed in the server-side session and logs the attacker in to the new application as that user. Now the session is authenticated as the victim, and a follow-up request to a protected page returns 200 rather than the 302 an unauthenticated session would get.
We validated this takeover end to end in our lab, replaying a cache-resident admin ticket from an unauthenticated context established a full administrator session on the AD360 console.
It is worth pointing out that both stages depend on a client IP check performed by the ticket resolver, and how strong that guard is depends on the deployment. By default, getClientIP() uses the raw socket address, and an X-Forwarded-For header from an untrusted source nulls the comparison rather than overriding it, so spoofing the header from an arbitrary host fails outright. Configured trust can weaken it, however: if the connecting address is listed in the server's TRUSTED_IPS setting, the handler takes the client IP from X-Forwarded-For instead, letting anyone who can reach the product from a trusted address assert the victim's IP without originating from it. A request from a recognized reverse proxy skips the comparison entirely. TRUSTED_IPS is empty by default, so a direct install enforces the guard, while a loosely configured proxy tier reduces it to a formality.
There is also a registered REST resolver, GET_SSO_TICKET_DETAILS, that turns a ticket into identity directly. It would be a clean oracle if an attacker could reach it, but they cannot: it is gated by a server-to-server handshake key, which we detail in the next section. The reachable attack is cookie replay against a protected page, not a call to the resolver.
Why Brute Force is Impractical
A millisecond timestamp sounds trivially guessable, and on paper the keyspace is tiny. The catch is that the keyspace is not the binding constraint. The obvious fast path, reading identity straight out of the resolver, is closed off. That forces the attacker onto a slower cookie-replay search, and three further properties of the system, none of which the attacker can remove, make even that small search space a slow and noisy grind that only works from one place.
The Fast Oracle is Closed
The cleanest way to weaponize a predictable ticket would be to call the GET_SSO_TICKET_DETAILS resolver directly, once per candidate, and read identity straight back. That endpoint is registered as requiring a server-to-server handshake, and its filter rejects any external call, right or wrong ticket alike, before the resolver runs unless the request carries a valid handshake key. That key is a random UUID rotated every 120 seconds, held in memory and exchanged only between trusted server components, so an attacker cannot supply it. With the direct oracle closed, the only reachable attack is cookie replay against a protected page, which is the path the rest of this section grinds against.
The Candidate Space is Small but Can Only Be Searched Slowly
Because the ticket is monotonic wall-clock time, an attacker who knows the login happened within a one-second window has only about 1,000 millisecond candidates to try, and a one-minute window is about 60,000. But every candidate is a request, and a rolling-window throttle caps the achievable rate at roughly 40 requests per 60 seconds, about 0.6 per second:
Window around the login instant | Candidate tickets | Single-IP wall-clock at ~40 req / 60 s |
1 second | ~1,000 | ~25 minutes |
1 minute | ~60,000 | ~25 hours |
That cap is not incidental. Every request runs through ManageEngine’s IAM security filter, which enforces the throttle keyed on the request path plus the real socket IP: more than about 40 requests to a given path from one source in 60 seconds locks that path and IP out for a further 60 seconds, during which every request, valid or not, gets a generic error page. The key is the real TCP source address, not a spoofable forwarded header, so an attacker cannot inflate the budget by varying X-Forwarded-For. And because a hit is only possible from a single IP anyway (the next constraint), the throttle and that IP binding compound: the one address that could produce a hit is the same one whose small request budget is being counted.
So the attacker’s real problem is not the size of the space, it is predicting the login instant tightly enough that the search finishes before the ticket is gone, while paced at well under one request per second.
Only the Victim’s Own IP Yields A Hit
The cached ticket is bound to the client IP recorded at the victim’s login, and resolution enforces that the replay comes from the same IP. On a default install this binding holds: a request from any other source resolves the correct ticket to a login redirect that is indistinguishable from a wrong guess. That has a sharp consequence for brute force. The attacker cannot parallelize the search across many source IPs to go faster, because every IP except the victim’s returns “miss” even for the right answer. The one IP that can produce a hit is the victim’s, and that single IP is exactly the one the throttle rate-limits. Source-IP rotation, the standard way to beat a per-IP rate limit, does not help here at all.
In practice this means the attacker has to be on the victim’s IP: a shared NAT egress the victim sits behind, or an internal foothold on the victim’s network. A remote attacker with no such vantage cannot detect a hit even with the correct ticket. (A deployment that trusts a reverse proxy and derives the client IP from a forwarded header widens this, which is why we flag proxy-trust configuration in the defender checklist.)
The Cache Window Closes
A ticket is only resolvable while it is cache-resident, and the cache holds it from the victim’s login until logout or a JVM restart. An attacker without a foothold cannot see when a victim logs in, so blind searching means guessing both the right instant and a moment when a usable ticket happens to be sitting in cache.
Put together, blind brute force against an internet-facing target is not a realistic mass-exploitation technique. It requires sourcing requests from the victim’s network, predicting the login instant tightly, completing the search at sub-one-request-per-second through a throttle that locks you out for 60 seconds every time you push too hard, and doing all of it while a live ticket happens to be cached. It also leaves an obvious trail (see the throttle signature in the defender checklist).
The Patch: Replacing the Timestamp With a Random uuid
Credit where it is due. Long before this patch shipped, ManageEngine’s layered controls kept the underlying weakness from ever being easy to exploit. On its own, a predictable-timestamp ticket looks like a one-request-per-guess remote oracle, but three independent controls stood in the way. The server-to-server handshake closed off the direct resolver so it could not be used as an oracle. The source-IP binding meant a correct guess only registered from the victim’s own network. And the request throttle held the blind-search rate down to a crawl while logging every attempt. What makes this such a clean example of defense in depth is that the controls were not all built for this threat. The throttle in particular is a general-purpose rate limit, not a defense aimed at SSO ticket abuse, yet it is what turned a would-be trivial oracle into the slow, noisy grind we described earlier. Independent controls compound, and some of the ones that mattered most here were never designed with this attack in mind. Together they are the reason the bug was never mass-exploitable, and they bought defenders time.
The patch then closed the weakness at its root. Ticket generation now uses UUID.randomUUID() backed by SecureRandom, so a ticket carries 122 bits of entropy rather than a readable clock value. There is no remaining timestamp path in the program code, so a patched install never mints a guessable ticket. That removes the single property that made the ticket dangerous from the outside: predictability. Prediction was the only way to obtain a valid ticket with no prior access to the target, so closing it eliminates the vector the CVE actually describes, an unauthenticated attacker forging a session by guessing the clock. That path no longer exists.
A more thorough design would go further. The ticket is still a bearer token, an opaque secret that grants a session to whomever presents it. Carrying a signed assertion bound to its holder instead, one the server verifies cryptographically and that is useless to anyone but the client it was issued to, would make possession alone insufficient and would neutralize the one path the patch leaves open. As it stands, an attacker who obtains a live ticket some other way can still replay it, which we confirmed in the lab against a patched host, but that is a much narrower threat than the CVE. It requires the attacker to already have a foothold such as network interception or script execution in the victim's browser, and even then the source-IP binding still applies. Moving to signed, bound assertions is a hardening improvement, not a correction the vulnerability demands.
So, the patch is sufficient. It removes the remotely exploitable weakness, and the residual theft-and-replay risk sits behind the same layered controls that contained the original bug, now with prediction taken out of the equation. The most valuable thing a defender can do is the obvious thing: apply the fixed build. From there, the hardening steps in the defender takeaways list above extends the same layered defense logic that served so well here.
Safe Detection
Even though exploitability may be low, as a defender you likely still want to know which of your assets are affected by this vulnerability. Fortunately, detection is easy here, because the precondition (AD360 integration) is observable without touching a real ticket.
The probe sends a deliberately invalid ticket and an app tag to any *.do URL:
GET /AppsHome.do Cookie: CUSTOM_SSO_TICKET=1700000000000; CUSTOM_SSO_APP_TAG_NAME=AD360
On an AD360-integrated install, the cookie-SSO handler runs its resolution branch, fails to resolve the bogus ticket (an old, fixed millisecond that can never be cache-resident), and tells the client to clear the SSO cookies:
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; ...
On a standalone install, that branch never runs and no cleanup headers appear. Nothing is exploited: the probe ticket is unresolvable by construction, so no session is ever recovered, and the only thing the server discloses is “clear those cookies.” Our tool keys on the cleanup header plus the product’s session cookie, which also tells you which of the four products answered.
Note that a positive detection only confirms the precondition: the cookie-replay path is reachable and the product is AD360-integrated. It does not, by itself, tell you the build is unpatched, because a patched install emits the identical cleanup (the patch changed ticket generation, not this path). There is no safe passive unauthenticated signal that distinguishes vulnerable from patched, so confirm patch level out of band to judge actual exploitability. Be aware that an out-of-band hotfix can apply the UUID fix without bumping the on-disk build number, so the build string alone is not a reliable verdict. At scale, point the tool at whatever URL reaches the web UI, run it from inside the network as well as outside (the path is reachable wherever the UI is), and seed your target list from the products’ login-page fingerprints.
Conclusion
CVE-2026-11374 is a real, unauthenticated account takeover, and where the conditions line up it reaches all the way to an administrator with no credentials, so the Critical rating is justified and patching is not optional. But those conditions are narrow, held that way by the layered controls described above, so the realistic threat is a targeted attacker who already has a vantage on the victim’s network and can predict the login instant, not an opportunistic scan of the internet.
The patch fixes the part that made this dangerous. It removes the ticket's predictability, the only property that let an attacker forge a session with no prior access, so on a patched install the unauthenticated takeover path is closed. The bearer token handling is unchanged, so a stolen ticket still buys a session, but stealing one takes a foothold the vulnerability itself does not provide, and the same layered controls still stand in the way. Applying the fixed build is the step that matters, but the hardening steps in the defender takeaways list above can help contain the narrower theft-and-replay risk that remains. The vulnerability’s severity rating marks the ceiling; the preconditions and the vendor's layered controls tell you how far below it most deployments actually sit.
As usual, our Cosmos customers were notified about our research into this vulnerability shortly after the vendor advisory. If you are interested in learning more about managed services delivered through our Cosmos platform, visit bishopfox.com/services/continuous-threat-exposure-management.
Our detection tool is available on GitHub: https://github.com/BishopFox/CVE-2026-11374-check.
For more vulnerability intelligence insights, visit the Bishop Fox Blog.
Subscribe to our blog
Be first to learn about latest tools, advisories, and findings.
Thank You! You have been subscribed.
Recommended Posts