coming allong nicely. adding more commands, tests, and
artifacts for testing. basic framework down just adding features now
This commit is contained in:
@@ -71,16 +71,18 @@ Implements specialized forensic inspection utilities registered as a nested subg
|
|||||||
|
|
||||||
## 5. Planned Architectural Components
|
## 5. Planned Architectural Components
|
||||||
|
|
||||||
### A. File Scraper & Extended Configuration
|
### A. Forensics Pipeline & Extended Metadata
|
||||||
* A scraping module to fetch details/files for challenges or competitions.
|
* **Pipeline Coordination**: A central runner that coordinates file inspections, signature verification, and string searches sequentially on target files.
|
||||||
* Integration with an expanded configuration schema in [config.toml](file:///home/venus/code/ctf/config.toml) to store credentials, URLs, and directory preferences.
|
* **Carving & Extraction Toolkit**: Deep inspection and carving utilities (such as archive extracting, custom chunk/payload carvers, and EXIF/metadata header decoders).
|
||||||
|
|
||||||
### B. Download Organizer & Challenge Progress Documenter
|
### B. Core Solving Infrastructure & QOL
|
||||||
* Monitoring or organizing downloaded challenge assets (e.g., from the browser's downloads folder) and sorting them into the correct competition/challenge subdirectories.
|
* **Solving Assistants**: Standard templates and modular helper scripts for repeating CTF patterns (e.g., base conversions, cipher decoding, and request generation).
|
||||||
* An automated mechanism to log commands, notes, and milestones, providing clean progress documentation.
|
* **Context & Progress Management**: Full CLI commands to organize active challenges, automate note tracking, and manage workspace folders dynamically.
|
||||||
|
* **Per-Challenge Progress Logging**: A logging interface that creates separate progress logs for each challenge inside a configurable directory, tracking actions, milestones, attempts, and command history.
|
||||||
|
|
||||||
### C. Forensics Metadata Expansion
|
### C. Agentic Solving Orchestration
|
||||||
* Extend forensics capabilities inside [forensics.py](file:///home/venus/code/ctf/src/ctf/forensics.py) to extract file-specific metadata (e.g., EXIF header extraction for JPG/PNG files, archive contents listing, and PE section analysis).
|
* **Action Space & Sandboxing**: API boundaries and isolated docker/sandbox execution paths to run unknown commands and binaries safely.
|
||||||
|
* **Autonomous Solving Agent**: Integration with LLM controllers that parse output from the forensics pipeline, suggest plans, iterate on solving steps, and verify flag captures.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -96,3 +98,9 @@ To provide a clean, modern, and easily readable console output without building
|
|||||||
* **Tables**: Use `rich.table.Table` to align and structure multi-column metadata outputs.
|
* **Tables**: Use `rich.table.Table` to align and structure multi-column metadata outputs.
|
||||||
* **Formatting & Alerts**: Utilize `rich.console` or `rich.panel.Panel` to highlight warnings (such as signature/extension mismatches) with distinct styling and colors.
|
* **Formatting & Alerts**: Utilize `rich.console` or `rich.panel.Panel` to highlight warnings (such as signature/extension mismatches) with distinct styling and colors.
|
||||||
* **JSON Serialization**: Dataclasses should be easily convertible to dictionaries to support raw JSON output options for scripting pipelines.
|
* **JSON Serialization**: Dataclasses should be easily convertible to dictionaries to support raw JSON output options for scripting pipelines.
|
||||||
|
|
||||||
|
### C. Function Isolation & Pipeline Composition
|
||||||
|
To support fully automated or agentic solving, all processing functions must be isolated and highly composable:
|
||||||
|
* **Pure Functions**: Core analysis and transformation functions (e.g., file carving, cipher decoders, and metadata extraction) must remain pure. They must accept standard Python types (e.g., `Path`, `bytes`, `str`) and return structured dataclasses without depending on global CLI state, click contexts, or interactive user prompts.
|
||||||
|
* **Pipeline Chaining**: The output of one tool must serve as valid input for another. For example, a list of files or byte arrays carved from a file-carver must be pipeable directly into the string parser or decompression module.
|
||||||
|
* **Programmatic API**: The entire core logic must be importable as standard Python APIs (`import ctf.utils`, `import ctf.forensics`) separate from CLI bindings. This allows external scripts, test cases, or autonomous agents to programmatically chain functions together to build complex, self-contained solving pipelines.
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ The primary goal is to use the context of CTF challenges (forensics, crypto, web
|
|||||||
4. **Educational Context:** Use the existing scripts and tools in this repository (like `psk_crack.py`, `exploit.sh`, or the `tools/` directory) as examples when explaining technical concepts.
|
4. **Educational Context:** Use the existing scripts and tools in this repository (like `psk_crack.py`, `exploit.sh`, or the `tools/` directory) as examples when explaining technical concepts.
|
||||||
5. **Vim Folding Markers:** Wrap all classes, command groups, subcommands, and functions inside project implementation files in standard Vim/Neovim folding syntax markers (`# {{{ <name>` and `# }}}`).
|
5. **Vim Folding Markers:** Wrap all classes, command groups, subcommands, and functions inside project implementation files in standard Vim/Neovim folding syntax markers (`# {{{ <name>` and `# }}}`).
|
||||||
6. **Matching Test Files:** Every Python implementation file inside `src/` must have a corresponding test file under the `tests/` directory named `test_<filename>.py`.
|
6. **Matching Test Files:** Every Python implementation file inside `src/` must have a corresponding test file under the `tests/` directory named `test_<filename>.py`.
|
||||||
|
7. **Write Only, Do Not Run**: The agent must only write code and tests. Do not attempt to run tests or execute command-line scripts; the user will handle all execution, verification, and testing tasks.
|
||||||
|
|
||||||
## Interaction Workflow
|
## Interaction Workflow
|
||||||
When discussing new implementations, features, or additions:
|
When discussing new implementations, features, or additions:
|
||||||
@@ -21,6 +22,7 @@ When discussing new implementations, features, or additions:
|
|||||||
* **Add Test Cases**: Write or update the corresponding test cases in the test files (e.g. `tests/test_forensics.py`) following the SDET instructions.
|
* **Add Test Cases**: Write or update the corresponding test cases in the test files (e.g. `tests/test_forensics.py`) following the SDET instructions.
|
||||||
* **Git Commit Current State**: Create a git commit of the current workspace state (including the new tests) *before* modifying any implementation files.
|
* **Git Commit Current State**: Create a git commit of the current workspace state (including the new tests) *before* modifying any implementation files.
|
||||||
* **Update the Codebase**: Write/update the actual project implementation files as approved. Do **NOT** create a git commit after writing this implementation code. This ensures all implementation changes remain unstaged so the user can easily run `git diff` to review them.
|
* **Update the Codebase**: Write/update the actual project implementation files as approved. Do **NOT** create a git commit after writing this implementation code. This ensures all implementation changes remain unstaged so the user can easily run `git diff` to review them.
|
||||||
|
* **Hand Over**: Present the changes to the user so they can run the tests. Do not run the tests yourself.
|
||||||
|
|
||||||
## Python Learning Objectives
|
## Python Learning Objectives
|
||||||
- Understanding standard library modules relevant to security (e.g., `os`, `sys`, `base64`, `hashlib`).
|
- Understanding standard library modules relevant to security (e.g., `os`, `sys`, `base64`, `hashlib`).
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
[Competition]
|
[Competition]
|
||||||
producer = "testProd"
|
producer = "testProd"
|
||||||
competition = "testComp"
|
competition = "picoctf"
|
||||||
catagory = "textCat"
|
catagory = "textCat"
|
||||||
challenge = "testChal"
|
challenge = "testChal"
|
||||||
|
flag_format = "picoCTF\\{.*\\}"
|
||||||
|
last_flag = "picoCTF{flag}"
|
||||||
|
|
||||||
[Enviroment]
|
[Enviroment]
|
||||||
ctf_dir = "/home/venus/ctf"
|
ctf_dir = "/home/venus/ctf"
|
||||||
|
|||||||
@@ -19,13 +19,17 @@ This file tracks the completed progress and upcoming development milestones for
|
|||||||
|
|
||||||
## Upcoming Milestones & Features
|
## Upcoming Milestones & Features
|
||||||
|
|
||||||
### 📅 Phase 1: Configuration Completion & Basic File Scraper
|
### 📅 Phase 1: Functional Forensics Pipeline & Toolkit
|
||||||
* Extend [config.toml](file:///home/venus/code/ctf/config.toml) to store arbitrary data in the future
|
* **Pipeline Automation**: Integrate the existing forensics command-line utilities into a cohesive analysis pipeline where files are automatically checked for magic bytes, file extensions, and flag patterns.
|
||||||
* Implement a basic tool to load the latest download files into the active directory
|
* **Metadata & Extraction Toolkit**: Extend forensics tools to extract specific metadata (e.g., EXIF records, archive tables) and automate extraction/carving of nested data structures (e.g., binwalk-like carving, automated unzipping, extraction of hidden payloads).
|
||||||
|
* **QOL Utilities**: Add standard format outputs (JSON, Rich logs) and automatic logging of analysis artifacts to speed up user-led inspections.
|
||||||
|
|
||||||
### 📅 Phase 2: Downloads Organizer & Progress Tracker
|
### 📅 Phase 2: Core CTF Solving & Solver QOL
|
||||||
* Write a tool to scan specified download directories for newly acquired challenge files and automatically organize them into the active competition's directory structure.
|
* **Challenge Organization**: Implement automated download management, challenge creation, directory configuration, and context management (`set-challenge`).
|
||||||
* Implement progress tracking to output current exploration paths, notes, and milestones.
|
* **Solving Assistants**: Build automated helper scripts for common solving needs (e.g., basic cryptography decoders, web request templates, PSK cracking utility integration).
|
||||||
|
* **Solve Tracker & Note-taking QOL**: Create a command-line interface to capture solver actions, record active notes, log flag attempts, and update challenge statuses.
|
||||||
|
* **Per-Challenge Progress Logging**: Design a challenge-specific progress logger that creates and maintains isolated log files for each active challenge in a specified workspace directory, tracking attempts, timestamps, solver notes, and command history.
|
||||||
|
|
||||||
### 📅 Phase 3: Forensics Metadata Expansion
|
### 📅 Phase 3: Agentic Solving Capabilities
|
||||||
* Extend forensics tools to parse specific file-format metadata (e.g. EXIF data for JPGs or headers for specific archives).
|
* **Sandbox Environments**: Prepare secure, isolated environments to run untrusted challenge scripts or binaries.
|
||||||
|
* **Agent Orchestration**: Equip the toolchain with LLM agents capable of viewing the forensics pipeline outputs, reading challenge text, suggesting next steps, executing terminal tools, and recursively working to solve the challenge autonomously.
|
||||||
|
|||||||
79
src/ctf/cli_forensics.py
Normal file
79
src/ctf/cli_forensics.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# src/ctf/cli_forensics.py
|
||||||
|
# CLI wrapper and rendering layer for forensics commands
|
||||||
|
|
||||||
|
# vim foldmethod=marker
|
||||||
|
import click
|
||||||
|
from pathlib import Path
|
||||||
|
from ctf.forensics import get_metadata, COMMON_SIGNATURES
|
||||||
|
|
||||||
|
# {{{ forensics_group
|
||||||
|
@click.group(name="forensics")
|
||||||
|
def forensics_group():
|
||||||
|
""" A collection of forensics tools """
|
||||||
|
pass
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# {{{ tf
|
||||||
|
@forensics_group.command()
|
||||||
|
def tf():
|
||||||
|
click.echo("hello from forensics")
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# {{{ inspect
|
||||||
|
@forensics_group.command()
|
||||||
|
@click.argument('path', type=click.Path(exists=True))
|
||||||
|
def inspect(path):
|
||||||
|
"""Lists all basic inode metadata about a file"""
|
||||||
|
meta = get_metadata(Path(path))
|
||||||
|
|
||||||
|
# Present using Rich Table
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.table import Table
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
table = Table(title=f"Metadata: {meta.filename}", show_header=False)
|
||||||
|
table.add_column("Key", style="bold cyan")
|
||||||
|
table.add_column("Value")
|
||||||
|
|
||||||
|
table.add_row("Filename", meta.filename)
|
||||||
|
table.add_row("Size", f"{meta.size} bytes")
|
||||||
|
table.add_row("Magic Bytes (Hex)", meta.magic)
|
||||||
|
table.add_row("Detected Type", meta.detected_type)
|
||||||
|
table.add_row("Extension", meta.extension)
|
||||||
|
table.add_row("Permissions", f"{meta.permissions_symbolic} ({meta.permissions_octal})")
|
||||||
|
table.add_row("Owner", f"{meta.owner_username} (UID: {meta.owner_uid})")
|
||||||
|
table.add_row("Group", f"{meta.owner_groupname} (GID: {meta.owner_gid})")
|
||||||
|
table.add_row("Allocated Space", f"{meta.allocated_size} bytes")
|
||||||
|
table.add_row("Hard Links", str(meta.hard_links))
|
||||||
|
table.add_row("Inode", str(meta.inode))
|
||||||
|
table.add_row("Device", str(meta.device))
|
||||||
|
|
||||||
|
if meta.extended_attributes:
|
||||||
|
xattr_str = ", ".join(f"{k}={v}" for k, v in meta.extended_attributes.items())
|
||||||
|
table.add_row("Extended Attributes", xattr_str)
|
||||||
|
|
||||||
|
console.print(table)
|
||||||
|
return meta
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# {{{ list_signatures
|
||||||
|
@forensics_group.command(name="signatures")
|
||||||
|
def list_signatures():
|
||||||
|
"""List all supported file magic signatures and expected extensions."""
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.table import Table
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
table = Table(title="Supported Magic Signatures", show_header=True)
|
||||||
|
table.add_column("Magic Bytes (Hex)", style="bold cyan")
|
||||||
|
table.add_column("File Type", style="bold green")
|
||||||
|
table.add_column("Expected Exts")
|
||||||
|
|
||||||
|
for signature, (type_name, exts) in COMMON_SIGNATURES.items():
|
||||||
|
hex_str = signature.hex().upper()
|
||||||
|
exts_str = ", ".join(exts) if exts else "Any / None"
|
||||||
|
table.add_row(hex_str, type_name, exts_str)
|
||||||
|
|
||||||
|
console.print(table)
|
||||||
|
# }}}
|
||||||
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
# functions for commands needed
|
# functions for commands needed
|
||||||
# src/commands.py
|
# src/commands.py
|
||||||
|
|
||||||
|
# vim foldmethod=marker
|
||||||
import click
|
import click
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -48,11 +50,16 @@ def set_flag_format(pattern: str, original: str):
|
|||||||
if "Competition" not in config:
|
if "Competition" not in config:
|
||||||
config["Competition"] = {}
|
config["Competition"] = {}
|
||||||
|
|
||||||
|
comp_name = config["Competition"].get("competition", "")
|
||||||
|
|
||||||
if original:
|
if original:
|
||||||
if not original.strip():
|
if not original.strip():
|
||||||
raise click.UsageError("Original flag cannot be empty or whitespace only.")
|
raise click.UsageError("Original flag cannot be empty or whitespace only.")
|
||||||
patterns = suggest_patterns(original)
|
if comp_name and comp_name.lower() not in original.lower():
|
||||||
click.echo("Suggested regex patterns:")
|
click.echo(f"Warning: Current competition name '{comp_name}' was not found in the example flag.")
|
||||||
|
|
||||||
|
patterns = suggest_patterns(original, comp_name=comp_name)
|
||||||
|
click.echo(f"Suggested regex patterns for competition {comp_name}:")
|
||||||
for i, pat in enumerate(patterns, 1):
|
for i, pat in enumerate(patterns, 1):
|
||||||
click.echo(f" {i}. {pat}")
|
click.echo(f" {i}. {pat}")
|
||||||
|
|
||||||
@@ -64,15 +71,22 @@ def set_flag_format(pattern: str, original: str):
|
|||||||
click.echo(f"Flag format set to: {pattern}")
|
click.echo(f"Flag format set to: {pattern}")
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
# def Set_Challenge(comp: str, chal: str, setDirectory: bool):
|
# {{{ set_competition
|
||||||
# # set the current challenge and competition from input
|
# Sets the competition name in config.toml
|
||||||
# if state.current_comp != comp:
|
@basic_group.command(name="set-competition")
|
||||||
# state.current_comp=comp
|
@click.argument("name")
|
||||||
# state.comp_dir=pathlib
|
def set_competition(name: str):
|
||||||
# # TODO archive the old competitions
|
"""Set the name of the active competition."""
|
||||||
#
|
from ctf.utils import load_config, write_config
|
||||||
# if state.current_chal != chal:
|
|
||||||
# state.current_chal=chal
|
config_path = "/home/venus/code/ctf/config.toml"
|
||||||
# print("challenge already set")
|
config = load_config(config_path)
|
||||||
# # TODO archive the old challenges
|
|
||||||
# # TODO set the directory to challenge directory, with ignore option
|
if "Competition" not in config:
|
||||||
|
config["Competition"] = {}
|
||||||
|
|
||||||
|
config["Competition"]["competition"] = name
|
||||||
|
write_config(config, config_path)
|
||||||
|
click.echo(f"Competition name set to: {name}")
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
# src/ctf/forensics.py
|
# src/ctf/forensics.py
|
||||||
# Library for forensic analysis
|
# Library for forensic analysis (pure functions only)
|
||||||
|
|
||||||
# vim foldmethod=marker
|
# vim foldmethod=marker
|
||||||
import click
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import stat
|
import stat
|
||||||
@@ -35,16 +34,6 @@ COMMON_SIGNATURES = {
|
|||||||
b"ID3": ("MP3 Audio", [".mp3"]),
|
b"ID3": ("MP3 Audio", [".mp3"]),
|
||||||
}
|
}
|
||||||
|
|
||||||
# {{{ forensics_group
|
|
||||||
@click.group(name="forensics")
|
|
||||||
def forensics_group():
|
|
||||||
''' A collection of forensics tools '''
|
|
||||||
pass
|
|
||||||
|
|
||||||
@forensics_group.command()
|
|
||||||
def tf(): print("hello from forensics")
|
|
||||||
# }}}
|
|
||||||
|
|
||||||
# {{{ FileMetadata
|
# {{{ FileMetadata
|
||||||
@dataclass
|
@dataclass
|
||||||
class FileMetadata:
|
class FileMetadata:
|
||||||
@@ -78,12 +67,13 @@ class FileMetadata:
|
|||||||
extended_attributes: Dict[str, str] = field(default_factory=dict)
|
extended_attributes: Dict[str, str] = field(default_factory=dict)
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
# {{{ inspect
|
# {{{ get_metadata
|
||||||
@forensics_group.command()
|
def get_metadata(path: Path) -> FileMetadata:
|
||||||
@click.argument('path', type=click.Path(exists=True))
|
"""Extracts metadata attributes from a file without any console rendering."""
|
||||||
def inspect(path):
|
|
||||||
'''Lists all basic inode metadata about a file'''
|
|
||||||
p = Path(path)
|
p = Path(path)
|
||||||
|
if not p.exists():
|
||||||
|
raise FileNotFoundError(f"File not found: {p}")
|
||||||
|
|
||||||
stat_info = p.stat()
|
stat_info = p.stat()
|
||||||
|
|
||||||
# Apparent size & extension
|
# Apparent size & extension
|
||||||
@@ -156,7 +146,7 @@ def inspect(path):
|
|||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
meta = FileMetadata(
|
return FileMetadata(
|
||||||
filename=p.name,
|
filename=p.name,
|
||||||
size=size,
|
size=size,
|
||||||
magic=magic,
|
magic=magic,
|
||||||
@@ -174,95 +164,5 @@ def inspect(path):
|
|||||||
device=device,
|
device=device,
|
||||||
extended_attributes=extended_attributes
|
extended_attributes=extended_attributes
|
||||||
)
|
)
|
||||||
|
|
||||||
# Present using Rich Table
|
|
||||||
from rich.console import Console
|
|
||||||
from rich.table import Table
|
|
||||||
|
|
||||||
console = Console()
|
|
||||||
table = Table(title=f"Metadata: {meta.filename}", show_header=False)
|
|
||||||
table.add_column("Key", style="bold cyan")
|
|
||||||
table.add_column("Value")
|
|
||||||
|
|
||||||
table.add_row("Filename", meta.filename)
|
|
||||||
table.add_row("Size", f"{meta.size} bytes")
|
|
||||||
table.add_row("Magic Bytes (Hex)", meta.magic)
|
|
||||||
table.add_row("Detected Type", meta.detected_type)
|
|
||||||
table.add_row("Extension", meta.extension)
|
|
||||||
table.add_row("Permissions", f"{meta.permissions_symbolic} ({meta.permissions_octal})")
|
|
||||||
table.add_row("Owner", f"{meta.owner_username} (UID: {meta.owner_uid})")
|
|
||||||
table.add_row("Group", f"{meta.owner_groupname} (GID: {meta.owner_gid})")
|
|
||||||
table.add_row("Allocated Space", f"{meta.allocated_size} bytes")
|
|
||||||
table.add_row("Hard Links", str(meta.hard_links))
|
|
||||||
table.add_row("Inode", str(meta.inode))
|
|
||||||
table.add_row("Device", str(meta.device))
|
|
||||||
|
|
||||||
if meta.extended_attributes:
|
|
||||||
xattr_str = ", ".join(f"{k}={v}" for k, v in meta.extended_attributes.items())
|
|
||||||
table.add_row("Extended Attributes", xattr_str)
|
|
||||||
|
|
||||||
console.print(table)
|
|
||||||
return meta
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
# {{{ list_signatures
|
|
||||||
@forensics_group.command(name="signatures")
|
|
||||||
def list_signatures():
|
|
||||||
"""List all supported file magic signatures and expected extensions."""
|
|
||||||
from rich.console import Console
|
|
||||||
from rich.table import Table
|
|
||||||
|
|
||||||
console = Console()
|
|
||||||
table = Table(title="Supported Magic Signatures", show_header=True)
|
|
||||||
table.add_column("Magic Bytes (Hex)", style="bold cyan")
|
|
||||||
table.add_column("File Type", style="bold green")
|
|
||||||
table.add_column("Expected Exts")
|
|
||||||
|
|
||||||
for signature, (type_name, exts) in COMMON_SIGNATURES.items():
|
|
||||||
hex_str = signature.hex().upper()
|
|
||||||
exts_str = ", ".join(exts) if exts else "Any / None"
|
|
||||||
table.add_row(hex_str, type_name, exts_str)
|
|
||||||
|
|
||||||
console.print(table)
|
|
||||||
# }}}
|
|
||||||
|
|
||||||
# {{{ flag_detect
|
|
||||||
@forensics_group.command(name="flag-detect")
|
|
||||||
@click.argument("filepath", type=click.Path(exists=True, dir_okay=False, path_type=Path))
|
|
||||||
@click.option("--pattern", "-p", default=r"[a-zA-Z0-9_\-]+{[^}]+}", help="Regex pattern to search for.")
|
|
||||||
@click.option("--min-len", "-m", default=4, help="Minimum printable string length.")
|
|
||||||
def flag_detect(filepath: Path, pattern: str, min_len: int):
|
|
||||||
"""Extract printable strings and search for flag patterns."""
|
|
||||||
import re
|
|
||||||
from rich.console import Console
|
|
||||||
from rich.panel import Panel
|
|
||||||
|
|
||||||
console = Console()
|
|
||||||
try:
|
|
||||||
with open(filepath, "rb") as f:
|
|
||||||
content = f.read()
|
|
||||||
except Exception as e:
|
|
||||||
console.print(f"[bold red]Error reading file:[/bold red] {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Extract printable strings
|
|
||||||
printable_re = re.compile(rb"[a-zA-Z0-9/\-:.,_$%'\"()[\]<> ]{" + str(min_len).encode() + rb",}")
|
|
||||||
strings = printable_re.findall(content)
|
|
||||||
|
|
||||||
# Search for flags matching pattern
|
|
||||||
flag_re = re.compile(pattern.encode("utf-8"))
|
|
||||||
flags_found = []
|
|
||||||
for s in strings:
|
|
||||||
for match in flag_re.finditer(s):
|
|
||||||
flags_found.append(match.group().decode("utf-8", errors="ignore"))
|
|
||||||
|
|
||||||
if flags_found:
|
|
||||||
output = "\n".join(f" [bold green]✓[/bold green] {flag}" for flag in flags_found)
|
|
||||||
console.print(Panel(
|
|
||||||
output,
|
|
||||||
title=f"[bold green]Potential Flag(s) Found ({len(flags_found)})[/bold green]",
|
|
||||||
border_style="green"
|
|
||||||
))
|
|
||||||
else:
|
|
||||||
console.print("[bold yellow]No flag patterns found.[/bold yellow]")
|
|
||||||
# }}}
|
|
||||||
|
|||||||
@@ -2,8 +2,87 @@
|
|||||||
# Parses and calls commands
|
# Parses and calls commands
|
||||||
|
|
||||||
from ctf.commands import basic_group
|
from ctf.commands import basic_group
|
||||||
from ctf.forensics import forensics_group
|
from ctf.cli_forensics import forensics_group
|
||||||
import click
|
import click
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
|
||||||
|
# Globally track the detector instance if it is active
|
||||||
|
_active_detector = None
|
||||||
|
|
||||||
|
# {{{ FlagDetectorStream
|
||||||
|
class FlagDetectorStream:
|
||||||
|
"""Intercepts stdout to scan output for matching flag formats."""
|
||||||
|
def __init__(self, original_stream, pattern_str):
|
||||||
|
self.original_stream = original_stream
|
||||||
|
self._flag_buffer = ""
|
||||||
|
self.detecting = True
|
||||||
|
|
||||||
|
# Clean pattern from standard anchors ^ and $ for substring matching
|
||||||
|
pat = pattern_str
|
||||||
|
if pat.startswith("^") and pat.endswith("$"):
|
||||||
|
pat = pat[1:-1]
|
||||||
|
try:
|
||||||
|
self.pattern = re.compile(pat) if pat else None
|
||||||
|
except re.error:
|
||||||
|
self.pattern = None
|
||||||
|
|
||||||
|
def write(self, data):
|
||||||
|
self.original_stream.write(data)
|
||||||
|
if self.detecting and self.pattern:
|
||||||
|
self._flag_buffer += data
|
||||||
|
matches = []
|
||||||
|
for match in self.pattern.finditer(self._flag_buffer):
|
||||||
|
matches.append(match.group())
|
||||||
|
if matches:
|
||||||
|
self.detecting = False
|
||||||
|
self.original_stream.write("\n\033[1;32m[!] Potential flag(s) detected in command output:\033[0m\n")
|
||||||
|
for match in matches:
|
||||||
|
self.original_stream.write(f" \033[1;32m✓ {match}\033[0m\n")
|
||||||
|
try:
|
||||||
|
from ctf.utils import load_config, write_config
|
||||||
|
config_path = "/home/venus/code/ctf/config.toml"
|
||||||
|
config = load_config(config_path)
|
||||||
|
if "Competition" not in config:
|
||||||
|
config["Competition"] = {}
|
||||||
|
config["Competition"]["last_flag"] = match
|
||||||
|
write_config(config, config_path)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._flag_buffer = ""
|
||||||
|
self.detecting = True
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
self.original_stream.flush()
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
return getattr(self.original_stream, name)
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# {{{ flag_cmd
|
||||||
|
@click.command(name="flag")
|
||||||
|
@click.option("-p", "--plain", is_flag=True, help="Print raw flag without flavor text.")
|
||||||
|
def flag_cmd(plain):
|
||||||
|
"""Retrieve the last detected flag from config."""
|
||||||
|
global _active_detector
|
||||||
|
if _active_detector is not None:
|
||||||
|
_active_detector.detecting = False
|
||||||
|
try:
|
||||||
|
from ctf.utils import load_config
|
||||||
|
config = load_config("/home/venus/code/ctf/config.toml")
|
||||||
|
last_flag = config.get("Competition", {}).get("last_flag", "")
|
||||||
|
if not last_flag:
|
||||||
|
if not plain:
|
||||||
|
click.echo("No flag has been detected yet.")
|
||||||
|
return
|
||||||
|
if plain:
|
||||||
|
click.echo(last_flag, nl=False)
|
||||||
|
else:
|
||||||
|
click.echo(f"Last detected flag: {last_flag}")
|
||||||
|
finally:
|
||||||
|
if _active_detector is not None:
|
||||||
|
_active_detector.detecting = True
|
||||||
|
# }}}
|
||||||
|
|
||||||
# {{{ main
|
# {{{ main
|
||||||
def main():
|
def main():
|
||||||
@@ -12,6 +91,23 @@ def main():
|
|||||||
|
|
||||||
cli.add_command(forensics_group)
|
cli.add_command(forensics_group)
|
||||||
cli.add_command(basic_group)
|
cli.add_command(basic_group)
|
||||||
|
cli.add_command(flag_cmd)
|
||||||
|
|
||||||
|
from ctf.utils import load_config
|
||||||
|
config = load_config("/home/venus/code/ctf/config.toml")
|
||||||
|
flag_format = config.get("Competition", {}).get("flag_format", "")
|
||||||
|
|
||||||
|
global _active_detector
|
||||||
|
if flag_format:
|
||||||
|
detector = FlagDetectorStream(sys.stdout, flag_format)
|
||||||
|
sys.stdout = detector
|
||||||
|
_active_detector = detector
|
||||||
|
try:
|
||||||
|
cli()
|
||||||
|
finally:
|
||||||
|
sys.stdout = detector.original_stream
|
||||||
|
_active_detector = None
|
||||||
|
else:
|
||||||
cli()
|
cli()
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
|||||||
153
src/ctf/utils.py
153
src/ctf/utils.py
@@ -37,17 +37,14 @@ def active_competitions(dir: str) -> dict:
|
|||||||
comps[item] = active_categories(item)
|
comps[item] = active_categories(item)
|
||||||
print(item.name)
|
print(item.name)
|
||||||
print(comps[item])
|
print(comps[item])
|
||||||
|
return comps
|
||||||
|
|
||||||
# {{{ suggest_patterns
|
# {{{ suggest_patterns
|
||||||
def suggest_patterns(s: str) -> list[str]:
|
def suggest_patterns(s: str, comp_name: str = "") -> list[str]:
|
||||||
"""Suggests a list of regex patterns from a sample flag string."""
|
"""Suggests a list of regex patterns from a sample flag string, optionally incorporating the competition name."""
|
||||||
if not s or not s.strip():
|
if not s or not s.strip():
|
||||||
raise ValueError("Input string cannot be empty or whitespace only.")
|
raise ValueError("Input string cannot be empty or whitespace only.")
|
||||||
|
|
||||||
# Group characters into types
|
|
||||||
groups = []
|
|
||||||
current_type = None
|
|
||||||
current_chars = []
|
|
||||||
|
|
||||||
def get_type(c):
|
def get_type(c):
|
||||||
if c.isupper():
|
if c.isupper():
|
||||||
return 'U'
|
return 'U'
|
||||||
@@ -58,6 +55,54 @@ def suggest_patterns(s: str) -> list[str]:
|
|||||||
else:
|
else:
|
||||||
return 'S'
|
return 'S'
|
||||||
|
|
||||||
|
groups = []
|
||||||
|
comp_idx = -1
|
||||||
|
matched_prefix = ""
|
||||||
|
if comp_name and comp_name.strip():
|
||||||
|
comp_idx = s.lower().find(comp_name.lower())
|
||||||
|
|
||||||
|
if comp_idx != -1:
|
||||||
|
matched_prefix = s[comp_idx : comp_idx + len(comp_name)]
|
||||||
|
# Parse prefix before competition name
|
||||||
|
prefix_part = s[:comp_idx]
|
||||||
|
if prefix_part:
|
||||||
|
current_type = None
|
||||||
|
current_chars = []
|
||||||
|
for c in prefix_part:
|
||||||
|
ctype = get_type(c)
|
||||||
|
if ctype != current_type:
|
||||||
|
if current_type is not None:
|
||||||
|
groups.append((current_type, "".join(current_chars)))
|
||||||
|
current_type = ctype
|
||||||
|
current_chars = [c]
|
||||||
|
else:
|
||||||
|
current_chars.append(c)
|
||||||
|
if current_type is not None:
|
||||||
|
groups.append((current_type, "".join(current_chars)))
|
||||||
|
|
||||||
|
# Insert competition name group
|
||||||
|
groups.append(('C', matched_prefix))
|
||||||
|
|
||||||
|
# Parse suffix after competition name
|
||||||
|
suffix_part = s[comp_idx + len(comp_name):]
|
||||||
|
if suffix_part:
|
||||||
|
current_type = None
|
||||||
|
current_chars = []
|
||||||
|
for c in suffix_part:
|
||||||
|
ctype = get_type(c)
|
||||||
|
if ctype != current_type:
|
||||||
|
if current_type is not None:
|
||||||
|
groups.append((current_type, "".join(current_chars)))
|
||||||
|
current_type = ctype
|
||||||
|
current_chars = [c]
|
||||||
|
else:
|
||||||
|
current_chars.append(c)
|
||||||
|
if current_type is not None:
|
||||||
|
groups.append((current_type, "".join(current_chars)))
|
||||||
|
else:
|
||||||
|
# Normal grouping without competition name
|
||||||
|
current_type = None
|
||||||
|
current_chars = []
|
||||||
for c in s:
|
for c in s:
|
||||||
ctype = get_type(c)
|
ctype = get_type(c)
|
||||||
if ctype != current_type:
|
if ctype != current_type:
|
||||||
@@ -79,55 +124,36 @@ def suggest_patterns(s: str) -> list[str]:
|
|||||||
res.append(c)
|
res.append(c)
|
||||||
return "".join(res)
|
return "".join(res)
|
||||||
|
|
||||||
# 1. Exact counts for group types
|
def build_pat(use_counts=True, merge_case=False):
|
||||||
opt1_parts = []
|
parts = []
|
||||||
for gtype, gchars in groups:
|
for gtype, gchars in groups:
|
||||||
if gtype == 'U':
|
if gtype == 'C':
|
||||||
opt1_parts.append(f"[A-Z]{{{len(gchars)}}}")
|
parts.append(escape_special(gchars))
|
||||||
|
elif gtype == 'U':
|
||||||
|
if merge_case:
|
||||||
|
parts.append(f"[A-Za-z]{{{len(gchars)}}}" if use_counts else "[A-Za-z]+")
|
||||||
|
else:
|
||||||
|
parts.append(f"[A-Z]{{{len(gchars)}}}" if use_counts else "[A-Z]+")
|
||||||
elif gtype == 'L':
|
elif gtype == 'L':
|
||||||
opt1_parts.append(f"[a-z]{{{len(gchars)}}}")
|
if merge_case:
|
||||||
elif gtype == 'D':
|
parts.append(f"[A-Za-z]{{{len(gchars)}}}" if use_counts else "[A-Za-z]+")
|
||||||
opt1_parts.append(f"\\d{{{len(gchars)}}}")
|
|
||||||
else:
|
else:
|
||||||
opt1_parts.append(escape_special(gchars))
|
parts.append(f"[a-z]{{{len(gchars)}}}" if use_counts else "[a-z]+")
|
||||||
opt1 = "^" + "".join(opt1_parts) + "$"
|
|
||||||
|
|
||||||
# 2. Variable counts for group types
|
|
||||||
opt2_parts = []
|
|
||||||
for gtype, gchars in groups:
|
|
||||||
if gtype == 'U':
|
|
||||||
opt2_parts.append("[A-Z]+")
|
|
||||||
elif gtype == 'L':
|
|
||||||
opt2_parts.append("[a-z]+")
|
|
||||||
elif gtype == 'D':
|
elif gtype == 'D':
|
||||||
opt2_parts.append("\\d+")
|
parts.append(f"\\d{{{len(gchars)}}}" if use_counts else "\\d+")
|
||||||
else:
|
else:
|
||||||
opt2_parts.append(escape_special(gchars))
|
parts.append(escape_special(gchars))
|
||||||
opt2 = "^" + "".join(opt2_parts) + "$"
|
return "".join(parts)
|
||||||
|
|
||||||
# 3. Case-insensitive / merged letters with exact counts
|
candidates = []
|
||||||
opt3_parts = []
|
|
||||||
for gtype, gchars in groups:
|
|
||||||
if gtype in ('U', 'L'):
|
|
||||||
opt3_parts.append(f"[A-Za-z]{{{len(gchars)}}}")
|
|
||||||
elif gtype == 'D':
|
|
||||||
opt3_parts.append(f"\\d{{{len(gchars)}}}")
|
|
||||||
else:
|
|
||||||
opt3_parts.append(escape_special(gchars))
|
|
||||||
opt3 = "^" + "".join(opt3_parts) + "$"
|
|
||||||
|
|
||||||
# 4. Case-insensitive / merged letters with variable counts
|
# 1. Unanchored patterns (ideal for searching/scanning)
|
||||||
opt4_parts = []
|
candidates.append(build_pat(use_counts=True, merge_case=False))
|
||||||
for gtype, gchars in groups:
|
candidates.append(build_pat(use_counts=False, merge_case=False))
|
||||||
if gtype in ('U', 'L'):
|
candidates.append(build_pat(use_counts=True, merge_case=True))
|
||||||
opt4_parts.append("[A-Za-z]+")
|
candidates.append(build_pat(use_counts=False, merge_case=True))
|
||||||
elif gtype == 'D':
|
|
||||||
opt4_parts.append("\\d+")
|
|
||||||
else:
|
|
||||||
opt4_parts.append(escape_special(gchars))
|
|
||||||
opt4 = "^" + "".join(opt4_parts) + "$"
|
|
||||||
|
|
||||||
# 5. General alphanumeric character class plus unique special characters
|
# Custom character class
|
||||||
unique_specials = set()
|
unique_specials = set()
|
||||||
has_alpha = False
|
has_alpha = False
|
||||||
has_digit = False
|
has_digit = False
|
||||||
@@ -139,7 +165,6 @@ def suggest_patterns(s: str) -> list[str]:
|
|||||||
has_digit = True
|
has_digit = True
|
||||||
else:
|
else:
|
||||||
unique_specials.add(c)
|
unique_specials.add(c)
|
||||||
|
|
||||||
char_class_parts = []
|
char_class_parts = []
|
||||||
if has_alpha:
|
if has_alpha:
|
||||||
char_class_parts.append("A-Za-z")
|
char_class_parts.append("A-Za-z")
|
||||||
@@ -150,13 +175,35 @@ def suggest_patterns(s: str) -> list[str]:
|
|||||||
char_class_parts.append("\\" + spec)
|
char_class_parts.append("\\" + spec)
|
||||||
else:
|
else:
|
||||||
char_class_parts.append(spec)
|
char_class_parts.append(spec)
|
||||||
opt5 = f"^[{''.join(char_class_parts)}]+$"
|
candidates.append(f"[{''.join(char_class_parts)}]+")
|
||||||
|
candidates.append(escape_special(s))
|
||||||
|
|
||||||
# 6. Literal exact escaped match
|
# Wildcard patterns with .* and .*? (unanchored)
|
||||||
opt6 = "^" + escape_special(s) + "$"
|
if comp_idx == 0:
|
||||||
|
suffix_part = s[len(comp_name):]
|
||||||
|
esc_prefix = escape_special(matched_prefix)
|
||||||
|
if suffix_part.startswith("{") and suffix_part.endswith("}"):
|
||||||
|
candidates.append(f"{esc_prefix}\\{{.*\\}}")
|
||||||
|
candidates.append(f"{esc_prefix}\\{{.*?\\}}")
|
||||||
|
candidates.append(f"{esc_prefix}\\{{[^}}]*\\}}")
|
||||||
|
candidates.append(f"{esc_prefix}\\{{[A-Za-z0-9_\\-]+\\}}")
|
||||||
|
inner_content = suffix_part[1:-1]
|
||||||
|
if all(c.islower() or c.isdigit() or c == '_' for c in inner_content):
|
||||||
|
candidates.append(f"{esc_prefix}\\{{[a-z0-9_]+\\}}")
|
||||||
|
else:
|
||||||
|
candidates.append(f"{esc_prefix}.*")
|
||||||
|
candidates.append(f"{esc_prefix}.*?")
|
||||||
|
else:
|
||||||
|
candidates.append(".*")
|
||||||
|
candidates.append(".*?")
|
||||||
|
|
||||||
|
# 2. Anchored versions of the above (ideal for strict validation)
|
||||||
|
anchored_candidates = []
|
||||||
|
for cand in candidates:
|
||||||
|
anchored_candidates.append(f"^{cand}$")
|
||||||
|
candidates.extend(anchored_candidates)
|
||||||
|
|
||||||
# Deduplicate while preserving order
|
# Deduplicate while preserving order
|
||||||
candidates = [opt1, opt2, opt3, opt4, opt5, opt6]
|
|
||||||
seen = set()
|
seen = set()
|
||||||
result = []
|
result = []
|
||||||
for c in candidates:
|
for c in candidates:
|
||||||
|
|||||||
BIN
tests/artifacts/cat.jpg
Normal file
BIN
tests/artifacts/cat.jpg
Normal file
Binary file not shown.
BIN
tests/artifacts/easy-cat.jpg
Normal file
BIN
tests/artifacts/easy-cat.jpg
Normal file
Binary file not shown.
BIN
tests/artifacts/easy-cat.jpg_original
Normal file
BIN
tests/artifacts/easy-cat.jpg_original
Normal file
Binary file not shown.
BIN
tests/artifacts/hips.jpg
Normal file
BIN
tests/artifacts/hips.jpg
Normal file
Binary file not shown.
BIN
tests/artifacts/red.png
Normal file
BIN
tests/artifacts/red.png
Normal file
Binary file not shown.
1
tests/env/pure_flag_data.bin
vendored
Normal file
1
tests/env/pure_flag_data.bin
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pre_flag{hello_world_1337}post_stuff
|
||||||
Reference in New Issue
Block a user