[winrm] say goodbye to SMB

Signed-off-by: XiaoliChan <30458572+XiaoliChan@users.noreply.github.com>
main
XiaoliChan 2024-02-08 00:14:07 +08:00 committed by Alex
parent 7a46f9eb64
commit 9ceab247a8
2 changed files with 129 additions and 49 deletions

View File

@ -1,7 +1,7 @@
import os import os
import base64
import requests import requests
import urllib3 import urllib3
import contextlib
import logging import logging
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
@ -10,7 +10,6 @@ from datetime import datetime
from pypsrp.wsman import NAMESPACES from pypsrp.wsman import NAMESPACES
from pypsrp.client import Client from pypsrp.client import Client
from impacket.smbconnection import SMBConnection
from impacket.examples.secretsdump import LocalOperations, LSASecrets, SAMHashes from impacket.examples.secretsdump import LocalOperations, LSASecrets, SAMHashes
from nxc.config import process_secret from nxc.config import process_secret
@ -18,6 +17,7 @@ from nxc.connection import connection
from nxc.helpers.bloodhound import add_user_bh from nxc.helpers.bloodhound import add_user_bh
from nxc.helpers.misc import gen_random_string from nxc.helpers.misc import gen_random_string
from nxc.logger import NXCAdapter from nxc.logger import NXCAdapter
from nxc.protocols.winrm.winrm_ntlm_parser import parse_challenge
urllib3.disable_warnings() urllib3.disable_warnings()
@ -33,58 +33,33 @@ class winrm(connection):
self.lmhash = "" self.lmhash = ""
self.nthash = "" self.nthash = ""
self.ssl = False self.ssl = False
self.auth_type = None self.challenge_header = None
connection.__init__(self, args, db, host) connection.__init__(self, args, db, host)
def proto_logger(self): def proto_logger(self):
# Reason why default is SMB/445, because default is enumerate over SMB.
# For more details, please check the function "print_host_info" # For more details, please check the function "print_host_info"
logging.getLogger("pypsrp").disabled = True logging.getLogger("pypsrp").disabled = True
logging.getLogger("pypsrp.wsman").disabled = True logging.getLogger("pypsrp.wsman").disabled = True
self.logger = NXCAdapter( self.logger = NXCAdapter(
extra={ extra={
"protocol": "SMB", "protocol": "WINRM",
"host": self.host, "host": self.host,
"port": "445", "port": "5985",
"hostname": self.hostname, "hostname": self.hostname,
} }
) )
def enum_host_info(self): def enum_host_info(self):
# smb no open, specify the domain ntlm_info = parse_challenge(base64.b64decode(self.challenge_header.split(' ')[1].replace(',', '')))
if self.args.no_smb: self.domain = ntlm_info["target_info"]["MsvAvDnsDomainName"]
self.domain = self.args.domain self.hostname = ntlm_info["target_info"]["MsvAvNbComputerName"]
else: self.server_os = f'Windows NT {ntlm_info["version"]}'
try: self.logger.extra["hostname"] = self.hostname
smb_conn = SMBConnection(self.host, self.host, None, timeout=5)
no_ntlm = False
except Exception as e:
self.logger.fail(f"Error retrieving host domain: {e} specify one manually with the '-d' flag")
else:
try:
smb_conn.login("", "")
except BrokenPipeError:
self.logger.fail("Broken Pipe Error while attempting to login")
except Exception as e:
if "STATUS_NOT_SUPPORTED" in str(e):
# no ntlm supported
no_ntlm = True
self.domain = smb_conn.getServerDNSDomainName() if not no_ntlm else self.args.domain self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}")
self.hostname = smb_conn.getServerName() if not no_ntlm else self.host
self.server_os = smb_conn.getServerOS()
if isinstance(self.server_os.lower(), bytes):
self.server_os = self.server_os.decode("utf-8")
self.logger.extra["hostname"] = self.hostname self.db.add_host(self.host, self.port, self.hostname, self.domain, self.server_os)
self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}")
with contextlib.suppress(Exception):
smb_conn.logoff()
self.db.add_host(self.host, self.port, self.hostname, self.domain, self.server_os)
if self.args.domain: if self.args.domain:
self.domain = self.args.domain self.domain = self.args.domain
@ -98,16 +73,10 @@ class winrm(connection):
self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-")) self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-"))
def print_host_info(self): def print_host_info(self):
if self.args.no_smb: self.logger.extra["protocol"] = "WINRM-SSL" if self.ssl else "WINRM"
self.logger.extra["protocol"] = "WINRM-SSL" if self.ssl else "WINRM" self.logger.extra["port"] = self.port
self.logger.extra["port"] = self.port self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain})")
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain})")
else:
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain})")
self.logger.extra["protocol"] = "WINRM-SSL" if self.ssl else "WINRM"
self.logger.extra["port"] = self.port
self.logger.info(f"Connection information: {self.endpoint} (auth type:{self.auth_type}) (domain:{self.domain if self.args.domain else ''})")
return True return True
def create_conn_obj(self): def create_conn_obj(self):
@ -117,6 +86,14 @@ class winrm(connection):
endpoints = {} endpoints = {}
headers = {
"Content-Length": "0",
"Keep-Alive": "true",
"Content-Type": "application/soap+xml;charset=UTF-8",
"User-Agent": "Microsoft WinRM Client",
"Authorization": "Negotiate TlRMTVNTUAABAAAAB4IIogAAAAAAAAAAAAAAAAAAAAAGAbEdAAAADw=="
}
for protocol in self.args.check_proto: for protocol in self.args.check_proto:
endpoints[protocol] = {} endpoints[protocol] = {}
endpoints[protocol]["port"] = self.port[self.args.check_proto.index(protocol)] if len(self.port) == 2 else self.port[0] endpoints[protocol]["port"] = self.port[self.args.check_proto.index(protocol)] if len(self.port) == 2 else self.port[0]
@ -131,9 +108,12 @@ class winrm(connection):
self.port = endpoints[protocol]["port"] self.port = endpoints[protocol]["port"]
try: try:
self.logger.debug(f"Requesting URL: {endpoints[protocol]['url']}") self.logger.debug(f"Requesting URL: {endpoints[protocol]['url']}")
res = requests.post(endpoints[protocol]["url"], verify=False, timeout=self.args.http_timeout) res = requests.post(endpoints[protocol]["url"], headers=headers, verify=False, timeout=self.args.http_timeout)
self.logger.debug(f"Received response code: {res.status_code}") self.logger.debug(f"Received response code: {res.status_code}")
self.auth_type = res.headers["WWW-Authenticate"] if "WWW-Authenticate" in res.headers else "NOAUTH" self.challenge_header = res.headers["WWW-Authenticate"]
if (not self.challenge_header) or (not 'Negotiate' in self.challenge_header):
self.logger.info('Failed to get NTLM challenge from target "/wsman" endpoint, maybe isn\'t winrm service.')
return False
self.endpoint = endpoints[protocol]["url"] self.endpoint = endpoints[protocol]["url"]
self.ssl = endpoints[protocol]["ssl"] self.ssl = endpoints[protocol]["ssl"]
return True return True

View File

@ -0,0 +1,100 @@
# Original from here: https://github.com/nopfor/ntlm_challenger
import datetime
def decoder(byte_string, decode_type):
if decode_type == "byte":
return byte_string.decode("UTF-8").replace("\x00", "")
else:
return int.from_bytes(byte_string, "little")
def parse_version(version_bytes):
major_version = version_bytes[0]
minor_version = version_bytes[1]
product_build = decoder(version_bytes[2:4], "int")
return f"{major_version}.{minor_version}.{product_build}"
def parse_target_info(target_info_bytes):
MsvAvEOL = 0x0000
MsvAvNbComputerName = 0x0001
MsvAvNbDomainName = 0x0002
MsvAvDnsComputerName = 0x0003
MsvAvDnsDomainName = 0x0004
MsvAvDnsTreeName = 0x0005
MsvAvFlags = 0x0006
MsvAvTimestamp = 0x0007
MsvAvSingleHost = 0x0008
MsvAvTargetName = 0x0009
MsvAvChannelBindings = 0x000A
target_info = {
"MsvAvNbComputerName": None,
"MsvAvDnsDomainName": None,
}
info_offset = 0
while info_offset < len(target_info_bytes):
av_id = decoder(target_info_bytes[info_offset:info_offset + 2], "int")
av_len = decoder(target_info_bytes[info_offset + 2:info_offset + 4], "int")
av_value = target_info_bytes[info_offset + 4:info_offset + 4 + av_len]
info_offset = info_offset + 4 + av_len
if av_id == MsvAvEOL:
pass
elif av_id == MsvAvNbComputerName:
target_info["MsvAvNbComputerName"] = decoder(av_value, "byte")
elif av_id == MsvAvNbDomainName:
target_info["MsvAvNbDomainName"] = decoder(av_value, "byte")
elif av_id == MsvAvDnsComputerName:
target_info["MsvAvDnsComputerName"] = decoder(av_value, "byte")
elif av_id == MsvAvDnsDomainName:
target_info["MsvAvDnsDomainName"] = decoder(av_value, "byte")
elif av_id == MsvAvDnsTreeName:
target_info["MsvAvDnsTreeName"] = decoder(av_value, "byte")
elif av_id == MsvAvFlags:
pass
elif av_id == MsvAvTimestamp:
filetime = decoder(av_value, "int")
microseconds = (filetime - 116444736000000000) / 10
time = datetime.datetime(1970, 1, 1) + datetime.timedelta(microseconds=microseconds)
target_info["MsvAvTimestamp"] = time.strftime("%b %d, %Y %H:%M:%S.%f")
elif av_id == MsvAvSingleHost:
target_info["MsvAvSingleHost"] = decoder(av_value, "byte")
elif av_id == MsvAvTargetName:
target_info["MsvAvTargetName"] = decoder(av_value, "byte")
elif av_id == MsvAvChannelBindings:
target_info["MsvAvChannelBindings"] = av_value
return target_info
def parse_challenge(challenge_message):
# TargetNameFields
target_name_fields = challenge_message[12:20]
target_name_len = decoder(target_name_fields[0:2], "int")
target_name_offset = decoder(target_name_fields[4:8], "int")
# TargetInfoFields
target_info_fields = challenge_message[40:48]
target_info_len = decoder(target_info_fields[0:2], "int")
target_info_offset = decoder(target_info_fields[4:8], "int")
# Version
version = None
version_bytes = challenge_message[48:56]
version = parse_version(version_bytes)
# TargetName
target_name = challenge_message[target_name_offset:target_name_offset + target_name_len]
target_name = decoder(target_name, "byte")
# TargetInfo
target_info_bytes = challenge_message[target_info_offset:target_info_offset + target_info_len]
target_info = parse_target_info(target_info_bytes)
return {
"target_name": target_name,
"version": version,
"target_info": target_info
}