NTLM auth-string decoder
December 17, 2014 ยท View on GitHub
#!/usr/bin/env python
Decodes NTLM "Authenticate" HTTP-Header blobs.
Reads the raw blob from stdin; prints out the contained metadata.
Supports (auto-detects) Type 1, Type 2, and Type 3 messages.
Based on the excellent protocol description from:
http://davenport.sourceforge.net/ntlm.html
with additional detail subsequently added from the official protocol spec:
http://msdn.microsoft.com/en-us/library/cc236621.aspx
For example:
$ echo "TlRMTVNTUAABAAAABYIIAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAwAAAA" | ./ntlmdecoder.py
Found NTLMSSP header
Msg Type: 1 (Request)
Domain: '' [] (0b @0)
Workstation: '' [] (0b @0)
OS Ver: '????0???'
Flags: 0x88205 ["Negotiate Unicode", "Request Target", "Negotiate NTLM", "Negotiate Always Sign", "Negotiate NTLM2 Key"]
import sys import base64 import struct import string import collections
flags_tbl_str = """0x00000001 Negotiate Unicode 0x00000002 Negotiate OEM 0x00000004 Request Target 0x00000008 unknown 0x00000010 Negotiate Sign 0x00000020 Negotiate Seal 0x00000040 Negotiate Datagram Style 0x00000080 Negotiate Lan Manager Key 0x00000100 Negotiate Netware 0x00000200 Negotiate NTLM 0x00000400 unknown 0x00000800 Negotiate Anonymous 0x00001000 Negotiate Domain Supplied 0x00002000 Negotiate Workstation Supplied 0x00004000 Negotiate Local Call 0x00008000 Negotiate Always Sign 0x00010000 Target Type Domain 0x00020000 Target Type Server 0x00040000 Target Type Share 0x00080000 Negotiate NTLM2 Key 0x00100000 Request Init Response 0x00200000 Request Accept Response 0x00400000 Request Non-NT Session Key 0x00800000 Negotiate Target Info 0x01000000 unknown 0x02000000 unknown 0x04000000 unknown 0x08000000 unknown 0x10000000 unknown 0x20000000 Negotiate 128 0x40000000 Negotiate Key Exchange 0x80000000 Negotiate 56""" flags_tbl = [line.split('\t') for line in flags_tbl_str.split('\n')] flags_tbl = [(int(x,base=16),y) for x,y in flags_tbl]
def flags_lst(flags): return [desc for val, desc in flags_tbl if val & flags] def flags_str(flags): return ', '.join('"%s"' % s for s in flags_lst(flags))
VALID_CHRS = set(string.letters + string.digits + string.punctuation) def clean_str(st): return ''.join((s if s in VALID_CHRS else '?') for s in st)
class StrStruct(object): def init(self, pos_tup, raw): length, alloc, offset = pos_tup self.length = length self.alloc = alloc self.offset = offset self.raw = raw[offset:offset+length] self.utf16 = False
if len(self.raw) >= 2 and self.raw[1] == '\0':
self.string = self.raw.decode('utf-16')
self.utf16 = True
else:
self.string = self.raw
def __str__(self):
st = "%s'%s' [%s] (%db @%d)" % ('u' if self.utf16 else '',
clean_str(self.string),
self.raw.encode('hex'),
self.length, self.offset)
if self.alloc != self.length:
st += " alloc: %d" % self.alloc
return st
msg_types = collections.defaultdict(lambda: "UNKNOWN") msg_types[1] = "Request" msg_types[2] = "Challenge" msg_types[3] = "Response"
target_field_types = collections.defaultdict(lambda: "UNKNOWN") target_field_types[0] = "TERMINATOR" target_field_types[1] = "Server name" target_field_types[2] = "AD domain name" target_field_types[3] = "FQDN" target_field_types[4] = "DNS domain name" target_field_types[5] = "Parent DNS domain" target_field_types[7] = "Server Timestamp"
def main(): st_raw = sys.stdin.read() try: st = base64.b64decode(st_raw) except e: print "Input is not a valid base64-encoded string" return
if st[:8] == "NTLMSSP\0":
print "Found NTLMSSP header"
else:
print "NTLMSSP header not found at start of input string"
return
ver_tup = struct.unpack("<i", st[8:12])
ver = ver_tup[0]
print "Msg Type: %d (%s)" % (ver, msg_types[ver])
if ver == 1:
pretty_print_request(st)
elif ver == 2:
pretty_print_challenge(st)
elif ver == 3:
pretty_print_response(st)
else:
print "Unknown message structure. Have a raw (hex-encoded) message:"
print st.encode("hex")
def opt_str_struct(name, st, offset): nxt = st[offset:offset+8] if len(nxt) == 8: hdr_tup = struct.unpack("<hhi", nxt) print "%s: %s" % (name, StrStruct(hdr_tup, st)) else: print "%s: [omitted]" % name
def opt_inline_str(name, st, offset, sz): nxt = st[offset:offset+sz] if len(nxt) == sz: print "%s: '%s'" % (name, clean_str(nxt)) else: print "%s: [omitted]" % name
def pretty_print_request(st): hdr_tup = struct.unpack("<i", st[12:16]) flags = hdr_tup[0]
opt_str_struct("Domain", st, 16)
opt_str_struct("Workstation", st, 24)
opt_inline_str("OS Ver", st, 32, 8)
print "Flags: 0x%x [%s]" % (flags, flags_str(flags))
def pretty_print_challenge(st): hdr_tup = struct.unpack("<hhiiQ", st[12:32])
print "Target Name: %s" % StrStruct(hdr_tup[0:3], st)
print "Challenge: 0x%x" % hdr_tup[4]
flags = hdr_tup[3]
opt_str_struct("Context", st, 32)
nxt = st[40:48]
if len(nxt) == 8:
hdr_tup = struct.unpack("<hhi", nxt)
tgt = StrStruct(hdr_tup, st)
output = "Target: [block] (%db @%d)" % (tgt.length, tgt.offset)
if tgt.alloc != tgt.length:
output += " alloc: %d" % tgt.alloc
print output
raw = tgt.raw
pos = 0
while pos+4 < len(raw):
rec_hdr = struct.unpack("<hh", raw[pos : pos+4])
rec_type_id = rec_hdr[0]
rec_type = target_field_types[rec_type_id]
rec_sz = rec_hdr[1]
subst = raw[pos+4 : pos+4+rec_sz]
print " %s (%d): %s" % (rec_type, rec_type_id, subst)
pos += 4 + rec_sz
opt_inline_str("OS Ver", st, 48, 8)
print "Flags: 0x%x [%s]" % (flags, flags_str(flags))
def pretty_print_response(st): hdr_tup = struct.unpack("<hhihhihhihhihhi", st[12:52])
print "LM Resp: %s" % StrStruct(hdr_tup[0:3], st)
print "NTLM Resp: %s" % StrStruct(hdr_tup[3:6], st)
print "Target Name: %s" % StrStruct(hdr_tup[6:9], st)
print "User Name: %s" % StrStruct(hdr_tup[9:12], st)
print "Host Name: %s" % StrStruct(hdr_tup[12:15], st)
opt_str_struct("Session Key", st, 52)
opt_inline_str("OS Ver", st, 64, 8)
nxt = st[60:64]
if len(nxt) == 4:
flg_tup = struct.unpack("<i", nxt)
flags = flg_tup[0]
print "Flags: 0x%x [%s]" % (flags, flags_str(flags))
else:
print "Flags: [omitted]"
if name == "main": main()