[v8ctf] Update v8CTF challenges

This commit is contained in:
v8CTF github action
2025-06-25 08:18:47 +00:00
parent 8ab623118e
commit 446794ebe3
11 changed files with 383 additions and 0 deletions
+55
View File
@@ -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 <tab>` 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.
+27
View File
@@ -0,0 +1,27 @@
apiVersion: kctf.dev/v1
kind: Challenge
metadata:
name: chrome-138
spec:
deployed: true
powDifficultySeconds: 1
network:
public: true
healthcheck:
# TIP: disable the healthcheck during development
enabled: true
podTemplate:
template:
spec:
containers:
- name: challenge
volumeMounts:
- name: flag
mountPath: /chroot/flag
readOnly: true
volumes:
- name: flag
secret:
defaultMode: 0555
secretName: v8ctf-flag
optional: true
+57
View File
@@ -0,0 +1,57 @@
# 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 ubuntu:22.04 as chroot
RUN /usr/sbin/useradd -u 1000 user
RUN apt-get update && apt-get install -y gnupg2 wget
# Install latest chrome dev package and fonts to support major charsets (Chinese, Japanese, Arabic, Hebrew, Thai and a few others)
# Note: this installs the necessary libs to make the bundled version of Chromium that Puppeteer installs, work.
# Deps from https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md#chrome-headless-doesnt-launch-on-unix
# plus libxshmfence1 which seems to be missing
RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
&& sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
&& apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
google-chrome-stable \
&& rm -rf /var/lib/apt/lists/*
RUN apt-get update && apt-get install -y unzip
RUN mkdir /home/user
COPY version /tmp/version
RUN wget "https://storage.googleapis.com/chrome-for-testing-public/$(cat /tmp/version)/linux64/chrome-linux64.zip" -O /opt/chrome-linux.zip
RUN rm /tmp/version
RUN cd /opt && unzip chrome-linux.zip && rm chrome-linux.zip
COPY chal /opt/
FROM gcr.io/kctf-docker/challenge@sha256:0f7d757bcda470c3bbc063606335b915e03795d72ba1d8fdb6f0f9ff3757364f
COPY --from=chroot / /chroot
RUN mkdir /chroot/dev/shm
RUN touch /chroot/dev/null
RUN touch /chroot/dev/zero
RUN touch /chroot/dev/urandom
RUN mkdir /chroot/run/dbus
COPY nsjail.cfg /home/user/
CMD kctf_setup && \
kctf_drop_privs \
socat \
TCP-LISTEN:1337,reuseaddr,fork \
EXEC:"kctf_pow nsjail --config /home/user/nsjail.cfg -- /opt/chal",stderr
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/bash
CHROME=/opt/chrome-linux64/chrome
echo "Version: $($CHROME --version | head -n1)"
echo "Please send me a URL to open."
read -r url
if ! echo $url | grep -E '^https?://[A-Za-z0-9.:/?%\-_+&=]*$' -q; then
echo 'url regex fail'
exit 1
fi
dbus-daemon --system
dbus-run-session -- $CHROME --headless=new --no-sandbox --disable-crashpad --disable-breakpad --disable-crash-reporter --enable-logging=stderr --user-data-dir=/home/user "${url}"
+69
View File
@@ -0,0 +1,69 @@
# 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"}
disable_rl: true
clone_newnet: false
cwd: "/home/user"
mount: [
{
src: "/chroot"
dst: "/"
is_bind: true
},
{
src: "/dev"
dst: "/dev"
is_bind: true
},
{
dst: "/tmp"
fstype: "tmpfs"
rw: true
},
{
dst: "/home/user"
fstype: "tmpfs"
rw: true
},
{
dst: "/run/dbus"
fstype: "tmpfs"
rw: true
},
{
dst: "/run/user"
fstype: "tmpfs"
rw: true
},
{
dst: "/proc"
fstype: "proc"
rw: true
},
{
src: "/etc/resolv.conf"
dst: "/etc/resolv.conf"
is_bind: true
}
]
+1
View File
@@ -0,0 +1 @@
138.0.7204.49
+18
View File
@@ -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 gcr.io/kctf-docker/healthcheck@sha256:6709709a8cfd6e2d743c86d58398c00ca4eb26befd3b1a0a629ab35f91e98ef0
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
+14
View File
@@ -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.
+57
View File
@@ -0,0 +1,57 @@
#!/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 socket
from pwn import *
def handle_pow(r):
print(r.recvuntil(b'python3 '))
print(r.recvuntil(b' solve '))
challenge = r.recvline().decode('ascii').strip()
p = process(['kctf_bypass_pow', challenge])
solution = p.readall().strip()
r.sendline(solution)
print(r.recvuntil(b'Correct\n'))
r = remote('127.0.0.1', 1337)
print(r.recvuntil(b'== proof-of-work: '))
if r.recvline().startswith(b'enabled'):
handle_pow(r)
server1 = server()
server2 = server()
r.readuntil(b'URL to open.', timeout=10)
r.sendline(bytes('http://localhost:{}/ok'.format(server1.lport), 'ascii'))
def wait_for_line(serv, line, maxtries=6):
for i in range(maxtries):
conn = serv.next_connection()
try:
print(conn.readuntil(line))
return conn
except EOFError:
print('eof')
raise 'no GET /ok found'
conn = wait_for_line(server1, b'GET /ok HTTP/1.1')
content = f"<script>fetch('http://localhost:{server2.lport}/foo')</script>"
response = f'HTTP/1.1 200 OK\nContent-Length: {len(content)}\n\n{content}'
conn.send(response.encode())
wait_for_line(server2, b'GET /foo HTTP/1.1')
exit(0)
+32
View File
@@ -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
+39
View File
@@ -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()