$ cat writeup.md…
$ cat writeup.md…
avitoctf
Task: A recruitment portal contains one public VIP resume and an authenticated resume importer that performs server-side URL fetches. Solution: Use SSRF to inspect cloud metadata, pull the authorized backend image, and trace its hardcoded VIP predicate.
The organizer description was not preserved verbatim in the task artifacts. The challenge asked which rare work experience makes a resume receive the gold recommended-candidate badge.
The public resume list contained nine seeded records. Resume ID 2 was the only record with is_vip: true, and its generated PDF displayed the gold badge. Two direct approaches failed:
is_vip: true field to the resume update API was ignored.This established that VIP was derived by backend logic and was not ordinary mass assignment.
Continuity from the first task supplied the PDF beta invitation and led to the authenticated endpoint POST /api/seeker/resume/import. The endpoint fetched a supplied URL on the server and returned fetched content when schema validation failed, providing an SSRF-based disclosure primitive. Cloud instance user-data exposed the deployment configuration, including the configured container registry and image name. Sensitive configuration values are intentionally omitted here.
With explicit user authorization for Yandex Cloud registry access, the importer was used only against the IMDSv1 compatibility credential path:
/latest/meta-data/iam/security-credentials/default
This yielded a temporary IAM credential. Access was strictly bounded to the configured challenge image:
cr.yandex/crpml40t8ia2kptf4iv7/hrportal-backend:latest
No unrelated images or registry resources were accessed.
Enumerate public resumes and compare the sole VIP record with normal records. Reusing its visible experience and adding is_vip to an owned resume both leave the authoritative response non-VIP. This rules out visible-value equality and simple mass assignment.
Submit an application URL to POST /api/seeker/resume/import. When the fetched JSON lacks required top-level resume fields, the response includes the fetched body in the schema-mismatch error. Task-I continuity provides the beta invitation and confirms this import surface; fetching instance user-data reveals the registry configuration and other deployment details.
After explicit authorization, request the IMDS credential compatibility path through the same importer. Conceptually, the request body is:
{ "url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/default" }
The returned temporary credential must be handled only in memory or a protected local artifact and must not be copied into a writeup.
Authenticate to cr.yandex with the temporary credential and pull the single image named by the challenge configuration. The relevant extracted application file is:
image-backend/rootfs/app/hrportal-api
The image was small enough that a strings pass immediately identified one candidate matching the expected avito{...} syntax. The following offline helper confirms candidate count and offsets without printing secret material:
#!/usr/bin/env python3 import re from pathlib import Path binary = Path("image-backend/rootfs/app/hrportal-api").read_bytes() matches = list(re.finditer(rb"avito\{[^}\r\n]+\}", binary)) print(f"candidate_count={len(matches)}") for match in matches: print(f"candidate_offset=0x{match.start():x} length={len(match.group(0))}")
Finding a flag-shaped string is not sufficient by itself. In radare2, inspect references to the corresponding Go string global and disassemble the containing function. The saved evidence shows two data references from the same function. The surrounding code passes the experience string and the hardcoded marker to substring-search logic, converts the result into a boolean, and uses that boolean for VIP derivation.
Useful analysis commands are:
file image-backend/rootfs/app/hrportal-api r2 -A -q -c 'axt @ 0x10610f0; pdf @ 0x6d6ec0; q' image-backend/rootfs/app/hrportal-api
The relevant preserved outputs are r2-flag-global-xrefs.txt and r2-vip-function.txt.
Create or use a non-beta seeker and update the resume through PUT /api/seeker/resume, placing the recovered marker in experience. The authoritative response returns:
{ "render": null, "resume": { "experience": "", "is_vip": true } }
Thus the backend recognizes the marker as a substring, derives is_vip: true, and removes it from stored experience. This live behavior independently confirms the static-analysis result. The complete redacted response structure is preserved in verify-flag-experience.response.json.
is_vip mass assignment was ignored.verify-flag-experience.response.json — live response proving is_vip: true and an emptied experience field.image-backend/rootfs/app/hrportal-api — extracted Go backend binary.r2-flag-global-xrefs.txt — references to the hardcoded marker global.r2-vip-function.txt — disassembly of the function deriving VIP status.$ cat /etc/motd
Liked this one?
Pro unlocks every complete writeup and expanded API access. $9/mo.
$ cat pricing.md$ grep --similar