diff --git a/challenges/cybernet/Dockerfile b/challenges/cybernet/Dockerfile new file mode 100644 index 0000000..2b32bfd --- /dev/null +++ b/challenges/cybernet/Dockerfile @@ -0,0 +1,9 @@ +FROM python:3-slim + +ARG APP=/opt/app/ +WORKDIR $APP + +RUN mkdir -p $APP +COPY server.py $APP/ + +CMD ["python3", "server.py"] diff --git a/challenges/cybernet/README.md b/challenges/cybernet/README.md new file mode 100644 index 0000000..24bd946 --- /dev/null +++ b/challenges/cybernet/README.md @@ -0,0 +1,142 @@ +# Cybernet + +This challenge is about understanding of a C&C protocol. Students are given an IP address where a simple TCP C&C server +is hosted. Students should implement a client to fulfil all the servers' commands to eventually receive a flag. + +## How to solve +
+ Click to reveal how to solve steps + +TODO: write proper how to solve steps in more detail + +1. First, we find the port of the server using nmap and try to connect +```bash +root@hackerlab:~# nmap 172.20.0.52 +Starting Nmap 7.93 ( https://nmap.org ) at 2024-11-26 22:46 UTC +Nmap scan report for scl-challenge-cybernet.playground-net (172.20.0.52) +Host is up (0.0000060s latency). +Not shown: 999 closed tcp ports (reset) +PORT STATE SERVICE +4444/tcp open krb524 +MAC Address: 02:42:AC:14:00:34 (Unknown) + +Nmap done: 1 IP address (1 host up) scanned in 0.27 seconds +root@hackerlab:~# nc 172.20.0.52 4444 + + _ _ + ___ _ _| |__ ___ _ __ _ __ ___| |_ + / __| | | | '_ \ / _ \ '__| '_ \ / _ \ __| +| (__| |_| | |_) | __/ | | | | | __/ |_ + \___|\__, |_.__/ \___|_| |_| |_|\___|\__| + |___/ + + + + What do you want to do? +1 - Join +2 - Get a command +3 - Get a flag +4 - Quit +``` + +2. After that, we should interact with the server to understand its mechanism and all possible commands that proper bots should implement. One example of a working bot client implemented in Python is given below. If ran, it fulfils all servers' tasks and prints the flag +```python3 +import socket +import hashlib + +def sendline_after(sock: socket.socket, prompt: bytes, line: bytes): + while True: + data = sock.recv(4096) + if prompt in data: + sock.sendall(line + b"\n") + return + + +def recvline_startswith(sock: socket.socket, prefix: bytes) -> str: + while True: + data = sock.recv(4096) + lines = data.splitlines() + for line in lines: + if line.startswith(prefix): + return line.decode() + + +def join(sock: socket.socket): + sendline_after(sock, b"Quit", b"1") + + prefix = ' To join you must prove you are a bot and calculate me ' + line = recvline_startswith(sock, prefix.encode()) + print(f"#### received challenge:\n{line}") + + expression = line[len(prefix):].strip() + solution = str(eval(expression)) # yea I like living on the edge haha + print(f"### sending solution:\n{solution}\n") + sock.sendall(solution.encode() + b"\n") + + +def solve_command(line: str, local_ip: str) -> bytes: + line = line.strip() + if "Send me your machine's hostname." in line: + return b"yea I am sure the master does not check this" + + elif line.startswith(" Send me a SHA-256 of this string '"): + data = line[-21:-1] + return hashlib.sha256(data.encode()).hexdigest().encode() + + elif line.startswith(" Send me a 2 digit country code for this place: '"): + prefix = " Send me a 2 digit country code for this place: '" + place = line[len(prefix):-1] + resp = { + "Tokyo": "JP", + "Stonehenge": "GB", + "Machu Picchu": "PE", + "The Shire": "" + }[place] + return resp.encode() + + else: + raise Exception(f"unknown command '{line}'") + + +def do_commands(sock: socket.socket, local_ip: str, n: int = 10): + for i in range(n): + # Choose command + sendline_after(sock, b"Quit", b"2") + + line = recvline_startswith(sock, b"") + print(f"### received command:\n{line}") + + answer = solve_command(line, local_ip) + print(f"### sending answer:\n{answer}\n") + sock.sendall(answer + b"\n") + + +def get_flag(sock: socket.socket): + sendline_after(sock, b"Quit", b"3") + + prefix = " Ok then, here is your flag " + line = recvline_startswith(sock, prefix.encode()) + + flag = line[len(prefix):] + print(f"Got flag {flag}") + return flag + + +def run(server_ip: str, port: int): + with socket.create_connection((server_ip, port)) as sock: + local_ip = sock.getsockname()[0] + + join(sock) + do_commands(sock, local_ip, n=10) + get_flag(sock) + + +if __name__ == "__main__": + run(server_ip="172.20.0.52", port=4444) +``` + +
+ +## Testing + +The script [auto-solve.sh](./auto-solve.sh) automatically verifies that the challenge can be solved. diff --git a/challenges/cybernet/auto-solve.sh b/challenges/cybernet/auto-solve.sh new file mode 100755 index 0000000..b64b674 --- /dev/null +++ b/challenges/cybernet/auto-solve.sh @@ -0,0 +1,126 @@ +#!/bin/bash + +# Python client as a hardcoded string +PYTHON_CODE=$(cat << 'EOF' + +import socket +import hashlib + +def sendline_after(sock: socket.socket, prompt: bytes, line: bytes): + while True: + data = sock.recv(4096) + if prompt in data: + sock.sendall(line + b"\n") + return + + +def recvline_startswith(sock: socket.socket, prefix: bytes) -> str: + while True: + data = sock.recv(4096) + lines = data.splitlines() + for line in lines: + if line.startswith(prefix): + return line.decode() + + +def join(sock: socket.socket): + sendline_after(sock, b"Quit", b"1") + + prefix = ' To join you must prove you are a bot and calculate me ' + line = recvline_startswith(sock, prefix.encode()) + print(f"#### received challenge:\n{line}") + + expression = line[len(prefix):].strip() + solution = str(eval(expression)) # yea I like living on the edge haha + print(f"### sending solution:\n{solution}\n") + sock.sendall(solution.encode() + b"\n") + + +def solve_command(line: str, local_ip: str) -> bytes: + line = line.strip() + if "Send me your machine's hostname." in line: + return b"yea I am sure the master does not check this" + + elif line.startswith(" Send me a SHA-256 of this string '"): + data = line[-21:-1] + return hashlib.sha256(data.encode()).hexdigest().encode() + + elif line.startswith(" Send me a 2 digit country code for this place: '"): + prefix = " Send me a 2 digit country code for this place: '" + place = line[len(prefix):-1] + resp = { + "Tokyo": "JP", + "Stonehenge": "GB", + "Machu Picchu": "PE", + "The Shire": "" + }[place] + return resp.encode() + + else: + raise Exception(f"unknown command '{line}'") + + +def do_commands(sock: socket.socket, local_ip: str, n: int = 10): + for i in range(n): + # Choose command + sendline_after(sock, b"Quit", b"2") + + line = recvline_startswith(sock, b"") + print(f"### received command:\n{line}") + + answer = solve_command(line, local_ip) + print(f"### sending answer:\n{answer}\n") + sock.sendall(answer + b"\n") + + +def get_flag(sock: socket.socket): + sendline_after(sock, b"Quit", b"3") + + prefix = " Ok then, here is your flag " + line = recvline_startswith(sock, prefix.encode()) + + flag = line[len(prefix):] + print(f"Got flag {flag}") + return flag + + +def run(server_ip: str, port: int): + with socket.create_connection((server_ip, port)) as sock: + local_ip = sock.getsockname()[0] + + join(sock) + do_commands(sock, local_ip, n=10) + get_flag(sock) + + +if __name__ == "__main__": + run(server_ip="172.20.0.52", port=4444) + +EOF +) + +# Specify the output Python file +OUTPUT_FILE="/tmp/auto-solve-cybernet.py" + +echo "$PYTHON_CODE" > $OUTPUT_FILE +chmod +x $OUTPUT_FILE +MATCH=`python3 $OUTPUT_FILE | grep -o "BSY{eBhnIBKYNuHlaAKk2JhoDCBGJLJFogAk6uQ7gQxU89IADQGpe2ATVAbyagAg}"` +if [[ "$MATCH" == "" ]] +then + echo "Error - the Python client did not manage to get the flag" + exit 1 +fi + +# submit a flag in the submission server +RES=`curl -s 'http://172.20.0.3/api/challenges/submit' \ + -X POST \ + -H 'Content-Type: application/json' \ + --data-binary '{"challenge_id": "cybernet", "task_id": "task1", "flag" : "BSY{eBhnIBKYNuHlaAKk2JhoDCBGJLJFogAk6uQ7gQxU89IADQGpe2ATVAbyagAg}"}'` +if [[ $RES != *"Congratulations"* ]]; then + echo "Failed to submit the flag - $RES" + exit 2 +fi + + +echo "OK - tests passed" +exit 0 diff --git a/challenges/cybernet/docker-compose.yml b/challenges/cybernet/docker-compose.yml new file mode 100644 index 0000000..1e20615 --- /dev/null +++ b/challenges/cybernet/docker-compose.yml @@ -0,0 +1,20 @@ +version: '3.3' + +services: + challenge-cybernet: + container_name: scl-challenge-cybernet + stop_grace_period: 0s + build: . + networks: + playground-net: + ipv4_address: 172.20.0.52 + healthcheck: + test: ["CMD", "python", "-c", "'import requests; response = requests.get(\"http://localhost/\"); assert response.status_code == 200'"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + +networks: + playground-net: + external: true \ No newline at end of file diff --git a/challenges/cybernet/meta.json b/challenges/cybernet/meta.json new file mode 100644 index 0000000..d398f6d --- /dev/null +++ b/challenges/cybernet/meta.json @@ -0,0 +1,14 @@ +{ + "name": "Cybernet", + "id": "cybernet", + "difficulty": "medium", + "description": "", + "tasks": [ + { + "id": "task1", + "name": "Understand the master", + "description": "You are a botnet researcher. You were just told there is a C2 server hosted at ip address 172.20.0.52. You should understand its protocol fast!", + "flag": "BSY{eBhnIBKYNuHlaAKk2JhoDCBGJLJFogAk6uQ7gQxU89IADQGpe2ATVAbyagAg}" + } + ] + } \ No newline at end of file diff --git a/challenges/cybernet/server.py b/challenges/cybernet/server.py new file mode 100644 index 0000000..e262267 --- /dev/null +++ b/challenges/cybernet/server.py @@ -0,0 +1,225 @@ +import hashlib +import logging +import random +import secrets +import socket +import string +import threading +import time + +logging.basicConfig(level=logging.DEBUG) + +banner = """ + _ _ + ___ _ _| |__ ___ _ __ _ __ ___| |_ + / __| | | | '_ \ / _ \ '__| '_ \ / _ \ __| +| (__| |_| | |_) | __/ | | | | | __/ |_ + \___|\__, |_.__/ \___|_| |_| |_|\___|\__| + |___/ +""" + +menu = """ + What do you want to do? +1 - Join +2 - Get a command +3 - Get a flag +4 - Quit +""" + +flag = "BSY{eBhnIBKYNuHlaAKk2JhoDCBGJLJFogAk6uQ7gQxU89IADQGpe2ATVAbyagAg}" + +class ThreadedServer(object): + def __init__(self, host, port): + self.host = host + self.port = port + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.sock.bind((self.host, self.port)) + + def listen(self): + self.sock.listen(5) + while True: + client, address = self.sock.accept() + ts = time.localtime() + logging.info(f"[*] Received connection from {client}:{address} at {time.strftime('%Y-%m-%d %H:%M:%S', ts)}") + client.settimeout(60) + self.timestamp = time.strftime("%Y-%m-%d %H:%M:%S", ts) + thread = threading.Thread(target=self.listen_to_client, args=(client, address)) + thread.start() + + def check_pow(self, response, difficulty, challenge): + h = hashlib.sha256() + h.update(response.encode()) + + return h.hexdigest().startswith('0' * 2 * difficulty) and response.startswith(challenge) + + def listen_to_client(self, client, address): + start_time = time.time() + size = 64 * 1024 + max_time = 3 # in seconds + num_commands = 10 + CMD_COUNTRY = 0 + CMD_SHA256 = 1 + CMD_HOSTNAME = 2 + CMD_CORES = 3 + MENU_REG = 1 + MENU_CMD = 2 + MENU_FLAG = 3 + MENU_QUIT = 4 + + client.sendall(bytes(banner + "\n\n" + menu, encoding='utf-8')) + + joined = False + commands_executed = 0 + flag_sent = False + + try: + while (not joined or commands_executed < num_commands or not flag_sent): + data = client.recv(size).decode().strip() + try: + menu_rcvd = int(data) + except ValueError as e: + logging.info(f"Exception {e}") + client.sendall(bytes(f" {data} is not an integer!\n", encoding="latin")) + client.close() + return False + + # Check the time and exit if it takes them too much time to decide + if time.time() - start_time > max_time: + logging.info(f"Timeout!") + client.sendall(b" Oh no, human?! Bye bye...!\n") + client.close() + return False + + logging.info(f"Received data: {data}") + if menu_rcvd == MENU_REG: + op = random.choice(["+", "*", "-"]) + expression = f"{random.randint(0, 100)} {op} {random.randint(0, 100)}" + client.sendall(bytes(f" To join you must prove you are a bot and calculate me {expression}\n", encoding="utf-8")) + + data = client.recv(size).strip() + expected = str(eval(expression)) + if expected == data.decode().strip(): + joined = True + client.sendall(b" You have successfully joined!\n") + else: + client.sendall(b" Incorrect! Goodbye!\n") + client.close() + return False + + elif menu_rcvd == MENU_CMD: + if not joined: + client.sendall(b" You need to join first.\n") + else: + # Choose a command randomly + cmd = random.randint(0, 2) + if cmd == CMD_HOSTNAME: + client.sendall(b" Send me your machine's hostname.\n") + data = client.recv(size) + try: + _ = data.strip().decode("ascii") + commands_executed += 1 + client.sendall(b" Got it.\n") + except ValueError as e: + logging.info(f"Exception {e}") + client.close() + return False + + elif cmd == CMD_CORES: + client.sendall(b" Send me number of cores your machine has.\n") + data = client.recv(size) + try: + _ = int(data.strip().decode("ascii")) + commands_executed += 1 + client.sendall(b" Got it.\n") + except ValueError: + client.sendall(b" That was not a valid response. Goodbye!\n") + client.close() + return False + + elif cmd == CMD_SHA256: + alph = string.ascii_letters + string.digits + to_hash = ''.join([secrets.choice(alph) for _ in range(20)]) + client.sendall(f" Send me a SHA-256 of this string '{to_hash}'\n".encode()) + data = client.recv(size) + + expected = hashlib.sha256(to_hash.encode()).hexdigest() + try: + got = data.strip().decode("ascii") + if got != expected: + logging.info(f"Expected {expected} but got {got}") + client.sendall(b" Incorrect! Goodbye!\n") + client.close() + return False + + commands_executed += 1 + client.sendall(b" Correct.\n") + except Exception as e: + logging.info(f"Exception {e}") + client.close() + return False + + elif cmd == CMD_COUNTRY: + places_map = { + "Tokyo": "JP", + "Stonehenge": "GB", + "Machu Picchu": "PE", + "The Shire": "" + } + place = random.choice(list(places_map.keys())) + expected = places_map[place] + + client.sendall(f" Send me a 2 digit country code for this place: '{place}'\n".encode()) + data = client.recv(size) + + try: + got = data.strip().decode("ascii") + if expected != "" and got != expected: + logging.info(f"Expected {expected} but got {got}") + client.sendall(b" Incorrect! Goodbye!\n") + client.close() + return False + + commands_executed += 1 + client.sendall(b" Correct.\n") + except Exception as e: + logging.info(f"Exception {e}") + client.close() + return False + + elif menu_rcvd == MENU_FLAG: + if not joined: + client.sendall(bytes(" You need to join first.\n", encoding='utf-8')) + elif commands_executed < num_commands: + client.sendall( + bytes(f" You need to answer at least {num_commands} commands first.\n", + encoding='utf-8')) + else: + client.sendall(bytes(f" Ok then, here is your flag {flag}\n", encoding='utf-8')) + flag_sent = True + + elif menu_rcvd == MENU_QUIT: + client.sendall(b" Goodbye!\n") + client.close() + return False + else: + client.sendall(bytes(" This is not what I expected. Try harder.", encoding='utf-8')) + client.close() + return False + + # If the command was successful display the menu again and go at the beginning of the while loop + time.sleep(0.1) + client.sendall(bytes(menu, encoding='utf-8')) + + except ConnectionResetError as e: + logging.info(f"Client forced connection Reset") + + except Exception as e: + logging.info(f"Unexpected exception {e}") + + +if __name__ == "__main__": + logging.info("Server starting") + + PORT_NUM = 4444 + ThreadedServer('0.0.0.0', PORT_NUM).listen() diff --git a/docs/development.md b/docs/development.md index 4e5147a..d4b8d02 100644 --- a/docs/development.md +++ b/docs/development.md @@ -45,12 +45,13 @@ | playground-net | `172.20.0.10` | Challenge [Famous Quotes LFI](./../challenges/famous-quotes-lfi/) | | playground-net | `172.20.0.30` | Challenge [What's the date?](./../challenges/what-is-the-date/) | | playground-net | `172.20.0.35` | Challenge [What's that noise?](./../challenges/what-is-that-noise/) | - | playground-net | `172.20.0.39` | Callenge [Shockwave Report](./../challenges/shockwave-report) | - | playground-net | `172.20.0.41` | Callenge [Intrusion](./../challenges/intrusion) | - | playground-net | `172.20.0.45` | Callenge [Jump Around](./../challenges/jump-around) | - | playground-net | `172.20.0.47` | Callenge [Jump Around](./../challenges/jump-around) | - | playground-net | `172.20.0.49` | Callenge [Jump Around](./../challenges/jump-around) | - | playground-net | `172.20.0.67` | Callenge [Leet Messenger](./../challenges/leet-messenger) | + | playground-net | `172.20.0.39` | Challenge [Shockwave Report](./../challenges/shockwave-report) | + | playground-net | `172.20.0.41` | Challenge [Intrusion](./../challenges/intrusion) | + | playground-net | `172.20.0.45` | Challenge [Jump Around](./../challenges/jump-around) | + | playground-net | `172.20.0.47` | Challenge [Jump Around](./../challenges/jump-around) | + | playground-net | `172.20.0.49` | Challenge [Jump Around](./../challenges/jump-around) | + | playground-net | `172.20.0.67` | Challenge [Leet Messenger](./../challenges/leet-messenger) | + | playground-net | `172.20.0.52` | Challenge [Cybernet](./../challenges/cybernet) | | playground-net | `172.20.0.88` | [Class02](./../classes/class02) | | playground-net | `172.20.0.90` | [Class03](./../classes/class03) | | playground-net | `172.20.0.95` | [Class03](./../classes/class03) |