diff --git a/README.md b/README.md index 2364429..ee76514 100644 --- a/README.md +++ b/README.md @@ -80,9 +80,10 @@ Although some of the challenges may run as is, it is recommended that you have d | Name | Author | | ---- | ------ | -| [Arcane Nebula](./web/arcane-nebula) | koks | +| [Arcane Runes](./web/arcane-runes) | koks | | [Cross Checked Report](./web/cross-checked-report) | YetAnotherAlt123 | | [Microbuns](./web/microbuns) | koks | +| [Portal](./web/portal) | YetAnotherAlt123 | | [ShodanQL](./web/shodanql) | sAINT_barber | | [Underground Watch - Part 1](./web/underground_watch_part_1) | sAINT_barber | | [Warriors Tech Shop](./web/warriors_tech_shop) | sAINT_barber | diff --git a/reverse/drone-army/challenge.yml b/reverse/drone-army/challenge.yml new file mode 100644 index 0000000..d5ae718 --- /dev/null +++ b/reverse/drone-army/challenge.yml @@ -0,0 +1,25 @@ +name: "Drone Army" +author: "rok0s" +category: reverse + +description: | + OrionTech's drones are controlled by a highly encrypted program that autonomously manages surveillance operations. Your goal is to reverse engineer this program to uncover its internal mechanisms and secrets, allowing you to hijack control of the drone fleet. + +value: 500 +type: dynamic +extra: + initial: 500 + minimum: 100 + decay: 25 + +flags: + - reverse/arm_bkp/poc.py + +tags: + - reverse + +files: + - "public/drone_army.s" + +state: visible +version: "0.1" diff --git a/reverse/drone-army/public/drone_army.s b/reverse/drone-army/public/drone_army.s new file mode 100644 index 0000000..69bc667 --- /dev/null +++ b/reverse/drone-army/public/drone_army.s @@ -0,0 +1,65 @@ +.section .data +aa: .space 32 +ac: .byte 0x26, 0x65, 0x36, 0x75, 0xe, 0x3a, 0x48, 0x5, 0x7c, 0x23, 0x13, 0x75, 0x2a, 0x72, 0x42, 0x30, 0x43, 0x1c, 0x4e, 0x7d, 0xb, 0x38, 0x4a, 0x7f, 0x1a, 0x5e, 0x7f, 0x5e, 0x23 + +sm: .asciz "Success!\n" +fm: .asciz "Try again...\n" + +.section .text +.global _start + +_start: + mov x0, 0 + ldr x1, =aa + mov x2, 32 + mov x8, 63 + svc 0 + mov x3, x0 + sub x3, x3, #1 + mov x4, #29 + cmp x3, x4 + bne dd + ldr x1, =aa + mov x4, 0x65 + mov x5, 0 +al: + cmp x5, x3 + bge bd + ldrb w6, [x1, x5] + eor w6, w6, w4 + strb w6, [x1, x5] + mov w4, w6 + add x5, x5, 1 + b al +bd: + ldr x6, =ac + mov x7, 0 + mov x8, 1 +cbl: + cmp x7, x3 + bge bb + ldrb w9, [x1, x7] + ldrb w10, [x6, x7] + cmp w9, w10 + bne cbf + add x7, x7, 1 + b cbl +cbf: + mov x8, 0 + b bb +bb: + cmp x8, 1 + bne dd + ldr x1, =sm + mov x2, #10 + b de +dd: + ldr x1, =fm + mov x2, #13 +de: + mov x0, 1 + mov x8, 64 + svc 0 + mov x0, 0 + mov x8, 93 + svc 0 diff --git a/reverse/drone-army/setup/drone_army.s b/reverse/drone-army/setup/drone_army.s new file mode 100644 index 0000000..69bc667 --- /dev/null +++ b/reverse/drone-army/setup/drone_army.s @@ -0,0 +1,65 @@ +.section .data +aa: .space 32 +ac: .byte 0x26, 0x65, 0x36, 0x75, 0xe, 0x3a, 0x48, 0x5, 0x7c, 0x23, 0x13, 0x75, 0x2a, 0x72, 0x42, 0x30, 0x43, 0x1c, 0x4e, 0x7d, 0xb, 0x38, 0x4a, 0x7f, 0x1a, 0x5e, 0x7f, 0x5e, 0x23 + +sm: .asciz "Success!\n" +fm: .asciz "Try again...\n" + +.section .text +.global _start + +_start: + mov x0, 0 + ldr x1, =aa + mov x2, 32 + mov x8, 63 + svc 0 + mov x3, x0 + sub x3, x3, #1 + mov x4, #29 + cmp x3, x4 + bne dd + ldr x1, =aa + mov x4, 0x65 + mov x5, 0 +al: + cmp x5, x3 + bge bd + ldrb w6, [x1, x5] + eor w6, w6, w4 + strb w6, [x1, x5] + mov w4, w6 + add x5, x5, 1 + b al +bd: + ldr x6, =ac + mov x7, 0 + mov x8, 1 +cbl: + cmp x7, x3 + bge bb + ldrb w9, [x1, x7] + ldrb w10, [x6, x7] + cmp w9, w10 + bne cbf + add x7, x7, 1 + b cbl +cbf: + mov x8, 0 + b bb +bb: + cmp x8, 1 + bne dd + ldr x1, =sm + mov x2, #10 + b de +dd: + ldr x1, =fm + mov x2, #13 +de: + mov x0, 1 + mov x8, 64 + svc 0 + mov x0, 0 + mov x8, 93 + svc 0 diff --git a/web/arcane-nebula/public/.gitkeep b/reverse/drone-army/solution/.gitkeep similarity index 100% rename from web/arcane-nebula/public/.gitkeep rename to reverse/drone-army/solution/.gitkeep diff --git a/web/arcane-nebula/README.md b/web/arcane-runes/README.md similarity index 70% rename from web/arcane-nebula/README.md rename to web/arcane-runes/README.md index 1af6cc3..3c4377b 100644 --- a/web/arcane-nebula/README.md +++ b/web/arcane-runes/README.md @@ -1,6 +1,6 @@ -# Arcane Nebula +# Arcane Runes -[![Try in PWD](https://raw.githubusercontent.com/play-with-docker/stacks/master/assets/images/button.png)](https://labs.play-with-docker.com/?stack=https://raw.githubusercontent.com/cybermouflons/CCSC-CTF-2023/master/web/arcane-nebula/docker-compose.yml) +[![Try in PWD](https://raw.githubusercontent.com/play-with-docker/stacks/master/assets/images/button.png)](https://labs.play-with-docker.com/?stack=https://raw.githubusercontent.com/cybermouflons/CCSC-CTF-2023/master/web/arcane-runes/docker-compose.yml) **Category**: web @@ -19,10 +19,10 @@ Nevertheless, we have to persevere; retrieve the flag! Launch challenge: ``` -curl -sSL https://raw.githubusercontent.com/cybermouflons/CCSC-CTF-2023/master/web/arcane-nebula/docker-compose.yml | docker compose -f - up -d +curl -sSL https://raw.githubusercontent.com/cybermouflons/CCSC-CTF-2023/master/web/arcane-runes/docker-compose.yml | docker compose -f - up -d ``` Shutdown challenge: ``` -curl -sSL https://raw.githubusercontent.com/cybermouflons/CCSC-CTF-2023/master/web/arcane-nebula/docker-compose.yml | docker compose -f - down +curl -sSL https://raw.githubusercontent.com/cybermouflons/CCSC-CTF-2023/master/web/arcane-runes/docker-compose.yml | docker compose -f - down ``` diff --git a/web/arcane-nebula/challenge.yml b/web/arcane-runes/challenge.yml similarity index 95% rename from web/arcane-nebula/challenge.yml rename to web/arcane-runes/challenge.yml index 377065c..23ccd72 100644 --- a/web/arcane-nebula/challenge.yml +++ b/web/arcane-runes/challenge.yml @@ -1,4 +1,4 @@ -name: "Arcane Nebula" +name: "Arcane Runes" author: "koks" category: web diff --git a/web/arcane-nebula/docker-compose.yml b/web/arcane-runes/docker-compose.yml similarity index 53% rename from web/arcane-nebula/docker-compose.yml rename to web/arcane-runes/docker-compose.yml index 25cfb7a..5706b09 100644 --- a/web/arcane-nebula/docker-compose.yml +++ b/web/arcane-runes/docker-compose.yml @@ -1,7 +1,7 @@ version: '3' services: - arcane-nebula: - image: ghcr.io/cybermouflons/ccsc2024/arcane-nebula:latest + arcane-runes: + image: ghcr.io/cybermouflons/ccsc2024/arcane-runes:latest restart: always build: ./setup/ ports: diff --git a/web/arcane-runes/public/.gitkeep b/web/arcane-runes/public/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/web/arcane-nebula/setup/.dockerignore b/web/arcane-runes/setup/.dockerignore similarity index 100% rename from web/arcane-nebula/setup/.dockerignore rename to web/arcane-runes/setup/.dockerignore diff --git a/web/arcane-nebula/setup/Dockerfile b/web/arcane-runes/setup/Dockerfile similarity index 100% rename from web/arcane-nebula/setup/Dockerfile rename to web/arcane-runes/setup/Dockerfile diff --git a/web/arcane-nebula/setup/app/.gitignore b/web/arcane-runes/setup/app/.gitignore similarity index 100% rename from web/arcane-nebula/setup/app/.gitignore rename to web/arcane-runes/setup/app/.gitignore diff --git a/web/arcane-nebula/setup/app/assets/magick.css b/web/arcane-runes/setup/app/assets/magick.css similarity index 100% rename from web/arcane-nebula/setup/app/assets/magick.css rename to web/arcane-runes/setup/app/assets/magick.css diff --git a/web/arcane-nebula/setup/app/assets/normalize.css b/web/arcane-runes/setup/app/assets/normalize.css similarity index 100% rename from web/arcane-nebula/setup/app/assets/normalize.css rename to web/arcane-runes/setup/app/assets/normalize.css diff --git a/web/arcane-nebula/setup/app/bun.lockb b/web/arcane-runes/setup/app/bun.lockb similarity index 100% rename from web/arcane-nebula/setup/app/bun.lockb rename to web/arcane-runes/setup/app/bun.lockb diff --git a/web/arcane-nebula/setup/app/index.ts b/web/arcane-runes/setup/app/index.ts similarity index 100% rename from web/arcane-nebula/setup/app/index.ts rename to web/arcane-runes/setup/app/index.ts diff --git a/web/arcane-nebula/setup/app/package.json b/web/arcane-runes/setup/app/package.json similarity index 89% rename from web/arcane-nebula/setup/app/package.json rename to web/arcane-runes/setup/app/package.json index 2b9ce39..c5a33e0 100644 --- a/web/arcane-nebula/setup/app/package.json +++ b/web/arcane-runes/setup/app/package.json @@ -1,5 +1,5 @@ { - "name": "arcane-nebula", + "name": "arcane-runes", "module": "index.ts", "type": "module", "devDependencies": { diff --git a/web/arcane-nebula/setup/app/tsconfig.json b/web/arcane-runes/setup/app/tsconfig.json similarity index 100% rename from web/arcane-nebula/setup/app/tsconfig.json rename to web/arcane-runes/setup/app/tsconfig.json diff --git a/web/arcane-nebula/setup/app/views/denied.html b/web/arcane-runes/setup/app/views/denied.html similarity index 100% rename from web/arcane-nebula/setup/app/views/denied.html rename to web/arcane-runes/setup/app/views/denied.html diff --git a/web/arcane-nebula/setup/app/views/index.html b/web/arcane-runes/setup/app/views/index.html similarity index 100% rename from web/arcane-nebula/setup/app/views/index.html rename to web/arcane-runes/setup/app/views/index.html diff --git a/web/arcane-nebula/setup/app/views/spell.html b/web/arcane-runes/setup/app/views/spell.html similarity index 100% rename from web/arcane-nebula/setup/app/views/spell.html rename to web/arcane-runes/setup/app/views/spell.html diff --git a/web/arcane-nebula/solution/README.md b/web/arcane-runes/solution/README.md similarity index 99% rename from web/arcane-nebula/solution/README.md rename to web/arcane-runes/solution/README.md index c371e10..e38561f 100644 --- a/web/arcane-nebula/solution/README.md +++ b/web/arcane-runes/solution/README.md @@ -1,4 +1,4 @@ -# Arcane Nebula +# Arcane Runes ## Solution diff --git a/web/portal/README.md b/web/portal/README.md new file mode 100644 index 0000000..e55c02f --- /dev/null +++ b/web/portal/README.md @@ -0,0 +1,26 @@ +# Portal + +[![Try in PWD](https://raw.githubusercontent.com/play-with-docker/stacks/master/assets/images/button.png)](https://labs.play-with-docker.com/?stack=https://raw.githubusercontent.com/cybermouflons/CCSC-CTF-2023/master/web/portal/docker-compose.yml) + + +**Category**: web + +**Author**: YetAnotherAlt123 + +## Description + +We found an admin portal. Do we really need to say more? + + + +## Run locally + +Launch challenge: +``` +curl -sSL https://raw.githubusercontent.com/cybermouflons/CCSC-CTF-2023/master/web/portal/docker-compose.yml | docker compose -f - up -d +``` + +Shutdown challenge: +``` +curl -sSL https://raw.githubusercontent.com/cybermouflons/CCSC-CTF-2023/master/web/portal/docker-compose.yml | docker compose -f - down +``` diff --git a/web/portal/challenge.yml b/web/portal/challenge.yml new file mode 100644 index 0000000..20e43c7 --- /dev/null +++ b/web/portal/challenge.yml @@ -0,0 +1,25 @@ +name: "Portal" +author: "YetAnotherAlt123" +category: web + +description: | + We found an admin portal. Do we really need to say more? + +value: 500 +type: dynamic_docker +extra: + initial: 500 + minimum: 100 + decay: 25 + redirect_type: http + compose_stack: !filecontents docker-compose.yml + +flags: + - CCSC{s1d3_t0_s1d3_4ND_fr0n7_tO_b4ck} + +tags: + - web + - hard + +state: visible +version: "0.1" diff --git a/web/portal/docker-compose.yml b/web/portal/docker-compose.yml new file mode 100644 index 0000000..0468781 --- /dev/null +++ b/web/portal/docker-compose.yml @@ -0,0 +1,11 @@ +version: "3.7" + +services: + challenge: + image: ghcr.io/cybermouflons/ccsc2024/portal:latest + restart: always + ports: + - 3000:3000 + build: + context: ./setup + dockerfile: Dockerfile diff --git a/web/portal/public/.gitkeep b/web/portal/public/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/web/portal/setup/Dockerfile b/web/portal/setup/Dockerfile new file mode 100644 index 0000000..c633c67 --- /dev/null +++ b/web/portal/setup/Dockerfile @@ -0,0 +1,21 @@ +# Start with the official Python image +FROM python:3.9-slim + +ENV FLAG=CCSC{s1d3_t0_s1d3_4ND_fr0n7_tO_b4ck} + +# Install Python dependencies +WORKDIR /app +COPY requirements.txt /app/ +RUN pip install --no-cache-dir -r requirements.txt + +# Copy the app +COPY flag.txt /app +COPY app.py /app +COPY app-dev-version-abccdef.py /app + +RUN echo '#app:x:999:999::/app:/bin/false' >> /etc/passwd + +EXPOSE 3000 + +# Command to start services +CMD python3 app-dev-version-abccdef.py & python3 app.py diff --git a/web/portal/setup/app-dev-version-abccdef.py b/web/portal/setup/app-dev-version-abccdef.py new file mode 100644 index 0000000..e0ea9b0 --- /dev/null +++ b/web/portal/setup/app-dev-version-abccdef.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +from typing import Optional + +from time import sleep + +import os +import httpx +from fastapi import Request +from fastapi.responses import RedirectResponse +from starlette.middleware.base import BaseHTTPMiddleware + +from nicegui import Client, app, ui + +passwords = {'admin': 'adminpass123'} + +unrestricted_page_routes = {'/login', '/flag'} + + +class AuthMiddleware(BaseHTTPMiddleware): + + async def dispatch(self, request: Request, call_next): + if not app.storage.user.get('authenticated', False): + if request.url.path in Client.page_routes.values() and request.url.path not in unrestricted_page_routes: + app.storage.user['referrer_path'] = request.url.path # remember where the user wanted to go + return RedirectResponse('/login') + return await call_next(request) + + +class PageMiddleware(BaseHTTPMiddleware): + + async def dispatch(self, request: Request, call_next): + if request.url.path.lower() in Client.page_routes.values() and request.url.path.lower() != request.url.path: + app.storage.user['referrer_path'] = request.url.path + return RedirectResponse(request.url.path.lower()) + return await call_next(request) + + +app.add_middleware(PageMiddleware) +app.add_middleware(AuthMiddleware) + + +@ui.page('/') +def main_page() -> None: + with ui.column().classes('absolute-center items-center'): + ui.label(f'Hello {app.storage.user["username"]}!').classes('text-2xl') + ui.button(on_click=lambda: (app.storage.user.clear(), ui.navigate.to('/login')), icon='logout') \ + .props('outline round') + ui.label("We have preserved a map of the Area of Operations for you.") + m = ui.leaflet(center=(35.18, 33.38)) + m.set_zoom(7) + ui.label().bind_text_from(m, 'center', lambda center: f'Center: {center[0]:.1f}, {center[1]:.1f}') + ui.label().bind_text_from(m, 'zoom', lambda zoom: f'Zoom: {zoom}') + + +@ui.page('/super_secret_status_page') +async def status_page() -> None: + content_label = ui.label('Loading...') + try: + async with httpx.AsyncClient() as client: + r = await client.get("http://localhost:8888/flag", follow_redirects=True) + content_label.set_text("Status for " + app.storage.user.get('referrer_path') + " : " + str(r)) + except Exception as e: + content_label.set_text("Failed getting status for " + app.storage.user.get('referrer_path') + " : " + str(e)) + + +@ui.page('/flag') +async def flag_page() -> None: + ui.label(f"Flag: {os.getenv('FLAG')}") + + +@ui.page('/login') +def login() -> Optional[RedirectResponse]: + def try_login() -> None: # local function to avoid passing username and password as arguments + pass_val = passwords.get(username.value.lower()) + authenticated = True + for i in range(len(pass_val)): + try: + if pass_val[i] == password.value[i]: + print("Let it be known, that a character is matched!") + sleep(0.2) # Let the feeling sink in + else: + authenticated = False + ui.notify('Wrong username or password', color='negative') + except Exception: + authenticated = False + ui.notify('Wrong username or password', color='negative') + if authenticated: + app.storage.user.update({'username': username.value, 'authenticated': True}) + ui.navigate.to(app.storage.user.get('referrer_path', '/')) # go back to where the user wanted to go + + """ + Bad code. + + if passwords.get(username.value.lower()) == password.value: + app.storage.user.update({'username': username.value, 'authenticated': True}) + ui.navigate.to(app.storage.user.get('referrer_path', '/')) # go back to where the user wanted to go + else: + ui.notify('Wrong username or password', color='negative') + """ + + if app.storage.user.get('authenticated', False): + return RedirectResponse('/') + with ui.card().classes('absolute-center'): + ui.label("Please enter your username and password, admin:") + username = ui.input('Username').on('keydown.enter', try_login) + password = ui.input('Password', password=True, password_toggle_button=True).on('keydown.enter', try_login) + ui.button('Admin log in', on_click=try_login) + + return None + + +ui.run(storage_secret='VEERYSEEEECRET', title="Nice AI GUI", host="127.0.0.1", port=8888) diff --git a/web/portal/setup/app.py b/web/portal/setup/app.py new file mode 100644 index 0000000..fb7891c --- /dev/null +++ b/web/portal/setup/app.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +from typing import Optional + +from time import sleep + +import httpx +from fastapi import Request +from fastapi.responses import RedirectResponse +from starlette.middleware.base import BaseHTTPMiddleware + +from nicegui import Client, app, ui + +passwords = {'admin': 'adminpass123'} + +unrestricted_page_routes = {'/login'} # Restrict /flag for prod + + +class AuthMiddleware(BaseHTTPMiddleware): + + async def dispatch(self, request: Request, call_next): + if not app.storage.user.get('authenticated', False): + if request.url.path in Client.page_routes.values() and request.url.path not in unrestricted_page_routes: + app.storage.user['referrer_path'] = request.url.path # remember where the user wanted to go + return RedirectResponse('/login') + return await call_next(request) + + +class PageMiddleware(BaseHTTPMiddleware): + + async def dispatch(self, request: Request, call_next): + if request.url.path.lower() in Client.page_routes.values() and request.url.path.lower() != request.url.path: + app.storage.user['referrer_path'] = request.url.path + return RedirectResponse(request.url.path.lower()) + return await call_next(request) + + +app.add_middleware(PageMiddleware) +app.add_middleware(AuthMiddleware) + + +@ui.page('/') +def main_page() -> None: + app.storage.user['referrer_path'] = "/" + with ui.column().classes('absolute-center items-center'): + ui.label(f'Hello {app.storage.user["username"]}!').classes('text-2xl') + ui.button(on_click=lambda: (app.storage.user.clear(), ui.navigate.to('/login')), icon='logout') \ + .props('outline round') + ui.label("We have preserved a map of the Area of Operations for you.") + m = ui.leaflet(center=(35.18, 33.38)) + m.set_zoom(7) + ui.label().bind_text_from(m, 'center', lambda center: f'Center: {center[0]:.1f}, {center[1]:.1f}') + ui.label().bind_text_from(m, 'zoom', lambda zoom: f'Zoom: {zoom}') + + +@ui.page('/super_secret_status_page') +async def status_page() -> None: + content_label = ui.label('Loading...') + try: + path = app.storage.user.get('referrer_path', "/") + if path.endswith('/flag'): + path = "/" + async with httpx.AsyncClient() as client: + r = await client.get("http://localhost:8888" + path, follow_redirects=True) # TODO: point this to prod... At least it works hahaha + content_label.set_text("Status for " + path + " : " + str(r.status_code) + " " + str(r.content)) + except Exception as e: + content_label.set_text("Failed getting status for " + app.storage.user.get('referrer_path') + " : " + str(e)) + + +@ui.page('/flag') +async def flag_page() -> None: + app.storage.user['referrer_path'] = "/" + ui.label("Port from dev once secure.") # ui.label(f"Flag: {os.getenv('FLAG')}") + + +@ui.page('/login') +def login() -> Optional[RedirectResponse]: + def try_login() -> None: # local function to avoid passing username and password as arguments + pass_val = passwords.get(username.value.lower()) + authenticated = True + for i in range(len(pass_val)): + try: + if pass_val[i] == password.value[i]: + print("Let it be known, that a character is matched!") + sleep(0.2) # Let the feeling sink in + else: + authenticated = False + ui.notify('Wrong username or password', color='negative') + except Exception: + authenticated = False + ui.notify('Wrong username or password', color='negative') + if authenticated: + app.storage.user.update({'username': username.value, 'authenticated': True}) + ui.navigate.to(app.storage.user.get('referrer_path', '/')) # go back to where the user wanted to go + + """ + Bad code. + + if passwords.get(username.value.lower()) == password.value: + app.storage.user.update({'username': username.value, 'authenticated': True}) + ui.navigate.to(app.storage.user.get('referrer_path', '/')) # go back to where the user wanted to go + else: + ui.notify('Wrong username or password', color='negative') + """ + + if app.storage.user.get('authenticated', False): + return RedirectResponse('/') + with ui.card().classes('absolute-center'): + ui.label("Please enter your username and password, admin:") + username = ui.input('Username').on('keydown.enter', try_login) + password = ui.input('Password', password=True, password_toggle_button=True).on('keydown.enter', try_login) + ui.button('Admin log in', on_click=try_login) + + return None + + +ui.run(storage_secret='VEERYSEEEECRET', title="Nice AI GUI", port=3000) diff --git a/web/portal/setup/flag.txt b/web/portal/setup/flag.txt new file mode 100644 index 0000000..f6a714a --- /dev/null +++ b/web/portal/setup/flag.txt @@ -0,0 +1 @@ +Nope. \ No newline at end of file diff --git a/web/portal/setup/requirements.txt b/web/portal/setup/requirements.txt new file mode 100644 index 0000000..2fed26d --- /dev/null +++ b/web/portal/setup/requirements.txt @@ -0,0 +1,2 @@ +nicegui==1.4.20 +httpx \ No newline at end of file diff --git a/web/portal/solution/README.md b/web/portal/solution/README.md new file mode 100644 index 0000000..3159501 --- /dev/null +++ b/web/portal/solution/README.md @@ -0,0 +1,6 @@ +The admin portal is vulnerable to a side channel attack; there is a small delay after every character in the password is checked. + +Then, the player is able to exploit CVE-2024-32005 (https://cvefeed.io/vuln/detail/CVE-2024-32005#!) to read local files. The player is expected to find and read app.py, and figure out their next steps. + +In order to perform the SSRF to obtain the flag, the user needs to make a request to /FLAG, intercept and drop that request, navigate to /login, login and intercept the redirect, drop that request again and manually browse to /super_secret_status_page to perform the CSRF and get the flag. +