From 40ec791618ad707ec94077d763df4b3e1f8ac4bc 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 19:34:17 +0100 Subject: [PATCH] rootme support + fixes --- README.md | 6 +- main.py | 15 ++- rootme.json.example | 4 + src/platforms/cattheflag.py | 2 +- src/platforms/crackmes.py | 2 +- src/platforms/crackmy.py | 2 +- src/platforms/hackropole.py | 2 +- src/platforms/imaginaryctf.py | 3 +- src/platforms/rootme.py | 243 ++++++++++++++++++++++++++++++++++ 9 files changed, 269 insertions(+), 10 deletions(-) create mode 100644 rootme.json.example create mode 100644 src/platforms/rootme.py diff --git a/README.md b/README.md index 41f766e..2c006bc 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,11 @@ A Python tool to automatically generate CTF writeup templates and organize chall - https://crackmes.one - https://crackmy.app - https://cattheflag.org/defis.php - -### Will add : - https://imaginaryctf.org -- https://challenges.ecsc.eu/challenges - https://www.root-me.org + +### Will consider adding : +- https://challenges.ecsc.eu/challenges - https://www.hackthissite.org ## Features diff --git a/main.py b/main.py index 1828aba..4d855db 100644 --- a/main.py +++ b/main.py @@ -4,6 +4,7 @@ from src.platforms.crackmes import CrackmesPlatform 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.generator import WriteupGenerator from pathlib import Path @@ -54,7 +55,7 @@ def cattheflag(): generator.generate_writeup_structure(hugo_header=True, translated=True) def imaginaryctf(): - challenge_name = "Prime Cuts" + challenge_name = "Wrong ssh" challenge_url = challenge_name.lower().replace(' ', '-') platform = ImaginaryCTFPlatform() @@ -64,9 +65,19 @@ def imaginaryctf(): print(generator.challenges) generator.generate_writeup_structure(hugo_header=True, translated=True) +def rootme(): + challenge_url = 'https://www.root-me.org/fr/Challenges/Cracking/ELF-x86-0-protection' + platform = RootMePlatform(config_file="rootme.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() # crackmes() # crackmy() # cattheflag() -# imaginaryctf() \ No newline at end of file +# imaginaryctf() +rootme() \ No newline at end of file diff --git a/rootme.json.example b/rootme.json.example new file mode 100644 index 0000000..9217f6d --- /dev/null +++ b/rootme.json.example @@ -0,0 +1,4 @@ +{ + "email": "email", + "password": "password" +} \ No newline at end of file diff --git a/src/platforms/cattheflag.py b/src/platforms/cattheflag.py index 3906bd4..39180b4 100644 --- a/src/platforms/cattheflag.py +++ b/src/platforms/cattheflag.py @@ -158,7 +158,7 @@ class CatTheFlagPlatform(CTFPlatform): def generate_tags(self, challenge): tags = [] tags.append(challenge.category) - tags.append("CatTheFlag") + tags.append(challenge.platform) tags = list(set(filter(None, tags))) tags_str = '", "'.join(tags) return tags_str diff --git a/src/platforms/crackmes.py b/src/platforms/crackmes.py index f5e480f..52a320a 100644 --- a/src/platforms/crackmes.py +++ b/src/platforms/crackmes.py @@ -146,7 +146,7 @@ class CrackmesPlatform(CTFPlatform): def generate_tags(self, challenge): tags = [] tags.append(challenge.category) - tags.append("Crackmes") + tags.append(challenge.platform) if challenge.additional_info["platform"]: tags.append(challenge.additional_info["platform"]) if challenge.additional_info["language"]: diff --git a/src/platforms/crackmy.py b/src/platforms/crackmy.py index 69ba076..439dd22 100644 --- a/src/platforms/crackmy.py +++ b/src/platforms/crackmy.py @@ -136,7 +136,7 @@ class CrackmyPlatform(CTFPlatform): def generate_tags(self, challenge): tags = [] tags.append(challenge.category) - tags.append("Crackmy") + tags.append(challenge.platform) if challenge.additional_info["platform"]: tags.append(challenge.additional_info["platform"]) if challenge.additional_info["architecture"]: diff --git a/src/platforms/hackropole.py b/src/platforms/hackropole.py index 870fc51..4cbf31f 100644 --- a/src/platforms/hackropole.py +++ b/src/platforms/hackropole.py @@ -162,7 +162,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] + "Hackropole" + challenge.additional_info["badges"])) # Remove duplicates + all_tags = list(set([challenge.category] + [challenge.platform] + challenge.additional_info["badges"])) # Remove duplicates tags_str = '", "'.join(all_tags) hugo_header_template = textwrap.dedent( diff --git a/src/platforms/imaginaryctf.py b/src/platforms/imaginaryctf.py index 9666316..d70408a 100644 --- a/src/platforms/imaginaryctf.py +++ b/src/platforms/imaginaryctf.py @@ -30,6 +30,7 @@ class ImaginaryCTFPlatform(CTFPlatform): '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 @@ -153,7 +154,7 @@ class ImaginaryCTFPlatform(CTFPlatform): def generate_tags(self, challenge): tags = [] tags.append(challenge.category) - tags.append("ImaginaryCTF") + tags.append(challenge.platform) tags = list(set(filter(None, tags))) tags_str = '", "'.join(tags) return tags_str diff --git a/src/platforms/rootme.py b/src/platforms/rootme.py new file mode 100644 index 0000000..0582719 --- /dev/null +++ b/src/platforms/rootme.py @@ -0,0 +1,243 @@ +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 +from datetime import datetime +import textwrap +import requests +import re + +class RootMePlatform(CTFPlatform): + def __init__(self, url: str = "https://www.root-me.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', + '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': 'none', + '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.login() + + 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: + """Login to platform""" + params = (('page', 'login'),('lang', 'fr'),('ajah', '1'),) + data = {'triggerAjaxLoad': '',} + response = self.session.post('https://www.root-me.org/', headers=self.headers, params=params, data=data) + soup = BeautifulSoup(response.text, 'html.parser') + form_hidden = soup.find('span', class_='form-hidden') + formulaire_action_args = form_hidden.find('input', {'name': 'formulaire_action_args'})['value'] + + data = {'var_ajax': 'form','page': 'login','lang': 'fr','ajah': '1','formulaire_action': 'login','formulaire_action_args': formulaire_action_args,'formulaire_action_sign': '','var_login': self.email,'password': self.password} + response = self.session.post('https://www.root-me.org/', headers=self.headers, params=params, data=data) + if response.status_code == 200 and '>Vous êtes enregistré...' in response.text: + print("Login successful") + return True + else: + raise Exception("Login failed") + + + def get_challenges(self) -> List[Challenge]: + """Get all challenges from platform""" + raise NotImplementedError("RootMePlatform does not support fetching all 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 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') + + title = soup.find('h1', {'class': 'challenge-titre-41'}).text.strip() + + id = title.lower().replace(' ', '-') + author = soup.find('a', {'class': 'txt_0minirezo'}).text.strip() + points = int(soup.find('h2', {'class': 'challenge-score-41'}).text.split()[0]) + description = soup.find('div', {'class': 'challenge-descriptif-41'}).text.strip() + + # category_img = soup.find('a', {'href': re.compile(r'fr/Challenges/[^/]+/')}) + # category = category_img['href'].split('/')[2] if category_img else "Uncategorized" + category = challenge_url.split('/')[-2] + + difficulty_elements = soup.find_all('a', {'class': re.compile(r'difficulte.*')}) + difficulty = "Unknown" + for elem in difficulty_elements: + if 'a' in elem['class'][-1]: # Check if the last class ends with 'a' + difficulty = elem['title'].split(':')[0].strip() + break + + files = [] + file_links = soup.find_all('a', {'class': 'button small radius'}) + for link in file_links: + filename = link['href'].split('/')[-1] + files.append(File(name=filename, url=link['href'], hash=None)) + + validations = soup.find('a', {'title': 'Qui a validé ?'}).text.strip().split()[0] + votes = soup.find('span', {'class': 'notation_valeur'}).text.strip().split()[0] + + completion_div = soup.find('span', {'class': 'left gras'}) + completion_rate = completion_div.text.strip().replace('%', '') if completion_div else "Unknown" + + solve_count = int(validations.replace(',', '')) + additional_info = { + 'votes': int(votes), + 'completion_rate': completion_rate + } + + return Challenge( + id=id, + url=challenge_url, + platform="Root-Me", + name=title, + author=author, + category=category, + description=description, + difficulty=difficulty, + points=points, + files=files, + additional_info=additional_info, + solved_number=solve_count, + ) + + 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""" + + all_tags = list(set([challenge.category] + [challenge.platform])) + tags_str = '", "'.join(all_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.category.lower()} challenge with a "{challenge.difficulty.lower()}" difficulty (sucess rate : {challenge.additional_info['completion_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 {challenge.category.lower()} de difficulté "{challenge.difficulty.lower()}" (taux de réussite : {challenge.additional_info['completion_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_mapping = { + 'Très facile': 1, + 'Facile': 2, + 'Moyen': 3, + 'Difficile': 4, + 'Très difficile': 5 + } + difficulty_level = difficulty_mapping.get(challenge.difficulty, 1) + stars = "⭐" * difficulty_level + + 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['completion_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['completion_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