rootme support + fixes

This commit is contained in:
√(noham)²
2025-02-18 19:34:17 +01:00
parent dc7189a94e
commit 40ec791618
9 changed files with 269 additions and 10 deletions

View File

@@ -8,11 +8,11 @@ A Python tool to automatically generate CTF writeup templates and organize chall
- https://crackmes.one - https://crackmes.one
- https://crackmy.app - https://crackmy.app
- https://cattheflag.org/defis.php - https://cattheflag.org/defis.php
### Will add :
- https://imaginaryctf.org - https://imaginaryctf.org
- https://challenges.ecsc.eu/challenges
- https://www.root-me.org - https://www.root-me.org
### Will consider adding :
- https://challenges.ecsc.eu/challenges
- https://www.hackthissite.org - https://www.hackthissite.org
## Features ## Features

15
main.py
View File

@@ -4,6 +4,7 @@ from src.platforms.crackmes import CrackmesPlatform
from src.platforms.crackmy import CrackmyPlatform from src.platforms.crackmy import CrackmyPlatform
from src.platforms.cattheflag import CatTheFlagPlatform from src.platforms.cattheflag import CatTheFlagPlatform
from src.platforms.imaginaryctf import ImaginaryCTFPlatform from src.platforms.imaginaryctf import ImaginaryCTFPlatform
from src.platforms.rootme import RootMePlatform
from src.generator import WriteupGenerator from src.generator import WriteupGenerator
from pathlib import Path from pathlib import Path
@@ -54,7 +55,7 @@ def cattheflag():
generator.generate_writeup_structure(hugo_header=True, translated=True) generator.generate_writeup_structure(hugo_header=True, translated=True)
def imaginaryctf(): def imaginaryctf():
challenge_name = "Prime Cuts" challenge_name = "Wrong ssh"
challenge_url = challenge_name.lower().replace(' ', '-') challenge_url = challenge_name.lower().replace(' ', '-')
platform = ImaginaryCTFPlatform() platform = ImaginaryCTFPlatform()
@@ -64,9 +65,19 @@ def imaginaryctf():
print(generator.challenges) print(generator.challenges)
generator.generate_writeup_structure(hugo_header=True, translated=True) 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() # theblackside()
# hackropole() # hackropole()
# crackmes() # crackmes()
# crackmy() # crackmy()
# cattheflag() # cattheflag()
# imaginaryctf() # imaginaryctf()
rootme()

4
rootme.json.example Normal file
View File

@@ -0,0 +1,4 @@
{
"email": "email",
"password": "password"
}

View File

@@ -158,7 +158,7 @@ class CatTheFlagPlatform(CTFPlatform):
def generate_tags(self, challenge): def generate_tags(self, challenge):
tags = [] tags = []
tags.append(challenge.category) tags.append(challenge.category)
tags.append("CatTheFlag") tags.append(challenge.platform)
tags = list(set(filter(None, tags))) tags = list(set(filter(None, tags)))
tags_str = '", "'.join(tags) tags_str = '", "'.join(tags)
return tags_str return tags_str

View File

@@ -146,7 +146,7 @@ class CrackmesPlatform(CTFPlatform):
def generate_tags(self, challenge): def generate_tags(self, challenge):
tags = [] tags = []
tags.append(challenge.category) tags.append(challenge.category)
tags.append("Crackmes") tags.append(challenge.platform)
if challenge.additional_info["platform"]: if challenge.additional_info["platform"]:
tags.append(challenge.additional_info["platform"]) tags.append(challenge.additional_info["platform"])
if challenge.additional_info["language"]: if challenge.additional_info["language"]:

View File

@@ -136,7 +136,7 @@ class CrackmyPlatform(CTFPlatform):
def generate_tags(self, challenge): def generate_tags(self, challenge):
tags = [] tags = []
tags.append(challenge.category) tags.append(challenge.category)
tags.append("Crackmy") tags.append(challenge.platform)
if challenge.additional_info["platform"]: if challenge.additional_info["platform"]:
tags.append(challenge.additional_info["platform"]) tags.append(challenge.additional_info["platform"])
if challenge.additional_info["architecture"]: if challenge.additional_info["architecture"]:

View File

@@ -162,7 +162,7 @@ class HackropolePlatform(CTFPlatform):
def generate_template(self, challenge: Challenge, hugo_header: bool = False, translated: bool = False): def generate_template(self, challenge: Challenge, hugo_header: bool = False, translated: bool = False):
"""Generate writeup template for challenge""" """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) tags_str = '", "'.join(all_tags)
hugo_header_template = textwrap.dedent( hugo_header_template = textwrap.dedent(

View File

@@ -30,6 +30,7 @@ class ImaginaryCTFPlatform(CTFPlatform):
'sec-ch-ua-mobile': '?0', 'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"macOS"', 'sec-ch-ua-platform': '"macOS"',
} }
def login(self) -> bool: def login(self) -> bool:
"""Not implementing login since we don't need it :D""" """Not implementing login since we don't need it :D"""
return True return True
@@ -153,7 +154,7 @@ class ImaginaryCTFPlatform(CTFPlatform):
def generate_tags(self, challenge): def generate_tags(self, challenge):
tags = [] tags = []
tags.append(challenge.category) tags.append(challenge.category)
tags.append("ImaginaryCTF") tags.append(challenge.platform)
tags = list(set(filter(None, tags))) tags = list(set(filter(None, tags)))
tags_str = '", "'.join(tags) tags_str = '", "'.join(tags)
return tags_str return tags_str

243
src/platforms/rootme.py Normal file
View File

@@ -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