├── requirements.txt ├── TZSP_Wireshark_Filter.png ├── service ├── readme.md └── scratch_n_sniff ├── config.yaml ├── scapysnif.py ├── sniffer.py ├── scratchnsniff.py ├── README.md └── localCapture.py /requirements.txt: -------------------------------------------------------------------------------- 1 | pyshark==0.4.3 -------------------------------------------------------------------------------- /TZSP_Wireshark_Filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nickvsnetworking/Scratch-n-Sniff/HEAD/TZSP_Wireshark_Filter.png -------------------------------------------------------------------------------- /service/readme.md: -------------------------------------------------------------------------------- 1 | # Running as a Service 2 | 3 | * Update ``scratch_n_sniff`` to include the path you have it installed in, and the filter / destination info you need. 4 | * Copy the file to `/usr/bin/` and add the executable permission (``chmod +x /usr/bin/scratch_n_sniff``) 5 | * Check you can start the service and it runs / functions OK with ``scratch_n_sniff start`` 6 | -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | localCapture: 2 | # Maximum file size per capure, in kilobytes 3 | maxFilesize: 50000 4 | # Maximum amount of pcaps to store 5 | maxPcaps: 20 6 | # Whether to enable compression of the packet captures 7 | enableCompression: true 8 | # What to call the initial file. Note filename will be appended with sequence number and timestamp. 9 | outputFile: /etc/localcapture/localCapture.pcap 10 | # Which ports to capture traffic on 11 | portList: 12 | - 80 13 | - 443 -------------------------------------------------------------------------------- /service/scratch_n_sniff: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PIDFILE=/var/run/scratch_n_sniff.pid 4 | 5 | case $1 in 6 | start) 7 | # Launch your program as a detached process 8 | cd /home/nick/scratch_n_sniff && python3 scratchnsniff.py --dstip 10.98.1.2 --packetfilter 'sctp or icmp' --interface lo 9 | # Get its PID and store it 10 | echo $! > ${PIDFILE} 11 | ;; 12 | stop) 13 | kill `cat ${PIDFILE}` 14 | # Now that it's killed, don't forget to remove the PID file 15 | rm ${PIDFILE} 16 | pkill -9 -f hss.py 17 | ;; 18 | *) 19 | echo "usage: scratch_n_sniff {start|stop}" ;; 20 | esac 21 | exit 0 -------------------------------------------------------------------------------- /scapysnif.py: -------------------------------------------------------------------------------- 1 | import pyshark 2 | import sys 3 | import socket 4 | 5 | dest_ip = '10.0.1.252' 6 | dest_port = 37008 7 | packet_filter = 'port 5060' 8 | 9 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 10 | tzsp_header= '0100000101' 11 | 12 | def TZSP_Encap(data): 13 | 14 | eth_header = data[0:14].hex() 15 | 16 | #Convert to Hex 17 | data = data.hex() 18 | #Ethernet Header 19 | 20 | print("eth_header: " + str(eth_header)) 21 | 22 | #Append TZSP Header 23 | return tzsp_header + data 24 | 25 | def Tx(data): 26 | #Add TZSP Header 27 | data = TZSP_Encap(data) 28 | #Send it 29 | sock.sendto(bytes.fromhex(data), (dest_ip, dest_port)) 30 | print("Sent!") 31 | return 32 | 33 | capture = pyshark.LiveCapture(interface='enp0s25', bpf_filter='', include_raw=True, use_json=True) 34 | for packet in capture.sniff_continuously(): 35 | print('Just arrived:' + str(packet)) 36 | Tx(packet.get_raw_packet()) 37 | print("Sent!") 38 | 39 | -------------------------------------------------------------------------------- /sniffer.py: -------------------------------------------------------------------------------- 1 | 2 | import logging 3 | logging.basicConfig(level=logging.DEBUG) 4 | import pyshark 5 | import socket 6 | import os 7 | import argparse 8 | import sys 9 | 10 | # Instantiate the parser 11 | ##parser = argparse.ArgumentParser(description='Scratch\'n\'Sniff - Remote Packet Capture Agent') 12 | ##parser.add_argument('--destination', type=str, required=True, help='IP to forward traffic to') 13 | ##parser.add_argument('--packet filter', type=str, required=False, help='TCPDump formatted Capture Filter') 14 | ## 15 | ##args = parser.parse_args() 16 | 17 | # bind_ip = str(args.bind_ip) 18 | # rtp_destination = str(args.rtp_destination) 19 | # rtp_port = int(args.rtp_port) 20 | # rtp_payload_id = int(args.rtp_payload_id) 21 | 22 | dest_ip = '10.0.1.252' 23 | dest_port = 37008 24 | packet_filter = 'port 5060' 25 | 26 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 27 | tzsp_header= '0100000101' 28 | 29 | def TZSP_Encap(data): 30 | 31 | eth_header = data[0:14].hex() 32 | 33 | #Convert to Hex 34 | data = data.hex() 35 | #Ethernet Header 36 | 37 | print("eth_header: " + str(eth_header)) 38 | 39 | #Append TZSP Header 40 | return tzsp_header + data 41 | 42 | def Tx(data): 43 | #Add TZSP Header 44 | data = TZSP_Encap(data) 45 | #Send it 46 | sock.sendto(bytes.fromhex(data), (dest_ip, dest_port)) 47 | print("Sent!") 48 | return 49 | 50 | capture = pyshark.LiveCapture(interface='any', bpf_filter=packet_filter, include_raw=True, use_json=True) 51 | logging.debug("Starting capture...") 52 | for packet in capture.sniff_continuously(): 53 | print('Just arrived:' + str(packet)) 54 | Tx(packet.get_raw_packet()) 55 | 56 | 57 | -------------------------------------------------------------------------------- /scratchnsniff.py: -------------------------------------------------------------------------------- 1 | import pyshark 2 | import socket 3 | import argparse 4 | import sys 5 | 6 | # Instantiate the parser 7 | parser = argparse.ArgumentParser(description='Scratch\'n\'Sniff - Remote Packet Capture Agent') 8 | parser.add_argument('--dstip', type=str, required=True, help='IP to forward traffic to') 9 | parser.add_argument('--dstport', type=int, required=False, help='Port to forward traffic to') 10 | parser.add_argument('--packetfilter', type=str, required=False, help='TCPDump formatted Capture Filter') 11 | parser.add_argument('--interface', type=str, required=True, help='Interface to capture on') 12 | 13 | args = parser.parse_args() 14 | 15 | dest_ip = str(args.dstip) 16 | if args.dstport == None: 17 | dest_port = 37008 #Set Default Port if none specified 18 | else: 19 | dest_port = int(args.dstport) 20 | packet_filter = str(args.packetfilter) + ' and not port ' + str(dest_port) 21 | interface = str(args.interface) 22 | 23 | print("Scratch\'n\'Sniff - Remote Packet Capture Agent") 24 | 25 | if interface == 'any': 26 | print("Warning: Using interface \"any\" is not supported. It makes pyshark confused and breaks things.") 27 | if packet_filter == 'None': 28 | packet_filter = 'not port ' + str(dest_port) 29 | print("Capturing and forwarding all traffic on interface " + str(interface)) 30 | else: 31 | print("Forwarding all traffic matching filter " + str(packet_filter) + " on interface " + str(interface)) 32 | print("To remote host " + str(dest_ip) + " on UDP port " + str(dest_port)) 33 | 34 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 35 | 36 | def TZSP_Encap(data): 37 | data = data.hex() #Convert to Hex 38 | return '0100000101' + data #Append TZSP Header 39 | 40 | def Forward(data): 41 | data = TZSP_Encap(data) #Add TZSP Header 42 | #Send it 43 | sock.sendto(bytes.fromhex(data), (dest_ip, dest_port)) 44 | return 45 | 46 | count = 1 47 | print("Starting capture...") 48 | capture = pyshark.LiveCapture(interface=interface, bpf_filter=packet_filter, include_raw=True, use_json=True) 49 | for packet in capture.sniff_continuously(): 50 | Forward(packet.get_raw_packet()) 51 | print("Forwarded " + str(count) + " matching packets to remote host.", end='\r') 52 | count += 1 53 | 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Scrach'n'Sniff - Simple Remote Packet Sniffer / Mirror 2 | 3 | ## What it Does 4 | 5 | Scratch'n'Sniff is a very simple remote packet sniffer, that aims to avoid the start tcpdump, capture required info, transfer with SFTP, view in Wireshark, grind. 6 | 7 | It captures packets matching the defined packet filters (standard [TCPDump filters](https://www.tcpdump.org/manpages/pcap-filter.7.html)), then encapsulates the data into TZSP and forwards / mirrors it to a remote host, which can then view the data live with Wireshark. 8 | 9 | ## Usage 10 | 11 | The required parameters are: 12 | 13 | The **interface** to capture on (ie wlan0, eth0, enp0s25, etc) - This is the interface we will capture the traffic from. Usage of all interfaces (aka 'any') is not currently supported. 14 | 15 | The **dstip** (Destination IP) to send the matching packets to, this is the remote machine you're running Wireshark or similar on. 16 | 17 | Optional parameter are: 18 | 19 | The **packetfilter** which is the [TCPDump Filter](https://www.tcpdump.org/manpages/pcap-filter.7.html) formatted filter to be applied to incoming traffic, that if matched, will see it forwarded. If this is not set then all traffic on the interface is captured. 20 | 21 | The **dstport** (Destination Port) to send the TZSP encapsulated traffic to on the remote host (defaults to 37008.) 22 | 23 | You can stop the capture with **Control + C** to exit. 24 | 25 | On the remote machine, start Wireshark, and filter by 'tzsp' and you'll see all the remote traffic being mirrored. 26 | 27 | ![Wireshark TZSP Filter](TZSP_Wireshark_Filter.png) 28 | 29 | There's a good chance that you'll also see a lot of icmp errors, so suggest using the filter 'tzsp and not icmp' in Wireshark. 30 | 31 | ## Example Usage 32 | 33 | *Capture all traffic on port 5060 on interface enp0s25 and send it to 10.0.1.252* 34 |
35 | ```python3 scratchnsniff.py --dstip 10.0.1.252 --packetfilter 'port 5060' --interface enp0s25``` 36 | 37 | Capture all sctp and icmp traffic on interface lo and send it to 10.98.1.2: 38 |
39 | ```python3 scratchnsniff.py --dstip 10.98.1.2 --packetfilter 'sctp or icmp' --interface lo``` 40 | 41 | ## Installation 42 | 43 | Clone the repo and install the requirements: 44 | 45 | You will need to have installed: 46 | * Python3 47 | * pip 48 | * tcpdump 49 | * tshark 50 | All of these packages are in the repos for common Linux distros. 51 | 52 | ``` 53 | apt-get install -y --assume-yes git python3-pip tshark 54 | git clone https://github.com/nickvsnetworking/Scratch-n-Sniff 55 | cd Scratch-n-Sniff 56 | pip3 install -r requirements.txt 57 | ``` 58 | 59 | You can now call the program with ```python3 scratchnsniff.py``` and your arguments. 60 | 61 | 62 | ## Service / Daemon 63 | You can run the script as a service / Daemon by following the instructions [here](service/readme.md). 64 | 65 | ## Support / Contact 66 | 67 | You can find me at [NickVsNetworking](http://nickvsnetworking.com). -------------------------------------------------------------------------------- /localCapture.py: -------------------------------------------------------------------------------- 1 | import logging 2 | logging.basicConfig(level=logging.DEBUG) 3 | import shutil 4 | import glob 5 | import time 6 | import gzip 7 | import os 8 | import socket 9 | import yaml 10 | from pathlib import Path 11 | 12 | # Local Capture agent for cylic packet capturing. 13 | # Parameters such as maximum number of pcap files kept, source port and rotation size are defined in config.yaml 14 | 15 | with open("config.yaml", 'r') as configFile: 16 | yamlConfig = (yaml.safe_load(configFile)) 17 | 18 | maxFilesize = yamlConfig.get("localCapture", {}).get("maxFilesize", "200000") # Default 200mb max file size 19 | maxPcaps = yamlConfig.get("localCapture", {}).get("maxPcaps", "5") # Default max 5 pcaps 20 | outputFile = yamlConfig.get("localCapture", {}).get("outputFile", "/etc/localcapture/localCapture.pcap") # Default to the local directory 21 | outputDirectory = str(Path(outputFile).parent) 22 | portList = yamlConfig.get("localCapture", {}).get("portList", []) 23 | enableCompression = yamlConfig.get("localCapture", {}).get("enableCompression", True) 24 | hostname = socket.gethostname() 25 | 26 | # Port range keyword mappings 27 | PORT_RANGES = { 28 | 'rtp': (16384, 32767), # RTP dynamic port range 29 | 'ephemeral': (49152, 65535), # Ephemeral ports 30 | 'registered': (1024, 49151), # Registered ports 31 | 'sip': (5060, 5061), # SIP signaling 32 | } 33 | 34 | def expand_port_list(port_list): 35 | """Expand port list with keyword support into individual ports and ranges.""" 36 | expanded = [] 37 | for item in port_list: 38 | if isinstance(item, str) and item.lower() in PORT_RANGES: 39 | # It's a keyword, expand to all ports in the range 40 | start, end = PORT_RANGES[item.lower()] 41 | # Generate BPF filter for port range: (port >= start and port <= end) 42 | expanded.append(f'(portrange {start}-{end})') 43 | else: 44 | # It's a specific port number 45 | expanded.append(f'port {item}') 46 | return expanded 47 | 48 | #Include a port filter, if defined. 49 | portFilter = "" 50 | if portList: 51 | expanded_ports = expand_port_list(portList) 52 | portFilter = ' or '.join(expanded_ports) 53 | portFilter = '-f ' + '"' + portFilter + '"' 54 | 55 | def rotatingCapture(): 56 | while True: 57 | # Check if the output directory contains more pcaps than maxPcaps. If it does, keep only the newest maxPcaps (eg 20). 58 | files = sorted(glob.glob(f'{outputDirectory}/*.pcap.gz'), key=os.path.getmtime) 59 | print(files) 60 | oldPcaps = files[:-maxPcaps] if len(files) > maxPcaps else [] 61 | for file in oldPcaps: 62 | print(f"Removing {file}") 63 | os.remove(file) 64 | 65 | # Capture the PCAP 66 | print(f"Running: dumpcap -i any -w {outputFile} -a filesize:{maxFilesize} {portFilter}") 67 | os.system(f'dumpcap -i any -w {outputFile} -a filesize:{maxFilesize} {portFilter}') 68 | print("Done") 69 | 70 | timestamp = time.strftime("%Y%m%d-%H%M%S") 71 | # Give the PCAP a unique name by appending the timestamp 72 | finalFilename = f'{hostname}_{timestamp}.pcap' 73 | os.rename(outputFile, finalFilename) 74 | 75 | if enableCompression: 76 | # Compress the file using GZIP, then delete the uncompressed file 77 | with open(finalFilename, 'rb') as f_in, gzip.open(f'/{outputDirectory}/{finalFilename}' + '.gz', 'wb') as f_out: 78 | shutil.copyfileobj(f_in, f_out) 79 | os.remove(finalFilename) 80 | 81 | # Remove any stray .pcap files, incase they exist. 82 | try: 83 | os.system(f'rm {outputDirectory}/*.pcap') 84 | except Exception as e: 85 | pass 86 | 87 | # Set '777' Permissions on the PCAP folder contents 88 | os.system((f'chmod -R 777 {outputDirectory}')) 89 | 90 | 91 | if __name__ == '__main__': 92 | rotatingCapture() 93 | --------------------------------------------------------------------------------