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)的认证机制。
- 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连接。
数据包如下:
包含了服务器的挑战。
响应
尝试计算一下:
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 进行连接。
数据包如下:
包含了服务器的挑战。
响应
这个稍微复杂点:
- 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的内容就是了)。
尝试计算一下:
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