diff --git a/app/ctf/miniapi/packet1/route.ts b/app/ctf/miniapi/packet1/route.ts new file mode 100644 index 00000000..72f7821c --- /dev/null +++ b/app/ctf/miniapi/packet1/route.ts @@ -0,0 +1,14 @@ +import { NextResponse } from "next/server"; + +export async function GET() { + return NextResponse.json({ + fragment: "8720414d7ad2bdb65cd9", + param: "signal", + meta: { + processedAt: 1700000300, + serverId: "node-7f3a", + region: "us-east-2", + latency: 42 + } + }); +} diff --git a/app/ctf/miniapi/packet2/route.ts b/app/ctf/miniapi/packet2/route.ts new file mode 100644 index 00000000..76574869 --- /dev/null +++ b/app/ctf/miniapi/packet2/route.ts @@ -0,0 +1,14 @@ +import { NextResponse } from "next/server"; + +export async function GET() { + return NextResponse.json({ + fragment: "5b0125706439da", + param: "signal", + meta: { + processedAt: 1700000100, + serverId: "node-2b9c", + region: "us-west-1", + latency: 38 + } + }); +} diff --git a/app/ctf/miniapi/packet3/route.ts b/app/ctf/miniapi/packet3/route.ts new file mode 100644 index 00000000..a5430530 --- /dev/null +++ b/app/ctf/miniapi/packet3/route.ts @@ -0,0 +1,14 @@ +import { NextResponse } from "next/server"; + +export async function GET() { + return NextResponse.json({ + fragment: "ee3111b1dd03a7", + param: "signal", + meta: { + processedAt: 1700000400, + serverId: "node-1a4d", + region: "eu-central-1", + latency: 91 + } + }); +} diff --git a/app/ctf/miniapi/packet4/route.ts b/app/ctf/miniapi/packet4/route.ts new file mode 100644 index 00000000..8efde622 --- /dev/null +++ b/app/ctf/miniapi/packet4/route.ts @@ -0,0 +1,14 @@ +import { NextResponse } from "next/server"; + +export async function GET() { + return NextResponse.json({ + fragment: "1420c4108b01de39", + param: "signal", + meta: { + processedAt: 1700000200, + serverId: "node-9e1f", + region: "ap-southeast-1", + latency: 67 + } + }); +} diff --git a/app/ctf/miniapi/route.ts b/app/ctf/miniapi/route.ts new file mode 100644 index 00000000..70dd6ae3 --- /dev/null +++ b/app/ctf/miniapi/route.ts @@ -0,0 +1,39 @@ +import { NextResponse } from "next/server"; +import { derive } from "../utils"; + +async function sha256(text: string) { + const encoder = new TextEncoder(); + const data = encoder.encode(text); + + const hashBuffer = await crypto.subtle.digest("SHA-256", data); + + return Array.from(new Uint8Array(hashBuffer)) + .map(b => b.toString(16).padStart(2, "0")) + .join(""); +} + +const SERVER_SECRET = derive(["LWtleQ==", "ZWNyZXQ=", "dG9wLXM="]); + +export async function GET() { + const flag = derive([ + "MX0=", + "MW4xNHA=", + "YWc3LW0=", + "dGZ7Zmw=", + "aGFja2M=" + ]); + const hiddenFlag = derive([ + "NzNyfQ==", + "cDFtNDU=", + "YWc4LTQ=", + "dGZ7Zmw=", + "aGFja2M=" + ]); + + const secret = await sha256(hiddenFlag + SERVER_SECRET); + + return NextResponse.json({ + flag, + secret + }); +} diff --git a/app/ctf/miniapi/unlock/route.ts b/app/ctf/miniapi/unlock/route.ts new file mode 100644 index 00000000..0b1d5d34 --- /dev/null +++ b/app/ctf/miniapi/unlock/route.ts @@ -0,0 +1,63 @@ +import { NextResponse } from "next/server"; +import { derive } from "../../utils"; + +async function sha256(text: string) { + const encoder = new TextEncoder(); + const data = encoder.encode(text); + + const hashBuffer = await crypto.subtle.digest("SHA-256", data); + + return Array.from(new Uint8Array(hashBuffer)) + .map(b => b.toString(16).padStart(2, "0")) + .join(""); +} + +const SERVER_SECRET = derive(["LWtleQ==", "ZWNyZXQ=", "dG9wLXM="]); + +export async function GET(req: Request) { + const url = new URL(req.url); + const secret = url.searchParams.get("secret"); + const signal = url.searchParams.get("signal"); + + if (!secret && !signal) { + return NextResponse.json( + { error: "Missing query parameter" }, + { status: 400 } + ); + } + + if (secret && signal) { + return NextResponse.json( + { error: "Only one query parameter allowed" }, + { status: 400 } + ); + } + + let param = secret ? secret : signal; + let hiddenFlag = secret + ? derive(["NzNyfQ==", "cDFtNDU=", "YWc4LTQ=", "dGZ7Zmw=", "aGFja2M="]) + : derive([ + "fQ==", + "aDNjN2Y=", + "YjM0Nzc=", + "NzV5MHU=", + "MG5ncjQ=", + "YWc5LWM=", + "dGZ7Zmw=", + "aGFja2M=" + ]); + + const expected = await sha256(hiddenFlag + SERVER_SECRET); + console.log(expected); + + if (param !== expected) { + return NextResponse.json( + { error: "Parameter value is incorrect" }, + { status: 400 } + ); + } + + return NextResponse.json({ + decoded: hiddenFlag + }); +} diff --git a/app/ctf/page.module.scss b/app/ctf/page.module.scss new file mode 100644 index 00000000..7866dc1a --- /dev/null +++ b/app/ctf/page.module.scss @@ -0,0 +1,140 @@ +.ctfSection { + position: relative; + width: 100%; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + background: linear-gradient(to bottom, #020316, #16133e); + z-index: 1; +} + +.ctfBackgrounds { + position: absolute; + inset: 0; + pointer-events: none; + z-index: 0; + + -webkit-mask-image: linear-gradient( + to bottom, + rgba(0, 0, 0, 1) 90%, + rgba(0, 0, 0, 0) 100% + ); + mask-image: linear-gradient( + to bottom, + rgba(0, 0, 0, 1) 90%, + rgba(0, 0, 0, 0) 100% + ); +} + +@keyframes tinyStarsGlow { + 0% { + opacity: 0.2; + filter: drop-shadow(0 0 2px rgba(255, 255, 255, 0.2)); + } + 50% { + opacity: 0.4; + filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.8)); + } + 100% { + opacity: 0.2; + filter: drop-shadow(0 0 2px rgba(255, 255, 255, 0.2)); + } +} + +@media (max-width: 768px) { + .ctfSection { + min-height: 100vh; + } +} + +.flagInput { + transition: all 0.3s ease; + + &:hover { + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(163, 21, 214, 0.3); + } + + &:focus { + transform: translateY(-2px); + box-shadow: 0 8px 30px rgba(253, 171, 96, 0.4); + } + + &.correct { + border-color: #4caf50 !important; + background: rgba(76, 175, 80, 0.1) !important; + } + + &.incorrect { + border-color: #f44336 !important; + background: rgba(244, 67, 54, 0.1) !important; + } +} + +.progressBar { + background: rgba(255, 255, 255, 0.1); + border-radius: 10px; + overflow: hidden; + height: 12px; + + .progressFill { + height: 100%; + background: linear-gradient(90deg, #a315d6, #fdab60, #a315d6); + background-size: 200% 100%; + animation: shimmer 2s linear infinite; + transition: width 0.5s ease; + } +} + +@keyframes shimmer { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } +} + +.flagCard { + background: rgba(255, 255, 255, 0.08); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: 16px; + padding: 20px; + transition: all 0.3s ease; + + &:hover { + transform: translateY(-4px); + box-shadow: 0 12px 35px rgba(163, 21, 214, 0.25); + border-color: rgba(163, 21, 214, 0.4); + } +} + +.celebration { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + pointer-events: none; + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; + + & svg { + animation: confetti 3s ease-out forwards; + } +} + +@keyframes confetti { + 0% { + transform: scale(0) rotate(0deg); + opacity: 1; + } + 100% { + transform: scale(2) rotate(720deg); + opacity: 0; + } +} diff --git a/app/ctf/page.tsx b/app/ctf/page.tsx new file mode 100644 index 00000000..04db4a8c --- /dev/null +++ b/app/ctf/page.tsx @@ -0,0 +1,1106 @@ +"use client"; + +import ErrorSnackbar from "@/components/ErrorSnackbar/ErrorSnackbar"; +import Loading from "@/components/Loading/Loading"; +import { + Box, + Button, + Container, + Typography, + Accordion, + AccordionDetails, + AccordionSummary +} from "@mui/material"; +import { ExpandMore } from "@mui/icons-material"; +import { useEffect, useState } from "react"; +import { motion, Variants } from "framer-motion"; +import { GradientButton } from "@/components/GradientButton/GradientButton"; +import { derive } from "./utils"; + +const TwinklingStar = ({ + size, + top, + left, + delay, + duration +}: { + size: number; + top: string; + left: string; + delay: number; + duration: number; +}) => ( + +); + +const ShootingStar = ({ + delay, + duration +}: { + delay: number; + duration: number; +}) => ( + +); + +const FloatingDebris = ({ + src, + top, + left, + right, + size, + delay, + duration, + rotation +}: { + src: string; + top: string; + left?: string; + right?: string; + size: string; + delay: number; + duration: number; + rotation: number; +}) => ( + +); + +const containerVariants: Variants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { + delayChildren: 0.3, + staggerChildren: 0.15 + } + } +}; + +const itemVariants: Variants = { + hidden: { opacity: 0, y: 30 }, + visible: { + opacity: 1, + y: 0, + transition: { + duration: 0.8, + ease: [0.25, 0.46, 0.45, 0.94] + } + } +}; + +export default function CTF() { + const [loading, setLoading] = useState(true); + const [showErrorAlert, setShowErrorAlert] = useState(false); + const [errorMessage, setErrorMessage] = useState(""); + const [stars, setStars] = useState< + { + id: number; + size: number; + top: string; + left: string; + delay: number; + duration: number; + }[] + >([]); + + useEffect(() => { + const t = setTimeout(() => setLoading(false), 250); + + const generatedStars = Array.from({ length: 120 }).map((_, i) => ({ + id: i, + size: Math.random() * 4 + 1, + top: `${Math.random() * 100}%`, + left: `${Math.random() * 100}%`, + delay: Math.random() * 8, + duration: 2 + Math.random() * 3 + })); + setStars(generatedStars); + + (window as any).$SECRET$ = { + flag: (() => { + return derive([ + "Mzd9", + "MzUzY3I=", + "MG41MDE=", + "YWc0LWM=", + "dGZ7Zmw=", + "aGFja2M=" + ]); + })(), + reveal: () => { + console.log( + derive([ + "fQ==", + "cHIxbjc=", + "NDExNzA=", + "YWc1LWM=", + "dGZ7Zmw=", + "aGFja2M=" + ]) + ); + }, + password: (key: number) => { + if (key === (window as any).$MAGIC_NUMBER$.number) { + console.log( + btoa( + derive([ + "bTN9", + "M2MwZDM=", + "YWc2LWQ=", + "dGZ7Zmw=", + "aGFja2M=" + ]) + ) + ); + } else { + console.log( + "Authentication failed. What's the magic number?" + ); + } + } + }; + + (window as any).$MAGIC_NUMBER$ = { + number: 64 + }; + + (window as any).$HINT_1$ = { + hint: "I'm in Elements!" + }; + + (window as any).$HINT_2$ = { + hint: "I might be NOT-DISPLAYED until you check the CSS" + }; + + (window as any).$HINT_3$ = { + hint: "Check your comms, you received a message! I wonder where you can FIND-ME in the DOM" + }; + + (window as any).$HINT_4$ = { + hint: "There might be more than hints stored in the console.. Try entering \'window\'" + }; + + (window as any).$HINT_5$ = { + hint: "There's a lot to this secret. Hint: f is for function" + }; + + (window as any).$HINT_6$ = { + hint: "The password is encoded in magic!" + }; + + (window as any).$HINT_7$ = { + hint: "Psst.. I heard there’s a secret API. Maybe you can ping it from your comms" + }; + + (window as any).$HINT_8$ = { + hint: "A new endpoint! This one requires a secret..." + }; + + (window as any).$HINT_9$ = { + hint: "A signal has been scrambled across multiple transmissions. The truth is in the timing" + }; + + return () => clearTimeout(t); + }, []); + + const pingMiniApi = async () => { + try { + await fetch("/ctf/miniapi"); + } catch (e: any) { + setErrorMessage(e?.message || "Failed to ping MiniAPI."); + setShowErrorAlert(true); + } + }; + + const pingMiniApiAgain = async () => { + try { + await fetch("/ctf/miniapi/unlock"); + } catch (e: any) { + setErrorMessage(e?.message || "Failed to ping MiniAPI."); + setShowErrorAlert(true); + } + }; + + const decryptSignal = async () => { + try { + const steps = ["packet1", "packet2", "packet3", "packet4"]; + for (let i = steps.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [steps[i], steps[j]] = [steps[j], steps[i]]; + } + steps.map(step => { + fetch(`/ctf/miniapi/${step}`); + }); + } catch (e: any) { + setErrorMessage(e?.message || "Failed to decrypt."); + setShowErrorAlert(true); + } + }; + + if (loading) return ; + + return ( + + setShowErrorAlert(false)} + message={errorMessage} + anchorOrigin={{ vertical: "bottom", horizontal: "center" }} + /> + + {stars.map(star => ( + + ))} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CAPTURE THE FLAG + + + + + + + + + + + + Explore, discover, and uncover hidden secrets + embedded in this page. Use your browser's + developer tools and keen observation skills to find + all flags. Every flag you find brings you closer to + victory! + + + + + + + MISSION OBJECTIVE + + + Find all 9 hidden flags scattered throughout + this page. To get started, right-click anywhere + on the page and select Inspect. Keep an + eye on the Elements, Console, and{" "} + Network tabs. + + + + } + sx={{ + fontFamily: "Tsukimi Rounded", + fontWeight: 600, + color: "#FDAB60" + }} + > + Need hints? + + + + Luckily, your mission commander has left you + some clues. In your browser's Developer + Tools, navigate to the Console tab. + Type $HINT_1$ and press Enter to + reveal the hint. This works for all 9 hints! + + + + + + + + + COMMS + + + YOU HAVE 1 MESSAGE: + + + + {derive([ + "ZzN9", + "bTM1NTQ=", + "M2NyMzc=", + "YWczLTU=", + "dGZ7Zmw=", + "aGFja2M=" + ])} + + + + + + + + + + + + + + + + + + + + + + + + + + {`YOUR FIRST FLAG HERE: ${derive(["MWdoN30=", "MTQxbjU=", "YWcxLXA=", "dGZ7Zmw=", "aGFja2M="])}`} + + + + + ); +} diff --git a/app/ctf/submit/page.tsx b/app/ctf/submit/page.tsx new file mode 100644 index 00000000..7ac23244 --- /dev/null +++ b/app/ctf/submit/page.tsx @@ -0,0 +1,1413 @@ +"use client"; + +import { + Box, + Container, + Typography, + TextField, + Alert, + Button, + CircularProgress +} from "@mui/material"; +import { useState, useEffect, useRef, useCallback } from "react"; +import { motion, Variants } from "framer-motion"; +import Link from "next/link"; +import styles from "./submit.module.scss"; + +const API_URL = "https://adonix.hackillinois.org"; + +const FLAG_POINTS = [ + { range: "1-3", points: 2, color: "#4CAF50" }, + { range: "4-6", points: 6, color: "#2196F3" }, + { range: "7-8", points: 10, color: "#FF9800" }, + { range: "9", points: 16, color: "#E91E63" } +]; + +const getFlagPoints = (index: number): number => { + if (index < 3) return 2; + if (index < 6) return 6; + if (index < 8) return 10; + return 16; +}; + +const getFlagColor = (index: number): string => { + if (index < 3) return "#4CAF50"; + if (index < 6) return "#2196F3"; + if (index < 8) return "#FF9800"; + return "#E91E63"; +}; + +interface FlagStatusType { + correct: boolean | null; + claimed: boolean; + loading: boolean; +} + +const TwinklingStar = ({ + size, + top, + left, + delay, + duration +}: { + size: number; + top: string; + left: string; + delay: number; + duration: number; +}) => ( + +); + +const ShootingStar = ({ + delay, + duration +}: { + delay: number; + duration: number; +}) => ( + +); + +const FloatingPlanet = ({ + src, + top, + left, + right, + size, + delay, + duration +}: { + src: string; + top: string; + left?: string; + right?: string; + size: string | { xs?: string; sm?: string; md?: string; lg?: string }; + delay: number; + duration: number; +}) => ( + +); + +const containerVariants: Variants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { + delayChildren: 0.3, + staggerChildren: 0.1 + } + } +}; + +const itemVariants: Variants = { + hidden: { opacity: 0, y: 30 }, + visible: { + opacity: 1, + y: 0, + transition: { + duration: 0.6, + ease: [0.25, 0.46, 0.45, 0.94] + } + } +}; + +export default function CTFSubmit() { + const [flagInputs, setFlagInputs] = useState([ + "", + "", + "", + "", + "", + "", + "", + "", + "" + ]); + const [flagStatus, setFlagStatus] = useState( + Array(9) + .fill(null) + .map(() => ({ correct: null, claimed: false, loading: false })) + ); + const [showSuccess, setShowSuccess] = useState(false); + const [isAuthenticated, setIsAuthenticated] = useState( + null + ); + const debounceRefs = useRef<(NodeJS.Timeout | null)[]>(Array(9).fill(null)); + const [stars, setStars] = useState< + { + id: number; + size: number; + top: string; + left: string; + delay: number; + duration: number; + }[] + >([]); + + useEffect(() => { + const checkAuth = async () => { + try { + const response = await fetch(`${API_URL}/auth/token/`, { + mode: "cors", + credentials: "include" + }); + setIsAuthenticated(response.ok); + } catch { + setIsAuthenticated(false); + } + }; + checkAuth(); + + const savedFlags = localStorage.getItem("ctf_flags"); + if (savedFlags) { + const parsedFlags = JSON.parse(savedFlags); + const allFlags = Array(9) + .fill("") + .map((_, i) => parsedFlags[i] || ""); + setFlagInputs(allFlags); + } + + const savedStatus = localStorage.getItem("ctf_status"); + if (savedStatus) { + try { + const parsedStatus = JSON.parse(savedStatus); + const allStatus = Array(9) + .fill(null) + .map( + (_, i) => + parsedStatus[i] || { + correct: null, + claimed: false, + loading: false + } + ); + setFlagStatus(allStatus); + } catch { + /* ignore */ + } + } + + const generatedStars = Array.from({ length: 100 }).map((_, i) => ({ + id: i, + size: Math.random() * 4 + 1, + top: `${Math.random() * 100}%`, + left: `${Math.random() * 100}%`, + delay: Math.random() * 8, + duration: 2 + Math.random() * 3 + })); + setStars(generatedStars); + }, []); + + const submitFlag = useCallback(async (index: number, answer: string) => { + const flagId = `flag${index + 1}`; + + setFlagStatus(prev => { + const newStatus = [...prev]; + newStatus[index] = { ...newStatus[index], loading: true }; + return newStatus; + }); + + try { + const response = await fetch(`${API_URL}/ctf/submit/${flagId}/`, { + method: "POST", + mode: "cors", + credentials: "include", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ answer: answer.trim() }) + }); + + const data = await response.json(); + + if (response.ok) { + return { correct: true, claimed: false }; + } + if (data.error === "AlreadyClaimed") { + return { correct: true, claimed: true }; + } + return { correct: false, claimed: false }; + } catch { + return { correct: false, claimed: false }; + } + }, []); + + const handleFlagChange = (index: number, value: string) => { + const newInputs = [...flagInputs]; + newInputs[index] = value; + setFlagInputs(newInputs); + localStorage.setItem("ctf_flags", JSON.stringify(newInputs)); + + if (debounceRefs.current[index]) { + clearTimeout(debounceRefs.current[index]!); + } + + if (!value.trim()) return; + + debounceRefs.current[index] = setTimeout(async () => { + const result = await submitFlag(index, value); + setFlagStatus(prev => { + const newStatus = [...prev]; + newStatus[index] = { + correct: result.correct, + claimed: result.claimed, + loading: false + }; + localStorage.setItem("ctf_status", JSON.stringify(newStatus)); + return newStatus; + }); + }, 800); + }; + + const correctCount = flagStatus.filter(s => s.correct === true).length; + const progress = (correctCount / 9) * 100; + + useEffect(() => { + setShowSuccess(correctCount === 9); + }, [correctCount]); + + if (isAuthenticated === null) { + return ( + + + + ); + } + + if (!isAuthenticated) { + return ( + + + Please log in to submit flags + + + + ); + } + + return ( + + {stars.map(star => ( + + ))} + + + + + + + + + + + + + + + + + + + + + + + + + + SUBMIT YOUR FLAGS + + + + + + Enter the flags you've discovered. Your + progress will be automatically saved. Good luck! + + + + + + + Flag Format + + + hackctf{"{flag#-flagcontent}"} + + + Submit flags in the exact format. You can submit + in any order! + + + + + + + + Point Distribution + + + {FLAG_POINTS.map((item, index) => ( + + + Flag + {item.range.includes("-") + ? "s" + : ""}{" "} + {item.range} + + + {item.points}pts + + + ))} + + + + + {showSuccess && ( + + + Congratulations! You've found all the + flags! + + + )} + + + + + + + Progress: {correctCount} / {9} flags + + + + + + Level 1 + + + {flagInputs.slice(0, 3).map((flag, index) => ( + + + + + FLAG {index + 1} + {flagStatus[index].correct === + true && " (correct)"} + {flagStatus[index].correct === + false && " (incorrect)"} + + + {getFlagPoints(index)}pts + + + + handleFlagChange( + index, + e.target.value + ) + } + placeholder="Enter flag..." + sx={{ + fontFamily: "Montserrat", + "& .MuiOutlinedInput-root": { + fontFamily: "Montserrat", + color: "white", + backgroundColor: + "rgba(0, 0, 0, 0.3)", + borderRadius: "8px", + "& fieldset": { + borderColor: + flagStatus[index] + .correct === + true + ? "#4CAF50" + : flagStatus[ + index + ] + .correct === + false + ? "#F44336" + : "rgba(255, 255, 255, 0.3)" + }, + "&:hover fieldset": { + borderColor: + flagStatus[index] + .correct === + true + ? "#4CAF50" + : flagStatus[ + index + ] + .correct === + false + ? "#F44336" + : "rgba(163, 21, 214, 0.5)" + }, + "&.Mui-focused fieldset": { + borderColor: + flagStatus[index] + .correct === + true + ? "#4CAF50" + : flagStatus[ + index + ] + .correct === + false + ? "#F44336" + : "#A315D6" + } + }, + "& .MuiInputBase-input": { + color: "white", + fontFamily: "Montserrat" + } + }} + /> + + + ))} + + + + Level 2 + + + {flagInputs.slice(3, 6).map((flag, idx) => { + const index = idx + 3; + return ( + + + + + FLAG {index + 1} + {flagStatus[index] + .correct === true && + " (correct)"} + {flagStatus[index] + .correct === false && + " (incorrect)"} + + + {getFlagPoints(index)}pts + + + + handleFlagChange( + index, + e.target.value + ) + } + placeholder="Enter flag..." + sx={{ + fontFamily: "Montserrat", + "& .MuiOutlinedInput-root": + { + fontFamily: + "Montserrat", + color: "white", + backgroundColor: + "rgba(0, 0, 0, 0.3)", + borderRadius: "8px", + "& fieldset": { + borderColor: + flagStatus[ + index + ] + .correct === + true + ? "#4CAF50" + : flagStatus[ + index + ] + .correct === + false + ? "#F44336" + : "rgba(255, 255, 255, 0.3)" + }, + "&:hover fieldset": + { + borderColor: + flagStatus[ + index + ] + .correct === + true + ? "#4CAF50" + : flagStatus[ + index + ] + .correct === + false + ? "#F44336" + : "rgba(163, 21, 214, 0.5)" + }, + "&.Mui-focused fieldset": + { + borderColor: + flagStatus[ + index + ] + .correct === + true + ? "#4CAF50" + : flagStatus[ + index + ] + .correct === + false + ? "#F44336" + : "#A315D6" + } + }, + "& .MuiInputBase-input": { + color: "white", + fontFamily: "Montserrat" + } + }} + /> + + + ); + })} + + + + Level 3 + + + {flagInputs.slice(6, 9).map((flag, idx) => { + const index = idx + 6; + return ( + + + + + FLAG {index + 1} + {flagStatus[index] + .correct === true && + " (correct)"} + {flagStatus[index] + .correct === false && + " (incorrect)"} + + + {getFlagPoints(index)}pts + + + + handleFlagChange( + index, + e.target.value + ) + } + placeholder="Enter flag..." + sx={{ + fontFamily: "Montserrat", + "& .MuiOutlinedInput-root": + { + fontFamily: + "Montserrat", + color: "white", + backgroundColor: + "rgba(0, 0, 0, 0.3)", + borderRadius: "8px", + "& fieldset": { + borderColor: + flagStatus[ + index + ] + .correct === + true + ? "#4CAF50" + : flagStatus[ + index + ] + .correct === + false + ? "#F44336" + : "rgba(255, 255, 255, 0.3)" + }, + "&:hover fieldset": + { + borderColor: + flagStatus[ + index + ] + .correct === + true + ? "#4CAF50" + : flagStatus[ + index + ] + .correct === + false + ? "#F44336" + : "rgba(163, 21, 214, 0.5)" + }, + "&.Mui-focused fieldset": + { + borderColor: + flagStatus[ + index + ] + .correct === + true + ? "#4CAF50" + : flagStatus[ + index + ] + .correct === + false + ? "#F44336" + : "#A315D6" + } + }, + "& .MuiInputBase-input": { + color: "white", + fontFamily: "Montserrat" + } + }} + /> + + + ); + })} + + + + + + + + ← Back to CTF + + + + + + + + ); +} diff --git a/app/ctf/submit/submit.module.scss b/app/ctf/submit/submit.module.scss new file mode 100644 index 00000000..b09804eb --- /dev/null +++ b/app/ctf/submit/submit.module.scss @@ -0,0 +1,55 @@ +.ctfSubmitSection { + position: relative; + width: 100%; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + background: linear-gradient(to bottom, #020316, #16133e); + z-index: 1; +} + +@keyframes shimmer { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } +} + +.flagCard { + background: rgba(255, 255, 255, 0.08); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: 16px; + padding: 20px; + transition: all 0.3s ease; + + &:hover { + transform: translateY(-4px); + box-shadow: 0 12px 35px rgba(163, 21, 214, 0.25); + border-color: rgba(163, 21, 214, 0.4); + } +} + +.progressBar { + background: rgba(255, 255, 255, 0.1); + border-radius: 10px; + overflow: hidden; + height: 12px; + + .progressFill { + height: 100%; + background: linear-gradient(90deg, #a315d6, #fdab60, #a315d6); + background-size: 200% 100%; + animation: shimmer 2s linear infinite; + transition: width 0.5s ease; + } +} + +@media (max-width: 768px) { + .flagCard { + padding: 16px; + } +} diff --git a/app/ctf/utils.ts b/app/ctf/utils.ts new file mode 100644 index 00000000..7508f747 --- /dev/null +++ b/app/ctf/utils.ts @@ -0,0 +1,4 @@ +export function derive(split: string[]) { + const decoded = split.map(c => atob(c)); + return decoded.reverse().join(""); +}