[dploot upgrade] Add --dpapi on WMI, WINRM and MSSQL protocols - #1327
[dploot upgrade] Add --dpapi on WMI, WINRM and MSSQL protocols#1327zblurx wants to merge 34 commits into
Conversation
Signed-off-by: zblurx <68540460+zblurx@users.noreply.github.com>
|
Perfecto @zblurx 🎉 |
NeffIsBack
left a comment
There was a problem hiding this comment.
This PR is actually a really good example on how to properly standardize file/registry access across protocols. We should probably implement something like that in NetExec as well to extend cross-protocol support for modules. E.g. cross-protocol support for spidering.
A few notes throughout the review:
- Maybe we should move all of the DPAPI logic into something like
/helpers/dpapi/so it has its own folder. Gets quite a lot :D - Firefox does not seem to be supported for WinRM at the moment. Was this intentional or is this a bug? Looking at the screenshot of @mpgn, looks like a bug related to my system?
Fyi, since we already have dpapi_machinekey detection logic I fixed this weird two-line output that impacket gives back as a string. Before&After (in one screenshot):
|
|
||
| # The dpapi function for every protocol | ||
| def triage_dpapi(self): | ||
| self.output_file = open(self.context.output_file_template.format(output_folder="dpapi"), "w", encoding="utf-8") # noqa: SIM115 |
There was a problem hiding this comment.
Is there a reason for opening the file here and closing&then reopening it down below? This looks like to me as if we could just remove that here
There was a problem hiding this comment.
The logic here is to open the handle, then all the triage functions can use it. The part that is weird is on 342-346 :
if self.output_file:
self.output_file.close()
with open(self.context.output_file_template.format(output_folder="dpapi")) as f:
if sum(1 for _ in f) == 0:
self.context.logger.fail("No dpapi loot retrieved")TBH I don't remember having implemented this, and I don't get the logic. I have blindly copy pasted this from old smb.py dpapi routine, but this needs to be rewritten
There was a problem hiding this comment.
I have added a counter to keep track if triage functions are giving anything.
There was a problem hiding this comment.
Hmm okay, imo we should then:
- Store the filename as a variable
- Everywhere that we want to write to the file simply do
with open(self.output_file) as f: f.write(xyz)
Then we have the same behaviour but without having file handles dangling around that are potentially never closed.
There was a problem hiding this comment.
You are right, it is cleaner. I'm working on it
There was a problem hiding this comment.
I have implemented a solution with a function to handle the output_file management in coordination with the counter. I moved the logging text with the output file name to a __del__ function to have it implemented in every DPAPI looting (not just the --dpapi stuff, all others too)
| # Then, use nxcdb to fill wordlists, this can help to decrypt some masterkeys | ||
| if dump_users: | ||
| plaintexts = {username: password for _, _, username, password, _, _ in self.context.db.get_credentials(cred_type="plaintext")} | ||
| nthashes = {username: nt.split(":")[1] if ":" in nt else nt for _, _, username, nt, _, _ in self.context.db.get_credentials(cred_type="hash")} | ||
| # dploot matches user.lower() | ||
| if self.context.password != "": | ||
| plaintexts[self.context.username.lower()] = self.context.password | ||
| if self.context.nthash != "": | ||
| nthashes[self.context.username.lower()] = self.context.nthash |
There was a problem hiding this comment.
We don't do this so far right? Honestly I am not sure how we would integrate it without potentially overloading systems. In my mind I am running around on a pentest with some nxcdb holding >5k nt hashes from my ntds dump. If I then run --dpapi (or something similar) against a large network range we would do AES encryptions for thousands of NT hashes against hundreds of hosts.
I think that is a bit too much since we are basically bruteforcing the master key files with the NT hashes as wordlist at this point. Maybe an integration with a specific flag could be possible, but other than that imo it's not feasible.
|
|
||
| # The dpapi function for every protocol | ||
| def triage_dpapi(self): | ||
| self.output_file = open(self.context.output_file_template.format(output_folder="dpapi"), "w", encoding="utf-8") # noqa: SIM115 |
There was a problem hiding this comment.
Hmm okay, imo we should then:
- Store the filename as a variable
- Everywhere that we want to write to the file simply do
with open(self.output_file) as f: f.write(xyz)
Then we have the same behaviour but without having file handles dangling around that are potentially never closed.
| # Now prepare the SYSTEM part | ||
| if (dump_system | ||
| and self.context.dpapi_system_key is None | ||
| and hasattr(self.context, "lsa") | ||
| and callable(self.context.lsa)): | ||
| # We can use the protocol specific LSA dump if not already dumped. | ||
| # But first, just making sure the protocol supports LSA dump :) | ||
| self.context.lsa() |
There was a problem hiding this comment.
Actually calling lsa() itself is very noisy. Maybe we should implement some def lsa(self, quiet=False) mode to mute the highlight log messages? Thoughts
There was a problem hiding this comment.
I agree with you, gonna work on something
There was a problem hiding this comment.
Implemented. For now, when quiet is True, there will be no log message generated by lsa() function at all. Maybe we still want the Dumping LSA Secrets message, so that the user know what is hapenning ?
There was a problem hiding this comment.
I added a display log showing that nxc is dumping LSA to get DPAPI SYSTEM keys, and showing the keys if successfully dumped, otherwise showing a fail log
| # Then, use nxcdb to fill wordlists, this can help to decrypt some masterkeys | ||
| if dump_users: | ||
| plaintexts = {username: password for _, _, username, password, _, _ in self.context.db.get_credentials(cred_type="plaintext")} | ||
| nthashes = {username: nt.split(":")[1] if ":" in nt else nt for _, _, username, nt, _, _ in self.context.db.get_credentials(cred_type="hash")} | ||
| # dploot matches user.lower() | ||
| if self.context.password != "": | ||
| plaintexts[self.context.username.lower()] = self.context.password | ||
| if self.context.nthash != "": | ||
| nthashes[self.context.username.lower()] = self.context.nthash |
There was a problem hiding this comment.
Just saw that this was always the case? Is this the default since the beginning? I guess if we already do it this way...
|
I think it's redundant to have a table to store DPAPI secrets and domain backup key in every protocol database. Maybe we should implement a dedicated dpapi database that would be shared between protocols ? Could be nice, especially for the domain backup key sharing between protocols. What do you think @NeffIsBack ? Maybe in another PR tho |
Each protocol is its own sqlite database file so you can run multiple protocols at the same time without them running into write issues, so we'd need to look into fixing that first. |
|
Yeah at the moment we only load the protocol specific database with no access to others, so there is no way of cross-protocol database access. I guess we could always load some additional "dpapi database" tho, but yeah additional PR if we would like to do that. Fyi, we need to revert #1378 in here so we always install dploot >4.0.0. |
Description
I recently worked on a big refacto of DPLoot in order to support multiple network protocols.
This PR implements the DPLoot upgrade, also with a big refacto of the DPAPI functionalities in Netexec.
Now, Netexec supports
--dpapiin smb (not new), wmi, winrm and mssql protocols (including the DPAPI related database tables for each of them).The
--sccmhas also been implemented on wmi protocolBig update on the DPAPI SYSTEM triage : if the user wants to dump SYSTEM secrets, and therefore the DPAPI SYSTEM Machine and User keys are required, NetExec now calls
lsa()function of the network protocol automatically (as you can see in the screenshots). Unfortunately, wmi protocol does not have a pure WMI LSA dump, so no automatic DPAPI SYSTEM keys retrieval in WMI.A few modules have also been implemented in multiple protocols :
dpapi_hash,mobaxterm,mremoteng,rdcman,vnc,wamandwifiThe code has also been refactored in order to implement a better looting methodology : in the case you use multiple DPAPI related functionalities in one command (for example
nxc wmi [....] --dpapi -M wam -M rdcman), masterkeys will be dumped once.You will notice that the dependency is linked to a custom branch of dploot. I prefer it to keep it that way for the testing phase of the PR. Once everything will be validated, I'll push a clean release of dploot and will update the pyproject.toml
Type of change
Insert an "x" inside the brackets for relevant items (do not delete options)
Setup guide for the review
Install GOAD lab and add up a few secrets on some servers
Screenshots (if appropriate):
Checklist:
Insert an "x" inside the brackets for completed and relevant items (do not delete options)
poetry run ruff check ., use--fixto automatically fix what it can)tests/e2e_commands.txtfile if necessary (new modules or features are required to be added to the e2e tests)