fix + cattheflag support + crackmy support

This commit is contained in:
√(noham)²
2025-02-18 17:15:16 +01:00
parent 6c1ed3230c
commit 9ac8125be0
6 changed files with 565 additions and 18 deletions

View File

@@ -6,8 +6,10 @@ A Python tool to automatically generate CTF writeup templates and organize chall
- https://hackropole.fr - https://hackropole.fr
- https://theblackside.fr - https://theblackside.fr
- https://crackmes.one - https://crackmes.one
- https://crackmy.app
### Will add : ### Will add :
- https://cattheflag.org/defis.php
- https://imaginaryctf.org - https://imaginaryctf.org
- https://challenges.ecsc.eu/challenges - https://challenges.ecsc.eu/challenges
- https://www.root-me.org - https://www.root-me.org

30
main.py
View File

@@ -1,14 +1,13 @@
from src.platforms.hackropole import HackropolePlatform from src.platforms.hackropole import HackropolePlatform
from src.platforms.theblackside import TheBlackSidePlatform from src.platforms.theblackside import TheBlackSidePlatform
from src.platforms.crackmes import CrackmesPlatform from src.platforms.crackmes import CrackmesPlatform
from src.platforms.crackmy import CrackmyPlatform
from src.platforms.cattheflag import CatTheFlagPlatform
from src.generator import WriteupGenerator from src.generator import WriteupGenerator
from pathlib import Path from pathlib import Path
def hackropole(): def hackropole():
challenge_url = 'https://hackropole.fr/fr/challenges/reverse/fcsc2023-reverse-chaussette-xs/' 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() platform = HackropolePlatform()
generator = WriteupGenerator(platform, Path("./writeups")) generator = WriteupGenerator(platform, Path("./writeups"))
@@ -33,6 +32,29 @@ def crackmes():
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 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() # theblackside()
# hackropole() # hackropole()
crackmes() # crackmes()
# crackmy()
# cattheflag()

View File

@@ -8,7 +8,7 @@ class WriteupGenerator:
def __init__(self, platform: CTFPlatform, output_dir: Path): def __init__(self, platform: CTFPlatform, output_dir: Path):
self.platform = platform self.platform = platform
self.output_dir = output_dir self.output_dir = output_dir
self.challenges = [] self.challenges = {}
def fetch_challenges(self): def fetch_challenges(self):
"""Fetch all challenges from the platform""" """Fetch all challenges from the platform"""
@@ -27,6 +27,10 @@ class WriteupGenerator:
platform_dir.mkdir(parents=True, exist_ok=True) platform_dir.mkdir(parents=True, exist_ok=True)
challenge_dir = platform_dir / self._sanitize_filename(challenge.id) 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) challenge_dir.mkdir(exist_ok=True)
files_dir = challenge_dir / "files" files_dir = challenge_dir / "files"
@@ -35,18 +39,11 @@ class WriteupGenerator:
self.platform.generate_template(challenge, hugo_header, translated) self.platform.generate_template(challenge, hugo_header, translated)
if (challenge_dir / "index.md").exists(): (challenge_dir / "index.md").write_text(challenge.template)
print(f"Writeup for {challenge.id} already exists. Skipping...")
continue
else:
(challenge_dir / "index.md").write_text(challenge.template)
if translated: if translated:
if (challenge_dir / "index.fr.md").exists(): (challenge_dir / "index.fr.md").write_text(challenge.template_translated)
print(f"Writeup for {challenge.id} already exists. Skipping...") print(f"Writeup for {challenge.id} has been generated in {challenge_dir}")
continue
else:
(challenge_dir / "index.fr.md").write_text(challenge.template_translated)
@staticmethod @staticmethod

276
src/platforms/cattheflag.py Normal file
View File

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

252
src/platforms/crackmy.py Normal file
View File

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

View File

@@ -12,9 +12,7 @@ import textwrap
class HackropolePlatform(CTFPlatform): class HackropolePlatform(CTFPlatform):
def __init__( def __init__(self, url: str = "https://hackropole.fr", config_file: str | Path = None):
self, url: str = "https://hackropole.fr", config_file: str | Path = None
):
super().__init__(url) super().__init__(url)
if config_file: if config_file:
self.load_config(config_file) self.load_config(config_file)