After the last lesson the Hacker added protection that rejects any file ending in .php. But the server runs many other extensions as code, not just .php.
The same upload page, but the developer added protection that blocks only files ending in php. A one-extension blacklist is bypassed by other executable extensions for the same interpreter.
Upload a web shell that runs despite the .php ban.
You bypassed the .php ban with another executable extension and uploaded a web shell.
Why the solution worked
The protection was a blacklist of one extension, but servers run many (phtml, php5, phar and more), so banning one is not enough and a blacklist is always incomplete. The fix: an allowlist of the permitted extensions only (images), real content inspection, and renaming to a random name with an extension you choose, not the user.
The root cause
The blacklist blocks one extension while the interpreter runs several; the root is relying on extension blocking instead of an allowlist, plus storing where code runs.
How the code is fixed
Do not ban the known-bad; allow only the known-good. The executable extensions never end.
Vulnerable code
if (req.file.name.endsWith('.php')) return res.status(400).end(); // one extension
fs.writeFileSync('/var/www/uploads/' + req.file.name, req.file.data);
Safe code
const ALLOW = new Set(['png', 'jpg', 'jpeg']); // allowlist
const ext = req.file.name.split('.').pop().toLowerCase();
if (!ALLOW.has(ext) || !isImage(req.file.data)) return res.status(400).end();
fs.writeFileSync('/srv/blobs/' + randomId() + '.' + ext, req.file.data);
Impact in the real world
Extension blacklists are always broken by a forgotten extension, so planting a web shell stays possible.
How defenders detect it
Defenders use an extension allowlist, store outside the web root, and alert on executable extensions in uploads.