Skip to content

Commit ac60614

Browse files
authored
Merge pull request Pennyw0rth#172 from XiaoliChan/winrm-ntlm-info
[winrm] say goodbye to SMB
2 parents 7a46f9e + 9f0ac2c commit ac60614

3 files changed

Lines changed: 138 additions & 71 deletions

File tree

nxc/helpers/ntlm_parser.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# Original from here: https://github.com/nopfor/ntlm_challenger
2+
3+
import datetime
4+
5+
from impacket.smb3 import WIN_VERSIONS
6+
7+
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+
38+
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
107+
}

nxc/protocols/winrm.py

Lines changed: 30 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import os
2+
import base64
23
import requests
34
import urllib3
4-
import contextlib
55
import logging
66
import xml.etree.ElementTree as ET
77

@@ -10,13 +10,13 @@
1010
from pypsrp.wsman import NAMESPACES
1111
from pypsrp.client import Client
1212

13-
from impacket.smbconnection import SMBConnection
1413
from impacket.examples.secretsdump import LocalOperations, LSASecrets, SAMHashes
1514

1615
from nxc.config import process_secret
1716
from nxc.connection import connection
1817
from nxc.helpers.bloodhound import add_user_bh
1918
from nxc.helpers.misc import gen_random_string
19+
from nxc.helpers.ntlm_parser import parse_challenge
2020
from nxc.logger import NXCAdapter
2121

2222

@@ -33,58 +33,33 @@ def __init__(self, args, db, host):
3333
self.lmhash = ""
3434
self.nthash = ""
3535
self.ssl = False
36-
self.auth_type = None
36+
self.challenge_header = None
3737

3838
connection.__init__(self, args, db, host)
3939

4040
def proto_logger(self):
41-
# Reason why default is SMB/445, because default is enumerate over SMB.
42-
# For more details, please check the function "print_host_info"
41+
# For more details, please check the function "print_host_info"
4342
logging.getLogger("pypsrp").disabled = True
4443
logging.getLogger("pypsrp.wsman").disabled = True
4544
self.logger = NXCAdapter(
4645
extra={
47-
"protocol": "SMB",
46+
"protocol": "WINRM",
4847
"host": self.host,
49-
"port": "445",
48+
"port": "5985",
5049
"hostname": self.hostname,
5150
}
5251
)
5352

5453
def enum_host_info(self):
55-
# smb no open, specify the domain
56-
if self.args.no_smb:
57-
self.domain = self.args.domain
58-
else:
59-
try:
60-
smb_conn = SMBConnection(self.host, self.host, None, timeout=5)
61-
no_ntlm = False
62-
except Exception as e:
63-
self.logger.fail(f"Error retrieving host domain: {e} specify one manually with the '-d' flag")
64-
else:
65-
try:
66-
smb_conn.login("", "")
67-
except BrokenPipeError:
68-
self.logger.fail("Broken Pipe Error while attempting to login")
69-
except Exception as e:
70-
if "STATUS_NOT_SUPPORTED" in str(e):
71-
# no ntlm supported
72-
no_ntlm = True
73-
74-
self.domain = smb_conn.getServerDNSDomainName() if not no_ntlm else self.args.domain
75-
self.hostname = smb_conn.getServerName() if not no_ntlm else self.host
76-
self.server_os = smb_conn.getServerOS()
77-
if isinstance(self.server_os.lower(), bytes):
78-
self.server_os = self.server_os.decode("utf-8")
54+
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"]
58+
self.logger.extra["hostname"] = self.hostname
7959

80-
self.logger.extra["hostname"] = self.hostname
60+
self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}")
8161

82-
self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}")
83-
84-
with contextlib.suppress(Exception):
85-
smb_conn.logoff()
86-
87-
self.db.add_host(self.host, self.port, self.hostname, self.domain, self.server_os)
62+
self.db.add_host(self.host, self.port, self.hostname, self.domain, self.server_os)
8863

8964
if self.args.domain:
9065
self.domain = self.args.domain
@@ -98,16 +73,10 @@ def enum_host_info(self):
9873
self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-"))
9974

10075
def print_host_info(self):
101-
if self.args.no_smb:
102-
self.logger.extra["protocol"] = "WINRM-SSL" if self.ssl else "WINRM"
103-
self.logger.extra["port"] = self.port
104-
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain})")
105-
else:
106-
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain})")
107-
self.logger.extra["protocol"] = "WINRM-SSL" if self.ssl else "WINRM"
108-
self.logger.extra["port"] = self.port
109-
110-
self.logger.info(f"Connection information: {self.endpoint} (auth type:{self.auth_type}) (domain:{self.domain if self.args.domain else ''})")
76+
self.logger.extra["protocol"] = "WINRM-SSL" if self.ssl else "WINRM"
77+
self.logger.extra["port"] = self.port
78+
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain})")
79+
11180
return True
11281

11382
def create_conn_obj(self):
@@ -117,6 +86,14 @@ def create_conn_obj(self):
11786

11887
endpoints = {}
11988

89+
headers = {
90+
"Content-Length": "0",
91+
"Keep-Alive": "true",
92+
"Content-Type": "application/soap+xml;charset=UTF-8",
93+
"User-Agent": "Microsoft WinRM Client",
94+
"Authorization": "Negotiate TlRMTVNTUAABAAAAB4IIogAAAAAAAAAAAAAAAAAAAAAGAbEdAAAADw=="
95+
}
96+
12097
for protocol in self.args.check_proto:
12198
endpoints[protocol] = {}
12299
endpoints[protocol]["port"] = self.port[self.args.check_proto.index(protocol)] if len(self.port) == 2 else self.port[0]
@@ -131,9 +108,12 @@ def create_conn_obj(self):
131108
self.port = endpoints[protocol]["port"]
132109
try:
133110
self.logger.debug(f"Requesting URL: {endpoints[protocol]['url']}")
134-
res = requests.post(endpoints[protocol]["url"], verify=False, timeout=self.args.http_timeout)
111+
res = requests.post(endpoints[protocol]["url"], headers=headers, verify=False, timeout=self.args.http_timeout)
135112
self.logger.debug(f"Received response code: {res.status_code}")
136-
self.auth_type = res.headers["WWW-Authenticate"] if "WWW-Authenticate" in res.headers else "NOAUTH"
113+
self.challenge_header = res.headers["WWW-Authenticate"]
114+
if (not self.challenge_header) or ("Negotiate" not in self.challenge_header):
115+
self.logger.info('Failed to get NTLM challenge from target "/wsman" endpoint, maybe isn\'t winrm service.')
116+
return False
137117
self.endpoint = endpoints[protocol]["url"]
138118
self.ssl = endpoints[protocol]["ssl"]
139119
return True

nxc/protocols/winrm/proto_args.py

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
from argparse import _StoreTrueAction
2-
3-
41
def proto_args(parser, std_parser, module_parser):
52
winrm_parser = parser.add_parser("winrm", help="own stuff using WINRM", parents=[std_parser, module_parser])
63
winrm_parser.add_argument("-H", "--hash", metavar="HASH", dest="hash", nargs="+", default=[], help="NTLM hash(es) or file(s) containing NTLM hashes")
@@ -9,12 +6,10 @@ def proto_args(parser, std_parser, module_parser):
96
winrm_parser.add_argument("--check-proto", nargs="+", default=["http", "https"], help="Choose what prorocol you want to check, default is %(default)s, format: 'http https'(with space separated) or 'single-protocol'")
107
winrm_parser.add_argument("--laps", dest="laps", metavar="LAPS", type=str, help="LAPS authentification", nargs="?", const="administrator")
118
winrm_parser.add_argument("--http-timeout", dest="http_timeout", type=int, default=10, help="HTTP timeout for WinRM connections")
12-
no_smb_arg = winrm_parser.add_argument("--no-smb", action=get_conditional_action(_StoreTrueAction), make_required=[], help="No smb connection")
139

1410
dgroup = winrm_parser.add_mutually_exclusive_group()
15-
domain_arg = dgroup.add_argument("-d", metavar="DOMAIN", dest="domain", type=str, default=None, help="domain to authenticate to")
11+
dgroup.add_argument("-d", metavar="DOMAIN", dest="domain", type=str, default=None, help="domain to authenticate to")
1612
dgroup.add_argument("--local-auth", action="store_true", help="authenticate locally to each target")
17-
no_smb_arg.make_required = [domain_arg]
1813

1914
cgroup = winrm_parser.add_argument_group("Credential Gathering", "Options for gathering credentials")
2015
cgroup.add_argument("--dump-method", action="store", default="cmd", choices={"cmd", "powershell"}, help="Select shell type in hashes dump")
@@ -29,18 +24,3 @@ def proto_args(parser, std_parser, module_parser):
2924
cgroup.add_argument("-X", metavar="PS_COMMAND", dest="ps_execute", help="execute the specified PowerShell command")
3025

3126
return parser
32-
33-
34-
def get_conditional_action(baseAction):
35-
class ConditionalAction(baseAction):
36-
def __init__(self, option_strings, dest, **kwargs):
37-
x = kwargs.pop("make_required", [])
38-
super().__init__(option_strings, dest, **kwargs)
39-
self.make_required = x
40-
41-
def __call__(self, parser, namespace, values, option_string=None):
42-
for x in self.make_required:
43-
x.required = True
44-
super().__call__(parser, namespace, values, option_string)
45-
46-
return ConditionalAction

0 commit comments

Comments
 (0)