From 4c97dbb963e2e8c7503eaaf08e2c47218c14a019 Mon Sep 17 00:00:00 2001 From: Sascha Tommasone Date: Sat, 25 May 2024 10:25:40 +0200 Subject: [PATCH] [Assignment-4] added tcpproxy --- .../.gitignore | 216 ++++++++ .../README.md | 2 + .../proxy/mitm.pem | 83 +++ .../proxy/proxymodules/__init__.py | 0 .../proxy/proxymodules/digestdowngrade.py | 38 ++ .../proxy/proxymodules/hexdump.py | 34 ++ .../proxy/proxymodules/http_ok.py | 35 ++ .../proxy/proxymodules/http_post.py | 41 ++ .../proxy/proxymodules/http_strip.py | 27 + .../proxy/proxymodules/javaxml.py | 79 +++ .../proxy/proxymodules/log.py | 41 ++ .../proxy/proxymodules/mqtt.py | 73 +++ .../proxy/proxymodules/removegzip.py | 34 ++ .../proxy/proxymodules/replace.py | 62 +++ .../proxy/proxymodules/size.py | 34 ++ .../proxy/proxymodules/size404.py | 79 +++ .../proxy/proxymodules/syssec.py | 28 + .../proxy/proxymodules/textdump.py | 46 ++ .../proxy/requirements.txt | 2 + .../proxy/tcpproxy.py | 489 ++++++++++++++++++ 20 files changed, 1443 insertions(+) create mode 100644 Assignment 4 - Protokollsicherheit (Praxis)/.gitignore create mode 100644 Assignment 4 - Protokollsicherheit (Praxis)/README.md create mode 100644 Assignment 4 - Protokollsicherheit (Praxis)/proxy/mitm.pem create mode 100644 Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/__init__.py create mode 100644 Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/digestdowngrade.py create mode 100644 Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/hexdump.py create mode 100644 Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/http_ok.py create mode 100644 Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/http_post.py create mode 100644 Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/http_strip.py create mode 100644 Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/javaxml.py create mode 100644 Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/log.py create mode 100644 Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/mqtt.py create mode 100644 Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/removegzip.py create mode 100644 Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/replace.py create mode 100644 Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/size.py create mode 100644 Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/size404.py create mode 100644 Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/syssec.py create mode 100644 Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/textdump.py create mode 100644 Assignment 4 - Protokollsicherheit (Praxis)/proxy/requirements.txt create mode 100755 Assignment 4 - Protokollsicherheit (Praxis)/proxy/tcpproxy.py diff --git a/Assignment 4 - Protokollsicherheit (Praxis)/.gitignore b/Assignment 4 - Protokollsicherheit (Praxis)/.gitignore new file mode 100644 index 0000000..1122fd4 --- /dev/null +++ b/Assignment 4 - Protokollsicherheit (Praxis)/.gitignore @@ -0,0 +1,216 @@ +# ---> Python +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# ---> C +# Prerequisites +*.d + +# Object files +*.o +*.ko +*.obj +*.elf + +# Linker output +*.ilk +*.map +*.exp + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ +*.su +*.idb +*.pdb + +# Kernel Module Compile Results +*.mod* +*.cmd +.tmp_versions/ +modules.order +Module.symvers +Mkfile.old +dkms.conf + diff --git a/Assignment 4 - Protokollsicherheit (Praxis)/README.md b/Assignment 4 - Protokollsicherheit (Praxis)/README.md new file mode 100644 index 0000000..d22ee13 --- /dev/null +++ b/Assignment 4 - Protokollsicherheit (Praxis)/README.md @@ -0,0 +1,2 @@ +# Systemsicherheit + diff --git a/Assignment 4 - Protokollsicherheit (Praxis)/proxy/mitm.pem b/Assignment 4 - Protokollsicherheit (Praxis)/proxy/mitm.pem new file mode 100644 index 0000000..2ec6c15 --- /dev/null +++ b/Assignment 4 - Protokollsicherheit (Praxis)/proxy/mitm.pem @@ -0,0 +1,83 @@ +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIJALovM7ADVGykMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTgwMTI2MTEyMTE1WhcNMjgwMTI0MTEyMTE1WjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEAusz4JoOtp7PTNhCrkmBL5niiqnQgnODBwp/cd6ZzQdmxY9gRrZ/JOoDg +gxaJ0fLpF+S+XCIb6H3Hw+zmks15uZVg/EEEgc9cKvwrQw+7z5szIlGQ+OvXvHgO +ijDPE3pOeMss+/fm7zrfZPy3V9tROym/gVlhduyqCy+gLhQpdJ5Q6Qp20uLUdknK +6siO9ovXLggZ7GbFdscV1tkDMx7WFVXl2hYWL3Hw0fQ/yFBpORIBuRG+HizgYnEq +BQaZL66TdZ4MIH35PW/2Ox9q+szjTV4ATxnEZgJSn/xkb9OrRWcPPc+DUDRwNLvF +f5tJbsn3W9pZibzr6vAGhTsH0EY0fj9unJex4QWnS8C2dWiudJRuh1+FiK3R1mG9 +JLuVctRrbCApsp0XrquQD68Ts7NF6w6wNqXhB4mNFujNm3AFbhF4mByU39UL7AG2 +iiNoV7ydJmXvhoERcxVFzz/mNq5kDUoM79VgIuqyxz1CRnEx0LWIvqpReme2ElcW +WuB0oZKY/IPb1haoouBzBJTu6W9sYxABBM0pohUz/snZ/dfBu/XFhrhR80gtVjh8 +Q5OFne2lS7hs/Qz4FZkY27VGctzMsOy17vqdxwBSMnKy6Xnkanvau5PzShiEeoiC +dJvG19nKH07Jg8sQRaHCaoFWXjExgeDo4qHF2ODWXAfBXUpRhMUCAwEAAaNTMFEw +HQYDVR0OBBYEFH/7mpljxuqRaro1y9gXEIKFNz8CMB8GA1UdIwQYMBaAFH/7mplj +xuqRaro1y9gXEIKFNz8CMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQAD +ggIBAFQo/ZwtS3pno8pcPKooMBcvy+KyzFfwvQgtg65O4ltSmXKjfBKeB9IBasG1 +irHINcHMNK3u1C9gO/uufKiNOq5p5vxgU0EumaetXVh/ZmOxrgt5FLkmwxXkwaq7 +wrfQvO9Z4skhTNQ3SIOQwqSDtVKUJSHnaKlUgF/lZyFh1FW+DehWsWK0bdgDtdFh +f+Tfj9hBKaZSqnP0vv1x4tTL17bPTarrHMsEZWmEOtOv4/MNuUAhMzrcJkcpoQtl +GVMT2axVAjqATL9Liwy0UvRJIbK0nn8uO2R+8KGy2wdtCwHsrTq0Nq7JIcYlDClY +1MIUPGKMXFUlM84DsSzDItjCTL9Ugf1Nunruumdpo/+Sv3VVeOp1IX/nP44Bp7XU +gqpUvi7qF2n5o1OdXJmxfuTb8Qs1zB8SDPmhpsuJ9E/Ch1v4KUa2SJOhGSBPf02n +dj9zYXuloyRKMuPUFbnTxOI9YIxyfNUZT32D3s4k6MQP3rz2At6wfOVR/SQvbk+e ++IAMnxVWv34RkJzCBB4opE867T33XdpjzSbSj7qiFMC7szxdmE5rpKa6nZuEGz8q +HtkDWipeaRG9HAxOX/NJlac1aP8hQxJ9cIQwVSY2KqAFHIE5MtSpH4XXuoXOvkzU +NEAjtiKuJ8khbl+FrGZ7V3VbNZbzb5hHYcfXgb3LuiwehQ1E +-----END CERTIFICATE----- +-----BEGIN PRIVATE KEY----- +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 +s1vWHzIABKW/htxysnONHEUPla0d0A== +-----END PRIVATE KEY----- diff --git a/Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/__init__.py b/Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/digestdowngrade.py b/Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/digestdowngrade.py new file mode 100644 index 0000000..f948c1b --- /dev/null +++ b/Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/digestdowngrade.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +import os + + +class Module: + def __init__(self, incoming=False, verbose=False, options=None): + # extract the file name from __file__. __file__ is proxymodules/name.py + self.name = os.path.splitext(os.path.basename(__file__))[0] + self.description = 'Find HTTP Digest Authentication and replace it with a Basic Auth' + self.verbose = verbose + self.realm = 'tcpproxy' + + if options is not None: + if 'realm' in options.keys(): + self.realm = bytes(options['realm'], 'ascii') + + def detect_linebreak(self, data): + line = data.split(b'\n', 1)[0] + if line.endswith(b'\r'): + return b'\r\n' + else: + return b'\n' + + def execute(self, data): + delimiter = self.detect_linebreak(data) + lines = data.split(delimiter) + for index, line in enumerate(lines): + if line.lower().startswith(b'www-authenticate: digest'): + lines[index] = b'WWW-Authenticate: Basic realm="%s"' % self.realm + return delimiter.join(lines) + + def help(self): + h = '\trealm: use this instead of the default "tcpproxy"\n' + return h + + +if __name__ == '__main__': + print('This module is not supposed to be executed alone!') diff --git a/Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/hexdump.py b/Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/hexdump.py new file mode 100644 index 0000000..e9bc3a5 --- /dev/null +++ b/Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/hexdump.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +import os.path as path + + +class Module: + def __init__(self, incoming=False, verbose=False, options=None): + # extract the file name from __file__. __file__ is proxymodules/name.py + self.name = path.splitext(path.basename(__file__))[0] + self.description = 'Print a hexdump of the received data' + self.incoming = incoming # incoming means module is on -im chain + self.len = 16 + if options is not None: + if 'length' in options.keys(): + self.len = int(options['length']) + + def help(self): + return '\tlength: bytes per line (int)' + + def execute(self, data): + # this is a pretty hex dumping function directly taken from + # http://code.activestate.com/recipes/142812-hex-dumper/ + result = [] + digits = 2 + for i in range(0, len(data), self.len): + s = data[i:i + self.len] + hexa = ' '.join(['%0*X' % (digits, x) for x in s]) + text = ''.join([chr(x) if 0x20 <= x < 0x7F else '.' for x in s]) + result.append("%04X %-*s %s" % (i, self.len * (digits + 1), hexa, text)) + print("\n".join(result)) + return data + + +if __name__ == '__main__': + print ('This module is not supposed to be executed alone!') diff --git a/Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/http_ok.py b/Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/http_ok.py new file mode 100644 index 0000000..02444d8 --- /dev/null +++ b/Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/http_ok.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +import os.path as path + + +class Module: + def __init__(self, incoming=False, verbose=False, options=None): + # extract the file name from __file__. __file__ is proxymodules/name.py + self.name = path.splitext(path.basename(__file__))[0] + self.description = 'Prepend HTTP response header' + self.server = None + if options is not None: + if 'server' in options.keys(): + self.server = bytes(options['server'], 'ascii') + + # source will be set by the proxy thread later on + self.source = None + + def execute(self, data): + if self.server is None: + self.server = bytes(self.source[0], 'ascii') + + http = b"HTTP/1.1 200 OK\r\n" + http += b"Server: %s\r\n" % self.server + http += b"Connection: keep-alive\r\n" + http += b"Content-Length: %d\r\n" % len(data) + + return http + b"\r\n" + data + + def help(self): + h = '\tserver: remote source, used in response Server header\n' + return h + + +if __name__ == '__main__': + print('This module is not supposed to be executed alone!') diff --git a/Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/http_post.py b/Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/http_post.py new file mode 100644 index 0000000..f532d32 --- /dev/null +++ b/Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/http_post.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +import os.path as path + + +class Module: + def __init__(self, incoming=False, verbose=False, options=None): + # extract the file name from __file__. __file__ is proxymodules/name.py + self.name = path.splitext(path.basename(__file__))[0] + self.description = 'Prepend HTTP header' + self.incoming = incoming # incoming means module is on -im chain + self.targethost = None + self.targetport = None + if options is not None: + if 'host' in options.keys(): + self.targethost = bytes(options['host'], 'ascii') + if 'port' in options.keys(): + self.targetport = bytes(options['port'], 'ascii') + + # destination will be set by the proxy thread later on + self.destination = None + + def execute(self, data): + if self.targethost is None: + self.targethost = bytes(self.destination[0], 'ascii') + if self.targetport is None: + self.targetport = bytes(str(self.destination[1]), 'ascii') + http = b"POST /to/%s/%s HTTP/1.1\r\n" % (self.targethost, self.targetport) + http += b"Host: %s\r\n" % self.targethost + + http += b"Connection: keep-alive\r\n" + http += b"Content-Length: %d\r\n" % len(data) + return http + b"\r\n" + str(data) + + def help(self): + h = '\thost: remote target, used in request URL and Host header\n' + h += '\tport: remote target port, used in request URL\n' + return h + + +if __name__ == '__main__': + print('This module is not supposed to be executed alone!') diff --git a/Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/http_strip.py b/Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/http_strip.py new file mode 100644 index 0000000..e96a63f --- /dev/null +++ b/Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/http_strip.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +import os.path as path + + +class Module: + def __init__(self, incoming=False, verbose=False, options=None): + # extract the file name from __file__. __file__ is proxymodules/name.py + self.name = path.splitext(path.basename(__file__))[0] + self.description = 'Remove HTTP header from data' + self.incoming = incoming # incoming means module is on -im chain + + def detect_linebreak(self, data): + line = data.split(b'\n', 1)[0] + if line.endswith(b'\r'): + return b'\r\n' * 2 + else: + return b'\n' * 2 + + def execute(self, data): + delimiter = self.detect_linebreak(data) + if delimiter in data: + data = data.split(delimiter, 1)[1] + return data + + +if __name__ == '__main__': + print('This module is not supposed to be executed alone!') diff --git a/Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/javaxml.py b/Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/javaxml.py new file mode 100644 index 0000000..049da8d --- /dev/null +++ b/Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/javaxml.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# THIS MODULE DOES NOT WORK AND WILL BE REPLACED, DO NOT USE + +import os.path as path +import platform +if 'java' in platform.system().lower(): + import java.io as io + from com.thoughtworks.xstream import XStream + from java.lang import Exception + + +class Module: + def __init__(self, incoming=False, verbose=False, options=None): + self.is_jython = 'java' in platform.system().lower() + # extract the file name from __file__. __file__ is proxymodules/name.py + self.name = path.splitext(path.basename(__file__))[0] + self.description = 'Serialization or deserialization of Java objects' if self.is_jython else \ + 'Serialization or deserialization of Java objects (needs jython)' + self.incoming = incoming # incoming means module is on -im chain + self.execute = self.error + + if options is not None: + if 'mode' in options.keys(): + if 'deserial' in options['mode']: + self.execute = self.deserial + elif 'serial' in options['mode']: + self.execute = self.serial + + def help(self): + return '\tmode: [serial|deserial] select deserialization (to XML) or serialization (to Java object)' + + def deserial(self, data): + if not self.is_jython: + print ('[!] This module can only be used in jython!') + return data + + try: + # turn data into a Java object + bis = io.ByteArrayInputStream(data) + ois = io.ObjectInputStream(bis) + obj = ois.readObject() + + # converting Java object to XML structure + xs = XStream() + xml = xs.toXML(obj) + return xml + except Exception as e: + print ('[!] Caught Exception. Could not convert.\n') + return data + + def serial(self, data): + if not self.is_jython: + print ('[!] This module can only be used in jython!') + return data + try: + # Creating XStream object and creating Java object from XML structure + xs = XStream() + serial = xs.fromXML(data) + + # writing created Java object to and serializing it with ObjectOutputStream + bos = io.ByteArrayOutputStream() + oos = io.ObjectOutputStream(bos) + oos.writeObject(serial) + + # I had a problem with signed vs. unsigned bytes, hence the & 0xff + return "".join([chr(x & 0xff) for x in bos.toByteArray().tolist()]) + except Exception as e: + print ('[!] Caught Exception. Could not convert.\n') + return data + + def error(self, data): + print ('[!] Unknown mode. Please specify mode=[serial|deserial].') + return data + + +if __name__ == '__main__': + print ('This module is not supposed to be executed alone!') diff --git a/Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/log.py b/Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/log.py new file mode 100644 index 0000000..1af9ee4 --- /dev/null +++ b/Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/log.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +import os.path as path +import time + + +class Module: + def __init__(self, incoming=False, verbose=False, options=None): + # extract the file name from __file__. __file__ is proxymodules/name.py + self.name = path.splitext(path.basename(__file__))[0] + self.description = 'Log data in the module chain. Use in addition to general logging (-l/--log).' + self.incoming = incoming # incoming means module is on -im chain + self.find = None # if find is not None, this text will be highlighted + # file: the file name, format is (in|out)-20160601-112233.13413 + self.file = ('in-' if incoming else 'out-') + \ + time.strftime('%Y%m%d-%H%M%S.') + str(time.time()).split('.')[1] + if options is not None: + if 'file' in options.keys(): + self.file = options['file'] + self.handle = None + + def __del__(self): + if self.handle is not None: + self.handle.close() + + def execute(self, data): + if self.handle is None: + self.handle = open(self.file, 'wb', 0) # unbuffered + print('Logging to file', self.file) + logentry = bytes(time.strftime('%Y%m%d-%H%M%S') + ' ' + str(time.time()) + '\n', 'ascii') + logentry += data + logentry += b'-' * 20 + b'\n' + self.handle.write(logentry) + return data + + def help(self): + h = '\tfile: name of logfile' + return h + + +if __name__ == '__main__': + print('This module is not supposed to be executed alone!') diff --git a/Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/mqtt.py b/Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/mqtt.py new file mode 100644 index 0000000..a0c8071 --- /dev/null +++ b/Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/mqtt.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +import os.path as path +import paho.mqtt.client as mqtt +from distutils.util import strtobool + + +class Module: + def __init__(self, incoming=False, verbose=False, options=None): + # extract the file name from __file__. __file__ is proxymodules/name.py + self.name = path.splitext(path.basename(__file__))[0] + self.description = 'Publish the data to an MQTT server' + self.incoming = incoming # incoming means module is on -im chain + self.client_id = '' + self.username = None + self.password = None + self.server = None + self.port = 1883 + self.topic = '' + self.hex = False + if options is not None: + if 'clientid' in options.keys(): + self.client_id = options['clientid'] + if 'server' in options.keys(): + self.server = options['server'] + if 'username' in options.keys(): + self.username = options['username'] + if 'password' in options.keys(): + self.password = options['password'] + if 'port' in options.keys(): + try: + self.port = int(options['port']) + if self.port not in range(1, 65536): + raise ValueError + except ValueError: + print(f'port: invalid port {options["port"]}, using default {self.port}') + if 'topic' in options.keys(): + self.topic = options['topic'].strip() + if 'hex' in options.keys(): + try: + self.hex = bool(strtobool(options['hex'])) + except ValueError: + print(f'hex: {options["hex"]} is not a bool value, falling back to default value {self.hex}.') + + if self.server is not None: + self.mqtt = mqtt.Client(self.client_id) + if self.username is not None or self.password is not None: + self.mqtt.username_pw_set(self.username, self.password) + self.mqtt.connect(self.server, self.port) + else: + self.mqtt = None + + def execute(self, data): + if self.mqtt is not None: + + if self.hex is True: + self.mqtt.publish(self.topic, data.hex()) + else: + self.mqtt.publish(self.topic, data) + return data + + def help(self): + h = '\tserver: server to connect to, required\n' + h += ('\tclientid: what to use as client_id, default is empty\n' + '\tusername: username\n' + '\tpassword: password\n' + '\tport: port to connect to, default 1883\n' + '\ttopic: topic to publish to, default is empty\n' + '\thex: encode data as hex before sending it. AAAA becomes 41414141.') + return h + + +if __name__ == '__main__': + print('This module is not supposed to be executed alone!') diff --git a/Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/removegzip.py b/Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/removegzip.py new file mode 100644 index 0000000..ccd6835 --- /dev/null +++ b/Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/removegzip.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +import os.path as path + + +class Module: + def __init__(self, incoming=False, verbose=False, options=None): + # extract the file name from __file__. __file__ is proxymodules/name.py + self.name = path.splitext(path.basename(__file__))[0] + self.description = 'Replace gzip in the list of accepted encodings ' \ + 'in a HTTP request with booo.' + self.incoming = incoming # incoming means module is on -im chain + # I chose to replace gzip instead of removing it to keep the parsing + # logic as simple as possible. + + def execute(self, data): + try: + # split at \r\n\r\n to split the request into header and body + header, body = data.split(b'\r\n\r\n', 1) + except ValueError: + # no \r\n\r\n, so probably not HTTP, we can go now + return data + # now split the header string into its lines + headers = header.split(b'\r\n') + + for h in headers: + if h.lower().startswith(b'accept-encoding:') and b'gzip' in h: + headers[headers.index(h)] = h.replace(b'gzip', b'booo') + break + + return b'\r\n'.join(headers) + b'\r\n\r\n' + body + + +if __name__ == '__main__': + print('This module is not supposed to be executed alone!') diff --git a/Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/replace.py b/Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/replace.py new file mode 100644 index 0000000..258f8d2 --- /dev/null +++ b/Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/replace.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +import os +import re + + +class Module: + def __init__(self, incoming=False, verbose=False, options=None): + # extract the file name from __file__. __file__ is proxymodules/name.py + self.name = os.path.splitext(os.path.basename(__file__))[0] + self.description = 'Replace text on the fly by using regular expressions in a file or as module parameters' + self.verbose = verbose + self.search = None + self.replace = None + self.filename = None + self.separator = ':' + + if options is not None: + if 'search' in options.keys(): + self.search = bytes(options['search'], 'ascii') + if 'replace' in options.keys(): + self.replace = bytes(options['replace'], 'ascii') + if 'file' in options.keys(): + self.filename = options['file'] + try: + open(self.filename) + except IOError as ioe: + print("Error opening %s: %s" % (self.filename, ioe.strerror)) + self.filename = None + if 'separator' in options.keys(): + self.separator = options['separator'] + + def execute(self, data): + pairs = [] # list of (search, replace) tuples + if self.search is not None and self.replace is not None: + pairs.append((self.search, self.replace)) + + if self.filename is not None: + for line in open(self.filename).readlines(): + try: + search, replace = line.split(self.separator, 1) + pairs.append((bytes(search.strip(), 'ascii'), bytes(replace.strip(), 'ascii'))) + except ValueError: + # line does not contain separator and will be ignored + pass + + for search, replace in pairs: + # TODO: verbosity + data = re.sub(search, replace, data) + + return data + + def help(self): + h = '\tsearch: string or regular expression to search for\n' + h += ('\treplace: string the search string should be replaced with\n') + h += ('\tfile: file containing search:replace pairs, one per line\n') + h += ('\tseparator: define a custom search:replace separator in the file, e.g. search#replace\n') + h += ('\n\tUse at least file or search and replace (or both).\n') + return h + + +if __name__ == '__main__': + print('This module is not supposed to be executed alone!') diff --git a/Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/size.py b/Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/size.py new file mode 100644 index 0000000..bf6aabe --- /dev/null +++ b/Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/size.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +import os.path as path +from distutils.util import strtobool + + +class Module: + def __init__(self, incoming=False, verbose=False, options=None): + # extract the file name from __file__. __file__ is proxymodules/name.py + self.name = path.splitext(path.basename(__file__))[0] + self.description = 'Print the size of the data passed to the module' + self.verbose = verbose + self.source = None + self.destination = None + self.incoming = incoming + if options is not None: + if 'verbose' in options.keys(): + self.verbose = bool(strtobool(options['verbose'])) + + def execute(self, data): + size = len(data) + msg = "Received %d bytes" % size + if self.verbose: + msg += " from %s:%d" % self.source + msg += " for %s:%d" % self.destination + print(msg) + return data + + def help(self): + h = '\tverbose: override the global verbosity setting' + return h + + +if __name__ == '__main__': + print('This module is not supposed to be executed alone!') diff --git a/Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/size404.py b/Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/size404.py new file mode 100644 index 0000000..cf63a36 --- /dev/null +++ b/Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/size404.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python2 +import os.path as path +import time +from distutils.util import strtobool + + +class Module: + def __init__(self, incoming=False, verbose=False, options=None): + # extract the file name from __file__. __file__ is proxymodules/name.py + self.name = path.splitext(path.basename(__file__))[0] + self.description = 'Change HTTP responses of a certain size to 404.' + self.incoming = incoming # incoming means module is on -im chain + self.size = 2392 # if a response has this value as content-length, it will become a 404 + self.verbose = False + self.custom = False + self.rewriteall = False # will we block the first occurence? + self.firstfound = False # have we found the first occurence yet? + self.resetinterval = None # if we haven't found a fitting response in this many seconds, reset the state and set first to False again + self.timer = time.time() + if options is not None: + if 'size' in options.keys(): + try: + self.size = int(options['size']) + except ValueError: + pass # use the default if you can't parse the parameter + if 'verbose' in options.keys(): + self.verbose = bool(strtobool(options['verbose'])) + if 'custom' in options.keys(): + try: + with open(options['custom'], 'rb') as handle: + self.custom = handle.read() + except Exception: + print('Can\'t open custom error file, not using it.') + self.custom = False + if 'rewriteall' in options.keys(): + self.rewriteall = bool(strtobool(options['rewriteall'])) + if 'reset' in options.keys(): + try: + self.resetinterval = float(options['reset']) + except ValueError: + pass # use the default if you can't parse the parameter + + def execute(self, data): + contentlength = b'content-length: ' + bytes(str(self.size), 'ascii') + if data.startswith(b'HTTP/1.1 200 OK') and contentlength in data.lower(): + if self.resetinterval is not None: + t = time.time() + if t - self.timer >= self.resetinterval: + if self.verbose: + print('Timer elapsed') + self.firstfound = False + self.timer = t + if self.rewriteall is False and self.firstfound is False: + # we have seen this response size for the first time and are not blocking the first one + self.firstfound = True + if self.verbose: + print('Letting this response through') + return data + if self.custom is not False: + data = self.custom + if self.verbose: + print('Replaced response with custom response') + else: + data = data.replace(b'200 OK', b'404 Not Found', 1) + if self.verbose: + print('Edited return code') + return data + + def help(self): + h = '\tsize: if a response has this value as content-length, it will become a 404\n' + h += ('\tverbose: print a message if a string is replaced\n' + '\tcustom: path to a file containing a custom response, will replace the received response\n' + '\trewriteall: if set, it will rewrite all responses. Default is to let the first on through' + '\treset: number of seconds after which we will reset the state and will let the next response through.') + return h + + +if __name__ == '__main__': + print('This module is not supposed to be executed alone!') diff --git a/Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/syssec.py b/Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/syssec.py new file mode 100644 index 0000000..2dcde38 --- /dev/null +++ b/Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/syssec.py @@ -0,0 +1,28 @@ +import os.path as path +import json +import socket + +class Module: + def __init__(self, incoming=False, verbose=False, options=None): + # extract the file name from __file__. __file__ is proxymodules/name.py + self.name = path.splitext(path.basename(__file__))[0] + self.description = 'Simply print the received data as text' + self.incoming = incoming # incoming means module is on -im chain + self.find = None # if find is not None, this text will be highlighted + + def execute(self, data): + print(f"Incoming data: {data}") + + ### Work with data here ### + # data_json = json.loads(data) + # data_json["content"] = "Blablabla" + # data = json.dumps(data_json) + # print(f"Outgoing data: {data}") + # return data + "\n" + + print(f"Outgoing data: {data}") + return data + + +if __name__ == '__main__': + print('This module is not supposed to be executed alone!') diff --git a/Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/textdump.py b/Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/textdump.py new file mode 100644 index 0000000..a192899 --- /dev/null +++ b/Assignment 4 - Protokollsicherheit (Praxis)/proxy/proxymodules/textdump.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +import os.path as path +from codecs import decode, lookup + + +class Module: + def __init__(self, incoming=False, verbose=False, options=None): + # extract the file name from __file__. __file__ is proxymodules/name.py + self.name = path.splitext(path.basename(__file__))[0] + self.description = 'Simply print the received data as text' + self.incoming = incoming # incoming means module is on -im chain + self.find = None # if find is not None, this text will be highlighted + self.codec = 'latin_1' + if options is not None: + if 'find' in options.keys(): + self.find = bytes(options['find'], 'ascii') # text to highlight + if 'color' in options.keys(): + self.color = bytes('\033[' + options['color'] + 'm', 'ascii') # highlight color + else: + self.color = b'\033[31;1m' + if 'codec' in options.keys(): + codec = options['codec'] + try: + lookup(codec) + self.codec = codec + except LookupError: + print(f"{self.name}: {options['codec']} is not a valid codec, using {self.codec}") + + + def execute(self, data): + if self.find is None: + print(decode(data, self.codec)) + else: + pdata = data.replace(self.find, self.color + self.find + b'\033[0m') + print(decode(pdata, self.codec)) + return data + + def help(self): + h = '\tfind: string that should be highlighted\n' + h += ('\tcolor: ANSI color code. Will be wrapped with \\033[ and m, so' + ' passing 32;1 will result in \\033[32;1m (bright green)') + return h + + +if __name__ == '__main__': + print('This module is not supposed to be executed alone!') diff --git a/Assignment 4 - Protokollsicherheit (Praxis)/proxy/requirements.txt b/Assignment 4 - Protokollsicherheit (Praxis)/proxy/requirements.txt new file mode 100644 index 0000000..ab6e7f6 --- /dev/null +++ b/Assignment 4 - Protokollsicherheit (Praxis)/proxy/requirements.txt @@ -0,0 +1,2 @@ +paho-mqtt +PySocks diff --git a/Assignment 4 - Protokollsicherheit (Praxis)/proxy/tcpproxy.py b/Assignment 4 - Protokollsicherheit (Praxis)/proxy/tcpproxy.py new file mode 100755 index 0000000..c6904f6 --- /dev/null +++ b/Assignment 4 - Protokollsicherheit (Praxis)/proxy/tcpproxy.py @@ -0,0 +1,489 @@ +#!/usr/bin/env python3 +import argparse +import pkgutil +import os +import sys +import threading +import socket +import socks +import ssl +import time +import select +import errno + +# TODO: implement verbose output +# some code snippets, as well as the original idea, from Black Hat Python + + +def is_valid_ip4(ip): + # some rudimentary checks if ip is actually a valid IP + octets = ip.split('.') + if len(octets) != 4: + return False + return octets[0] != 0 and all(0 <= int(octet) <= 255 for octet in octets) + + +def parse_args(): + parser = argparse.ArgumentParser(description='Simple TCP proxy for data ' + + 'interception and ' + + 'modification. ' + + 'Select modules to handle ' + + 'the intercepted traffic.') + + parser.add_argument('-ti', '--targetip', dest='target_ip', + help='remote target IP or host name') + + parser.add_argument('-tp', '--targetport', dest='target_port', type=int, + help='remote target port') + + parser.add_argument('-li', '--listenip', dest='listen_ip', + default='0.0.0.0', help='IP address/host name to listen for ' + + 'incoming data') + + parser.add_argument('-lp', '--listenport', dest='listen_port', type=int, + default=8080, help='port to listen on') + + parser.add_argument('-pi', '--proxy-ip', dest='proxy_ip', default=None, + help='IP address/host name of proxy') + + parser.add_argument('-pp', '--proxy-port', dest='proxy_port', type=int, + default=1080, help='proxy port', ) + + parser.add_argument('-pt', '--proxy-type', dest='proxy_type', default='SOCKS5', choices=['SOCKS4', 'SOCKS5', 'HTTP'], + type = str.upper, help='proxy type. Options are SOCKS5 (default), SOCKS4, HTTP') + + parser.add_argument('-om', '--outmodules', dest='out_modules', + help='comma-separated list of modules to modify data' + + ' before sending to remote target.') + + parser.add_argument('-im', '--inmodules', dest='in_modules', + help='comma-separated list of modules to modify data' + + ' received from the remote target.') + + parser.add_argument('-v', '--verbose', dest='verbose', default=False, + action='store_true', + help='More verbose output of status information') + + parser.add_argument('-n', '--no-chain', dest='no_chain_modules', + action='store_true', default=False, + help='Don\'t send output from one module to the ' + + 'next one') + + parser.add_argument('-l', '--log', dest='logfile', default=None, + help='Log all data to a file before modules are run.') + + parser.add_argument('--list', dest='list', action='store_true', + help='list available modules') + + parser.add_argument('-lo', '--list-options', dest='help_modules', default=None, + help='Print help of selected module') + + parser.add_argument('-s', '--ssl', dest='use_ssl', action='store_true', + default=False, help='detect SSL/TLS as well as STARTTLS') + + parser.add_argument('-sc', '--server-certificate', default='mitm.pem', + help='server certificate in PEM format (default: %(default)s)') + + parser.add_argument('-sk', '--server-key', default='mitm.pem', + help='server key in PEM format (default: %(default)s)') + + parser.add_argument('-cc', '--client-certificate', default=None, + help='client certificate in PEM format in case client authentication is required by the target') + + parser.add_argument('-ck', '--client-key', default=None, + help='client key in PEM format in case client authentication is required by the target') + + return parser.parse_args() + + +def generate_module_list(modstring, incoming=False, verbose=False): + # This method receives the comma-separated module list, imports the modules + # and creates a Module instance for each module. A list of these instances + # is then returned. + # The incoming parameter is True when the modules belong to the incoming + # chain (-im) + # modstring looks like mod1,mod2:key=val,mod3:key=val:key2=val2,mod4 ... + modlist = [] + namelist = modstring.split(',') + for n in namelist: + name, options = parse_module_options(n) + try: + __import__('proxymodules.' + name) + modlist.append(sys.modules['proxymodules.' + name].Module(incoming, verbose, options)) + except ImportError: + print('Module %s not found' % name) + sys.exit(3) + return modlist + + +def parse_module_options(n): + # n is of the form module_name:key1=val1:key2=val2 ... + # this method returns the module name and a dict with the options + n = n.split(':', 1) + if len(n) == 1: + # no module options present + return n[0], None + name = n[0] + optionlist = n[1].split(':') + options = {} + for op in optionlist: + try: + k, v = op.split('=') + options[k] = v + except ValueError: + print(op, ' is not valid!') + sys.exit(23) + return name, options + + +def list_modules(): + # show all available proxy modules + cwd = os.getcwd() + module_path = cwd + os.sep + 'proxymodules' + for _, module, _ in pkgutil.iter_modules([module_path]): + __import__('proxymodules.' + module) + m = sys.modules['proxymodules.' + module].Module() + print(f'{m.name} - {m.description}') + + +def print_module_help(modlist): + # parse comma-separated list of module names, print module help text + modules = generate_module_list(modlist) + for m in modules: + try: + print(f'{m.name} - {m.description}') + print(m.help()) + except AttributeError: + print('\tNo options or missing help() function.') + + +def update_module_hosts(modules, source, destination): + # set source and destination IP/port for each module + # source and destination are ('IP', port) tuples + # this can only be done once local and remote connections have been established + if modules is not None: + for m in modules: + if hasattr(m, 'source'): + m.source = source + if hasattr(m, 'destination'): + m.destination = destination + + +def receive_from(s): + # receive data from a socket until no more data is there + b = b"" + while True: + data = s.recv(4096) + b += data + if not data or len(data) < 4096: + break + return b + + +def handle_data(data, modules, dont_chain, incoming, verbose): + # execute each active module on the data. If dont_chain is set, feed the + # output of one plugin to the following plugin. Not every plugin will + # necessarily modify the data, though. + for m in modules: + vprint(("> > > > in: " if incoming else "< < < < out: ") + m.name, verbose) + if dont_chain: + m.execute(data) + else: + data = m.execute(data) + return data + + +def is_client_hello(sock): + firstbytes = sock.recv(128, socket.MSG_PEEK) + return (len(firstbytes) >= 3 and + firstbytes[0] == 0x16 and + firstbytes[1:3] in [b"\x03\x00", + b"\x03\x01", + b"\x03\x02", + b"\x03\x03", + b"\x02\x00"] + ) + + +def enable_ssl(args, remote_socket, local_socket): + sni = None + + def sni_callback(sock, name, ctx): + nonlocal sni + sni = name + + try: + ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + ctx.sni_callback = sni_callback + ctx.load_cert_chain(certfile=args.server_certificate, + keyfile=args.server_key, + ) + local_socket = ctx.wrap_socket(local_socket, + server_side=True, + ) + except ssl.SSLError as e: + print("SSL handshake failed for listening socket", str(e)) + raise + + try: + ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + if args.client_certificate and args.client_key: + ctx.load_cert_chain(certfile=args.client_certificate, + keyfile=args.client_key, + ) + remote_socket = ctx.wrap_socket(remote_socket, + server_hostname=sni, + ) + except ssl.SSLError as e: + print("SSL handshake failed for remote socket", str(e)) + raise + + return [remote_socket, local_socket] + + +def starttls(args, local_socket, read_sockets): + return (args.use_ssl and + local_socket in read_sockets and + not isinstance(local_socket, ssl.SSLSocket) and + is_client_hello(local_socket) + ) + + +def start_proxy_thread(local_socket, args, in_modules, out_modules): + # This method is executed in a thread. It will relay data between the local + # host and the remote host, while letting modules work on the data before + # passing it on. + remote_socket = socks.socksocket() + + if args.proxy_ip: + proxy_types = {'SOCKS5': socks.SOCKS5, 'SOCKS4': socks.SOCKS4, 'HTTP': socks.HTTP} + remote_socket.set_proxy(proxy_types[args.proxy_type], args.proxy_ip, args.proxy_port) + + try: + remote_socket.connect((args.target_ip, args.target_port)) + vprint('Connected to %s:%d' % remote_socket.getpeername(), args.verbose) + log(args.logfile, 'Connected to %s:%d' % remote_socket.getpeername()) + except socket.error as serr: + if serr.errno == errno.ECONNREFUSED: + for s in [remote_socket, local_socket]: + s.close() + print(f'{time.strftime("%Y%m%d-%H%M%S")}, {args.target_ip}:{args.target_port}- Connection refused') + log(args.logfile, f'{time.strftime("%Y%m%d-%H%M%S")}, {args.target_ip}:{args.target_port}- Connection refused') + return None + elif serr.errno == errno.ETIMEDOUT: + for s in [remote_socket, local_socket]: + s.close() + print(f'{time.strftime("%Y%m%d-%H%M%S")}, {args.target_ip}:{args.target_port}- Connection timed out') + log(args.logfile, f'{time.strftime("%Y%m%d-%H%M%S")}, {args.target_ip}:{args.target_port}- Connection timed out') + return None + else: + for s in [remote_socket, local_socket]: + s.close() + raise serr + + try: + update_module_hosts(out_modules, local_socket.getpeername(), remote_socket.getpeername()) + update_module_hosts(in_modules, remote_socket.getpeername(), local_socket.getpeername()) + except socket.error as serr: + if serr.errno == errno.ENOTCONN: + # kind of a blind shot at fixing issue #15 + # I don't yet understand how this error can happen, but if it happens I'll just shut down the thread + # the connection is not in a useful state anymore + for s in [remote_socket, local_socket]: + s.close() + return None + else: + for s in [remote_socket, local_socket]: + s.close() + print(f"{time.strftime('%Y%m%d-%H%M%S')}: Socket exception in start_proxy_thread") + raise serr + + # This loop ends when no more data is received on either the local or the + # remote socket + running = True + while running: + read_sockets, _, _ = select.select([remote_socket, local_socket], [], []) + + if starttls(args, local_socket, read_sockets): + try: + ssl_sockets = enable_ssl(args, remote_socket, local_socket) + remote_socket, local_socket = ssl_sockets + vprint("SSL enabled", args.verbose) + log(args.logfile, "SSL enabled") + except ssl.SSLError as e: + print("SSL handshake failed", str(e)) + log(args.logfile, "SSL handshake failed", str(e)) + break + + read_sockets, _, _ = select.select(ssl_sockets, [], []) + + for sock in read_sockets: + try: + peer = sock.getpeername() + except socket.error as serr: + if serr.errno == errno.ENOTCONN: + # kind of a blind shot at fixing issue #15 + # I don't yet understand how this error can happen, but if it happens I'll just shut down the thread + # the connection is not in a useful state anymore + for s in [remote_socket, local_socket]: + s.close() + running = False + break + else: + print(f"{time.strftime('%Y%m%d-%H%M%S')}: Socket exception in start_proxy_thread") + raise serr + + data = receive_from(sock) + log(args.logfile, 'Received %d bytes' % len(data)) + + if sock == local_socket: + if len(data): + log(args.logfile, b'< < < out\n' + data) + if out_modules is not None: + data = handle_data(data, out_modules, + args.no_chain_modules, + False, # incoming data? + args.verbose) + remote_socket.send(data.encode() if isinstance(data, str) else data) + else: + vprint("Connection from local client %s:%d closed" % peer, args.verbose) + log(args.logfile, "Connection from local client %s:%d closed" % peer) + remote_socket.close() + running = False + break + elif sock == remote_socket: + if len(data): + log(args.logfile, b'> > > in\n' + data) + if in_modules is not None: + data = handle_data(data, in_modules, + args.no_chain_modules, + True, # incoming data? + args.verbose) + local_socket.send(data) + else: + vprint("Connection to remote server %s:%d closed" % peer, args.verbose) + log(args.logfile, "Connection to remote server %s:%d closed" % peer) + local_socket.close() + running = False + break + + +def log(handle, message, message_only=False): + # if message_only is True, only the message will be logged + # otherwise the message will be prefixed with a timestamp and a line is + # written after the message to make the log file easier to read + if not isinstance(message, bytes): + message = bytes(message, 'ascii') + if handle is None: + return + if not message_only: + logentry = bytes("%s %s\n" % (time.strftime('%Y%m%d-%H%M%S'), str(time.time())), 'ascii') + else: + logentry = b'' + logentry += message + if not message_only: + logentry += b'\n' + b'-' * 20 + b'\n' + handle.write(logentry) + + +def vprint(msg, is_verbose): + # this will print msg, but only if is_verbose is True + if is_verbose: + print(msg) + + +def main(): + args = parse_args() + if args.list is False and args.help_modules is None: + if not args.target_ip: + print('Target IP is required: -ti') + sys.exit(6) + if not args.target_port: + print('Target port is required: -tp') + sys.exit(7) + + if ((args.client_key is None) ^ (args.client_certificate is None)): + print("You must either specify both the client certificate and client key or leave both empty") + sys.exit(8) + + if args.logfile is not None: + try: + args.logfile = open(args.logfile, 'ab', 0) # unbuffered + except Exception as ex: + print('Error opening logfile') + print(ex) + sys.exit(4) + + if args.list: + list_modules() + sys.exit(0) + + if args.help_modules is not None: + print_module_help(args.help_modules) + sys.exit(0) + + if args.listen_ip != '0.0.0.0' and not is_valid_ip4(args.listen_ip): + try: + ip = socket.gethostbyname(args.listen_ip) + except socket.gaierror: + ip = False + if ip is False: + print('%s is not a valid IP address or host name' % args.listen_ip) + sys.exit(1) + else: + args.listen_ip = ip + + if not is_valid_ip4(args.target_ip): + try: + ip = socket.gethostbyname(args.target_ip) + except socket.gaierror: + ip = False + if ip is False: + print('%s is not a valid IP address or host name' % args.target_ip) + sys.exit(2) + else: + args.target_ip = ip + + if args.in_modules is not None: + in_modules = generate_module_list(args.in_modules, incoming=True, verbose=args.verbose) + else: + in_modules = None + + if args.out_modules is not None: + out_modules = generate_module_list(args.out_modules, incoming=False, verbose=args.verbose) + else: + out_modules = None + + # this is the socket we will listen on for incoming connections + proxy_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + proxy_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + + try: + proxy_socket.bind((args.listen_ip, args.listen_port)) + except socket.error as e: + print(e.strerror) + sys.exit(5) + + proxy_socket.listen(100) + log(args.logfile, str(args)) + # endless loop until ctrl+c + try: + while True: + in_socket, in_addrinfo = proxy_socket.accept() + vprint('Connection from %s:%d' % in_addrinfo, args.verbose) + log(args.logfile, 'Connection from %s:%d' % in_addrinfo) + proxy_thread = threading.Thread(target=start_proxy_thread, + args=(in_socket, args, in_modules, + out_modules)) + log(args.logfile, "Starting proxy thread " + proxy_thread.name) + proxy_thread.start() + except KeyboardInterrupt: + log(args.logfile, 'Ctrl+C detected, exiting...') + print('\nCtrl+C detected, exiting...') + sys.exit(0) + + +if __name__ == '__main__': + main()