mgeeky-Penetration-Testing-.../web/py-collaborator/py-collaborator-server.py

345 lines
12 KiB
Python

#!/usr/bin/python3
from http.server import BaseHTTPRequestHandler,HTTPServer
import urllib
import re
import sys
import ssl
import json
import string
import random
import socket
import pymysql
import argparse
import datetime
import threading
from Database import Database
from Logger import *
VERSION = '0.1'
#
# CONFIGURE THE BELOW VARIABLES
#
# Must point to JSON file containing configuration mentioned in `config` dictionary below.
# One can either supply that configuration file, or let the below variable empty and fill the `config`
# dictionary instead.
CONFIGURATION_FILE = 'config.json'
config = {
'debug' : '',
'listen' : '0.0.0.0',
'pingback-host': '',
'server-remote-addr': '',
'listen-on-ports' : (80, 443, 8080),
# You can generate it using Let's Encrypt wildcard certificate.
'server-ca-cert' : '',
"server-key-file": '',
'mysql-host': '',
'mysql-user': '',
'mysql-pass': '',
'mysql-database': '',
'exclude-pingbacks-from-clients' : [],
}
databaseInstance = None
def generateRandomId():
randomized = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(50))
return "xxx" + randomized + "yyy"
class PingbackServer(BaseHTTPRequestHandler):
method = ''
def __init__(self, *args, **kwargs):
self.server_version = 'nginx'
try:
BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
except Exception as e:
if config['debug']:
Logger.dbg('Failure along __init__ of BaseHTTPRequestHandler: {}'.format(str(e)))
raise
#Logger.info('Previously catched pingbacks:\n--------------------------\n')
#self.presentAtStart()
def presentAtStart(self):
rows = databaseInstance.query(f'SELECT * FROM calledbacks')
if not rows:
return
for row in rows:
request = databaseInstance.query(f"SELECT * FROM requests WHERE id = {row['requestid']}")
Logger.info(row['request'])
def log_message(self, format, *args):
return
def extractUuid(self):
uuidRex = re.compile(r'(\bxxx[a-z0-9]{50}yyy\b)', re.I|re.M)
if 'xxx' in self.path and 'yyy' in self.path:
# Request path
m = uuidRex.search(self.path)
if m:
return ('URL path', m.group(1))
# Request headers
for h in self.headers:
value = self.headers[h]
if ('xxx' not in value or 'yyy' not in value):
continue
m = uuidRex.search(value)
if m:
return (f'Header: {h}', m.group(1))
return ('', '')
def presentPingbackedRequest(self, where, uuid, record):
fmt = '%Y-%m-%d %H:%M:%S'
now = datetime.datetime.utcnow().strftime(fmt)
delay = str(datetime.datetime.utcnow() - datetime.datetime.strptime(record['sent'], fmt))
req = '\r\n'.join([f'\t{x}' for x in record['request'].split('\r\n')])
req2 = '\r\n'.join([f'\t{x}' for x in PingbackServer.requestToString(self).split('\r\n')])
try:
reverse = socket.gethostbyaddr(self.client_address[0])[0]
except:
reverse = self.client_address[0]
message = f'''
-------------------------------------------------------------------------------------
Issue: Pingback {record['id']} ({self.command} {self.path} ) found in request's {where}
Where payload was put: {record['whereput']}
Contacting host: {reverse}
Tried to reach vhost: {self.headers['Host']}:{self.server.server_port}
Issue detail:
Our pingback-server was contacted by ({self.client_address[0]}:{self.client_address[1]}) after a delay of ({delay}):
Original request where this pingback was inserted:
---
{req}
Request that was sent to us in return:
---
{req2}
The payload was sent at ({record['sent']}) and received on ({now}).
-------------------------------------------------------------------------------------
'''
Logger._out(message)
return message
def savePingback(self, requestid, message):
query = 'INSERT INTO calledbacks(id, requestid, uuid, whereput) VALUES(%d, %d, "%s")' % (\
0, requestid, message)
Logger.dbg(f'Saving pingback: (requestid={str(requestid)})')
Logger.dbg(query)
databaseInstance.insert(query)
def checkUuid(self, where, uuid):
if not (uuid.startswith('xxx') and uuid.endswith('yyy')):
return
for a in uuid:
if a not in string.ascii_lowercase + string.digits:
return
out = databaseInstance.query(f'SELECT * FROM requests WHERE uuid = "{uuid}"')
if out:
message = self.presentPingbackedRequest(where, uuid, out[0])
self.savePingback(out[0]['id'], message)
def send_header(self, name, value):
if name == 'Server':
return super(PingbackServer, self).send_header(name, 'nginx')
return super(PingbackServer, self).send_header(name, value)
def _set_response(self):
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
@staticmethod
def requestToString(request):
headers = '\r\n'.join(['{}: {}'.format(k, v) for k, v in request.headers.items()])
out = '{} {} {}\r\n{}'.format(request.command, request.path, request.request_version, headers)
return out
def do_GET(self):
if not (self.client_address[0] in config['exclude-pingbacks-from-clients']):
if config['debug']:
Logger.dbg('--------------------------\nIncoming HTTP request from {}: {} {}'.format(
self.client_address[0],
self.method,
self.path[:25]
))
Logger.dbg(PingbackServer.requestToString(self) + '\n')
(where, uuid) = PingbackServer.extractUuid(self)
if uuid:
self.checkUuid(where, uuid)
else:
Logger.dbg('Skipping Client ({}) as it was excluded in config file.'.format(self.client_address[0]))
self._set_response()
self.wfile.write(b'Ok')
do_POST = do_GET
do_DELETE = do_GET
do_PUT = do_GET
do_OPTIONS = do_GET
do_HEAD = do_GET
do_TRACE = do_GET
do_CONNECT = do_GET
do_PATCH = do_GET
def parseOptions(argv):
global config
print('''
:: Cracking the Lens pingback responding server
Responds to every Out-of-band request correlating them along the way
Mariusz B. / mgeeky '16-18, <mb@binary-offensive.com>
''')
parser = argparse.ArgumentParser(prog = argv[0], usage='%(prog)s [options]')
parser.add_argument('-l', '--listen', default='0.0.0.0', help = 'Specifies interface address to bind the HTTP server on / listen on. Default: 0.0.0.0 (all interfaces)')
parser.add_argument('-p', '--port', metavar='PORT', default='80', type=int, help='Specifies the port to listen on. Default: 80')
parser.add_argument('-r', '--rhost', metavar='HOST', default=config['server-remote-addr'], help='Specifies attackers host address where the victim\'s XML parser should refer while fetching external entities')
parser.add_argument('--mysql-host', metavar='MYSQLHOST', default='127.0.0.1', type=str, help='Specifies the MySQL hostname. Defalut: 127.0.0.1:3306')
parser.add_argument('--mysql-user', metavar='MYSQLUSER', default='root', type=str, help='Specifies the MySQL user, that will be able to create database, tables, select/insert records and so on. Default: root')
parser.add_argument('--mysql-pass', metavar='MYSQLPASS', type=str, help='Specifies the MySQL password')
parser.add_argument('-d', '--debug', action='store_true', help='Display debug output.')
args = parser.parse_args()
config['debug'] = args.debug
config['listen'] = args.listen
config['port'] = int(args.port)
config['server-remote-addr'] = args.rhost
port = int(args.port)
if port < 1 or port > 65535:
Logger.err("Invalid port number. Must be in <1, 65535>")
sys.exit(-1)
try:
if not args.mysql_host or not args.mysql_port or not args.mysql_user or not args.mysql_pass:
Logger.warn("You shall specify all needed MySQL connection data either via program options or config file.")
#sys.exit(-1)
else:
config['mysql-host'] = args.mysql_host
config['mysql-user'] = args.mysql_user
config['mysql-pass'] = args.mysql_pass
except:
Logger.warn("You shall specify all needed MySQL connection data either via program options or config file.")
return args
def connectToDatabase():
global databaseInstance
databaseInstance = Database()
return databaseInstance.connection(config['mysql-host'], config['mysql-user'], config['mysql-pass'])
def initDatabase():
initQueries = (
f"CREATE DATABASE IF NOT EXISTS {config['mysql-database']}",
f'''CREATE TABLE IF NOT EXISTS {config['mysql-database']}.requests (
id integer AUTO_INCREMENT,
sent text NOT NULL,
uuid text NOT NULL,
desthost text NOT NULL,
pingback text NOT NULL,
whereput text NOT NULL,
request text NOT NULL,
PRIMARY KEY (id)) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;''',
f'''CREATE TABLE IF NOT EXISTS {config['mysql-database']}.calledbacks (
id integer AUTO_INCREMENT,
requestid integer NOT NULL,
request text NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY(requestid) REFERENCES requests(id)) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;''',
)
for query in initQueries:
databaseInstance.query(query)
databaseInstance.databaseConnection.select_db(config['mysql-database'])
Logger.ok('Database initialized.')
def fetchRhost():
global config
config['server-remote-addr'] = socket.gethostbyname(socket.gethostname())
def main(argv):
global config
fetchRhost()
opts = parseOptions(argv)
if not opts:
Logger.err('Options parsing failed.')
return False
if CONFIGURATION_FILE:
config.update(json.loads(open(CONFIGURATION_FILE).read()))
if not connectToDatabase():
Logger.err('Could not connect to database: {}'.format(config['mysql-host']))
sys.exit(-1)
initDatabase()
Logger.dbg('Local host\'s IP address (RHOST) set to: {}'.format(config['server-remote-addr']))
for port in config['listen-on-ports']:
try:
server = HTTPServer((config['listen'], port), PingbackServer)
server.server_version = 'nginx'
except OSError as e:
Logger.err(f'Could not server on port {port}: {str(e)}')
Logger.warn('Skipping...')
continue
#return
if port == 443:
try:
server.socket = ssl.wrap_socket(server.socket, keyfile = config['server-key-file'], certfile = config['server-ca-cert'], server_side = True)
except ssl.SSLError as e:
Logger.warn(f'Could not serve HTTPS due to SSL error: {str(e)}')
Logger.warn('Skipping...')
continue
thread = threading.Thread(target=server.serve_forever)
thread.daemon = True
thread.start()
Logger.ok('Serving HTTP server on: ("{}", {})'.format(
config['listen'], port)
)
try:
Logger.info('Entering infinite serving loop.')
while True:
pass
except KeyboardInterrupt:
pass
if __name__ == '__main__':
main(sys.argv)