diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d1c8995 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.class +*.pyc diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..774fdc3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 René Werner + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 151c2c4..3d672b8 100644 --- a/README.md +++ b/README.md @@ -1,87 +1,211 @@ # tcpproxy.py - An intercepting proxy for TCP data + This tool opens a listening socket, receives data and then runs this data through a chain of proxy modules. After the modules are done, the resulting data is sent to the target server. The response is received and again run through a chain of modules before sending the final data back to the client. To intercept the data, you will either have to be the gateway or do some kind of man-in-the-middle attack. Set up iptables so that the PREROUTING chain will modify the destination and send it to the proxy process. The proxy will then send the data on to whatever target was specified. -This tool is inspired and partially based on the TCP proxy example used in Justin Seitz' book "Black Hat Python" by no starch press. +This tool is inspired by and partially based on the TCP proxy example used in Justin Seitz' book "Black Hat Python" by no starch press. ## Usage ``` -$ python2 tcpproxy.py -h -usage: tcpproxy.py [-h] [-li LISTEN_IP] [-ti TARGET_IP] [-lp LISTEN_PORT] - [-tp TARGET_PORT] [-om OUT_MODULES] [-im IN_MODULES] - [-t TIMEOUT] [-v] [-r] [-n] [-l] [-s] +$ ./tcpproxy.py -h +usage: tcpproxy.py [-h] [-ti TARGET_IP] [-tp TARGET_PORT] [-li LISTEN_IP] + [-lp LISTEN_PORT] [-pi PROXY_IP] [-pp PROXY_PORT] + [-pt {SOCKS4,SOCKS5,HTTP}] [-om OUT_MODULES] + [-im IN_MODULES] [-v] [-n] [-l LOGFILE] [--list] + [-lo HELP_MODULES] [-s] [-sc SERVER_CERTIFICATE] + [-sk SERVER_KEY] [-cc CLIENT_CERTIFICATE] [-ck CLIENT_KEY] Simple TCP proxy for data interception and modification. Select modules to handle the intercepted traffic. optional arguments: -h, --help show this help message and exit - -li LISTEN_IP, --listenip LISTEN_IP - IP address to listen for incoming data -ti TARGET_IP, --targetip TARGET_IP - remote target IP - -lp LISTEN_PORT, --listenport LISTEN_PORT - port to listen on + remote target IP or host name -tp TARGET_PORT, --targetport TARGET_PORT remote target port + -li LISTEN_IP, --listenip LISTEN_IP + IP address/host name to listen for incoming data + -lp LISTEN_PORT, --listenport LISTEN_PORT + port to listen on + -pi PROXY_IP, --proxy-ip PROXY_IP + IP address/host name of proxy + -pp PROXY_PORT, --proxy-port PROXY_PORT + proxy port + -pt {SOCKS4,SOCKS5,HTTP}, --proxy-type {SOCKS4,SOCKS5,HTTP} + proxy type. Options are SOCKS5 (default), SOCKS4, HTTP -om OUT_MODULES, --outmodules OUT_MODULES comma-separated list of modules to modify data before sending to remote target. -im IN_MODULES, --inmodules IN_MODULES comma-separated list of modules to modify data received from the remote target. - -t TIMEOUT, --timeout TIMEOUT - Socket timeout to wait for incoming data -v, --verbose More verbose output of status information - -r, --receivefirst Receive data from remote first, e.g. a banner -n, --no-chain Don't send output from one module to the next one - -l, --list list available modules - -s, --ssl use SSL, certificate is mitm.pem + -l LOGFILE, --log LOGFILE + Log all data to a file before modules are run. + --list list available modules + -lo HELP_MODULES, --list-options HELP_MODULES + Print help of selected module + -s, --ssl detect SSL/TLS as well as STARTTLS + -sc SERVER_CERTIFICATE, --server-certificate SERVER_CERTIFICATE + server certificate in PEM format (default: mitm.pem) + -sk SERVER_KEY, --server-key SERVER_KEY + server key in PEM format (default: mitm.pem) + -cc CLIENT_CERTIFICATE, --client-certificate CLIENT_CERTIFICATE + client certificate in PEM format in case client + authentication is required by the target + -ck CLIENT_KEY, --client-key CLIENT_KEY + client key in PEM format in case client authentication + is required by the target ``` You will have to provide TARGET_IP and TARGET_PORT, the default listening settings are 0.0.0.0:8080. To make the program actually useful, you will have to decide which modules you want to use on outgoing (client to server) and incoming (server to client) traffic. You can use different modules for each direction. Pass the list of modules as comma-separated list, e.g. -im mod1,mod4,mod2. The data will be passed to the first module, the returned data will be passed to the second module and so on, unless you use the -n/--no/chain switch. In that case, every module will receive the original data. +You can also pass options to each module: -im mod1:key1=val1,mod4,mod2:key1=val1:key2=val2. To learn which options you can pass to a module use -lo/--list-options like this: -lo mod1,mod2,mod4 -### Modules +## Modules ``` -$ python2 tcpproxy.py -l -deserializer - Deserialize Java objects (needs jython) +$ ./tcpproxy.py --list +digestdowngrade - Find HTTP Digest Authentication and replace it with a Basic Auth hexdump - Print a hexdump of the received data -httpparser - Check if data is HTTP and try to parse it +http_ok - Prepend HTTP response header +http_post - Prepend HTTP header +http_strip - Remove HTTP header from data +log - Log data in the module chain. Use in addition to general logging (-l/--log). removegzip - Replace gzip in the list of accepted encodings in a HTTP request with booo. +replace - Replace text on the fly by using regular expressions in a file or as module parameters +hexreplace - Replace hex data in tcp packets +size - Print the size of the data passed to the module +size404 - Change HTTP responses of a certain size to 404. textdump - Simply print the received data as text -all - use all available modules ``` + Tcpproxy.py uses modules to view or modify the intercepted data. To see the possibly easiest implementation of a module, have a look at the textdump.py module in the proxymodules directory: -``` -#!/usr/bin/env python2 +```python +#!/usr/bin/env python3 +import os.path as path class Module: - def __init__(self): - self.name = 'Text display' + 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 + 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' def execute(self, data): - print data + if self.find is None: + print(data) + else: + pdata = data.replace(self.find, self.color + self.find + b'\033[0m') + print(pdata.decode('ascii')) 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!') +``` + +Every module file contains a class named Module. Every module MUST set self.description and MUST implement an execute method that accepts one parameter, the input data. The execute method MUST return something, this something is then either passed to the next module or sent on. Other than that, you are free to do whatever you want inside a module. +The incoming parameter in the constructor is set to True when the module is in the incoming chain (-im), otherwise it's False. This way, a module knows in which direction the data is flowing (credits to jbarg for this idea). +The verbose parameter is set to True if the proxy is started with -v/--verbose. +The options parameter is a dictionary with the keys and values passed to the module on the command line. Note that if you use the options dictionary in your module, you should also implement a help() method. This method must return a string. Use one line per option, make sure each line starts with a \t character for proper indentation. + +See the hexdump module for an additional options example: + +```python +#!/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): + # -- 8< --- snip + for i in range(0, len(data), self.len): + s = data[i:i + self.len] + # # -- 8< --- snip + if __name__ == '__main__': print 'This module is not supposed to be executed alone!' ``` -Every module file contains a class named Module. Every module MUST set self.description and MUST implement an execute method that accepts one paramter, the input data. The execute method MUST return something, this something is then either passed to the next module or sent on. Other than that, you are free to do whatever you want inside a module. +The above example should give you an idea how to make use of module parameters. A calling example would be: -### Playing with Java objects -The deserializer module implements a way to alter serialized java objects on the fly. To use it, change the CLASSPATH env variable to make sure the custom classes are available to your code. ``` -CLASSPATH=$CLASSPATH:/home/user/test/Someclass.jar jython27 tcpproxy.py -ti 127.0.0.1 -tp 12346 -lp 12345 -om hexdump,deserializer,hexdump +./tcpproxy.py -om hexdump:length=8,http_post,hexdump:length=12 -ti 127.0.0.1 -tp 12345 +< < < < out: hexdump +0000 77 6C 6B 66 6A 6C 77 71 wlkfjlwq +0008 6B 66 6A 68 6C 6B 77 71 kfjhlkwq +0010 6A 65 68 66 6C 6B 65 77 jehflkew +0018 71 6A 66 68 6C 6B 65 77 qjfhlkew +0020 71 6A 66 68 6C 6B 65 77 qjfhlkew +0028 71 6A 66 6C 68 77 71 6B qjflhwqk +0030 65 6A 66 68 77 71 6C 6B ejfhwqlk +0038 65 6A 66 68 0A ejfh. +< < < < out: http_post +< < < < out: hexdump +0000 50 4F 53 54 20 2F 20 48 54 54 50 2F POST / HTTP/ +000C 31 2E 31 0A 48 6F 73 74 3A 20 74 63 1.1.Host: tc +0018 70 70 72 6F 78 79 0A 43 6F 6E 74 65 pproxy.Conte +0024 6E 74 2D 4C 65 6E 67 74 68 3A 20 36 nt-Length: 6 +0030 31 0A 0A 77 6C 6B 66 6A 6C 77 71 6B 1..wlkfjlwqk +003C 66 6A 68 6C 6B 77 71 6A 65 68 66 6C fjhlkwqjehfl +0048 6B 65 77 71 6A 66 68 6C 6B 65 77 71 kewqjfhlkewq +0054 6A 66 68 6C 6B 65 77 71 6A 66 6C 68 jfhlkewqjflh +0060 77 71 6B 65 6A 66 68 77 71 6C 6B 65 wqkejfhwqlke +006C 6A 66 68 0A jfh. ``` -Note that when using jython, the SSL mitm does not seem to work. It looks like a jython bug to me, but I haven't yet done extensive debugging so I can't say for sure. + +You can see how the first hexdump instance gets a length of 8 bytes per row and the second instance gets a length of 12 bytes. To pass more than one option to a single module, seperate the options with a : character, modname:key1=val1:key2=val2... + +## Logging + +You can write all data that is sent or received by the proxy to a file using the -l/--log parameter. Data (and some housekeeping info) is written to the log before passing it to the module chains. If you want to log the state of the data during or after the modules are run, you can use the log proxymodule. Using the chain -im http_post,log:file=log.1,http_strip,log would first log the data after the http_post module to the logfile with the name log.1. The second use of the log module at the end of the chain would write the final state of the data to a logfile with the default name in- right before passing it on . ## TODO -- implement a way to pass parameters to modules -- implement logging (pre-/post modification) -- make the process interactive by implementing some kind of editor module (will probably complicate matters with regard to timeouts) + +- [ ] make the process interactive by implementing some kind of editor module (will probably complicate matters with regard to timeouts, can be done for now by using the burp solution detailed above and modifying data inside burp) +- [ ] Create and maintain a parallel branch that is compatible with jython but also has most of the new stuff introduced after e3290261 + +## Contributions + +I want to thank the following people for spending their valuable time and energy on improving this little tool: + +- [Adrian Vollmer](https://github.com/AdrianVollmer) +- [Michael Füllbier](https://github.com/mfuellbier) +- [Stefan Grönke](https://github.com/gronke) +- [Mattia](https://github.com/sowdust) +- [bjorns163](https://github.com/bjorns163) +- [Pernat1y](https://github.com/Pernat1y) +- [hrzlgnm](https://github.com/hrzlgnm) +- [MKesenheimer](https://github.com/MKesenheimer) diff --git a/mitm.pem b/mitm.pem index cc0669d..2ec6c15 100644 --- a/mitm.pem +++ b/mitm.pem @@ -1,49 +1,83 @@ ------BEGIN PRIVATE KEY----- -MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC9H4C5NkE/X1Me -88y2WjwC2xupndKtXYyljJm5U6ecdiLpTz8wCA0EEFvgFaTf8dKCzk4cuZ6HcWX/ -Iwg19vuY27MombjM67XaV3FoA4DO4VpvEOTRQekNi2E1s94IBqJkDOetef2wEF7U -zvxFvJYvVC30aRUiYNwGWpo8TuiQIk4GeVJbJslrMFr5UbitWkr9zq6t2a7PlylU -8X/Jz5H6gbOjIoy2mJNZfLebPg/a6XAJJpx/nAVTyqoK0b95K2RMmi+EQcKOdbgM -RpdtMie7aVT3gJxKb8nRpKmIyPp5cuNOOvEHGpwltckGOaLjFdkh0uD1fLMhbY31 -e7szZjcNAgMBAAECggEAOQT5e13XODMWTXu12bjE5RuIcJArx6cv023bnxuQqkSX -6/2/kEytF++Ss7Hy3q37CQMIW/K+0BkpZk36mMKZQpHipzgJlobuciDxCSodOMKK -0HeodUrI6BOAwH81TvgpF78oTo48JUwaO1EYkDH2mdhobosMGyxWyfehDtO/nEym -xv20rKqO3h2wx86XVl+ebBXU7fHndgfyYUUyv3QoPZWjL/9Z9MfMwPnxpARMKLPi -onIajXXzi3J/Z2zlXFVF3ARAYg9Wqemq4BnRS20sS+EBaXBbhVazoZIS69WihMGA -riF+HAOUSiS1EDW7TIUpSV2kqFZ5S2FGomKLd8mlhQKBgQDerAiAGJCWADk3Rh3Q -bCIenVIT+0WAMqEVpURRXoFHd6WuEh7g2lgR/ef83C+IuOEh+1rdzT6gubuHvURK -M1WWyr8IuaUUod08ewqSavmMUkHQqBppDh78NmrT81aDNXinazW8jBKkQhm64+ZM -E9WjhQjk3dYqEW+YRYkM0PcCywKBgQDZbf+JjvkWh6ftVGXj3PPbvej4FpOKvrTm -/Bwwaz4FqmqEIGfcktvUTkQYdVz2QfFa8OGWxYiJav0twdRyjI3uwwlwRkgk75WO -rjv2bgyCDhGkpJNkQKcul6QRcTbw3dtn2gbLpWODKHVdlQQKgfl8BatyZR0MwBjh -4bxsQI16hwKBgE2Cuu7EHkhoyYHpIWW0zmezwad89yN5/ELJpa9hY0UabAzc9+yz -dKbGqKOHjfBc0tl+YpIE6QEPxiypAIWHuwpjhv4liUZWVenAttxi6n0jAQ/+BDt/ -k9+dnbAr63h++4Hjuu/oHnEZJVW+ESN4YAysuXzZj7xTF3J8+gkBEIrjAoGADHHO -SWpEeXSkOOI2vrb7whz5g5GPOka2Be5yEpdgwmRBmnRcXXSOXnVoUloNSw71KHZX -AxElQnA8M20/oprG2N6S4Lk1EeAgmD0Cs5US5DK38ct1oCxPJUyKmHD5awnXr/b7 -opZBvtUG+qc3xv4vcFjGulJtOjiYc/0+kpeTQWsCgYBQ8955Rz40lNMQ0GcXULz0 -N2K3sD4lkr/G6ltyaKYWbO6IJM4UM0W6uaLvIBEnCErev0zFtghB+5pkpNzRrMhR -XQaBTwFKk7mMooYE4FPsCMpv3kzvql+PJY0hPOqhgSH6tX8yY46VCVLTx33dYKVF -zBTe9W1WXILDBWsfDe05PA== ------END PRIVATE KEY----- -----BEGIN CERTIFICATE----- -MIIDdTCCAl2gAwIBAgIJAJYUnU1Nr7LzMA0GCSqGSIb3DQEBCwUAMFExCzAJBgNV +MIIFYDCCA0igAwIBAgIJALovM7ADVGykMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX -aWRnaXRzIFB0eSBMdGQxCjAIBgNVBAMMASowHhcNMTUwNTMwMTQxNTUzWhcNMTYw -NTI5MTQxNTUzWjBRMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEh -MB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMQowCAYDVQQDDAEqMIIB -IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvR+AuTZBP19THvPMtlo8Atsb -qZ3SrV2MpYyZuVOnnHYi6U8/MAgNBBBb4BWk3/HSgs5OHLmeh3Fl/yMINfb7mNuz -KJm4zOu12ldxaAOAzuFabxDk0UHpDYthNbPeCAaiZAznrXn9sBBe1M78RbyWL1Qt -9GkVImDcBlqaPE7okCJOBnlSWybJazBa+VG4rVpK/c6urdmuz5cpVPF/yc+R+oGz -oyKMtpiTWXy3mz4P2ulwCSacf5wFU8qqCtG/eStkTJovhEHCjnW4DEaXbTInu2lU -94CcSm/J0aSpiMj6eXLjTjrxBxqcJbXJBjmi4xXZIdLg9XyzIW2N9Xu7M2Y3DQID -AQABo1AwTjAdBgNVHQ4EFgQUCh9nSfLT1wEaXoWWXL7pYE47lYUwHwYDVR0jBBgw -FoAUCh9nSfLT1wEaXoWWXL7pYE47lYUwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0B -AQsFAAOCAQEAFD1KHvbttYcH76SZgzZbu6lABJnxfe4dWFaP6y6JEa75NpAFC67N -r6mK04JuR80c0maaqHakb1I+evJWiJubX5+HfZG5WfCpKkHMLGgDUSmzQOfUdWiQ -3V2VwvSurVuVBotrrklXn/fTEKeeUU1ekqAb0claklc5Yc7ykBo8lf5dq5PPfikv -g2v6XmJaPPrd5r2vjccIpvUQ9dsKGI84ZKPyRUOuHWFzXeTO8Y9ZAtKiiyrEy2te -ot7aQWrmw7s5FjVYm7uIS8xhkmueNQoq5StlNXoW6aHZ6Qg5GB3zLyHguSRLKSaa -xQr+/zWzsR3IRgd5tLHBbjQbOcLAyQcS3g== +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/proxymodules/delay.py b/proxymodules/delay.py new file mode 100644 index 0000000..6bed5fa --- /dev/null +++ b/proxymodules/delay.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +import os.path as path +import time +import random + + +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 = 'Set delay in passing through the packet, used for simulating various network conditions' + self.incoming = incoming # incoming means module is on -im chain + self.random = False + self.seconds = None + self.verbose = verbose + if options is not None: + if 'seconds' in options.keys(): + try: + self.seconds = abs(float(options['seconds'])) + except ValueError: + print(f"Can't parse {options['seconds']} as float") + pass # leave it set to None + if 'random' in options.keys(): + # set random=true to enable delay randomization + self.random = (options['random'].lower() == 'true') + if self.random and self.seconds is None: + # set a upper bound of 1s if seconds is not being used, otherwise keep the seconds value + self.seconds = 1.0 + + def execute(self, data): + delay = None + if self.random: + delay = round(random.uniform(0, self.seconds), 3) # round to milliseconds + else: + delay = self.seconds + # here delay is either None or a positive float + # if the module was instantiated w/o either seconds or random, effectively nothing happens + if delay is not None: + if self.verbose: + print(f"Waiting {delay}s.") + time.sleep(delay) + return data + + def help(self): + h = '\tseconds: number of seconds you want the packet to be delayed\n' + h += ('\trandom: optional; set to true to randomize the delay between 0 and seconds (default: 1.0s)\n') + return h + + +if __name__ == '__main__': + print('This module is not supposed to be executed alone!') diff --git a/proxymodules/deserializer.py b/proxymodules/deserializer.py deleted file mode 100644 index 51198bb..0000000 --- a/proxymodules/deserializer.py +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env python2 -# -*- coding: utf-8 -*- -import platform -if 'java' in platform.system().lower(): - import java.io as io - import java.lang.reflect.Modifier as Modifier - - -class Module: - """ - This is a quick PoC and definitely needs to be modified to be useful. - """ - def __init__(self): - self.is_jython = 'java' in platform.system().lower() - self.name = 'Number Changer' - self.description = 'Deserialize Java objects' if self.is_jython else \ - 'Deserialize Java objects (needs jython)' - - def execute(self, data): - # this is a quick and dirty PoC to show that jython can be used to - # deserialize Java objects on the fly, manipulate them and send them - # on their way. - if not self.is_jython: - print '[!] This module can only be used in jython!' - return data - - # turn data into a Java object - bis = io.ByteArrayInputStream(data) - ois = io.ObjectInputStream(bis) - obj = ois.readObject() - # at this point you have the deserialized object in obj, do what you - # want. Set the jython classpath to include custom classes and cast obj - - mod = Modifier() - # get all methods of the object - print type(obj) - methods = obj.getClass().getDeclaredMethods() - print "Object is of class " + obj.getClass().getName() - print "Methods:" - for m in methods: - print "\t%s %s %s(%s)" % (mod.toString(m.getModifiers()), - m.getGenericReturnType(), - m.getName(), - m.getGenericParameterTypes().tolist()) - - declared_fields = obj.getClass().getDeclaredFields() - print "Fields:" - for f in declared_fields: - if not f.isAccessible(): - f.setAccessible(True) - print "\t%s %s %s => %s" % (mod.toString(f.getModifiers()), - f.getType(), f.getName(), f.get(obj)) - - # serialize the object again and turn it into a string again - bos = io.ByteArrayOutputStream() - oos = io.ObjectOutputStream(bos) - oos.writeObject(obj) - - # I had a problem with signed vs. unsigned bytes, hence the & 0xff - return "".join([chr(x & 0xff) for x in bos.toByteArray().tolist()]) - -if __name__ == '__main__': - print 'This module is not supposed to be executed alone!' diff --git a/proxymodules/digestdowngrade.py b/proxymodules/digestdowngrade.py new file mode 100644 index 0000000..f948c1b --- /dev/null +++ b/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/proxymodules/hexdump.py b/proxymodules/hexdump.py index c3091ce..e9bc3a5 100644 --- a/proxymodules/hexdump.py +++ b/proxymodules/hexdump.py @@ -1,27 +1,34 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 +import os.path as path class Module: - def __init__(self): - self.name = 'Hexdump display' + 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 = [] - length = 16 - digits = 4 if isinstance(data, unicode) else 2 - - for i in xrange(0, len(data), length): - s = data[i:i+length] - hexa = b' '.join(["%0*X" % (digits, ord(x)) for x in s]) - text = b''.join([x if 0x20 <= ord(x) < 0x7F else b'.' for x in s]) - result.append(b"%04X %-*s %s" % (i, length*(digits + 1), - hexa, text)) - - print b'\n'.join(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!' + print ('This module is not supposed to be executed alone!') diff --git a/proxymodules/hexreplace.py b/proxymodules/hexreplace.py new file mode 100644 index 0000000..4488853 --- /dev/null +++ b/proxymodules/hexreplace.py @@ -0,0 +1,79 @@ +#!/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 = 'Replace hex data on the fly defining search and replace-pairs in a file or as module parameters' + self.verbose = verbose + self.filename = None + self.separator = ':' + self.len = 16 + + search = None + if options is not None: + if 'search' in options.keys(): + search = bytes.fromhex(options['search']) + if 'replace' in options.keys(): + replace = bytes.fromhex(options['replace']) + 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'] + + self.pairs = [] # list of (search, replace) tuples + if search is not None and replace is not None: + self.pairs.append((search, replace)) + + if self.filename is not None: + for line in open(self.filename).readlines(): + try: + search, replace = line.split(self.separator, 1) + self.pairs.append((bytes.fromhex(search.strip()), bytes.fromhex(replace.strip()))) + except ValueError: + # line does not contain separator and will be ignored + pass + + def hexdump(self, data): + 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)) + + def execute(self, data): + if self.verbose: + print(f"Incoming packet with size {len(data)}:") + for search, replace in self.pairs: + if search in data: + if self.verbose: + print("########## data found ###########") + print("[Before:]") + self.hexdump(data) + data = data.replace(search, replace) + if self.verbose: + print("[After:]") + self.hexdump(data) + return data + + def help(self): + h = '\tsearch: hex string (i.e. "deadbeef") to search for\n' + h += ('\treplace: hex 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/proxymodules/http_ok.py b/proxymodules/http_ok.py new file mode 100644 index 0000000..02444d8 --- /dev/null +++ b/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/proxymodules/http_post.py b/proxymodules/http_post.py new file mode 100644 index 0000000..f532d32 --- /dev/null +++ b/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/proxymodules/http_strip.py b/proxymodules/http_strip.py new file mode 100644 index 0000000..e96a63f --- /dev/null +++ b/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/proxymodules/log.py b/proxymodules/log.py new file mode 100644 index 0000000..1af9ee4 --- /dev/null +++ b/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/proxymodules/mqtt.py b/proxymodules/mqtt.py new file mode 100644 index 0000000..a0c8071 --- /dev/null +++ b/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/proxymodules/removegzip.py b/proxymodules/removegzip.py index feec8c1..ccd6835 100644 --- a/proxymodules/removegzip.py +++ b/proxymodules/removegzip.py @@ -1,30 +1,34 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 +import os.path as path class Module: - def __init__(self): - self.name = 'Remove gzip' + 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('\r\n\r\n', 1) + 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('\r\n') + headers = header.split(b'\r\n') for h in headers: - if h.lower().startswith('accept-encoding:') and 'gzip' in h: - headers[headers.index(h)] = h.replace('gzip', 'booo') + if h.lower().startswith(b'accept-encoding:') and b'gzip' in h: + headers[headers.index(h)] = h.replace(b'gzip', b'booo') break - return '\r\n'.join(headers) + '\r\n\r\n' + body + 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!' + print('This module is not supposed to be executed alone!') diff --git a/proxymodules/replace.py b/proxymodules/replace.py new file mode 100644 index 0000000..258f8d2 --- /dev/null +++ b/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/proxymodules/size.py b/proxymodules/size.py new file mode 100644 index 0000000..bf6aabe --- /dev/null +++ b/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/proxymodules/size404.py b/proxymodules/size404.py new file mode 100644 index 0000000..cf63a36 --- /dev/null +++ b/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/proxymodules/textdump.py b/proxymodules/textdump.py index 79af84c..a192899 100644 --- a/proxymodules/textdump.py +++ b/proxymodules/textdump.py @@ -1,14 +1,46 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 +import os.path as path +from codecs import decode, lookup class Module: - def __init__(self): - self.name = 'Text display' + 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): - print 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!' + print('This module is not supposed to be executed alone!') diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ab6e7f6 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +paho-mqtt +PySocks diff --git a/tcpproxy.py b/tcpproxy.py old mode 100644 new mode 100755 index 50c8bf7..4143a14 --- a/tcpproxy.py +++ b/tcpproxy.py @@ -1,11 +1,15 @@ -#!/usr/bin/env python2 +#!/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 @@ -26,18 +30,33 @@ def parse_args(): '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 to listen for ' + + default='0.0.0.0', help='IP address/host name to listen for ' + 'incoming data') - parser.add_argument('-ti', '--targetip', dest='target_ip', - help='remote target IP') - parser.add_argument('-lp', '--listenport', dest='listen_port', type=int, default=8080, help='port to listen on') - parser.add_argument('-tp', '--targetport', dest='target_port', type=int, - help='remote target port') + parser.add_argument('-si', '--sourceip', dest='source_ip', + help='IP address the other end will see') + + parser.add_argument('-sp', '--sourceport', dest='source_port', type=int, + help='source port the other end will see') + + 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' + @@ -47,87 +66,132 @@ def parse_args(): help='comma-separated list of modules to modify data' + ' received from the remote target.') - parser.add_argument('-t', '--timeout', dest='timeout', type=int, default=5, - help='Socket timeout to wait for incoming data') - parser.add_argument('-v', '--verbose', dest='verbose', default=False, action='store_true', help='More verbose output of status information') - parser.add_argument('-r', '--receivefirst', dest='receive_first', - action='store_true', default=False, - help='Receive data from remote first, e.g. a banner') - 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', '--list', dest='list', action='store_true', + 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='use SSL, certificate is mitm.pem') + 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): +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 = [] - if modstring == 'all': - cwd = os.getcwd() - # all modules must exist in the proxymodules directory - module_path = cwd + os.sep + 'proxymodules' - for _, n, _ in pkgutil.iter_modules([module_path]): - __import__('proxymodules.' + n) - modlist.append(sys.modules['proxymodules.' + n].Module()) - else: - namelist = modstring.split(',') - for n in namelist: - try: - __import__('proxymodules.' + n) - modlist.append(sys.modules['proxymodules.' + n].Module()) - except ImportError: - print 'Module %s not found' % n - sys.exit(3) + 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) - print '%s - %s' % (module, sys.modules['proxymodules.' + - module].Module().description) - print 'all - use all available modules' + m = sys.modules['proxymodules.' + module].Module() + print(f'{m.name} - {m.description}') -def receive_from(s, timeout): - # receive data from a socket until no more data is there or until timeout - b = "" - s.settimeout(timeout) - try: - while True: - data = s.recv(4096) - if not data: - break - b += data - except: - pass +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=False): +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: - print ("> > > > in: " if incoming else "< < < < out: ") + m.name + vprint(("> > > > in: " if incoming else "< < < < out: ") + m.name, verbose) if dont_chain: m.execute(data) else: @@ -135,103 +199,297 @@ def handle_data(data, modules, dont_chain, incoming=False): 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 date before + # host and the remote host, while letting modules work on the data before # passing it on. - remote_socket = socket.socket() - if args.use_ssl: - remote_socket = ssl.wrap_socket(remote_socket) - remote_socket.connect((args.target_ip, args.target_port)) - in_data = '' # incoming data, intended for the local host - out_data = '' # outgoing data, intended for the remote host - - # instead of sending data to the remote host, receive some data first. - # might be necessary to read banners, etc. - if args.receive_first: - in_data = receive_from(remote_socket, args.timeout) - if len(in_data): - if in_modules is not None: - in_data = handle_data(in_data, in_modules, - args.chain_modules, True) - local_socket.send(in_data) + 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: + if args.source_ip or args.source_port: + remote_socket.bind((args.source_ip, args.source_port)) + 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 - while True: - out_data = receive_from(local_socket, args.timeout) - if len(out_data): - if out_modules is not None: - out_data = handle_data(out_data, out_modules, - args.no_chain_modules) - remote_socket.send(out_data) - - in_data = receive_from(remote_socket, args.timeout) - if len(in_data): - if in_modules is not None: - in_data = handle_data(in_data, in_modules, - args.no_chain_modules, True) - local_socket.send(in_data) - - if not len(in_data) or not len(out_data): - # no more data on one of the sockets, exit the loop and return - local_socket.close() - remote_socket.close() - break + 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): - print '%s is not a valid IP address' % args.listen_ip - sys.exit(1) + 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): - print '%s is not a valid IP address' % args.target_ip - sys.exit(2) + 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) + 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) + 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, e: - print e.strerror + except socket.error as e: + print(e.strerror) sys.exit(5) - proxy_socket.listen(10) - + proxy_socket.listen(100) + log(args.logfile, str(args)) # endless loop until ctrl+c try: while True: in_socket, in_addrinfo = proxy_socket.accept() - if args.verbose: - print 'Connection from %s:%d' % in_addrinfo - if args.use_ssl: - in_socket = ssl.wrap_socket(in_socket, certfile="mitm.pem", - keyfile="mitm.pem", - server_side=True, - ssl_version=ssl.PROTOCOL_SSLv23) + 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: - print '\nCtrl+C detected, exiting...' + log(args.logfile, 'Ctrl+C detected, exiting...') + print('\nCtrl+C detected, exiting...') sys.exit(0)