
Python DPAPI NG Decryptor for non-Windows Platforms
Bibliothèque pour le dé- et le chiffrement de DPAPI NG, également connu sous le nom de CNG DPAPI, en Python. Elle est conçue pour reproduire le comportement de NCryptUnprotectSecret et NCryptProtectSecret. Elle peut être utilisée sur des hôtes non-Windows pour dé-/chiffrer des secrets protégés par DPAPI NG, comme le mot de passe protégé par l'utilisateur d'un PFX, ou le mot de passe chiffré de LAPS. Elle peut soit déchiffrer n'importe quel blob DPAPI NG à l'aide d'une copie hors ligne de la clé racine du domaine, soit dé-/chiffrer en utilisant les informations d'identification de l'utilisateur fourni pour récupérer les informations requises via RPC.
Actuellement, seuls ces descripteurs de protection sont pris en charge :
| Type | Objectif |
|---|---|
| SID | Seul l'utilisateur SID ou les membres du groupe SID peuvent déchiffrer le secret |
Ceci implémente le protocole MS-GKDI Group Key Distribution Protocol.
Pour installer dpapi-ng avec toutes les fonctionnalités de base, exécutez
python -m pip install dpapi-ng
La prise en charge de l'authentification Kerberos n'est pas installée par défaut car elle dépend des bibliothèques système et de la présence d'un compilateur valide. Les bibliothèques krb5 et le compilateur peuvent être installés en installant ces paquets :
# Debian/Ubuntu
apt-get install gcc python3-dev libkrb5-dev
# Centos/RHEL
yum install gcc python-devel krb5-devel
# Fedora
dnf install gcc python-devel krb5-devel
# Arch Linux
pacman -S gcc krb5
Une fois installé, les extras Python Kerberos peuvent être installés avec
python -m pip install dpapi-ng[kerberos]
Kerberos doit également être configuré pour communiquer avec le domaine, mais cela dépasse le cadre de cette page.
git clone https://github.com/jborean93/dpapi-ng.git
cd dpapi-ng
pip install -e .
Il existe à la fois une API synchrone et une API asyncio pour dé-/chiffrer un blob.
import dpapi_ng
### DÉCHIFFREMENT ###
dpapi_ng_blob = b"..."
decrypted_blob = dpapi_ng.ncrypt_unprotect_secret(dpapi_ng_blob)
# équivalent asynchrone de ce qui précède
decrypted_blob = await dpapi_ng.async_ncrypt_unprotect_secret(dpapi_ng_blob)
### CHIFFREMENT ###
data = b"..."
target_sid = "S-1-5-21-XXXXXXXXXX-XXXXXXXXXX-XXXXXXXXXX-XXXX"
dpapi_ng_blob = dpapi_ng.ncrypt_protect_secret(data, target_sid)
# équivalent asynchrone de ce qui précède
dpapi_ng_blob = await dpapi_ng.async_ncrypt_protect_secret(data, target_sid)
Pour déchiffrer le blob, la clé spécifiée dans le blob doit être récupérée auprès du contrôleur de domaine qui a généré le blob.
Pour chiffrer un blob, la clé de groupe du SID cible spécifié doit être récupérée.
Pour le déchiffrement, le nom d'hôte du contrôleur de domaine est automatiquement récupéré via une recherche DNS SRV de _ldap._tcp.dc._msdcs.{domain_name} avec le nom de domaine trouvé dans le blob DPAPI-NG ou avec la valeur spécifiée dans l'argument server.
Pour le chiffrement, le nom d'hôte du contrôleur de domaine est déterminé soit en utilisant l'argument domain_name pour une recherche DNS SRV, soit l'argument server directement, soit en utilisant le domaine de recherche du système (si disponible).
Il tentera de s'authentifier avec l'identifiant utilisateur actuel qui, sur Linux, n'existera que si kinit a déjà été appelé pour récupérer le ticket d'un utilisateur.
Sinon, si aucune identité n'est disponible, les arguments username et password peuvent être utilisés pour spécifier un utilisateur personnalisé.
Les arguments suivants peuvent être utilisés pour ncrypt_unprotect_secret, async_ncrypt_unprotect_secret, ncrypt_protect_secret et async_ncrypt_protect_secret.
server : Utiliser ce serveur comme cible RPC si une clé doit être récupéréeusername : Le nom d'utilisateur avec lequel s'authentifier pour la connexion RPCpassword : Le mot de passe avec lequel s'authentifier pour la connexion RPCauth_protocol : Le protocole d'authentification (negotiate, kerberos, ntlm) à utiliser pour la connexion RPCcache : Un cache pour stocker les clés récupérées pour une utilisation ultérieureEn plus de cela, ncrypt_protect_secret et async_ncrypt_protect_secret acceptent l'argument supplémentaire suivant.
domain_name : Le nom du domaine/forest à utiliser lors de la recherche de la cible RPC pour récupérer les informations de cléroot_key_identifier : L'UUID de l'identifiant de la clé racine, nécessaire pour faire fonctionner le cache pour ces fonctionsIl est également possible de chiffrer et de déchiffrer le blob DPAPI-NG en fournissant la clé racine stockée dans le domaine. Celle-ci peut être récupérée soit par une attaque hors ligne, soit via une requête LDAP si vous exécutez en tant qu'utilisateur Admin de domaine. Pour récupérer les clés racines du domaine à l'aide de PowerShell, la commande suivante peut être exécutée :
$configurationContext = (Get-ADRootDSE).configurationNamingContext
$getParams = @{
LDAPFilter = '(objectClass=msKds-ProvRootKey)'
SearchBase = "CN=Master Root Keys,CN=Group Key Distribution Service,CN=Services,$configurationContext"
SearchScope = 'OneLevel'
Properties = @(
'cn'
'msKds-KDFAlgorithmID'
'msKds-KDFParam'
'msKds-SecretAgreementAlgorithmID'
'msKds-SecretAgreementParam'
'msKds-PrivateKeyLength'
'msKds-PublicKeyLength'
'msKds-RootKeyData'
)
}
Get-ADObject @getParams | ForEach-Object {
[PSCustomObject]@{
Version = 1
RootKeyId = [Guid]::new($_.cn)
KdfAlgorithm = $_.'msKds-KDFAlgorithmID'
KdfParameters = [System.Convert]::ToBase64String($_.'msKds-KDFParam')
SecretAgreementAlgorithm = $_.'msKds-SecretAgreementAlgorithmID'
SecretAgreementParameters = [System.Convert]::ToBase64String($_.'msKds-SecretAgreementParam')
PrivateKeyLength = $_.'msKds-PrivateKeyLength'
PublicKeyLength = $_.'msKds-PublicKeyLength'
RootKeyData = [System.Convert]::ToBase64String($_.'msKds-RootKeyData')
}
}
La commande ldapsearch suivante peut être utilisée en dehors de Windows :
ldapsearch \
-b 'CN=Master Root Keys,CN=Group Key Distribution Service,CN=Services,CN=Configuration,DC=domain,DC=test' \
-s one \
'(objectClass=msKds-ProvRootKey)' \
cn \
msKds-KDFAlgorithmID \
msKds-KDFParam \
msKds-SecretAgreementAlgorithmID \
msKds-SecretAgreementParam \
msKds-PrivateKeyLength \
msKds-PublicKeyLength \
msKds-RootKeyData
Remarque : ldapsearch aura très probablement besoin des options -H et des informations de liaison utilisateur pour réussir.
Les informations récupérées peuvent être stockées dans un cache et utilisées pour les appels ultérieurs à ncrypt_protect_secret et ncrypt_unprotect_secret :
import uuid
import dpapi_ng
cache = dpapi_ng.KeyCache()
root_key_id = uuid.UUID("76ec8b2d-d444-4f67-9db7-2f62b4358b35")
cache.load_key(
b"...", # msKds-RootKeydata
root_key_id, # cn
version=1,
kdf_algorithm="SP800_108_CTR_HMAC", # msKds-KDFAlgorithmID
kdf_parameters=b"...", # msKds-KDFParam
secret_algorithm="DH", # mskds-SecretAgreementAlgorithmID
secret_parameters=b"...", # msKds-SecretAgreementParam
private_key_length=512, # msKds-PrivateKeyLength
public_key_length=2048, # msKds-PublicKeyLength
)
dpapi_ng.ncrypt_unprotect_secret(b"...", cache=cache)
Actuellement, l'algorithme KDF SP800_108_CTR_HMAC et les algorithmes d'accord secret DH, ECDH_P256 et ECDH_P384 ont été testés et fonctionnent.
L'algorithme d'accord secret ECDH_P521 devrait également fonctionner mais n'a pas été testé car un environnement de test ne peut pas être créé avec celui-ci pour le moment.
Je tiens à remercier les personnes suivantes (identifiants GitHub ou Twitter entre parenthèses) pour leur aide sur ce projet :
Sans leur patience et leurs connaissances, cela n'aurait probablement pas été possible.