From 6c1ed3230c05403c78184f4867b9f01c7b2d505d 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: Mon, 17 Feb 2025 02:42:06 +0100 Subject: [PATCH] crackmes support + fixes --- README.md | 4 + main.py | 16 +- src/platforms/crackmes.py | 264 +++++++++++++++++++++++++++++++++ src/platforms/hackropole.py | 6 +- src/platforms/theblackside.py | 4 +- src/utils/challenge_handler.py | 57 ++++++- 6 files changed, 339 insertions(+), 12 deletions(-) create mode 100644 src/platforms/crackmes.py diff --git a/README.md b/README.md index f300613..f9ff573 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,13 @@ A Python tool to automatically generate CTF writeup templates and organize chall ### Supported CTF Websites : - https://hackropole.fr - https://theblackside.fr +- https://crackmes.one ### Will add : - https://imaginaryctf.org +- https://challenges.ecsc.eu/challenges +- https://www.root-me.org +- https://www.hackthissite.org ## Features diff --git a/main.py b/main.py index 8244ead..261207e 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,6 @@ from src.platforms.hackropole import HackropolePlatform from src.platforms.theblackside import TheBlackSidePlatform +from src.platforms.crackmes import CrackmesPlatform from src.generator import WriteupGenerator from pathlib import Path @@ -17,12 +18,21 @@ def hackropole(): def theblackside(): challenge_url = 'https://theblackside.fr/challenges/steganographie/Meow' - platform = TheBlackSidePlatform(cookies_file="cookies.json") + platform = TheBlackSidePlatform(cookies_file="theblackside.cookies.json") + generator = WriteupGenerator(platform, Path("./writeups")) + generator.fetch_challenge(challenge_url=challenge_url) + generator.generate_writeup_structure(hugo_header=True, translated=True) + +def crackmes(): + challenge_url = 'https://crackmes.one/crackme/6784f8a84d850ac5f7dc5173' + platform = CrackmesPlatform() + 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() \ No newline at end of file +# theblackside() +# hackropole() +crackmes() \ No newline at end of file diff --git a/src/platforms/crackmes.py b/src/platforms/crackmes.py new file mode 100644 index 0000000..90668b9 --- /dev/null +++ b/src/platforms/crackmes.py @@ -0,0 +1,264 @@ +from typing import List, Dict +from pathlib import Path +import requests +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 re + +class CrackmesPlatform(CTFPlatform): + def __init__(self, url: str = "https://crackmes.one"): + super().__init__(url) + self.headers = {} + + 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") + + container = soup.find('div', class_='container grid-lg wrapper') + + download_link = soup.find('a', class_='btn-download') + + author_element = container.find('a', href=re.compile(r'/user/')) + author_name = author_element.text if author_element else None + + title_element = container.find('h3') + name = title_element.text.strip().replace("{author_name}'s ".format(author_name=author_name), "") if title_element else None + + id = f"{name}-{author_name}" + + + description_element = container.find('span', style='white-space: pre-line') + description = description_element.text.strip() if description_element else None + + difficulty_element = container.find('p', text=re.compile('Difficulty:')) + difficulty = float(difficulty_element.text.replace('Difficulty:', '').strip()) if difficulty_element else None + + if download_link: + file = File( + name = download_link['href'].split('/')[-1], + url = "https://crackmes.one" + download_link['href'], + hash=None, + ) + + columns = container.find_all('div', class_='column') + + def get_text_after_br(element): + if not element: + return None + br = element.find('br') + if br and br.next_sibling: + return br.next_sibling.strip() + return None + + platform = None + language = None + architecture = None + quality = None + difficulty = None + + for column in columns: + p_tag = column.find('p') + if not p_tag: + continue + + text = p_tag.text.strip() + if text.startswith('Language:'): + language = get_text_after_br(p_tag) + elif text.startswith('Platform'): + platform = get_text_after_br(p_tag) + elif text.startswith('Arch:'): + architecture = get_text_after_br(p_tag) + elif text.startswith('Quality:'): + quality_text = get_text_after_br(p_tag) + if quality_text: + try: + quality = float(quality_text) + except ValueError: + quality = None + elif text.startswith('Difficulty:'): + difficulty_text = get_text_after_br(p_tag) + if difficulty_text: + try: + difficulty = float(difficulty_text) + except ValueError: + difficulty = None + + additional_info = { + 'platform': platform, + 'language': language, + 'architecture': architecture, + 'quality': quality, + } + + print('additional_info: ', additional_info) + + return Challenge( + id=id, + url=challenge_url, + platform="Crackmes", + name=name, + author=author_name, + category="Reverse", + description=description, + difficulty=difficulty, + points=None, + files=[file], + additional_info=additional_info, + ) + + + def download_challenge_files(self, challenge: Challenge, output_dir: Path): + download_files(self, challenge, output_dir, password="crackmes.one") + + def get_difficulty_str(self, difficulty): + if difficulty is None: + return None + if difficulty < 2: + return "easy" + elif difficulty < 3: + return "medium" + elif difficulty < 4: + return "hard" + else: + return "very hard" + + def generate_tags(self, challenge): + tags = [] + tags.append(challenge.category) + tags.append("Crackmes") + if challenge.additional_info["platform"]: + tags.append(challenge.additional_info["platform"]) + if challenge.additional_info["language"]: + tags.append(challenge.additional_info["language"]) + 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) + + difficulty_str = self.get_difficulty_str(challenge.difficulty) + + 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 "{difficulty_str}" challenge." + 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 {difficulty_str}." + 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 + --- + """ + ) + + 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: {challenge.difficulty}/5 + - 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é: {challenge.difficulty}/5 + - 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 331796e..c4f497e 100644 --- a/src/platforms/hackropole.py +++ b/src/platforms/hackropole.py @@ -164,7 +164,7 @@ class HackropolePlatform(CTFPlatform): def generate_template(self, challenge: Challenge, hugo_header: bool = False, translated: bool = False): """Generate writeup template for challenge""" - all_tags = list(set([challenge.category] + challenge.additional_info["badges"])) # Remove duplicates + all_tags = list(set([challenge.category] + "Hackropole" + challenge.additional_info["badges"])) # Remove duplicates tags_str = '", "'.join(all_tags) hugo_header_template = textwrap.dedent( @@ -233,11 +233,11 @@ class HackropolePlatform(CTFPlatform): 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 if challenge.difficulty > 0 else 1}/5) - - Challenge URL: [{challenge.name} - {challenge.platform}]({challenge.url}) - Files provided: {files_section} ## Writeup @@ -246,11 +246,11 @@ class HackropolePlatform(CTFPlatform): 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 if challenge.difficulty > 0 else 1}/5) - - URL du challenge: [{challenge.name} - {challenge.platform}]({challenge.url}) - Fichiers fournis: {files_section} ## Writeup diff --git a/src/platforms/theblackside.py b/src/platforms/theblackside.py index 201e39b..a7a5a27 100644 --- a/src/platforms/theblackside.py +++ b/src/platforms/theblackside.py @@ -202,12 +202,12 @@ class TheBlackSidePlatform(CTFPlatform): main_content = textwrap.dedent( f"""\ + - Challenge URL: [{challenge.name} - {challenge.platform}]({challenge.url}) - Author: {challenge.author} - Category: {challenge.category} - Challenge description: {challenge.description} - Points: {challenge.points} - Solved by: {challenge.solved_number} users - - Challenge URL: [{challenge.name} - {challenge.platform}]({challenge.url}) - Files provided: {files_section} ## Writeup @@ -216,12 +216,12 @@ class TheBlackSidePlatform(CTFPlatform): 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} - Points: {challenge.points} - Résolu par: {challenge.solved_number} utilisateurs - - URL du challenge: [{challenge.name} - {challenge.platform}]({challenge.url}) - Fichiers fournis: {files_section} ## Writeup diff --git a/src/utils/challenge_handler.py b/src/utils/challenge_handler.py index e4f4ec4..a0194f0 100644 --- a/src/utils/challenge_handler.py +++ b/src/utils/challenge_handler.py @@ -2,18 +2,67 @@ from ..models import Challenge, File from typing import List from pathlib import Path import requests +import zipfile +import os -def download_files(self, challenge: Challenge, destination: Path) -> List[Path]: - """Download challenge files to the specified directory""" +def download_files(self, challenge: Challenge, destination: Path, password: str = None) -> List[Path]: + """ + Download challenge files to the specified directory and handle zip extraction + + Args: + challenge (Challenge): Challenge object containing files + destination (Path): Destination directory path + password (str, optional): Password for encrypted zip files + + Returns: + List[Path]: List of paths to downloaded/extracted files + """ + for file in challenge.files: file_url = file.url name = file.name name = name.replace("public.yml", ".yml") + try: response = self.session.get(file_url) if response.status_code == 200: file_path = destination / name file_path.write_bytes(response.content) - print(f"Downloaded {name} to {destination}") + print(f"Downloaded {name} to {file_path}") + + # Handle zip files + if name.lower().endswith('.zip'): + try: + with zipfile.ZipFile(file_path, 'r') as zip_ref: + # Check if zip is password protected + if zip_ref.namelist()[0].endswith('/'): + is_encrypted = False + else: + try: + zip_ref.read(zip_ref.namelist()[0]) + is_encrypted = False + except RuntimeError: + is_encrypted = True + + # Extract file + if is_encrypted and password: + zip_ref.extractall( + path=destination, + pwd=password.encode('utf-8') + ) + print(f"Extracted encrypted zip {name} with password") + elif not is_encrypted: + zip_ref.extractall(path=destination) + print(f"Extracted zip {name}") + else: + print(f"Zip file {name} is encrypted but no password provided") + + for extracted_file in zip_ref.namelist(): + extracted_path = destination / extracted_file + if extracted_path.is_file(): + print(f"Extracted file {extracted_file}") + except zipfile.BadZipFile: + raise f"Error: {name} is not a valid zip file or password is incorrect" + except requests.RequestException as e: - raise f"Error downloading file {file_url}: {e}" \ No newline at end of file + raise Exception(f"Error downloading file {file_url}: {e}") \ No newline at end of file