├── .gitallowed
├── .github
└── workflows
│ ├── release.yml
│ └── update-images.yaml
├── .gitignore
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── SECURITY.md
├── dist
├── activate
├── bin
│ ├── kctf-challenge
│ ├── kctf-cluster
│ ├── kctf-completion
│ └── kctf-log
├── challenge-templates
│ ├── pwn
│ │ ├── README.md
│ │ ├── challenge.yaml
│ │ ├── challenge
│ │ │ ├── Dockerfile
│ │ │ ├── Makefile
│ │ │ ├── chal.c
│ │ │ ├── flag
│ │ │ └── nsjail.cfg
│ │ └── healthcheck
│ │ │ ├── Dockerfile
│ │ │ ├── README.md
│ │ │ ├── healthcheck.py
│ │ │ ├── healthcheck_loop.sh
│ │ │ └── healthz_webserver.py
│ ├── web
│ │ ├── README.md
│ │ ├── challenge.yaml
│ │ ├── challenge
│ │ │ ├── Dockerfile
│ │ │ ├── apache2-kctf-nsjail.conf
│ │ │ ├── cgi-bin.nsjail.cfg
│ │ │ ├── cgi-bin
│ │ │ │ └── nsjail-php-cgi
│ │ │ ├── flag
│ │ │ ├── web-apps
│ │ │ │ ├── nodejs
│ │ │ │ │ └── app.js
│ │ │ │ └── php
│ │ │ │ │ └── index.php
│ │ │ ├── web-servers.nsjail.cfg
│ │ │ └── web-servers
│ │ │ │ └── nodejs.sh
│ │ └── healthcheck
│ │ │ ├── Dockerfile
│ │ │ ├── README.md
│ │ │ ├── healthcheck.py
│ │ │ ├── healthcheck_loop.sh
│ │ │ └── healthz_webserver.py
│ └── xss-bot
│ │ ├── README.md
│ │ ├── challenge.yaml
│ │ ├── challenge
│ │ ├── .puppeteerrc.cjs
│ │ ├── Dockerfile
│ │ ├── bot.js
│ │ └── cookie
│ │ └── healthcheck
│ │ ├── Dockerfile
│ │ ├── README.md
│ │ ├── healthcheck.py
│ │ ├── healthcheck_loop.sh
│ │ └── healthz_webserver.py
└── resources
│ └── install.yaml
├── docker-images
├── certbot
│ ├── Dockerfile
│ └── certbot.sh
├── challenge
│ ├── Dockerfile
│ ├── kctf_drop_privs
│ ├── kctf_pow
│ ├── kctf_setup
│ └── pow.py
├── gcsfuse
│ └── Dockerfile
└── healthcheck
│ ├── Dockerfile
│ ├── kctf_bypass_pow
│ └── kctf_drop_privs
├── docs
├── _config.yml
├── _layouts
│ └── default.html
├── ctf-playbook.md
├── custom-domains.md
├── google-cloud.md
├── images
│ ├── flag-locations.png
│ ├── introduction-k8s.png
│ ├── php_sample.png
│ └── threat-model-graph.png
├── index.md
├── introduction.md
├── kctf-exploits.html
├── kctf-operator.md
├── local-testing.md
├── security-threat-model.md
├── troubleshooting.md
└── vrp.md
└── kctf-operator
├── .dockerignore
├── .gitignore
├── Dockerfile
├── Makefile
├── PROJECT
├── api
└── v1
│ ├── challenge_types.go
│ ├── groupversion_info.go
│ └── zz_generated.deepcopy.go
├── bin
└── .gitignore
├── build-and-deploy-operator.sh
├── bundle.Dockerfile
├── bundle
└── .gitignore
├── config
├── crd
│ ├── bases
│ │ └── kctf.dev_challenges.yaml
│ ├── kustomization.yaml
│ ├── kustomizeconfig.yaml
│ └── patches
│ │ ├── cainjection_in_challenges.yaml
│ │ └── webhook_in_challenges.yaml
├── default
│ ├── kustomization.yaml
│ ├── manager_auth_proxy_patch.yaml
│ └── manager_config_patch.yaml
├── manager
│ ├── controller_manager_config.yaml
│ ├── kustomization.yaml
│ └── manager.yaml
├── manifests
│ ├── bases
│ │ └── kctf-operator.clusterserviceversion.yaml
│ └── kustomization.yaml
├── prometheus
│ ├── kustomization.yaml
│ └── monitor.yaml
├── rbac
│ ├── auth_proxy_client_clusterrole.yaml
│ ├── auth_proxy_role.yaml
│ ├── auth_proxy_role_binding.yaml
│ ├── auth_proxy_service.yaml
│ ├── challenge_editor_role.yaml
│ ├── challenge_viewer_role.yaml
│ ├── kustomization.yaml
│ ├── leader_election_role.yaml
│ ├── leader_election_role_binding.yaml
│ ├── role.yaml
│ ├── role_binding.yaml
│ └── service_account.yaml
├── samples
│ ├── kctf_v1_challenge.yaml
│ ├── kustomization.yaml
│ ├── mychal.yaml
│ ├── mychal2.yaml
│ ├── mychal3.yaml
│ └── simple-challenge.yaml
└── scorecard
│ ├── bases
│ └── config.yaml
│ ├── kustomization.yaml
│ └── patches
│ ├── basic.config.yaml
│ └── olm.config.yaml
├── controllers
├── autoscaling
│ ├── functions.go
│ └── horizontal-pod-autoscaler.go
├── challenge_controller.go
├── deployment
│ ├── deployment-with-healthcheck.go
│ ├── deployment.go
│ ├── functions.go
│ ├── image.go
│ └── replicas.go
├── network-policy
│ ├── functions.go
│ └── network-policy.go
├── pow
│ ├── configmap.go
│ └── functions.go
├── secrets
│ ├── functions.go
│ └── secrets.go
├── service
│ ├── functions.go
│ └── service.go
├── set
│ └── default.go
├── status
│ └── functions.go
├── suite_test.go
├── utils
│ └── utils.go
└── volumes
│ ├── functions.go
│ ├── persistentvolume.go
│ └── persistentvolumeclaim.go
├── go.mod
├── go.sum
├── hack
└── boilerplate.go.txt
├── main.go
└── resources
├── allow-dns.go
├── constants.go
├── daemon-gcsfuse.go
├── external-dns.go
├── initializer.go
├── network-policy.go
└── secret-pow.go
/.gitallowed:
--------------------------------------------------------------------------------
1 | KCTF_CLOUD_API_KEY.*
2 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Create Release
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | version:
7 | description: 'Version'
8 | required: true
9 | default: 'MAJOR.MINOR.PATCH'
10 | release_notes:
11 | description: 'Release Notes'
12 | required: true
13 |
14 | jobs:
15 | create_draft_release:
16 | runs-on: ubuntu-latest
17 | permissions:
18 | contents: write
19 | steps:
20 | - uses: actions/checkout@v2
21 |
22 | - name: Check version has the right format
23 | run: |
24 | [[ "${{ github.event.inputs.version }}" =~ ^[0-9]+[.][0-9]+[.][0-9]+$ ]]
25 |
26 | - name: Create archive
27 | run: |
28 | mv dist kctf
29 | echo ${{ github.event.inputs.version }} > kctf/VERSION
30 | tar -cz kctf > kctf.tgz
31 | git config user.name ${{ github.actor }}
32 | git config user.email action@github.com
33 | git tag v${{ github.event.inputs.version }}
34 | git push origin v${{ github.event.inputs.version }}
35 |
36 | - name: Create Release
37 | id: create_release
38 | uses: actions/create-release@v1
39 | env:
40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
41 | with:
42 | tag_name: v${{ github.event.inputs.version }}
43 | release_name: Release ${{ github.event.inputs.version }}
44 | body: ${{ github.event.inputs.release_notes }}
45 | draft: true
46 | prerelease: false
47 |
48 | - name: Upload Release Asset
49 | id: upload-release-asset
50 | uses: actions/upload-release-asset@v1
51 | env:
52 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
53 | with:
54 | upload_url: ${{ steps.create_release.outputs.upload_url }}
55 | asset_path: kctf.tgz
56 | asset_name: kctf.tgz
57 | asset_content_type: application/gzip
58 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist/bin/kind
2 | dist/bin/kubectl
3 | dist/bin/yq
4 | dist/config/*
5 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to Contribute
2 |
3 | We'd love to accept your patches and contributions to this project. There are
4 | just a few small guidelines you need to follow.
5 |
6 | ## Contributor License Agreement
7 |
8 | Contributions to this project must be accompanied by a Contributor License
9 | Agreement. You (or your employer) retain the copyright to your contribution;
10 | this simply gives us permission to use and redistribute your contributions as
11 | part of the project. Head over to to see
12 | your current agreements on file or to sign a new one.
13 |
14 | You generally only need to submit a CLA once, so if you've already submitted one
15 | (even if it was for a different project), you probably don't need to do it
16 | again.
17 |
18 | ## Code reviews
19 |
20 | All submissions, including submissions by project members, require review. We
21 | use GitHub pull requests for this purpose. Consult
22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
23 | information on using pull requests.
24 |
25 | ## Community Guidelines
26 |
27 | This project follows [Google's Open Source Community
28 | Guidelines](https://opensource.google.com/conduct/).
29 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # kCTF
2 | [](https://github.com/google/kctf/actions?query=workflow%3A%22GKE+Deployment%22)
3 |
4 | kCTF is a Kubernetes-based infrastructure for CTF competitions.
5 |
6 | ## Prerequisites
7 |
8 | * [gcloud](https://cloud.google.com/sdk/install)
9 | * [docker](https://docs.docker.com/install/)
10 |
11 | ## Getting Started / Documentation
12 |
13 | For an introduction to what kCTF is and how it interacts with Kubernetes, see [kCTF in 8 Minutes](https://google.github.io/kctf/introduction.html).
14 |
15 | Additional documentation resources are:
16 |
17 | * **[Local Testing Walkthrough](https://google.github.io/kctf/local-testing.html) – A quick start guide showing you how to build and test challenges locally.**
18 | * [Google Cloud Walkthrough](https://google.github.io/kctf/google-cloud.html) – Once you have everything up and running, try deploying to Google Cloud.
19 | * [Troubleshooting](https://google.github.io/kctf/troubleshooting.html) – Help with fixing broken challenges.
20 | * [Security Threat Model](https://google.github.io/kctf/security-threat-model.html) – Security considerations regarding kCTF including information on assets, risks, and potential attackers.
21 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | To report a vulnerability in this repository, or in Google Cloud please contact the Google Security Team at https://g.co/vulnz. Read more about reporting vulnerabilities in kCTF [here](docs/vrp.md). You can read the threat model [here](docs/security-threat-model.md).
4 |
5 | To report a vulnerability in a dependency, please contact the upstream maintainer directly.
6 |
7 | - [Kubernetes](https://kubernetes.io/docs/reference/issues-security/security/#report-a-vulnerability)
8 | - [Docker](https://github.com/moby/moby/blob/master/CONTRIBUTING.md#reporting-security-issues)
9 | - [Ubuntu](https://wiki.ubuntu.com/SecurityTeam/FAQ?_ga=2.254550412.542177495.1583355140-2013298171.1583355140#Contact)
10 | - [Linux kernel](https://www.kernel.org/doc/html/v4.11/admin-guide/security-bugs.html)
11 |
--------------------------------------------------------------------------------
/dist/bin/kctf-log:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Copyright 2020 Google LLC
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # https://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 |
16 | _KCTF_COLOR_RED=$'\e[0;31m'
17 | _KCTF_COLOR_GREEN=$'\e[0;32m'
18 | _KCTF_COLOR_YELLOW=$'\e[0;33m'
19 | _KCTF_COLOR_END=$'\e[0m'
20 |
21 | function _kctf_log {
22 | echo -n "${_KCTF_COLOR_GREEN}[*]${_KCTF_COLOR_END} " >&2
23 | echo "$@" >&2
24 | }
25 |
26 | function _kctf_log_warn {
27 | echo -n "${_KCTF_COLOR_YELLOW}[W]${_KCTF_COLOR_END} " >&2
28 | echo "$@" >&2
29 | }
30 |
31 | function _kctf_log_err {
32 | echo -n "${_KCTF_COLOR_RED}[E]${_KCTF_COLOR_END} " >&2
33 | echo "$@" >&2
34 | }
35 |
36 |
--------------------------------------------------------------------------------
/dist/challenge-templates/pwn/README.md:
--------------------------------------------------------------------------------
1 | # Quickstart guide to writing a challenge
2 |
3 | The basic steps when preparing a challenge are:
4 |
5 | * A Docker image is built from the `challenge` directory. For the simplest challenges, replacing `challenge/chal.c` is sufficient.
6 | * Edit `challenge/Dockerfile` to change the commandline or the files you want to include.
7 | * To try the challenge locally, you will need to
8 | * create a a local cluster with `kctf cluster create --type kind --start $configname`
9 | * build the challenge binary with `make -C challenge`
10 | * and then deploy the challenge with `kctf chal start`
11 | * To access the challenge, create a port forward with `kctf chal debug port-forward` and connect to it via `nc localhost PORT` using the printed port.
12 | * Check out `kctf chal ` for more commands.
13 |
14 | ## Directory layout
15 |
16 | The following files/directories are available:
17 |
18 | ### /challenge.yaml
19 |
20 | `challenge.yaml` is the main configuration file. You can use it to change
21 | settings like the name and namespace of the challenge, the exposed ports, the
22 | proof-of-work difficulty etc.
23 | For documentation on the available fields, you can run `kubectl explain challenge` and
24 | `kubectl explain challenge.spec`.
25 |
26 | ### /challenge
27 |
28 | The `challenge` directory contains a Dockerfile that describes the challenge and
29 | any challenge files. This template comes with a Makefile to build the challenge,
30 | which is the recommended way for pwnables if the deployed binary matters, e.g.
31 | if you hand it out as an attachment for ROP gadgets.
32 | If the binary layout doesn't matter, you can build it using an intermediate
33 | container as part of the Dockerfile similar to how the chroot is created.
34 |
35 | ### /healthcheck
36 |
37 | The `healthcheck` directory is optional. If you don't want to write a healthcheck, feel free to delete it. However, we strongly recommend that you implement a healthcheck :).
38 |
39 | We provide a basic healthcheck skeleton that uses pwntools to implement the
40 | healthcheck code. The only requirement is that the healthcheck replies to GET
41 | requests to http://$host:45281/healthz with either a success or an error status
42 | code.
43 |
44 | In most cases, you will only have to modify `healthcheck/healthcheck.py`.
45 |
46 | ## API contract
47 |
48 | Ensure your setup fulfills the following requirements to ensure it works with kCTF:
49 |
50 | * Verify `kctf_setup` is used as the first command in the CMD instruction of your `challenge/Dockerfile`.
51 | * You can do pretty much whatever you want in the `challenge` directory but:
52 | * We strongly recommend using nsjail in all challenges. While nsjail is already installed, you need to configure it in `challenge/nsjail.cfg`. For more information on nsjail, see the [official website](https://nsjail.dev/).
53 | * Your challenge receives connections on port 1337. The port can be changed in `challenge.yaml`.
54 | * The healthcheck directory is optional.
55 | * If it exists, the image should run a webserver on port 45281 and respond to `/healthz` requests.
56 |
--------------------------------------------------------------------------------
/dist/challenge-templates/pwn/challenge.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: kctf.dev/v1
2 | kind: Challenge
3 | metadata:
4 | name: pwn
5 | spec:
6 | deployed: true
7 | powDifficultySeconds: 0
8 | network:
9 | public: false
10 | healthcheck:
11 | # TIP: disable the healthcheck during development
12 | enabled: true
13 |
--------------------------------------------------------------------------------
/dist/challenge-templates/pwn/challenge/Dockerfile:
--------------------------------------------------------------------------------
1 | # Copyright 2025 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 | FROM ubuntu:24.04 as chroot
15 |
16 | # ubuntu24 includes the ubuntu user by default
17 | RUN /usr/sbin/userdel -r ubuntu && /usr/sbin/useradd --no-create-home -u 1000 user
18 |
19 | COPY flag /
20 | COPY chal /home/user/
21 |
22 | FROM gcr.io/kctf-docker/challenge@sha256:9f15314c26bd681a043557c9f136e7823414e9e662c08dde54d14a6bfd0b619f
23 |
24 | COPY --from=chroot / /chroot
25 |
26 | COPY nsjail.cfg /home/user/
27 |
28 | CMD kctf_setup && \
29 | kctf_drop_privs \
30 | socat \
31 | TCP-LISTEN:1337,reuseaddr,fork \
32 | EXEC:"kctf_pow nsjail --config /home/user/nsjail.cfg -- /home/user/chal"
33 |
--------------------------------------------------------------------------------
/dist/challenge-templates/pwn/challenge/Makefile:
--------------------------------------------------------------------------------
1 | LDFLAGS=-static
2 |
3 | chal: chal.c
4 |
--------------------------------------------------------------------------------
/dist/challenge-templates/pwn/challenge/chal.c:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | int main(int argc, char *argv[]) {
4 | system("cat /flag");
5 | return 0;
6 | }
7 |
--------------------------------------------------------------------------------
/dist/challenge-templates/pwn/challenge/flag:
--------------------------------------------------------------------------------
1 | CTF{TestFlag}
--------------------------------------------------------------------------------
/dist/challenge-templates/pwn/challenge/nsjail.cfg:
--------------------------------------------------------------------------------
1 | # Copyright 2020 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | # See options available at https://github.com/google/nsjail/blob/master/config.proto
16 |
17 | name: "default-nsjail-configuration"
18 | description: "Default nsjail configuration for pwnable-style CTF task."
19 |
20 | mode: ONCE
21 | uidmap {inside_id: "1000"}
22 | gidmap {inside_id: "1000"}
23 | rlimit_as_type: HARD
24 | rlimit_cpu_type: HARD
25 | rlimit_nofile_type: HARD
26 | rlimit_nproc_type: HARD
27 |
28 | cwd: "/home/user"
29 |
30 | mount: [
31 | {
32 | src: "/chroot"
33 | dst: "/"
34 | is_bind: true
35 | },
36 | {
37 | dst: "/tmp"
38 | fstype: "tmpfs"
39 | rw: true
40 | },
41 | {
42 | dst: "/proc"
43 | fstype: "proc"
44 | rw: true
45 | },
46 | {
47 | src: "/etc/resolv.conf"
48 | dst: "/etc/resolv.conf"
49 | is_bind: true
50 | }
51 | ]
52 |
--------------------------------------------------------------------------------
/dist/challenge-templates/pwn/healthcheck/Dockerfile:
--------------------------------------------------------------------------------
1 | # Copyright 2020 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 | FROM gcr.io/kctf-docker/healthcheck@sha256:66b34a47e7bbb832012905e229da0bbed80c5c3cddd4703127ca4026ba528cfc
15 |
16 | COPY healthcheck_loop.sh healthcheck.py healthz_webserver.py /home/user/
17 |
18 | CMD kctf_drop_privs /home/user/healthcheck_loop.sh & /home/user/healthz_webserver.py
19 |
--------------------------------------------------------------------------------
/dist/challenge-templates/pwn/healthcheck/README.md:
--------------------------------------------------------------------------------
1 | # Healthcheck
2 |
3 | kCTF checks the health of challenges by accessing the healthcheck via
4 | http://host:45281/healthz which needs to return either 200 ok or an error
5 | depending on the status of the challenge.
6 |
7 | The default healthcheck consists of:
8 | * a loop that repeatedly calls a python script and writes the status to a file
9 | * a webserver that checks the file and serves /healthz
10 | * the actual healthcheck code using pwntools for convenience
11 |
12 | To modify it, you will likely only have to change the script in healthcheck.py.
13 | You can test if the challenge replies as expected or better add a full example
14 | solution that will try to get the flag from the challenge.
15 |
--------------------------------------------------------------------------------
/dist/challenge-templates/pwn/healthcheck/healthcheck.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | # Copyright 2020 Google LLC
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # https://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 |
17 | import pwnlib.tubes
18 |
19 | def handle_pow(r):
20 | print(r.recvuntil(b'python3 '))
21 | print(r.recvuntil(b' solve '))
22 | challenge = r.recvline().decode('ascii').strip()
23 | p = pwnlib.tubes.process.process(['kctf_bypass_pow', challenge])
24 | solution = p.readall().strip()
25 | r.sendline(solution)
26 | print(r.recvuntil(b'Correct\n'))
27 |
28 | r = pwnlib.tubes.remote.remote('127.0.0.1', 1337)
29 | print(r.recvuntil('== proof-of-work: '))
30 | if r.recvline().startswith(b'enabled'):
31 | handle_pow(r)
32 |
33 | print(r.recvuntil(b'CTF{'))
34 | print(r.recvuntil(b'}'))
35 |
36 | exit(0)
37 |
--------------------------------------------------------------------------------
/dist/challenge-templates/pwn/healthcheck/healthcheck_loop.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Copyright 2020 Google LLC
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # https://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 | set -Eeuo pipefail
16 |
17 | TIMEOUT=20
18 | PERIOD=30
19 |
20 | export TERM=linux
21 | export TERMINFO=/etc/terminfo
22 |
23 | while true; do
24 | echo -n "[$(date)] "
25 | if timeout "${TIMEOUT}" /home/user/healthcheck.py; then
26 | echo 'ok' | tee /tmp/healthz
27 | else
28 | echo -n "$? "
29 | echo 'err' | tee /tmp/healthz
30 | fi
31 | sleep "${PERIOD}"
32 | done
33 |
--------------------------------------------------------------------------------
/dist/challenge-templates/pwn/healthcheck/healthz_webserver.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | # Copyright 2020 Google LLC
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # https://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 | import http.server
17 |
18 | class HealthzHandler(http.server.BaseHTTPRequestHandler):
19 | def do_GET(self):
20 | if self.path != '/healthz':
21 | self.send_response(404)
22 | self.send_header("Content-length", "0")
23 | self.end_headers()
24 | return
25 |
26 | content = b'err'
27 | try:
28 | with open('/tmp/healthz', 'rb') as fd:
29 | content = fd.read().strip()
30 | except:
31 | pass
32 | self.send_response(200 if content == b'ok' else 400)
33 | self.send_header("Content-type", "text/plain")
34 | self.send_header("Content-length", str(len(content)))
35 | self.end_headers()
36 | self.wfile.write(content)
37 |
38 | httpd = http.server.HTTPServer(('', 45281), HealthzHandler)
39 | httpd.serve_forever()
40 |
--------------------------------------------------------------------------------
/dist/challenge-templates/web/README.md:
--------------------------------------------------------------------------------
1 | # Quickstart guide to writing a challenge
2 |
3 | The basic steps when preparing a challenge are:
4 |
5 | * A Docker image is built from the `challenge` directory. For the simplest challenges, replacing `challenge/chal.c` is sufficient.
6 | * Edit `challenge/Dockerfile` to change the commandline or the files you want to include.
7 | * To try the challenge locally, you will need to
8 | * create a a local cluster with `kctf cluster create --type kind --start $configname`
9 | * and then deploy the challenge with `kctf chal start`
10 | * To access the challenge, create a port forward with `kctf chal debug port-forward` and connect to it via `nc localhost PORT` using the printed port.
11 | * Check out `kctf chal ` for more commands.
12 |
13 | ## Sandboxing
14 |
15 | Sandboxing is only necessary for challenges that give players RCE-type of access. If a challenge does not provide such access, then it is reasonable to just use a normal HTTP server out of the box listening on port 1337, without any additonal sandboxing.
16 |
17 | For challenges that give users RCE-level access, it is then necessary to sandbox every player. In order to make that possible, kCTF provides two ways to sandbox a web server:
18 | 1. **CGI-sandbox**: You can configure PHP (or any other CGI) to be sandboxed.
19 | 2. **Proxy sandbox**: You can configure an HTTP server that sandboxes every HTTP request.
20 |
21 | A Proxy sandbox is a bit expensive, it starts an HTTP server on every TCP connection, hence it is a bit slow. A CGI sandbox is cheaper, and it just calls the normal CGI endpoint but with nsjail.
22 |
23 | The template challenge has an example of both (NodeJS running as a proxy, and PHP running as CGI). It is recommended that static resources are served with only Apache, as to save CPU and RAM. This can be accomplished by configuring apache to redirect certain sub-paths to the sandboxed web server, but to serve directly all other paths.
24 |
25 | ## Directory layout
26 |
27 | The following files/directories are available:
28 |
29 | ### /challenge.yaml
30 |
31 | `challenge.yaml` is the main configuration file. You can use it to change
32 | settings like the name and namespace of the challenge, the exposed ports, the
33 | proof-of-work difficulty etc.
34 | For documentation on the available fields, you can run `kubectl explain challenge` and
35 | `kubectl explain challenge.spec`.
36 |
37 | If you would like to have a shared directory (for sessions, or uploads), you can mount it using:
38 |
39 |
40 | ```yaml
41 | spec:
42 | persistentVolumeClaims:
43 | - $PUT_THE_NAME_OF_THE_CHALLENGE_HERE
44 | podTemplate:
45 | template:
46 | spec:
47 | containers:
48 | - name: challenge
49 | volumeMounts:
50 | - name: gcsfuse
51 | subPath: sessions # this this a folder inside volume
52 | mountPath: /mnt/disks/sessions
53 | - name: gcsfuse
54 | subPath: uploads
55 | mountPath: /mnt/disks/uploads
56 | volumes:
57 | - name: gcsfuse
58 | persistentVolumeClaim:
59 | claimName: $PUT_THE_NAME_OF_THE_CHALLENGE_HERE
60 | ```
61 |
62 | This will mount a file across all challenges in that directory. You can test this setup on a remote cluster using the PHP/CGI sandbox.
63 |
64 | ### /challenge
65 |
66 | The `challenge` directory contains a Dockerfile that describes the challenge and
67 | any challenge files. You can use the Dockerfile to build your challenge as well
68 | if required.
69 |
70 | ### /healthcheck
71 |
72 | The `healthcheck` directory is optional. If you don't want to write a healthcheck, feel free to delete it. However, we strongly recommend that you implement a healthcheck :).
73 |
74 | We provide a basic healthcheck skeleton that uses pwntools to implement the
75 | healthcheck code. The only requirement is that the healthcheck replies to GET
76 | requests to http://$host:45281/healthz with either a success or an error status
77 | code.
78 |
79 | In most cases, you will only have to modify `healthcheck/healthcheck.py`.
80 |
81 | ## API contract
82 |
83 | Ensure your setup fulfills the following requirements to ensure it works with kCTF:
84 |
85 | * Verify `kctf_setup` is used as the first command in the CMD instruction of your `challenge/Dockerfile`.
86 | * You can do pretty much whatever you want in the `challenge` directory but:
87 | * We strongly recommend using nsjail in all challenges. While nsjail is already installed, you need to configure it in `challenge/nsjail.cfg`. For more information on nsjail, see the [official website](https://nsjail.dev/).
88 | * Your challenge receives connections on port 1337. The port can be changed in `challenge.yaml`.
89 | * The healthcheck directory is optional.
90 | * If it exists, the image should run a webserver on port 45281 and respond to `/healthz` requests.
91 |
--------------------------------------------------------------------------------
/dist/challenge-templates/web/challenge.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: kctf.dev/v1
2 | kind: Challenge
3 | metadata:
4 | name: apache-others
5 | spec:
6 | deployed: true
7 | powDifficultySeconds: 0
8 | network:
9 | public: false
10 | ports:
11 | - protocol: "HTTPS"
12 | targetPort: 1337
13 | healthcheck:
14 | # TIP: disable the healthcheck during development
15 | enabled: true
16 |
--------------------------------------------------------------------------------
/dist/challenge-templates/web/challenge/Dockerfile:
--------------------------------------------------------------------------------
1 | # Copyright 2025 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 | FROM ubuntu:24.04 as chroot
15 |
16 | # ubuntu24 includes the ubuntu user by default
17 | RUN /usr/sbin/userdel -r ubuntu && /usr/sbin/useradd --no-create-home -u 1000 user
18 |
19 | RUN apt-get update \
20 | && apt-get install -yq --no-install-recommends \
21 | curl ca-certificates socat gnupg lsb-release software-properties-common php-cgi \
22 | && rm -rf /var/lib/apt/lists/*
23 |
24 | RUN curl -fsSL https://deb.nodesource.com/setup_20.x -o nodesource_setup.sh \
25 | && bash nodesource_setup.sh \
26 | && add-apt-repository universe \
27 | && apt-get update \
28 | && apt-get install -yq --no-install-recommends nodejs socat \
29 | && rm -rf /var/lib/apt/lists/*
30 |
31 | RUN mkdir -p /mnt/disks/sessions
32 | RUN mkdir -p /mnt/disks/uploads
33 |
34 | VOLUME /mnt/disks/sessions
35 | VOLUME /mnt/disks/uploads
36 |
37 | COPY web-apps /web-apps
38 | COPY web-servers /web-servers
39 |
40 | COPY flag /
41 |
42 | FROM gcr.io/kctf-docker/challenge@sha256:9f15314c26bd681a043557c9f136e7823414e9e662c08dde54d14a6bfd0b619f
43 |
44 | RUN apt-get update \
45 | && DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends tzdata apache2 \
46 | && ln -fs /usr/share/zoneinfo/Europe/Berlin /etc/localtime \
47 | && dpkg-reconfigure --frontend noninteractive tzdata \
48 | && rm -rf /var/lib/apt/lists/*
49 |
50 | RUN service apache2 start
51 |
52 | COPY --from=chroot / /chroot
53 |
54 | # For Proxy
55 | RUN ln -s /etc/apache2/mods-available/proxy.load /etc/apache2/mods-enabled/
56 | RUN ln -s /etc/apache2/mods-available/proxy_http.load /etc/apache2/mods-enabled/
57 |
58 | # For CGI sandboxing
59 | RUN ln -s /etc/apache2/mods-available/cgi.load /etc/apache2/mods-enabled/cgi.load
60 | RUN ln -s /etc/apache2/mods-available/actions.load /etc/apache2/mods-enabled/actions.load
61 | RUN ln -s /chroot/web-apps /web-apps
62 | COPY cgi-bin /usr/lib/cgi-bin
63 |
64 | COPY apache2-kctf-nsjail.conf /etc/apache2/conf-enabled/
65 |
66 | COPY web-servers.nsjail.cfg /home/user/web-servers.nsjail.cfg
67 | COPY cgi-bin.nsjail.cfg /home/user/cgi-bin.nsjail.cfg
68 |
69 | VOLUME /var/log/apache2
70 | VOLUME /var/run/apache2
71 |
72 | CMD kctf_setup \
73 | && (kctf_drop_privs nsjail --config /home/user/web-servers.nsjail.cfg --port 8081 -- /web-servers/nodejs.sh &) \
74 | && bash -c 'source /etc/apache2/envvars && APACHE_RUN_USER=user APACHE_RUN_GROUP=user /usr/sbin/apache2 -D FOREGROUND'
75 |
--------------------------------------------------------------------------------
/dist/challenge-templates/web/challenge/apache2-kctf-nsjail.conf:
--------------------------------------------------------------------------------
1 | ServerName kctf-nsjail
2 | Listen 1337
3 | User user
4 |
5 | # This is only necessary for CGI sandboxing
6 |
7 | Options +ExecCGI
8 | Options +FollowSymLinks
9 | Action application/x-nsjail-httpd-php /cgi-bin/nsjail-php-cgi
10 | AddHandler application/x-nsjail-httpd-php php
11 | Require all granted
12 |
13 |
14 |
15 | # For proxy sandboxing use the two lines below
16 | ProxyPreserveHost On
17 | ProxyPass "/nodejs" "http://localhost:8081/"
18 | # For CGI sandboxing use the line below
19 | DocumentRoot "/web-apps/php"
20 |
21 |
--------------------------------------------------------------------------------
/dist/challenge-templates/web/challenge/cgi-bin.nsjail.cfg:
--------------------------------------------------------------------------------
1 | # Copyright 2020 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | # See options available at https://github.com/google/nsjail/blob/master/config.proto
16 |
17 | name: "apache2-proxy-nsjail"
18 | description: "Example nsjail configuration for containing a web server."
19 |
20 | mode: ONCE
21 | uidmap {inside_id: "1000"}
22 | gidmap {inside_id: "1000"}
23 | mount_proc: true
24 | keep_env: true
25 | rlimit_as_type: HARD
26 | rlimit_cpu_type: HARD
27 | rlimit_nofile_type: HARD
28 | rlimit_nproc_type: HARD
29 |
30 | mount: [
31 | {
32 | src: "/chroot"
33 | dst: "/"
34 | is_bind: true
35 | },
36 | {
37 | src: "/dev"
38 | dst: "/dev"
39 | is_bind: true
40 | },
41 | {
42 | src: "/dev/null"
43 | dst: "/dev/null"
44 | is_bind: true
45 | },
46 | {
47 | src: "/etc/resolv.conf"
48 | dst: "/etc/resolv.conf"
49 | is_bind: true
50 | },
51 | {
52 | dst: "/mnt/disks/sessions"
53 | fstype: "tmpfs"
54 | rw: true
55 | },
56 | {
57 | src: "/mnt/disks/sessions"
58 | dst: "/mnt/disks/sessions"
59 | is_bind: true
60 | rw: true
61 | mandatory: false
62 | },
63 | {
64 | dst: "/mnt/disks/uploads"
65 | fstype: "tmpfs"
66 | rw: true
67 | },
68 | {
69 | src: "/mnt/disks/uploads"
70 | dst: "/mnt/disks/uploads"
71 | is_bind: true
72 | rw: true
73 | mandatory: false
74 | },
75 | {
76 | dst: "/tmp"
77 | fstype: "tmpfs"
78 | rw: true
79 | }
80 | ]
81 |
--------------------------------------------------------------------------------
/dist/challenge-templates/web/challenge/cgi-bin/nsjail-php-cgi:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | /usr/bin/nsjail --config /home/user/cgi-bin.nsjail.cfg -- /usr/lib/cgi-bin/php $@
4 |
--------------------------------------------------------------------------------
/dist/challenge-templates/web/challenge/flag:
--------------------------------------------------------------------------------
1 | CTF{TestFlag}
--------------------------------------------------------------------------------
/dist/challenge-templates/web/challenge/web-apps/nodejs/app.js:
--------------------------------------------------------------------------------
1 | const http = require('http');
2 |
3 | const hostname = '127.0.0.1';
4 | const port = 8080;
5 |
6 | const server = http.createServer((req, res) => {
7 | res.statusCode = 200;
8 | res.setHeader('Content-Type', 'text/plain');
9 | res.end(req.url.split('').reverse().join(''));
10 | });
11 |
12 | server.listen(port, hostname, () => {
13 | console.log(`Server running at http://${hostname}:${port}/`);
14 | });
15 |
--------------------------------------------------------------------------------
/dist/challenge-templates/web/challenge/web-apps/php/index.php:
--------------------------------------------------------------------------------
1 |
8 |
9 |
17 |
18 |
19 |
24 |
25 |
28 |
--------------------------------------------------------------------------------
/dist/challenge-templates/web/challenge/web-servers.nsjail.cfg:
--------------------------------------------------------------------------------
1 | # Copyright 2020 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | # See options available at https://github.com/google/nsjail/blob/master/config.proto
16 |
17 | name: "apache2-proxy-nsjail"
18 | description: "Example nsjail configuration for containing a web server."
19 |
20 | mode: LISTEN
21 | uidmap {inside_id: "1000"}
22 | gidmap {inside_id: "1000"}
23 | mount_proc: true
24 | rlimit_as_type: HARD
25 | rlimit_cpu_type: HARD
26 | rlimit_nofile_type: HARD
27 | rlimit_nproc_type: HARD
28 |
29 | mount: [
30 | {
31 | src: "/chroot"
32 | dst: "/"
33 | is_bind: true
34 | },
35 | {
36 | src: "/dev"
37 | dst: "/dev"
38 | is_bind: true
39 | },
40 | {
41 | src: "/dev/null"
42 | dst: "/dev/null"
43 | is_bind: true
44 | rw: true
45 | },
46 | {
47 | src: "/etc/resolv.conf"
48 | dst: "/etc/resolv.conf"
49 | is_bind: true
50 | },
51 | {
52 | dst: "/tmp"
53 | fstype: "tmpfs"
54 | rw: true
55 | }
56 | ]
57 |
--------------------------------------------------------------------------------
/dist/challenge-templates/web/challenge/web-servers/nodejs.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Start node web server
4 | (&>/dev/null node /web-apps/nodejs/app.js)&
5 |
6 | # Proxy stdin/stdout to web server
7 | socat - TCP:127.0.0.1:8080,forever
8 |
--------------------------------------------------------------------------------
/dist/challenge-templates/web/healthcheck/Dockerfile:
--------------------------------------------------------------------------------
1 | # Copyright 2020 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 | FROM gcr.io/kctf-docker/healthcheck@sha256:66b34a47e7bbb832012905e229da0bbed80c5c3cddd4703127ca4026ba528cfc
15 |
16 | COPY healthcheck_loop.sh healthcheck.py healthz_webserver.py /home/user/
17 |
18 | CMD kctf_drop_privs /home/user/healthcheck_loop.sh & /home/user/healthz_webserver.py
19 |
--------------------------------------------------------------------------------
/dist/challenge-templates/web/healthcheck/README.md:
--------------------------------------------------------------------------------
1 | # Healthcheck
2 |
3 | kCTF checks the health of challenges by accessing the healthcheck via
4 | http://host:45281/healthz which needs to return either 200 ok or an error
5 | depending on the status of the challenge.
6 |
7 | The default healthcheck consists of:
8 | * a loop that repeatedly calls a python script and writes the status to a file
9 | * a webserver that checks the file and serves /healthz
10 | * the actual healthcheck code using pwntools for convenience
11 |
12 | To modify it, you will likely only have to change the script in healthcheck.py.
13 | You can test if the challenge replies as expected or better add a full example
14 | solution that will try to get the flag from the challenge.
15 |
--------------------------------------------------------------------------------
/dist/challenge-templates/web/healthcheck/healthcheck.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | # Copyright 2020 Google LLC
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # https://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 |
17 | import pwnlib.util.web
18 |
19 | if b"imanode" in pwnlib.util.web.wget("http://localhost:1337/nodejs?edonami"):
20 | exit(0)
21 |
22 | exit(1)
23 |
--------------------------------------------------------------------------------
/dist/challenge-templates/web/healthcheck/healthcheck_loop.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Copyright 2020 Google LLC
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # https://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 | set -Eeuo pipefail
16 |
17 | TIMEOUT=20
18 | PERIOD=30
19 |
20 | export TERM=linux
21 | export TERMINFO=/etc/terminfo
22 |
23 | while true; do
24 | echo -n "[$(date)] "
25 | if timeout "${TIMEOUT}" /home/user/healthcheck.py; then
26 | echo 'ok' | tee /tmp/healthz
27 | else
28 | echo -n "$? "
29 | echo 'err' | tee /tmp/healthz
30 | fi
31 | sleep "${PERIOD}"
32 | done
33 |
--------------------------------------------------------------------------------
/dist/challenge-templates/web/healthcheck/healthz_webserver.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | # Copyright 2020 Google LLC
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # https://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 | import http.server
17 |
18 | class HealthzHandler(http.server.BaseHTTPRequestHandler):
19 | def do_GET(self):
20 | if self.path != '/healthz':
21 | self.send_response(404)
22 | self.send_header("Content-length", "0")
23 | self.end_headers()
24 | return
25 |
26 | content = b'err'
27 | try:
28 | with open('/tmp/healthz', 'rb') as fd:
29 | content = fd.read().strip()
30 | except:
31 | pass
32 | self.send_response(200 if content == b'ok' else 400)
33 | self.send_header("Content-type", "text/plain")
34 | self.send_header("Content-length", str(len(content)))
35 | self.end_headers()
36 | self.wfile.write(content)
37 |
38 | httpd = http.server.HTTPServer(('', 45281), HealthzHandler)
39 | httpd.serve_forever()
40 |
--------------------------------------------------------------------------------
/dist/challenge-templates/xss-bot/README.md:
--------------------------------------------------------------------------------
1 | = Example XSS Bot =
2 |
3 | This bot will read a url from the user and then connect to it using chrome (puppeteer).
4 | For the simplest setup, it should be enough to modify the `challenge/cookie`
5 | file and deploy.
6 |
--------------------------------------------------------------------------------
/dist/challenge-templates/xss-bot/challenge.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: kctf.dev/v1
2 | kind: Challenge
3 | metadata:
4 | name: xss-bot
5 | spec:
6 | deployed: true
7 | powDifficultySeconds: 0
8 | network:
9 | public: false
10 | healthcheck:
11 | # TIP: disable the healthcheck during development
12 | enabled: true
13 | # You can allow the bot to connect to other challenges internally.
14 | # This can be useful during testing so that you don't have to make your
15 | # challenge public.
16 | # The challenge will be reachable at $name.default.svc.cluster.local or
17 | # simply at $name with the default k8s search list.
18 | #allowConnectTo:
19 | # - otherchallenge
20 |
--------------------------------------------------------------------------------
/dist/challenge-templates/xss-bot/challenge/.puppeteerrc.cjs:
--------------------------------------------------------------------------------
1 |
2 | const {join} = require('path');
3 |
4 |
5 | /**
6 | * @type {import("puppeteer").Configuration}
7 | */
8 | module.exports = {
9 | // Changes the cache location for Puppeteer.
10 | cacheDirectory: join(__dirname, ".cache", "puppeteer"),
11 | };
12 |
--------------------------------------------------------------------------------
/dist/challenge-templates/xss-bot/challenge/Dockerfile:
--------------------------------------------------------------------------------
1 | # Copyright 2020 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 | FROM gcr.io/kctf-docker/challenge@sha256:9f15314c26bd681a043557c9f136e7823414e9e662c08dde54d14a6bfd0b619f
15 |
16 | RUN apt-get update && apt-get install -y gnupg2 wget
17 |
18 | # Install latest chrome dev package and fonts to support major charsets (Chinese, Japanese, Arabic, Hebrew, Thai and a few others)
19 | # Note: this installs the necessary libs to make the bundled version of Chromium that Puppeteer installs, work.
20 | # Deps from https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md#chrome-headless-doesnt-launch-on-unix
21 | # plus libxshmfence1 which seems to be missing
22 | RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
23 | && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
24 | && wget -q -O - https://deb.nodesource.com/setup_20.x | bash - \
25 | && apt-get update \
26 | && DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
27 | ca-certificates \
28 | fonts-liberation \
29 | libappindicator3-1 \
30 | libasound2t64 \
31 | libatk-bridge2.0-0 \
32 | libatk1.0-0 \
33 | libc6 \
34 | libcairo2 \
35 | libcups2 \
36 | libdbus-1-3 \
37 | libexpat1 \
38 | libfontconfig1 \
39 | libgbm1 \
40 | libgcc1 \
41 | libglib2.0-0 \
42 | libgtk-3-0 \
43 | libnspr4 \
44 | libnss3 \
45 | libpango-1.0-0 \
46 | libpangocairo-1.0-0 \
47 | libstdc++6 \
48 | libx11-6 \
49 | libx11-xcb1 \
50 | libxcb1 \
51 | libxcomposite1 \
52 | libxcursor1 \
53 | libxdamage1 \
54 | libxext6 \
55 | libxfixes3 \
56 | libxi6 \
57 | libxrandr2 \
58 | libxrender1 \
59 | libxshmfence1 \
60 | libxss1 \
61 | libxtst6 \
62 | lsb-release \
63 | wget \
64 | xdg-utils \
65 | nodejs \
66 | && rm -rf /var/lib/apt/lists/*
67 |
68 | COPY bot.js /home/user/
69 | COPY cookie /home/user/
70 | COPY .puppeteerrc.cjs /home/user/
71 | RUN cd /home/user && npm install puppeteer
72 |
73 | ENV DOMAIN="www.example.com"
74 | # Hosting multiple web challenges same-site to each other can lead to
75 | # unintended solutions. E.g. an xss on a.foo.com will be able to overwrite
76 | # cookies on b.foo.com.
77 | # To prevent this, we can block chrome from accessing any subdomains under
78 | # foo.com except for the real challenge domain using a PAC script.
79 | # Unfortunately, PAC will not work in chrome headless mode, so this will use
80 | # more resources.
81 | ENV BLOCK_SUBORIGINS="1"
82 | ENV REGISTERED_DOMAIN="example.com"
83 |
84 | RUN if [ "${BLOCK_SUBORIGINS}" = "1" ]; then \
85 | apt-get update \
86 | && apt-get install -yq --no-install-recommends xvfb \
87 | && rm -rf /var/lib/apt/lists/*; \
88 | fi
89 | RUN sed -i -e "s/DOMAIN_SET_IN_DOCKERFILE/${DOMAIN}/" /home/user/cookie
90 |
91 | CMD kctf_setup && \
92 | mount -t tmpfs none /tmp && \
93 | mkdir /tmp/chrome-userdata && chmod o+rwx /tmp/chrome-userdata && \
94 | while true; do \
95 | if [ "${BLOCK_SUBORIGINS}" = "1" ]; then \
96 | kctf_drop_privs env BLOCK_SUBORIGINS="${BLOCK_SUBORIGINS}" DOMAIN="${DOMAIN}" REGISTERED_DOMAIN="${REGISTERED_DOMAIN}" xvfb-run /usr/bin/node /home/user/bot.js; \
97 | else \
98 | kctf_drop_privs env BLOCK_SUBORIGINS="${BLOCK_SUBORIGINS}" DOMAIN="${DOMAIN}" REGISTERED_DOMAIN="${REGISTERED_DOMAIN}" /usr/bin/node /home/user/bot.js; \
99 | fi; \
100 | done & \
101 | kctf_drop_privs \
102 | socat \
103 | TCP-LISTEN:1337,reuseaddr,fork \
104 | EXEC:"kctf_pow socat STDIN TCP\:localhost\:1338"
105 |
--------------------------------------------------------------------------------
/dist/challenge-templates/xss-bot/challenge/bot.js:
--------------------------------------------------------------------------------
1 | const puppeteer = require('puppeteer');
2 | const fs = require('fs');
3 | const net = require('net');
4 |
5 | const DOMAIN = process.env.DOMAIN;
6 | if (DOMAIN == undefined) throw 'domain undefined'
7 | const REGISTERED_DOMAIN = process.env.REGISTERED_DOMAIN;
8 | const BLOCK_SUBORIGINS = process.env.BLOCK_SUBORIGINS == "1";
9 | const BOT_TIMEOUT = process.env.BOT_TIMEOUT || 60*1000;
10 |
11 | // will only be used if BLOCK_SUBORIGINS is enabled
12 | const PAC_B64 = Buffer.from(`
13 | function FindProxyForURL (url, host) {
14 | if (host == "${DOMAIN}") {
15 | return 'DIRECT';
16 | }
17 | if (host == "${REGISTERED_DOMAIN}" || dnsDomainIs(host, ".${REGISTERED_DOMAIN}")) {
18 | return 'PROXY 127.0.0.1:1';
19 | }
20 | return 'DIRECT';
21 | }
22 | `).toString('base64');
23 | const puppeter_args = {};
24 | if (BLOCK_SUBORIGINS) {
25 | puppeter_args.headless = false;
26 | puppeter_args.args = [
27 | '--user-data-dir=/tmp/chrome-userdata',
28 | '--breakpad-dump-location=/tmp/chrome-crashes',
29 | '--proxy-pac-url=data:application/x-ns-proxy-autoconfig;base64,'+PAC_B64,
30 | ];
31 | }
32 | puppeter_args.args.push('--incognito');
33 |
34 | (async function(){
35 | const browser = await puppeteer.launch(puppeter_args);
36 |
37 | function ask_for_url(socket) {
38 | socket.state = 'URL';
39 | socket.write('Please send me a URL to open.\n');
40 | }
41 |
42 | async function load_url(socket, data) {
43 | let url = data.toString().trim();
44 | console.log(`checking url: ${url}`);
45 | if (!url.startsWith('http://') && !url.startsWith('https://')) {
46 | socket.state = 'ERROR';
47 | socket.write('Invalid scheme (http/https only).\n');
48 | socket.destroy();
49 | return;
50 | }
51 | socket.state = 'LOADED';
52 | let cookie = JSON.parse(fs.readFileSync('/home/user/cookie'));
53 |
54 | const context = await browser.createBrowserContext();
55 | const page = await context.newPage();
56 | await page.setCookie(cookie);
57 | socket.write(`Loading page ${url}.\n`);
58 | setTimeout(()=>{
59 | try {
60 | context.close();
61 | socket.write('timeout\n');
62 | socket.destroy();
63 | } catch (err) {
64 | console.log(`err: ${err}`);
65 | }
66 | }, BOT_TIMEOUT);
67 | await page.goto(url);
68 | }
69 |
70 | var server = net.createServer();
71 | server.listen(1338);
72 | console.log('listening on port 1338');
73 |
74 | server.on('connection', socket=>{
75 | socket.on('data', data=>{
76 | try {
77 | if (socket.state == 'URL') {
78 | load_url(socket, data);
79 | }
80 | } catch (err) {
81 | console.log(`err: ${err}`);
82 | }
83 | });
84 |
85 | try {
86 | ask_for_url(socket);
87 | } catch (err) {
88 | console.log(`err: ${err}`);
89 | }
90 | });
91 | })();
92 |
93 |
--------------------------------------------------------------------------------
/dist/challenge-templates/xss-bot/challenge/cookie:
--------------------------------------------------------------------------------
1 | {
2 | "name": "session",
3 | "value": "aiy3Uushcha4Zuzu",
4 | "domain": "DOMAIN_SET_IN_DOCKERFILE",
5 | "url": "https://DOMAIN_SET_IN_DOCKERFILE/",
6 | "path": "/",
7 | "httpOnly": true,
8 | "secure": true
9 | }
10 |
--------------------------------------------------------------------------------
/dist/challenge-templates/xss-bot/healthcheck/Dockerfile:
--------------------------------------------------------------------------------
1 | # Copyright 2020 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 | FROM gcr.io/kctf-docker/healthcheck@sha256:66b34a47e7bbb832012905e229da0bbed80c5c3cddd4703127ca4026ba528cfc
15 |
16 | COPY healthcheck_loop.sh healthcheck.py healthz_webserver.py /home/user/
17 |
18 | CMD kctf_drop_privs /home/user/healthcheck_loop.sh & /home/user/healthz_webserver.py
19 |
--------------------------------------------------------------------------------
/dist/challenge-templates/xss-bot/healthcheck/README.md:
--------------------------------------------------------------------------------
1 | # Healthcheck
2 |
3 | kCTF checks the health of challenges by accessing the healthcheck via
4 | http://host:45281/healthz which needs to return either 200 ok or an error
5 | depending on the status of the challenge.
6 |
7 | The default healthcheck consists of:
8 | * a loop that repeatedly calls a python script and writes the status to a file
9 | * a webserver that checks the file and serves /healthz
10 | * the actual healthcheck code using pwntools for convenience
11 |
12 | To modify it, you will likely only have to change the script in healthcheck.py.
13 | You can test if the challenge replies as expected or better add a full example
14 | solution that will try to get the flag from the challenge.
15 |
--------------------------------------------------------------------------------
/dist/challenge-templates/xss-bot/healthcheck/healthcheck.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 | import socket
5 | from pwn import *
6 |
7 | r = remote('127.0.0.1', 1337)
8 | l = listen()
9 |
10 | r.readuntil(b'URL to open.', timeout=10)
11 | r.send(bytes('http://localhost:{}/ok'.format(l.lport), 'ascii'))
12 |
13 | _ = l.wait_for_connection()
14 | l.readuntil(b'GET /ok HTTP/1.1')
15 | l.send(b'HTTP/1.1 200 OK\nContent-Length: 0\n\n')
16 |
17 | exit (0)
18 |
--------------------------------------------------------------------------------
/dist/challenge-templates/xss-bot/healthcheck/healthcheck_loop.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Copyright 2020 Google LLC
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # https://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 | set -Eeuo pipefail
16 |
17 | TIMEOUT=20
18 | PERIOD=30
19 |
20 | export TERM=linux
21 | export TERMINFO=/etc/terminfo
22 |
23 | while true; do
24 | echo -n "[$(date)] "
25 | if timeout "${TIMEOUT}" /home/user/healthcheck.py; then
26 | echo 'ok' | tee /tmp/healthz
27 | else
28 | echo -n "$? "
29 | echo 'err' | tee /tmp/healthz
30 | fi
31 | sleep "${PERIOD}"
32 | done
33 |
--------------------------------------------------------------------------------
/dist/challenge-templates/xss-bot/healthcheck/healthz_webserver.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | # Copyright 2020 Google LLC
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # https://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 | import http.server
17 |
18 | class HealthzHandler(http.server.BaseHTTPRequestHandler):
19 | def do_GET(self):
20 | if self.path != '/healthz':
21 | self.send_response(404)
22 | self.send_header("Content-length", "0")
23 | self.end_headers()
24 | return
25 |
26 | content = b'err'
27 | try:
28 | with open('/tmp/healthz', 'rb') as fd:
29 | content = fd.read().strip()
30 | except:
31 | pass
32 | self.send_response(200 if content == b'ok' else 400)
33 | self.send_header("Content-type", "text/plain")
34 | self.send_header("Content-length", str(len(content)))
35 | self.end_headers()
36 | self.wfile.write(content)
37 |
38 | httpd = http.server.HTTPServer(('', 45281), HealthzHandler)
39 | httpd.serve_forever()
40 |
--------------------------------------------------------------------------------
/docker-images/certbot/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM ubuntu:24.04
2 | RUN apt update && DEBIAN_FRONTEND=noninteractive apt install -y certbot python3-certbot-dns-google curl jq
3 | RUN curl -LO "https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl" && chmod +x kubectl
4 | COPY certbot.sh certbot.sh
5 | RUN chmod +x certbot.sh
6 | CMD ./certbot.sh
7 |
--------------------------------------------------------------------------------
/docker-images/certbot/certbot.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | if [ -z "${DOMAIN}" ]; then
4 | echo "Make sure the DOMAIN environment variable points to the domain."
5 | exit 1
6 | fi
7 |
8 | if [ -z "${SECRET}" ]; then
9 | echo "Make sure the SECRET environment variable points to the k8s secret."
10 | exit 1
11 | fi
12 |
13 | if [ -z "${PROD}" ]; then
14 | echo "Making a TEST certificate because PROD environment variable is NOT set."
15 | TEST="--test-cert"
16 | else
17 | echo "Making a REAL certificate because PROD environment variable is set."
18 | TEST=""
19 | fi
20 |
21 | if [ -z "${EMAIL}" ]; then
22 | echo "Registering certificate unsafely without email. Pass an EMAIL to register an account with an email address."
23 | EMAIL_FLAG="--register-unsafely-without-email"
24 | else
25 | EMAIL_FLAG="-m ${EMAIL}"
26 | fi
27 |
28 | function request_certificate() {
29 | certbot certonly ${TEST} --non-interactive --agree-tos ${EMAIL_FLAG} --dns-google -d '*.'"${DOMAIN}" --dns-google-propagation-seconds 120
30 | }
31 |
32 | function update_tls_secret() {
33 | ./kubectl create secret tls "${SECRET}" --cert /etc/letsencrypt/live/"${DOMAIN}"/fullchain.pem --key /etc/letsencrypt/live/"${DOMAIN}"/privkey.pem --namespace kctf-system --dry-run=client --save-config -o yaml | ./kubectl apply -f -
34 | }
35 |
36 | function check_tls_validity() {
37 | ./kubectl get secret "${SECRET}" --namespace kctf-system -o 'jsonpath={.data}' | jq -r '.["tls.crt"]' | base64 -d | openssl x509 -checkend 2592000 -noout -in -
38 | }
39 |
40 | while true; do
41 | echo "Waiting 2 minutes to avoid hitting rate limits"
42 | sleep 2m
43 | if check_tls_validity; then
44 | echo "Certificate is valid for at least 30 days"
45 | else
46 | request_certificate && update_tls_secret && echo "TLS cert updated"
47 | fi
48 | done
49 |
--------------------------------------------------------------------------------
/docker-images/challenge/Dockerfile:
--------------------------------------------------------------------------------
1 | # Copyright 2020 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | # build nsjail first
16 | FROM ubuntu:24.04 as nsjail
17 |
18 | ENV BUILD_PACKAGES build-essential git protobuf-compiler libprotobuf-dev bison flex pkg-config libnl-route-3-dev ca-certificates
19 | ENV NSJAIL_COMMIT 3677ccbe45b184bd4600415cbfb48762a2735674
20 |
21 | RUN apt-get update \
22 | && apt-get install -yq --no-install-recommends $BUILD_PACKAGES \
23 | && rm -rf /var/lib/apt/lists/* \
24 | && git clone https://github.com/google/nsjail.git \
25 | && cd /nsjail && git checkout $NSJAIL_COMMIT && make -j && cp nsjail /usr/bin/ \
26 | && rm -R /nsjail
27 |
28 | # challenge image
29 | FROM ubuntu:24.04
30 |
31 | RUN apt-get update \
32 | && apt-get install -yq --no-install-recommends build-essential python3-dev python3.8 python3-pip libgmp3-dev libmpc-dev uidmap libprotobuf32t64 libnl-route-3-200 wget netcat-traditional ca-certificates socat \
33 | && rm -rf /var/lib/apt/lists/*
34 |
35 | # ubuntu24 includes the ubuntu user by default
36 | RUN /usr/sbin/userdel -r ubuntu && /usr/sbin/useradd --no-create-home -u 1000 user
37 |
38 | COPY --from=nsjail /usr/bin/nsjail /usr/bin/nsjail
39 |
40 | # gmpy2 and ecdsa used by the proof of work
41 | RUN python3 -m pip install --break-system-packages ecdsa gmpy2
42 |
43 | # we need a clean proc to allow nsjail to remount it in the user namespace
44 | RUN mkdir /kctf
45 | RUN mkdir -p /kctf/.fullproc/proc
46 | RUN chmod 0700 /kctf/.fullproc
47 |
48 | COPY kctf_setup /usr/bin/
49 | COPY kctf_drop_privs /usr/bin/
50 | COPY kctf_pow /usr/bin/
51 | COPY pow.py /kctf/
52 |
--------------------------------------------------------------------------------
/docker-images/challenge/kctf_drop_privs:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # There are two copies of this file in the nsjail and healthcheck base images.
4 |
5 | all_caps="-cap_0"
6 | for i in $(seq 1 $(cat /proc/sys/kernel/cap_last_cap)); do
7 | all_caps+=",-cap_${i}"
8 | done
9 |
10 | exec setpriv --init-groups --reset-env --reuid user --regid user --inh-caps=${all_caps} -- "$@"
11 |
--------------------------------------------------------------------------------
/docker-images/challenge/kctf_pow:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Copyright 2020 Google LLC
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # https://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 |
16 | POW_FILE="/kctf/pow/pow.conf"
17 |
18 | if [ -f ${POW_FILE} ]; then
19 | POW="$(cat ${POW_FILE})"
20 | if ! /kctf/pow.py ask "${POW}"; then
21 | echo 'pow fail'
22 | exit 1
23 | fi
24 | fi
25 |
26 | exec "$@"
27 |
--------------------------------------------------------------------------------
/docker-images/challenge/kctf_setup:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Copyright 2020 Google LLC
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # https://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 | set -Eeuxo pipefail
16 |
17 | # We need a clean proc for user namespaces
18 | mount -t proc none /kctf/.fullproc/proc
19 |
--------------------------------------------------------------------------------
/docker-images/challenge/pow.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | # Copyright 2020 Google LLC
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # https://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 |
17 | import base64
18 | import os
19 | import secrets
20 | import socket
21 | import sys
22 | import hashlib
23 |
24 | try:
25 | import gmpy2
26 | HAVE_GMP = True
27 | except ImportError:
28 | HAVE_GMP = False
29 | sys.stderr.write("[NOTICE] Running 10x slower, gotta go fast? pip3 install gmpy2\n")
30 |
31 | VERSION = 's'
32 | MODULUS = 2**1279-1
33 | CHALSIZE = 2**128
34 |
35 | SOLVER_URL = 'https://goo.gle/kctf-pow'
36 |
37 | def python_sloth_root(x, diff, p):
38 | exponent = (p + 1) // 4
39 | for i in range(diff):
40 | x = pow(x, exponent, p) ^ 1
41 | return x
42 |
43 | def python_sloth_square(y, diff, p):
44 | for i in range(diff):
45 | y = pow(y ^ 1, 2, p)
46 | return y
47 |
48 | def gmpy_sloth_root(x, diff, p):
49 | exponent = (p + 1) // 4
50 | for i in range(diff):
51 | x = gmpy2.powmod(x, exponent, p).bit_flip(0)
52 | return int(x)
53 |
54 | def gmpy_sloth_square(y, diff, p):
55 | y = gmpy2.mpz(y)
56 | for i in range(diff):
57 | y = gmpy2.powmod(y.bit_flip(0), 2, p)
58 | return int(y)
59 |
60 | def sloth_root(x, diff, p):
61 | if HAVE_GMP:
62 | return gmpy_sloth_root(x, diff, p)
63 | else:
64 | return python_sloth_root(x, diff, p)
65 |
66 | def sloth_square(x, diff, p):
67 | if HAVE_GMP:
68 | return gmpy_sloth_square(x, diff, p)
69 | else:
70 | return python_sloth_square(x, diff, p)
71 |
72 | def encode_number(num):
73 | size = (num.bit_length() // 24) * 3 + 3
74 | return str(base64.b64encode(num.to_bytes(size, 'big')), 'utf-8')
75 |
76 | def decode_number(enc):
77 | return int.from_bytes(base64.b64decode(bytes(enc, 'utf-8')), 'big')
78 |
79 | def decode_challenge(enc):
80 | dec = enc.split('.')
81 | if dec[0] != VERSION:
82 | raise Exception('Unknown challenge version')
83 | return list(map(decode_number, dec[1:]))
84 |
85 | def encode_challenge(arr):
86 | return '.'.join([VERSION] + list(map(encode_number, arr)))
87 |
88 | def get_challenge(diff):
89 | x = secrets.randbelow(CHALSIZE)
90 | return encode_challenge([diff, x])
91 |
92 | def solve_challenge(chal):
93 | [diff, x] = decode_challenge(chal)
94 | y = sloth_root(x, diff, MODULUS)
95 | return encode_challenge([y])
96 |
97 | def can_bypass(chal, sol):
98 | from ecdsa import VerifyingKey
99 | from ecdsa.util import sigdecode_der
100 | if not sol.startswith('b.'):
101 | return False
102 | sig = bytes.fromhex(sol[2:])
103 | with open("/kctf/pow-bypass/pow-bypass-key-pub.pem", "r") as fd:
104 | vk = VerifyingKey.from_pem(fd.read())
105 | return vk.verify(signature=sig, data=bytes(chal, 'ascii'), hashfunc=hashlib.sha256, sigdecode=sigdecode_der)
106 |
107 | def verify_challenge(chal, sol, allow_bypass=True):
108 | if allow_bypass and can_bypass(chal, sol):
109 | return True
110 | [diff, x] = decode_challenge(chal)
111 | [y] = decode_challenge(sol)
112 | res = sloth_square(y, diff, MODULUS)
113 | return (x == res) or (MODULUS - x == res)
114 |
115 | def usage():
116 | sys.stdout.write('Usage:\n')
117 | sys.stdout.write('Solve pow: {} solve $challenge\n')
118 | sys.stdout.write('Check pow: {} ask $difficulty\n')
119 | sys.stdout.write(' $difficulty examples (for 1.6GHz CPU) in fast mode:\n')
120 | sys.stdout.write(' 1337: 1 sec\n')
121 | sys.stdout.write(' 31337: 30 secs\n')
122 | sys.stdout.write(' 313373: 5 mins\n')
123 | sys.stdout.flush()
124 | sys.exit(1)
125 |
126 | def main():
127 | if len(sys.argv) != 3:
128 | usage()
129 | sys.exit(1)
130 |
131 | cmd = sys.argv[1]
132 |
133 | if cmd == 'ask':
134 | difficulty = int(sys.argv[2])
135 |
136 | if difficulty == 0:
137 | sys.stdout.write("== proof-of-work: disabled ==\n")
138 | sys.exit(0)
139 |
140 |
141 | challenge = get_challenge(difficulty)
142 |
143 | sys.stdout.write("== proof-of-work: enabled ==\n")
144 | sys.stdout.write("please solve a pow first\n")
145 | sys.stdout.write("You can run the solver with:\n")
146 | sys.stdout.write(" python3 <(curl -sSL {}) solve {}\n".format(SOLVER_URL, challenge))
147 | sys.stdout.write("===================\n")
148 | sys.stdout.write("\n")
149 | sys.stdout.write("Solution? ")
150 | sys.stdout.flush()
151 | solution = ''
152 | with os.fdopen(0, "rb", 0) as f:
153 | while not solution:
154 | line = f.readline().decode("utf-8")
155 | if not line:
156 | sys.stdout.write("EOF")
157 | sys.stdout.flush()
158 | sys.exit(1)
159 | solution = line.strip()
160 |
161 | if verify_challenge(challenge, solution):
162 | sys.stdout.write("Correct\n")
163 | sys.stdout.flush()
164 | sys.exit(0)
165 | else:
166 | sys.stdout.write("Proof-of-work fail")
167 | sys.stdout.flush()
168 |
169 | elif cmd == 'solve':
170 | challenge = sys.argv[2]
171 | solution = solve_challenge(challenge)
172 |
173 | if verify_challenge(challenge, solution, False):
174 | sys.stderr.write("Solution: \n".format(solution))
175 | sys.stderr.flush()
176 | sys.stdout.write(solution)
177 | sys.stdout.flush()
178 | sys.stderr.write("\n")
179 | sys.stderr.flush()
180 | sys.exit(0)
181 | else:
182 | usage()
183 |
184 | sys.exit(1)
185 |
186 | if __name__ == "__main__":
187 | main()
188 |
--------------------------------------------------------------------------------
/docker-images/gcsfuse/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM ubuntu:24.04
2 |
3 | RUN apt-get update && apt-get install -y wget fuse
4 | RUN wget -q https://github.com/GoogleCloudPlatform/gcsfuse/releases/download/v1.4.2/gcsfuse_1.4.2_amd64.deb && dpkg -i gcsfuse_1.4.2_amd64.deb
5 | RUN mkdir -p /mnt/disks/gcs
6 |
7 | CMD test -f /config/gcs_bucket &&\
8 | gcsfuse --foreground --debug_fuse --debug_gcs --stat-cache-ttl 0 -o allow_other --file-mode 0777 --dir-mode 0777 --uid 1000 --gid 1000 "$(cat /config/gcs_bucket)" /mnt/disks/gcs
9 |
--------------------------------------------------------------------------------
/docker-images/healthcheck/Dockerfile:
--------------------------------------------------------------------------------
1 | # Copyright 2025 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 | FROM ubuntu:24.04
15 |
16 | ENV BUILD_PACKAGES python3-pip build-essential python3-dev
17 |
18 | RUN apt-get update \
19 | && apt-get -yq --no-install-recommends install $BUILD_PACKAGES \
20 | && rm -rf /var/lib/apt/lists/* \
21 | && python3 -m pip install --break-system-packages pwntools \
22 | && apt-get remove --purge -y $BUILD_PACKAGES && apt-get autoremove -y
23 |
24 | RUN apt-get update && apt-get -yq --no-install-recommends install cpio openssl python3 && rm -rf /var/lib/apt/lists/*
25 |
26 | # ubuntu24 includes the ubuntu user by default
27 | RUN /usr/sbin/userdel -r ubuntu && /usr/sbin/useradd --no-create-home -u 1000 user
28 |
29 | RUN mkdir -p /home/user/.pwntools-cache && echo never > /home/user/.pwntools-cache/update
30 |
31 | COPY kctf_drop_privs /usr/bin/
32 | COPY kctf_bypass_pow /usr/bin/
33 |
--------------------------------------------------------------------------------
/docker-images/healthcheck/kctf_bypass_pow:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | CHALLENGE="$1"
4 |
5 | KEY="/pow-bypass/pow-bypass-key.pem"
6 |
7 | SIG=$(echo -n "${CHALLENGE}" | openssl dgst -SHA256 -hex -sign "${KEY}" - | awk '{print $2}')
8 |
9 | echo -n "b.${SIG}"
10 |
--------------------------------------------------------------------------------
/docker-images/healthcheck/kctf_drop_privs:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # There are two copies of this file in the nsjail and healthcheck base images.
4 |
5 | all_caps="-cap_0"
6 | for i in $(seq 1 $(cat /proc/sys/kernel/cap_last_cap)); do
7 | all_caps+=",-cap_${i}"
8 | done
9 |
10 | exec setpriv --init-groups --reset-env --reuid user --regid user --inh-caps=${all_caps} -- $@
11 |
--------------------------------------------------------------------------------
/docs/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-leap-day
2 | title: kCTF
3 | description: kCTF is a Kubernetes-based infrastructure for CTF competitions
4 |
--------------------------------------------------------------------------------
/docs/_layouts/default.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {% seo %}
8 |
9 |
10 |
11 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |