Attack/Defence infrastruktura Českého týmu na ECSC

Jan Černohorský

Slidy najdete na talks.grsc.cz/ad-infra

ECSC?

European Coal and Steel Community

Eurovision Cover Song Contest

European Cybersecurity Challenge

Organizuje ENISA

Desetičlenné týmy do 26 let (min. 5 do 20)

Česká kvalifikace je Kybersoutěž

Existuje OpenECSC (každý rok jiné)

Attack/Defence?

Obránci

  • přístup na vulnbox po SSH
  • typicky root (ne vždy)
  • služby neznají dopředu

Útočníci

  • přistupují přímo ke službám
  • většinou HTTP nebo Raw TCP
  • většinou dostanou zdroják

Flagbot

  • umisťuje vlajky do služeb
  • kontroluje jejich přítomnost
  • ⇒ přiděluje body za uptime
  • chová se jako „normální uživatel“

Útočníci

  • kradou vlajky ze služeb
  • odevzdávají gameserveru
  • ⇒ přiděluje body za útok
> POST /register
< 200 OK

> POST /notes/private
> 
> ECSC_080A02AF0J07UMOPHNE00KJ48KAE28C=
< 201 Created
<
< New note ID: 12

...

> GET /notes/private
< 200 OK
<
< Note 12: ECSC_080A02AF0J07UMOPHNE00KJ48KAE28C=
> GET /notes/private/1
< 404 Not Found
> GET /notes/private/2
< 404 Not Found

...

> GET /notes/private/11
< 404 Not Found
> GET /notes/private/12
< 200 OK
<
< Note 12: ECSC_080A02AF0J07UMOPHNE00KJ48KAE28C=

Router

  • veškerý provoz jde skrz
  • NATuje (i na IPv6)
  • (resetuje TTL)
  • (normalizuje HTTP hlavičky)

Vulnbox nesmí poznat,
s kým se baví!

Reálná soutěž

mnoho týmů, každý útočí a brání zároveň

existuje NOP tým

Ticky

  • Celá hra probíhá v ticích
  • Typicky 1 – 3 minuty
  • V jednom ticku
    • flagbot přidá do služby 1 vlajku
    • flagbot zkontroluje n¹ posledních vlajek
  • Útočníci útočí kdykoliv – častěji to ale nemá smysl

¹ typicky 3 – 6

Skórování

  • skóre se počítá za každý tick
    • + body za uptime = SLA
      • up, faulty, down, check fail, recovering
    • + body za ukradené vlajky = offence
    • - body za ztracené vlajky = defence
  • dynamické skóre
    • vlajka, kterou kradou všichni je za málo bodů

Flag submission

  • útočníci musí útočit pořád
  • FAUST: 1 vlajka × 8 služeb × ~100 týmů / 3 minuty
  • ECSC 2024: 1 vlajka × 8 služeb × 38 týmů / 2 minuty
  • jednoduché HTTP/TCP
  • 
    import requests
    
    TEAM_TOKEN = '4242424242424242'
    
    flags = ['AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=', 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=']
    
    print(requests.put('http://10.10.0.1:8080/flags', headers={
        'X-Team-Token': TEAM_TOKEN
    }, json=flags).text)
                                

Flag IDs

  • jednoznačně určují, kde je konkrétní vlajka
    • username vlastníka poznámky
    • ID dokumentu
    • souřadnice vesmírné lodi
    • číslo objednávky

Grace period

Čas kdy týmy mají přístup k vulnboxu, ale není otevřená síť

Obvykle ½ – 1 hodina

Quirky pravidel

Různá CTF mají různá pravidla

Přístup k vulnboxu

DIY
Full access
git only
  • orgové připraví image
  • týmy si hostují vulnbox
  • připojení pomocí VPN
  • patche aplikují týmy
    • FaustCTF
  • orgové hostují vulnboxy
  • mohou mít přístup
  • týmy mají root
    • ECSC 2024
    • RabaCTF
    • ENOWARS
  • týmy nemají přístup vůbec
  • změny se pushují přes git
  • z webUI se deployují commity
    • ECSC 2022
    • ECSC 2023

Smí se shazovat?

Ano, ale ne DoS
Ne
  • nesmí se zatěžovat služby a síť
    • ani zranitelnostmi
  • smí se zranitelností shodit služba
  • smí se mazat vlajky
    • FaustCTF
    • ECSC 2023
    • ENOWARS (možná?)
  • cizí tým nesmí kazit SLA
    • ECSC 2024
    • RabaCTF

Dynamické skórování

  • vlajka má pevné body, dělí se mezi týmy
  • vlajka má cenu podle aktuální pozice týmů
    • ECSC 2024
    • slabý tým útočí na silný = hodně bodů

Servicebox

Buďkyber!

Buďkyber

  • (virtuální) server
  • hostuje různé služby, které tým potřebuje
  • je taky připojený ve VPN (jako hráč)

Tulip 🌷

  • flow-based network monitoring
  • z vulnboxu se posílají .pcapy
  • ty se skládají a indexují do databáze
  • webové rozhraní
    • vyhledávání
    • sdílení
    • grafy
    • copy as
  • původně TeamEU (rdj 🇳🇱, Bazumo 🇨🇭, Šimon 🇨🇿)
  • velký pull request od CCT 🇨🇿, primárně Tonda

DestructiveFarm

  • exploit management
    • rozesílá flagIDs
    • sbírá vlajky
    • odesílá gameserveru
    • zaznamenává úspěšnost
    • řídí, na koho se útočí
  • distribuované spouštění exploitů
  • původně Ruského týmu Destructive Voice
  • většina týmů má vlastní, silně přepsanou variantu
  • u nás > 90 % 🇨🇿

Exploit runner

  • klientská knihovna pro DF
  • hráč napíše jednoduchý skript na kradení vlajky = exploit
  • runner:
    • řeší komunikaci s DF
    • spustí mnoho instancí exploitu paralelně
    • extrahuje vlajky z std. out

Runner v1

  • per-team
  • od 🇨🇿, původní DF pouštělo jednu instanci
  • JSON v argv, vlajky stdout

target = json.loads(sys.argv[1])
team_id = target["id"]
flag_ids = target.get("flag_ids", {})
time = target.get("time")

# Example of flag_ids: {
#   'service-1': [ 'flag-id-1', 'flag-id-2' ]
#   'service-2': [ 'flag-id-3', 'flag-id-4' ]
# }

flag_ids = flag_ids['service-1']

host = f"10.10.{team_id}.3"

# Anything printed to stdout or stderr is regex matched to find valid flags
# Flags are extracted and sent to the server
# Non-flag output is just printed on the client
print(f"Attacking {team_id=} at {host=}")

for flag_id in flag_ids:
    r = requests.get(f"http://{host}:3000/users/{flag_id}").text
    print(r)
                        

Runner v2

  • per-flagID
  • server poskytuje host info

info = json.loads(sys.argv[1]) if len(sys.argv) > 1 else {}

team_id = info.get("team_id")
service = info.get("service")
# Note that this could be `None` if DF doesn't provide host info
host = info.get("host")
# Flag id is present if the script is set to run per flag id
flag_id = info.get("flag_id")

# Anything printed to stdout or stderr is regex matched to find valid flags
# Flags are extracted and sent to the server
# Non-flag output is just printed out on the client
print(f"Attacking {service=} of {team_id=} at {host=} with {flag_id=}")

# Example: Generating some flags
# NOTE: These example flags will likely not be matched by your flag regex

r = requests.get(f"http://{host}:3000/users/{flag_id}").text
print(r)
                        
  • ~10 flagIDs × 100 týmů = 1000 exploitů běžících paralelně
  • typicky v pythonu ⇒ 1000 běžicích interpretů

Runner v3


from exploitlib import *

# class Team(id: str, name: str | None, display: str, metadata: dict[str, Any], services: dict[str, Service])
# class Service(id: str, name: str | None, host: str | None, metadata: dict[str, Any], flag_ids: list[FlagId])
# class FlagId(id: int, content: str, service: Service, team: Team, info: dict[str, Any], received: datetime, has_flag: bool)

class Exploit(ExploitBase):
    def process_tick(self) -> None:
        # This is called exacly once per attack_period
        # This is the first method to be called
        # State (stuff assigned to self) created here will be usable in process_team and process_flag_id
        print("Exploit.process_tick")

    def process_team(self, team: Team, service: Service | None) -> None:
        # This is called once for each team every attack_period
        # This will not be execute in --single-instance mode
        # State (stuff assigned to self) created here will be usable in process_flag_id
        print(f"Exploit.process_team: {team.display}, {service.display if service else None}")

    def process_flag_id(self, team: Team, service: Service, flag_id: FlagId) -> None:
        # This is called once for each flag id of every team every attack_period
        # This will not be execute in --single-instance and --per-team modes
        # State (stuff assigned to self) created here will be usable only here
        print(f"Exploit.process_flag_id: {team.display}, {service.display}, {flag_id.id}")

if __name__ == "__main__":
    main(Exploit)
                        

Rozdělování času exploitům

  • některé exploity jen komunikují po síti
  • → služby mohou leakovat spojení
  • jiné exploity bruteforcují krypto
  • → potřebují běžet dlouho

  • vlajky mají expiraci
  • jak férově rozdělit čas mezi exploity?
  • abychom získali nejvíc bodů?

anipicu-rs

  • staticky zkompilovaná filtrující TCP reverse proxy
  • dá se zabalit do neznámého docker kontejneru

$ ./anipicu-rs help
This is a self-modifying binary. Any config option you set will be reflected into the binary.
See example commands below. Any other command will start the proxy.

./anipicu-rs help                       (Show help)
./anipicu-rs config                     (Show config)
./anipicu-rs exec ./cmd arg1 arg2       (Set command to execute in the background)
./anipicu-rs exec                       (Clear exec command)
./anipicu-rs bind 0.0.0.0:5000          (Set address to bind to, required)
./anipicu-rs forward 127.0.0.1:6000     (Set address to forward to, required if forward-exec is not set)
./anipicu-rs forward-exec ./cmd arg1    (Set command to spawn and forward to, required if forward is not set)
./anipicu-rs message NE                 (Set shutdown message)
./anipicu-rs linger                     (Toggles leaking of connections)
./anipicu-rs unpack-target /meme        (Set target path to unpack archive)
./anipicu-rs whitelist-add 'y[eE][eE]t' (Add regex to whitelist)
./anipicu-rs whitelist-remove 3         (Remove regex at index from whitelist)
./anipicu-rs blacklist-add 'y[eE][eE]t' (Add regex to blacklist)
./anipicu-rs blacklist-remove 3         (Remove regex at index from blacklist)
./anipicu-rs ip-whitelist-add 1.1.1.1   (Add address to whitelist)
./anipicu-rs ip-whitelist-remove 3      (Remove address at index from whitelist)
./anipicu-rs ip-blacklist-add 1.1.1.1   (Add address to blacklist)
./anipicu-rs ip-blacklist-remove 3      (Remove address at index from blacklist)
./anipicu-rs tar-add-file in/tar ./src  (Add file to archive)
./anipicu-rs tar-add-dir in/tar ./src   (Add recursive directory to archive)
./anipicu-rs tar-remove in/tar          (Remove file or directory from archive)
./anipicu-rs reset                      (Clear all storage and restore defaults)
                        

Statek

  • sbírá data z různých zdrojů
    • DF
    • Scoreboard
  • dopočítává užitečné metriky
    • jaké týmy kradou vlajky konkrétní služby?
    • jaké týmy neztrácí vlajky?
    • ztrácime víc bodů za defense než dostáváme za SLA?
  • zobrazuje v Grafaně

Vulnbox

  • nutné udržovat pořádek, neinstalovat neofetch
  • infrastruktura je extrémně minimalistická
  • musí běžet na různých systémech

Git


CLONE_HOST=${CLONE_HOST-"10.60.10.1"} # with square brackets if IPv6
CLONE_USER=${CLONE_USER-"root"}

if [ -z "$(git config --global init.defaultBranch)" ]; then
    git config --global init.defaultBranch master
fi

if [ -z "$(git config --global user.email)" ]; then
    git config --global user.email "${CLONE_USER}@${CLONE_HOST}"
    git config --global user.name "Vulnbox"
fi

for repo in "$@"; do
    git init "$repo"
    (cd "$repo" && git config --local receive.denyCurrentBranch updateInstead)
    (cd "$repo" && git config --local receive.denyNonFastForwards true)
    (cd "$repo" && git config --local receive.denyDeletes true)
done

for repo in "$@"; do
    echo "git clone \"$CLONE_USER@$CLONE_HOST\":$(realpath "$repo")"
done
                        

Packet capture


URL=${URL-"https://nop:nop@collector.budkyber.cz"}
INTERFACE=${INTERFACE-"eth0"}
PREFIX=${PREFIX-$(hostname)}

mkdir -p /tmp/tcpdump
chmod 777 /tmp/tcpdump

echo '#!/usr/bin/env bash
set -e

curl -XPOST -F file=@"$1" -F prefix="'"$PREFIX"'" "'"$URL"'"
rm -v "$1"
' >/tmp/tcpdump/upload.sh
chmod +x /tmp/tcpdump/upload.sh


for pcap in /tmp/tcpdump/pcap-*; do
	/tmp/tcpdump/upload.sh "$pcap"
done

tcpdump -ni "$INTERFACE" -G15 -C 1000 -w /tmp/tcpdump/pcap-%s.pcap -z /tmp/tcpdump/upload.sh \
        not port 22 and not port 443 and not port 6256
                        
Join us!

Odkazy