$ cat writeup.md…
$ cat writeup.md…
sekai2026
Task: a Go note app sanitizes each message and serves stored files as HTML to an admin bot. Solution: race concurrent PUT writes so two individually safe sanitized bodies splice into an executable img/onerror tag.
$ cat /etc/rate-limit
Rate limit reached (20 reads/hour per IP). Showing preview only — full content returns at the next hour roll-over.
Original organizer task description was not available in the solving notes.
English summary: the challenge provided a Go note application and an admin bot. The goal was to make the bot visit an attacker-controlled note and print its FLAG cookie to the console.
The application stores notes as files under /app/notes/{uuid} and serves them with:
Content-Type: text/html;charset=utf-8
Creating or updating a note runs this sanitizer in app/main.go:
sanitized := bluemonday.StrictPolicy().Sanitize(msg) sanitized = strings.ReplaceAll(sanitized, "<", "<") sanitized = strings.ReplaceAll(sanitized, ">", ">") sanitized = regexp.MustCompile(`<(/)?\w+`).ReplaceAllString(sanitized, "")
Per request this looks fairly strong for normal stored XSS. Encoded tags are decoded and then the final regex removes any < followed by an optional slash and a word character. For example, <img/src/onerror=console.log(document.cookie)> becomes only the attribute fragment src/onerror=console.log(document.cookie)>, not an element.
The important bug was not inside the HTML parser or bluemonday. It was in the update path:
f, _ := os.OpenFile(filePath, os.O_WRONLY|os.O_TRUNC, 0644) f.Write([]byte(sanitized))
There was no locking. Concurrent PUT /notes/{id} requests can open/truncate/write the same file at overlapping times. Each request writes a body that was safe by itself, but a later short write can overwrite the beginning of a longer write and create unsafe HTML in the final file.
The admin bot sets FLAG as a cookie for ltw.chals.sekai.team, visits https://ltw.chals.sekai.team/notes/{uuid}, and logs browser console output. CSP is restrictive for external resources but allows inline JavaScript:
default-src 'none'; script-src 'unsafe-inline'
So console.log(document.cookie) is enough for exfiltration.
...
$ grep --similar