forked from ShutdownRepo/httpmethods
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathhttpmethods.py
executable file
·328 lines (290 loc) · 11.3 KB
/
httpmethods.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import argparse
import sys
from concurrent.futures import ThreadPoolExecutor
import requests
from rich.console import Console
from rich import box
from rich.table import Table
import json
from http.cookies import SimpleCookie
banner = "[~] HTTP Methods Tester, v1.1.3\n"
methods = [
'CHECKIN', 'CHECKOUT', 'CONNECT', 'COPY', 'DELETE', 'GET', 'HEAD', 'INDEX',
'LINK', 'LOCK', 'MKCOL', 'MOVE', 'NOEXISTE', 'OPTIONS', 'ORDERPATCH',
'PATCH', 'POST', 'PROPFIND', 'PROPPATCH', 'PUT', 'REPORT', 'SEARCH',
'SHOWMETHOD', 'SPACEJUMP', 'TEXTSEARCH', 'TRACE', 'TRACK', 'UNCHECKOUT',
'UNLINK', 'UNLOCK', 'VERSION-CONTROL', 'BAMBOOZLE'
]
class Logger(object):
def __init__(self, verbosity=0, quiet=False):
self.verbosity = verbosity
self.quiet = quiet
def debug(self, message):
if self.verbosity == 2:
console.print("{}[DEBUG]{} {}".format("[yellow3]", "[/yellow3]", message), highlight=False)
def verbose(self, message):
if self.verbosity >= 1:
console.print("{}[VERBOSE]{} {}".format("[blue]", "[/blue]", message), highlight=False)
def info(self, message):
if not self.quiet:
console.print("{}[*]{} {}".format("[bold blue]", "[/bold blue]", message), highlight=False)
def success(self, message):
if not self.quiet:
console.print("{}[+]{} {}".format("[bold green]", "[/bold green]", message), highlight=False)
def warning(self, message):
if not self.quiet:
console.print("{}[-]{} {}".format("[bold orange3]", "[/bold orange3]", message), highlight=False)
def error(self, message):
if not self.quiet:
console.print("{}[!]{} {}".format("[bold red]", "[/bold red]", message), highlight=False)
def get_options():
description = "This Python script can be used for HTTP verb tampering to bypass forbidden access, and for HTTP " \
"methods enumeration to find dangerous enabled methods like PUT "
parser = argparse.ArgumentParser(
description=description,
formatter_class=argparse.RawTextHelpFormatter,
)
parser.add_argument(
"url",
help="e.g. https://example.com:port/path"
)
parser.add_argument(
"-v",
"--verbose",
dest="verbosity",
action="count",
default=0,
help="verbosity level (-v for verbose, -vv for debug)",
)
parser.add_argument(
"-q",
"--quiet",
dest="quiet",
action="store_true",
default=False,
help="Show no information at all",
)
parser.add_argument(
"-k",
"--insecure",
dest="verify",
action="store_false",
default=True,
required=False,
help="Allow insecure server connections when using SSL (default: False)",
)
parser.add_argument(
"-L",
"--location",
dest="redirect",
action="store_true",
default=False,
required=False,
help="Follow redirects (default: False)",
)
parser.add_argument(
"-w",
"--wordlist",
dest="wordlist",
action="store",
default=None,
required=False,
help="HTTP methods wordlist (default is a builtin wordlist)",
)
parser.add_argument(
"-t",
"--threads",
dest="threads",
action="store",
type=int,
default=5,
required=False,
help="Number of threads (default: 5)",
)
parser.add_argument(
"-j",
"--jsonfile",
dest="jsonfile",
default=None,
required=False,
help="Save results to specified JSON file.",
)
parser.add_argument(
'-x',
'--proxy',
action="store",
default=None,
dest='proxy',
help="Specify a proxy to use for requests (e.g., http://localhost:8080)"
)
parser.add_argument(
'-b', '--cookies',
action="store",
default=None,
dest='cookies',
help='Specify cookies to use in requests. (e.g., --cookies "cookie1=blah;cookie2=blah")'
)
options = parser.parse_args()
return options
def methods_from_wordlist(wordlist):
logger.verbose(f"Retrieving methods from wordlist {wordlist}")
try:
with open(options.wordlist, "r") as infile:
methods += infile.read().split()
except Exception as e:
logger.error(f"Had some kind of error loading the wordlist ¯\_(ツ)_/¯: {e}")
def methods_from_http_options(console, options, proxies, cookies):
options_methods = []
logger.verbose("Pulling available methods from server with an OPTIONS request")
try:
r = requests.options(
url=options.url,
proxies=proxies,
cookies=cookies,
verify=options.verify
)
except requests.exceptions.ProxyError:
logger.error("Invalid proxy specified ")
raise SystemExit
if r.status_code == 200:
logger.verbose("URL accepts OPTIONS")
logger.debug(r.headers)
if "Allow" in r.headers:
logger.info("URL answers with a list of options: {}".format(r.headers["Allow"]))
include_options_methods = console.input(
"[bold orange3][?][/bold orange3] Do you want to add these methods to the test (be careful, some methods can be dangerous)? [Y/n] ")
if not include_options_methods.lower() == "n":
for method in r.headers["Allow"].replace(" ", "").split(","):
if method not in options_methods:
logger.debug(f"Adding new method {method} to methods")
options_methods.append(method)
else:
logger.debug(f"Method {method} already in known methods, passing")
else:
logger.debug("Methods found with OPTIONS won't be added to the tested methods")
else:
logger.verbose("URL doesn't answer with a list of options")
else:
logger.verbose("URL rejects OPTIONS")
return options_methods
def test_method(options, method, proxies, cookies, results):
try:
r = requests.request(
method=method,
url=options.url,
verify=options.verify, # this is to set the client to accept insecure servers
proxies=proxies,
cookies=cookies,
allow_redirects=options.redirect,
stream=True, # this is to prevent the download of huge files, focus on the request, not on the data
)
except requests.exceptions.ProxyError:
logger.error("Invalid proxy specified ")
raise SystemExit
logger.debug(f"Obtained results: {method}, {str(r.status_code)}, {str(len(r.text))}, {r.reason}")
results[method] = {"status_code": r.status_code, "length": len(r.text), "reason": r.reason[:100]}
def print_results(console, results):
logger.verbose("Parsing & printing results")
table = Table(show_header=True, header_style="bold blue", border_style="blue", box=box.SIMPLE)
table.add_column("Method")
table.add_column("Length")
table.add_column("Status code")
table.add_column("Reason")
for result in results.items():
if result[1]["status_code"] == 200: # This means the method is accepted
style = "green"
elif (300 <= result[1]["status_code"] <= 399):
style = "cyan"
elif 400 <= result[1]["status_code"] <= 499: # This means the method is disabled in most cases
style = "red"
elif (500 <= result[1]["status_code"] <= 599) and result[1][
"status_code"] != 502: # This means the method is not implemented in most cases
style = "orange3"
elif result[1]["status_code"] == 502: # This probably means the method is accepted but request was malformed
style = "yellow4"
else:
style = None
table.add_row(result[0], str(result[1]["length"]), str(result[1]["status_code"]), result[1]["reason"], style=style)
console.print(table)
def json_export(results, json_file):
f = open(json_file, "w")
f.write(json.dumps(results, indent=4) + "\n")
f.close()
def main(options, logger, console):
logger.info("Starting HTTP verb enumerating and tampering")
global methods
results = {}
# Verifying the proxy option
if options.proxy:
try:
proxies = {
"http": "http://" + options.proxy.split('//')[1],
"https": "http://" + options.proxy.split('//')[1]
}
logger.debug(f"Setting proxies to {str(proxies)}")
except (IndexError, ValueError):
logger.error("Invalid proxy specified ")
sys.exit(1)
else:
logger.debug("Setting proxies to 'None'")
proxies = None
# Parsing cookie option
if options.cookies:
cookie = SimpleCookie()
cookie.load(options.cookies)
cookies = {key: value.value for key, value in cookie.items()}
else:
cookies = {}
if options.wordlist is not None:
methods += methods_from_wordlist(options.wordlist)
methods += methods_from_http_options(console, options, proxies, cookies)
# Sort uniq
methods = [m.upper() for m in methods]
methods = sorted(list(set(methods)))
# Filtering for dangerous methods
filtered_methods = []
for method in methods:
if method in ["DELETE", "COPY", "PUT", "PATCH", "UNCHECKOUT"]:
test_dangerous_method = console.input(
f"[bold orange3][?][/bold orange3] Do you really want to test method {method} (can be dangerous)? \[y/N] ")
if not test_dangerous_method.lower() == "y":
logger.verbose(f"Method {method} will not be tested")
else:
logger.verbose(f"Method {method} will be tested")
filtered_methods.append(method)
else:
filtered_methods.append(method)
methods = filtered_methods[:]
del filtered_methods
# Waits for all the threads to be completed
with ThreadPoolExecutor(max_workers=min(options.threads, len(methods))) as tp:
for method in methods:
tp.submit(test_method, options, method, proxies, cookies, results)
# Sorting the results by method name
results = {key: results[key] for key in sorted(results)}
# Parsing and print results
print_results(console, results)
# Export to JSON if specified
if options.jsonfile is not None:
json_export(results, options.jsonfile)
if __name__ == '__main__':
try:
print(banner)
options = get_options()
logger = Logger(options.verbosity, options.quiet)
console = Console()
if not options.verify:
# Disable warings of insecure connection for invalid cerificates
requests.packages.urllib3.disable_warnings()
# Allow use of deprecated and weak cipher methods
requests.packages.urllib3.util.ssl_.DEFAULT_CIPHERS += ':HIGH:!DH:!aNULL'
try:
requests.packages.urllib3.contrib.pyopenssl.util.ssl_.DEFAULT_CIPHERS += ':HIGH:!DH:!aNULL'
except AttributeError:
pass
main(options, logger, console)
except KeyboardInterrupt:
logger.info("Terminating script...")
raise SystemExit