NTLM理解

NTLM机制理解

Posted by hmoytx on May 19, 2020

NTLM理解

简介

Windows体系下两种认证机制,NTLM与Kerberos。这里主要对NTLM认证机制进行一下分析,加深理解。

LM Hash

在NTLM协议之前,使用的是LM(LAN Manager)协议。
LM与NTLM协议的认证机制是相同的,但是加密方式有所不同。
先来看加密计算,LM Hash的计算方法如下:

  • 1.将用户口令明文转换为大写,并转换为16进制
  • 2.口令补 0x00或截断为14位,并且分作前后2个部分,各7字节
  • 3.分别转化为比特流不足56比特则在左边加0,然后分成8组,每7比特后补一个0,变成64比特的DES密钥
  • 4.将上面的2个Key,使用DES算法,分别加密固定字符串(KGS!@#$%),得到2个8字节的密文
  • 5.2个8字节的密文连成1个16字节的密文,称为LM Hash

模拟下这个加密过程。

import sys
import binascii
from pyDes import *

def Encrypt(str, Des_Key):
    k = des(Des_Key, ECB, pad=None)
    EncryptStr = k.encrypt(str)
    return binascii.b2a_hex(EncryptStr)


def padding(str):
    b = []
    l = len(str)
    num = 0
    for n in range(l):
        if (num < 8) and n % 7 == 0:
            b.append(str[n:n + 7] + '0')
            num = num + 1
    return ''.join(b)


def processing(p1, p2):
     # 每部分转换成比特流,并且长度位56bit,长度不足使用0在左边补齐长度
    p1 = bin(int(p1, 16)).lstrip('0b').rjust(56, '0')
    p2 = bin(int(p2, 16)).lstrip('0b').rjust(56, '0')

    # 再分7bit为一组末尾加0,组成新的编码
    p1 = padding(p1)
    p2 = padding(p2)
    p1 = hex(int(p1, 2))
    p2 = hex(int(p2, 2))
    p1 = p1[2:].rstrip('L')
    p2 = p2[2:].rstrip('L')
    #print(part_1+"   "+part_2)
    if '0' == p2:
        p2 = "0000000000000000"
    p1 = binascii.a2b_hex(p1)
    p2 = binascii.a2b_hex(p2)
    return p1, p2


if __name__ == "__main__":

    Password = "Abcd123"
    Password = Password.upper().encode().hex()
    # 转换为大写 16进制
    str_len = len(Password)

    # 密码不足14字节将会用0来补全
    if str_len < 28:
        Password = Password.ljust(28, '0')
    # 14字节分成两个7字节部分
    part_1 = Password[0:int(len(Password) / 2)]
    part_2 = Password[int(len(Password) / 2):]

    part_1, part_2= processing(part_1, part_2)
    
    #  key为"KGS!@#$%" des加密。
    LM_1 = Encrypt("KGS!@#$%", part_1)
    LM_2 = Encrypt("KGS!@#$%", part_2)

    # 拼接得到最终LM HASH值。
    LM = LM_1 + LM_2
    print(LM)

最后得到结果为:’6f87cd328120cc55aad3b435b51404ee’

LM-Hash存在的问题:

  • 1.加密的key公开,DES强度不够。
  • 2.不区分大小写。
  • 3.可以通过LM Hash后8字节是否为AAD3B435B51404EE来判断密码长度是否大于7。

挑战/响应机制

认证协议是基于挑战(challenge)/响应(Response)的认证机制。
200518_1

  • 1.客户端向服务器端发送请求,请求内容中包含了用户信息
  • 2.服务器接受到请求,经过一系列步骤生成一个8字节的随机数(挑战),发送给客户端
  • 3.客户端接受到挑战后,使用将要登录到账户对应的LM Hash使用挑战加密生成Response,然后将Response发送至服务器端

LM的respons计算:

  • 1.LM Hash后补充5字节0至21字节
  • 2.分成3组各7字节,转为比特流,每7比特后补0,转换成3组8字节字符,作为密钥
  • 3.使用上述3个密钥,分别对8字节的挑战进行DES获得三组8字节密文组成24字节的密文,称为响应,发送给服务端
    没有老环境,没有办法得到challenge,不做演示。

NTLM Hash

NT Hash计算过程相比LM Hash来得简单。

  • 1.转换为16进制
  • 2.Unicode编码
  • 3.MD4加密的16进制就是NT hash
import sys
from Crypto.Hash import MD4

password = "admin"

password = password.encode('utf-16le')


m = MD4.new()
m.update(password)
nt_hash = m.hexdigest()

print("nt hash: ", nt_hash)

结果为:’nt hash: 209c6174da490caeb422f3fa5a7ae634’
使用mimikatz抓到的hash为NT Hash + LM Hash,统一当成NTLM Hash。
有时候还存在AAD3B435B51404EEAAD3B435B51404EE这种样子的Hash,眼熟么?可以对比上面的LM Hash,密码小于7位时的情况。这就是空口令。

响应算法:

跟LM的响应计算一样。

  • 1.NT Hash后补充5字节0至21字节
  • 2.分成3组各7字节,转为比特流,每7比特后补0,转换成3组8字节字符,作为密钥
  • 3.使用上述3个密钥,分别对8字节的挑战进行DES获得三组8字节密文组成24字节的密文,作为响应,发送给服务端

这里将安全选项中的LAN管理器身份验证级别设为仅发送NTLM响应。
尝试net use连接。
数据包如下:
包含了服务器的挑战。 200518_2
响应
200518_3

尝试计算一下:

import sys
from pyDes import *


nt_hash = "32ed87bdb5fdc5e9cba88547376818d4"
nt_hash = bytes.fromhex(nt_hash)
print(nt_hash)

server_challenge = "08453c02839f4461"
server_challenge = bytes.fromhex(server_challenge)



def des_encrypt(key, data):
    d = des(key, ECB, pad=None)
    return d.encrypt(data)


def padding(string):
    bin_str = bin(int(string.hex(), 16))[2:].rjust(len(string)*8, '0')
    print(bin_str)
    bin_str = [bin_str[i:i+7] for i in range(0, len(bin_str), 7)]
    result = "".join([i.ljust(8, '0') for i in bin_str])
    result = bytes.fromhex(hex(int(result, 2))[2:].rjust(16, '0').strip("L"))
    return result




key = nt_hash + b"\x00" * 5

k_1 = padding(key[:7])
k_2 = padding(key[7:14])
k_3 = padding(key[14:])


data = server_challenge    

response = des_encrypt(k_1, data) + des_encrypt(k_2, data) + des_encrypt(k_3, data)

print(response.hex())


计算得到的结果:dcc5c4d03fa9d3bb76ff1d300995a23efc79d62fc637e7db。离谱居然和wireshark中的结果是不一样的。

查资料以后发现,这里有一个坑,响应算法还有一种。

  • 1.NT Hash后补充5字节0至21字节
  • 2.分成3组各7字节,转为比特流,每7比特后补0,转换成3组8字节字符,作为密钥
  • 3.拼接8字节Server Challenge和8字节Client Challenge后,取MD5的前8字节
  • 4.使用3组8字节字符,分别对MD5前8字节数据进行DES加密获得三组8字节密文,共组成24字节的密文,作为响应,发送给服务端

稍稍修改下代码即可,将data = server_challenge 进行修改,这里只摘出部分代码。

........
import hashlib 
.......

client_challenge = "47a3c60e191b425e"
client_challenge = bytes.fromhex(client_challenge)

.......

client_challenge = "47a3c60e191b425e"
client_challenge = bytes.fromhex(client_challenge)

.......


这一次再进行计算,得到正确的结果:a900e376727b6f5293008a3f9e58474e09da5552bf39e93d。

Net-NTLMv2响应计算

同样的使用net use 进行连接。
数据包如下: 包含了服务器的挑战。 200518_4
响应
200518_5

这个稍微复杂点:

  • 1.用户名和计算机名转为16进制后小端序,以NT Hash为密钥进行hmac_md5加密,得到字符串1
  • 2.拼接server challenge和client challenge,将上述的字符串1作为密钥,进行hmac_md5加密,拼接client challenge,得到LM响应
  • 3.拼接server challenge和特定字符串,将字符串1作为密钥,进行hmac_md5加密,拼接上特定字符串,得到NT响应。

其中特定字符串是指响应中,包含了计算机信息,时间等信息的一个16进制字符串,体现在wireshark中就是NTProofStr后的全部内容(把整个response复制下来,去掉前面NTProofStr的内容就是了)。
200518_6

尝试计算一下:


import hmac




def hmacmd5(key, data):
    h = hmac.new(key)
    h.update(data)
    return h.digest()




nt_hash = "32ed87bdb5fdc5e9cba88547376818d4"
nt_hash = bytes.fromhex(nt_hash)

server_challenge = "26699c8beb2b77e6"
server_challenge = bytes.fromhex(server_challenge)

client_challenge = "bdef5fa409a74894"
client_challenge = bytes.fromhex(client_challenge)

"""
原始响应:0ad0d84dea028a598e0bb6f0fb4d841b01010000000000000bb9ac74ee2cd601bdef5fa409a748940000000002001e004c004100500054004f0050002d0045004f0051004e00350043005000410001001e004c004100500054004f0050002d0045004f0051004e00350043005000410004001e004c004100500054004f0050002d0045004f0051004e00350043005000410003001e004c004100500054004f0050002d0045004f0051004e003500430050004100070008000bb9ac74ee2cd60106000400020000000800300030000000000000000000000000300000741993b4c84ac89fc18f3097048b82fad981310668083ba46fa789bdc0b24a400a001000000000000000000000000000000000000900240063006900660073002f003100390032002e003100360038002e0031002e00310031003100000000000000000000000000
NTProofStr:0ad0d84dea028a598e0bb6f0fb4d841b
"""
str1 = "01010000000000000bb9ac74ee2cd601bdef5fa409a748940000000002001e004c004100500054004f0050002d0045004f0051004e00350043005000410001001e004c004100500054004f0050002d0045004f0051004e00350043005000410004001e004c004100500054004f0050002d0045004f0051004e00350043005000410003001e004c004100500054004f0050002d0045004f0051004e003500430050004100070008000bb9ac74ee2cd60106000400020000000800300030000000000000000000000000300000741993b4c84ac89fc18f3097048b82fad981310668083ba46fa789bdc0b24a400a001000000000000000000000000000000000000900240063006900660073002f003100390032002e003100360038002e0031002e00310031003100000000000000000000000000"
str1 = bytes.fromhex(str1)
user = "123"
domain = "mz-PC"


hash1 = hmacmd5(nt_hash, user.upper().encode('utf-16le') + domain.encode('utf-16le'))

ntv2 = hmacmd5(hash1, server_challenge + str1) + str1

print(ntv2.hex())

得到计算结果为:

0ad0d84dea028a598e0bb6f0fb4d841b01010000000000000bb9ac74ee2cd601bdef5fa409a748940000000002001e004c004100500054004f0050002d0045004f0051004e00350043005000410001001e004c004100500054004f0050002d0045004f0051004e00350043005000410004001e004c004100500054004f0050002d0045004f0051004e00350043005000410003001e004c004100500054004f0050002d0045004f0051004e003500430050004100070008000bb9ac74ee2cd60106000400020000000800300030000000000000000000000000300000741993b4c84ac89fc18f3097048b82fad981310668083ba46fa789bdc0b24a400a001000000000000000000000000000000000000900240063006900660073002f003100390032002e003100360038002e0031002e00310031003100000000000000000000000000

参考

https://payloads.online/archivers/2018-11-30/1
http://d1iv3.me/2018/12/08/LM-Hash%E3%80%81NTLM-Hash%E3%80%81Net-NTLMv1%E3%80%81Net-NTLMv2%E8%AF%A6%E8%A7%A3/
https://xz.aliyun.com/t/1943