Skip to content

Commit 072ca44

Browse files
authored
Merge pull request #95 from ShutdownRepo/tgssub
[tgssub.py] New example script: adding tgssub for SPN-jacking and manual sname manipulation
2 parents b3c19ad + a01fd0e commit 072ca44

File tree

1 file changed

+146
-0
lines changed

1 file changed

+146
-0
lines changed

examples/tgssub.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
#!/usr/bin/env python3
2+
# Impacket - Collection of Python classes for working with network protocols.
3+
#
4+
# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved.
5+
#
6+
# This software is provided under a slightly modified version
7+
# of the Apache Software License. See the accompanying LICENSE file
8+
# for more information.
9+
#
10+
# Description:
11+
# Python equivalent to Rubeus tgssub: Substitute an sname or SPN into an existing service ticket
12+
# New value can be of many forms
13+
# - (service class only) cifs
14+
# - (service class with hostname) cifs/service
15+
# - (service class with hostname and realm) cifs/[email protected]
16+
#
17+
# Authors:
18+
# Charlie Bromberg (@_nwodtuhs)
19+
20+
import logging
21+
import sys
22+
import traceback
23+
import argparse
24+
25+
26+
from impacket import version
27+
from impacket.examples import logger
28+
from impacket.krb5 import constants, types
29+
from impacket.krb5.asn1 import TGS_REP, Ticket
30+
from impacket.krb5.types import Principal
31+
from impacket.krb5.ccache import CCache, CountedOctetString
32+
from pyasn1.codec.der import decoder, encoder
33+
34+
def substitute_sname(args):
35+
ccache = CCache.loadFile(args.inticket)
36+
cred_number = len(ccache.credentials)
37+
logging.info('Number of credentials in cache: %d' % cred_number)
38+
if cred_number > 1:
39+
raise ValueError("More than one credentials in cache, this is not handled at the moment")
40+
credential = ccache.credentials[0]
41+
tgs = credential.toTGS()
42+
decodedST = decoder.decode(tgs['KDC_REP'], asn1Spec=TGS_REP())[0]
43+
tgs = ccache.credentials[0].toTGS()
44+
sname = decodedST['ticket']['sname']['name-string']
45+
if len(decodedST['ticket']['sname']['name-string']) == 1:
46+
logging.debug("Original sname is not formatted as usual (i.e. CLASS/HOSTNAME), automatically filling the substitution service will fail")
47+
logging.debug("Original sname is: %s" % sname[0])
48+
if '/' not in args.altservice:
49+
raise ValueError("Substitution service must include service class AND name (i.e. CLASS/HOSTNAME@REALM, or CLASS/HOSTNAME)")
50+
service_class, service_hostname = ('', sname[0])
51+
service_realm = decodedST['ticket']['realm']
52+
elif len(decodedST['ticket']['sname']['name-string']) == 2:
53+
service_class, service_hostname = decodedST['ticket']['sname']['name-string']
54+
service_realm = decodedST['ticket']['realm']
55+
else:
56+
logging.debug("Original sname is: %s" % '/'.join(sname))
57+
raise ValueError("Original sname is not formatted as usual (i.e. CLASS/HOSTNAME), something's wrong here...")
58+
if '@' in args.altservice:
59+
new_service_realm = args.altservice.split('@')[1].upper()
60+
if not '.' in new_service_realm:
61+
logging.debug("New service realm is not FQDN, you may encounter errors")
62+
if '/' in args.altservice:
63+
new_service_hostname = args.altservice.split('@')[0].split('/')[1]
64+
new_service_class = args.altservice.split('@')[0].split('/')[0]
65+
else:
66+
logging.debug("No service hostname in new SPN, using the current one (%s)" % service_hostname)
67+
new_service_hostname = service_hostname
68+
new_service_class = args.altservice.split('@')[0]
69+
else:
70+
logging.debug("No service realm in new SPN, using the current one (%s)" % service_realm)
71+
new_service_realm = service_realm
72+
if '/' in args.altservice:
73+
new_service_hostname = args.altservice.split('/')[1]
74+
new_service_class = args.altservice.split('/')[0]
75+
else:
76+
logging.debug("No service hostname in new SPN, using the current one (%s)" % service_hostname)
77+
new_service_hostname = service_hostname
78+
new_service_class = args.altservice
79+
if len(service_class) == 0:
80+
current_service = "%s@%s" % (service_hostname, service_realm)
81+
else:
82+
current_service = "%s/%s@%s" % (service_class, service_hostname, service_realm)
83+
new_service = "%s/%s@%s" % (new_service_class, new_service_hostname, new_service_realm)
84+
logging.info('Changing service from %s to %s' % (current_service, new_service))
85+
# the values are changed in the ticket
86+
decodedST['ticket']['sname']['name-string'][0] = new_service_class
87+
decodedST['ticket']['sname']['name-string'][1] = new_service_hostname
88+
decodedST['ticket']['realm'] = new_service_realm
89+
90+
ticket = encoder.encode(decodedST)
91+
credential.ticket = CountedOctetString()
92+
credential.ticket['data'] = encoder.encode(decodedST['ticket'].clone(tagSet=Ticket.tagSet, cloneValueFlag=True))
93+
credential.ticket['length'] = len(credential.ticket['data'])
94+
ccache.credentials[0] = credential
95+
96+
# the values need to be changed in the ccache credentials
97+
# we already checked everything above, we can simply do the second replacement here
98+
ccache.credentials[0]['server'].fromPrincipal(Principal(new_service, type=constants.PrincipalNameType.NT_PRINCIPAL.value))
99+
logging.info('Saving ticket in %s' % args.outticket)
100+
ccache.saveFile(args.outticket)
101+
102+
103+
def parse_args():
104+
parser = argparse.ArgumentParser(add_help=True, description='Substitute an sname or SPN into an existing service ticket')
105+
106+
parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON')
107+
parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output')
108+
parser.add_argument('-in', dest='inticket', action="store", metavar="TICKET.CCACHE", help='input ticket to modify', required=True)
109+
parser.add_argument('-out', dest='outticket', action="store", metavar="TICKET.CCACHE", help='output ticket', required=True)
110+
parser.add_argument('-altservice', action="store", metavar="SERVICE", help='New sname/SPN', required=True)
111+
parser.add_argument('-force', action='store_true', help='Force the service substitution without taking the original into consideration')
112+
113+
if len(sys.argv) == 1:
114+
parser.print_help()
115+
sys.exit(1)
116+
117+
args = parser.parse_args()
118+
return args
119+
120+
121+
def init_logger(args):
122+
# Init the example's logger theme and debug level
123+
logger.init(args.ts)
124+
if args.debug is True:
125+
logging.getLogger().setLevel(logging.DEBUG)
126+
# Print the Library's installation path
127+
logging.debug(version.getInstallationPath())
128+
else:
129+
logging.getLogger().setLevel(logging.INFO)
130+
logging.getLogger('impacket.smbserver').setLevel(logging.ERROR)
131+
132+
133+
def main():
134+
print(version.BANNER)
135+
args = parse_args()
136+
init_logger(args)
137+
138+
try:
139+
substitute_sname(args)
140+
except Exception as e:
141+
if logging.getLogger().level == logging.DEBUG:
142+
traceback.print_exc()
143+
logging.error(str(e))
144+
145+
if __name__ == '__main__':
146+
main()

0 commit comments

Comments
 (0)