diff --git a/others/chal-reverb/README.md b/others/chal-reverb/README.md new file mode 100644 index 00000000..44415f63 --- /dev/null +++ b/others/chal-reverb/README.md @@ -0,0 +1,55 @@ +# Quickstart guide to writing a challenge + +The basic steps when preparing a challenge are: + +* A Docker image is built from the `challenge` directory. For the simplest challenges, replacing `challenge/chal.c` is sufficient. +* Edit `challenge/Dockerfile` to change the commandline or the files you want to include. +* To try the challenge locally, you will need to + * create a a local cluster with `kctf cluster create --type kind --start $configname` + * build the challenge binary with `make -C challenge` + * and then deploy the challenge with `kctf chal start` +* 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. +* Check out `kctf chal ` for more commands. + +## Directory layout + +The following files/directories are available: + +### /challenge.yaml + +`challenge.yaml` is the main configuration file. You can use it to change +settings like the name and namespace of the challenge, the exposed ports, the +proof-of-work difficulty etc. +For documentation on the available fields, you can run `kubectl explain challenge` and +`kubectl explain challenge.spec`. + +### /challenge + +The `challenge` directory contains a Dockerfile that describes the challenge and +any challenge files. This template comes with a Makefile to build the challenge, +which is the recommended way for pwnables if the deployed binary matters, e.g. +if you hand it out as an attachment for ROP gadgets. +If the binary layout doesn't matter, you can build it using an intermediate +container as part of the Dockerfile similar to how the chroot is created. + +### /healthcheck + +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 :). + +We provide a basic healthcheck skeleton that uses pwntools to implement the +healthcheck code. The only requirement is that the healthcheck replies to GET +requests to http://$host:45281/healthz with either a success or an error status +code. + +In most cases, you will only have to modify `healthcheck/healthcheck.py`. + +## API contract + +Ensure your setup fulfills the following requirements to ensure it works with kCTF: + +* Verify `kctf_setup` is used as the first command in the CMD instruction of your `challenge/Dockerfile`. +* You can do pretty much whatever you want in the `challenge` directory but: +* 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/). +* Your challenge receives connections on port 1337. The port can be changed in `challenge.yaml`. +* The healthcheck directory is optional. + * If it exists, the image should run a webserver on port 45281 and respond to `/healthz` requests. diff --git a/others/chal-reverb/app-secrets.yaml b/others/chal-reverb/app-secrets.yaml new file mode 100644 index 00000000..faacaab3 --- /dev/null +++ b/others/chal-reverb/app-secrets.yaml @@ -0,0 +1,10 @@ +apiVersion: secrets-store.csi.x-k8s.io/v1 +kind: SecretProviderClass +metadata: + name: reverb-secret-provider +spec: + provider: gke + parameters: + secrets: | + - resourceName: "projects/internet-ctf/secrets/reverb-root-flag/versions/latest" + path: "flag.txt" diff --git a/others/chal-reverb/challenge.yaml b/others/chal-reverb/challenge.yaml new file mode 100644 index 00000000..c8d7cf38 --- /dev/null +++ b/others/chal-reverb/challenge.yaml @@ -0,0 +1,33 @@ +apiVersion: kctf.dev/v1 +kind: Challenge +metadata: + name: chal-reverb +spec: + deployed: true + powDifficultySeconds: 0 + network: + public: true + ports: + - protocol: "TCP" + targetPort: 1337 + podTemplate: + template: + spec: + containers: + - name: challenge + volumeMounts: + - name: flag-volume + mountPath: "/chroot/flag" + serviceAccountName: secret-readonly-sa + volumes: + - name: flag-volume + csi: + driver: secrets-store-gke.csi.k8s.io + readOnly: true + volumeAttributes: + secretProviderClass: reverb-secret-provider + healthcheck: + # TIP: disable the healthcheck during development + enabled: true + image: europe-west4-docker.pkg.dev/internet-ctf/challenges/healthcheck:27054f7b9c2c38bfbe153e0dbe7df4e2ab5536c5749cfde9f13997da8034e72f + image: europe-west4-docker.pkg.dev/internet-ctf/challenges/challenge:bf23a96280b24e6398b514bae83ce2587864365e7582be672a172b6d45e4ca40 diff --git a/others/chal-reverb/challenge/Dockerfile b/others/chal-reverb/challenge/Dockerfile new file mode 100644 index 00000000..73ab2252 --- /dev/null +++ b/others/chal-reverb/challenge/Dockerfile @@ -0,0 +1,83 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +FROM python:3.10-slim as chroot + +RUN /usr/sbin/useradd -m -u 1000 user + +RUN apt-get update \ + && apt-get install -yq --no-install-recommends \ + python3 python3-pip socat nano + +#Install socat +USER root +RUN echo 'deb http://archive.debian.org/debian/ rodete main' >> /etc/apt/sources.list +RUN apt-get update; exit 0 +RUN apt-get install socat -y --force-yes; exit 0 + +RUN pip3 install numpy --upgrade +RUN pip3 install dm-tree +RUN pip3 install dm-reverb[tensorflow] +RUN pip3 install tensorrt +RUN pip3 install flask +RUN pip3 install numpy --upgrade; pip3 install dm-tree; pip3 install dm-reverb[tensorflow] + +#Copy +RUN chmod +777 /home/user +ADD script.py /home/user +ADD app.py /home/user +RUN chmod +777 /home/user/script.py +RUN chmod +777 /home/user/app.py +RUN chmod +777 /dev/random +RUN chmod +777 /dev/urandom + +RUN mkdir -p /logs +RUN touch /logs/log +RUN chmod -R +777 /logs +RUN chmod a=w /logs/log + +ADD start.sh /home/user/start.sh +RUN chmod a+wx /home/user/start.sh + +FROM europe-west4-docker.pkg.dev/internet-ctf/custom-images/kctf-source + +RUN apt-get update \ + && apt-get install -yq --no-install-recommends cron +RUN apt-get install -y --reinstall rsyslog + +ADD cleanup.sh /cleanup.sh +RUN chmod +777 /cleanup.sh + +COPY cron /etc/cron.d/cron +RUN chmod 0644 /etc/cron.d/cron +RUN crontab /etc/cron.d/cron + +VOLUME /var/log +VOLUME /run +VOLUME /tmp + +VOLUME /chroot/usr/local/lib/python3.10/dist-packages +VOLUME /chroot/dev +VOLUME /chroot/logs +COPY --from=chroot / /chroot + +VOLUME /chroot/home/user + +COPY nsjail.cfg /home/user/ + +CMD service rsyslog start; \ + cron; \ + kctf_setup && \ + kctf_setup \ + && kctf_drop_privs nsjail --config /home/user/nsjail.cfg --port 1337 -- /home/user/start.sh \ + && tail -F /var/log/syslog \ No newline at end of file diff --git a/others/chal-reverb/challenge/app.py b/others/chal-reverb/challenge/app.py new file mode 100644 index 00000000..c3320912 --- /dev/null +++ b/others/chal-reverb/challenge/app.py @@ -0,0 +1,33 @@ +from flask import Flask, request, render_template_string +import reverb + +app = Flask(__name__) + +html = """ + + Execute Python Expression +

Enter a Python expression:

+
+

+ +
+

Output:

+
{{ result }}
+""" + +@app.route('/', methods=['GET', 'POST']) +def index(): + result = "" + if request.method == 'POST': + expression = request.form['expression'] + try: + # define the variables and functions allowed, including the reverb lib + allowed_globals = {"__builtins__": None, "reverb": reverb} + result = eval(expression, allowed_globals, {}) + except Exception as e: + result = f"Error: {str(e)}" + + return render_template_string(html, result=result) + +if __name__ == '__main__': + app.run(debug=True) \ No newline at end of file diff --git a/others/chal-reverb/challenge/cleanup.sh b/others/chal-reverb/challenge/cleanup.sh new file mode 100644 index 00000000..ce13e1c7 --- /dev/null +++ b/others/chal-reverb/challenge/cleanup.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +logFolder="/chroot/logs" + +now=$(date +%s) +cutoff=$((now - 12*60)) + +for file in "$logFolder"/*.log +do + filename="${file##*/}" + file_time="${filename%.log}" + if [ $file_time -lt $cutoff ]; then + cat "$file" 2>&1 | /usr/bin/logger -t cronjob + rm "$file" + fi + unset filename file_time + sleep 1 + cat /dev/null > /var/log/syslog +done diff --git a/others/chal-reverb/challenge/cron b/others/chal-reverb/challenge/cron new file mode 100644 index 00000000..02e4fea4 --- /dev/null +++ b/others/chal-reverb/challenge/cron @@ -0,0 +1,2 @@ +* * * * * /cleanup.sh +# Don't remove the empty line at the end of this file. It is required to run the cron job diff --git a/others/chal-reverb/challenge/flask.py b/others/chal-reverb/challenge/flask.py new file mode 100644 index 00000000..ffa27bfa --- /dev/null +++ b/others/chal-reverb/challenge/flask.py @@ -0,0 +1,17 @@ +from flask import Flask, request, jsonify +import sys + +app = Flask(__name__) + +@app.route('/execute', methods=['POST']) +def execute_python_code(): + code = request.get_json()['code'] + try: + # Execute the Python code + exec(code, globals()) + return jsonify({'output': 'Code executed successfully!'}) + except Exception as e: + return jsonify({'output': f'Error: {str(e)}'}), 400 + +if __name__ == '__main__': + app.run(debug=True) \ No newline at end of file diff --git a/others/chal-reverb/challenge/nsjail.cfg b/others/chal-reverb/challenge/nsjail.cfg new file mode 100644 index 00000000..1e87b6af --- /dev/null +++ b/others/chal-reverb/challenge/nsjail.cfg @@ -0,0 +1,81 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# See options available at https://github.com/google/nsjail/blob/master/config.proto + +name: "default-nsjail-configuration" +description: "Default nsjail configuration for pwnable-style CTF task." + +mode: ONCE +uidmap {inside_id: "1000"} +gidmap {inside_id: "1000"} +rlimit_as_type: HARD +rlimit_cpu_type: HARD +rlimit_nofile_type: HARD +rlimit_nproc_type: HARD + +cwd: "/home/user" +hostname: "localhost" + +envar: "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + +mount: [ + { + src: "/chroot" + dst: "/" + is_bind: true + }, + { + dst: "/tmp" + fstype: "tmpfs" + rw: true + }, + { + dst: "/proc" + fstype: "proc" + rw: true + }, + { + src: "/dev" + dst: "/dev" + is_bind: true + }, + { + src: "/dev/random" + dst: "/dev/random" + is_bind: true + }, + { + dst: "/dev/urandom" + src: "/dev/urandom" + is_bind: true + }, + { + src: "/chroot/home/user" + dst: "/home/user" + is_bind: true + rw: true + }, + { + src: "/chroot/logs" + dst: "/logs" + is_bind: true + rw: true + }, + { + src: "/etc/resolv.conf" + dst: "/etc/resolv.conf" + is_bind: true + } +] \ No newline at end of file diff --git a/others/chal-reverb/challenge/script.py b/others/chal-reverb/challenge/script.py new file mode 100644 index 00000000..4cadaddd --- /dev/null +++ b/others/chal-reverb/challenge/script.py @@ -0,0 +1,31 @@ +import reverb +import tensorflow as tf +import numpy as np + +OBSERVATION_SPEC = tf.TensorSpec([10, 10], tf.uint8) +ACTION_SPEC = tf.TensorSpec([2], tf.float32) + +def agent_step(unused_timestep) -> tf.Tensor: + return tf.cast(tf.random.uniform(ACTION_SPEC.shape) > .5, + ACTION_SPEC.dtype) + +def environment_step(unused_action) -> tf.Tensor: + return tf.cast(tf.random.uniform(OBSERVATION_SPEC.shape, maxval=256), + OBSERVATION_SPEC.dtype) + + +# Initialize the reverb server. +simple_server = reverb.Server( + tables=[ + reverb.Table( + name='my_table', + sampler=reverb.selectors.Prioritized(priority_exponent=0.8), + remover=reverb.selectors.Fifo(), + max_size=100, + rate_limiter=reverb.rate_limiters.MinSize(1)), + ], + # Sets the port to None to make the server pick one automatically. + # This can be omitted as it's the default. + port=8888) + +simple_server.wait() \ No newline at end of file diff --git a/others/chal-reverb/challenge/start.sh b/others/chal-reverb/challenge/start.sh new file mode 100644 index 00000000..f942eee6 --- /dev/null +++ b/others/chal-reverb/challenge/start.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +python3 script.py > /logs/$(date +%s).log 2>&1 & +flask --app app run > /logs/$(date +%s).log 2>&1 & + +# Proxy stdin/stdout to server +socat - TCP:127.0.0.1:5000,forever \ No newline at end of file diff --git a/others/chal-reverb/healthcheck/Dockerfile b/others/chal-reverb/healthcheck/Dockerfile new file mode 100644 index 00000000..7e8e32b1 --- /dev/null +++ b/others/chal-reverb/healthcheck/Dockerfile @@ -0,0 +1,18 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +FROM europe-west4-docker.pkg.dev/internet-ctf/challenges/healthcheck_base + +COPY healthcheck_loop.sh healthcheck.py healthz_webserver.py /home/user/ + +CMD kctf_drop_privs /home/user/healthcheck_loop.sh & /home/user/healthz_webserver.py diff --git a/others/chal-reverb/healthcheck/README.md b/others/chal-reverb/healthcheck/README.md new file mode 100644 index 00000000..8dbcd6a8 --- /dev/null +++ b/others/chal-reverb/healthcheck/README.md @@ -0,0 +1,14 @@ +# Healthcheck + +kCTF checks the health of challenges by accessing the healthcheck via +http://host:45281/healthz which needs to return either 200 ok or an error +depending on the status of the challenge. + +The default healthcheck consists of: +* a loop that repeatedly calls a python script and writes the status to a file +* a webserver that checks the file and serves /healthz +* the actual healthcheck code using pwntools for convenience + +To modify it, you will likely only have to change the script in healthcheck.py. +You can test if the challenge replies as expected or better add a full example +solution that will try to get the flag from the challenge. diff --git a/others/chal-reverb/healthcheck/healthcheck.py b/others/chal-reverb/healthcheck/healthcheck.py new file mode 100644 index 00000000..1a10061d --- /dev/null +++ b/others/chal-reverb/healthcheck/healthcheck.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import requests + +response = requests.get("http://localhost:1337") + +if response.status_code == 200: + print("Reverb server is healthy") + exit(0) +else: + print("Reverb server is not healthy") + exit(1) diff --git a/others/chal-reverb/healthcheck/healthcheck_loop.sh b/others/chal-reverb/healthcheck/healthcheck_loop.sh new file mode 100644 index 00000000..acf69158 --- /dev/null +++ b/others/chal-reverb/healthcheck/healthcheck_loop.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +set -Eeuo pipefail + +TIMEOUT=20 +PERIOD=30 + +export TERM=linux +export TERMINFO=/etc/terminfo + +while true; do + echo -n "[$(date)] " + if timeout "${TIMEOUT}" /home/user/healthcheck.py; then + echo 'ok' | tee /tmp/healthz + else + echo -n "$? " + echo 'err' | tee /tmp/healthz + fi + sleep "${PERIOD}" +done diff --git a/others/chal-reverb/healthcheck/healthz_webserver.py b/others/chal-reverb/healthcheck/healthz_webserver.py new file mode 100644 index 00000000..62cf0198 --- /dev/null +++ b/others/chal-reverb/healthcheck/healthz_webserver.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import http.server + +class HealthzHandler(http.server.BaseHTTPRequestHandler): + def do_GET(self): + if self.path != '/healthz': + self.send_response(404) + self.send_header("Content-length", "0") + self.end_headers() + return + + content = b'err' + try: + with open('/tmp/healthz', 'rb') as fd: + content = fd.read().strip() + except: + pass + self.send_response(200 if content == b'ok' else 400) + self.send_header("Content-type", "text/plain") + self.send_header("Content-length", str(len(content))) + self.end_headers() + self.wfile.write(content) + +httpd = http.server.HTTPServer(('', 45281), HealthzHandler) +httpd.serve_forever()