#!/usr/bin/python #Copyright 2008, William Stearns #Dedicated to Mae Anne Laroche. #Released under the GPL. import sys import re from scapy import sniff, p0f #sr1,IP,ICMP import os #Count=0 #Next two are used to discover servers. 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. 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? LiveUDPService = { } #Boolean dictionary: we've found a server here at "IP,Proto_Port" ServerDescription = { } #String dictionary: What server is this "IP,Proto_Port" pair? 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' ServerPayloadDir = '/var/tmp/passer-server/' ClientPayloadDir = '/var/tmp/passer-client/' #FIXME - put in try/except, and CLP if os.path.isfile(LogFileName): LogFile=open(LogFileName, 'a') 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.""" 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 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 # 'UD', IPaddr, 'UDP_'Port, open or closed, udp 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"): #Only assign this (and the others in this function) if Description non-null if (Description != ''): ServerDescription[Location] = Description elif (Type == "UD"): if (Description != ''): ServerDescription[Location] = Description elif (Type == "TC"): 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 Count global SynSentToTCPService global SynAckSentToTCPClient global LiveTCPService global LiveUDPService global LiveTCPClient global ServerDescription global ClientDescription global MacAddr global OSDescription #Count=Count+1 #print Count 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" #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. #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("UD", 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 ServerDescription.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") elif (Payload.find('SSH-1.5-') > -1): ReportId("TS", sIP, "TCP_" + sport, "listening", "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") elif (sport == "25") and (Payload.find(' - Welcome to our SMTP server ESMTP') > -1): ReportId("TS", sIP, "TCP_" + sport, "listening", "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") elif (Payload.find('Server: Embedded HTTP Server') > -1): ReportId("TS", sIP, "TCP_" + sport, "listening", "http/embedded") elif (Payload.find('Server: gws') > -1): ReportId("TS", sIP, "TCP_" + sport, "listening", "http/gws") elif (Payload.find('Server: KFWebServer') > -1): ReportId("TS", sIP, "TCP_" + sport, "listening", "http/kfwebserver") elif (Payload.find('Server: micro_httpd') > -1): ReportId("TS", sIP, "TCP_" + sport, "listening", "http/micro-httpd") elif (Payload.find('Server: Microsoft-IIS') > -1): ReportId("TS", sIP, "TCP_" + sport, "listening", "http/iis") elif (Payload.find('Server: lighttpd') > -1): ReportId("TS", sIP, "TCP_" + sport, "listening", "http/lighttpd") elif (Payload.find('Server: MIIxpc') > -1): ReportId("TS", sIP, "TCP_" + sport, "listening", "http/mirrorimage") elif (Payload.find('Server: mini_httpd') > -1): ReportId("TS", sIP, "TCP_" + sport, "listening", "http/mini-httpd") elif (Payload.find('Server: nc -l -p 80') > -1): ReportId("TS", sIP, "TCP_" + sport, "listening", "http/nc") elif (Payload.find('Server: nginx/') > -1): ReportId("TS", sIP, "TCP_" + sport, "listening", "http/nginx") elif (Payload.find('Server: Nucleus') > -1): ReportId("TS", sIP, "TCP_" + sport, "listening", "http/nucleus") elif (Payload.find('Server: RomPager') > -1): ReportId("TS", sIP, "TCP_" + sport, "listening", "http/rompager") elif (Payload.find('Server: Server') > -1): ReportId("TS", sIP, "TCP_" + sport, "listening", "http/server") elif (Payload.find('Server: Sun-ONE-Web-Server/') > -1): ReportId("TS", sIP, "TCP_" + sport, "listening", "http/sun-one") elif (Payload.find('Server: TrustRank Frontend') > -1): ReportId("TS", sIP, "TCP_" + sport, "listening", "http/trustrank") elif (Payload.find('Server: YTS/') > -1): ReportId("TS", sIP, "TCP_" + sport, "listening", "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") 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") elif (sport == "143") and (Payload.find(' IMAP4rev1 ') > -1): ReportId("TS", sIP, "TCP_" + sport, "listening", "imap/generic") LogNewPayload(ServerPayloadDir, FromPort, Payload) elif (sport == "783") and (Payload.find('SPAMD/1.1 ') > -1): ReportId("TS", sIP, "TCP_" + sport, "listening", "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") 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 sport=str(p['UDP.sport']) #dport=str(p['UDP.dport']) Service = sIP + ",UDP_" + sport #FIXME - clean these up if not needed any more 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(Service)) or (LiveUDPService[Service] == False)): LiveUDPService[Service] = True #Also report the TLD from one of the query answers to show what it's willing to answer for? ReportId("UD", sIP, "UDP_" + sport, "open", "dns/generic") 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() if len(sys.argv) > 1: bpfilter = sys.argv[1] else: bpfilter = "" LoadMacData('/usr/share/ettercap/etter.finger.mac') LoadMacData('/usr/share/nmap/nmap-mac-prefixes') LoadMacData('/usr/share/wireshark/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.") #sniff(store=0, offline='/home/wstearns/med/pcap/flagoricmporudp.20080215.pcap', filter=bpfilter, prn=lambda x: processpacket(x)) #sniff(store=0, offline='/home/wstearns/med/sans/perimsec/t2-files/usb/traces/perimeter_class.cap', filter=bpfilter, prn=lambda x: processpacket(x)) sniff(store=0, filter=bpfilter, prn=lambda x: processpacket(x)) #, count=500 #To debug: #p.show() #quit() #scapy.ls(scapy.Ether) #No longer needed #MuteWarned = { } #Boolean dictionary: if true for a given key, we've warned that we won't report this object any more. #Former code to mute multiple IP's for a single mac: # Found = False # for Key in MacAddr.keys(): # if (MacAddr[Key] == State): # Found=True # break # if Found: # if (not MuteWarned.has_key(State)): # Debug("Duplicate IPs found for " + State + ", no longer printing for it.") # MuteWarned[State] = True # else: #if not IPCount.has_key(p['IP.src']): # IPCount[p['IP.src']] = 0 #IPCount[p['IP.src']] += 1 #TODO Identify routers/nics with >1 IP #TODO Use unreachables #TODO: fingerprint OS based on echo payloads #New signatures from: #pads-signature-list #nmap-service-probes