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 + + + + +
+

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