Skip to content

Commit

Permalink
[v8ctf] M125 release
Browse files Browse the repository at this point in the history
  • Loading branch information
sroettger committed May 16, 2024
1 parent 5f123f9 commit 839fa04
Show file tree
Hide file tree
Showing 11 changed files with 376 additions and 2 deletions.
55 changes: 55 additions & 0 deletions v8ctf/chrome-124/README.md
Original file line number Diff line number Diff line change
@@ -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 changes: 27 additions & 0 deletions v8ctf/chrome-124/challenge.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
apiVersion: kctf.dev/v1
kind: Challenge
metadata:
name: chrome-124
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
56 changes: 56 additions & 0 deletions v8ctf/chrome-124/challenge/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# 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
# This version is to be released on 2024-04-19
RUN wget 'https://storage.googleapis.com/chrome-for-testing-public/124.0.6367.60/linux64/chrome-linux64.zip' -O /opt/chrome-linux.zip
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 changes: 14 additions & 0 deletions v8ctf/chrome-124/challenge/chal
Original file line number Diff line number Diff line change
@@ -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 "${url}"
69 changes: 69 additions & 0 deletions v8ctf/chrome-124/challenge/nsjail.cfg
Original file line number Diff line number Diff line change
@@ -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
}
]
18 changes: 18 additions & 0 deletions v8ctf/chrome-124/healthcheck/Dockerfile
Original file line number Diff line number Diff line change
@@ -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 changes: 14 additions & 0 deletions v8ctf/chrome-124/healthcheck/README.md
Original file line number Diff line number Diff line change
@@ -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.
50 changes: 50 additions & 0 deletions v8ctf/chrome-124/healthcheck/healthcheck.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/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)

l = listen()
l2 = listen()

r.readuntil(b'URL to open.', timeout=10)
r.sendline(bytes('http://localhost:{}/ok'.format(l.lport), 'ascii'))

_ = l.wait_for_connection()

print(l.readuntil(b'GET /ok HTTP/1.1'))
content = f"<script>fetch('http://localhost:{l2.lport}/foo')</script>"
response = f'HTTP/1.1 200 OK\nContent-Length: {len(content)}\n\n{content}'
l.send(response.encode())

_ = l2.wait_for_connection()
print(l2.readuntil(b'GET /foo HTTP/1.1'))

exit(0)
32 changes: 32 additions & 0 deletions v8ctf/chrome-124/healthcheck/healthcheck_loop.sh
Original file line number Diff line number Diff line change
@@ -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 changes: 39 additions & 0 deletions v8ctf/chrome-124/healthcheck/healthz_webserver.py
Original file line number Diff line number Diff line change
@@ -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()
4 changes: 2 additions & 2 deletions v8ctf/chrome/challenge/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key
RUN apt-get update && apt-get install -y unzip

RUN mkdir /home/user
# This version is to be released on 2024-04-19
RUN wget 'https://storage.googleapis.com/chrome-for-testing-public/124.0.6367.60/linux64/chrome-linux64.zip' -O /opt/chrome-linux.zip
# This version is to be released on 2024-05-17
RUN wget 'https://storage.googleapis.com/chrome-for-testing-public/125.0.6422.60/linux64/chrome-linux64.zip' -O /opt/chrome-linux.zip
RUN cd /opt && unzip chrome-linux.zip && rm chrome-linux.zip

COPY chal /opt/
Expand Down

0 comments on commit 839fa04

Please sign in to comment.