Hacktricks-skills soap-threadlocal-auth-bypass
How to identify and exploit SOAP/JAX-WS ThreadLocal authentication bypass vulnerabilities in Java web services. Use this skill whenever the user mentions SOAP endpoints, JAX-WS handlers, authentication bypass, ThreadLocal, WebLogic, JBoss, GlassFish, or any SOAP-based Java web service security testing. Also trigger when users are investigating SOAP header-based authentication, middleware authentication caching, or Java EE security handler chains. This skill covers reconnaissance, exploitation, and validation of ThreadLocal-based authentication bypass attacks.
git clone https://github.com/abelrguezr/hacktricks-skills
skills/pentesting-web/soap-jax-ws-threadlocal-auth-bypass/SKILL.MDSOAP/JAX-WS ThreadLocal Authentication Bypass
This skill helps you identify and exploit authentication bypass vulnerabilities in SOAP-based Java web services where handlers cache authenticated subjects in
ThreadLocal variables without clearing them when authentication headers are missing.
Understanding the Vulnerability
The Root Cause
Some Java middleware handlers store the authenticated
Subject/Principal in a static ThreadLocal and only refresh it when a proprietary SOAP header arrives. Because application servers like WebLogic, JBoss, and GlassFish recycle worker threads, dropping that header causes the last privileged Subject processed by the thread to be silently reused.
Why this happens:
public boolean handleMessage(SOAPMessageContext ctx) { if (!outbound) { SOAPHeader hdr = ctx.getMessage().getSOAPPart().getEnvelope().getHeader(); SOAPHeaderElement e = findHeader(hdr, subjectName); if (e != null) { SubjectHolder.setSubject(unmarshal(e)); } // BUG: Subject is never cleared when header is missing! } return true; }
The handler only sets the
Subject when the custom header is present, but never clears it when absent. This means the previous request's context survives on reused threads.
The Attack Pattern
- Trigger authenticated context - Send requests with valid authentication headers to populate the
cache on worker threadsThreadLocal - Flood with header-less requests - Send well-formed SOAP bodies without the authentication header
- Catch the reuse - When a header-less request lands on a thread that previously executed a privileged action, it inherits that
Subject - Execute privileged operations - Access protected endpoints like user managers, credential managers, or admin functions
Reconnaissance
Step 1: Map SOAP Endpoints
Start by enumerating the target to locate SOAP endpoints:
- Check reverse proxy and routing rules for hidden SOAP trees that may block
but accept POSTs?wsdl - Look for common SOAP paths:
,/services/*
,/jaxws/*
,/soap/*/webservice/* - Map these alongside your overall web methodology
Step 2: Analyze Application Artifacts
If you have access to EAR/WAR/EJB artifacts:
# Unpack and inspect unzip *.ear # Look for: # - application.xml (service definitions) # - web.xml (handler chain configurations) # - @WebService annotations in Java sources # - LoginHandlerChain.xml or similar handler configs
Key things to find:
- Handler class names (e.g.,
,LoginHandler
)AuthHandler - SOAP header QNames (e.g.,
,mySubjectHeader
)authToken - Backing EJB names (e.g.,
,UserManager
)CredentialManager
Step 3: Recover WSDL
If metadata is missing:
- Brute-force likely
pathsServiceName?wsdl - Temporarily relax lab proxies to capture WSDL
- Import recovered WSDL into tools like Burp Suite Wsdler to generate baseline envelopes
Step 4: Review Handler Sources
Look for
ThreadLocal patterns in handler code:
- Search for
or similar patternsSubjectHolder.setSubject() - Check if the
is ever cleared when authentication headers are missing or malformedThreadLocal - Look for static
declarations that persist across requestsThreadLocal
Exploitation
Phase 1: Establish Baseline
Send a valid request with the proprietary authentication header to learn:
- Normal response codes
- Error messages for invalid tokens
- Expected SOAP envelope structure
- Required namespaces and formatting
Phase 2: Craft Header-Less Requests
Resend the same SOAP body while omitting the authentication header:
- Keep the XML well-formed
- Respect all required namespaces
- Ensure the handler exits cleanly (don't trigger parsing errors)
Example request:
POST /ac-iasp-backend-jaxws/UserManager HTTP/1.1 Host: target Content-Type: text/xml;charset=UTF-8 <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:jax="http://jaxws.user.frontend.iasp.service.actividentity.com"> <soapenv:Header/> <soapenv:Body> <jax:findUserIds> <arg0></arg0> <arg1>spl*</arg1> </jax:findUserIds> </soapenv:Body> </soapenv:Envelope>
Phase 3: Flood for Thread Reuse
Loop the header-less request with high parallelism:
- Use multiple concurrent connections
- Reuse Keep-Alive connections to maximize thread reuse probability
- Target large worker pools
- Monitor responses for privileged data or success indicators
Why parallelism matters: The more concurrent requests you send, the higher the probability that some will land on threads that previously executed privileged actions.
Phase 4: Execute Privileged Operations
Once you observe privileged responses, escalate:
- Access user management endpoints
- Create administrator accounts
- Import credentials
- Access credential managers
- Perform any operation that requires elevated privileges
Real-World Case Study: HID ActivID/IASP (HID-PSA-2025-002)
Vulnerability Details
Synacktiv discovered that JAX-WS
LoginHandler in ActivID 8.6–8.7 sets SubjectHolder.subject when a mySubjectHeader SOAP header is present or when console/SSP traffic authenticates, but never clears it when the header is absent.
Exploitation Pattern
-
Trigger authenticated context on many threads:
- Spam
endpoint/ssp - Log into
as admin in another browser tab/aiconsole - Generate authenticated traffic to populate the thread pool
- Spam
-
Flood header-less SOAP bodies:
- Target
or other EJB-backed JAX-WS endpoints/ac-iasp-backend-jaxws/UserManager - Use high parallelism
- Each hit that reuses an "infected" thread executes with elevated
Subject
- Target
-
Repeat until privileged responses:
- Reuse Keep-Alive connections
- Maximize thread reuse probability
- Monitor for admin-level responses
Handler Flow
→LoginHandlerChain.xml
unmarshalsLoginHandler.handleMessage()
and stores themySubjectHeader
inSubject
(a staticSubjectHolder
)ThreadLocal
later injectsProcessManager.triggerProcess()
into business processesSubjectHolder.getSubject()- Missing headers leave stale identities intact
In-Field PoC
The advisory demonstrates a two-step SOAP abuse:
- First
to leak informationgetUsers - Then
+createUser
to plant a rogue admin when the privileged thread hitsimportCredential
Validating the Vulnerability
Development/Debug Environment
Attach JDWP to watch the
ThreadLocal contents:
# Start with JDWP -agentlib:jdwp=transport=dt_socket,server=y,address=5005,suspend=n # Monitor ThreadLocal before and after each call # Confirm that unauthenticated requests inherit prior administrator Subject
Production Environment
Use JFR (Java Flight Recorder) or BTrace to dump
SubjectHolder.getSubject() per request:
- Verify header-less reuse is occurring
- Confirm the
persists across requestsSubject - Document the thread reuse pattern
Common Indicators
Look for these signs that a SOAP endpoint may be vulnerable:
- SOAP handlers that only set authentication context when headers are present
- Static
variables in handler chainsThreadLocal - No explicit clearing of authentication state
- Application servers with thread pooling (WebLogic, JBoss, GlassFish)
- Custom authentication headers that are optional or can be omitted
- SOAP endpoints that accept requests without authentication headers
Mitigation Recommendations
If you're securing a vulnerable system:
- Always clear ThreadLocal on request completion - Use
blocks or interceptor cleanupfinally - Validate authentication on every request - Don't rely on cached state
- Use request-scoped authentication - Bind authentication to the request lifecycle
- Clear ThreadLocal in handler chains - Explicitly reset state when headers are missing
- Implement proper thread-local cleanup - Use
in finally blocksThreadLocal.remove()