From 71ad18cfb2b8631e4856f6439ae017a4249fa500 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=88=9A=28noham=29=C2=B2?= <100566912+NohamR@users.noreply.github.com> Date: Tue, 18 Feb 2025 21:16:36 +0100 Subject: [PATCH] ecsc support --- README.md | 5 +- main.py | 14 ++- src/models.py | 4 +- src/platforms/ecsc.py | 217 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 236 insertions(+), 4 deletions(-) create mode 100644 src/platforms/ecsc.py diff --git a/README.md b/README.md index 2c006bc..61b93fa 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,14 @@ A Python tool to automatically generate CTF writeup templates and organize chall - https://cattheflag.org/defis.php - https://imaginaryctf.org - https://www.root-me.org +- https://challenges.ecsc.eu/challenges ### Will consider adding : -- https://challenges.ecsc.eu/challenges - https://www.hackthissite.org +### To do : +- Add stars number of [crackmes.one](https://crackmes.one) (down for now :/) + ## Features - Automated writeup template generation diff --git a/main.py b/main.py index 1b9e1e1..cf50562 100644 --- a/main.py +++ b/main.py @@ -5,6 +5,7 @@ from src.platforms.crackmy import CrackmyPlatform from src.platforms.cattheflag import CatTheFlagPlatform from src.platforms.imaginaryctf import ImaginaryCTFPlatform from src.platforms.rootme import RootMePlatform +from src.platforms.ecsc import ECSCPlatform from src.generator import WriteupGenerator from pathlib import Path @@ -23,6 +24,7 @@ def theblackside(): generator = WriteupGenerator(platform, Path("./writeups")) generator.fetch_challenge(challenge_url=challenge_url) + print(generator.challenges) generator.generate_writeup_structure(hugo_header=True, translated=True) def crackmes(): @@ -74,10 +76,20 @@ def rootme(): print(generator.challenges) generator.generate_writeup_structure(hugo_header=True, translated=True) +def ecsc(): + challenge_url = 'https://challenges.ecsc.eu/challenges/binary' + platform = ECSCPlatform() + + generator = WriteupGenerator(platform, Path("./writeups")) + generator.fetch_challenge(challenge_url=challenge_url) + print(generator.challenges) + generator.generate_writeup_structure(hugo_header=True, translated=True) + # theblackside() # hackropole() # crackmes() # crackmy() # cattheflag() # imaginaryctf() -# rootme() \ No newline at end of file +# rootme() +# ecsc() \ No newline at end of file diff --git a/src/models.py b/src/models.py index 67d470b..a8dc513 100644 --- a/src/models.py +++ b/src/models.py @@ -21,5 +21,5 @@ class Challenge: @dataclass class File: name: str - hash: str - url: str \ No newline at end of file + url: str + hash: Optional[str] = None \ No newline at end of file diff --git a/src/platforms/ecsc.py b/src/platforms/ecsc.py new file mode 100644 index 0000000..23d4ae5 --- /dev/null +++ b/src/platforms/ecsc.py @@ -0,0 +1,217 @@ +from typing import List, Dict +from pathlib import Path +import requests +from .base import CTFPlatform +from ..models import Challenge, File +from ..utils.challenge_handler import download_files +from bs4 import BeautifulSoup +from datetime import datetime +import textwrap +import re + +class ECSCPlatform(CTFPlatform): + def __init__(self, url: str = "https://challenges.ecsc.eu"): + super().__init__(url) + self.headers = { + 'Accept': 'text/css,*/*;q=0.1', + 'Accept-Language': 'fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + 'Pragma': 'no-cache', + 'Referer': 'https://challenges.ecsc.eu/', + 'Sec-Fetch-Dest': 'style', + 'Sec-Fetch-Mode': 'no-cors', + 'Sec-Fetch-Site': 'same-origin', + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36', + 'sec-ch-ua': '"Google Chrome";v="135", "Not-A.Brand";v="8", "Chromium";v="135"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '"macOS"', + } + + def login(self) -> bool: + """Not implementing login since we don't need it :D""" + return True + + def get_challenges(self) -> List[Challenge]: + raise NotImplementedError("Method not implemented") + + def get_challenge(self, challenge_url: str) -> Challenge: + """Get a specific challenge by URL""" + try: + response = self.session.get(challenge_url, headers=self.headers) + if response.status_code != 200: + raise Exception( + f"Error fetching challenge {challenge_url}: {response.status_code}" + ) + except requests.RequestException as e: + raise Exception(f"Error fetching challenge {challenge_url}: {e}") + + response.encoding = "utf-8" # Force UTF-8 encoding + soup = BeautifulSoup(response.text, "html.parser") + + + title = soup.find('h1', class_='documentFirstHeading').text.strip() + id = title.lower().replace(' ', '-') # Using lowercase title as ID + + description_elem = soup.find('span', text='Description').find_next('span') + description = description_elem.text.strip() if description_elem else "" + + difficulty_elem = soup.find('span', text='Difficulty').find_next('div', class_='difficulty') + difficulty = difficulty_elem.find('span').text.strip() if difficulty_elem else "Unknown" + + provider_elem = soup.find('span', text='Provider').find_next('span') + author = provider_elem.text.strip() if provider_elem else "Unknown" + + tags_elem = soup.find('span', text='Tags').find_next('span', class_='challenge-tags') + category = tags_elem.find('span').text.strip() if tags_elem else "Uncategorized" + + files = [] + # write_ups_elem = soup.find('span', text='Write-ups').find_next('ul', class_='other-artefacts') + # if write_ups_elem: + # for file in write_ups_elem.find_all('a'): + # files.append({ + # 'name': file.text.strip(), + # 'url': file['href'] + # }) + + other_files_elem = soup.find('span', text='Other artefacts').find_next('ul', class_='other-artefacts') + if other_files_elem: + for file in other_files_elem.find_all('a'): + files.append(File(name=file.text.strip(), url=file['href'])) + + additional_info_elem = soup.find('span', text='Additional Info').find_next('span') + additional_info = { + 'event': soup.find('span', text='Event').find_next('span').text.strip(), + 'extra': additional_info_elem.text.strip() if additional_info_elem else "" + } + + return Challenge( + id=id, + url=challenge_url, + platform="ECSC", + name=title, + author=author, + category=category, + description=description, + difficulty=difficulty, + points=None, + files=files, + additional_info=additional_info + ) + + def download_challenge_files(self, challenge: Challenge, output_dir: Path): + download_files(self, challenge, output_dir) + + def generate_tags(self, challenge): + tags = [] + tags.append(challenge.category) + tags.append(challenge.platform) + tags = list(set(filter(None, tags))) + tags_str = '", "'.join(tags) + return tags_str + + def generate_template(self, challenge: Challenge, hugo_header: bool = False, translated: bool = False): + """Generate writeup template for challenge""" + + tags_str = self.generate_tags(challenge) + + hugo_header_template = textwrap.dedent( + f"""\ + --- + title: "{challenge.name}" + date: "{datetime.now().isoformat()}" + tags: ["{tags_str}"] + author: "Noham" + summary: "Writeup for {challenge.name} from {challenge.platform}. A {challenge.category.lower()} challenge with a "{challenge.difficulty.lower()}" difficulty." + showToc: false + TocOpen: false + draft: false + hidemeta: false + comments: true + disableHLJS: false + disableShare: false + hideSummary: false + searchHidden: false + ShowReadingTime: true + ShowBreadCrumbs: true + searchHidden: true + ShowPostNavLinks: true + ShowWordCount: true + ShowRssButtonInSectionTermList: true + UseHugoToc: true + --- + """ + ) + + hugo_header_template_fr = textwrap.dedent( + f"""\ + --- + title: "{challenge.name}" + date: "{datetime.now().isoformat()}" + tags: ["{tags_str}"] + author: "Noham" + summary: "Writeup pour {challenge.name} de {challenge.platform}. Un challenge {challenge.category.lower()} de difficulté {challenge.difficulty.lower()}." + showToc: false + TocOpen: false + draft: false + hidemeta: false + comments: true + disableHLJS: false + disableShare: false + hideSummary: false + searchHidden: false + ShowReadingTime: true + ShowBreadCrumbs: true + searchHidden: true + ShowPostNavLinks: true + ShowWordCount: true + ShowRssButtonInSectionTermList: true + UseHugoToc: true + --- + """ + ) + + difficulty_map = {"Easy": 1, "Medium": 2, "Hard": 3} + star_count = difficulty_map.get(challenge.difficulty, 1) + stars = "⭐" * star_count + + files_section = "" + for file in challenge.files: + hash_text = f" *(SHA256: {file.hash})*" if file.hash else "" + files_section += f"[{file.name}]({file.url}){hash_text}, " + files_section = files_section.rstrip(", ") + + main_content = textwrap.dedent( + f"""\ + - Challenge URL: [{challenge.name} - {challenge.platform}]({challenge.url}) + - Author: {challenge.author} + - Category: {challenge.category} + - Challenge description: {challenge.description} + - Difficulty: {stars} ({challenge.difficulty}) + - Files provided: {files_section} + + ## Writeup + """ + ) + + main_content_fr = textwrap.dedent( + f"""\ + - URL du challenge: [{challenge.name} - {challenge.platform}]({challenge.url}) + - Auteur: {challenge.author} + - Catégorie: {challenge.category} + - Description du challenge: {challenge.description} + - Difficulté: {stars} ({challenge.difficulty}) + - Fichiers fournis: {files_section} + + ## Writeup + """ + ) + + if hugo_header: + challenge.template = hugo_header_template + main_content + if translated: + challenge.template_translated = hugo_header_template_fr + main_content_fr + else: + challenge.template = main_content + if translated: + challenge.template_translated = main_content_fr \ No newline at end of file