mirror of
https://github.com/google/security-research
synced 2026-06-08 14:27:23 +00:00
[v8ctf] Update v8CTF challenges
This commit is contained in:
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
Executable
+14
@@ -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}"
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
138.0.7204.49
|
||||
@@ -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
|
||||
@@ -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.
|
||||
Executable
+57
@@ -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
@@ -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
@@ -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()
|
||||
Reference in New Issue
Block a user