├── .gitignore ├── LICENSE ├── README.md ├── mitm.pem ├── proxymodules ├── __init__.py ├── delay.py ├── digestdowngrade.py ├── hexdump.py ├── hexreplace.py ├── http_ok.py ├── http_post.py ├── http_strip.py ├── log.py ├── mqtt.py ├── removegzip.py ├── replace.py ├── size.py ├── size404.py └── textdump.py ├── requirements.txt └── tcpproxy.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.pyc 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 René Werner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tcpproxy.py - An intercepting proxy for TCP data 2 | 3 | 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. 4 | 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. 5 | 6 | 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. 7 | 8 | ## Usage 9 | 10 | ``` 11 | $ ./tcpproxy.py -h 12 | usage: tcpproxy.py [-h] [-ti TARGET_IP] [-tp TARGET_PORT] [-li LISTEN_IP] 13 | [-lp LISTEN_PORT] [-pi PROXY_IP] [-pp PROXY_PORT] 14 | [-pt {SOCKS4,SOCKS5,HTTP}] [-om OUT_MODULES] 15 | [-im IN_MODULES] [-v] [-n] [-l LOGFILE] [--list] 16 | [-lo HELP_MODULES] [-s] [-sc SERVER_CERTIFICATE] 17 | [-sk SERVER_KEY] [-cc CLIENT_CERTIFICATE] [-ck CLIENT_KEY] 18 | 19 | Simple TCP proxy for data interception and modification. Select modules to 20 | handle the intercepted traffic. 21 | 22 | optional arguments: 23 | -h, --help show this help message and exit 24 | -ti TARGET_IP, --targetip TARGET_IP 25 | remote target IP or host name 26 | -tp TARGET_PORT, --targetport TARGET_PORT 27 | remote target port 28 | -li LISTEN_IP, --listenip LISTEN_IP 29 | IP address/host name to listen for incoming data 30 | -lp LISTEN_PORT, --listenport LISTEN_PORT 31 | port to listen on 32 | -pi PROXY_IP, --proxy-ip PROXY_IP 33 | IP address/host name of proxy 34 | -pp PROXY_PORT, --proxy-port PROXY_PORT 35 | proxy port 36 | -pt {SOCKS4,SOCKS5,HTTP}, --proxy-type {SOCKS4,SOCKS5,HTTP} 37 | proxy type. Options are SOCKS5 (default), SOCKS4, HTTP 38 | -om OUT_MODULES, --outmodules OUT_MODULES 39 | comma-separated list of modules to modify data before 40 | sending to remote target. 41 | -im IN_MODULES, --inmodules IN_MODULES 42 | comma-separated list of modules to modify data 43 | received from the remote target. 44 | -v, --verbose More verbose output of status information 45 | -n, --no-chain Don't send output from one module to the next one 46 | -l LOGFILE, --log LOGFILE 47 | Log all data to a file before modules are run. 48 | --list list available modules 49 | -lo HELP_MODULES, --list-options HELP_MODULES 50 | Print help of selected module 51 | -s, --ssl detect SSL/TLS as well as STARTTLS 52 | -sc SERVER_CERTIFICATE, --server-certificate SERVER_CERTIFICATE 53 | server certificate in PEM format (default: mitm.pem) 54 | -sk SERVER_KEY, --server-key SERVER_KEY 55 | server key in PEM format (default: mitm.pem) 56 | -cc CLIENT_CERTIFICATE, --client-certificate CLIENT_CERTIFICATE 57 | client certificate in PEM format in case client 58 | authentication is required by the target 59 | -ck CLIENT_KEY, --client-key CLIENT_KEY 60 | client key in PEM format in case client authentication 61 | is required by the target 62 | ``` 63 | 64 | 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. 65 | 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 66 | 67 | ## Modules 68 | 69 | ``` 70 | $ ./tcpproxy.py --list 71 | digestdowngrade - Find HTTP Digest Authentication and replace it with a Basic Auth 72 | hexdump - Print a hexdump of the received data 73 | http_ok - Prepend HTTP response header 74 | http_post - Prepend HTTP header 75 | http_strip - Remove HTTP header from data 76 | log - Log data in the module chain. Use in addition to general logging (-l/--log). 77 | removegzip - Replace gzip in the list of accepted encodings in a HTTP request with booo. 78 | replace - Replace text on the fly by using regular expressions in a file or as module parameters 79 | hexreplace - Replace hex data in tcp packets 80 | size - Print the size of the data passed to the module 81 | size404 - Change HTTP responses of a certain size to 404. 82 | textdump - Simply print the received data as text 83 | ``` 84 | 85 | 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: 86 | 87 | ```python 88 | #!/usr/bin/env python3 89 | import os.path as path 90 | 91 | 92 | class Module: 93 | def __init__(self, incoming=False, verbose=False, options=None): 94 | # extract the file name from __file__. __file__ is proxymodules/name.py 95 | self.name = path.splitext(path.basename(__file__))[0] 96 | self.description = 'Simply print the received data as text' 97 | self.incoming = incoming # incoming means module is on -im chain 98 | self.find = None # if find is not None, this text will be highlighted 99 | if options is not None: 100 | if 'find' in options.keys(): 101 | self.find = bytes(options['find'], 'ascii') # text to highlight 102 | if 'color' in options.keys(): 103 | self.color = bytes('\033[' + options['color'] + 'm', 'ascii') # highlight color 104 | else: 105 | self.color = b'\033[31;1m' 106 | 107 | def execute(self, data): 108 | if self.find is None: 109 | print(data) 110 | else: 111 | pdata = data.replace(self.find, self.color + self.find + b'\033[0m') 112 | print(pdata.decode('ascii')) 113 | return data 114 | 115 | def help(self): 116 | h = '\tfind: string that should be highlighted\n' 117 | h += ('\tcolor: ANSI color code. Will be wrapped with \\033[ and m, so' 118 | ' passing 32;1 will result in \\033[32;1m (bright green)') 119 | return h 120 | 121 | 122 | if __name__ == '__main__': 123 | print('This module is not supposed to be executed alone!') 124 | ``` 125 | 126 | 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. 127 | 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). 128 | The verbose parameter is set to True if the proxy is started with -v/--verbose. 129 | 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. 130 | 131 | See the hexdump module for an additional options example: 132 | 133 | ```python 134 | #!/usr/bin/env python3 135 | import os.path as path 136 | 137 | 138 | class Module: 139 | def __init__(self, incoming=False, verbose=False, options=None): 140 | # extract the file name from __file__. __file__ is proxymodules/name.py 141 | self.name = path.splitext(path.basename(__file__))[0] 142 | self.description = 'Print a hexdump of the received data' 143 | self.incoming = incoming # incoming means module is on -im chain 144 | self.len = 16 145 | if options is not None: 146 | if 'length' in options.keys(): 147 | self.len = int(options['length']) 148 | 149 | def help(self): 150 | return '\tlength: bytes per line (int)' 151 | 152 | def execute(self, data): 153 | # -- 8< --- snip 154 | for i in range(0, len(data), self.len): 155 | s = data[i:i + self.len] 156 | # # -- 8< --- snip 157 | 158 | if __name__ == '__main__': 159 | print 'This module is not supposed to be executed alone!' 160 | ``` 161 | 162 | The above example should give you an idea how to make use of module parameters. A calling example would be: 163 | 164 | ``` 165 | ./tcpproxy.py -om hexdump:length=8,http_post,hexdump:length=12 -ti 127.0.0.1 -tp 12345 166 | < < < < out: hexdump 167 | 0000 77 6C 6B 66 6A 6C 77 71 wlkfjlwq 168 | 0008 6B 66 6A 68 6C 6B 77 71 kfjhlkwq 169 | 0010 6A 65 68 66 6C 6B 65 77 jehflkew 170 | 0018 71 6A 66 68 6C 6B 65 77 qjfhlkew 171 | 0020 71 6A 66 68 6C 6B 65 77 qjfhlkew 172 | 0028 71 6A 66 6C 68 77 71 6B qjflhwqk 173 | 0030 65 6A 66 68 77 71 6C 6B ejfhwqlk 174 | 0038 65 6A 66 68 0A ejfh. 175 | < < < < out: http_post 176 | < < < < out: hexdump 177 | 0000 50 4F 53 54 20 2F 20 48 54 54 50 2F POST / HTTP/ 178 | 000C 31 2E 31 0A 48 6F 73 74 3A 20 74 63 1.1.Host: tc 179 | 0018 70 70 72 6F 78 79 0A 43 6F 6E 74 65 pproxy.Conte 180 | 0024 6E 74 2D 4C 65 6E 67 74 68 3A 20 36 nt-Length: 6 181 | 0030 31 0A 0A 77 6C 6B 66 6A 6C 77 71 6B 1..wlkfjlwqk 182 | 003C 66 6A 68 6C 6B 77 71 6A 65 68 66 6C fjhlkwqjehfl 183 | 0048 6B 65 77 71 6A 66 68 6C 6B 65 77 71 kewqjfhlkewq 184 | 0054 6A 66 68 6C 6B 65 77 71 6A 66 6C 68 jfhlkewqjflh 185 | 0060 77 71 6B 65 6A 66 68 77 71 6C 6B 65 wqkejfhwqlke 186 | 006C 6A 66 68 0A jfh. 187 | ``` 188 | 189 | 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... 190 | 191 | ## Logging 192 | 193 | 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 . 194 | 195 | ## TODO 196 | 197 | - [ ] 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) 198 | - [ ] Create and maintain a parallel branch that is compatible with jython but also has most of the new stuff introduced after e3290261 199 | 200 | ## Contributions 201 | 202 | I want to thank the following people for spending their valuable time and energy on improving this little tool: 203 | 204 | - [Adrian Vollmer](https://github.com/AdrianVollmer) 205 | - [Michael Füllbier](https://github.com/mfuellbier) 206 | - [Stefan Grönke](https://github.com/gronke) 207 | - [Mattia](https://github.com/sowdust) 208 | - [bjorns163](https://github.com/bjorns163) 209 | - [Pernat1y](https://github.com/Pernat1y) 210 | - [hrzlgnm](https://github.com/hrzlgnm) 211 | - [MKesenheimer](https://github.com/MKesenheimer) 212 | -------------------------------------------------------------------------------- /mitm.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | 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 31 | -----END CERTIFICATE----- 32 | -----BEGIN PRIVATE KEY----- 33 | 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 82 | s1vWHzIABKW/htxysnONHEUPla0d0A== 83 | -----END PRIVATE KEY----- 84 | -------------------------------------------------------------------------------- /proxymodules/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ickerwx/tcpproxy/ddd487ca69205e5e9f5af7166b00dcac0d0c2931/proxymodules/__init__.py -------------------------------------------------------------------------------- /proxymodules/delay.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os.path as path 3 | import time 4 | import random 5 | 6 | 7 | class Module: 8 | def __init__(self, incoming=False, verbose=False, options=None): 9 | # extract the file name from __file__. __file__ is proxymodules/name.py 10 | self.name = path.splitext(path.basename(__file__))[0] 11 | self.description = 'Set delay in passing through the packet, used for simulating various network conditions' 12 | self.incoming = incoming # incoming means module is on -im chain 13 | self.random = False 14 | self.seconds = None 15 | self.verbose = verbose 16 | if options is not None: 17 | if 'seconds' in options.keys(): 18 | try: 19 | self.seconds = abs(float(options['seconds'])) 20 | except ValueError: 21 | print(f"Can't parse {options['seconds']} as float") 22 | pass # leave it set to None 23 | if 'random' in options.keys(): 24 | # set random=true to enable delay randomization 25 | self.random = (options['random'].lower() == 'true') 26 | if self.random and self.seconds is None: 27 | # set a upper bound of 1s if seconds is not being used, otherwise keep the seconds value 28 | self.seconds = 1.0 29 | 30 | def execute(self, data): 31 | delay = None 32 | if self.random: 33 | delay = round(random.uniform(0, self.seconds), 3) # round to milliseconds 34 | else: 35 | delay = self.seconds 36 | # here delay is either None or a positive float 37 | # if the module was instantiated w/o either seconds or random, effectively nothing happens 38 | if delay is not None: 39 | if self.verbose: 40 | print(f"Waiting {delay}s.") 41 | time.sleep(delay) 42 | return data 43 | 44 | def help(self): 45 | h = '\tseconds: number of seconds you want the packet to be delayed\n' 46 | h += ('\trandom: optional; set to true to randomize the delay between 0 and seconds (default: 1.0s)\n') 47 | return h 48 | 49 | 50 | if __name__ == '__main__': 51 | print('This module is not supposed to be executed alone!') 52 | -------------------------------------------------------------------------------- /proxymodules/digestdowngrade.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | 4 | 5 | class Module: 6 | def __init__(self, incoming=False, verbose=False, options=None): 7 | # extract the file name from __file__. __file__ is proxymodules/name.py 8 | self.name = os.path.splitext(os.path.basename(__file__))[0] 9 | self.description = 'Find HTTP Digest Authentication and replace it with a Basic Auth' 10 | self.verbose = verbose 11 | self.realm = 'tcpproxy' 12 | 13 | if options is not None: 14 | if 'realm' in options.keys(): 15 | self.realm = bytes(options['realm'], 'ascii') 16 | 17 | def detect_linebreak(self, data): 18 | line = data.split(b'\n', 1)[0] 19 | if line.endswith(b'\r'): 20 | return b'\r\n' 21 | else: 22 | return b'\n' 23 | 24 | def execute(self, data): 25 | delimiter = self.detect_linebreak(data) 26 | lines = data.split(delimiter) 27 | for index, line in enumerate(lines): 28 | if line.lower().startswith(b'www-authenticate: digest'): 29 | lines[index] = b'WWW-Authenticate: Basic realm="%s"' % self.realm 30 | return delimiter.join(lines) 31 | 32 | def help(self): 33 | h = '\trealm: use this instead of the default "tcpproxy"\n' 34 | return h 35 | 36 | 37 | if __name__ == '__main__': 38 | print('This module is not supposed to be executed alone!') 39 | -------------------------------------------------------------------------------- /proxymodules/hexdump.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os.path as path 3 | 4 | 5 | class Module: 6 | def __init__(self, incoming=False, verbose=False, options=None): 7 | # extract the file name from __file__. __file__ is proxymodules/name.py 8 | self.name = path.splitext(path.basename(__file__))[0] 9 | self.description = 'Print a hexdump of the received data' 10 | self.incoming = incoming # incoming means module is on -im chain 11 | self.len = 16 12 | if options is not None: 13 | if 'length' in options.keys(): 14 | self.len = int(options['length']) 15 | 16 | def help(self): 17 | return '\tlength: bytes per line (int)' 18 | 19 | def execute(self, data): 20 | # this is a pretty hex dumping function directly taken from 21 | # http://code.activestate.com/recipes/142812-hex-dumper/ 22 | result = [] 23 | digits = 2 24 | for i in range(0, len(data), self.len): 25 | s = data[i:i + self.len] 26 | hexa = ' '.join(['%0*X' % (digits, x) for x in s]) 27 | text = ''.join([chr(x) if 0x20 <= x < 0x7F else '.' for x in s]) 28 | result.append("%04X %-*s %s" % (i, self.len * (digits + 1), hexa, text)) 29 | print("\n".join(result)) 30 | return data 31 | 32 | 33 | if __name__ == '__main__': 34 | print ('This module is not supposed to be executed alone!') 35 | -------------------------------------------------------------------------------- /proxymodules/hexreplace.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | 4 | 5 | class Module: 6 | def __init__(self, incoming=False, verbose=False, options=None): 7 | # extract the file name from __file__. __file__ is proxymodules/name.py 8 | self.name = os.path.splitext(os.path.basename(__file__))[0] 9 | self.description = 'Replace hex data on the fly defining search and replace-pairs in a file or as module parameters' 10 | self.verbose = verbose 11 | self.filename = None 12 | self.separator = ':' 13 | self.len = 16 14 | 15 | search = None 16 | if options is not None: 17 | if 'search' in options.keys(): 18 | search = bytes.fromhex(options['search']) 19 | if 'replace' in options.keys(): 20 | replace = bytes.fromhex(options['replace']) 21 | if 'file' in options.keys(): 22 | self.filename = options['file'] 23 | try: 24 | open(self.filename) 25 | except IOError as ioe: 26 | print("Error opening %s: %s" % (self.filename, ioe.strerror)) 27 | self.filename = None 28 | if 'separator' in options.keys(): 29 | self.separator = options['separator'] 30 | 31 | self.pairs = [] # list of (search, replace) tuples 32 | if search is not None and replace is not None: 33 | self.pairs.append((search, replace)) 34 | 35 | if self.filename is not None: 36 | for line in open(self.filename).readlines(): 37 | try: 38 | search, replace = line.split(self.separator, 1) 39 | self.pairs.append((bytes.fromhex(search.strip()), bytes.fromhex(replace.strip()))) 40 | except ValueError: 41 | # line does not contain separator and will be ignored 42 | pass 43 | 44 | def hexdump(self, data): 45 | result = [] 46 | digits = 2 47 | for i in range(0, len(data), self.len): 48 | s = data[i:i + self.len] 49 | hexa = ' '.join(['%0*X' % (digits, x) for x in s]) 50 | text = ''.join([chr(x) if 0x20 <= x < 0x7F else '.' for x in s]) 51 | result.append("%04X %-*s %s" % (i, self.len * (digits + 1), hexa, text)) 52 | print("\n".join(result)) 53 | 54 | def execute(self, data): 55 | if self.verbose: 56 | print(f"Incoming packet with size {len(data)}:") 57 | for search, replace in self.pairs: 58 | if search in data: 59 | if self.verbose: 60 | print("########## data found ###########") 61 | print("[Before:]") 62 | self.hexdump(data) 63 | data = data.replace(search, replace) 64 | if self.verbose: 65 | print("[After:]") 66 | self.hexdump(data) 67 | return data 68 | 69 | def help(self): 70 | h = '\tsearch: hex string (i.e. "deadbeef") to search for\n' 71 | h += ('\treplace: hex string the search string should be replaced with\n') 72 | h += ('\tfile: file containing search:replace pairs, one per line\n') 73 | h += ('\tseparator: define a custom search:replace separator in the file, e.g. search#replace\n') 74 | h += ('\n\tUse at least file or search and replace (or both).\n') 75 | return h 76 | 77 | 78 | if __name__ == '__main__': 79 | print('This module is not supposed to be executed alone!') 80 | -------------------------------------------------------------------------------- /proxymodules/http_ok.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os.path as path 3 | 4 | 5 | class Module: 6 | def __init__(self, incoming=False, verbose=False, options=None): 7 | # extract the file name from __file__. __file__ is proxymodules/name.py 8 | self.name = path.splitext(path.basename(__file__))[0] 9 | self.description = 'Prepend HTTP response header' 10 | self.server = None 11 | if options is not None: 12 | if 'server' in options.keys(): 13 | self.server = bytes(options['server'], 'ascii') 14 | 15 | # source will be set by the proxy thread later on 16 | self.source = None 17 | 18 | def execute(self, data): 19 | if self.server is None: 20 | self.server = bytes(self.source[0], 'ascii') 21 | 22 | http = b"HTTP/1.1 200 OK\r\n" 23 | http += b"Server: %s\r\n" % self.server 24 | http += b"Connection: keep-alive\r\n" 25 | http += b"Content-Length: %d\r\n" % len(data) 26 | 27 | return http + b"\r\n" + data 28 | 29 | def help(self): 30 | h = '\tserver: remote source, used in response Server header\n' 31 | return h 32 | 33 | 34 | if __name__ == '__main__': 35 | print('This module is not supposed to be executed alone!') 36 | -------------------------------------------------------------------------------- /proxymodules/http_post.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os.path as path 3 | 4 | 5 | class Module: 6 | def __init__(self, incoming=False, verbose=False, options=None): 7 | # extract the file name from __file__. __file__ is proxymodules/name.py 8 | self.name = path.splitext(path.basename(__file__))[0] 9 | self.description = 'Prepend HTTP header' 10 | self.incoming = incoming # incoming means module is on -im chain 11 | self.targethost = None 12 | self.targetport = None 13 | if options is not None: 14 | if 'host' in options.keys(): 15 | self.targethost = bytes(options['host'], 'ascii') 16 | if 'port' in options.keys(): 17 | self.targetport = bytes(options['port'], 'ascii') 18 | 19 | # destination will be set by the proxy thread later on 20 | self.destination = None 21 | 22 | def execute(self, data): 23 | if self.targethost is None: 24 | self.targethost = bytes(self.destination[0], 'ascii') 25 | if self.targetport is None: 26 | self.targetport = bytes(str(self.destination[1]), 'ascii') 27 | http = b"POST /to/%s/%s HTTP/1.1\r\n" % (self.targethost, self.targetport) 28 | http += b"Host: %s\r\n" % self.targethost 29 | 30 | http += b"Connection: keep-alive\r\n" 31 | http += b"Content-Length: %d\r\n" % len(data) 32 | return http + b"\r\n" + str(data) 33 | 34 | def help(self): 35 | h = '\thost: remote target, used in request URL and Host header\n' 36 | h += '\tport: remote target port, used in request URL\n' 37 | return h 38 | 39 | 40 | if __name__ == '__main__': 41 | print('This module is not supposed to be executed alone!') 42 | -------------------------------------------------------------------------------- /proxymodules/http_strip.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os.path as path 3 | 4 | 5 | class Module: 6 | def __init__(self, incoming=False, verbose=False, options=None): 7 | # extract the file name from __file__. __file__ is proxymodules/name.py 8 | self.name = path.splitext(path.basename(__file__))[0] 9 | self.description = 'Remove HTTP header from data' 10 | self.incoming = incoming # incoming means module is on -im chain 11 | 12 | def detect_linebreak(self, data): 13 | line = data.split(b'\n', 1)[0] 14 | if line.endswith(b'\r'): 15 | return b'\r\n' * 2 16 | else: 17 | return b'\n' * 2 18 | 19 | def execute(self, data): 20 | delimiter = self.detect_linebreak(data) 21 | if delimiter in data: 22 | data = data.split(delimiter, 1)[1] 23 | return data 24 | 25 | 26 | if __name__ == '__main__': 27 | print('This module is not supposed to be executed alone!') 28 | -------------------------------------------------------------------------------- /proxymodules/log.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os.path as path 3 | import time 4 | 5 | 6 | class Module: 7 | def __init__(self, incoming=False, verbose=False, options=None): 8 | # extract the file name from __file__. __file__ is proxymodules/name.py 9 | self.name = path.splitext(path.basename(__file__))[0] 10 | self.description = 'Log data in the module chain. Use in addition to general logging (-l/--log).' 11 | self.incoming = incoming # incoming means module is on -im chain 12 | self.find = None # if find is not None, this text will be highlighted 13 | # file: the file name, format is (in|out)-20160601-112233.13413 14 | self.file = ('in-' if incoming else 'out-') + \ 15 | time.strftime('%Y%m%d-%H%M%S.') + str(time.time()).split('.')[1] 16 | if options is not None: 17 | if 'file' in options.keys(): 18 | self.file = options['file'] 19 | self.handle = None 20 | 21 | def __del__(self): 22 | if self.handle is not None: 23 | self.handle.close() 24 | 25 | def execute(self, data): 26 | if self.handle is None: 27 | self.handle = open(self.file, 'wb', 0) # unbuffered 28 | print('Logging to file', self.file) 29 | logentry = bytes(time.strftime('%Y%m%d-%H%M%S') + ' ' + str(time.time()) + '\n', 'ascii') 30 | logentry += data 31 | logentry += b'-' * 20 + b'\n' 32 | self.handle.write(logentry) 33 | return data 34 | 35 | def help(self): 36 | h = '\tfile: name of logfile' 37 | return h 38 | 39 | 40 | if __name__ == '__main__': 41 | print('This module is not supposed to be executed alone!') 42 | -------------------------------------------------------------------------------- /proxymodules/mqtt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os.path as path 3 | import paho.mqtt.client as mqtt 4 | from distutils.util import strtobool 5 | 6 | 7 | class Module: 8 | def __init__(self, incoming=False, verbose=False, options=None): 9 | # extract the file name from __file__. __file__ is proxymodules/name.py 10 | self.name = path.splitext(path.basename(__file__))[0] 11 | self.description = 'Publish the data to an MQTT server' 12 | self.incoming = incoming # incoming means module is on -im chain 13 | self.client_id = '' 14 | self.username = None 15 | self.password = None 16 | self.server = None 17 | self.port = 1883 18 | self.topic = '' 19 | self.hex = False 20 | if options is not None: 21 | if 'clientid' in options.keys(): 22 | self.client_id = options['clientid'] 23 | if 'server' in options.keys(): 24 | self.server = options['server'] 25 | if 'username' in options.keys(): 26 | self.username = options['username'] 27 | if 'password' in options.keys(): 28 | self.password = options['password'] 29 | if 'port' in options.keys(): 30 | try: 31 | self.port = int(options['port']) 32 | if self.port not in range(1, 65536): 33 | raise ValueError 34 | except ValueError: 35 | print(f'port: invalid port {options["port"]}, using default {self.port}') 36 | if 'topic' in options.keys(): 37 | self.topic = options['topic'].strip() 38 | if 'hex' in options.keys(): 39 | try: 40 | self.hex = bool(strtobool(options['hex'])) 41 | except ValueError: 42 | print(f'hex: {options["hex"]} is not a bool value, falling back to default value {self.hex}.') 43 | 44 | if self.server is not None: 45 | self.mqtt = mqtt.Client(self.client_id) 46 | if self.username is not None or self.password is not None: 47 | self.mqtt.username_pw_set(self.username, self.password) 48 | self.mqtt.connect(self.server, self.port) 49 | else: 50 | self.mqtt = None 51 | 52 | def execute(self, data): 53 | if self.mqtt is not None: 54 | 55 | if self.hex is True: 56 | self.mqtt.publish(self.topic, data.hex()) 57 | else: 58 | self.mqtt.publish(self.topic, data) 59 | return data 60 | 61 | def help(self): 62 | h = '\tserver: server to connect to, required\n' 63 | h += ('\tclientid: what to use as client_id, default is empty\n' 64 | '\tusername: username\n' 65 | '\tpassword: password\n' 66 | '\tport: port to connect to, default 1883\n' 67 | '\ttopic: topic to publish to, default is empty\n' 68 | '\thex: encode data as hex before sending it. AAAA becomes 41414141.') 69 | return h 70 | 71 | 72 | if __name__ == '__main__': 73 | print('This module is not supposed to be executed alone!') 74 | -------------------------------------------------------------------------------- /proxymodules/removegzip.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os.path as path 3 | 4 | 5 | class Module: 6 | def __init__(self, incoming=False, verbose=False, options=None): 7 | # extract the file name from __file__. __file__ is proxymodules/name.py 8 | self.name = path.splitext(path.basename(__file__))[0] 9 | self.description = 'Replace gzip in the list of accepted encodings ' \ 10 | 'in a HTTP request with booo.' 11 | self.incoming = incoming # incoming means module is on -im chain 12 | # I chose to replace gzip instead of removing it to keep the parsing 13 | # logic as simple as possible. 14 | 15 | def execute(self, data): 16 | try: 17 | # split at \r\n\r\n to split the request into header and body 18 | header, body = data.split(b'\r\n\r\n', 1) 19 | except ValueError: 20 | # no \r\n\r\n, so probably not HTTP, we can go now 21 | return data 22 | # now split the header string into its lines 23 | headers = header.split(b'\r\n') 24 | 25 | for h in headers: 26 | if h.lower().startswith(b'accept-encoding:') and b'gzip' in h: 27 | headers[headers.index(h)] = h.replace(b'gzip', b'booo') 28 | break 29 | 30 | return b'\r\n'.join(headers) + b'\r\n\r\n' + body 31 | 32 | 33 | if __name__ == '__main__': 34 | print('This module is not supposed to be executed alone!') 35 | -------------------------------------------------------------------------------- /proxymodules/replace.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import re 4 | 5 | 6 | class Module: 7 | def __init__(self, incoming=False, verbose=False, options=None): 8 | # extract the file name from __file__. __file__ is proxymodules/name.py 9 | self.name = os.path.splitext(os.path.basename(__file__))[0] 10 | self.description = 'Replace text on the fly by using regular expressions in a file or as module parameters' 11 | self.verbose = verbose 12 | self.search = None 13 | self.replace = None 14 | self.filename = None 15 | self.separator = ':' 16 | 17 | if options is not None: 18 | if 'search' in options.keys(): 19 | self.search = bytes(options['search'], 'ascii') 20 | if 'replace' in options.keys(): 21 | self.replace = bytes(options['replace'], 'ascii') 22 | if 'file' in options.keys(): 23 | self.filename = options['file'] 24 | try: 25 | open(self.filename) 26 | except IOError as ioe: 27 | print("Error opening %s: %s" % (self.filename, ioe.strerror)) 28 | self.filename = None 29 | if 'separator' in options.keys(): 30 | self.separator = options['separator'] 31 | 32 | def execute(self, data): 33 | pairs = [] # list of (search, replace) tuples 34 | if self.search is not None and self.replace is not None: 35 | pairs.append((self.search, self.replace)) 36 | 37 | if self.filename is not None: 38 | for line in open(self.filename).readlines(): 39 | try: 40 | search, replace = line.split(self.separator, 1) 41 | pairs.append((bytes(search.strip(), 'ascii'), bytes(replace.strip(), 'ascii'))) 42 | except ValueError: 43 | # line does not contain separator and will be ignored 44 | pass 45 | 46 | for search, replace in pairs: 47 | # TODO: verbosity 48 | data = re.sub(search, replace, data) 49 | 50 | return data 51 | 52 | def help(self): 53 | h = '\tsearch: string or regular expression to search for\n' 54 | h += ('\treplace: string the search string should be replaced with\n') 55 | h += ('\tfile: file containing search:replace pairs, one per line\n') 56 | h += ('\tseparator: define a custom search:replace separator in the file, e.g. search#replace\n') 57 | h += ('\n\tUse at least file or search and replace (or both).\n') 58 | return h 59 | 60 | 61 | if __name__ == '__main__': 62 | print('This module is not supposed to be executed alone!') 63 | -------------------------------------------------------------------------------- /proxymodules/size.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os.path as path 3 | from distutils.util import strtobool 4 | 5 | 6 | class Module: 7 | def __init__(self, incoming=False, verbose=False, options=None): 8 | # extract the file name from __file__. __file__ is proxymodules/name.py 9 | self.name = path.splitext(path.basename(__file__))[0] 10 | self.description = 'Print the size of the data passed to the module' 11 | self.verbose = verbose 12 | self.source = None 13 | self.destination = None 14 | self.incoming = incoming 15 | if options is not None: 16 | if 'verbose' in options.keys(): 17 | self.verbose = bool(strtobool(options['verbose'])) 18 | 19 | def execute(self, data): 20 | size = len(data) 21 | msg = "Received %d bytes" % size 22 | if self.verbose: 23 | msg += " from %s:%d" % self.source 24 | msg += " for %s:%d" % self.destination 25 | print(msg) 26 | return data 27 | 28 | def help(self): 29 | h = '\tverbose: override the global verbosity setting' 30 | return h 31 | 32 | 33 | if __name__ == '__main__': 34 | print('This module is not supposed to be executed alone!') 35 | -------------------------------------------------------------------------------- /proxymodules/size404.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | import os.path as path 3 | import time 4 | from distutils.util import strtobool 5 | 6 | 7 | class Module: 8 | def __init__(self, incoming=False, verbose=False, options=None): 9 | # extract the file name from __file__. __file__ is proxymodules/name.py 10 | self.name = path.splitext(path.basename(__file__))[0] 11 | self.description = 'Change HTTP responses of a certain size to 404.' 12 | self.incoming = incoming # incoming means module is on -im chain 13 | self.size = 2392 # if a response has this value as content-length, it will become a 404 14 | self.verbose = False 15 | self.custom = False 16 | self.rewriteall = False # will we block the first occurence? 17 | self.firstfound = False # have we found the first occurence yet? 18 | self.resetinterval = None # if we haven't found a fitting response in this many seconds, reset the state and set first to False again 19 | self.timer = time.time() 20 | if options is not None: 21 | if 'size' in options.keys(): 22 | try: 23 | self.size = int(options['size']) 24 | except ValueError: 25 | pass # use the default if you can't parse the parameter 26 | if 'verbose' in options.keys(): 27 | self.verbose = bool(strtobool(options['verbose'])) 28 | if 'custom' in options.keys(): 29 | try: 30 | with open(options['custom'], 'rb') as handle: 31 | self.custom = handle.read() 32 | except Exception: 33 | print('Can\'t open custom error file, not using it.') 34 | self.custom = False 35 | if 'rewriteall' in options.keys(): 36 | self.rewriteall = bool(strtobool(options['rewriteall'])) 37 | if 'reset' in options.keys(): 38 | try: 39 | self.resetinterval = float(options['reset']) 40 | except ValueError: 41 | pass # use the default if you can't parse the parameter 42 | 43 | def execute(self, data): 44 | contentlength = b'content-length: ' + bytes(str(self.size), 'ascii') 45 | if data.startswith(b'HTTP/1.1 200 OK') and contentlength in data.lower(): 46 | if self.resetinterval is not None: 47 | t = time.time() 48 | if t - self.timer >= self.resetinterval: 49 | if self.verbose: 50 | print('Timer elapsed') 51 | self.firstfound = False 52 | self.timer = t 53 | if self.rewriteall is False and self.firstfound is False: 54 | # we have seen this response size for the first time and are not blocking the first one 55 | self.firstfound = True 56 | if self.verbose: 57 | print('Letting this response through') 58 | return data 59 | if self.custom is not False: 60 | data = self.custom 61 | if self.verbose: 62 | print('Replaced response with custom response') 63 | else: 64 | data = data.replace(b'200 OK', b'404 Not Found', 1) 65 | if self.verbose: 66 | print('Edited return code') 67 | return data 68 | 69 | def help(self): 70 | h = '\tsize: if a response has this value as content-length, it will become a 404\n' 71 | h += ('\tverbose: print a message if a string is replaced\n' 72 | '\tcustom: path to a file containing a custom response, will replace the received response\n' 73 | '\trewriteall: if set, it will rewrite all responses. Default is to let the first on through' 74 | '\treset: number of seconds after which we will reset the state and will let the next response through.') 75 | return h 76 | 77 | 78 | if __name__ == '__main__': 79 | print('This module is not supposed to be executed alone!') 80 | -------------------------------------------------------------------------------- /proxymodules/textdump.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os.path as path 3 | from codecs import decode, lookup 4 | 5 | 6 | class Module: 7 | def __init__(self, incoming=False, verbose=False, options=None): 8 | # extract the file name from __file__. __file__ is proxymodules/name.py 9 | self.name = path.splitext(path.basename(__file__))[0] 10 | self.description = 'Simply print the received data as text' 11 | self.incoming = incoming # incoming means module is on -im chain 12 | self.find = None # if find is not None, this text will be highlighted 13 | self.codec = 'latin_1' 14 | if options is not None: 15 | if 'find' in options.keys(): 16 | self.find = bytes(options['find'], 'ascii') # text to highlight 17 | if 'color' in options.keys(): 18 | self.color = bytes('\033[' + options['color'] + 'm', 'ascii') # highlight color 19 | else: 20 | self.color = b'\033[31;1m' 21 | if 'codec' in options.keys(): 22 | codec = options['codec'] 23 | try: 24 | lookup(codec) 25 | self.codec = codec 26 | except LookupError: 27 | print(f"{self.name}: {options['codec']} is not a valid codec, using {self.codec}") 28 | 29 | 30 | def execute(self, data): 31 | if self.find is None: 32 | print(decode(data, self.codec)) 33 | else: 34 | pdata = data.replace(self.find, self.color + self.find + b'\033[0m') 35 | print(decode(pdata, self.codec)) 36 | return data 37 | 38 | def help(self): 39 | h = '\tfind: string that should be highlighted\n' 40 | h += ('\tcolor: ANSI color code. Will be wrapped with \\033[ and m, so' 41 | ' passing 32;1 will result in \\033[32;1m (bright green)') 42 | return h 43 | 44 | 45 | if __name__ == '__main__': 46 | print('This module is not supposed to be executed alone!') 47 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | paho-mqtt 2 | PySocks 3 | -------------------------------------------------------------------------------- /tcpproxy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import argparse 3 | import pkgutil 4 | import os 5 | import sys 6 | import threading 7 | import socket 8 | import socks 9 | import ssl 10 | import time 11 | import select 12 | import errno 13 | 14 | # TODO: implement verbose output 15 | # some code snippets, as well as the original idea, from Black Hat Python 16 | 17 | 18 | def is_valid_ip4(ip): 19 | # some rudimentary checks if ip is actually a valid IP 20 | octets = ip.split('.') 21 | if len(octets) != 4: 22 | return False 23 | return octets[0] != 0 and all(0 <= int(octet) <= 255 for octet in octets) 24 | 25 | 26 | def parse_args(): 27 | parser = argparse.ArgumentParser(description='Simple TCP proxy for data ' + 28 | 'interception and ' + 29 | 'modification. ' + 30 | 'Select modules to handle ' + 31 | 'the intercepted traffic.') 32 | 33 | parser.add_argument('-ti', '--targetip', dest='target_ip', 34 | help='remote target IP or host name') 35 | 36 | parser.add_argument('-tp', '--targetport', dest='target_port', type=int, 37 | help='remote target port') 38 | 39 | parser.add_argument('-li', '--listenip', dest='listen_ip', 40 | default='0.0.0.0', help='IP address/host name to listen for ' + 41 | 'incoming data') 42 | 43 | parser.add_argument('-lp', '--listenport', dest='listen_port', type=int, 44 | default=8080, help='port to listen on') 45 | 46 | parser.add_argument('-si', '--sourceip', dest='source_ip', 47 | help='IP address the other end will see') 48 | 49 | parser.add_argument('-sp', '--sourceport', dest='source_port', type=int, 50 | help='source port the other end will see') 51 | 52 | parser.add_argument('-pi', '--proxy-ip', dest='proxy_ip', default=None, 53 | help='IP address/host name of proxy') 54 | 55 | parser.add_argument('-pp', '--proxy-port', dest='proxy_port', type=int, 56 | default=1080, help='proxy port', ) 57 | 58 | parser.add_argument('-pt', '--proxy-type', dest='proxy_type', default='SOCKS5', choices=['SOCKS4', 'SOCKS5', 'HTTP'], 59 | type = str.upper, help='proxy type. Options are SOCKS5 (default), SOCKS4, HTTP') 60 | 61 | parser.add_argument('-om', '--outmodules', dest='out_modules', 62 | help='comma-separated list of modules to modify data' + 63 | ' before sending to remote target.') 64 | 65 | parser.add_argument('-im', '--inmodules', dest='in_modules', 66 | help='comma-separated list of modules to modify data' + 67 | ' received from the remote target.') 68 | 69 | parser.add_argument('-v', '--verbose', dest='verbose', default=False, 70 | action='store_true', 71 | help='More verbose output of status information') 72 | 73 | parser.add_argument('-n', '--no-chain', dest='no_chain_modules', 74 | action='store_true', default=False, 75 | help='Don\'t send output from one module to the ' + 76 | 'next one') 77 | 78 | parser.add_argument('-l', '--log', dest='logfile', default=None, 79 | help='Log all data to a file before modules are run.') 80 | 81 | parser.add_argument('--list', dest='list', action='store_true', 82 | help='list available modules') 83 | 84 | parser.add_argument('-lo', '--list-options', dest='help_modules', default=None, 85 | help='Print help of selected module') 86 | 87 | parser.add_argument('-s', '--ssl', dest='use_ssl', action='store_true', 88 | default=False, help='detect SSL/TLS as well as STARTTLS') 89 | 90 | parser.add_argument('-sc', '--server-certificate', default='mitm.pem', 91 | help='server certificate in PEM format (default: %(default)s)') 92 | 93 | parser.add_argument('-sk', '--server-key', default='mitm.pem', 94 | help='server key in PEM format (default: %(default)s)') 95 | 96 | parser.add_argument('-cc', '--client-certificate', default=None, 97 | help='client certificate in PEM format in case client authentication is required by the target') 98 | 99 | parser.add_argument('-ck', '--client-key', default=None, 100 | help='client key in PEM format in case client authentication is required by the target') 101 | 102 | return parser.parse_args() 103 | 104 | 105 | def generate_module_list(modstring, incoming=False, verbose=False): 106 | # This method receives the comma-separated module list, imports the modules 107 | # and creates a Module instance for each module. A list of these instances 108 | # is then returned. 109 | # The incoming parameter is True when the modules belong to the incoming 110 | # chain (-im) 111 | # modstring looks like mod1,mod2:key=val,mod3:key=val:key2=val2,mod4 ... 112 | modlist = [] 113 | namelist = modstring.split(',') 114 | for n in namelist: 115 | name, options = parse_module_options(n) 116 | try: 117 | __import__('proxymodules.' + name) 118 | modlist.append(sys.modules['proxymodules.' + name].Module(incoming, verbose, options)) 119 | except ImportError: 120 | print('Module %s not found' % name) 121 | sys.exit(3) 122 | return modlist 123 | 124 | 125 | def parse_module_options(n): 126 | # n is of the form module_name:key1=val1:key2=val2 ... 127 | # this method returns the module name and a dict with the options 128 | n = n.split(':', 1) 129 | if len(n) == 1: 130 | # no module options present 131 | return n[0], None 132 | name = n[0] 133 | optionlist = n[1].split(':') 134 | options = {} 135 | for op in optionlist: 136 | try: 137 | k, v = op.split('=') 138 | options[k] = v 139 | except ValueError: 140 | print(op, ' is not valid!') 141 | sys.exit(23) 142 | return name, options 143 | 144 | 145 | def list_modules(): 146 | # show all available proxy modules 147 | cwd = os.getcwd() 148 | module_path = cwd + os.sep + 'proxymodules' 149 | for _, module, _ in pkgutil.iter_modules([module_path]): 150 | __import__('proxymodules.' + module) 151 | m = sys.modules['proxymodules.' + module].Module() 152 | print(f'{m.name} - {m.description}') 153 | 154 | 155 | def print_module_help(modlist): 156 | # parse comma-separated list of module names, print module help text 157 | modules = generate_module_list(modlist) 158 | for m in modules: 159 | try: 160 | print(f'{m.name} - {m.description}') 161 | print(m.help()) 162 | except AttributeError: 163 | print('\tNo options or missing help() function.') 164 | 165 | 166 | def update_module_hosts(modules, source, destination): 167 | # set source and destination IP/port for each module 168 | # source and destination are ('IP', port) tuples 169 | # this can only be done once local and remote connections have been established 170 | if modules is not None: 171 | for m in modules: 172 | if hasattr(m, 'source'): 173 | m.source = source 174 | if hasattr(m, 'destination'): 175 | m.destination = destination 176 | 177 | 178 | def receive_from(s): 179 | # receive data from a socket until no more data is there 180 | b = b"" 181 | while True: 182 | data = s.recv(4096) 183 | b += data 184 | if not data or len(data) < 4096: 185 | break 186 | return b 187 | 188 | 189 | def handle_data(data, modules, dont_chain, incoming, verbose): 190 | # execute each active module on the data. If dont_chain is set, feed the 191 | # output of one plugin to the following plugin. Not every plugin will 192 | # necessarily modify the data, though. 193 | for m in modules: 194 | vprint(("> > > > in: " if incoming else "< < < < out: ") + m.name, verbose) 195 | if dont_chain: 196 | m.execute(data) 197 | else: 198 | data = m.execute(data) 199 | return data 200 | 201 | 202 | def is_client_hello(sock): 203 | firstbytes = sock.recv(128, socket.MSG_PEEK) 204 | return (len(firstbytes) >= 3 and 205 | firstbytes[0] == 0x16 and 206 | firstbytes[1:3] in [b"\x03\x00", 207 | b"\x03\x01", 208 | b"\x03\x02", 209 | b"\x03\x03", 210 | b"\x02\x00"] 211 | ) 212 | 213 | 214 | def enable_ssl(args, remote_socket, local_socket): 215 | sni = None 216 | 217 | def sni_callback(sock, name, ctx): 218 | nonlocal sni 219 | sni = name 220 | 221 | try: 222 | ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) 223 | ctx.sni_callback = sni_callback 224 | ctx.load_cert_chain(certfile=args.server_certificate, 225 | keyfile=args.server_key, 226 | ) 227 | local_socket = ctx.wrap_socket(local_socket, 228 | server_side=True, 229 | ) 230 | except ssl.SSLError as e: 231 | print("SSL handshake failed for listening socket", str(e)) 232 | raise 233 | 234 | try: 235 | ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) 236 | ctx.check_hostname = False 237 | ctx.verify_mode = ssl.CERT_NONE 238 | if args.client_certificate and args.client_key: 239 | ctx.load_cert_chain(certfile=args.client_certificate, 240 | keyfile=args.client_key, 241 | ) 242 | remote_socket = ctx.wrap_socket(remote_socket, 243 | server_hostname=sni, 244 | ) 245 | except ssl.SSLError as e: 246 | print("SSL handshake failed for remote socket", str(e)) 247 | raise 248 | 249 | return [remote_socket, local_socket] 250 | 251 | 252 | def starttls(args, local_socket, read_sockets): 253 | return (args.use_ssl and 254 | local_socket in read_sockets and 255 | not isinstance(local_socket, ssl.SSLSocket) and 256 | is_client_hello(local_socket) 257 | ) 258 | 259 | 260 | def start_proxy_thread(local_socket, args, in_modules, out_modules): 261 | # This method is executed in a thread. It will relay data between the local 262 | # host and the remote host, while letting modules work on the data before 263 | # passing it on. 264 | remote_socket = socks.socksocket() 265 | 266 | if args.proxy_ip: 267 | proxy_types = {'SOCKS5': socks.SOCKS5, 'SOCKS4': socks.SOCKS4, 'HTTP': socks.HTTP} 268 | remote_socket.set_proxy(proxy_types[args.proxy_type], args.proxy_ip, args.proxy_port) 269 | 270 | try: 271 | if args.source_ip or args.source_port: 272 | remote_socket.bind((args.source_ip, args.source_port)) 273 | remote_socket.connect((args.target_ip, args.target_port)) 274 | vprint('Connected to %s:%d' % remote_socket.getpeername(), args.verbose) 275 | log(args.logfile, 'Connected to %s:%d' % remote_socket.getpeername()) 276 | except socket.error as serr: 277 | if serr.errno == errno.ECONNREFUSED: 278 | for s in [remote_socket, local_socket]: 279 | s.close() 280 | print(f'{time.strftime("%Y%m%d-%H%M%S")}, {args.target_ip}:{args.target_port}- Connection refused') 281 | log(args.logfile, f'{time.strftime("%Y%m%d-%H%M%S")}, {args.target_ip}:{args.target_port}- Connection refused') 282 | return None 283 | elif serr.errno == errno.ETIMEDOUT: 284 | for s in [remote_socket, local_socket]: 285 | s.close() 286 | print(f'{time.strftime("%Y%m%d-%H%M%S")}, {args.target_ip}:{args.target_port}- Connection timed out') 287 | log(args.logfile, f'{time.strftime("%Y%m%d-%H%M%S")}, {args.target_ip}:{args.target_port}- Connection timed out') 288 | return None 289 | else: 290 | for s in [remote_socket, local_socket]: 291 | s.close() 292 | raise serr 293 | 294 | try: 295 | update_module_hosts(out_modules, local_socket.getpeername(), remote_socket.getpeername()) 296 | update_module_hosts(in_modules, remote_socket.getpeername(), local_socket.getpeername()) 297 | except socket.error as serr: 298 | if serr.errno == errno.ENOTCONN: 299 | # kind of a blind shot at fixing issue #15 300 | # I don't yet understand how this error can happen, but if it happens I'll just shut down the thread 301 | # the connection is not in a useful state anymore 302 | for s in [remote_socket, local_socket]: 303 | s.close() 304 | return None 305 | else: 306 | for s in [remote_socket, local_socket]: 307 | s.close() 308 | print(f"{time.strftime('%Y%m%d-%H%M%S')}: Socket exception in start_proxy_thread") 309 | raise serr 310 | 311 | # This loop ends when no more data is received on either the local or the 312 | # remote socket 313 | running = True 314 | while running: 315 | read_sockets, _, _ = select.select([remote_socket, local_socket], [], []) 316 | 317 | if starttls(args, local_socket, read_sockets): 318 | try: 319 | ssl_sockets = enable_ssl(args, remote_socket, local_socket) 320 | remote_socket, local_socket = ssl_sockets 321 | vprint("SSL enabled", args.verbose) 322 | log(args.logfile, "SSL enabled") 323 | except ssl.SSLError as e: 324 | print("SSL handshake failed", str(e)) 325 | log(args.logfile, "SSL handshake failed", str(e)) 326 | break 327 | 328 | read_sockets, _, _ = select.select(ssl_sockets, [], []) 329 | 330 | for sock in read_sockets: 331 | try: 332 | peer = sock.getpeername() 333 | except socket.error as serr: 334 | if serr.errno == errno.ENOTCONN: 335 | # kind of a blind shot at fixing issue #15 336 | # I don't yet understand how this error can happen, but if it happens I'll just shut down the thread 337 | # the connection is not in a useful state anymore 338 | for s in [remote_socket, local_socket]: 339 | s.close() 340 | running = False 341 | break 342 | else: 343 | print(f"{time.strftime('%Y%m%d-%H%M%S')}: Socket exception in start_proxy_thread") 344 | raise serr 345 | 346 | data = receive_from(sock) 347 | log(args.logfile, 'Received %d bytes' % len(data)) 348 | 349 | if sock == local_socket: 350 | if len(data): 351 | log(args.logfile, b'< < < out\n' + data) 352 | if out_modules is not None: 353 | data = handle_data(data, out_modules, 354 | args.no_chain_modules, 355 | False, # incoming data? 356 | args.verbose) 357 | remote_socket.send(data.encode() if isinstance(data, str) else data) 358 | else: 359 | vprint("Connection from local client %s:%d closed" % peer, args.verbose) 360 | log(args.logfile, "Connection from local client %s:%d closed" % peer) 361 | remote_socket.close() 362 | running = False 363 | break 364 | elif sock == remote_socket: 365 | if len(data): 366 | log(args.logfile, b'> > > in\n' + data) 367 | if in_modules is not None: 368 | data = handle_data(data, in_modules, 369 | args.no_chain_modules, 370 | True, # incoming data? 371 | args.verbose) 372 | local_socket.send(data) 373 | else: 374 | vprint("Connection to remote server %s:%d closed" % peer, args.verbose) 375 | log(args.logfile, "Connection to remote server %s:%d closed" % peer) 376 | local_socket.close() 377 | running = False 378 | break 379 | 380 | 381 | def log(handle, message, message_only=False): 382 | # if message_only is True, only the message will be logged 383 | # otherwise the message will be prefixed with a timestamp and a line is 384 | # written after the message to make the log file easier to read 385 | if not isinstance(message, bytes): 386 | message = bytes(message, 'ascii') 387 | if handle is None: 388 | return 389 | if not message_only: 390 | logentry = bytes("%s %s\n" % (time.strftime('%Y%m%d-%H%M%S'), str(time.time())), 'ascii') 391 | else: 392 | logentry = b'' 393 | logentry += message 394 | if not message_only: 395 | logentry += b'\n' + b'-' * 20 + b'\n' 396 | handle.write(logentry) 397 | 398 | 399 | def vprint(msg, is_verbose): 400 | # this will print msg, but only if is_verbose is True 401 | if is_verbose: 402 | print(msg) 403 | 404 | 405 | def main(): 406 | args = parse_args() 407 | if args.list is False and args.help_modules is None: 408 | if not args.target_ip: 409 | print('Target IP is required: -ti') 410 | sys.exit(6) 411 | if not args.target_port: 412 | print('Target port is required: -tp') 413 | sys.exit(7) 414 | 415 | if ((args.client_key is None) ^ (args.client_certificate is None)): 416 | print("You must either specify both the client certificate and client key or leave both empty") 417 | sys.exit(8) 418 | 419 | if args.logfile is not None: 420 | try: 421 | args.logfile = open(args.logfile, 'ab', 0) # unbuffered 422 | except Exception as ex: 423 | print('Error opening logfile') 424 | print(ex) 425 | sys.exit(4) 426 | 427 | if args.list: 428 | list_modules() 429 | sys.exit(0) 430 | 431 | if args.help_modules is not None: 432 | print_module_help(args.help_modules) 433 | sys.exit(0) 434 | 435 | if args.listen_ip != '0.0.0.0' and not is_valid_ip4(args.listen_ip): 436 | try: 437 | ip = socket.gethostbyname(args.listen_ip) 438 | except socket.gaierror: 439 | ip = False 440 | if ip is False: 441 | print('%s is not a valid IP address or host name' % args.listen_ip) 442 | sys.exit(1) 443 | else: 444 | args.listen_ip = ip 445 | 446 | if not is_valid_ip4(args.target_ip): 447 | try: 448 | ip = socket.gethostbyname(args.target_ip) 449 | except socket.gaierror: 450 | ip = False 451 | if ip is False: 452 | print('%s is not a valid IP address or host name' % args.target_ip) 453 | sys.exit(2) 454 | else: 455 | args.target_ip = ip 456 | 457 | if args.in_modules is not None: 458 | in_modules = generate_module_list(args.in_modules, incoming=True, verbose=args.verbose) 459 | else: 460 | in_modules = None 461 | 462 | if args.out_modules is not None: 463 | out_modules = generate_module_list(args.out_modules, incoming=False, verbose=args.verbose) 464 | else: 465 | out_modules = None 466 | 467 | # this is the socket we will listen on for incoming connections 468 | proxy_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 469 | proxy_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 470 | 471 | try: 472 | proxy_socket.bind((args.listen_ip, args.listen_port)) 473 | except socket.error as e: 474 | print(e.strerror) 475 | sys.exit(5) 476 | 477 | proxy_socket.listen(100) 478 | log(args.logfile, str(args)) 479 | # endless loop until ctrl+c 480 | try: 481 | while True: 482 | in_socket, in_addrinfo = proxy_socket.accept() 483 | vprint('Connection from %s:%d' % in_addrinfo, args.verbose) 484 | log(args.logfile, 'Connection from %s:%d' % in_addrinfo) 485 | proxy_thread = threading.Thread(target=start_proxy_thread, 486 | args=(in_socket, args, in_modules, 487 | out_modules)) 488 | log(args.logfile, "Starting proxy thread " + proxy_thread.name) 489 | proxy_thread.start() 490 | except KeyboardInterrupt: 491 | log(args.logfile, 'Ctrl+C detected, exiting...') 492 | print('\nCtrl+C detected, exiting...') 493 | sys.exit(0) 494 | 495 | 496 | if __name__ == '__main__': 497 | main() 498 | --------------------------------------------------------------------------------