Skip to content

Commit 31b2a1f

Browse files
authored
Merge pull request Pennyw0rth#191 from XiaoliChan/ntlm_parser
[lib] Improve ntlm_parser.py
2 parents d695202 + 423b70b commit 31b2a1f

3 files changed

Lines changed: 37 additions & 132 deletions

File tree

nxc/helpers/ntlm_parser.py

Lines changed: 27 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,107 +1,34 @@
1-
# Original from here: https://github.com/nopfor/ntlm_challenger
1+
# Original from here: https://github.com/fortra/impacket/blob/master/examples/DumpNTLMInfo.py#L568
22

3-
import datetime
3+
import struct
44

5+
from impacket import ntlm
56
from impacket.smb3 import WIN_VERSIONS
7+
import contextlib
68

79

8-
def decoder(byte_string, decode_type):
9-
if decode_type == "byte":
10-
return byte_string.decode("UTF-8").replace("\x00", "")
11-
else:
12-
return int.from_bytes(byte_string, "little")
13-
14-
15-
def parse_version(version_bytes):
16-
major_version = version_bytes[0]
17-
minor_version = version_bytes[1]
18-
product_build = decoder(version_bytes[2:4], "int")
19-
if product_build in WIN_VERSIONS:
20-
return f"{WIN_VERSIONS[product_build]} Build {product_build}"
21-
else:
22-
return f"Windows {major_version}.{minor_version} Build {product_build}"
23-
24-
25-
def parse_target_info(target_info_bytes):
26-
MsvAvEOL = 0x0000
27-
MsvAvNbComputerName = 0x0001
28-
MsvAvNbDomainName = 0x0002
29-
MsvAvDnsComputerName = 0x0003
30-
MsvAvDnsDomainName = 0x0004
31-
MsvAvDnsTreeName = 0x0005
32-
MsvAvFlags = 0x0006
33-
MsvAvTimestamp = 0x0007
34-
MsvAvSingleHost = 0x0008
35-
MsvAvTargetName = 0x0009
36-
MsvAvChannelBindings = 0x000A
37-
10+
def parse_challenge(challange):
3811
target_info = {
39-
"MsvAvNbComputerName": None,
40-
"MsvAvDnsDomainName": None,
41-
}
42-
info_offset = 0
43-
44-
while info_offset < len(target_info_bytes):
45-
av_id = decoder(target_info_bytes[info_offset:info_offset + 2], "int")
46-
av_len = decoder(target_info_bytes[info_offset + 2:info_offset + 4], "int")
47-
av_value = target_info_bytes[info_offset + 4:info_offset + 4 + av_len]
48-
49-
info_offset = info_offset + 4 + av_len
50-
51-
if av_id == MsvAvEOL:
52-
pass
53-
elif av_id == MsvAvNbComputerName:
54-
target_info["MsvAvNbComputerName"] = decoder(av_value, "byte")
55-
elif av_id == MsvAvNbDomainName:
56-
target_info["MsvAvNbDomainName"] = decoder(av_value, "byte")
57-
elif av_id == MsvAvDnsComputerName:
58-
target_info["MsvAvDnsComputerName"] = decoder(av_value, "byte")
59-
elif av_id == MsvAvDnsDomainName:
60-
target_info["MsvAvDnsDomainName"] = decoder(av_value, "byte")
61-
elif av_id == MsvAvDnsTreeName:
62-
target_info["MsvAvDnsTreeName"] = decoder(av_value, "byte")
63-
elif av_id == MsvAvFlags:
64-
pass
65-
elif av_id == MsvAvTimestamp:
66-
filetime = decoder(av_value, "int")
67-
microseconds = (filetime - 116444736000000000) / 10
68-
time = datetime.datetime(1970, 1, 1) + datetime.timedelta(microseconds=microseconds)
69-
target_info["MsvAvTimestamp"] = time.strftime("%b %d, %Y %H:%M:%S.%f")
70-
elif av_id == MsvAvSingleHost:
71-
target_info["MsvAvSingleHost"] = decoder(av_value, "byte")
72-
elif av_id == MsvAvTargetName:
73-
target_info["MsvAvTargetName"] = decoder(av_value, "byte")
74-
elif av_id == MsvAvChannelBindings:
75-
target_info["MsvAvChannelBindings"] = av_value
76-
return target_info
77-
78-
79-
def parse_challenge(challenge_message):
80-
# TargetNameFields
81-
target_name_fields = challenge_message[12:20]
82-
target_name_len = decoder(target_name_fields[0:2], "int")
83-
target_name_offset = decoder(target_name_fields[4:8], "int")
84-
85-
# TargetInfoFields
86-
target_info_fields = challenge_message[40:48]
87-
target_info_len = decoder(target_info_fields[0:2], "int")
88-
target_info_offset = decoder(target_info_fields[4:8], "int")
89-
90-
# Version
91-
version = None
92-
version_bytes = challenge_message[48:56]
93-
version = parse_version(version_bytes)
94-
95-
# TargetName
96-
target_name = challenge_message[target_name_offset:target_name_offset + target_name_len]
97-
target_name = decoder(target_name, "byte")
98-
99-
# TargetInfo
100-
target_info_bytes = challenge_message[target_info_offset:target_info_offset + target_info_len]
101-
target_info = parse_target_info(target_info_bytes)
102-
103-
return {
104-
"target_name": target_name,
105-
"version": version,
106-
"target_info": target_info
12+
"hostname": None,
13+
"domain": None,
14+
"os_version": None
10715
}
16+
challange = ntlm.NTLMAuthChallenge(challange)
17+
av_pairs = ntlm.AV_PAIRS(challange["TargetInfoFields"][:challange["TargetInfoFields_len"]])
18+
if av_pairs[ntlm.NTLMSSP_AV_HOSTNAME] is not None:
19+
with contextlib.suppress(Exception):
20+
target_info["hostname"] = av_pairs[ntlm.NTLMSSP_AV_HOSTNAME][1].decode("utf-16le")
21+
if av_pairs[ntlm.NTLMSSP_AV_DNS_DOMAINNAME] is not None:
22+
with contextlib.suppress(Exception):
23+
target_info["domain"] = av_pairs[ntlm.NTLMSSP_AV_DNS_DOMAINNAME][1].decode("utf-16le")
24+
if "Version" in challange.fields:
25+
version = challange["Version"]
26+
if len(version) >= 4:
27+
major_version = version[0]
28+
minor_version = version[1]
29+
product_build = struct.unpack("<H", version[2:4])[0]
30+
if product_build in WIN_VERSIONS:
31+
target_info["os_version"] = f"{WIN_VERSIONS[product_build]} Build {product_build}"
32+
else:
33+
target_info["os_version"] = f"{major_version}.{minor_version} Build {product_build}"
34+
return target_info

nxc/protocols/winrm.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,9 @@ def proto_logger(self):
5252

5353
def enum_host_info(self):
5454
ntlm_info = parse_challenge(base64.b64decode(self.challenge_header.split(" ")[1].replace(",", "")))
55-
self.domain = ntlm_info["target_info"]["MsvAvDnsDomainName"]
56-
self.hostname = ntlm_info["target_info"]["MsvAvNbComputerName"]
57-
self.server_os = ntlm_info["version"]
55+
self.domain = ntlm_info["domain"]
56+
self.hostname = ntlm_info["hostname"]
57+
self.server_os = ntlm_info["os_version"]
5858
self.logger.extra["hostname"] = self.hostname
5959

6060
self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}")

nxc/protocols/wmi.py

Lines changed: 7 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import os
2-
import struct
32
import logging
43

54
from io import StringIO
6-
from six import indexbytes
75
from datetime import datetime
6+
7+
from nxc.helpers.ntlm_parser import parse_challenge
88
from nxc.config import process_secret
99
from nxc.connection import connection, dcom_FirewallChecker, requires_admin
1010
from nxc.logger import NXCAdapter
@@ -18,7 +18,6 @@
1818
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_AUTHN_WINNT, RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_INTEGRITY, MSRPC_BIND, MSRPCBind, CtxItem, MSRPCHeader, SEC_TRAILER, MSRPCBindAck
1919
from impacket.dcerpc.v5.dcomrt import DCOMConnection
2020
from impacket.dcerpc.v5.dcom.wmi import CLSID_WbemLevel1Login, IID_IWbemLevel1Login, IWbemLevel1Login
21-
import contextlib
2221

2322
MSRPC_UUID_PORTMAP = uuidtup_to_bin(("E1AF8308-5D1F-11C9-91A4-08002B14A0FA", "3.0"))
2423

@@ -86,7 +85,6 @@ def create_conn_obj(self):
8685
def enum_host_info(self):
8786
# All code pick from DumpNTLNInfo.py
8887
# https://github.com/fortra/impacket/blob/master/examples/DumpNTLMInfo.py
89-
ntlmChallenge = None
9088

9189
bind = MSRPCBind()
9290
item = CtxItem()
@@ -123,39 +121,19 @@ def enum_host_info(self):
123121
if buffer != 0:
124122
response = MSRPCHeader(buffer)
125123
bindResp = MSRPCBindAck(response.getData())
126-
127-
ntlmChallenge = ntlm.NTLMAuthChallenge(bindResp["auth_data"])
128-
129-
if ntlmChallenge["TargetInfoFields_len"] > 0:
130-
av_pairs = ntlm.AV_PAIRS(ntlmChallenge["TargetInfoFields"][: ntlmChallenge["TargetInfoFields_len"]])
131-
if av_pairs[ntlm.NTLMSSP_AV_HOSTNAME][1] is not None:
132-
try:
133-
self.hostname = av_pairs[ntlm.NTLMSSP_AV_HOSTNAME][1].decode("utf-16le")
134-
except Exception:
135-
self.hostname = self.host
136-
if av_pairs[ntlm.NTLMSSP_AV_DNS_DOMAINNAME][1] is not None:
137-
try:
138-
self.domain = av_pairs[ntlm.NTLMSSP_AV_DNS_DOMAINNAME][1].decode("utf-16le")
139-
except Exception:
140-
self.domain = self.args.domain
141-
if av_pairs[ntlm.NTLMSSP_AV_DNS_HOSTNAME][1] is not None:
142-
with contextlib.suppress(Exception):
143-
self.fqdn = av_pairs[ntlm.NTLMSSP_AV_DNS_HOSTNAME][1].decode("utf-16le")
144-
if "Version" in ntlmChallenge.fields:
145-
version = ntlmChallenge["Version"]
146-
if len(version) >= 4:
147-
self.server_os = "Windows NT %d.%d Build %d" % (indexbytes(version, 0), indexbytes(version, 1), struct.unpack("<H", version[2:4])[0])
124+
ntlm_info = parse_challenge(bindResp["auth_data"])
125+
self.domain = ntlm_info["domain"]
126+
self.hostname = ntlm_info["hostname"]
127+
self.server_os = ntlm_info["os_version"]
128+
self.logger.extra["hostname"] = self.hostname
148129
else:
149130
self.hostname = self.host
150-
151131
if self.args.local_auth:
152132
self.domain = self.hostname
153133
if self.args.domain:
154134
self.domain = self.args.domain
155135
self.fqdn = f"{self.hostname}.{self.domain}"
156136

157-
self.logger.extra["hostname"] = self.hostname
158-
159137
self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-"))
160138

161139
def print_host_info(self):

0 commit comments

Comments
 (0)