Source: Blind SQLi (time-based)

apps/sqli/labs/blind_time.py · view on GitHub

← back to lab

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
"""SQLi lab: blind-time — INTENTIONALLY VULNERABLE.

Username availability check. The response is identical regardless of
whether the username exists, so there's no boolean oracle. The attacker
gets nothing — except control over how long the query takes, via SLEEP().

Time-based blind is the slowest extraction primitive, and it's the one
tools that lack a real timing model tend to miss.
"""
from __future__ import annotations

from pathlib import Path

import pymysql
from flask import Blueprint, render_template, request

from ..db import get_conn

bp = Blueprint("sqli_blind_time", __name__, url_prefix="/blind-time")

META = {
    "slug": "blind-time",
    "title": "Blind SQLi (time-based)",
    "summary": "Username availability returns identical responses; only query time leaks.",
    "hint": (
        "Query: SELECT username FROM users WHERE username='<input>' LIMIT 1. "
        "Response is the same string either way. Use SLEEP() inside a UNION "
        "or AND clause: nonexistent' UNION SELECT IF(SUBSTRING((SELECT "
        "value FROM secrets WHERE name='sqli-blind-time'),1,1)='V', "
        "SLEEP(2), 'x')-- "
    ),
    "sink": "string-concatenated SELECT (no response variation)",
    "source_path": str(Path(__file__).resolve()),
    "vulnerable": True,
}


@bp.route("/", methods=["GET"])
def lab():
    username = request.args.get("username", "")
    response_msg = None
    if username:
        # INTENTIONAL: raw concatenation. Note the response normalization
        # below — we never reveal which branch was taken.
        query = (
            f"SELECT username FROM users WHERE username='{username}' LIMIT 1"
        )
        try:
            with get_conn().cursor() as cur:
                cur.execute(query)
                cur.fetchall()  # drain
        except pymysql.MySQLError:
            pass
        # Same message either way. Timing is the only signal.
        response_msg = "Username check complete."
    return render_template("lab_blind_time.html", meta=META, username=username, msg=response_msg)