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 0e1fe73..3d672b8 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,20 @@ # 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 by and partially based on the TCP proxy example used in Justin Seitz' book "Black Hat Python" by no starch press. ## Usage + ``` $ ./tcpproxy.py -h -usage: tcpproxy.py [-h] -ti TARGET_IP -tp TARGET_PORT [-li LISTEN_IP] - [-lp LISTEN_PORT] [-om OUT_MODULES] [-im IN_MODULES] [-v] - [-n] [-l LOGFILE] [--list] [-lo HELP_MODULES] [-s] +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. @@ -17,13 +22,19 @@ handle the intercepted traffic. optional arguments: -h, --help show this help message and exit -ti TARGET_IP, --targetip TARGET_IP - remote target IP + remote target IP or host name -tp TARGET_PORT, --targetport TARGET_PORT remote target port -li LISTEN_IP, --listenip LISTEN_IP - IP address to listen for incoming data + 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. @@ -37,51 +48,68 @@ optional arguments: --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, certificate is - mitm.pem + -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 + ``` $ ./tcpproxy.py --list +digestdowngrade - Find HTTP Digest Authentication and replace it with a Basic Auth hexdump - Print a hexdump of the received data http_ok - Prepend HTTP response header http_post - Prepend HTTP header http_strip - Remove HTTP header from data -javaxml - Serialization or deserialization of Java objects (needs jython) 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 by using regular expressions +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 ``` + 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: + ```python -#!/usr/bin/env python2 +#!/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 = __file__.rsplit('/', 1)[1].split('.')[0] + 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 = options['find'] # text to highlight + self.find = bytes(options['find'], 'ascii') # text to highlight if 'color' in options.keys(): - self.color = '\033[' + options['color'] + 'm' # highlight color + self.color = bytes('\033[' + options['color'] + 'm', 'ascii') # highlight color else: - self.color = '\033[31;1m' + self.color = b'\033[31;1m' def execute(self, data): if self.find is None: - print data + print(data) else: - pdata = data.replace(self.find, self.color + self.find + '\033[0m') - print pdata + pdata = data.replace(self.find, self.color + self.find + b'\033[0m') + print(pdata.decode('ascii')) return data def help(self): @@ -92,21 +120,27 @@ class Module: if __name__ == '__main__': - print 'This module is not supposed to be executed alone!' + 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 python2 +#!/usr/bin/env python3 +import os.path as path class Module: def __init__(self, incoming=False, verbose=False, options=None): - # -- 8< --- snip + # 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(): @@ -117,16 +151,18 @@ class Module: def execute(self, data): # -- 8< --- snip - for i in xrange(0, len(data), self.len): + 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!' ``` + The above example should give you an idea how to make use of module parameters. A calling example would be: + ``` -python2 tcpproxy.py -om hexdump:length=8,http_post,hexdump:length=12 -ti 127.0.0.1 -tp 12345 +./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 @@ -149,43 +185,27 @@ python2 tcpproxy.py -om hexdump:length=8,http_post,hexdump:length=12 -ti 127.0.0 0060 77 71 6B 65 6A 66 68 77 71 6C 6B 65 wqkejfhwqlke 006C 6A 66 68 0A jfh. ``` -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... -## Deserializing and Serializing Java Objects to XML -Using the Java xstream libary, it is possible to deserialize intercepted serialised objects if the .jar with class definitions is known to tcpproxy. -``` -CLASSPATH=./lib/* jython tcpproxy.py -ti 127.0.0.1 -tp 12346 -lp 12345 -om javaxml:mode=deserial,textdump -``` -If you would like to use a 3rd tool like BurpSuite to manipulate the XStream XML structure use this setup: -``` - - +---------+ - +-------> |BurpSuite+-----+ - | +---------+ | - | V -+------------------+ +--------+--+ +-----------+ +-----------+ -| Java ThickClient +------> |1. tcpproxy| |2. tcpproxy+------------> |Java Server| -+------------------+ +-----------+ +-----------+ +-----------+ -``` -The setup works like this: Let's say you want to intercept an manipulate serialized objects between the thick client and the Java server. The idea is to intercept serialized objects, turn them into XML (deserialize them), pipe them into another tool (BurpSuite in this example) where you manipulate the data, then take that data and send it to the server. The server replies with another object which is again deserialized into XML, fed to the tool and then serialized before sending the response to the client. -``` -$ CLASSPATH=./lib/*:/pathTo/jarFiles/* jython27 tcpproxy.py -ti -tp -lp -om javaxml:mode=deserial,http_post -im http_strip,javaxml:mode=serial -``` -The call above is for the first tcpproxy instance between the client and Burp (or whatever tool you want to use). The target IP is the IP Burp is using, target port tp is Burp's listening port. For listening IP li and listening port lp you either configure the client or do some ARP spoofing/iptables magic. With -om you prepare the data for burp. Since Burp only consumes HTTP, use the http_post module after the deserializer to prepend an HTTP header. Then manipulate the data within burp. Take care to configure Burp to redirect the data to the second tcpproxy instance's listen IP/listen port and enable invisible proxying. -Burp's response will be HTTP with an XML body, so in the incoming chain (-im) first strip the header (http_strip), then serialize the XML before the data is sent to the client. -``` -$ CLASSPATH=./lib/*:/pathTo/jarFiles/* jython27 tcpproxy.py -ti -tp -lp -im javaxml:mode=deserial,http_ok -om http_strip,javaxml:mode=serial -``` -This is the second tcpproxy instance. Burp will send the data there if you correctly configured the request handling in Burp's proxy listener options. Before sending the data to the server in the outgoing chain (-om), first strip the HTTP header, then serialize the XML. The server's response will be handled by the incoming chain (-im), so deserialize it, prepend the HTTP response header, then send the data to burp. - -Using this setup, you are able to take advantage of Burp's capabilities, like the repeater or intruder or simply use it for logging purposes. This was originally the idea of jbarg. -If you are doing automated modifications and have no need for interactivity, you can simply take advantage of the (de-)serialization modules by writing a module to work on the deserialized XML structure. Then plug your module into the chain by doing -im java_deserializer,your_module,java_serializer (or -om of course). This way you also only need one tcpproxy instance, of course. - -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 -- [X] implement a way to pass parameters to modules -- [X] implement logging (pre-/post modification) + - [ ] 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/lib/kxml2-min-2.3.0.jar b/lib/kxml2-min-2.3.0.jar deleted file mode 100755 index a77dd1d..0000000 Binary files a/lib/kxml2-min-2.3.0.jar and /dev/null differ diff --git a/lib/xmlpull-1.1.3.1.jar b/lib/xmlpull-1.1.3.1.jar deleted file mode 100755 index cbc149d..0000000 Binary files a/lib/xmlpull-1.1.3.1.jar and /dev/null differ diff --git a/lib/xpp3-1.1.4.jar b/lib/xpp3-1.1.4.jar deleted file mode 100755 index 90131a3..0000000 Binary files a/lib/xpp3-1.1.4.jar and /dev/null differ diff --git a/lib/xstream-1.4.4.jar b/lib/xstream-1.4.4.jar deleted file mode 100755 index 137607f..0000000 Binary files a/lib/xstream-1.4.4.jar and /dev/null differ 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/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 17c1d35..e9bc3a5 100644 --- a/proxymodules/hexdump.py +++ b/proxymodules/hexdump.py @@ -1,10 +1,11 @@ -#!/usr/bin/env python2 +#!/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 = __file__.rsplit('/', 1)[1].split('.')[0] + 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 @@ -19,17 +20,15 @@ def execute(self, data): # this is a pretty hex dumping function directly taken from # http://code.activestate.com/recipes/142812-hex-dumper/ result = [] - digits = 4 if isinstance(data, unicode) else 2 - - for i in xrange(0, len(data), self.len): + digits = 2 + for i in range(0, len(data), self.len): s = data[i:i + self.len] - 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, self.len * (digits + 1), - hexa, text)) - - print b'\n'.join(result) + 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 index 30f1c77..02444d8 100644 --- a/proxymodules/http_ok.py +++ b/proxymodules/http_ok.py @@ -1,30 +1,30 @@ -#!/usr/bin/env python2 +#!/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 = __file__.rsplit('/', 1)[1].split('.')[0] + 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 = options['server'] + 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 = self.source[0] + self.server = bytes(self.source[0], 'ascii') - http = "HTTP/1.1 200 OK\r\n" - http += "Server: %s\r\n" % self.server - http += "Connection: keep-alive\r\n" - http += "Content-Length: %d\r\n" % len(data) + 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 + "\r\n" + data + return http + b"\r\n" + data def help(self): h = '\tserver: remote source, used in response Server header\n' @@ -32,4 +32,4 @@ def help(self): 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/http_post.py b/proxymodules/http_post.py index f5a98da..f532d32 100644 --- a/proxymodules/http_post.py +++ b/proxymodules/http_post.py @@ -1,34 +1,35 @@ -#!/usr/bin/env python2 +#!/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 = __file__.rsplit('/', 1)[1].split('.')[0] + 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 = options['host'] + self.targethost = bytes(options['host'], 'ascii') if 'port' in options.keys(): - self.targetport = options['port'] + 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 = self.destination[0] + self.targethost = bytes(self.destination[0], 'ascii') if self.targetport is None: - self.targetport = str(self.destination[1]) - http = "POST /to/%s/%s HTTP/1.1\r\n" % (self.targethost, self.targetport) - http += "Host: %s\r\n" % self.targethost + 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 += "Connection: keep-alive\r\n" - http += "Content-Length: %d\r\n" % len(data) - return http + "\r\n" + data + 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' @@ -37,4 +38,4 @@ def help(self): 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/http_strip.py b/proxymodules/http_strip.py index 329a637..e96a63f 100644 --- a/proxymodules/http_strip.py +++ b/proxymodules/http_strip.py @@ -1,19 +1,20 @@ -#!/usr/bin/env python2 +#!/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 = __file__.rsplit('/', 1)[1].split('.')[0] + 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('\n', 1)[0] - if line.endswith('\r'): - return '\r\n' * 2 + line = data.split(b'\n', 1)[0] + if line.endswith(b'\r'): + return b'\r\n' * 2 else: - return '\n' * 2 + return b'\n' * 2 def execute(self, data): delimiter = self.detect_linebreak(data) @@ -23,4 +24,4 @@ def execute(self, 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/javaxml.py b/proxymodules/javaxml.py deleted file mode 100644 index 88d2e29..0000000 --- a/proxymodules/javaxml.py +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/env python2 -# -*- coding: utf-8 -*- -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 = __file__.rsplit('/', 1)[1].split('.')[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/proxymodules/log.py b/proxymodules/log.py index f1eb656..1af9ee4 100644 --- a/proxymodules/log.py +++ b/proxymodules/log.py @@ -1,11 +1,12 @@ -#!/usr/bin/env python2 +#!/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 = __file__.rsplit('/', 1)[1].split('.')[0] + 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 @@ -23,11 +24,11 @@ def __del__(self): def execute(self, data): if self.handle is None: - self.handle = open(self.file, 'w', 0) # unbuffered - print 'Logging to file', self.file - logentry = time.strftime('%Y%m%d-%H%M%S') + ' ' + str(time.time()) + '\n' + 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 += '-' * 20 + '\n' + logentry += b'-' * 20 + b'\n' self.handle.write(logentry) return data @@ -37,4 +38,4 @@ def help(self): 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/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 19b5684..ccd6835 100644 --- a/proxymodules/removegzip.py +++ b/proxymodules/removegzip.py @@ -1,10 +1,11 @@ -#!/usr/bin/env python2 +#!/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 = __file__.rsplit('/', 1)[1].split('.')[0] + 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 @@ -14,19 +15,20 @@ def __init__(self, incoming=False, verbose=False, options=None): 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 index f3bcb57..258f8d2 100644 --- a/proxymodules/replace.py +++ b/proxymodules/replace.py @@ -1,29 +1,62 @@ -#!/usr/bin/env python2 - +#!/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 = __file__.rsplit('/', 1)[1].split('.')[0] - self.description = 'Replace text by using regular expressions' - self.incoming = incoming # incoming means module is on -im chain + 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: - self.search = options['search'] - self.replace = options['replace'] + 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): - new_data = re.sub(self.search, self.replace, data) - if not new_data == data: - print("Replacing '%s' with '%s'" % (self.search, self.replace)) - return new_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 that should be replaced\n' - h += ('\treplace: value that it should be replaced with') + 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!' + print('This module is not supposed to be executed alone!') diff --git a/proxymodules/size.py b/proxymodules/size.py index abf6544..bf6aabe 100644 --- a/proxymodules/size.py +++ b/proxymodules/size.py @@ -1,10 +1,12 @@ -#!/usr/bin/env python2 -from distutils.utils import strtobool +#!/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 = __file__.rsplit('/', 1)[1].split('.')[0] + 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 @@ -14,21 +16,19 @@ def __init__(self, incoming=False, verbose=False, options=None): if 'verbose' in options.keys(): self.verbose = bool(strtobool(options['verbose'])) - def execute(self, data): size = len(data) - msg = "Received %d bytes" %size + msg = "Received %d bytes" % size if self.verbose: msg += " from %s:%d" % self.source msg += " for %s:%d" % self.destination - print msg + print(msg) return data - - def help(self): + 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!' + 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 5d504f5..a192899 100644 --- a/proxymodules/textdump.py +++ b/proxymodules/textdump.py @@ -1,27 +1,38 @@ -#!/usr/bin/env python2 +#!/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 = __file__.rsplit('/', 1)[1].split('.')[0] + 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 = options['find'] # text to highlight + self.find = bytes(options['find'], 'ascii') # text to highlight if 'color' in options.keys(): - self.color = '\033[' + options['color'] + 'm' # highlight color + self.color = bytes('\033[' + options['color'] + 'm', 'ascii') # highlight color else: - self.color = '\033[31;1m' + 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 data + print(decode(data, self.codec)) else: - pdata = data.replace(self.find, self.color + self.find + '\033[0m') - print pdata + pdata = data.replace(self.find, self.color + self.find + b'\033[0m') + print(decode(pdata, self.codec)) return data def help(self): @@ -32,4 +43,4 @@ def help(self): 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 index 161cd69..4143a14 100755 --- a/tcpproxy.py +++ b/tcpproxy.py @@ -1,10 +1,11 @@ -#!/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 @@ -42,6 +43,21 @@ def parse_args(): parser.add_argument('-lp', '--listenport', dest='listen_port', type=int, default=8080, help='port to listen on') + 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' + ' before sending to remote target.') @@ -69,7 +85,19 @@ def parse_args(): 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, 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() @@ -89,7 +117,7 @@ def generate_module_list(modstring, incoming=False, verbose=False): __import__('proxymodules.' + name) modlist.append(sys.modules['proxymodules.' + name].Module(incoming, verbose, options)) except ImportError: - print 'Module %s not found' % name + print('Module %s not found' % name) sys.exit(3) return modlist @@ -109,7 +137,7 @@ def parse_module_options(n): k, v = op.split('=') options[k] = v except ValueError: - print op, ' is not valid!' + print(op, ' is not valid!') sys.exit(23) return name, options @@ -121,7 +149,7 @@ def list_modules(): for _, module, _ in pkgutil.iter_modules([module_path]): __import__('proxymodules.' + module) m = sys.modules['proxymodules.' + module].Module() - print '%s - %s' % (m.name, m.description) + print(f'{m.name} - {m.description}') def print_module_help(modlist): @@ -129,10 +157,10 @@ def print_module_help(modlist): modules = generate_module_list(modlist) for m in modules: try: - print m.name - print m.help() + print(f'{m.name} - {m.description}') + print(m.help()) except AttributeError: - print '\tNo options.' + print('\tNo options or missing help() function.') def update_module_hosts(modules, source, destination): @@ -149,11 +177,11 @@ def update_module_hosts(modules, source, destination): def receive_from(s): # receive data from a socket until no more data is there - b = "" + b = b"" while True: data = s.recv(4096) b += data - if not data or len(data)<4096: + if not data or len(data) < 4096: break return b @@ -163,8 +191,7 @@ def handle_data(data, modules, dont_chain, incoming, verbose): # output of one plugin to the following plugin. Not every plugin will # necessarily modify the data, though. for m in modules: - if verbose: - print ("> > > > in: " if incoming else "< < < < out: ") + m.name + vprint(("> > > > in: " if incoming else "< < < < out: ") + m.name, verbose) if dont_chain: m.execute(data) else: @@ -175,53 +202,111 @@ def handle_data(data, modules, dont_chain, incoming, verbose): def is_client_hello(sock): firstbytes = sock.recv(128, socket.MSG_PEEK) return (len(firstbytes) >= 3 and - firstbytes[0] == "\x16" and - firstbytes[1:3] in [ "\x03\x00", - "\x03\x01", - "\x03\x02", - "\x03\x03", - "\x02\x00", ] - ) - - -def enable_ssl(remote_socket, local_socket): - local_socket = ssl.wrap_socket(local_socket, - server_side=True, - certfile="mitm.pem", - keyfile="mitm.pem", - ssl_version=ssl.PROTOCOL_TLS, - ) - - remote_socket = ssl.wrap_socket(remote_socket) + 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) - ) + 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 = socket.socket() + 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: - print '%s:%d - Connection refused' % (args.target_ip, - args.target_port) + 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 - update_module_hosts(out_modules, local_socket.getpeername(), remote_socket.getpeername()) - update_module_hosts(in_modules, remote_socket.getpeername(), local_socket.getpeername()) + 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 @@ -231,38 +316,54 @@ def start_proxy_thread(local_socket, args, in_modules, out_modules): if starttls(args, local_socket, read_sockets): try: - if args.verbose: - print "Enable SSL" - ssl_sockets = enable_ssl(remote_socket, local_socket) + 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) + 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: - peer = sock.getpeername() + 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, '< < < out\n' + 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) + remote_socket.send(data.encode() if isinstance(data, str) else data) else: - if args.verbose: - print "Connection from local client %s:%d closed" % peer + 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, '> > > in\n' + data) + log(args.logfile, b'> > > in\n' + data) if in_modules is not None: data = handle_data(data, in_modules, args.no_chain_modules, @@ -270,8 +371,8 @@ def start_proxy_thread(local_socket, args, in_modules, out_modules): args.verbose) local_socket.send(data) else: - if args.verbose: - print "Connection to remote server %s:%d closed" % peer + 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 @@ -281,35 +382,46 @@ 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 = "%s %s\n" % (time.strftime('%Y%m%d-%H%M%S'), - str(time.time())) + logentry = bytes("%s %s\n" % (time.strftime('%Y%m%d-%H%M%S'), str(time.time())), 'ascii') else: - logentry = '' + logentry = b'' logentry += message if not message_only: - logentry += '\n' + '-' * 20 + '\n' + 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' + print('Target IP is required: -ti') sys.exit(6) if not args.target_port: - print 'Target port is required: -tp' + 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, 'a', 0) # unbuffered + args.logfile = open(args.logfile, 'ab', 0) # unbuffered except Exception as ex: - print 'Error opening logfile' - print ex + print('Error opening logfile') + print(ex) sys.exit(4) if args.list: @@ -326,7 +438,7 @@ def main(): except socket.gaierror: ip = False if ip is False: - print '%s is not a valid IP address or host name' % args.listen_ip + print('%s is not a valid IP address or host name' % args.listen_ip) sys.exit(1) else: args.listen_ip = ip @@ -337,7 +449,7 @@ def main(): except socket.gaierror: ip = False if ip is False: - print '%s is not a valid IP address or host name' % args.target_ip + print('%s is not a valid IP address or host name' % args.target_ip) sys.exit(2) else: args.target_ip = ip @@ -359,25 +471,25 @@ def main(): try: proxy_socket.bind((args.listen_ip, args.listen_port)) except socket.error as e: - print e.strerror + 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 + 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") + 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...' + print('\nCtrl+C detected, exiting...') sys.exit(0)