Internal network penetration Swiss Army knife - impacket tool analysis (Part 1)
impacket tool analysis of NTLM protocol implementation

impacket is a series of Python implementations of network protocols, including basic network protocols such as IP, TCP, ICMP, and more importantly, it implements a large number of Windows communication protocols, including the ntlm and kerberos protocols used for Windows authentication, the ldap used for storing Active Directory data, and a large number of msrpc protocols. With impacket, developers can construct protocol corresponding data packets through a series of Python classes and methods, or deserialize the original data packets returned by the server into corresponding Python classes. Through Impacket, developers can write Python scripts to build custom network security tools, perform network protocol analysis, penetration testing, vulnerability exploitation, and other tasks. It is widely used in penetration testing, red team operations, and network security research.
The impacket tool analysis series of articles will take the protocol as the breakthrough point, and carry out detailed analysis on different protocol implementations. Due to the large number of protocols implemented in impacket, we will focus on the analysis of commonly used protocols in internal network penetration, such as ntlm, kerberos, ldap, smb, rpc, and so on. Through code interpretation and examples, we will help everyone understand the principle and usage of this tool. As the first article in the series, we will first introduce the implementation of the ntlm protocol.
We use the impacket0.11.0 version as an example. The implementation of ntlm in impacket is located in a file named ntlm.py, which is located in impacket-ntlm.py
01 NTLM Constants
USE_NTLMv2
On lines 35-36, the author first defines two global variables USE_NTLMv2, TEST_CASE to represent the use of NTLMv2 and test cases. NTLMv1 is very rare in most network environments, so here it is set to use NTLMv1 by default.
Auth Level
Lines 55-60 define 6 constants, each representing 6 authentication levels that will be used in dcerpc. However, these parameters are not used in ntlm.
NEGOTIATE FLAGS
Lines 62-191 describe the meaning of each bit in the FLAG field of the ntlm message. NEGOTIATE FLAGS is a field in the ntlm message, with a fixed length of 4 bytes (32bit). Each field and its meaning are as follows:
Identifier Bits | Name | Description |
0x00000001 | Negotiate Unicode | Indicates support for the use of Unicode strings in secure buffer data. |
0x00000002 | Negotiate OEM | Indicates support for the use of OEM strings in secure buffer data. |
0x00000004 | Request Target | Requests that the server include the domain name of the target server in the Type2 message. |
0x00000008 | unknown | Reserved field (not used) |
0x00000010 | Negotiate Sign | Specifies that authenticated communications between the client and server should carry a digital signature (message integrity). |
0x00000020 | Negotiate Seal | Specifies that authenticated communications between the client and server should be encrypted (message confidentiality). |
0x00000040 | Negotiate Datagram Style | Indicates that datagram verification is being used. |
0x00000080 | Negotiate Lan Manager Key | Indicates that the lm Session Key should be applied to sign and seal authenticated communications. |
0x00000100 | Negotiate Netware | Reserved field (not used) |
0x00000200 | Negotiate NTLM | Indicates that NTLM authentication is being used. |
0x00000400 | unknown | Reserved field (not used) |
0x00000800 | Negotiate Anonymous | 由客户端在 Type 3 消息中发送,表明匿名上下文已建立。这也会影响响应字段(如“匿名响应”部分中详述)。 |
Sent by the client in Type 3 messages to indicate that an anonymous context has been established. This also affects the response fields (as detailed in the 'Anonymous Response' section). | 0x00001000 | Negotiate Domain Supplied |
Sent by the client in Type 1 messages to indicate that the message includes the domain name where the client is located, which the server uses to determine if the client is eligible for local authentication. | 0x00002000 | Negotiate Workstation Supplied |
Sent by the client in Type 1 messages to indicate that the message includes the name of the client's workstation, which the server uses to determine if the client is eligible for local authentication. | 0x00004000 | Reserved field (not used) |
Negotiate Local Call | 0x00008000 | Negotiate Always Sign |
Setting this field generates a session key regardless of whether Negotiate Sign or Negotiate Seal is set. | 0x00010000 | Target Type Domain |
Indicates that the type of the Target field in the Type 2 message is the domain name. | 0x00020000 | Target Type Server |
Indicates that the type of the Target field in the Type 2 message is the server name. | 0x00040000 | Reserved field (not used) |
Target Type Share | 0x00080000 | Negotiate NTLM2 Key |
Indicates that the NTLM2 signature and sealing scheme should be used to protect authenticated communication. Note that this refers to a specific session security scheme and is unrelated to the use of NTLMv2 authentication. | Request Init Response | Reserved field (not used) |
0x00200000 | Request Accept Response | Reserved field (not used) |
0x00400000 | Request Non-NT Session Key | Reserved field (not used) |
0x00800000 | Negotiate Target Info | Sent by the server in Type 2 messages to indicate that it includes the TargetInfo field in the message, which is used to calculate the NTLMv2 response. |
0x01000000 | unknown | Reserved field (not used) |
0x02000000 | unknown | Reserved field (not used) |
0x04000000 | unknown | Reserved field (not used) |
0x08000000 | unknown | Reserved field (not used) |
0x10000000 | unknown | Reserved field (not used) |
0x20000000 | Negotiate 128 | Indicates support for 128-bit encryption. |
0x40000000 | Negotiate Key Exchange | indicating that the client will provide the main encryption key in the 'session key' field of Type 3 messages. |
0x80000000 | Negotiate 56 | indicating support for 56-bit encryption. |
02NTLM message structure
AVPAIRS
AVPAIRS is a structure used in both Challenge and ChallengeResponse, consisting of a series of AV_PAIRs, and ends with an AV_PAIR structure with AvId as MsvAvEOL. The structure of AV_PAIR is as follows.
Among them, AvId represents the type of AV_PAIR, AvLen represents the length of the value, followed by the content of the value, and a class AV_PAIRS is defined in impacket to represent this structure.
class AV_PAIRS:
def __init__(self, data = None):
self.fields = {}
if data is not None:
self.fromString(data)
def __setitem__(self,key,value):
self.fields[key] = (len(value),value)
def __getitem__(self, key):
if key in self.fields:
return self.fields[key]
return None
def __delitem__(self, key):
del self.fields[key]
def __len__(self):
return len(self.getData())
def __str__(self):
return len(self.getData())
def fromString(self, data):
tInfo = data
fType = 0xff
while fType is not NTLMSSP_AV_EOL:
fType = struct.unpack('<H',tInfo[:struct.calcsize('<H')])[0]
tInfo = tInfo[struct.calcsize('<H'):]
length = struct.unpack('<H',tInfo[:struct.calcsize('<H')])[0]
tInfo = tInfo[struct.calcsize('<H'):]
content = tInfo[:length]
self.fields[fType]=(length,content)
tInfo = tInfo[length:]
def dump(self):
for i in list(self.fields.keys()):
print("%s: {%r}" % (i,self[i]))
def getData(self):
if NTLMSSP_AV_EOL in self.fields:
del self.fields[NTLMSSP_AV_EOL]
ans = b''
for i in list(self.fields.keys()):
ans+= struct.pack('<HH', i, self[i][0])
ans+= self[i][1]
# end with a NTLMSSP_AV_EOL
ans += struct.pack('<HH', NTLMSSP_AV_EOL, 0)
return ans
The AV_PAIRS class uses the dictionary fields to store AV_PAIR. getData and fromString are used for serialization and deserialization of the AV_PAIRS data type.
Version
The second class defined by ntlm in impacket is Version. The main function of this class is to be used for deserialization of the version field in the NTLM message. Let's take a look at the definition of the version field in the protocol.
The first 8 bytes represent the operating system information, and the last byte represents the NTLMSSP version, which is fixed to the constant NTLMSSP_REVISION_W2K3(0x0F). Let's take a look at the representation method in impacket.
class VERSION(Structure):
NTLMSSP_REVISION_W2K3 = 0x0F
structure = (
('ProductMajorVersion', '<B=0'),
('ProductMinorVersion', '<B=0'),
('ProductBuild', '<H=0'),
('Reserved', '3s=""'),
('NTLMRevisionCurrent', '<B=self.NTLMSSP_REVISION_W2K3'),
)
It can be seen that the VERSION class inherits from the Structure class, which is a very core class in impacket.基本上all the data structures in impacket are implemented by inheriting this class, and the usage methods of this class can be found in the documentation of the Structure class.
subclasses can define commonHdr and/or structure.
each of them is an tuple of either two: (fieldName, format) or three: (fieldName, ':', class) fields.
[it can't be a dictionary, because order is important]
where format specifies how the data in the field will be converted to/from bytes (string)
class is the class to use when unpacking ':' fields.
each field can only contain one value (or an array of values for *)
i.e. struct.pack('Hl',1,2) is valid, but format specifier 'Hl' is not (you must use 2 different fields)
format specifiers:
specifiers from module pack can be used with the same format
see struct.__doc__ (pack/unpack is finally called)
x [padding byte]
c [character]
b [signed byte]
B [unsigned byte]
h [signed short]
H [unsigned short]
l [signed long]
L [unsigned long]
i [signed integer]
I [unsigned integer]
q [signed long long (quad)]
Q [unsigned long long (quad)]
s [string (array of chars), must be preceded with length in format specifier, padded with zeros]
p [pascal string (includes byte count), must be preceded with length in format specifier, padded with zeros]
f [float]
d [double]
= [native byte ordering, size and alignment]
@ [native byte ordering, standard size and alignment]
! [network byte ordering]
< [little endian]
> [big endian]
usual printf like specifiers can be used (if started with %)
[not recommended, there is no way to unpack this]
%08x will output an 8 bytes hex
%s will output a string
%s\\x00 will output a NUL terminated string
%d%d will output 2 decimal digits (against the very same specification of Structure)
...
some additional format specifiers:
: just copy the bytes from the field into the output string (input may be string, other structure, or anything responding to __str__()) (for unpacking, all what's left is returned)
z same as :, but adds a NUL byte at the end (asciiz) (for unpacking the first NUL byte is used as terminator) [asciiz string]
u same as z, but adds two NUL bytes at the end (after padding to an even size with NULs). (same for unpacking) [unicode string]
w DCE-RPC/NDR string (it's a macro for [ '<L=(len(field)+1)/2','"\\x00\\x00\\x00\\x00','<L=(len(field)+1)/2',':' ])
?-field length of field named 'field', formatted as specified with ? ('?' may be '!H' for example). The input value overrides the real length
?1*?2 array of elements. Each formatted as '?2', the number of elements in the array is stored as specified by '?1' (?1 is optional, or can also be a constant (number), for unpacking)
'xxxx literal xxxx (field's value doesn't change the output. quotes must not be closed or escaped)
"xxxx literal xxxx (field's value doesn't change the output. quotes must not be closed or escaped)
_ will not pack the field. Accepts a third argument, which is an unpack code. See _Test_UnpackCode for an example.
?=packcode will evaluate packcode in the context of the structure, and pack the result as specified by ?. Unpacking is straightforward.
?&fieldname "Address of field fieldname".
For packing, it will simply pack the id() of fieldname. Or use 0 if fieldname doesn't exist.
For unpacking, it is used to determine whether the fieldname has to be unpacked or not, i.e., by adding a & to fieldname, you turn another field (fieldname) into an optional field.
From the document, we can see that the 'structure' field is a tuple list containing 2 or 3 elements, used to define the names and structures of various fields, such as the field (‘ProductMajorVersion’, ‘<B=0’), which indicates that the field name is ProductMajorVersion, the format is <B=0. From the document, we can see the format of ?=packcode, which first calculates the value of packcode, and then packs the value 0 according to the specified ? method, i.e., in little-endian order occupying one byte. Those unfamiliar with packing and unpacking in Python can refer to the usage method of the built-in struct package of Python. Therefore, it is indicated that the ProductMajorVersion field occupies 1 byte, with a default value of 0. This is a process of deserialization. Serialization is relatively simple, which is to calculate the number of bytes occupied according to <B, and then unpack the corresponding bytes and assign them to the ProductMajorVersion field.
Negotiate
Negotiate is one of the three major data packets used by NTLM authentication, usually referred to as Type1. The message structure is as follows
Here are two new field formats, DomainNameFields and WorkstationFields, let's take a look at the definitions of these two fields
DomainNameLen indicates the length of the field, DomainNameMaxLen and DomainNameLen are the same, DomainNameBufferOffset indicates the offset of the value of this field. Let's take a look at the class design of impacket.
class NTLMAuthNegotiate(Structure):
structure = (
('', '"NTLMSSP\x00'),
('message_type', '<L=1'),
('flags','<L'),
('domain_len','<H-domain_name'),
('domain_max_len','<H-domain_name'),
('domain_offset', '<L=0'),
('host_len','<H-host_name'),
('host_maxlen', '<H-host_name'),
('host_offset', '<L=0'),
('os_version', ':'),
('host_name',':'),
('domain_name', ':'))
def __init__(self):
Structure.__init__(self)
self['flags'] = (
NTLMSSP_NEGOTIATE_128 |
NTLMSSP_NEGOTIATE_KEY_EXCH|
# NTLMSSP_LM_KEY |
NTLMSSP_NEGOTIATE_NTLM |
NTLMSSP_NEGOTIATE_UNICODE |
# NTLMSSP_ALWAYS_SIGN |
NTLMSSP_NEGOTIATE_SIGN |
NTLMSSP_NEGOTIATE_SEAL |
# NTLMSSP_TARGET |
0)
self['host_name'] = ''
self['domain_name'] = ''
self['os_version'] = ''
self._workstation = ''
def setWorkstation(self, workstation):
self._workstation = workstation
def getWorkstation(self):
return self._workstation
def __hasNegotiateVersion(self):
return (self['flags'] & NTLMSSP_NEGOTIATE_VERSION) == NTLMSSP_NEGOTIATE_VERSION
def getData(self):
if len(self.fields['host_name']) > 0:
self['flags'] |= NTLMSSP_NEGOTIATE_OEM_WORKSTATION_SUPPLIED
if len(self.fields['domain_name']) > 0:
self['flags'] |= NTLMSSP_NEGOTIATE_OEM_DOMAIN_SUPPLIED
version_len = len(self.fields['os_version'])
if version_len > 0:
self['flags'] |= NTLMSSP_NEGOTIATE_VERSION
elif self.__hasNegotiateVersion():
raise Exception('Must provide the os_version field if the NTLMSSP_NEGOTIATE_VERSION flag is set')
if (self['flags'] & NTLMSSP_NEGOTIATE_OEM_WORKSTATION_SUPPLIED) == NTLMSSP_NEGOTIATE_OEM_WORKSTATION_SUPPLIED:
self['host_offset'] = 32 + version_len
if (self['flags'] & NTLMSSP_NEGOTIATE_OEM_DOMAIN_SUPPLIED) == NTLMSSP_NEGOTIATE_OEM_DOMAIN_SUPPLIED:
self['domain_offset'] = 32 + len(self['host_name']) + version_len
return Structure.getData(self)
def fromString(self, data):
Structure.fromString(self, data)
domain_offset = self['domain_offset']
domain_end = self['domain_len'] + domain_offset
self['domain_name'] = data[ domain_offset : domain_end ]
host_offset = self['host_offset']
host_end = self['host_len'] + host_offset
self['host_name'] = data[ host_offset : host_end ]
if len(data) >= 36 and self.__hasNegotiateVersion():
self['os_version'] = VERSION(data[32:])
else:
self['os_version'] = ''
Here is also a representation method for the Field field: ?-field, indicating that the field packs the length of field. In this class, the fromString method of the Structure class is also overridden. The fromString method is used for deserialization, i.e., unpacking. A simple experiment can be conducted to test deserialization.
from impacket.ntlm import NTLMAuthNegotiate
if __name__ == '__main__':
nego_data = bytes.fromhex('4e544c4d5353500001000000978208e2000000000000000000000000000000000a00614a0000000f')
nego = NTLMAuthNegotiate()
nego.fromString(nego_data)
nego.dump()
The result is as follows
NTLMAuthNegotiate
: {'NTLMSSP\x00'}
message_type: {1}
flags: {3792208535}
domain_len: {0}
domain_max_len: {0}
domain_offset: {0}
host_len: {0}
host_maxlen: {0}
host_offset: {0}
os_version: {
ProductMajorVersion: {10}
ProductMinorVersion: {0}
ProductBuild: {19041}
Reserved: {b'\x00\x00\x00'}
NTLMRevisionCurrent: {15}
}
host_name: {b''}
domain_name: {b''}
Challenge
class NTLMAuthChallenge(Structure):
structure = (
('', '"NTLMSSP\x00'),
('message_type', '<L=2'),
('domain_len','<H-domain_name'),
('domain_max_len','<H-domain_name'),
('domain_offset', '<L=40'),
('flags', '<L=0'),
('challenge', '8s'),
('reserved', '8s=""'),
('TargetInfoFields_len', '<H-TargetInfoFields'),
('TargetInfoFields_max_len', '<H-TargetInfoFields'),
('TargetInfoFields_offset', '<L'),
('VersionLen','_-Version','self.checkVersion(self["flags"])'),
('Version', ':'),
('domain_name',':'),
('TargetInfoFields', ':'))
@staticmethod
def checkVersion(flags):
if flags is not None:
if flags & NTLMSSP_NEGOTIATE_VERSION == 0:
return 0
return 8
def getData(self):
if self['TargetInfoFields'] is not None and type(self['TargetInfoFields']) is not bytes:
raw_av_fields = self['TargetInfoFields'].getData()
self['TargetInfoFields'] = raw_av_fields
return Structure.getData(self)
def fromString(self, data):
Structure.fromString(self, data)
self['domain_name'] = data[self['domain_offset']:][:self['domain_len']]
self['TargetInfoFields'] = data[self['TargetInfoFields_offset']:][:self['TargetInfoFields_len']]
return self
Challenge messages are also known as Type2. In the definition of Type2, there appears a new field format ('VersionLen', '_-Version', 'self.checkVersion(self["flags"])'). This indicates that the 'VersionLen' field represents the length of 'Version', and this field will not be added to the data during serialization. Moreover, its value can be overridden by the result of 'self.checkVersion(self["flags"])'. The author has rewritten the 'fromString' method to deserialize the 'domain_name' and 'TargetInfoFields' fields, but in fact, this section is not necessary. The judgment of the 'Field' field's corresponding value is already implemented in the 'findLengthFieldFor' implementation of 'Structure', which means that the value of 'domain_name' and 'TargetInfoFields' is also determined when 'domain_field' and 'TargetInfo_Fields' are determined. The length is also determined.
Commenting out this code segment also allows for normal serialization.
Example
if __name__ == '__main__':
challenge_data = bytes.fromhex('4e544c4d53535000020000000400040038000000158289e267bf6a81e0c5dcd300000000000000006a006a003c0000000a0063450000000f4a004400020004004a004400010008004400430030003100040010006a0064002e006c006f00630061006c0003001a0044004300300031002e006a0064002e006c006f00630061006c00050010006a0064002e006c006f00630061006c00070008004e3a1907b2dbd90100000000')
challenge = NTLMAuthChallenge()
challenge.fromString(nego_data)
challenge.dump()
NTLMAuthChallenge
: {'NTLMSSP\x00'}
message_type: {2}
domain_len: {4}
domain_max_len: {4}
domain_offset: {56}
flags: {3800662549}
challenge: {b'g\xbfj\x81\xe0\xc5\xdc\xd3'}
reserved: {b'\x00\x00\x00\x00\x00\x00\x00\x00'}
TargetInfoFields_len: {106}
TargetInfoFields_max_len: {106}
TargetInfoFields_offset: {60}
VersionLen: {8}
Version: {b'\n\x00cE\x00\x00\x00\x0f'}
domain_name: {b'J\x00D\x00'}
TargetInfoFields: {b'\x02\x00\x04\x00J\x00D\x00\x01\x00\x08\x00D\x00C\x000\x001\x00\x04\x00\x10\x00j\x00d\x00.\x00l\x00o\x00c\x00a\x00l\x00\x03\x00\x1a\x00D\x00C\x000\x001\x00.\x00j\x00d\x00.\x00l\x00o\x00c\x00a\x00l\x00\x05\x00\x10\x00j\x00d\x00.\x00l\x00o\x00c\x00a\x00l\x00\x07\x00\x08\x00N:\x19\x07\xb2\xdb\xd9\x01\x00\x00\x00\x00'}
ChallengeResponse
ChallengeResponse, also known as Type3, has more fields compared to other types. Important fields include lanman, ntlm, session_key, and MIC.
class NTLMAuthChallengeResponse(Structure):
structure = (
('', '"NTLMSSP\x00'),
('message_type','<L=3'),
('lanman_len','<H-lanman'),
('lanman_max_len','<H-lanman'),
('lanman_offset','<L'),
('ntlm_len','<H-ntlm'),
('ntlm_max_len','<H-ntlm'),
('ntlm_offset','<L'),
('domain_len','<H-domain_name'),
('domain_max_len','<H-domain_name'),
('domain_offset','<L'),
('user_len','<H-user_name'),
('user_max_len','<H-user_name'),
('user_offset','<L'),
('host_len','<H-host_name'),
('host_max_len','<H-host_name'),
('host_offset','<L'),
('session_key_len','<H-session_key'),
('session_key_max_len','<H-session_key'),
('session_key_offset','<L'),
('flags','<L'),
('VersionLen','_-Version','self.checkVersion(self["flags"])'),
('Version',':=""'),
('MICLen','_-MIC','self.checkMIC(self["flags"])'),
('MIC',':=""'),
('domain_name',':'),
('user_name',':'),
('host_name',':'),
('lanman', ':'),
('ntlm', ':'),
('session_key', ':'))
...
lanman, representing LMChallengeResponse, calculated by lm, and the value of this field is mostly empty in modern network environments.
ntlm, representing NTChallengeResponse, which exists in both ntlm v1 and v2, and the calculation method is different.
session_key, used for negotiating the encryption key of the communication protocol using the ntlm protocol.
MIC, used to ensure the integrity of the ChallengeResponse message and prevent tampering.
03NTLMSSP
In addition to defining the basic data structure, there are two commonly used high-level functions in the ntlm implementation. Since ntlm is basically used as a client in impacket, and the client needs to construct Type1 and Type3 in ntlm authentication, getNTLMSSPType1 and getNTLMSSPType3 functions are defined here to construct packets.
getNTLMSSPType1
getNTLMSSPType1 is used to construct Type1 packets. Although the function receives 4 parameters, the workstation and domain parameters are not used, and it is mainly used for initializing the negotiation flag.
def getNTLMSSPType1(workstation='', domain='', signingRequired = False, use_ntlmv2 = USE_NTLMv2):
# 在继续之前做一些编码检查。有点脏,但在处理时发现效果不错。
# international characters.
import sys
encoding = sys.getfilesystemencoding()
if encoding is not None:
try:
workstation.encode('utf-16le')
except:
workstation = workstation.decode(encoding)
try:
domain.encode('utf-16le')
except:
domain = domain.decode(encoding)
# Let's prepare a Type 1 NTLMSSP Message
auth = NTLMAuthNegotiate()
# auth['os_version'] = bytes.fromhex('0a00614a0000000f')
auth['flags']=0
if signingRequired:
auth['flags'] = NTLMSSP_NEGOTIATE_KEY_EXCH | NTLMSSP_NEGOTIATE_SIGN | NTLMSSP_NEGOTIATE_ALWAYS_SIGN | \
NTLMSSP_NEGOTIATE_SEAL
if use_ntlmv2:
auth['flags'] |= NTLMSSP_NEGOTIATE_TARGET_INFO
auth['flags'] |= NTLMSSP_NEGOTIATE_NTLM | NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY | NTLMSSP_NEGOTIATE_UNICODE | \
NTLMSSP_REQUEST_TARGET | NTLMSSP_NEGOTIATE_128 | NTLMSSP_NEGOTIATE_56
# 这次我们不添加工作站/域字段。通常 Windows 客户端不会添加此类信息,但...
# 我们将保存工作站名称以供以后使用。
auth.setWorkstation(workstation)
return auth
getNTLMSSPType3
Type 3 消息是 NTLMSSP 握手过程的最后一步,用于向服务器发送身份验证凭据以进行身份验证,这也是ntlm认证最核心的部分。
def getNTLMSSPType3(type1, type2, user, password, domain, lmhash = '', nthash = '', use_ntlmv2 = USE_NTLMv2):
# 在某人发送 password = None 的情况下进行安全检查。这是不允许的。将其设置为 '' 并希望一切顺利。
if password is None:
password = ''
# 在继续之前做一些编码检查。有点脏,但在处理时发现效果不错。
# international characters.
import sys
encoding = sys.getfilesystemencoding()
if encoding is not None:
try:
user.encode('utf-16le')
except:
user = user.decode(encoding)
try:
password.encode('utf-16le')
except:
password = password.decode(encoding)
try:
domain.encode('utf-16le')
except:
domain = user.decode(encoding)
ntlmChallenge = NTLMAuthChallenge(type2)
# Let's start with the original flags sent in the type1 message
responseFlags = type1['flags']
# responseFlags = 3767042613
# Token received and parsed. Depending on the authentication
# method we will create a valid ChallengeResponse
ntlmChallengeResponse = NTLMAuthChallengeResponse(user, password, ntlmChallenge['challenge'])
clientChallenge = b("".join([random.choice(string.digits+string.ascii_letters) for _ in range(8)]))
serverName = ntlmChallenge['TargetInfoFields']
ntResponse, lmResponse, sessionBaseKey = computeResponse(ntlmChallenge['flags'], ntlmChallenge['challenge'],
clientChallenge, serverName, domain, user, password,
lmhash, nthash, use_ntlmv2)
Since the response needs to be calculated through the information in Type2, it can be seen that the function takes type1 and type2 as input parameters. At line 25, it initializes the NTLMAuthChallenge class using type2, which automatically performs deserialization. At lines 32 and 36, it retrieves the challenge and TargetInfoFields fields of Type2, respectively. Line 34 generates an 8-byte client challenge. Note that the client challenge is composed of numbers and letters, but a normal client challenge is almost impossible to be composed entirely of visible characters, so it can also be used as a feature to identify the impacket's ntlm implementation. Line 38 is the most important step in calculating the ntResponse and lmResponse. Many people may confuse the concepts of ntlmv1, ntlmv2, nthash, and lmhash, but we can discover through computeResponse that the basic structure of ntlmv1 and ntlmv2 is the same, the only difference being the calculation method of ntResponse and lmResponse.
computeResponseNTLMv1
computeResponseNTLMv1 is used to calculate the ntResponse and lmResponse in ntlmv1
def computeResponseNTLMv1(flags, serverChallenge, clientChallenge, serverName, domain, user, password, lmhash='',
nthash='', use_ntlmv2=USE_NTLMv2):
if user == '' and password == ''
# Special case for anonymous authentication
lmResponse = ''
ntResponse = ''
else:
lmhash = LMOWFv1(password, lmhash, nthash)
nthash = NTOWFv1(password, lmhash, nthash)
if flags & NTLMSSP_NEGOTIATE_LM_KEY:
ntResponse = ''
lmResponse = get_ntlmv1_response(lmhash, serverChallenge)
elif flags & NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY:
md5 = hashlib.new('md5')
chall = (serverChallenge + clientChallenge)
md5.update(chall)
ntResponse = ntlmssp_DES_encrypt(nthash, md5.digest()[:8])
lmResponse = clientChallenge + b'\x00'*16
else:
ntResponse = get_ntlmv1_response(nthash,serverChallenge)
lmResponse = get_ntlmv1_response(lmhash, serverChallenge)
sessionBaseKey = generateSessionKeyV1(password, lmhash, nthash)
return ntResponse, lmResponse, sessionBaseKey
The calculation of ntResponse and lmResponse in ntlmv1 is divided into three cases. If the flag NTLMSSP_NEGOTIATE_LM_KEY is set in the Type, then only lmResponse is present. The lmResponse is encrypted by lmhash using serverChallenge. If the flag NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY is set in the Type, the lmResponse part is filled with clientChallenge and 16 zeros, and ntResponse uses nthash encryption. The encrypted content is the first 8 bytes of md5 after concatenating serverChallenge and clientChallenge. In other cases, ntResponse and lmResponse are encrypted by nthash and lmhash respectively using serverChallenge. It can be found that in all three cases, the calculated ntResponse and lmResponse are both 24 bytes long.
computeResponseNTLMv2
In ntlmv2, there are significant changes in the calculation of ntResponse and lmResponse. Firstly, the encryption key no longer uses lmhash, and it does not directly use nthash as the key, but instead uses a responseKeyNT derived from nthash. The encryption method also changes from des to hmac_md5
def computeResponseNTLMv2(flags, serverChallenge, clientChallenge, serverName, domain, user, password, lmhash='',)
nthash='', use_ntlmv2=USE_NTLMv2):
responseServerVersion = b'\x01'
hiResponseServerVersion = b'\x01'
responseKeyNT = NTOWFv2(user, password, domain, nthash)
av_pairs = AV_PAIRS(serverName)
# In order to support SPN target name validation, we have to add this to the serverName av_pairs. Otherwise we will
# get access denied
# This is set at Local Security Policy -> Local Policies -> Security Options -> Server SPN target name validation
# level
if TEST_CASE is False:
av_pairs[NTLMSSP_AV_TARGET_NAME] = 'cifs/'.encode('utf-16le') + av_pairs[NTLMSSP_AV_HOSTNAME][1]
if av_pairs[NTLMSSP_AV_TIME] is not None:
aTime = av_pairs[NTLMSSP_AV_TIME][1]
else:
aTime = struct.pack('<q', (116444736000000000 + calendar.timegm(time.gmtime()) * 10000000) )
av_pairs[NTLMSSP_AV_TIME] = aTime
serverName = av_pairs.getData()
else:
aTime = b'\x00'*8
temp = responseServerVersion + hiResponseServerVersion + b'\x00' * 6 + aTime + clientChallenge + b'\x00' * 4 + \
serverName + b'\x00' * 4
ntProofStr = hmac_md5(responseKeyNT, serverChallenge + temp)
ntChallengeResponse = ntProofStr + temp
lmChallengeResponse = hmac_md5(responseKeyNT, serverChallenge + clientChallenge) + clientChallenge
sessionBaseKey = hmac_md5(responseKeyNT, ntProofStr)
if user == '' and password == ''
# Special case for anonymous authentication
ntChallengeResponse = ''
lmChallengeResponse = ''
return ntChallengeResponse, lmChallengeResponse, sessionBaseKey
ntChallengeResponse consists of two parts, a 16-byte Response and an NTLMv2_CLIENT_CHALLENGE structure. NTLMv2_CLIENT_CHALLENGE includes the current time, client challenge, and TargetInfo information from Type2. The Response is obtained by encrypting (serverChallenge + NTLMv2_CLIENT_CHALLENGE) with responseKeyNT. lmChallengeResponse contains a 16-byte Response and ChallengeFromClient. The Response calculation is relatively simple, encrypting serverChallenge + clientChallenge with responseKeyNT, and filling the ChallengeFromClient field with clientChallenge.
From here, it can be seen that both NTLMv1 and NTLMv2 convert passwords into hashes through OWF (one way function) before using them as encryption keys. This is also the reason why we can use nthash for PTH.
This article briefly introduces the implementation of NTLM in the impacket library, including some underlying data structure design, encryption process, and two auxiliary functions for constructing data packets. Since NTLM is an embedded protocol, we will elaborate on the interaction between the application layer and NTLM in subsequent application layer protocols.

评论已关闭