Skip to content

Commit 7e44276

Browse files
committed
[v8ctf] chrome 123
1 parent 60533ec commit 7e44276

10 files changed

+374
-0
lines changed

v8ctf/chrome/README.md

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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 <tab>` 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.

v8ctf/chrome/challenge.yaml

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
apiVersion: kctf.dev/v1
2+
kind: Challenge
3+
metadata:
4+
name: chrome
5+
spec:
6+
deployed: true
7+
powDifficultySeconds: 1
8+
network:
9+
public: true
10+
healthcheck:
11+
# TIP: disable the healthcheck during development
12+
enabled: true
13+
podTemplate:
14+
template:
15+
spec:
16+
containers:
17+
- name: challenge
18+
volumeMounts:
19+
- name: flag
20+
mountPath: /chroot/flag
21+
readOnly: true
22+
volumes:
23+
- name: flag
24+
secret:
25+
defaultMode: 0555
26+
secretName: v8ctf-flag
27+
optional: true

v8ctf/chrome/challenge/Dockerfile

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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 ubuntu:22.04 as chroot
15+
16+
RUN /usr/sbin/useradd -u 1000 user
17+
18+
RUN apt-get update && apt-get install -y gnupg2 wget
19+
20+
# Install latest chrome dev package and fonts to support major charsets (Chinese, Japanese, Arabic, Hebrew, Thai and a few others)
21+
# Note: this installs the necessary libs to make the bundled version of Chromium that Puppeteer installs, work.
22+
# Deps from https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md#chrome-headless-doesnt-launch-on-unix
23+
# plus libxshmfence1 which seems to be missing
24+
RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
25+
&& sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
26+
&& apt-get update \
27+
&& DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
28+
google-chrome-stable \
29+
&& rm -rf /var/lib/apt/lists/*
30+
31+
RUN apt-get update && apt-get install -y unzip
32+
33+
RUN mkdir /home/user
34+
# This version is to be released on 2024-03-22
35+
RUN wget 'https://storage.googleapis.com/chrome-for-testing-public/123.0.6312.58/linux64/chrome-linux64.zip' -O /opt/chrome-linux.zip
36+
RUN cd /opt && unzip chrome-linux.zip && rm chrome-linux.zip
37+
38+
COPY chal /opt/
39+
40+
FROM gcr.io/kctf-docker/challenge@sha256:0f7d757bcda470c3bbc063606335b915e03795d72ba1d8fdb6f0f9ff3757364f
41+
42+
COPY --from=chroot / /chroot
43+
RUN mkdir /chroot/dev/shm
44+
RUN touch /chroot/dev/null
45+
RUN touch /chroot/dev/zero
46+
RUN touch /chroot/dev/urandom
47+
48+
RUN mkdir /chroot/run/dbus
49+
50+
COPY nsjail.cfg /home/user/
51+
52+
CMD kctf_setup && \
53+
kctf_drop_privs \
54+
socat \
55+
TCP-LISTEN:1337,reuseaddr,fork \
56+
EXEC:"kctf_pow nsjail --config /home/user/nsjail.cfg -- /opt/chal",stderr

v8ctf/chrome/challenge/chal

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/usr/bin/bash
2+
3+
CHROME=/opt/chrome-linux64/chrome
4+
5+
echo "Version: $($CHROME --version | head -n1)"
6+
echo "Please send me a URL to open."
7+
read -r url
8+
if ! echo $url | grep -E '^https?://[A-Za-z0-9.:/?%\-_+&=]*$' -q; then
9+
echo 'url regex fail'
10+
exit 1
11+
fi
12+
13+
dbus-daemon --system
14+
dbus-run-session -- $CHROME --headless=new --no-sandbox --disable-crashpad --disable-breakpad --disable-crash-reporter --enable-logging=stderr "${url}"

v8ctf/chrome/challenge/nsjail.cfg

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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+
disable_rl: true
24+
clone_newnet: false
25+
26+
cwd: "/home/user"
27+
28+
mount: [
29+
{
30+
src: "/chroot"
31+
dst: "/"
32+
is_bind: true
33+
},
34+
{
35+
src: "/dev"
36+
dst: "/dev"
37+
is_bind: true
38+
},
39+
{
40+
dst: "/tmp"
41+
fstype: "tmpfs"
42+
rw: true
43+
},
44+
{
45+
dst: "/home/user"
46+
fstype: "tmpfs"
47+
rw: true
48+
},
49+
{
50+
dst: "/run/dbus"
51+
fstype: "tmpfs"
52+
rw: true
53+
},
54+
{
55+
dst: "/run/user"
56+
fstype: "tmpfs"
57+
rw: true
58+
},
59+
{
60+
dst: "/proc"
61+
fstype: "proc"
62+
rw: true
63+
},
64+
{
65+
src: "/etc/resolv.conf"
66+
dst: "/etc/resolv.conf"
67+
is_bind: true
68+
}
69+
]

v8ctf/chrome/healthcheck/Dockerfile

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
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:6709709a8cfd6e2d743c86d58398c00ca4eb26befd3b1a0a629ab35f91e98ef0
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

v8ctf/chrome/healthcheck/README.md

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
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.
+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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 socket
18+
from pwn import *
19+
20+
def handle_pow(r):
21+
print(r.recvuntil(b'python3 '))
22+
print(r.recvuntil(b' solve '))
23+
challenge = r.recvline().decode('ascii').strip()
24+
p = process(['kctf_bypass_pow', challenge])
25+
solution = p.readall().strip()
26+
r.sendline(solution)
27+
print(r.recvuntil(b'Correct\n'))
28+
29+
r = remote('127.0.0.1', 1337)
30+
print(r.recvuntil(b'== proof-of-work: '))
31+
if r.recvline().startswith(b'enabled'):
32+
handle_pow(r)
33+
34+
l = listen()
35+
l2 = listen()
36+
37+
r.readuntil(b'URL to open.', timeout=10)
38+
r.sendline(bytes('http://localhost:{}/ok'.format(l.lport), 'ascii'))
39+
40+
_ = l.wait_for_connection()
41+
42+
print(l.readuntil(b'GET /ok HTTP/1.1'))
43+
content = f"<script>fetch('http://localhost:{l2.lport}/foo')</script>"
44+
response = f'HTTP/1.1 200 OK\nContent-Length: {len(content)}\n\n{content}'
45+
l.send(response.encode())
46+
47+
_ = l2.wait_for_connection()
48+
print(l2.readuntil(b'GET /foo HTTP/1.1'))
49+
50+
exit(0)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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()

0 commit comments

Comments
 (0)