# 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