diff --git a/README.md b/README.md index d99f998..f300613 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,10 @@ A Python tool to automatically generate CTF writeup templates and organize chall ### Supported CTF Websites : - https://hackropole.fr +- https://theblackside.fr ### Will add : - https://imaginaryctf.org -- https://theblackside.fr ## Features diff --git a/main.py b/main.py index b815534..8244ead 100644 --- a/main.py +++ b/main.py @@ -1,12 +1,13 @@ from src.platforms.hackropole import HackropolePlatform +from src.platforms.theblackside import TheBlackSidePlatform from src.generator import WriteupGenerator from pathlib import Path -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/' +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/' -def main(): platform = HackropolePlatform() generator = WriteupGenerator(platform, Path("./writeups")) @@ -14,5 +15,14 @@ def main(): print(generator.challenges) generator.generate_writeup_structure(hugo_header=True, translated=True) -if __name__ == "__main__": - main() \ No newline at end of file +def theblackside(): + challenge_url = 'https://theblackside.fr/challenges/steganographie/Meow' + platform = TheBlackSidePlatform(cookies_file="cookies.json") + + 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 diff --git a/src/models.py b/src/models.py index 64db6bb..962d8fd 100644 --- a/src/models.py +++ b/src/models.py @@ -16,6 +16,7 @@ class Challenge: additional_info: Dict = None template: str = None template_translated: str = None + solved_number: int = 0 @dataclass class File: diff --git a/src/platforms/hackropole.py b/src/platforms/hackropole.py index 6ada000..331796e 100644 --- a/src/platforms/hackropole.py +++ b/src/platforms/hackropole.py @@ -233,11 +233,11 @@ class HackropolePlatform(CTFPlatform): main_content = textwrap.dedent( f"""\ - - Author: {challenge.author} - - Category: {challenge.category} - - Difficulty: {stars} ({challenge.difficulty if challenge.difficulty > 0 else 1}/5) - - 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 @@ -248,9 +248,9 @@ class HackropolePlatform(CTFPlatform): f"""\ - 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}) - - Description du challenge: {challenge.description} - Fichiers fournis: {files_section} ## Writeup diff --git a/src/platforms/theblackside.py b/src/platforms/theblackside.py new file mode 100644 index 0000000..201e39b --- /dev/null +++ b/src/platforms/theblackside.py @@ -0,0 +1,238 @@ +from typing import List, Dict +from pathlib import Path +import requests +from .base import CTFPlatform +from ..models import Challenge, File +from ..utils.cookie_handler import load_cookies_from_file +from ..utils.challenge_handler import download_files +from bs4 import BeautifulSoup +import unicodedata +from datetime import datetime +import textwrap +import re + +class TheBlackSidePlatform(CTFPlatform): + def __init__( + self, url: str = "https://theblackside.fr/", cookies_file: str | Path = None + ): + super().__init__(url) + if cookies_file: + self.cookie = load_cookies_from_file(cookies_file) + else: + raise Exception("No cookies provided") + 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", + "referer": "https://theblackside.fr/", + "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" + } + self.login() + + def login(self) -> bool: + """ + Not implementing traditional login as we're using cookies + Returns True if we can access authenticated endpoints + """ + try: + response = self.session.get( + "https://theblackside.fr/", + headers=self.headers, + cookies=self.cookie, + ) + if "/profil/" in response.text: + print("Logged in") + except requests.exceptions.RequestException: + raise Exception("Failed to login") + + 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, cookies=self.cookie) + 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") + + main = soup.find('main') + + title = main.find('h1').text.strip() + id = "-".join([word.lower() for word in title.split()]) + description = main.find('p').text.strip() + + author_link = main.find('a', href=re.compile(r'/profil/')) + author_name = author_link.find('span').find('a').text.strip() if author_link else None + + metadata_div = main.find('div', class_='metadata') + points = int(metadata_div.find('div', class_='button').find('span').text.strip()) + solved_number = int([div for div in metadata_div.find_all('div', class_='button') + if div.find('svg', class_='feather-check-circle')][0] + .find('span').text.strip()) + + category_button = metadata_div.find('a', href=re.compile(r'/challenges/')) + category = category_button.find('span').text.strip() if category_button else "Uncategorized" + + categorydict = { + "Web": "Web", + "Stéganographie": "Steganography", + "Cryptographie": "Cryptography", + "Reverse": "Reverse", + "Réseau": "Network", + "Forensic": "Forensic", + "Développement": "Development", + "Pwn": "Pwn", + "Box": "Box", + "Divers": "Miscellaneous", + } + category = categorydict.get(category, category) + + + file_url = main.find('a', class_='startChall')['href'] + + file = File( + name=file_url.split('/')[-1], + url=file_url, + hash=None, + ) + + + return Challenge( + id=id, + url=challenge_url, + platform="TheBlackSide", + name=title, + author=author_name, + category=category if category else "Uncategorized", + description=description, + difficulty=None, + solved_number=solved_number, + points=points, + files=[file], + ) + + + def download_challenge_files(self, challenge: Challenge, output_dir: Path): + download_files(self, challenge, output_dir) + + def generate_template(self, challenge: Challenge, hugo_header: bool = False, translated: bool = False): + """Generate writeup template for challenge""" + + tags = [challenge.category, "TheBlackSide"] + tags_str = ", ".join(tags) + + 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.points} points {challenge.category.lower()} challenge with {challenge.solved_number} solves." + 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()} à {challenge.points} points avec {challenge.solved_number} résolutions." + 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"""\ + - 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 + """ + ) + + main_content_fr = textwrap.dedent( + f"""\ + - 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 + """ + ) + + 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