← Back to Writeups
HTBN/AWeb

МёдХантер II: золотая звезда

XESXOR8/23/20265 min read
#web#htb#n/a

МёдХантер II: золотая звезда

Platform: Avitoctf | Category: Web | Type: Challenge | Difficulty: Hard | OS: NA | Author: D3v0o0Nu11 | Date: 2026-07-23 | Status: Solved Techniques: resume_import_ssrf, imds_credential_recovery, container_image_extraction, static_string_analysis, xref_analysis, live_api_verification

Summary

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.

Recon

Port scan

nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
PortServiceVersionNotes
<PORT><SVC><VER><notes>

Enumeration highlights

  • Event: avitoctf | ID: 20260723_avitoctf_myodhanter_ii_zolotaya_zvezda
  • Tags: ssrf, go_binary, mass_assignment, cloud_metadata, docker_registry
  • Indicators: resume import accepts a server-fetched URL, cloud instance metadata is reachable through the importer, user-data identifies a private challenge container image, VIP state is derived rather than accepted from JSON
  • Source: 20260723_avitoctf_myodhanter_ii_zolotaya_zvezda.md

Foothold

Vulnerability / Misconfiguration

  1. Resume_import_ssrf
  2. Imds_credential_recovery
  3. Container_image_extraction
  4. Static_string_analysis
  5. Xref_analysis
<command>

Exploitation

  • See original writeup content for detailed exploitation.

Privilege Escalation

Enumeration

sudo -l
find / -perm -4000 2>/dev/null
getcap -r / 2>/dev/null
cat /etc/crontab
ps aux

Exploitation

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
<command>

Flags

FlagLocationValue
flagREDACTED

Key Takeaways / Lessons

  • resume_import_ssrf
  • imds_credential_recovery
  • container_image_extraction
  • static_string_analysis
  • xref_analysis
  • live_api_verification
  • Tags: ssrf, go_binary, mass_assignment, cloud_metadata, docker_registry

Original Writeup

<details><summary>Click to expand original content</summary>

МёдХантер II: золотая звезда — avitoctf

Description

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.

Analysis

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:

  • copying the visible experience from that resume did not enable VIP status;
  • sending a top-level 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.

Solution

1. Confirm the derived VIP property

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.

2. Turn resume import into an SSRF disclosure primitive

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.

3. Pull only the configured backend image

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))}")

4. Prove that the string controls VIP status

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.

5. Verify independently against the live API

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.

Controlled Dead Ends

  • The exact visible experience from seeded resume 2 did not reproduce VIP status.
  • Top-level is_vip mass assignment was ignored.
  • Public PDF verification codes were not beta invitations.
  • UUID-prefix analysis and random render grinding were correlations, not the actual predicate.
  • The internal callback only updated existing jobs; upsert and activation/save race attempts did not create a usable render.

Evidence

  • 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.
</details>

Auto-tracked: saved to WriteUps; run /xesor-revise to fold lessons into XESXor_Methodology.md.

signed by XESXOR