├── KeytabParser.py ├── LICENSE └── README.md /KeytabParser.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Author Cody Thomas, @its_a_feature_ 3 | # May 21, 2019 4 | import binascii 5 | import sys 6 | import time 7 | import json 8 | import base64 9 | # endian value of 0x01 means native byte order (version one of keytab) 10 | # endian value of 0x02 means big-endian byte order (version two of keytab) 11 | # Format pulled from https://www.h5l.org/manual/HEAD/krb5/krb5_fileformats.html 12 | def usage(): 13 | print("Usage: python2.7 keytab_extract /path/to/keytab") 14 | 15 | def get_bytes_number(keytab, index, number, version): 16 | if (number*2) + index >= len(keytab): 17 | return 0 18 | if version == 1: 19 | #print(''.join(keytab[index:(number*2) + index])[::-2]) 20 | return int(''.join(keytab[index:(number*2) + index])[::-2], 16) 21 | if version == 2: 22 | return int(keytab[index:(number*2) + index], 16) 23 | 24 | def get_bytes_string(keytab, index, number): 25 | if (number*2) + index >= len(keytab): 26 | return str(0) 27 | return str(bytearray.fromhex(keytab[index:(number*2)+index])) 28 | 29 | def get_bytes_key(keytab, index, number): 30 | if (number*2) + index >= len(keytab): 31 | return 0 32 | return base64.b64encode(bytearray.fromhex(keytab[index:(number*2)+index])).decode('utf-8') 33 | 34 | enc_types = { 35 | 23: "rc4-hmac", 36 | 18: "aes256-cts-hmac-sha1-96", 37 | 17: "aes128-cts-hmac-sha1-96", 38 | 16: "des3-cbc-sha1-kd" 39 | } 40 | name_types = { 41 | 1: "KRB5_NT_PRINCIPAL", 42 | 2: "KRB5_NT_SRV_INST", 43 | 5: "KRB5_NT_UID" 44 | } 45 | 46 | 47 | def extract_keys(keytab): 48 | #print(keytab) 49 | #keytab metadata 50 | i = 0 51 | if get_bytes_number(keytab, index=i, number=1, version=1) != 5: 52 | print("Keytab files start with 0x05, this isn't formatted properly") 53 | sys.exit() 54 | i += 2 55 | if get_bytes_number(keytab, index=i, number=1, version=1) == 2: 56 | version = 2 57 | elif get_bytes_number(keytab, index=i, number=1, version=1) == 1: 58 | version = 1 59 | else: 60 | print("Second byte must be 0x01 or 0x02 to indicate byte ordering for integers") 61 | sys.exit() 62 | i += 2 63 | entries = {} 64 | #int32_t size of entry 65 | entry_length = get_bytes_number(keytab, index=i, number=4, version=version) 66 | #print("entry_length: {}".format(str(entry_length))) 67 | #print("entry: {}".format(keytab[i: i + (entry_length*2) ])) 68 | i += 8 69 | # iterate through entries 70 | while entry_length != 0: 71 | try: 72 | if entry_length > 0: 73 | start_value = i # start of this entry 74 | #uint16_t num_components 75 | num_components = get_bytes_number(keytab, index=i, number=2, version=version) 76 | 77 | #print("num_components: {}".format(str(num_components))) 78 | #print("entry_length: {}".format(str(entry_length))) 79 | if num_components == 0 and entry_length == 3: 80 | #print("num_comp is zero, next group: {}".format(keytab[i:])) 81 | continue 82 | i += 4 83 | #counted octect string realm (prefixed with 16bit length, no null terminator) 84 | realm_length = get_bytes_number(keytab, index=i, number=2, version=version) 85 | i += 4 86 | #print("realm_length: {}".format(str(realm_length))) 87 | if realm_length == 0: 88 | continue 89 | realm = get_bytes_string(keytab, index=i, number=realm_length) 90 | i += realm_length * 2 91 | #print(realm) 92 | spn = "" 93 | for component in range(num_components): 94 | component_length = get_bytes_number(keytab, index=i, number=2, version=version) 95 | i += 4 96 | spn += get_bytes_string(keytab, index=i, number=component_length) 97 | #print("spn piece: {}".format(spn)) 98 | i += component_length * 2 99 | spn += "/" 100 | spn = spn[:-1] + "@" + realm 101 | #print("full spn: {}".format(spn)) 102 | #uint32_t name_type 103 | name_type = get_bytes_number(keytab, index=i, number=4, version=version) 104 | i += 8 105 | #print("name_type: {}".format(str(name_type))) 106 | #print(name_types[name_type]) 107 | #uint32_t timestamp (time key was established) 108 | timestamp = get_bytes_number(keytab, index=i, number=4, version=version) 109 | i += 8 110 | #print("timestamp: {}".format(time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(timestamp)))) 111 | #uint8_t vno8 112 | vno = get_bytes_number(keytab, index=i, number=1, version=version) 113 | i += 2 114 | #print("vno: {}".format(str(vno))) 115 | #keyblock structure: 16bit value for encryption type and then counted_octet for key 116 | encryption_type = get_bytes_number(keytab, index=i, number=2, version=version) 117 | i += 4 118 | #print("encryption_type: {}".format(str(encryption_type))) 119 | #print(enc_types[encryption_type]) 120 | key_length = get_bytes_number(keytab, index=i, number=2, version=version) 121 | i += 4 122 | #print("key length: {}".format(str(key_length))) 123 | key = get_bytes_key(keytab, index=i, number=key_length) 124 | i += key_length * 2 125 | #print("key: {}".format(key)) 126 | #uint32_t vno if >=4 bytes left in entry_length 127 | if entry_length > i - start_value: 128 | vno = get_bytes_number(keytab, index=i, number=4, version=version) 129 | i += 8 130 | #print("updated vno: {}".format(str(updated_vno))) 131 | #uint32_t flags if >=4 bytes left in entry_length 132 | if entry_length > i - start_value: 133 | flags = get_bytes_number(keytab, index=i, number=4, version=version) 134 | i += 8 135 | #print("flags: {}".format(str(flags))) 136 | if spn in entries: 137 | entries[spn]['keys'].append({"EncType": enc_types[encryption_type], "Key": key, 'Time': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(timestamp)), "KeyLength": key_length}) 138 | else: 139 | entries[spn] = {'keys': [{"EncType": enc_types[encryption_type], "Key": key, 'Time': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(timestamp)), "KeyLength": key_length}]} 140 | else: 141 | i += abs(entry_length) * 2 142 | #print("skipping an entry") 143 | except Exception as e: 144 | print(e) 145 | finally: 146 | entry_length = get_bytes_number(keytab, index=i, number=4, version=version) 147 | #print("entry_length: {}".format(str(entry_length))) 148 | #print("entry: {}".format(keytab[i: i + (entry_length*2) ])) 149 | i += 8 150 | 151 | print(json.dumps(entries, indent=4, sort_keys=True)) 152 | 153 | if __name__ == "__main__": 154 | if len(sys.argv) == 1: 155 | usage() 156 | sys.exit() 157 | else: 158 | try: 159 | file = sys.argv[1] 160 | f = open(file, 'rb').read() 161 | keytab = str(binascii.hexlify(f)) 162 | #print(keytab) 163 | extract_keys(keytab) 164 | except Exception as e: 165 | print(str(e)) 166 | usage() 167 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, Cody Thomas 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KeytabParser 2 | Python script to parse macOS's Heimdal Keytab file (typically /etc/krb5.keytab) 3 | 4 | For additional information on Keytabs, APIs, and structures, check out the following: 5 | - https://www.h5l.org/manual/HEAD/krb5/krb5_ccache_intro.html 6 | - http://web.mit.edu/~kerberos/krb5-1.5/krb5-1.5.4/doc/krb5-admin/Kerberos-V5-Library-Error-Codes.html 7 | - https://docs.oracle.com/cd/E19683-01/816-1331/kerberrs-2/index.html 8 | - https://web.mit.edu/Kerberos/www/krb5-1.16/doc/appdev/refs/api/index.html 9 | - https://web.mit.edu/Kerberos/www/krb5-1.16/doc/appdev/refs/types/index.html 10 | - https://www.h5l.org/manual/heimdal-1-5-branch/krb5/index.html 11 | --------------------------------------------------------------------------------