mirror of
https://github.com/NohamR/LetCTF.git
synced 2026-05-25 19:59:23 +00:00
crackmes support + fixes
This commit is contained in:
@@ -5,9 +5,13 @@ A Python tool to automatically generate CTF writeup templates and organize chall
|
|||||||
### Supported CTF Websites :
|
### Supported CTF Websites :
|
||||||
- https://hackropole.fr
|
- https://hackropole.fr
|
||||||
- https://theblackside.fr
|
- https://theblackside.fr
|
||||||
|
- https://crackmes.one
|
||||||
|
|
||||||
### Will add :
|
### Will add :
|
||||||
- https://imaginaryctf.org
|
- https://imaginaryctf.org
|
||||||
|
- https://challenges.ecsc.eu/challenges
|
||||||
|
- https://www.root-me.org
|
||||||
|
- https://www.hackthissite.org
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
|||||||
14
main.py
14
main.py
@@ -1,5 +1,6 @@
|
|||||||
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.generator import WriteupGenerator
|
from src.generator import WriteupGenerator
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -17,12 +18,21 @@ def hackropole():
|
|||||||
|
|
||||||
def theblackside():
|
def theblackside():
|
||||||
challenge_url = 'https://theblackside.fr/challenges/steganographie/Meow'
|
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 = WriteupGenerator(platform, Path("./writeups"))
|
||||||
generator.fetch_challenge(challenge_url=challenge_url)
|
generator.fetch_challenge(challenge_url=challenge_url)
|
||||||
print(generator.challenges)
|
print(generator.challenges)
|
||||||
generator.generate_writeup_structure(hugo_header=True, translated=True)
|
generator.generate_writeup_structure(hugo_header=True, translated=True)
|
||||||
|
|
||||||
theblackside()
|
# theblackside()
|
||||||
# hackropole()
|
# hackropole()
|
||||||
|
crackmes()
|
||||||
264
src/platforms/crackmes.py
Normal file
264
src/platforms/crackmes.py
Normal file
@@ -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
|
||||||
@@ -164,7 +164,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] + challenge.additional_info["badges"])) # Remove duplicates
|
all_tags = list(set([challenge.category] + "Hackropole" + 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(
|
||||||
@@ -233,11 +233,11 @@ class HackropolePlatform(CTFPlatform):
|
|||||||
|
|
||||||
main_content = textwrap.dedent(
|
main_content = textwrap.dedent(
|
||||||
f"""\
|
f"""\
|
||||||
|
- Challenge URL: [{challenge.name} - {challenge.platform}]({challenge.url})
|
||||||
- Author: {challenge.author}
|
- Author: {challenge.author}
|
||||||
- Category: {challenge.category}
|
- Category: {challenge.category}
|
||||||
- Challenge description: {challenge.description}
|
- Challenge description: {challenge.description}
|
||||||
- Difficulty: {stars} ({challenge.difficulty if challenge.difficulty > 0 else 1}/5)
|
- Difficulty: {stars} ({challenge.difficulty if challenge.difficulty > 0 else 1}/5)
|
||||||
- Challenge URL: [{challenge.name} - {challenge.platform}]({challenge.url})
|
|
||||||
- Files provided: {files_section}
|
- Files provided: {files_section}
|
||||||
|
|
||||||
## Writeup
|
## Writeup
|
||||||
@@ -246,11 +246,11 @@ class HackropolePlatform(CTFPlatform):
|
|||||||
|
|
||||||
main_content_fr = textwrap.dedent(
|
main_content_fr = textwrap.dedent(
|
||||||
f"""\
|
f"""\
|
||||||
|
- URL du challenge: [{challenge.name} - {challenge.platform}]({challenge.url})
|
||||||
- Auteur: {challenge.author}
|
- Auteur: {challenge.author}
|
||||||
- Catégorie: {challenge.category}
|
- Catégorie: {challenge.category}
|
||||||
- Description du challenge: {challenge.description}
|
- Description du challenge: {challenge.description}
|
||||||
- Difficulté: {stars} ({challenge.difficulty if challenge.difficulty > 0 else 1}/5)
|
- Difficulté: {stars} ({challenge.difficulty if challenge.difficulty > 0 else 1}/5)
|
||||||
- URL du challenge: [{challenge.name} - {challenge.platform}]({challenge.url})
|
|
||||||
- Fichiers fournis: {files_section}
|
- Fichiers fournis: {files_section}
|
||||||
|
|
||||||
## Writeup
|
## Writeup
|
||||||
|
|||||||
@@ -202,12 +202,12 @@ class TheBlackSidePlatform(CTFPlatform):
|
|||||||
|
|
||||||
main_content = textwrap.dedent(
|
main_content = textwrap.dedent(
|
||||||
f"""\
|
f"""\
|
||||||
|
- Challenge URL: [{challenge.name} - {challenge.platform}]({challenge.url})
|
||||||
- Author: {challenge.author}
|
- Author: {challenge.author}
|
||||||
- Category: {challenge.category}
|
- Category: {challenge.category}
|
||||||
- Challenge description: {challenge.description}
|
- Challenge description: {challenge.description}
|
||||||
- Points: {challenge.points}
|
- Points: {challenge.points}
|
||||||
- Solved by: {challenge.solved_number} users
|
- Solved by: {challenge.solved_number} users
|
||||||
- Challenge URL: [{challenge.name} - {challenge.platform}]({challenge.url})
|
|
||||||
- Files provided: {files_section}
|
- Files provided: {files_section}
|
||||||
|
|
||||||
## Writeup
|
## Writeup
|
||||||
@@ -216,12 +216,12 @@ class TheBlackSidePlatform(CTFPlatform):
|
|||||||
|
|
||||||
main_content_fr = textwrap.dedent(
|
main_content_fr = textwrap.dedent(
|
||||||
f"""\
|
f"""\
|
||||||
|
- URL du challenge: [{challenge.name} - {challenge.platform}]({challenge.url})
|
||||||
- Auteur: {challenge.author}
|
- Auteur: {challenge.author}
|
||||||
- Catégorie: {challenge.category}
|
- Catégorie: {challenge.category}
|
||||||
- Description du challenge: {challenge.description}
|
- Description du challenge: {challenge.description}
|
||||||
- Points: {challenge.points}
|
- Points: {challenge.points}
|
||||||
- Résolu par: {challenge.solved_number} utilisateurs
|
- Résolu par: {challenge.solved_number} utilisateurs
|
||||||
- URL du challenge: [{challenge.name} - {challenge.platform}]({challenge.url})
|
|
||||||
- Fichiers fournis: {files_section}
|
- Fichiers fournis: {files_section}
|
||||||
|
|
||||||
## Writeup
|
## Writeup
|
||||||
|
|||||||
@@ -2,18 +2,67 @@ from ..models import Challenge, File
|
|||||||
from typing import List
|
from typing import List
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import requests
|
import requests
|
||||||
|
import zipfile
|
||||||
|
import os
|
||||||
|
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
|
||||||
def download_files(self, challenge: Challenge, destination: Path) -> List[Path]:
|
|
||||||
"""Download challenge files to the specified directory"""
|
|
||||||
for file in challenge.files:
|
for file in challenge.files:
|
||||||
file_url = file.url
|
file_url = file.url
|
||||||
name = file.name
|
name = file.name
|
||||||
name = name.replace("public.yml", ".yml")
|
name = name.replace("public.yml", ".yml")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = self.session.get(file_url)
|
response = self.session.get(file_url)
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
file_path = destination / name
|
file_path = destination / name
|
||||||
file_path.write_bytes(response.content)
|
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:
|
except requests.RequestException as e:
|
||||||
raise f"Error downloading file {file_url}: {e}"
|
raise Exception(f"Error downloading file {file_url}: {e}")
|
||||||
Reference in New Issue
Block a user