diff --git a/.github/workflows/configure-challs.yml b/.github/workflows/configure-challs.yml
index a733728..67c8352 100644
--- a/.github/workflows/configure-challs.yml
+++ b/.github/workflows/configure-challs.yml
@@ -7,9 +7,9 @@ on:
workflow_dispatch:
jobs:
- create_challenge:
- name: Create challenge
- uses: ctfpilot/challenge-ci/.github/workflows/configure-challs.yml@v1.0.0
+ configure_challenges:
+ name: Configure challenges
+ uses: ctfpilot/challenge-ci/.github/workflows/configure-challs.yml@v1.0.2
permissions:
contents: write
packages: write
diff --git a/.github/workflows/configure-pages.yml b/.github/workflows/configure-pages.yml
index bd410a3..627f398 100644
--- a/.github/workflows/configure-pages.yml
+++ b/.github/workflows/configure-pages.yml
@@ -9,9 +9,9 @@ on:
jobs:
create_pages:
name: Create pages
- uses: ctfpilot/challenge-ci/.github/workflows/configure-pages.yml@v1.0.0
+ uses: ctfpilot/challenge-ci/.github/workflows/configure-pages.yml@v1.0.2
permissions:
contents: write
with:
runs-on: "ubuntu-latest"
- toolkit-path: "./challenge-toolkit/"
\ No newline at end of file
+ toolkit-path: "./challenge-toolkit/"
diff --git a/.github/workflows/create-chall.yml b/.github/workflows/create-chall.yml
index 5e932b6..486f134 100644
--- a/.github/workflows/create-chall.yml
+++ b/.github/workflows/create-chall.yml
@@ -81,7 +81,7 @@ on:
jobs:
create_challenge:
name: Create challenge
- uses: ctfpilot/challenge-ci/.github/workflows/create-chall.yml@v1.0.0
+ uses: ctfpilot/challenge-ci/.github/workflows/create-chall.yml@v1.0.2
permissions:
contents: write
pull-requests: write
diff --git a/.github/workflows/render-templates.yml b/.github/workflows/render-templates.yml
index 8213889..92aa725 100644
--- a/.github/workflows/render-templates.yml
+++ b/.github/workflows/render-templates.yml
@@ -6,7 +6,7 @@ on:
jobs:
render_templates:
name: Render all templates
- uses: ctfpilot/challenge-ci/.github/workflows/render-templates.yml@v1.0.0
+ uses: ctfpilot/challenge-ci/.github/workflows/render-templates.yml@v1.0.2
permissions:
contents: write
packages: write
diff --git a/README.md b/README.md
index 89ffb70..32ee6c8 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,12 @@
# Challenges example repository
-This repository contains an example challenges repository, for CTF Pilot's Challenge Repository Template
+> [!IMPORTANT]
+> This is an example repository of the [CTF Pilot's Challenge Repository Template](https://github.com/ctfpilot/challenges-template).
+> It is intended to be used as a reference for creating your own challenges repository, and testing the infrastructure with some example challenges.
+>
+> **Please use the [Challenge Repository Template](https://github.com/ctfpilot/challenges-template) if you want to create your own challenges repository.**
+
+This repository contains an example challenges repository, for [CTF Pilot's Challenge Repository Template](https://github.com/ctfpilot/challenges-template).
## Challenge toolkit
@@ -205,3 +211,8 @@ To create a new challenge, follow these steps:
7. Once the workflow is complete, a new branch, pull request, and issue will be created for the challenge.
If errors occur during the workflow execution, please reach out to the infrastructure team for assistance. Logs from the workflow can be found in the `Actions` tab of the repository.
+
+## Project board
+
+A project board is available to track the progress of challenges.
+This can be found at: [Challenges Project board](https://github.com/orgs/ctfpilot/projects/2)
diff --git a/challenge-toolkit b/challenge-toolkit
index 4a714ce..38b0360 160000
--- a/challenge-toolkit
+++ b/challenge-toolkit
@@ -1 +1 @@
-Subproject commit 4a714ceecae83031b30ccf599381a22a4ac59743
+Subproject commit 38b03600b49b24180dae1aa66913664018c7a35a
diff --git a/challenges/forensics/oh-look-a-flag/README.md b/challenges/forensics/oh-look-a-flag/README.md
new file mode 100644
index 0000000..1801b93
--- /dev/null
+++ b/challenges/forensics/oh-look-a-flag/README.md
@@ -0,0 +1,6 @@
+# Oh look, A flag!
+
+This challenge is an example challenge for testing the handout system.
+
+It contains a single text file, which contains a flag.
+In the infrastructure, this file should be automatically zipped into a handout file, and uploaded to the platform.
diff --git a/challenges/test/example/challenge.yml b/challenges/forensics/oh-look-a-flag/challenge.yml
similarity index 67%
rename from challenges/test/example/challenge.yml
rename to challenges/forensics/oh-look-a-flag/challenge.yml
index bd58f01..940e5cd 100644
--- a/challenges/test/example/challenge.yml
+++ b/challenges/forensics/oh-look-a-flag/challenge.yml
@@ -1,23 +1,26 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/ctfpilot/challenge-schema/refs/heads/main/schema.json
enabled: true
-name: Example
-slug: example
+name: Oh look, A flag!
+slug: oh-look-a-flag
author: The Mikkel
-category: test
-difficulty: easy
-tags: []
+category: forensics
+difficulty: beginner
+tags: [
+ Static,
+ Example
+]
type: static
instanced_type: none
instanced_name: null
instanced_subdomains: []
connection: null
flag:
-- flag: test{demo}
+- flag: ctfpilot{handouts-may-contain-flag}
case_sensitive: false
points: 1000
decay: 75
-min_points: 10
+min_points: 100
description_location: description.md
handout_dir: handout
diff --git a/challenges/forensics/oh-look-a-flag/description.md b/challenges/forensics/oh-look-a-flag/description.md
new file mode 100644
index 0000000..cfcf6ef
--- /dev/null
+++ b/challenges/forensics/oh-look-a-flag/description.md
@@ -0,0 +1,6 @@
+# Oh look, A flag!
+
+**Difficulty:** Beginner
+**Author:** The Mikkel
+
+Handouts may contain flags, but does this one?
diff --git a/challenges/test/example/handout/.gitkeep b/challenges/forensics/oh-look-a-flag/handout/.gitkeep
similarity index 100%
rename from challenges/test/example/handout/.gitkeep
rename to challenges/forensics/oh-look-a-flag/handout/.gitkeep
diff --git a/challenges/forensics/oh-look-a-flag/handout/handout.txt b/challenges/forensics/oh-look-a-flag/handout/handout.txt
new file mode 100644
index 0000000..ede38af
--- /dev/null
+++ b/challenges/forensics/oh-look-a-flag/handout/handout.txt
@@ -0,0 +1,2 @@
+Alright, here is the flag:
+ctfpilot{handouts-may-contain-flag}
diff --git a/challenges/forensics/oh-look-a-flag/k8s/config/Chart.yaml b/challenges/forensics/oh-look-a-flag/k8s/config/Chart.yaml
new file mode 100644
index 0000000..a8c42fc
--- /dev/null
+++ b/challenges/forensics/oh-look-a-flag/k8s/config/Chart.yaml
@@ -0,0 +1,6 @@
+apiVersion: v2
+name: configmap-oh-look-a-flag
+version: 1.5.0
+description: Challenge configmap for oh-look-a-flag in category forensics
+appVersion: "1.5.0"
+type: application
diff --git a/challenges/test/example/k8s/config/templates/k8s.yml b/challenges/forensics/oh-look-a-flag/k8s/config/templates/k8s.yml
similarity index 59%
rename from challenges/test/example/k8s/config/templates/k8s.yml
rename to challenges/forensics/oh-look-a-flag/k8s/config/templates/k8s.yml
index cccd3ca..3a6423e 100644
--- a/challenges/test/example/k8s/config/templates/k8s.yml
+++ b/challenges/forensics/oh-look-a-flag/k8s/config/templates/k8s.yml
@@ -1,30 +1,33 @@
apiVersion: v1
kind: ConfigMap
metadata:
- name: "challenge-test-example"
+ name: "challenge-forensics-oh-look-a-flag"
labels:
challenges.ctfpilot.com/type: "none"
- challenges.ctfpilot.com/name: "example"
- challenges.ctfpilot.com/category: "test"
+ challenges.ctfpilot.com/name: "oh-look-a-flag"
+ challenges.ctfpilot.com/category: "forensics"
challenges.ctfpilot.com/version: "5"
challenges.ctfpilot.com/configmap: "challenge-config"
challenges.ctfpilot.com/enabled: "true"
ctfpilot.com/component: "challenge-config"
data:
- name: "example"
- path: "challenges/test/example"
+ name: "oh-look-a-flag"
+ path: "challenges/forensics/oh-look-a-flag"
repository: "ctfpilot/challenges-example"
- generated_at: "2025-12-20 10:41:26"
+ generated_at: "2025-12-20 15:54:13"
challenge: |
{
"$schema": "https://raw.githubusercontent.com/ctfpilot/challenge-schema/refs/heads/main/schema.json",
"enabled": true,
- "name": "Example",
- "slug": "example",
+ "name": "Oh look, A flag!",
+ "slug": "oh-look-a-flag",
"author": "The Mikkel",
- "category": "test",
- "difficulty": "easy",
- "tags": [],
+ "category": "forensics",
+ "difficulty": "beginner",
+ "tags": [
+ "Static",
+ "Example"
+ ],
"type": "static",
"instanced_type": "none",
"instanced_name": null,
@@ -32,22 +35,22 @@ data:
"connection": null,
"flag": [
{
- "flag": "test{demo}",
+ "flag": "ctfpilot{handouts-may-contain-flag}",
"case_sensitive": false
}
],
"points": 1000,
"decay": 75,
- "min_points": 10,
+ "min_points": 100,
"description_location": "description.md",
"handout_dir": "handout"
}
description: |
- # Example
+ # Oh look, A flag!
- **Difficulty:** Easy
+ **Difficulty:** Beginner
**Author:** The Mikkel
- *Add challenge description here*
+ Handouts may contain flags, but does this one?
diff --git a/challenges/test/example/k8s/config/values.yaml b/challenges/forensics/oh-look-a-flag/k8s/config/values.yaml
similarity index 56%
rename from challenges/test/example/k8s/config/values.yaml
rename to challenges/forensics/oh-look-a-flag/k8s/config/values.yaml
index 81cf4bb..fb2a662 100644
--- a/challenges/test/example/k8s/config/values.yaml
+++ b/challenges/forensics/oh-look-a-flag/k8s/config/values.yaml
@@ -1,10 +1,10 @@
challenge:
enabled: true
- name: example
- category: test
+ name: oh-look-a-flag
+ category: forensics
type: none
version: 5
- path: challenges/test/example
+ path: challenges/forensics/oh-look-a-flag
kubectf:
expires: 3600
availableAt: 0
diff --git a/challenges/test/example/k8s/files/.gitkeep b/challenges/forensics/oh-look-a-flag/k8s/files/.gitkeep
similarity index 100%
rename from challenges/test/example/k8s/files/.gitkeep
rename to challenges/forensics/oh-look-a-flag/k8s/files/.gitkeep
diff --git a/challenges/forensics/oh-look-a-flag/k8s/files/forensics_oh-look-a-flag.zip b/challenges/forensics/oh-look-a-flag/k8s/files/forensics_oh-look-a-flag.zip
new file mode 100644
index 0000000..64862e0
Binary files /dev/null and b/challenges/forensics/oh-look-a-flag/k8s/files/forensics_oh-look-a-flag.zip differ
diff --git a/challenges/forensics/oh-look-a-flag/solution/README.md b/challenges/forensics/oh-look-a-flag/solution/README.md
new file mode 100644
index 0000000..b860944
--- /dev/null
+++ b/challenges/forensics/oh-look-a-flag/solution/README.md
@@ -0,0 +1,3 @@
+# Solution
+
+Download the handout file and extract it. Inside, you will find a text file containing the flag.
diff --git a/challenges/test/example/src/.gitkeep b/challenges/forensics/oh-look-a-flag/src/.gitkeep
similarity index 100%
rename from challenges/test/example/src/.gitkeep
rename to challenges/forensics/oh-look-a-flag/src/.gitkeep
diff --git a/challenges/test/example/version b/challenges/forensics/oh-look-a-flag/version
similarity index 100%
rename from challenges/test/example/version
rename to challenges/forensics/oh-look-a-flag/version
diff --git a/challenges/test/example/README.md b/challenges/test/example/README.md
deleted file mode 100644
index 8a2278f..0000000
--- a/challenges/test/example/README.md
+++ /dev/null
@@ -1,4 +0,0 @@
-# Example
-
-*Add information about challenge here*
-*It is meant to contain internal documentation of the challenge, such as how it is solved*
diff --git a/challenges/test/example/description.md b/challenges/test/example/description.md
deleted file mode 100644
index 1d07ceb..0000000
--- a/challenges/test/example/description.md
+++ /dev/null
@@ -1,6 +0,0 @@
-# Example
-
-**Difficulty:** Easy
-**Author:** The Mikkel
-
-*Add challenge description here*
diff --git a/challenges/test/example/k8s/config/Chart.yaml b/challenges/test/example/k8s/config/Chart.yaml
deleted file mode 100644
index 1c246e7..0000000
--- a/challenges/test/example/k8s/config/Chart.yaml
+++ /dev/null
@@ -1,6 +0,0 @@
-apiVersion: v2
-name: configmap-example
-version: 1.5.0
-description: Challenge configmap for example in category test
-appVersion: "1.5.0"
-type: application
diff --git a/challenges/test/example/solution/README.md b/challenges/test/example/solution/README.md
deleted file mode 100644
index 8614e07..0000000
--- a/challenges/test/example/solution/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Solution
-This directory is used to store the solution script for the challenge.
-This file should contain the steps to solve the challenge.
\ No newline at end of file
diff --git a/challenges/web/the-shared-site/README.md b/challenges/web/the-shared-site/README.md
new file mode 100644
index 0000000..e4f2f17
--- /dev/null
+++ b/challenges/web/the-shared-site/README.md
@@ -0,0 +1,3 @@
+# The shared site
+
+This challenge is an example challenge, that is meant to demonstrate how shared challenges work.
diff --git a/challenges/web/the-shared-site/challenge.yml b/challenges/web/the-shared-site/challenge.yml
new file mode 100644
index 0000000..2258613
--- /dev/null
+++ b/challenges/web/the-shared-site/challenge.yml
@@ -0,0 +1,30 @@
+# yaml-language-server: $schema=https://raw.githubusercontent.com/ctfpilot/challenge-schema/refs/heads/main/schema.json
+
+enabled: true
+name: The shared site
+slug: the-shared-site
+author: The Mikkel
+category: web
+difficulty: beginner
+tags: [
+ Shared,
+ Example
+]
+type: shared
+instanced_type: web
+instanced_name: null
+instanced_subdomains: []
+connection: null
+flag:
+- flag: ctfpilot{thanks-for-visiting-the-shared-site}
+ case_sensitive: false
+points: 1000
+decay: 75
+min_points: 100
+description_location: description.md
+handout_dir: handout
+dockerfile_locations:
+- location: src/Dockerfile
+ context: src/
+ identifier: null
+
diff --git a/challenges/web/the-shared-site/description.md b/challenges/web/the-shared-site/description.md
new file mode 100644
index 0000000..42c7e0f
--- /dev/null
+++ b/challenges/web/the-shared-site/description.md
@@ -0,0 +1,6 @@
+# The shared site
+
+**Difficulty:** Beginner
+**Author:** The Mikkel
+
+The site may contain the precious object you are looking for.
diff --git a/challenges/web/the-shared-site/handout/.gitkeep b/challenges/web/the-shared-site/handout/.gitkeep
new file mode 100644
index 0000000..0f065bc
--- /dev/null
+++ b/challenges/web/the-shared-site/handout/.gitkeep
@@ -0,0 +1,2 @@
+# This file is used to keep the directory in the repository.
+# This directory is used to store files that are handed out, for the challenge. The files are automatically zipped and copied to the files directory.
\ No newline at end of file
diff --git a/challenges/web/the-shared-site/k8s/challenge/Chart.yaml b/challenges/web/the-shared-site/k8s/challenge/Chart.yaml
new file mode 100644
index 0000000..14d17f4
--- /dev/null
+++ b/challenges/web/the-shared-site/k8s/challenge/Chart.yaml
@@ -0,0 +1,6 @@
+apiVersion: v2
+name: the-shared-site
+version: 1.3.0
+description: Challenge the-shared-site in category web
+appVersion: "1.3.0"
+type: application
diff --git a/challenges/web/the-shared-site/k8s/challenge/templates/k8s.yml b/challenges/web/the-shared-site/k8s/challenge/templates/k8s.yml
new file mode 100644
index 0000000..d068608
--- /dev/null
+++ b/challenges/web/the-shared-site/k8s/challenge/templates/k8s.yml
@@ -0,0 +1,121 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: "ctf-web-the-shared-site"
+ namespace: kubectf-challenges
+ labels:
+ challenges.ctfpilot.com/type: "web"
+ challenges.ctfpilot.com/name: "the-shared-site"
+ challenges.ctfpilot.com/category: "web"
+ ctfpilot.com/component: "shared-challenge"
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ challenges.ctfpilot.com/type: "web"
+ challenges.ctfpilot.com/name: "the-shared-site"
+ challenges.ctfpilot.com/category: "web"
+ template:
+ metadata:
+ labels:
+ challenges.ctfpilot.com/type: "web"
+ challenges.ctfpilot.com/name: "the-shared-site"
+ challenges.ctfpilot.com/category: "web"
+ ctfpilot.com/component: "shared-challenge"
+ spec:
+ enableServiceLinks: false
+ automountServiceAccountToken: false
+ imagePullSecrets:
+ - name: dockerconfigjson-github-com
+ dnsPolicy: None
+ dnsConfig:
+ nameservers:
+ - 1.1.1.1
+ - 8.8.8.8
+ tolerations:
+ - key: "cluster.ctfpilot.com/node"
+ value: "scaler"
+ effect: "PreferNoSchedule"
+ affinity:
+ nodeAffinity:
+ requiredDuringSchedulingIgnoredDuringExecution:
+ nodeSelectorTerms:
+ - matchExpressions:
+ - key: "cluster.ctfpilot.com/node"
+ operator: In
+ values:
+ - scaler
+ containers:
+ - name: web
+ image: ghcr.io/ctfpilot/challenges-example-web-the-shared-site:3
+ imagePullPolicy: IfNotPresent
+ resources:
+ limits:
+ cpu: 200m
+ memory: 256Mi
+ requests:
+ cpu: 10m
+ memory: 32Mi
+ ports:
+ - containerPort: 80
+ name: web-port
+ startupProbe:
+ httpGet:
+ path: /
+ port: web-port
+ failureThreshold: 12
+ periodSeconds: 10
+ readinessProbe:
+ httpGet:
+ path: /
+ port: web-port
+ initialDelaySeconds: 5
+ timeoutSeconds: 5
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: "ctf-web-the-shared-site"
+ namespace: kubectf-challenges
+ labels:
+ challenges.ctfpilot.com/type: "web"
+ challenges.ctfpilot.com/name: "the-shared-site"
+ challenges.ctfpilot.com/category: "web"
+ ctfpilot.com/component: "shared-challenge"
+spec:
+ selector:
+ challenges.ctfpilot.com/type: "web"
+ challenges.ctfpilot.com/name: "the-shared-site"
+ challenges.ctfpilot.com/category: "web"
+ ports:
+ - port: 80
+ targetPort: web-port
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: "ingress-ctf-web-the-shared-site"
+ namespace: kubectf-challenges
+ labels:
+ challenges.ctfpilot.com/type: "web"
+ challenges.ctfpilot.com/name: "the-shared-site"
+ challenges.ctfpilot.com/category: "web"
+ ctfpilot.com/component: "shared-challenge"
+ annotations:
+ traefik.ingress.kubernetes.io/router.middlewares: kubectf-instancing-fallback@kubernetescrd
+spec:
+ tls:
+ - hosts:
+ - "the-shared-site.{{ .Values.kubectf.host }}"
+ secretName: kubectf-cert-challs
+ rules:
+ - host: "the-shared-site.{{ .Values.kubectf.host }}"
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: "ctf-web-the-shared-site"
+ port:
+ number: 80
diff --git a/challenges/web/the-shared-site/k8s/challenge/values.yaml b/challenges/web/the-shared-site/k8s/challenge/values.yaml
new file mode 100644
index 0000000..2d12a4b
--- /dev/null
+++ b/challenges/web/the-shared-site/k8s/challenge/values.yaml
@@ -0,0 +1,12 @@
+challenge:
+ enabled: true
+ name: the-shared-site
+ category: web
+ type: web
+ version: 3
+ path: challenges/web/the-shared-site
+ dockerImage: web-the-shared-site
+kubectf:
+ expires: 3600
+ availableAt: 0
+ host: example.com
diff --git a/challenges/web/the-shared-site/k8s/config/Chart.yaml b/challenges/web/the-shared-site/k8s/config/Chart.yaml
new file mode 100644
index 0000000..5c3d680
--- /dev/null
+++ b/challenges/web/the-shared-site/k8s/config/Chart.yaml
@@ -0,0 +1,6 @@
+apiVersion: v2
+name: configmap-the-shared-site
+version: 1.3.0
+description: Challenge configmap for the-shared-site in category web
+appVersion: "1.3.0"
+type: application
diff --git a/challenges/web/the-shared-site/k8s/config/templates/k8s.yml b/challenges/web/the-shared-site/k8s/config/templates/k8s.yml
new file mode 100644
index 0000000..22f4edd
--- /dev/null
+++ b/challenges/web/the-shared-site/k8s/config/templates/k8s.yml
@@ -0,0 +1,63 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: "challenge-web-the-shared-site"
+ labels:
+ challenges.ctfpilot.com/type: "web"
+ challenges.ctfpilot.com/name: "the-shared-site"
+ challenges.ctfpilot.com/category: "web"
+ challenges.ctfpilot.com/version: "3"
+ challenges.ctfpilot.com/configmap: "challenge-config"
+ challenges.ctfpilot.com/enabled: "true"
+ ctfpilot.com/component: "challenge-config"
+data:
+ name: "the-shared-site"
+ path: "challenges/web/the-shared-site"
+ repository: "ctfpilot/challenges-example"
+ generated_at: "2025-12-20 15:52:29"
+ challenge: |
+ {
+ "$schema": "https://raw.githubusercontent.com/ctfpilot/challenge-schema/refs/heads/main/schema.json",
+ "enabled": true,
+ "name": "The shared site",
+ "slug": "the-shared-site",
+ "author": "The Mikkel",
+ "category": "web",
+ "difficulty": "beginner",
+ "tags": [
+ "Shared",
+ "Example"
+ ],
+ "type": "shared",
+ "instanced_type": "web",
+ "instanced_name": null,
+ "instanced_subdomains": [],
+ "connection": null,
+ "flag": [
+ {
+ "flag": "ctfpilot{thanks-for-visiting-the-shared-site}",
+ "case_sensitive": false
+ }
+ ],
+ "points": 1000,
+ "decay": 75,
+ "min_points": 100,
+ "description_location": "description.md",
+ "handout_dir": "handout",
+ "dockerfile_locations": [
+ {
+ "location": "src/Dockerfile",
+ "context": "src/",
+ "identifier": null
+ }
+ ]
+ }
+
+ description: |
+ # The shared site
+
+ **Difficulty:** Beginner
+ **Author:** The Mikkel
+
+ The site may contain the precious object you are looking for.
+
diff --git a/challenges/web/the-shared-site/k8s/config/values.yaml b/challenges/web/the-shared-site/k8s/config/values.yaml
new file mode 100644
index 0000000..f399ee3
--- /dev/null
+++ b/challenges/web/the-shared-site/k8s/config/values.yaml
@@ -0,0 +1,11 @@
+challenge:
+ enabled: true
+ name: the-shared-site
+ category: web
+ type: web
+ version: 3
+ path: challenges/web/the-shared-site
+kubectf:
+ expires: 3600
+ availableAt: 0
+ host: example.com
diff --git a/challenges/web/the-shared-site/k8s/files/.gitkeep b/challenges/web/the-shared-site/k8s/files/.gitkeep
new file mode 100644
index 0000000..ef3682f
--- /dev/null
+++ b/challenges/web/the-shared-site/k8s/files/.gitkeep
@@ -0,0 +1 @@
+# This file is to keep the directory in git.
diff --git a/challenges/web/the-shared-site/solution/README.md b/challenges/web/the-shared-site/solution/README.md
new file mode 100644
index 0000000..991c897
--- /dev/null
+++ b/challenges/web/the-shared-site/solution/README.md
@@ -0,0 +1,3 @@
+# Solution
+
+The flag is visible on the main page of the website handed out as part of this challenge.
diff --git a/challenges/web/the-shared-site/src/.gitkeep b/challenges/web/the-shared-site/src/.gitkeep
new file mode 100644
index 0000000..76376a2
--- /dev/null
+++ b/challenges/web/the-shared-site/src/.gitkeep
@@ -0,0 +1,2 @@
+# This file is used to keep the directory in the repository.
+# This directory is used to store source files for the challenge.
\ No newline at end of file
diff --git a/challenges/web/the-shared-site/src/Dockerfile b/challenges/web/the-shared-site/src/Dockerfile
new file mode 100644
index 0000000..8ebc473
--- /dev/null
+++ b/challenges/web/the-shared-site/src/Dockerfile
@@ -0,0 +1,4 @@
+# Dockerfile for web - The shared site
+FROM httpd:2.4-alpine
+
+COPY ./html/ /usr/local/apache2/htdocs/
diff --git a/challenges/web/the-shared-site/src/docker-compose.yml b/challenges/web/the-shared-site/src/docker-compose.yml
new file mode 100644
index 0000000..6925645
--- /dev/null
+++ b/challenges/web/the-shared-site/src/docker-compose.yml
@@ -0,0 +1,6 @@
+services:
+ the-shared-site:
+ build: .
+ ports:
+ - "8080:80"
+ restart: always
\ No newline at end of file
diff --git a/challenges/web/the-shared-site/src/html/index.html b/challenges/web/the-shared-site/src/html/index.html
new file mode 100644
index 0000000..20c86b6
--- /dev/null
+++ b/challenges/web/the-shared-site/src/html/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+ The shared site
+
+
+
+ Welcome to the shared site!
+ This is an example of a shared web challenge.
+ Flag: ctfpilot{thanks-for-visiting-the-shared-site}
+
+
+
\ No newline at end of file
diff --git a/challenges/web/the-shared-site/template/.gitkeep b/challenges/web/the-shared-site/template/.gitkeep
new file mode 100644
index 0000000..709b816
--- /dev/null
+++ b/challenges/web/the-shared-site/template/.gitkeep
@@ -0,0 +1,2 @@
+# This file is used to keep the directory in the repository.
+# This directory is used to store templates for the challenge deployment.
\ No newline at end of file
diff --git a/challenges/web/the-shared-site/template/k8s.yml b/challenges/web/the-shared-site/template/k8s.yml
new file mode 100644
index 0000000..f64c276
--- /dev/null
+++ b/challenges/web/the-shared-site/template/k8s.yml
@@ -0,0 +1,121 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: "ctf-{{ CHALLENGE_CATEGORY }}-{{ CHALLENGE_NAME }}"
+ namespace: kubectf-challenges
+ labels:
+ challenges.ctfpilot.com/type: "{{ CHALLENGE_TYPE }}"
+ challenges.ctfpilot.com/name: "{{ CHALLENGE_NAME }}"
+ challenges.ctfpilot.com/category: "{{ CHALLENGE_CATEGORY }}"
+ ctfpilot.com/component: "shared-challenge"
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ challenges.ctfpilot.com/type: "{{ CHALLENGE_TYPE }}"
+ challenges.ctfpilot.com/name: "{{ CHALLENGE_NAME }}"
+ challenges.ctfpilot.com/category: "{{ CHALLENGE_CATEGORY }}"
+ template:
+ metadata:
+ labels:
+ challenges.ctfpilot.com/type: "{{ CHALLENGE_TYPE }}"
+ challenges.ctfpilot.com/name: "{{ CHALLENGE_NAME }}"
+ challenges.ctfpilot.com/category: "{{ CHALLENGE_CATEGORY }}"
+ ctfpilot.com/component: "shared-challenge"
+ spec:
+ enableServiceLinks: false
+ automountServiceAccountToken: false
+ imagePullSecrets:
+ - name: dockerconfigjson-github-com
+ dnsPolicy: None
+ dnsConfig:
+ nameservers:
+ - 1.1.1.1
+ - 8.8.8.8
+ tolerations:
+ - key: "cluster.ctfpilot.com/node"
+ value: "scaler"
+ effect: "PreferNoSchedule"
+ affinity:
+ nodeAffinity:
+ requiredDuringSchedulingIgnoredDuringExecution:
+ nodeSelectorTerms:
+ - matchExpressions:
+ - key: "cluster.ctfpilot.com/node"
+ operator: In
+ values:
+ - scaler
+ containers:
+ - name: web
+ image: ghcr.io/{{ CHALLENGE_REPO }}-{{ DOCKER_IMAGE }}:{{ CHALLENGE_VERSION }}
+ imagePullPolicy: IfNotPresent
+ resources:
+ limits:
+ cpu: 200m
+ memory: 256Mi
+ requests:
+ cpu: 10m
+ memory: 32Mi
+ ports:
+ - containerPort: 80
+ name: web-port
+ startupProbe:
+ httpGet:
+ path: /
+ port: web-port
+ failureThreshold: 12
+ periodSeconds: 10
+ readinessProbe:
+ httpGet:
+ path: /
+ port: web-port
+ initialDelaySeconds: 5
+ timeoutSeconds: 5
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: "ctf-{{ CHALLENGE_CATEGORY }}-{{ CHALLENGE_NAME }}"
+ namespace: kubectf-challenges
+ labels:
+ challenges.ctfpilot.com/type: "{{ CHALLENGE_TYPE }}"
+ challenges.ctfpilot.com/name: "{{ CHALLENGE_NAME }}"
+ challenges.ctfpilot.com/category: "{{ CHALLENGE_CATEGORY }}"
+ ctfpilot.com/component: "shared-challenge"
+spec:
+ selector:
+ challenges.ctfpilot.com/type: "{{ CHALLENGE_TYPE }}"
+ challenges.ctfpilot.com/name: "{{ CHALLENGE_NAME }}"
+ challenges.ctfpilot.com/category: "{{ CHALLENGE_CATEGORY }}"
+ ports:
+ - port: 80
+ targetPort: web-port
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: "ingress-ctf-{{ CHALLENGE_CATEGORY }}-{{ CHALLENGE_NAME }}"
+ namespace: kubectf-challenges
+ labels:
+ challenges.ctfpilot.com/type: "{{ CHALLENGE_TYPE }}"
+ challenges.ctfpilot.com/name: "{{ CHALLENGE_NAME }}"
+ challenges.ctfpilot.com/category: "{{ CHALLENGE_CATEGORY }}"
+ ctfpilot.com/component: "shared-challenge"
+ annotations:
+ traefik.ingress.kubernetes.io/router.middlewares: kubectf-instancing-fallback@kubernetescrd
+spec:
+ tls:
+ - hosts:
+ - "{{ CHALLENGE_NAME }}.{{ .Values.kubectf.host }}"
+ secretName: kubectf-cert-challs
+ rules:
+ - host: "{{ CHALLENGE_NAME }}.{{ .Values.kubectf.host }}"
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: "ctf-{{ CHALLENGE_CATEGORY }}-{{ CHALLENGE_NAME }}"
+ port:
+ number: 80
diff --git a/challenges/web/the-shared-site/version b/challenges/web/the-shared-site/version
new file mode 100644
index 0000000..e440e5c
--- /dev/null
+++ b/challenges/web/the-shared-site/version
@@ -0,0 +1 @@
+3
\ No newline at end of file
diff --git a/challenges/web/where-robots-cannot-search/README.md b/challenges/web/where-robots-cannot-search/README.md
new file mode 100644
index 0000000..0024c3a
--- /dev/null
+++ b/challenges/web/where-robots-cannot-search/README.md
@@ -0,0 +1,14 @@
+# Where robots cannot search
+
+A challenge based on the notion of robots.txt, and how it should never be used to hide sensitive information.
+
+The challenge name refers to robots (web scrapers) that are not allowed to search for certain files on a website, therefore giving the hint that the flag is located in a file that is disallowed in the robots.txt file.
+
+## Challenge
+
+A website is given, which contains a robots.txt file, in this file one or more directories and files are disallowed.
+However, one of them is called `flag.txt`, which contains the flag.
+
+## Story
+
+To fit the Brunnerne story, the website is a simple landing page for the `Brunnerne company`.
diff --git a/challenges/web/where-robots-cannot-search/challenge.yml b/challenges/web/where-robots-cannot-search/challenge.yml
new file mode 100644
index 0000000..23eb068
--- /dev/null
+++ b/challenges/web/where-robots-cannot-search/challenge.yml
@@ -0,0 +1,31 @@
+# yaml-language-server: $schema=https://raw.githubusercontent.com/ctfpilot/challenge-schema/refs/heads/main/schema.json
+
+enabled: true
+name: Where robots cannot search
+slug: where-robots-cannot-search
+author: The Mikkel
+category: web
+difficulty: beginner
+tags: [
+ "BrunnerCTF 2025",
+ Instanced,
+ Example
+]
+type: instanced
+instanced_type: web
+instanced_name: null
+instanced_subdomains: []
+connection: null
+flag:
+- flag: ctfpilot{r0bot5_sh0u1d_nOt_637_h3re_b0t_You_g07_h3re}
+ case_sensitive: false
+points: 1000
+decay: 75
+min_points: 100
+description_location: description.md
+handout_dir: handout
+dockerfile_locations:
+- location: src/Dockerfile
+ context: src/
+ identifier: null
+
diff --git a/challenges/web/where-robots-cannot-search/description.md b/challenges/web/where-robots-cannot-search/description.md
new file mode 100644
index 0000000..d8680ae
--- /dev/null
+++ b/challenges/web/where-robots-cannot-search/description.md
@@ -0,0 +1,7 @@
+# Where robots cannot search
+
+**Difficulty:** Beginner
+**Author:** The Mikkel
+
+Ahh, the Brunnerne company.
+But they have a secret, hidden away from robot search.
diff --git a/challenges/web/where-robots-cannot-search/handout/.gitkeep b/challenges/web/where-robots-cannot-search/handout/.gitkeep
new file mode 100644
index 0000000..0f065bc
--- /dev/null
+++ b/challenges/web/where-robots-cannot-search/handout/.gitkeep
@@ -0,0 +1,2 @@
+# This file is used to keep the directory in the repository.
+# This directory is used to store files that are handed out, for the challenge. The files are automatically zipped and copied to the files directory.
\ No newline at end of file
diff --git a/challenges/web/where-robots-cannot-search/k8s/challenge/k8s.yml b/challenges/web/where-robots-cannot-search/k8s/challenge/k8s.yml
new file mode 100644
index 0000000..ff6a12e
--- /dev/null
+++ b/challenges/web/where-robots-cannot-search/k8s/challenge/k8s.yml
@@ -0,0 +1,147 @@
+apiVersion: kube-ctf.ctfpilot.com/v1
+kind: instancedChallenge
+metadata:
+ name: "where-robots-cannot-search"
+ labels:
+ challenges.ctfpilot.com/type: "web"
+ challenges.ctfpilot.com/name: "where-robots-cannot-search"
+ challenges.ctfpilot.com/category: "web"
+ ctfpilot.com/component: "instanced-challenge"
+spec:
+ expires: 3600
+ available_at: 0
+ type: web
+ template: |
+ apiVersion: apps/v1
+ kind: Deployment
+ metadata:
+ name: "ctf-{{ deployment_id }}"
+ namespace: kubectf-challenges-instanced
+ labels:
+ challenges.ctfpilot.com/type: "web"
+ challenges.ctfpilot.com/name: "where-robots-cannot-search"
+ challenges.ctfpilot.com/category: "web"
+ instanced.challenges.ctfpilot.com/deployment: "{{ deployment_id }}"
+ instanced.challenges.ctfpilot.com/owner: "{{ owner_id }}"
+ ctfpilot.com/component: "instanced-challenge"
+ annotations:
+ janitor/expires: "{{ expires }}"
+ spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ instanced.challenges.ctfpilot.com/deployment: "{{ deployment_id }}"
+ instanced.challenges.ctfpilot.com/owner: "{{ owner_id }}"
+ template:
+ metadata:
+ labels:
+ role: "web"
+ challenges.ctfpilot.com/type: "web"
+ challenges.ctfpilot.com/name: "where-robots-cannot-search"
+ challenges.ctfpilot.com/category: "web"
+ instanced.challenges.ctfpilot.com/deployment: "{{ deployment_id }}"
+ instanced.challenges.ctfpilot.com/owner: "{{ owner_id }}"
+ ctfpilot.com/component: "instanced-challenge"
+ spec:
+ enableServiceLinks: false
+ automountServiceAccountToken: false
+ imagePullSecrets:
+ - name: dockerconfigjson-github-com
+ dnsPolicy: None
+ dnsConfig:
+ nameservers:
+ - 1.1.1.1
+ - 8.8.8.8
+ tolerations:
+ - key: "cluster.ctfpilot.com/node"
+ value: "scaler"
+ effect: "PreferNoSchedule"
+ affinity:
+ nodeAffinity:
+ requiredDuringSchedulingIgnoredDuringExecution:
+ nodeSelectorTerms:
+ - matchExpressions:
+ - key: "cluster.ctfpilot.com/node"
+ operator: In
+ values:
+ - scaler
+ containers:
+ - name: web
+ image: ghcr.io/ctfpilot/challenges-example-web-where-robots-cannot-search:7
+ imagePullPolicy: IfNotPresent
+ resources:
+ limits:
+ cpu: 200m
+ memory: 256Mi
+ requests:
+ cpu: 10m
+ memory: 32Mi
+ ports:
+ - containerPort: 80
+ name: web-port
+ startupProbe:
+ httpGet:
+ path: /
+ port: web-port
+ failureThreshold: 12
+ periodSeconds: 10
+ readinessProbe:
+ httpGet:
+ path: /
+ port: web-port
+ initialDelaySeconds: 5
+ timeoutSeconds: 5
+ ---
+ apiVersion: v1
+ kind: Service
+ metadata:
+ name: "ctf-{{ deployment_id }}"
+ namespace: kubectf-challenges-instanced
+ labels:
+ challenges.ctfpilot.com/type: "web"
+ challenges.ctfpilot.com/name: "where-robots-cannot-search"
+ challenges.ctfpilot.com/category: "web"
+ instanced.challenges.ctfpilot.com/deployment: "{{ deployment_id }}"
+ instanced.challenges.ctfpilot.com/owner: "{{ owner_id }}"
+ ctfpilot.com/component: "instanced-challenge"
+ annotations:
+ janitor/expires: "{{ expires }}"
+ spec:
+ selector:
+ role: "web"
+ instanced.challenges.ctfpilot.com/deployment: "{{ deployment_id }}"
+ ports:
+ - port: 80
+ targetPort: web-port
+ ---
+ apiVersion: networking.k8s.io/v1
+ kind: Ingress
+ metadata:
+ name: ingress-ctf-{{ deployment_id }}
+ namespace: kubectf-challenges-instanced
+ labels:
+ challenges.ctfpilot.com/type: "web"
+ challenges.ctfpilot.com/name: "where-robots-cannot-search"
+ challenges.ctfpilot.com/category: "web"
+ instanced.challenges.ctfpilot.com/deployment: "{{ deployment_id }}"
+ instanced.challenges.ctfpilot.com/owner: "{{ owner_id }}"
+ ctfpilot.com/component: "instanced-challenge"
+ annotations:
+ janitor/expires: "{{ expires }}"
+ traefik.ingress.kubernetes.io/router.middlewares: kubectf-instancing-fallback@kubernetescrd
+ spec:
+ tls:
+ - hosts:
+ - "{{ deployment_id }}.{{ domain }}"
+ secretName: kubectf-cert-challs
+ rules:
+ - host: "{{ deployment_id }}.{{ domain }}"
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: ctf-{{ deployment_id }}
+ port:
+ number: 80
diff --git a/challenges/web/where-robots-cannot-search/k8s/config/Chart.yaml b/challenges/web/where-robots-cannot-search/k8s/config/Chart.yaml
new file mode 100644
index 0000000..5809e9d
--- /dev/null
+++ b/challenges/web/where-robots-cannot-search/k8s/config/Chart.yaml
@@ -0,0 +1,6 @@
+apiVersion: v2
+name: configmap-where-robots-cannot-search
+version: 1.7.0
+description: Challenge configmap for where-robots-cannot-search in category web
+appVersion: "1.7.0"
+type: application
diff --git a/challenges/web/where-robots-cannot-search/k8s/config/templates/k8s.yml b/challenges/web/where-robots-cannot-search/k8s/config/templates/k8s.yml
new file mode 100644
index 0000000..5f685e9
--- /dev/null
+++ b/challenges/web/where-robots-cannot-search/k8s/config/templates/k8s.yml
@@ -0,0 +1,65 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: "challenge-web-where-robots-cannot-search"
+ labels:
+ challenges.ctfpilot.com/type: "web"
+ challenges.ctfpilot.com/name: "where-robots-cannot-search"
+ challenges.ctfpilot.com/category: "web"
+ challenges.ctfpilot.com/version: "7"
+ challenges.ctfpilot.com/configmap: "challenge-config"
+ challenges.ctfpilot.com/enabled: "true"
+ ctfpilot.com/component: "challenge-config"
+data:
+ name: "where-robots-cannot-search"
+ path: "challenges/web/where-robots-cannot-search"
+ repository: "ctfpilot/challenges-example"
+ generated_at: "2025-12-20 15:54:20"
+ challenge: |
+ {
+ "$schema": "https://raw.githubusercontent.com/ctfpilot/challenge-schema/refs/heads/main/schema.json",
+ "enabled": true,
+ "name": "Where robots cannot search",
+ "slug": "where-robots-cannot-search",
+ "author": "The Mikkel",
+ "category": "web",
+ "difficulty": "beginner",
+ "tags": [
+ "BrunnerCTF 2025",
+ "Instanced",
+ "Example"
+ ],
+ "type": "instanced",
+ "instanced_type": "web",
+ "instanced_name": null,
+ "instanced_subdomains": [],
+ "connection": null,
+ "flag": [
+ {
+ "flag": "ctfpilot{r0bot5_sh0u1d_nOt_637_h3re_b0t_You_g07_h3re}",
+ "case_sensitive": false
+ }
+ ],
+ "points": 1000,
+ "decay": 75,
+ "min_points": 100,
+ "description_location": "description.md",
+ "handout_dir": "handout",
+ "dockerfile_locations": [
+ {
+ "location": "src/Dockerfile",
+ "context": "src/",
+ "identifier": null
+ }
+ ]
+ }
+
+ description: |
+ # Where robots cannot search
+
+ **Difficulty:** Beginner
+ **Author:** The Mikkel
+
+ Ahh, the Brunnerne company.
+ But they have a secret, hidden away from robot search.
+
diff --git a/challenges/web/where-robots-cannot-search/k8s/config/values.yaml b/challenges/web/where-robots-cannot-search/k8s/config/values.yaml
new file mode 100644
index 0000000..31cf09a
--- /dev/null
+++ b/challenges/web/where-robots-cannot-search/k8s/config/values.yaml
@@ -0,0 +1,11 @@
+challenge:
+ enabled: true
+ name: where-robots-cannot-search
+ category: web
+ type: web
+ version: 7
+ path: challenges/web/where-robots-cannot-search
+kubectf:
+ expires: 3600
+ availableAt: 0
+ host: example.com
diff --git a/challenges/web/where-robots-cannot-search/k8s/files/.gitkeep b/challenges/web/where-robots-cannot-search/k8s/files/.gitkeep
new file mode 100644
index 0000000..ef3682f
--- /dev/null
+++ b/challenges/web/where-robots-cannot-search/k8s/files/.gitkeep
@@ -0,0 +1 @@
+# This file is to keep the directory in git.
diff --git a/challenges/web/where-robots-cannot-search/solution/README.md b/challenges/web/where-robots-cannot-search/solution/README.md
new file mode 100644
index 0000000..3023e39
--- /dev/null
+++ b/challenges/web/where-robots-cannot-search/solution/README.md
@@ -0,0 +1,3 @@
+# Solution
+
+Go to `/robots.txt` and see `/flag.txt` is disallowed. Go there for flag.
diff --git a/challenges/web/where-robots-cannot-search/solution/solve.py b/challenges/web/where-robots-cannot-search/solution/solve.py
new file mode 100644
index 0000000..1a985d9
--- /dev/null
+++ b/challenges/web/where-robots-cannot-search/solution/solve.py
@@ -0,0 +1,109 @@
+import requests
+import argparse
+
+# ----------------- Logger class -----------------
+
+# Fold this line in to minimize the code
+class Logger:
+ level = 0
+
+ def __init__(self, verbose: bool, debug: bool = False):
+ if verbose:
+ self.level = 1
+ self.write('Verbose logging enabled')
+ if debug:
+ self.write('[DEBUG] Debug logging enabled')
+ self.level = 2
+
+ '''
+ Write a message to the console, regardless of the log level
+ '''
+ def write(self, msg: str):
+ print(msg)
+
+ '''
+ Log a message to the console, if verbose logging is enabled
+ '''
+ def log(self, msg: str):
+ if self.level > 0:
+ print(msg)
+
+ '''
+ Log a message to the console, if debug logging is enabled
+ '''
+ def debug(self, msg: str):
+ if self.level > 1:
+ print(f'[DEBUG] {msg}')
+
+logger = Logger(False, False) # Is replaced by the logger in the main function
+
+# ----------------- Challenge specific code -----------------
+
+def solve(url: str, flag: str) -> bool:
+ # Solve the challenge
+ # Ensure website is reachable
+ try:
+ logger.debug(f'Connecting to {url}')
+ response = requests.get(url)
+ except requests.exceptions.RequestException as e:
+ logger.log(f'Failed to connect to {url}')
+ return False
+
+ # Send a GET request to the robots.txt file
+ try:
+ logger.debug('Sending GET request to robots.txt')
+ response = requests.get(url + '/robots.txt')
+ except requests.exceptions.RequestException as e:
+ logger.log('Failed to connect to /robots.txt')
+ return False
+ # Check if flag.txt is disallowed
+ if '/flag.txt' in response.text:
+ logger.log('Flag.txt is disallowed')
+ else:
+ logger.log('Flag.txt not specified in /robots.txt')
+ return False
+
+ # Send a GET request to the flag.txt file
+ try:
+ logger.debug('Sending GET request to /flag.txt')
+ response = requests.get(url + '/flag.txt')
+ except requests.exceptions.RequestException as e:
+ logger.log('Failed to connect to flag.txt')
+ return False
+ # Check if the response contains the flag
+ if flag in response.text:
+ logger.log('Flag found in flag.txt')
+ return True
+ else:
+ logger.log('Flag not found')
+ return False
+
+# ----------------- Helper classes and functions -----------------
+
+def get_args():
+ parser = argparse.ArgumentParser()
+ parser.add_argument('url', help='URL to the challenge', type=str)
+ parser.add_argument('flag', help='Correct flag, to check the challenge against')
+ parser.add_argument('-v', '--verbose', help='Verbose output', action='store_true', default=False)
+ parser.add_argument('-d', '--debug', help='Debug output', action='store_true', default=False)
+ return parser.parse_args()
+
+def main():
+ args = get_args()
+ url = args.url
+ flag = args.flag
+
+ logger = Logger(args.verbose, args.debug)
+ logger.log(f'URL: {url}')
+ logger.log(f'Flag: {flag}')
+
+ logger.log('Starting solve script...')
+ if solve(url, flag):
+ print('Challenge solved successfully')
+ exit(0)
+ else:
+ print('Failed to solve challenge')
+ exit(1)
+
+if __name__ == '__main__':
+ main()
diff --git a/challenges/web/where-robots-cannot-search/src/.gitkeep b/challenges/web/where-robots-cannot-search/src/.gitkeep
new file mode 100644
index 0000000..76376a2
--- /dev/null
+++ b/challenges/web/where-robots-cannot-search/src/.gitkeep
@@ -0,0 +1,2 @@
+# This file is used to keep the directory in the repository.
+# This directory is used to store source files for the challenge.
\ No newline at end of file
diff --git a/challenges/web/where-robots-cannot-search/src/Dockerfile b/challenges/web/where-robots-cannot-search/src/Dockerfile
new file mode 100644
index 0000000..fa0ebfe
--- /dev/null
+++ b/challenges/web/where-robots-cannot-search/src/Dockerfile
@@ -0,0 +1,7 @@
+# Dockerfile for web - Where robots cannot search
+FROM httpd:2.4-alpine
+
+RUN sed -i 's|#LoadModule rewrite_module modules/mod_rewrite.so|LoadModule rewrite_module modules/mod_rewrite.so|' /usr/local/apache2/conf/httpd.conf
+RUN sed -i 's|AllowOverride None|AllowOverride All|' /usr/local/apache2/conf/httpd.conf
+
+COPY ./html/ /usr/local/apache2/htdocs/
diff --git a/challenges/web/where-robots-cannot-search/src/docker-compose.yml b/challenges/web/where-robots-cannot-search/src/docker-compose.yml
new file mode 100644
index 0000000..bfd6d46
--- /dev/null
+++ b/challenges/web/where-robots-cannot-search/src/docker-compose.yml
@@ -0,0 +1,7 @@
+services:
+ where-robots-cannot-search:
+ build: .
+ ports:
+ - "8080:80"
+ volumes:
+ - ./html:/usr/local/apache2/htdocs/
diff --git a/challenges/web/where-robots-cannot-search/src/html/.htaccess b/challenges/web/where-robots-cannot-search/src/html/.htaccess
new file mode 100644
index 0000000..c4b71b7
--- /dev/null
+++ b/challenges/web/where-robots-cannot-search/src/html/.htaccess
@@ -0,0 +1,5 @@
+# Allow redirection from none .html files to .html files
+RewriteEngine On
+RewriteCond %{REQUEST_FILENAME} !-f
+RewriteCond %{REQUEST_FILENAME} !-d
+RewriteRule ^(.*)$ $1.html [L]
diff --git a/challenges/web/where-robots-cannot-search/src/html/admin.html b/challenges/web/where-robots-cannot-search/src/html/admin.html
new file mode 100644
index 0000000..34dc2d1
--- /dev/null
+++ b/challenges/web/where-robots-cannot-search/src/html/admin.html
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+ Admin - Brunnerne Company
+
+
+
+
+
+
Admin Page
+
This page is restricted to administrators only.
+
+
+
+
\ No newline at end of file
diff --git a/challenges/web/where-robots-cannot-search/src/html/flag.txt b/challenges/web/where-robots-cannot-search/src/html/flag.txt
new file mode 100644
index 0000000..bfd98c7
--- /dev/null
+++ b/challenges/web/where-robots-cannot-search/src/html/flag.txt
@@ -0,0 +1 @@
+ctfpilot{r0bot5_sh0u1d_nOt_637_h3re_b0t_You_g07_h3re}
diff --git a/challenges/web/where-robots-cannot-search/src/html/hidden.html b/challenges/web/where-robots-cannot-search/src/html/hidden.html
new file mode 100644
index 0000000..4daa3f3
--- /dev/null
+++ b/challenges/web/where-robots-cannot-search/src/html/hidden.html
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+ Hidden - Brunnerne Company
+
+
+
+
+
+
Hidden Page
+
This page is hidden from search engines.
+
+
+
+
\ No newline at end of file
diff --git a/challenges/web/where-robots-cannot-search/src/html/index.html b/challenges/web/where-robots-cannot-search/src/html/index.html
new file mode 100644
index 0000000..fbeb2fb
--- /dev/null
+++ b/challenges/web/where-robots-cannot-search/src/html/index.html
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+ Brunnerne Company
+
+
+
+
+
+
+
Welcome to Brunnerne Company
+
We specialize in creating the most delicious Brunsviger. Come by our store in Odense!
+
Check out our location
+
+
+
+
+
\ No newline at end of file
diff --git a/challenges/web/where-robots-cannot-search/src/html/private.html b/challenges/web/where-robots-cannot-search/src/html/private.html
new file mode 100644
index 0000000..0330375
--- /dev/null
+++ b/challenges/web/where-robots-cannot-search/src/html/private.html
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+ Private - Brunnerne Company
+
+
+
+
+
+
Private Page
+
This page contains private information.
+
+
+
+
\ No newline at end of file
diff --git a/challenges/web/where-robots-cannot-search/src/html/robots.txt b/challenges/web/where-robots-cannot-search/src/html/robots.txt
new file mode 100644
index 0000000..db86de4
--- /dev/null
+++ b/challenges/web/where-robots-cannot-search/src/html/robots.txt
@@ -0,0 +1,5 @@
+User-agent: *
+Disallow: /admin
+Disallow: /private
+Disallow: /hidden
+Disallow: /flag.txt
diff --git a/challenges/web/where-robots-cannot-search/template/.gitkeep b/challenges/web/where-robots-cannot-search/template/.gitkeep
new file mode 100644
index 0000000..709b816
--- /dev/null
+++ b/challenges/web/where-robots-cannot-search/template/.gitkeep
@@ -0,0 +1,2 @@
+# This file is used to keep the directory in the repository.
+# This directory is used to store templates for the challenge deployment.
\ No newline at end of file
diff --git a/challenges/web/where-robots-cannot-search/template/k8s.yml b/challenges/web/where-robots-cannot-search/template/k8s.yml
new file mode 100644
index 0000000..51cdafe
--- /dev/null
+++ b/challenges/web/where-robots-cannot-search/template/k8s.yml
@@ -0,0 +1,133 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: "ctf-{{ deployment_id }}"
+ namespace: kubectf-challenges-instanced
+ labels:
+ challenges.ctfpilot.com/type: "{{ CHALLENGE_TYPE }}"
+ challenges.ctfpilot.com/name: "{{ CHALLENGE_NAME }}"
+ challenges.ctfpilot.com/category: "{{ CHALLENGE_CATEGORY }}"
+ instanced.challenges.ctfpilot.com/deployment: "{{ deployment_id }}"
+ instanced.challenges.ctfpilot.com/owner: "{{ owner_id }}"
+ ctfpilot.com/component: "instanced-challenge"
+ annotations:
+ janitor/expires: "{{ expires }}"
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ instanced.challenges.ctfpilot.com/deployment: "{{ deployment_id }}"
+ instanced.challenges.ctfpilot.com/owner: "{{ owner_id }}"
+ template:
+ metadata:
+ labels:
+ role: "{{ CHALLENGE_TYPE }}"
+ challenges.ctfpilot.com/type: "{{ CHALLENGE_TYPE }}"
+ challenges.ctfpilot.com/name: "{{ CHALLENGE_NAME }}"
+ challenges.ctfpilot.com/category: "{{ CHALLENGE_CATEGORY }}"
+ instanced.challenges.ctfpilot.com/deployment: "{{ deployment_id }}"
+ instanced.challenges.ctfpilot.com/owner: "{{ owner_id }}"
+ ctfpilot.com/component: "instanced-challenge"
+ spec:
+ enableServiceLinks: false
+ automountServiceAccountToken: false
+ imagePullSecrets:
+ - name: dockerconfigjson-github-com
+ dnsPolicy: None
+ dnsConfig:
+ nameservers:
+ - 1.1.1.1
+ - 8.8.8.8
+ tolerations:
+ - key: "cluster.ctfpilot.com/node"
+ value: "scaler"
+ effect: "PreferNoSchedule"
+ affinity:
+ nodeAffinity:
+ requiredDuringSchedulingIgnoredDuringExecution:
+ nodeSelectorTerms:
+ - matchExpressions:
+ - key: "cluster.ctfpilot.com/node"
+ operator: In
+ values:
+ - scaler
+ containers:
+ - name: web
+ image: ghcr.io/{{ CHALLENGE_REPO }}-{{ DOCKER_IMAGE }}:{{ CHALLENGE_VERSION }}
+ imagePullPolicy: IfNotPresent
+ resources:
+ limits:
+ cpu: 200m
+ memory: 256Mi
+ requests:
+ cpu: 10m
+ memory: 32Mi
+ ports:
+ - containerPort: 80
+ name: web-port
+ startupProbe:
+ httpGet:
+ path: /
+ port: web-port
+ failureThreshold: 12
+ periodSeconds: 10
+ readinessProbe:
+ httpGet:
+ path: /
+ port: web-port
+ initialDelaySeconds: 5
+ timeoutSeconds: 5
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: "ctf-{{ deployment_id }}"
+ namespace: kubectf-challenges-instanced
+ labels:
+ challenges.ctfpilot.com/type: "{{ CHALLENGE_TYPE }}"
+ challenges.ctfpilot.com/name: "{{ CHALLENGE_NAME }}"
+ challenges.ctfpilot.com/category: "{{ CHALLENGE_CATEGORY }}"
+ instanced.challenges.ctfpilot.com/deployment: "{{ deployment_id }}"
+ instanced.challenges.ctfpilot.com/owner: "{{ owner_id }}"
+ ctfpilot.com/component: "instanced-challenge"
+ annotations:
+ janitor/expires: "{{ expires }}"
+spec:
+ selector:
+ role: "{{ CHALLENGE_TYPE }}"
+ instanced.challenges.ctfpilot.com/deployment: "{{ deployment_id }}"
+ ports:
+ - port: 80
+ targetPort: web-port
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: ingress-ctf-{{ deployment_id }}
+ namespace: kubectf-challenges-instanced
+ labels:
+ challenges.ctfpilot.com/type: "{{ CHALLENGE_TYPE }}"
+ challenges.ctfpilot.com/name: "{{ CHALLENGE_NAME }}"
+ challenges.ctfpilot.com/category: "{{ CHALLENGE_CATEGORY }}"
+ instanced.challenges.ctfpilot.com/deployment: "{{ deployment_id }}"
+ instanced.challenges.ctfpilot.com/owner: "{{ owner_id }}"
+ ctfpilot.com/component: "instanced-challenge"
+ annotations:
+ janitor/expires: "{{ expires }}"
+ traefik.ingress.kubernetes.io/router.middlewares: kubectf-instancing-fallback@kubernetescrd
+spec:
+ tls:
+ - hosts:
+ - "{{ deployment_id }}.{{ domain }}"
+ secretName: kubectf-cert-challs
+ rules:
+ - host: "{{ deployment_id }}.{{ domain }}"
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: ctf-{{ deployment_id }}
+ port:
+ number: 80
diff --git a/challenges/web/where-robots-cannot-search/version b/challenges/web/where-robots-cannot-search/version
new file mode 100644
index 0000000..c793025
--- /dev/null
+++ b/challenges/web/where-robots-cannot-search/version
@@ -0,0 +1 @@
+7
\ No newline at end of file
diff --git a/template/instanced-tcp-k8s.yml b/template/instanced-tcp-k8s.yml
index 8d050c3..03c4634 100644
--- a/template/instanced-tcp-k8s.yml
+++ b/template/instanced-tcp-k8s.yml
@@ -52,7 +52,7 @@ spec:
values:
- scaler
containers:
- - name: web
+ - name: tcp
image: ghcr.io/{{ CHALLENGE_REPO }}-{{ DOCKER_IMAGE }}:{{ CHALLENGE_VERSION }}
imagePullPolicy: IfNotPresent
resources:
@@ -87,6 +87,7 @@ spec:
ports:
- port: 8080
name: tcp
+ targetPort: tcp
---
apiVersion: traefik.io/v1alpha1
kind: IngressRouteTCP
diff --git a/template/shared-tcp-k8s.yml b/template/shared-tcp-k8s.yml
new file mode 100644
index 0000000..f7c920e
--- /dev/null
+++ b/template/shared-tcp-k8s.yml
@@ -0,0 +1,106 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: "ctf-{{ CHALLENGE_CATEGORY }}-{{ CHALLENGE_NAME }}"
+ namespace: kubectf-challenges
+ labels:
+ challenges.ctfpilot.com/type: "{{ CHALLENGE_TYPE }}"
+ challenges.ctfpilot.com/name: "{{ CHALLENGE_NAME }}"
+ challenges.ctfpilot.com/category: "{{ CHALLENGE_CATEGORY }}"
+ ctfpilot.com/component: "shared-challenge"
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ challenges.ctfpilot.com/type: "{{ CHALLENGE_TYPE }}"
+ challenges.ctfpilot.com/name: "{{ CHALLENGE_NAME }}"
+ challenges.ctfpilot.com/category: "{{ CHALLENGE_CATEGORY }}"
+ template:
+ metadata:
+ labels:
+ challenges.ctfpilot.com/type: "{{ CHALLENGE_TYPE }}"
+ challenges.ctfpilot.com/name: "{{ CHALLENGE_NAME }}"
+ challenges.ctfpilot.com/category: "{{ CHALLENGE_CATEGORY }}"
+ ctfpilot.com/component: "shared-challenge"
+ spec:
+ enableServiceLinks: false
+ automountServiceAccountToken: false
+ imagePullSecrets:
+ - name: dockerconfigjson-github-com
+ dnsPolicy: None
+ dnsConfig:
+ nameservers:
+ - 1.1.1.1
+ - 8.8.8.8
+ tolerations:
+ - key: "cluster.ctfpilot.com/node"
+ value: "scaler"
+ effect: "PreferNoSchedule"
+ affinity:
+ nodeAffinity:
+ requiredDuringSchedulingIgnoredDuringExecution:
+ nodeSelectorTerms:
+ - matchExpressions:
+ - key: "cluster.ctfpilot.com/node"
+ operator: In
+ values:
+ - scaler
+ containers:
+ - name: tcp
+ image: ghcr.io/{{ CHALLENGE_REPO }}-{{ DOCKER_IMAGE }}:{{ CHALLENGE_VERSION }}
+ imagePullPolicy: IfNotPresent
+ resources:
+ limits:
+ cpu: 200m
+ memory: 256Mi
+ requests:
+ cpu: 10m
+ memory: 32Mi
+ ports:
+ - containerPort: 8080
+ name: tcp
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: "ctf-{{ CHALLENGE_CATEGORY }}-{{ CHALLENGE_NAME }}"
+ namespace: kubectf-challenges
+ labels:
+ challenges.ctfpilot.com/type: "{{ CHALLENGE_TYPE }}"
+ challenges.ctfpilot.com/name: "{{ CHALLENGE_NAME }}"
+ challenges.ctfpilot.com/category: "{{ CHALLENGE_CATEGORY }}"
+ ctfpilot.com/component: "shared-challenge"
+spec:
+ selector:
+ challenges.ctfpilot.com/type: "{{ CHALLENGE_TYPE }}"
+ challenges.ctfpilot.com/name: "{{ CHALLENGE_NAME }}"
+ challenges.ctfpilot.com/category: "{{ CHALLENGE_CATEGORY }}"
+ ports:
+ - port: 8080
+ name: tcp
+ targetPort: tcp
+---
+apiVersion: traefik.io/v1alpha1
+kind: IngressRouteTCP
+metadata:
+ name: "ingress-ctf-{{ CHALLENGE_CATEGORY }}-{{ CHALLENGE_NAME }}"
+ namespace: kubectf-challenges
+ labels:
+ challenges.ctfpilot.com/type: "{{ CHALLENGE_TYPE }}"
+ challenges.ctfpilot.com/name: "{{ CHALLENGE_NAME }}"
+ challenges.ctfpilot.com/category: "{{ CHALLENGE_CATEGORY }}"
+ ctfpilot.com/component: "shared-challenge"
+ annotations:
+ traefik.ingress.kubernetes.io/router.priority: "100"
+spec:
+ entryPoints:
+ - tcp-{{ CHALLENGE_CATEGORY }}-{{ CHALLENGE_NAME }} # Needs to be a custom entrypoint defined in Traefik - This is defined per challenge
+ routes:
+ - match: HostSNI(`*`)
+ priority: 10
+ middlewares:
+ - name: challenge-ipwhitelist-tcp
+ namespace: kubectf-challenges
+ services:
+ - name: "ctf-{{ CHALLENGE_CATEGORY }}-{{ CHALLENGE_NAME }}"
+ port: 8080
diff --git a/template/shared-web-k8s.yml b/template/shared-web-k8s.yml
new file mode 100644
index 0000000..f64c276
--- /dev/null
+++ b/template/shared-web-k8s.yml
@@ -0,0 +1,121 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: "ctf-{{ CHALLENGE_CATEGORY }}-{{ CHALLENGE_NAME }}"
+ namespace: kubectf-challenges
+ labels:
+ challenges.ctfpilot.com/type: "{{ CHALLENGE_TYPE }}"
+ challenges.ctfpilot.com/name: "{{ CHALLENGE_NAME }}"
+ challenges.ctfpilot.com/category: "{{ CHALLENGE_CATEGORY }}"
+ ctfpilot.com/component: "shared-challenge"
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ challenges.ctfpilot.com/type: "{{ CHALLENGE_TYPE }}"
+ challenges.ctfpilot.com/name: "{{ CHALLENGE_NAME }}"
+ challenges.ctfpilot.com/category: "{{ CHALLENGE_CATEGORY }}"
+ template:
+ metadata:
+ labels:
+ challenges.ctfpilot.com/type: "{{ CHALLENGE_TYPE }}"
+ challenges.ctfpilot.com/name: "{{ CHALLENGE_NAME }}"
+ challenges.ctfpilot.com/category: "{{ CHALLENGE_CATEGORY }}"
+ ctfpilot.com/component: "shared-challenge"
+ spec:
+ enableServiceLinks: false
+ automountServiceAccountToken: false
+ imagePullSecrets:
+ - name: dockerconfigjson-github-com
+ dnsPolicy: None
+ dnsConfig:
+ nameservers:
+ - 1.1.1.1
+ - 8.8.8.8
+ tolerations:
+ - key: "cluster.ctfpilot.com/node"
+ value: "scaler"
+ effect: "PreferNoSchedule"
+ affinity:
+ nodeAffinity:
+ requiredDuringSchedulingIgnoredDuringExecution:
+ nodeSelectorTerms:
+ - matchExpressions:
+ - key: "cluster.ctfpilot.com/node"
+ operator: In
+ values:
+ - scaler
+ containers:
+ - name: web
+ image: ghcr.io/{{ CHALLENGE_REPO }}-{{ DOCKER_IMAGE }}:{{ CHALLENGE_VERSION }}
+ imagePullPolicy: IfNotPresent
+ resources:
+ limits:
+ cpu: 200m
+ memory: 256Mi
+ requests:
+ cpu: 10m
+ memory: 32Mi
+ ports:
+ - containerPort: 80
+ name: web-port
+ startupProbe:
+ httpGet:
+ path: /
+ port: web-port
+ failureThreshold: 12
+ periodSeconds: 10
+ readinessProbe:
+ httpGet:
+ path: /
+ port: web-port
+ initialDelaySeconds: 5
+ timeoutSeconds: 5
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: "ctf-{{ CHALLENGE_CATEGORY }}-{{ CHALLENGE_NAME }}"
+ namespace: kubectf-challenges
+ labels:
+ challenges.ctfpilot.com/type: "{{ CHALLENGE_TYPE }}"
+ challenges.ctfpilot.com/name: "{{ CHALLENGE_NAME }}"
+ challenges.ctfpilot.com/category: "{{ CHALLENGE_CATEGORY }}"
+ ctfpilot.com/component: "shared-challenge"
+spec:
+ selector:
+ challenges.ctfpilot.com/type: "{{ CHALLENGE_TYPE }}"
+ challenges.ctfpilot.com/name: "{{ CHALLENGE_NAME }}"
+ challenges.ctfpilot.com/category: "{{ CHALLENGE_CATEGORY }}"
+ ports:
+ - port: 80
+ targetPort: web-port
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: "ingress-ctf-{{ CHALLENGE_CATEGORY }}-{{ CHALLENGE_NAME }}"
+ namespace: kubectf-challenges
+ labels:
+ challenges.ctfpilot.com/type: "{{ CHALLENGE_TYPE }}"
+ challenges.ctfpilot.com/name: "{{ CHALLENGE_NAME }}"
+ challenges.ctfpilot.com/category: "{{ CHALLENGE_CATEGORY }}"
+ ctfpilot.com/component: "shared-challenge"
+ annotations:
+ traefik.ingress.kubernetes.io/router.middlewares: kubectf-instancing-fallback@kubernetescrd
+spec:
+ tls:
+ - hosts:
+ - "{{ CHALLENGE_NAME }}.{{ .Values.kubectf.host }}"
+ secretName: kubectf-cert-challs
+ rules:
+ - host: "{{ CHALLENGE_NAME }}.{{ .Values.kubectf.host }}"
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: "ctf-{{ CHALLENGE_CATEGORY }}-{{ CHALLENGE_NAME }}"
+ port:
+ number: 80