From 9ac8125be099c45afa00aa0d94dd06080e2867d3 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 17:15:16 +0100 Subject: [PATCH] fix + cattheflag support + crackmy support --- README.md | 2 + main.py | 30 +++- src/generator.py | 19 ++- src/platforms/cattheflag.py | 276 ++++++++++++++++++++++++++++++++++++ src/platforms/crackmy.py | 252 ++++++++++++++++++++++++++++++++ src/platforms/hackropole.py | 4 +- 6 files changed, 565 insertions(+), 18 deletions(-) create mode 100644 src/platforms/cattheflag.py create mode 100644 src/platforms/crackmy.py diff --git a/README.md b/README.md index f9ff573..0d73369 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,10 @@ A Python tool to automatically generate CTF writeup templates and organize chall - https://hackropole.fr - https://theblackside.fr - https://crackmes.one +- https://crackmy.app ### Will add : +- https://cattheflag.org/defis.php - https://imaginaryctf.org - https://challenges.ecsc.eu/challenges - https://www.root-me.org diff --git a/main.py b/main.py index 261207e..13f1fa0 100644 --- a/main.py +++ b/main.py @@ -1,14 +1,13 @@ from src.platforms.hackropole import HackropolePlatform from src.platforms.theblackside import TheBlackSidePlatform from src.platforms.crackmes import CrackmesPlatform +from src.platforms.crackmy import CrackmyPlatform +from src.platforms.cattheflag import CatTheFlagPlatform from src.generator import WriteupGenerator from pathlib import Path def hackropole(): challenge_url = 'https://hackropole.fr/fr/challenges/reverse/fcsc2023-reverse-chaussette-xs/' - # challenge_url = 'https://hackropole.fr/fr/challenges/crypto/fcsc2022-crypto-t-rex/' - # challenge_url = 'https://hackropole.fr/fr/challenges/crypto/fcsc2022-crypto-a-laise/' - platform = HackropolePlatform() generator = WriteupGenerator(platform, Path("./writeups")) @@ -33,6 +32,29 @@ def crackmes(): print(generator.challenges) generator.generate_writeup_structure(hugo_header=True, translated=True) +def crackmy(): + challenge_url = 'https://crackmy.app/crackmes/yet-another-packer-v1-5514' + # platform = CrackmyPlatform(config_file="crackmy.json") + platform = CrackmyPlatform() + + 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 cattheflag(): + challenge_url = 'https://cattheflag.org/defis/reverse2.php' + platform = CatTheFlagPlatform(config_file="catthefile.json") + + generator = WriteupGenerator(platform, Path("./writeups")) + generator.fetch_challenges() # Mandatory to get every information about a specific challenge for this platform + generator.fetch_challenge(challenge_url=challenge_url) + print(generator.challenges) + generator.generate_writeup_structure(hugo_header=True, translated=True) + + # theblackside() # hackropole() -crackmes() \ No newline at end of file +# crackmes() +# crackmy() +# cattheflag() \ No newline at end of file diff --git a/src/generator.py b/src/generator.py index 0f4b497..a180d2c 100644 --- a/src/generator.py +++ b/src/generator.py @@ -8,7 +8,7 @@ class WriteupGenerator: def __init__(self, platform: CTFPlatform, output_dir: Path): self.platform = platform self.output_dir = output_dir - self.challenges = [] + self.challenges = {} def fetch_challenges(self): """Fetch all challenges from the platform""" @@ -27,6 +27,10 @@ class WriteupGenerator: platform_dir.mkdir(parents=True, exist_ok=True) challenge_dir = platform_dir / self._sanitize_filename(challenge.id) + if challenge_dir.exists(): + print(f"Challenge directory for {challenge.id} already exists. Skipping...") + continue + challenge_dir.mkdir(exist_ok=True) files_dir = challenge_dir / "files" @@ -35,18 +39,11 @@ class WriteupGenerator: self.platform.generate_template(challenge, hugo_header, translated) - if (challenge_dir / "index.md").exists(): - print(f"Writeup for {challenge.id} already exists. Skipping...") - continue - else: - (challenge_dir / "index.md").write_text(challenge.template) + (challenge_dir / "index.md").write_text(challenge.template) if translated: - if (challenge_dir / "index.fr.md").exists(): - print(f"Writeup for {challenge.id} already exists. Skipping...") - continue - else: - (challenge_dir / "index.fr.md").write_text(challenge.template_translated) + (challenge_dir / "index.fr.md").write_text(challenge.template_translated) + print(f"Writeup for {challenge.id} has been generated in {challenge_dir}") @staticmethod diff --git a/src/platforms/cattheflag.py b/src/platforms/cattheflag.py new file mode 100644 index 0000000..41602bf --- /dev/null +++ b/src/platforms/cattheflag.py @@ -0,0 +1,276 @@ +from typing import List, Dict +from pathlib import Path +from .base import CTFPlatform +from ..models import Challenge, File +from ..utils.config_handler import load_config +from ..utils.challenge_handler import download_files +from bs4 import BeautifulSoup +import unicodedata +from datetime import datetime +import textwrap +import requests +import re +from pprint import pprint + +class CatTheFlagPlatform(CTFPlatform): + def __init__(self, url: str = "https://cattheflag.org", config_file: str | Path = None): + super().__init__(url) + self.url = url + self.headers = { + 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', + 'accept-language': 'fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7', + 'cache-control': 'no-cache', + 'content-type': 'application/x-www-form-urlencoded', + 'origin': 'https://cattheflag.org', + 'pragma': 'no-cache', + 'priority': 'u=0, i', + 'referer': 'https://cattheflag.org/connexion.php', + '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"', + 'sec-fetch-dest': 'document', + 'sec-fetch-mode': 'navigate', + 'sec-fetch-site': 'same-origin', + 'sec-fetch-user': '?1', + 'upgrade-insecure-requests': '1', + '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', + } + if config_file: + self.load_config(config_file) + self.csrf_token = self.get_csrf_token() + self.login() + else: + raise Exception("No configuration file provided") + + def get_csrf_token(self) -> str: + try: + response = self.session.get('https://cattheflag.org/connexion.php', headers=self.headers) + soup = BeautifulSoup(response.text, 'html.parser') + csrf_token = soup.find('input', {'name': 'csrf_token'})['value'] + return csrf_token + except requests.RequestException: + return None + + def load_config(self, config_file: str | Path): + """Load configuration from file""" + config = load_config(config_file) + self.email = config.get("email") + self.password = config.get("password") + + def login(self) -> bool: + data = { + 'csrf_token': self.csrf_token, + 'email': self.email, + 'mot_de_passe': self.password, + } + response = self.session.post('https://cattheflag.org/connexion.php', headers=self.headers, data=data) + if response.status_code != 200: + raise f"Error logging in: {response.status_code}" + else: + print("Successfully logged in") + return True + + + def get_challenges(self) -> List[Challenge]: + """Get all challenges from platform""" + try: + response = self.session.get('https://cattheflag.org/defis.php', headers=self.headers) + if response.status_code != 200: + raise Exception(f"Error fetching challenges: {response.status_code}") + except requests.RequestException as e: + raise Exception(f"Error fetching challenges: {e}") + + response.encoding = "utf-8" + soup = BeautifulSoup(response.text, 'html.parser') + challenges = {} + + challenge_sections = soup.find_all('div', class_='challeng__wrap') + + for section in challenge_sections: + category = section.find('h3').text.strip() + if category != 'Histoire': + table = section.find('table') + rows = table.find_all('tr')[1:] + + for row in rows: + cols = row.find_all(['th', 'td']) + + link = cols[3].find('a') + url = link['href'] if link else None + + challenge_name = cols[0].text.strip() + challenge_id = url.split('/')[-1].replace('.php', '') if url else re.sub(r'[^a-zA-Z0-9]', '-', challenge_name.lower()) + + challenge_url = self.base_url + url + challenges[challenge_url] = Challenge( + id=challenge_id, + url=challenge_url, + platform="CatTheFlag", + name=challenge_name, + author=None, + category=category, + description=None, + difficulty=cols[1].text.strip(), + points=int(cols[2].text.strip()), + files=None, + additional_info={'validation_rate': float(cols[4].text.strip().replace('%', ''))} + ) + self.challenges = challenges + + 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').text.strip() + + description = soup.find('p', style='color:white').text.strip() + + author_link = soup.find('a', href=lambda x: x and 'page_membre.php' in x) + author_name = author_link.text.strip() if author_link else None + + file_link = soup.find('a', href=lambda x: x and 'cdn.cattheflag.org' in x) + if file_link: + files = [File( + name=file_link['href'].split('/')[-1], + url=file_link['href'], + hash=None + )] + else: + files = [] + + challenge = self.challenges.get(challenge_url) + challenge.description = description + challenge.author = author_name + challenge.files = files + return challenge + + 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("CatTheFlag") + 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 (sucess rate : {challenge.additional_info['validation_rate']}%)." + 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 de {challenge.category.lower()} avec une difficulté {challenge.difficulty.lower()} (taux de réussite : {challenge.additional_info['validation_rate']}%)." + 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_stars = { + 'facile': 1, + 'simple': 2, + 'medium': 3, + 'difficile': 4 + } + stars = "⭐" * difficulty_stars.get(challenge.difficulty.lower(), 1) + + 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}, success rate: {challenge.additional_info['validation_rate']}%) + - 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}, taux de réussite : {challenge.additional_info['validation_rate']}%) + - 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 diff --git a/src/platforms/crackmy.py b/src/platforms/crackmy.py new file mode 100644 index 0000000..89f344f --- /dev/null +++ b/src/platforms/crackmy.py @@ -0,0 +1,252 @@ +from typing import List, Dict +from pathlib import Path +from .base import CTFPlatform +from ..models import Challenge, File +from ..utils.config_handler import load_config +from ..utils.challenge_handler import download_files +from bs4 import BeautifulSoup +import unicodedata +from datetime import datetime +import textwrap +import requests + +class CrackmyPlatform(CTFPlatform): + def __init__(self, url: str = "https://crackmy.app", config_file: str | Path = None): + super().__init__(url) + self.url = url + if config_file: + self.load_config(config_file) + self.csrf_token = self.get_csrf_token() + self.login() + self.headers = { + 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', + 'accept-language': 'fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7', + 'cache-control': 'no-cache', + 'pragma': 'no-cache', + 'priority': 'u=0, i', + '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"', + 'sec-fetch-dest': 'document', + 'sec-fetch-mode': 'navigate', + 'sec-fetch-site': 'same-origin', + 'sec-fetch-user': '?1', + 'upgrade-insecure-requests': '1', + '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', + } + + def get_csrf_token(self) -> str: + try: + response = self.session.get('https://crackmy.app/api/auth/csrf', headers=self.headers) + return response.json()['csrfToken'] + except requests.RequestException: + return None + + def load_config(self, config_file: str | Path): + """Load configuration from file""" + config = load_config(config_file) + self.email = config.get("email") + self.password = config.get("password") + + def login(self) -> bool: + data = { + 'email': self.email, + 'password': self.password, + 'remember': 'false', + 'redirect': 'false', + 'csrfToken': self.csrf_token, + 'callbackUrl': 'https://crackmy.app/', + 'json': 'true' + } + try: + response = self.session.post('https://crackmy.app/api/auth/callback/credentials', headers=self.headers, data=data).json() + if response['url'] != "https://crackmy.app/api/auth/error?error=CredentialsSignin&provider=credentials": + return True + else: + return False + except requests.RequestException: + return False + + 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""" + + api_url = challenge_url.replace('https://crackmy.app/crackmes/', 'https://crackmy.app/api/crackmes/') + + try: + response = self.session.get(api_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 = response.json() + title = response['title'] + id = title.replace(' ', '-').lower() + author_name = response['author']['name'] + description = response['description'] + + additional_info = { + 'platform': response['os'], + 'architecture': response['architecture'], + 'quality': response['qualityRating'], + 'category': response['category'], + 'rating': response['rating'], + } + + difficulty = { + "difficulty": response["difficulty"], + "difficultyRating": response["difficultyRating"], + } + + try: + data = {"fileId":response['file']["id"]} + download_request = self.session.post('https://crackmy.app/api/download/create', headers=self.headers, json=data).json() + except requests.RequestException as e: + raise Exception(f"Error fetching challenge {challenge_url}: {e}") + file_url = self.base_url + download_request['url'] + + files = [ + File( + name=response["file"]["fileName"], + url=file_url, + hash=response["file"]["fileSha256"], + ) + ] + + return Challenge( + id=id, + url=challenge_url, + platform="Crackmy", + name=title, + author=author_name, + category="Reverse", + 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("Crackmy") + if challenge.additional_info["platform"]: + tags.append(challenge.additional_info["platform"]) + if challenge.additional_info["architecture"]: + tags.append(challenge.additional_info["architecture"]) + 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['difficulty'].lower()} difficulty ({challenge.difficulty['difficultyRating'] if challenge.difficulty['difficultyRating'] > 0 else 0}/10)." + 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 de {challenge.category.lower()} avec une difficulté {challenge.difficulty['difficulty'].lower()} ({challenge.difficulty['difficultyRating'] if challenge.difficulty['difficultyRating'] > 0 else 0}/10)." + 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 + --- + """ + ) + + stars = "⭐" * (int(challenge.difficulty['difficultyRating']) // 2 if challenge.difficulty['difficultyRating'] > 0 else 1) + + 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['difficultyRating'] if challenge.difficulty['difficultyRating'] > 0 else 1}/10) + - 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['difficultyRating'] if challenge.difficulty['difficultyRating'] > 0 else 1}/10) + - 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 diff --git a/src/platforms/hackropole.py b/src/platforms/hackropole.py index c4f497e..870fc51 100644 --- a/src/platforms/hackropole.py +++ b/src/platforms/hackropole.py @@ -12,9 +12,7 @@ import textwrap class HackropolePlatform(CTFPlatform): - def __init__( - self, url: str = "https://hackropole.fr", config_file: str | Path = None - ): + def __init__(self, url: str = "https://hackropole.fr", config_file: str | Path = None): super().__init__(url) if config_file: self.load_config(config_file)