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