$ cat writeup.md…
$ cat writeup.md…
alfactf
Task: Flask credit scoring app with ML-based loan approval; ULTRA-LOW-RISK product reveals flag but requires ML model approval. Solution: path traversal in document download to extract sklearn model, reverse engineer threshold and special categorical values, craft application that passes ML scoring.
$ cat /etc/rate-limit
Rate limit reached (20 reads/hour per IP). Showing preview only — full content returns at the next hour roll-over.
A credit scoring web application "Karabin Capital" where users can submit loan applications. The goal is to get an approved "ULTRA-LOW-RISK" loan which reveals the flag.
The application is a Flask web service with PostgreSQL backend. Users register, submit loan applications with personal and financial data, and receive approval decisions. Applications with amount <= 10000 are auto-approved as MICRO-CASH. Larger amounts go through an ML scoring model (sklearn RandomForestClassifier). The ULTRA-LOW-RISK product requires amount >= 500000 AND ML model approval (probability >= threshold), and reveals the flag upon approval.
amount <= 10000: auto-approved as MICRO-CASHamount > 10000: requires ML model scoringThe vulnerability is in the download_document function (app.py lines 820-864):
@app.get("/api/applications/<application_id>/document") def download_document(application_id: str) -> Response: ... base_dir = STORAGE_ROOT / user["public_uuid"] / user["login"] unsafe_path = base_dir / row["name"] # row["name"] = application_name from DB safe_path = safe_document_path(user, row["name"]) ... target_path = unsafe_path if unsafe_path.exists() else safe_path file_bytes = target_path.read_bytes() # Reads arbitrary file!
The application_name field from the database is used directly in file path construction without sanitization. When creating an application, the user controls the application_name field, which gets stored in the database and later used to construct the file path for document download.
/srv/karabin/data/storage/{uuid}/{login}//srv/karabin/secrets/karabin-ratebook (the ML model)../../../../secrets/karabin-ratebookimport requests BASE_URL = 'https://capital-xxx.alfactf.ru' session = requests.Session() # Register a new user session.post(f'{BASE_URL}/api/register', json={ 'login': 'attacker01', 'password': 'password123' }) ...
$ grep --similar