#!/usr/bin/python #Copyright 2008, William Stearns #Passer is a PASsive SERvice sniffer. #Home site http://www.stearns.org/passer/ #Dedicated to Mae Anne Laroche. #Released under the GPL. #Version 1.3 #======== Imports ======== import sys import re #This may be too restrictive. Damn. from scapy import sniff, p0f, sr1, IP, ICMP, IPerror, TCPerror, UDPerror, ICMPerror #from scapy import * import os #======== Global arrays ======== #These two are used to discover servers. If we've seen a SYN go to a port, and a SYN/ACK back from it, #that's a pretty good sign it's a server. Not truly stateful, but a generally good guess. SynSentToTCPService = { } #Boolean dictionary: Have we seen a syn sent to this "IP,Proto_Port" pair yet? LiveTCPService = { } #Boolean dictionary: Have we seen a SYN/ACK come back (true) or a RST (False) from this "IP,Proto_Port" pair? #Next two are used to discover clients. If we've seen a SYN/ACK going to what appears to be a client port, and it #later responds with a FIN, we'll call that a live TCP client. SynAckSentToTCPClient = { } #Boolean dictionary: Have we seen a SYN/ACK sent to this "IP,Proto_Port" pair yet? LiveTCPClient = { } #Boolean dictionary: Have we seen a FIN from this client, indicating a 3 way handshake and successful conversation? #And finally UDP. LiveUDPService = { } #Boolean dictionary: we've found a server here at "IP,Proto_Port" LiveUDPClient = { } #Boolean dictionary: we've found a udp client here at "IP,Proto_Port". Like TCP, this is client IP and _server_ port. NmapServerDescription = { } #String dictionary: What server is this "IP,Proto_Port" pair? These descriptions come from nmap-service-probes. ManualServerDescription = { } #Same as above, but locally found strings ClientDescription = { } #String dictionary: What client is on this "IP,Proto_Port"? NOTE: the port here is the _server_ port at the other end. So if #Firefox on 1.2.3.4 is making outbound connections to port 80 on remote servers, ClientDescription['1.2.3.4,TCP_80'] = "http/firefox" OSDescription = { } #String dictionary: What OS is this IP key? MacAddr = { } #String dictionary: For a given IP (key), what is its mac (value)? EtherManuf = { } #String dictionary: for a given key of the first three uppercase octets of a mac address ("00:01:0F"), who made this card? LogFileName = '/var/tmp/passer-log' ServiceFPs = { } #Dictionary of service fingerprints. Keys are straight int port numbers (no TCP or UDP), or 'all' for strings that need #to be matched against all ports. These are loaded from nmap's "nmap-service-probes", ignoring the probes since we're passive. #Values are lists of tuples, ala: [("Apache *server ready.", "Apache web"), ("VSFTPD FTP at your service", "vsftpd ftp")] #Note that the first object in a tuple is a _compiled regex_ rather than the printable strings I've used above. #A sample (non-compiled) version looks like: {80: [('^Server: Apache/', 'http/apachewebserver')]} Devel = False #For my internal use to look for new service strings #This payload logging is disabled when Devel == False #Quite likely a security risk, I don't recommend enabling it. ServerPayloadDir = '/var/tmp/passer-server/' ClientPayloadDir = '/var/tmp/passer-client/' #======== Functions ======== def Debug(DebugStr): """Prints a note to stderr""" sys.stderr.write(DebugStr + '\n') def LogNewPayload(PayloadDir, PayloadFile, Payload): """Saves the payload from an ack packet to a file named after the server or client port involved.""" global Devel if (Devel == True): if os.path.isdir(PayloadDir): if (not Payload == "None"): pfile=open(PayloadFile, 'a') pfile.write(Payload) pfile.close() def LoadMacData(MacFile): """Load Ethernet Mac address prefixes from standard locations (from ettercap, nmap, wireshark, and/or arp-scan).""" global EtherManuf More='' if (len(EtherManuf) > 0): More=' more' LoadCount = 0 if os.path.isfile(MacFile): try: MacHandle=open(MacFile, 'r') for line in MacHandle: if (len(line) >= 8) and (line[2] == ':') and (line[5] == ':'): #uppercase incoming strings just in case one of the files uses lowercase MacHeader=line[:8].upper() Manuf=line[8:].strip() if (not EtherManuf.has_key(MacHeader)): EtherManuf[MacHeader] = Manuf LoadCount += 1 #elif (string.hexdigits.find(line[0]) > -1) and (string.hexdigits.find(line[1]) > -1) ...: #Nah. :-) elif (len(line) >= 7) and (re.search('^[0-9A-F]{6}[ \t]', line) <> None): MacHeader=str.upper(line[0:2] + ':' + line[2:4] + ':' + line[4:6]) Manuf=line[7:].strip() if (not EtherManuf.has_key(MacHeader)): EtherManuf[MacHeader] = Manuf LoadCount += 1 MacHandle.close() if EtherManuf.has_key('00:00:00'): del EtherManuf['00:00:00'] #Not really Xerox LoadCount -= 1 Debug(str(LoadCount) + More + " mac prefixes loaded from" + str(MacFile)) return True except: Debug("Unable to load " + str(MacFile)) return False else: Debug("Unable to load " + str(MacFile)) return False def LoadNmapServiceFP(ServiceFileName): """Load nmap fingerprints from nmap-service-probes, usually in /usr/share/nmap.""" global ServiceFPs LoadCount = 0 CompileSuccess = 0 CompileFail = 0 PortList = "" PortArray = [ ] if os.path.isfile(ServiceFileName): try: ServiceHandle = open(ServiceFileName, "r") for line in ServiceHandle: if (len(line) >= 5) and (line[0:6] == "Probe "): #print "==== PROBE ====" PortArray = [ ] #print len(PortArray), PortArray #len of empty array is 0 elif (len(line) >= 5) and (line[0:6] == "match "): #print "match" #print line #Sample line: # match srun m|^X\0\0\0$| p/Caucho Resin JSP Engine srun/ Remainder=line[6:].strip() # srun m|^X\0\0\0$| p/Caucho Resin JSP Engine srun/ MatchStart=Remainder.find(" m") # 4 ProtoString=Remainder[:MatchStart].replace(',', ';') # srun #At the moment, nmap-service-probes uses these separators: #3 m%, 2 m+, 126 m/, 29 m=, 2 m@, and 3509 m| #No flags on %, +, #Only flags should be "i" (case-insensitive) and "s" ("." can match newline) Separator=Remainder[MatchStart+2:MatchStart+3] # | MatchEnd=Remainder.find(Separator,MatchStart+3) # 16 MatchString=Remainder[MatchStart+3:MatchEnd] # ^X\0\0\0$ #Substitute ; for , in ProtoString and ServerDescription since we're using commas as field delimiters in output #FIXME - search for next space after 2nd separator instead of assuming it's followed by " p/", might be an "i" or "s" flag after separator ServerDescription=Remainder[MatchEnd+2:].replace(',', ';') # p/Caucho Resin JSP Engine srun/ #The nmap-service-probes file uses a character set ("[...]") issue that python doesn't like. #If a "-" is used inside a character set, it should either be in the first or last position, #or used in a character range ("[.....a-z.....]"). The following move any dashes to first or #last position so re.compile is happy. MatchString=MatchString.replace("[\w-","[-\w") #The dash needs to be at the end or it's treated as a range specifier MatchString=MatchString.replace("[\d-","[-\d") #same MatchString=MatchString.replace("[\w\d-_.]","[\w\d_.-]") #and so on... MatchString=MatchString.replace("[\w\d-_]","[\w\d_-]") MatchString=MatchString.replace("[.-\w]","[.\w-]") MatchString=MatchString.replace("[\s-\w.,]","[\s\w.,-]") MatchString=MatchString.replace("[\w\d-.]","[\w\d.-]") MatchString=MatchString.replace("[\d\.-\w]","[\d\.\w-]") MatchString=MatchString.replace("[^-A-Z0-9]","[^A-Z0-9-]") #Rest are replaced by the first two generic "[\w-" and "[\d-" rules #MatchString=MatchString.replace("[\w-_.]","[\w_.-]") #MatchString=MatchString.replace("[\w-.]","[\w.-]") #MatchString=MatchString.replace("[\d-.]","[\d.-]") #MatchString=MatchString.replace("[\w-_. ]","[\w_. -]") #MatchString=MatchString.replace("[\w-_.@]","[\w_.@-]") #MatchString=MatchString.replace("[\w-_.\s]","[\w_.\s-]") #MatchString=MatchString.replace("[\w-_]","[\w_-]") #MatchString=MatchString.replace("[\d-_.]","[\d_.-]") #MatchString=MatchString.replace("[\w-_.)(/]","[\w_.)(/-]") #MatchString=MatchString.replace("[\w-_+. ]","[\w_+. -]") #MatchString=MatchString.replace("[\w-_.+]","[\w_.+-]") #MatchString=MatchString.replace("[\w-_+. ()]","[\w_+. ()-]") #MatchString=MatchString.replace("[\w-+.]","[\w+.-]") #MatchString=MatchString.replace("[\w-.+]","[\w.+-]") #MatchString=MatchString.replace("[\w-_./]","[\w_./-]") #MatchString=MatchString.replace("[\w-+]","[\w+-]") #MatchString=MatchString.replace("[\w-+/.]","[\w+/.-]") #MatchString=MatchString.replace("[\w-_.:/]","[\w_.:/-]") #MatchString=MatchString.replace("[\w-_.;]","[\w_.;-]") #MatchString=MatchString.replace("[\d-/]","[\d/-]") #MatchString=MatchString.replace("[\w-. ]","[\w. -]") if (ServerDescription.find('Skype VoIP data channel') > -1): #This "14 bytes of random stuff" signature way misfires. pass elif (ServerDescription.find('Microsoft Distributed Transaction Coordinator') > -1): #This "ERROR" signature matches other protocols. pass #Fixed these regex issues above with replaces. #elif (MatchString.find(' CDDBP server ') > -1): # #Regex issue; "-" in [] character sets is not at the beginning or end: "[\w-_.]" should be "[\w_.-]" # pass #elif (MatchString.find(' running FileZilla Server ') > -1): # #Regex issue; "-" in [] character sets is not at the beginning or end # pass #elif (MatchString.find('220-SECURE FTP SERVER VERSION') > -1): # #Regex issue; "-" in [] character sets is not at the beginning or end # pass else: try: #We try to compile the MatchString now before inserting into ServiceFPs so the work only needs to be #done once. If this fails - and I don't know why 413 out of 3671 match lines fail to compile - #we fall down to the except and simply don't use the tuple. Hmmm. SearchTuple=(re.compile(MatchString, re.M), ProtoString + "://" + ServerDescription) CompileSuccess += 1 if (len(PortArray) == 0): #No ports declared yet; we'll place this search pair under the special port "all" if (not(ServiceFPs.has_key('all'))): ServiceFPs['all'] = [ ] ServiceFPs['all'].append(SearchTuple) LoadCount += 1 else: #Register this search pair for every port requested for OnePort in PortArray: if (not(ServiceFPs.has_key(int(OnePort)))): ServiceFPs[int(OnePort)] = [ ] ServiceFPs[int(OnePort)].append(SearchTuple) LoadCount += 1 except: #print "Failed to compile " + MatchString CompileFail += 1 elif (len(line) >= 5) and (line[0:6] == "ports "): PortArray = [ ] RawPortsString=line[6:].strip() #print "ports are ", RawPortsString for PortBlock in RawPortsString.split(","): #Each PortBlock is either an individual port or port range if (PortBlock.find("-") > -1): #We have a port range PortRange=PortBlock.split("-") for OnePort in range(int(PortRange[0]), int(PortRange[1]) + 1): PortArray.append(OnePort) else: PortArray.append(PortBlock) #print len(PortArray), PortArray elif (len(line) >= 9) and (line[0:10] == "softmatch "): pass #softmatches look very weak at the moment; none give a productname. Skip for the moment. #print "softmatch" ServiceHandle.close() Debug(str(CompileSuccess) + " successful compiles, " + str(CompileFail) + " failures.") return True except: Debug("Failed to load " + ServiceFileName) return False else: Debug("Unable to find " + ServiceFileName) return False def ReportId(Type, IPAddr, Proto, State, Description): """Print and log a new piece of network information.""" #Can't use : for separator, IPv6, similarly '.' for ipv4 #Can't use "/" because of filesystem #Don't want to use space because of filesystem # Type, IPAddr, Proto State Optional description (may be empty) # 'IP', IPaddr, 'IP', dead or live, p0f OS description # 'MA', IPaddr, 'Ethernet', MacAddr, ManufDescription # 'TC', IPaddr, 'TCP_'Port, closed or open, client description # 'TS', IPaddr, 'TCP_'Port, closed or listening, server description # 'UC', IPaddr, 'UDP_'Port, open or closed, udp client port description # 'US', IPaddr, 'UDP_'Port, open or closed, udp server port description global ServerDescription global ClientDescription global MacAddr global EtherManuf global LogFile global OSDescription #No longer needed #global MuteWarned Location = IPAddr + "," + Proto if (Type == "TS"): pass #Only assign this (and the others in this function) if Description non-null #if (Description != ''): # #FIXME - assign externally to the right *ServerDescription # ServerDescription[Location] = Description elif (Type == "TC"): if (Description != ''): ClientDescription[Location] = Description elif (Type == "US"): if (Description != ''): ManualServerDescription[Location] = Description elif (Type == "UC"): if (Description != ''): ClientDescription[Location] = Description elif (Type == "IP"): if (Description != ''): OSDescription[IPAddr] = Description elif (Type == "MA"): State = State.upper() MacAddr[IPAddr] = State if EtherManuf.has_key(State[:8]): Description = EtherManuf[State[:8]] OutString = Type + "," + IPAddr + "," + Proto + "," + State + "," + Description print OutString LogFile.write(OutString + '\n') LogFile.flush() def processpacket(p): """Extract information from a single packet off the wire.""" global SynSentToTCPService global SynAckSentToTCPClient global LiveTCPService global LiveTCPClient global LiveUDPService global LiveUDPClient global NmapServerDescription global ManualServerDescription global ClientDescription global MacAddr global OSDescription global ServiceFPs global SipPhoneMatch if p['Ethernet.type'] == 0x0806: #ARP #pull arp data from here instead of tcp/udp packets, as these are all local if (p['ARP.op'] == 2): #1 is request ("who-has"), 2 is reply ("is-at") IPAddr=p['ARP.psrc'] MyMac=p['ARP.hwsrc'].upper() if (not MacAddr.has_key(IPAddr)) or (MacAddr[IPAddr] != MyMac): ReportId("MA", IPAddr, 'Ethernet', MyMac, '') elif p['Ethernet.type'] == 0x0800: #IP sIP=str(p['IP.src']) dIP=str(p['IP.dst']) #Best to get these from arps instead; if we get them from here, we get router macs for foreign addresses. #if not MacAddr.has_key(sIP): # ReportId("MA", sIP, "Ethernet", p['Ethernet.src'], '') #if not MacAddr.has_key(dIP): # ReportId("MA", dIP, "Ethernet", p['Ethernet.dst'], '') if p['IP.proto'] == 1: #ICMP pass ##print "ICMP" Type = p['ICMP.type'] Code = p['ICMP.code'] if (Type == 3): #Unreachable if (Code == 1): #Host unreachable pass #p.show() #print p #Just gives a binary blob #scapy.dir(scapy.p) #Can't seem to find the right syntax #print p.keys() #Nope #help(p) #interactive help on structures #print p.fields_desc #Just Ether fields #print p.IP.__dict__ #Nope #print p['IP'].__dict__ #Like IP.__dict__, but focused on IP layer #print p['IPerror'].__dict__ #Fails, type None #print p['ICMP'].__dict__ #Focuses on ICMP #print p['IPerror.dst'], "unreachable." #quit() #else: # p.show() #print p['ICMP.type'], p['ICMP.code'] ##Nope print p['ICMP.payload.IP.proto'] #print p['Raw.load'] #OK ##print p['Raw.load.IP.proto'] #Nope #print 'portcheck:' + str(p['UDPerror.sport']) + ":", p['IPerror.proto'] #It doesn't look like IPerror/UDPerror fields actually get values. Hmmmmm. #But oddly enough, p.show does seem to _show_ the values. Hmmmm. #Stripme once we have UDPerror fields #sport=str(p['UDP.sport']) #dport=str(p['UDP.dport']) #Service = sIP + ",UDP_" + sport #if (sport == "53") and (p['DNS.qr'] == 1): #For some reason I can't check: (p.has_key('DNS.qr')) # if ((not LiveUDPService.has_key(Service)) or (LiveUDPService[Service] == False)): # LiveUDPService[Service] = True # ReportId("US", sIP, "UDP_" + sport, 'open', "dns/generic") elif p['IP.proto'] == 2: #IGMP pass elif p['IP.proto'] == 6: #TCP sport=str(p['TCP.sport']) dport=str(p['TCP.dport']) #print p['IP.src'] + ":" + sport + " -> ", p['IP.dst'] + ":" + dport, if (p['TCP.flags'] & 0x17) == 0x12: #SYN/ACK (RST and FIN off) CliService = dIP + ",TCP_" + sport if not SynAckSentToTCPClient.has_key(CliService): SynAckSentToTCPClient[CliService] = True #If we've seen a syn sent to this port and have either not seen any SA/R, or we've seen a R in the past: #The last test is for a service that was previously closed and is now open; report each transition once. Service = sIP + ",TCP_" + sport if ( (SynSentToTCPService.has_key(Service)) and ((not LiveTCPService.has_key(Service)) or (LiveTCPService[Service] == False)) ): LiveTCPService[Service] = True ReportId("TS", sIP, "TCP_" + sport, "listening", '') elif (p['TCP.flags'] & 0x17) == 0x02: #SYN (ACK, RST, and FIN off) Service = dIP + ",TCP_" + dport if not SynSentToTCPService.has_key(Service): SynSentToTCPService[Service] = True #Debug("trying to fingerprint " + sIP) try: if (len(p0f(p)) >=1): PDescription = p0f(p)[0][0] + " " + p0f(p)[0][1] #FIXME - Grabbing just the first candidate, may need to compare correlation values; provided? PDescription = PDescription.replace(',', ';') #Commas are delimiters in output if (not(OSDescription.has_key(sIP))) or (OSDescription[sIP] != PDescription): OSDescription[sIP] = PDescription ReportId("IP", sIP, "IP", "live", PDescription) except: PDescription = 'p0f failure' if (not(OSDescription.has_key(sIP))) or (OSDescription[sIP] != PDescription): Debug("P0f failure in " + sIP + ":" + sport + " -> " + dIP + ":" + dport) OSDescription[sIP] = PDescription ReportId("IP", sIP, "IP", "live", PDescription) elif (p['TCP.flags'] & 0x07) == 0x01: #FIN (SYN/RST off) CliService = sIP + ",TCP_" + dport if ( (SynAckSentToTCPClient.has_key(CliService)) and ((not LiveTCPClient.has_key(CliService)) or (LiveTCPClient[CliService] == False)) ): LiveTCPClient[CliService] = True ReportId("TC", sIP, "TCP_" + dport, "open", '') elif (p['TCP.flags'] & 0x07) == 0x04: #RST (SYN and FIN off) #FIXME - handle rst going in the other direction? Service = sIP + ",TCP_" + sport if ( (SynSentToTCPService.has_key(Service)) and ((not LiveTCPService.has_key(Service)) or (LiveTCPService[Service] == True)) ): LiveTCPService[Service] = False ReportId("TS", sIP, "TCP_" + sport, "closed", '') elif (p['TCP.flags'] & 0x17) == 0x10: #ACK (RST, SYN, and FIN off) FromPort = sIP + ",TCP_" + sport ToPort = dIP + ",TCP_" + dport Payload = str(p['Raw.load']) if ( (LiveTCPService.has_key(FromPort)) and (LiveTCPService[FromPort] == True) and (LiveTCPService.has_key(ToPort)) and (LiveTCPService[ToPort] == True)): print "Logic failure: both " + FromPort + " and " + ToPort + " are listed as live services." elif ((LiveTCPService.has_key(FromPort)) and (LiveTCPService[FromPort] == True)): #If the "From" side is a known TCP server: if (not NmapServerDescription.has_key(FromPort) ): #Check nmap fingerprint strings for this server port if (ServiceFPs.has_key(int(sport))): for OneTuple in ServiceFPs[int(sport)]: MatchObj = OneTuple[0].search(Payload) if (MatchObj != None): OutputDescription = OneTuple[1] if len(MatchObj.groups()) >= 1: #We have subexpressions matched, these need to be inserted into the description string for Index in range(1,len(MatchObj.groups())+1): #Example: Replace "$1" with MatchObj.group(1) OutputDescription = OutputDescription.replace('$' + str(Index), MatchObj.group(Index)) ReportId("TS", sIP, "TCP_" + sport, "listening", OutputDescription) NmapServerDescription[sIP + ",TCP_" + sport] = OutputDescription break #Exit for loop, no need to check any more fingerprints now that we've found a match if (not NmapServerDescription.has_key(FromPort)): #If the above loop didn't find a server description if (ServiceFPs.has_key('all')): #Now recheck against regexes not associated with a specific port (port 'all'). for OneTuple in ServiceFPs['all']: MatchObj = OneTuple[0].search(Payload) if (MatchObj != None): OutputDescription = OneTuple[1] if len(MatchObj.groups()) >= 1: #We have subexpressions matched, these need to be inserted into the description string for Index in range(1,len(MatchObj.groups())+1): OutputDescription = OutputDescription.replace('$' + str(Index), MatchObj.group(Index)) ReportId("TS", sIP, "TCP_" + sport, "listening", OutputDescription) NmapServerDescription[sIP + ",TCP_" + sport] = OutputDescription break if (not ManualServerDescription.has_key(FromPort) ): if (sport == "22") and (Payload.find('SSH-') > -1): if ( (Payload.find('SSH-1.99-OpenSSH_') > -1) or (Payload.find('SSH-2.0-OpenSSH_') > -1) ): ReportId("TS", sIP, "TCP_" + sport, "listening", "ssh/openssh") ManualServerDescription[sIP + ",TCP_" + sport] = "ssh/openssh" elif (Payload.find('SSH-1.5-') > -1): ReportId("TS", sIP, "TCP_" + sport, "listening", "ssh/generic") ManualServerDescription[sIP + ",TCP_" + sport] = "ssh/generic" #LogNewPayload(ServerPayloadDir, FromPort, Payload) else: LogNewPayload(ServerPayloadDir, FromPort, Payload) elif (sport == "25") and (Payload.find(' ESMTP Sendmail ') > -1): ReportId("TS", sIP, "TCP_" + sport, "listening", "smtp/sendmail") ManualServerDescription[sIP + ",TCP_" + sport] = "smtp/sendmail" elif (sport == "25") and (Payload.find(' - Welcome to our SMTP server ESMTP') > -1): ReportId("TS", sIP, "TCP_" + sport, "listening", "smtp/generic") ManualServerDescription[sIP + ",TCP_" + sport] = "smtp/generic" LogNewPayload(ServerPayloadDir, FromPort, Payload) #Check for port 80 and search for "Server: " once elif (sport == "80") and (Payload.find('Server: ') > -1): if (Payload.find('Server: Apache') > -1): ReportId("TS", sIP, "TCP_" + sport, "listening", "http/apache") ManualServerDescription[sIP + ",TCP_" + sport] = "http/apache" elif (Payload.find('Server: Embedded HTTP Server') > -1): ReportId("TS", sIP, "TCP_" + sport, "listening", "http/embedded") ManualServerDescription[sIP + ",TCP_" + sport] = "http/embedded" elif (Payload.find('Server: gws') > -1): ReportId("TS", sIP, "TCP_" + sport, "listening", "http/gws") ManualServerDescription[sIP + ",TCP_" + sport] = "http/gws" elif (Payload.find('Server: KFWebServer') > -1): ReportId("TS", sIP, "TCP_" + sport, "listening", "http/kfwebserver") ManualServerDescription[sIP + ",TCP_" + sport] = "http/kfwebserver" elif (Payload.find('Server: micro_httpd') > -1): ReportId("TS", sIP, "TCP_" + sport, "listening", "http/micro-httpd") ManualServerDescription[sIP + ",TCP_" + sport] = "http/micro-httpd" elif (Payload.find('Server: Microsoft-IIS') > -1): ReportId("TS", sIP, "TCP_" + sport, "listening", "http/iis") ManualServerDescription[sIP + ",TCP_" + sport] = "http/iis" elif (Payload.find('Server: lighttpd') > -1): ReportId("TS", sIP, "TCP_" + sport, "listening", "http/lighttpd") ManualServerDescription[sIP + ",TCP_" + sport] = "http/lighttpd" elif (Payload.find('Server: MIIxpc') > -1): ReportId("TS", sIP, "TCP_" + sport, "listening", "http/mirrorimage") ManualServerDescription[sIP + ",TCP_" + sport] = "http/mirrorimage" elif (Payload.find('Server: mini_httpd') > -1): ReportId("TS", sIP, "TCP_" + sport, "listening", "http/mini-httpd") ManualServerDescription[sIP + ",TCP_" + sport] = "http/mini-httpd" elif (Payload.find('Server: nc -l -p 80') > -1): ReportId("TS", sIP, "TCP_" + sport, "listening", "http/nc") ManualServerDescription[sIP + ",TCP_" + sport] = "http/nc" elif (Payload.find('Server: nginx/') > -1): ReportId("TS", sIP, "TCP_" + sport, "listening", "http/nginx") ManualServerDescription[sIP + ",TCP_" + sport] = "http/nginx" elif (Payload.find('Server: Nucleus') > -1): ReportId("TS", sIP, "TCP_" + sport, "listening", "http/nucleus") ManualServerDescription[sIP + ",TCP_" + sport] = "http/nucleus" elif (Payload.find('Server: RomPager') > -1): ReportId("TS", sIP, "TCP_" + sport, "listening", "http/rompager") ManualServerDescription[sIP + ",TCP_" + sport] = "http/rompager" elif (Payload.find('Server: Server') > -1): ReportId("TS", sIP, "TCP_" + sport, "listening", "http/server") ManualServerDescription[sIP + ",TCP_" + sport] = "http/server" elif (Payload.find('Server: Sun-ONE-Web-Server/') > -1): ReportId("TS", sIP, "TCP_" + sport, "listening", "http/sun-one") ManualServerDescription[sIP + ",TCP_" + sport] = "http/sun-one" elif (Payload.find('Server: TrustRank Frontend') > -1): ReportId("TS", sIP, "TCP_" + sport, "listening", "http/trustrank") ManualServerDescription[sIP + ",TCP_" + sport] = "http/trustrank" elif (Payload.find('Server: YTS/') > -1): ReportId("TS", sIP, "TCP_" + sport, "listening", "http/yahoo") ManualServerDescription[sIP + ",TCP_" + sport] = "http/yahoo" elif (Payload.find('HTTP/1.0 404 Not Found') > -1) or (Payload.find('HTTP/1.1 200 OK') > -1): ReportId("TS", sIP, "TCP_" + sport, "listening", "http/generic") ManualServerDescription[sIP + ",TCP_" + sport] = "http/generic" LogNewPayload(ServerPayloadDir, FromPort, Payload) else: LogNewPayload(ServerPayloadDir, FromPort, Payload) elif (sport == "143") and (Payload.find('* OK dovecot ready') > -1): ReportId("TS", sIP, "TCP_" + sport, "listening", "imap/dovecot") ManualServerDescription[sIP + ",TCP_" + sport] = "imap/dovecot" elif (sport == "143") and (Payload.find(' IMAP4rev1 ') > -1): ReportId("TS", sIP, "TCP_" + sport, "listening", "imap/generic") ManualServerDescription[sIP + ",TCP_" + sport] = "imap/generic" LogNewPayload(ServerPayloadDir, FromPort, Payload) elif (sport == "783") and (Payload.find('SPAMD/1.1 ') > -1): ReportId("TS", sIP, "TCP_" + sport, "listening", "spamd/spamd") ManualServerDescription[sIP + ",TCP_" + sport] = "spamd/spamd" elif ( (sport == "3128") or (sport == "80") ) and (Payload.find('Via: ') > -1) and (Payload.find(' (squid/') > -1): ReportId("TS", sIP, "TCP_" + sport, "listening", "proxy/squid") ManualServerDescription[sIP + ",TCP_" + sport] = "proxy/squid" else: LogNewPayload(ServerPayloadDir, FromPort, Payload) elif ((LiveTCPService.has_key(ToPort)) and (LiveTCPService[ToPort] == True)): #If the "To" side is a known TCP server: ClientKey = sIP + ",TCP_" + dport #Note: CLIENT ip and SERVER port if (not ClientDescription.has_key(ClientKey)): if (dport == "22") and ( (Payload.find('SSH-2.0-OpenSSH_') > -1) or (Payload.find('SSH-1.5-OpenSSH_') > -1) ): ReportId("TC", sIP, "TCP_" + dport, "open", "ssh/openssh") elif (dport == "25") and (Payload.find('Message-ID: -1): ReportId("TC", sIP, "TCP_" + dport, "open", "smtp/pine") elif ( (dport == "80") or (dport == "3128") ) and (Payload.find('User-Agent: libwww-perl/') > -1): ReportId("TC", sIP, "TCP_" + dport, "open", "http/libwww-perl") elif ( (dport == "80") or (dport == "3128") ) and (Payload.find('User-Agent: Lynx') > -1): ReportId("TC", sIP, "TCP_" + dport, "open", "http/lynx") elif ( (dport == "80") or (dport == "3128") ) and (Payload.find('User-Agent: Mozilla') > -1) and (Payload.find(' Firefox/') > -1): ReportId("TC", sIP, "TCP_" + dport, "open", "http/firefox") elif ( (dport == "80") or (dport == "3128") ) and (Payload.find('User-Agent: Wget/') > -1): ReportId("TC", sIP, "TCP_" + dport, "open", "http/wget") elif (dport == "143") and (Payload.find('A0001 CAPABILITY') > -1): ReportId("TC", sIP, "TCP_" + dport, "open", "imap/generic") #LogNewPayload(ClientPayloadDir, ClientKey, Payload) elif (dport == "783") and (Payload.find('PROCESS SPAMC') > -1): ReportId("TC", sIP, "TCP_" + dport, "open", "spamd/spamc") else: LogNewPayload(ClientPayloadDir, ClientKey, Payload) else: #Neither port pair is known as a server pass #Following is debugging at best; it should only show up early on as the sniffer listens to conversations for which it didn't hear the SYN/ACK #print "note: neither " + FromPort + " nor " + ToPort + " is listed as a live service." #else: #Test for other TCP flag combinations here elif p['IP.proto'] == 17: #UDP #FIXME - possibly run udp packets through ServiceFPs as well? sport=str(p['UDP.sport']) dport=str(p['UDP.dport']) SrcService = sIP + ",UDP_" + sport DstService = dIP + ",UDP_" + dport SrcClient = sIP + ",UDP_" + dport Payload = p['Raw.load'] if (sport == "53") and (p['DNS.qr'] == 1): #For some reason I can't check: (p.has_key('DNS.qr')) #FIXME - check return code for dns failure? if ((not LiveUDPService.has_key(SrcService)) or (LiveUDPService[SrcService] == False)): LiveUDPService[SrcService] = True #Also report the TLD from one of the query answers to show what it's willing to answer for? ReportId("US", sIP, "UDP_" + sport, "open", "dns/server") elif (dport == "53") and (p['DNS.qr'] == 0): if ((not LiveUDPClient.has_key(SrcClient)) or (LiveUDPClient[SrcClient] == False)): LiveUDPClient[SrcClient] = True ReportId("UC", sIP, "UDP_" + dport, "open", "dns/client") elif (sport == "67") and (dport == "68"): #Bootp/dhcp server talking to client if ((not LiveUDPService.has_key(SrcService)) or (LiveUDPService[SrcService] == False)): LiveUDPService[SrcService] = True ReportId("US", sIP, "UDP_" + sport, "open", "bootpordhcp/server") elif (sport == "68") and (dport == "67"): #Bootp/dhcp client talking to server if (sIP != "0.0.0.0"): #If the client is simply renewing an IP, remember it. if ((not LiveUDPClient.has_key(SrcClient)) or (LiveUDPClient[SrcClient] == False)): LiveUDPClient[SrcClient] = True ReportId("UC", sIP, "UDP_" + dport, "open", "bootpordhcp/client") #else: #If you want to record which macs are asking for addresses, do it here. # pass elif (sport == "123") and (dport == "123") and (p['NTP.stratum'] != ''): if ((not LiveUDPService.has_key(SrcService)) or (LiveUDPService[SrcService] == False)): LiveUDPService[SrcService] = True ReportId("US", sIP, "UDP_" + sport, "open", "ntp/generic") elif (dport == "123") and ( (dIP == "216.115.23.75") or (dIP == "216.115.23.76") or (dIP == "69.59.240.75") ): if ((not LiveUDPClient.has_key(SrcClient)) or (LiveUDPClient[SrcClient] == False)): LiveUDPClient[SrcClient] = True ReportId("UC", sIP, "UDP_" + dport, "open", "ntp/vonageclient") elif (sport == "123") and ( (sIP == "216.115.23.75") or (sIP == "216.115.23.76") or (sIP == "69.59.240.75") ): if ((not LiveUDPService.has_key(SrcService)) or (LiveUDPService[SrcService] == False)): LiveUDPService[SrcService] = True ReportId("US", sIP, "UDP_" + sport, "open", "ntp/vonageserver") elif (sport == "500") and (dport == "500") and (p['ISAKMP.init_cookie'] != ''): if ((not LiveUDPService.has_key(SrcService)) or (LiveUDPService[SrcService] == False)): LiveUDPService[SrcService] = True ReportId("US", sIP, "UDP_" + sport, "open", "isakmp/generic") elif (dport == "512"): #BIFF if ((not LiveUDPClient.has_key(SrcClient)) or (LiveUDPClient[SrcClient] == False)): if (Payload.find('@') > -1): LiveUDPClient[SrcClient] = True ReportId("UC", sIP, "UDP_" + dport, "open", "biff/client") else: p.show() elif ( (dport == "1026") or (dport == "1027") or (dport == "1028") ): #winpopup spam client if ((not LiveUDPClient.has_key(SrcClient)) or (LiveUDPClient[SrcClient] == False)): if ( (Payload.find('Download Registry Update from:') > -1) or (Payload.find('CRITICAL ERROR MESSAGE! - REGISTRY DAMAGED AND CORRUPTED.') > -1) or (Payload.find('Your system registry is corrupted and needs to be cleaned immediately.') > -1) ): LiveUDPClient[SrcClient] = True ReportId("UC", sIP, "UDP_" + dport, "open", "winpopup/spamclient") else: p.show() elif (dport == "1434"): #Probable mssql attack if ((not LiveUDPClient.has_key(SrcClient)) or (LiveUDPClient[SrcClient] == False)): if (Payload.find('Qh.dll') > -1): LiveUDPClient[SrcClient] = True ReportId("UC", sIP, "UDP_" + dport, "open", "mssql/clientattack") else: p.show() elif (sport == "1900") and (dport == "1900") and (dIP == "239.255.255.250"): #SSDP if ((not LiveUDPClient.has_key(SrcClient)) or (LiveUDPClient[SrcClient] == False)): if (Payload.find('NOTIFY') > -1): LiveUDPClient[SrcClient] = True ReportId("UC", sIP, "UDP_" + dport, "open", "ssdp/client") else: p.show() elif (sport == "5061") and (dport == "5061") and ( (dIP == "216.115.30.28") or (dIP == "69.59.227.77") or (dIP == "69.59.232.33") or (dIP == "69.59.240.84") ): #Vonage SIP client if ((not LiveUDPClient.has_key(SrcClient)) or (LiveUDPClient[SrcClient] == False)): if (Payload.find('.vonage.net:5061 SIP/2.0') > -1): LiveUDPClient[SrcClient] = True SipMatch = SipPhoneMatch.search(Payload) if (SipMatch != None) and (len(SipMatch.groups()) >= 1): ReportId("UC", sIP, "UDP_" + dport, "open", "sip/vonage_client, phone number: " + SipMatch.group(1)) else: ReportId("UC", sIP, "UDP_" + dport, "open", "sip/vonage_client") else: p.show() elif (sport == "5061") and (dport == "5061") and ( (sIP == "216.115.30.28") or (sIP == "69.59.227.77") or (sIP == "69.59.232.33") or (sIP == "69.59.240.84") ): #Vonage SIP server if ((not LiveUDPService.has_key(SrcService)) or (LiveUDPService[SrcService] == False)): if (Payload.find('.vonage.net:5061>') > -1): LiveUDPService[SrcService] = True ReportId("US", sIP, "UDP_" + sport, "open", "sip/vonage_server") else: p.show() elif ( (sport == "9052") or (sport == "9053") or (sport == "9054") ) and ( (sIP == "205.188.146.72") or (sIP == "205.188.157.241") or (sIP == "205.188.157.242") or (sIP == "205.188.157.243") or (sIP == "205.188.157.244") or (sIP == "64.12.51.145") or (sIP == "64.12.51.148") or (sIP == "149.174.54.131") ): #Possibly AOL dns response if ((not LiveUDPService.has_key(SrcService)) or (LiveUDPService[SrcService] == False)): if (Payload.find('dns-01') > -1): LiveUDPService[SrcService] = True ReportId("US", sIP, "UDP_" + sport, "open", "aoldns/server") else: p.show() elif (sport == "27005") and ( (dport == "27016") or (dport == "27017") ): #Halflife client live game if ((not LiveUDPClient.has_key(SrcClient)) or (LiveUDPClient[SrcClient] == False)): LiveUDPClient[SrcClient] = True ReportId("UC", sIP, "UDP_" + dport, "open", "halflife/client") elif (dport == "27013") and (dIP == "207.173.177.12"): #variable payload, so can't (Payload.find('Steam.exe') > -1) #Halflife client if ((not LiveUDPClient.has_key(SrcClient)) or (LiveUDPClient[SrcClient] == False)): LiveUDPClient[SrcClient] = True ReportId("UC", sIP, "UDP_" + dport, "open", "halflife/client") elif (sport == "27013") and (sIP == "207.173.177.12"): #halflife server if ((not LiveUDPService.has_key(SrcService)) or (LiveUDPService[SrcService] == False)): LiveUDPService[SrcService] = True ReportId("US", sIP, "UDP_" + sport, "open", "halflife/server") elif ( (sport == "27016") or (sport == "27017") ) and (dport == "27005"): #halflife server live game if ((not LiveUDPService.has_key(SrcService)) or (LiveUDPService[SrcService] == False)): LiveUDPService[SrcService] = True ReportId("US", sIP, "UDP_" + sport, "open", "halflife/server") elif ( (dport == "27015") or (dport == "27016") or (dport == "27025") or (dport == "27026") ): #Variable payload, so can't: (Payload.find('basic') > -1) #Halflife client if ((not LiveUDPClient.has_key(SrcClient)) or (LiveUDPClient[SrcClient] == False)): LiveUDPClient[SrcClient] = True ReportId("UC", sIP, "UDP_" + dport, "open", "halflife/client") elif (dport == "27017") and ( (dIP == "69.28.148.250") or (dIP == "72.165.61.161") or (dIP == "72.165.61.185") or (dIP == "72.165.61.186") or (dIP == "72.165.61.188") or (dIP == "68.142.64.164") or (dIP == "68.142.64.165") or (dIP == "68.142.64.166") ): #Steamfriends client if ((not LiveUDPClient.has_key(SrcClient)) or (LiveUDPClient[SrcClient] == False)): if (Payload.find('VS01') > -1): LiveUDPClient[SrcClient] = True ReportId("UC", sIP, "UDP_" + dport, "open", "steamfriends/client") else: p.show() elif (sport == "27017") and ( (sIP == "69.28.148.250") or (sIP == "72.165.61.161") or (sIP == "72.165.61.185") or (sIP == "72.165.61.186") or (sIP == "72.165.61.188") or (sIP == "68.142.64.164") or (sIP == "68.142.64.165") or (sIP == "68.142.64.166") ): #Steamfriends server if ((not LiveUDPService.has_key(SrcService)) or (LiveUDPService[SrcService] == False)): if (Payload.find('VS01') > -1): LiveUDPService[SrcService] = True ReportId("US", sIP, "UDP_" + sport, "open", "steamfriends/server") else: p.show() elif ( (sport == "21020") or (sport == "21250") or (sport == "27016") or (sport == "27017") or (sport == "27018") or (sport == "27030") or (sport == "27035") or (sport == "27040") or (sport == "28015") ): #halflife server if ((not LiveUDPService.has_key(SrcService)) or (LiveUDPService[SrcService] == False)): if (Payload.find('Team Fortress') > -1): LiveUDPService[SrcService] = True ReportId("US", sIP, "UDP_" + sport, "open", "halflife/server") else: p.show() elif (sport == "27019"): #halflife server if ((not LiveUDPService.has_key(SrcService)) or (LiveUDPService[SrcService] == False)): LiveUDPService[SrcService] = True ReportId("US", sIP, "UDP_" + sport, "open", "halflife/server") elif ( (dport == "1265") or (dport == "20100") or (dport == "21550") or (dport == "27000") or (dport == "27017") or (dport == "27018") or (dport == "27019") or (dport == "27022") or (dport == "27030") or (dport == "27035") or (dport == "27050") or (dport == "27078") or (dport == "27080") or (dport == "28015") or (dport == "28100") or (dport == "45081") ): #Halflife client if ((not LiveUDPClient.has_key(SrcClient)) or (LiveUDPClient[SrcClient] == False)): if (Payload.find('Source Engine Query') > -1): LiveUDPClient[SrcClient] = True ReportId("UC", sIP, "UDP_" + dport, "open", "halflife/client") else: p.show() elif (dport == "24441"): #Pyzor if ((not LiveUDPClient.has_key(SrcClient)) or (LiveUDPClient[SrcClient] == False)): if (Payload.find('User:') > -1): LiveUDPClient[SrcClient] = True ReportId("UC", sIP, "UDP_" + dport, "open", "pyzor/client") else: p.show() elif ( (dport >= "33434") and (dport <= "33524") ) and (p['IP.ttl'] <= 5): #udptraceroute client if ((not LiveUDPClient.has_key(sIP + "UDP_33434")) or (LiveUDPClient[sIP + "UDP_33434"] == False)): LiveUDPClient[sIP + "UDP_33434"] = True ReportId("UC", sIP, "UDP_33434", "open", "udptraceroute/client") elif (dport == "40348"): if ((not LiveUDPClient.has_key(SrcClient)) or (LiveUDPClient[SrcClient] == False)): if (Payload.find('HLS') > -1): LiveUDPClient[SrcClient] = True ReportId("UC", sIP, "UDP_" + dport, "open", "halflife/client") else: p.show() elif (p['IP.frag'] > 0): pass elif (sIP == "207.46.51.74") or (sIP == "65.55.251.10"): #Bigfish.com - dns? pass elif (sIP == "61.215.106.146"): #junk pass else: p.show() #quit() else: print "Other IP protocol (", p['IP.src'], "->", p['IP.dst'], "): ", p['IP.proto'] p.show() elif p['Ethernet.type'] == 0x86DD: #IPv6 pass else: print "Unregistered ethernet type:", p['Ethernet.type'] p.show() #======== Start of main code. ======== #FIXME - put in try/except, and CLP if os.path.isfile(LogFileName): LogFile=open(LogFileName, 'a') if (not(os.path.isfile("/etc/p0f/p0f.fp"))): Debug("/etc/p0f/p0f.fp not found; please install p0f version 2 to enable OS fingerprinting.") if len(sys.argv) > 1: bpfilter = sys.argv[1] else: bpfilter = "" Debug("BPFilter is " + bpfilter) #Hmmm, bpfilter appears not to work. It loads correctly into the variable, but the sniff command appears to ignore it. LoadMacData('/usr/share/ettercap/etter.finger.mac') LoadMacData('/usr/share/nmap/nmap-mac-prefixes') LoadMacData('/usr/share/wireshark/manuf') LoadMacData('/usr/share/ethereal/manuf') LoadMacData('/usr/share/arp-scan/ieee-oui.txt') if (len(EtherManuf) == 0): Debug("None of the default mac address listings found. Please install ettercap, nmap, wireshark, and/or arp-scan.") else: Debug(str(len(EtherManuf)) + " mac prefixes loaded.") LoadNmapServiceFP('/usr/share/nmap/nmap-service-probes') if (len(ServiceFPs) == 0): Debug("Can't locate /usr/share/nmap/nmap-service-probes. Please install nmap to support more server descriptions.") else: Debug("Fingerprints for " + str(len(ServiceFPs)) + " ports loaded.") SipPhoneMatch = re.compile('Contact: ([0-9-]+) 1 IP #- Use unreachables #- fingerprint OS based on echo payloads #- DHCP identification #- check if /i and other regex flags #- check for *LiveClient.has_key and ... == True in front of every .find() (done for udp, not for rest) #Done: #- New signatures from: # - pads-signature-list (appears to be an import of the nmap-service-probes anyways) # - nmap-service-probes (done) #- Remove p/Skype VoIP data channel/ signature (done) #- Replace "$N"'s in nmap descriptions with corresponding match groups (done) #- Double "\"'s in re's to handle \b? Doesn't appear to be needed; none of the nmap sigs use \b. (done) #Temporary code to load up a stub ServiceFPs #ServiceFPs[80] = [ ] #MatchTuple=('Apache', "http/apachewebserver") #Above works just fine, but lets precompile regexes as they're being stored: #"M" is needed so "^" and "$" work in a multiline payload block #MatchTuple=(re.compile('^Server: Apache/', re.M), "http/apachewebserver") #ServiceFPs[80].append(MatchTuple) #print ServiceFPs #Result: {80: [('Apache', 'http/apachewebserver')]} #Taken from the ack block, under NmapServiceDescription #Stub code to test ServiceFPs regex matching #if (sport == "80"): # print "In regex block" # MatchTuple=ServiceFPs[80][0] # #CompiledRegex=re.compile(MatchTuple[0]) # #Above works just fine with non-compiled. Let's precompile at insert time to save time - works fine :-D : # CompiledRegex=MatchTuple[0] # #MatchObject=CompiledRegex.search(Payload) # #if (MatchObject.start > -1): #Nope, "NoneType has no attribute "start"" # #if (MatchObject != None): # #Simpler than above and works: # if (CompiledRegex.search(Payload) != None): #re.M flag only works at compile time, not here # print "Test match found", MatchTuple[1]