Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .env.template
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
LITELLM_BASE_URL=https://cyclonus.gtisc.gatech.edu:41414/
LITELLM_BASE_URL=https://goldbug.gtisc.gatech.edu:41414
LITELLM_API_KEY=sk-your-team-key-here
CTFD_URL=http://129.213.16.86:8000
CTFD_TOKEN=ctfd-your-team-token-here
61 changes: 61 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,64 @@ The system uses Docker containers for challenge execution with the following flo
6. **Resource Cleanup**: Automatic cleanup of containers and networks

For detailed architecture documentation, see [docs/architecture.md](./docs/architecture.md).

## CTFd Integration Setup

### Overview

The agent can now fetch challenges directly from a CTFd instance and shoot flags at it, rather than using local challenge directories.

### Usage

#### Basic: Run all challenges from CTFd

```bash
uv run eval_agent.py --ctfd
```

#### Run a specific challenge

```bash
uv run eval_agent.py --ctfd --challenge "File Intro"
```


### Configuration

The default settings are already configured in `eval_agent.py`:

- **CTFd URL**: `http://129.213.16.86:8000` (the deployed CTFd instance)
- **API Token**: Go to http://129.213.16.86:8000, create an account and team. Then in settings, creating a new access token and paste it into the .env

### How It Works

1. The agent fetches challenges from the CTFd API
2. Downloads any challenge files automatically
3. Uses your LLM agent to solve the challenge
4. Submits flags directly to CTFd
5. Results are saved in `eval_results/`

### Current CTFd Challenges

Your instance has 9 challenges:

- File Intro (Forensics, 100 pts)
- Net Intro (Networking, 100 pts)
- Baby Cat (Miscellaneous, 100 pts)
- Baby Web (Web, 150 pts)
- Easy SQL Injection (Web, 200 pts)
- Rotatouille (Cryptography, 150 pts)
- Simple Crypto 1 (Cryptography, 200 pts)
- Simple Rev 1 (Reverse Engineering, 200 pts)
- Simple Rev 2 (Reverse Engineering, 250 pts)

### Get Your API Token

If you need a new token:

1. Visit http://129.213.16.86:8000
2. create an account
3. Go to settings
4. access token tab
5. create a new access token and copy it into .env

52 changes: 50 additions & 2 deletions agent/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,11 @@ def solve_challenge(self, challenge: CTFChallengeClient) -> str | None:
self.log(f"Challenge categories: {challenge.challenge.categories}")

# Check if this is a network-based challenge (has network_info)
if (challenge.network_info and
challenge.network_info.get('network_name')):
# For CTFd challenges, check for connection_info
is_ctfd_network = challenge.network_info and challenge.network_info.get('is_ctfd_challenge')
has_network_info = challenge.network_info and challenge.network_info.get('network_name')

if has_network_info or is_ctfd_network:
self.log("Detected network-based challenge")
return self._solve_network_challenge(challenge)
else:
Expand All @@ -58,6 +61,51 @@ def _solve_network_challenge(self, challenge: CTFChallengeClient) -> str | None:
"""Handle network-based challenges with service discovery."""

network_name = challenge.network_info.get('network_name', 'unknown')
connection_info = challenge.network_info.get('connection_info', '')
is_ctfd = challenge.network_info.get('is_ctfd_challenge', False)

if is_ctfd and connection_info:
# For CTFd network challenges, use the connection_info directly
self.log(f"CTFd Network Challenge: {connection_info}")
# Extract host and port from connection_info (format: "nc HOST PORT")
parts = connection_info.split()
self.log(f"Parsed connection_info parts: {parts}")
if len(parts) >= 3 and parts[0] == 'nc':
host = parts[1]
port = parts[2]
self.log(f"Connecting to {host}:{port}")

# Try to connect with netcat and get flag
import subprocess
try:
cmd = f"nc {host} {port}"
self.log(f"Running command: {cmd}")
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=10)
output = result.stdout
self.log(f"Netcat output: {output}")

# Extract flag from output
import re
match = re.search(challenge.challenge.flag_regex, output)
if match:
flag = match.group(0)
self.log(f"Found flag: {flag}")
if challenge.submit_flag(flag):
self.log("CORRECT FLAG SUBMITTED!")
return flag
else:
self.log("Flag submitted but marked as incorrect")
else:
self.log(f"No flag found in output matching pattern: {challenge.challenge.flag_regex}")
except Exception as e:
self.log(f"Error connecting to {host}:{port}: {e}")
import traceback
traceback.print_exc()
else:
self.log(f"connection_info format not recognized: {connection_info}")

return None

self.log(f"Operating in Docker network: {network_name}")

llm_client = self.lite_llm_manager.create_client()
Expand Down
117 changes: 113 additions & 4 deletions eval_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,20 @@
import time
from datetime import datetime

from helper.ctf_challenge import create_challenge_from_chaldir
from helper.ctf_challenge import create_challenge_from_chaldir, get_challenges_from_ctfd
from helper.llm_helper import LiteLLMManager
from helper.docker_manager import DockerManager
import dotenv

# Setup logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

def get_challenge_dirs(challenge_target=None):
def get_challenge_dirs(challenge_target=None, use_ctfd=False, ctfd_url=None, ctfd_token=None):
"""Gets a list of challenge directories."""
# If using CTFd, return special marker to fetch from API
if use_ctfd:
return [{'ctfd': True, 'url': ctfd_url, 'token': ctfd_token, 'name': challenge_target}]

challenge_base_dir = 'challenges'
if challenge_target:
chal_dir = os.path.join(challenge_base_dir, challenge_target)
Expand All @@ -26,6 +31,95 @@ def get_challenge_dirs(challenge_target=None):
return [os.path.join(challenge_base_dir, d) for d in os.listdir(challenge_base_dir) if os.path.isdir(os.path.join(challenge_base_dir, d))]

def evaluate_challenge(chal_dir, llm_manager, run_output_dir, run_timestamp):
# Check if this is a CTFd challenge
if isinstance(chal_dir, dict) and chal_dir.get('ctfd'):
challenge_name = chal_dir.get('name', 'all')
ctfd_url = chal_dir.get('url')
ctfd_token = chal_dir.get('token')

logging.info(f"--- Fetching challenges from CTFd ---")
challenges = get_challenges_from_ctfd(ctfd_url, ctfd_token, challenge_name)

# Evaluate each challenge
results = []
temp_artifacts_dirs = [] # Keep track of temp dirs to avoid cleanup

for challenge in challenges:
challenge_name = challenge.name
challenge_output_dir = os.path.join(run_output_dir, challenge_name)
os.makedirs(challenge_output_dir, exist_ok=True)

# For CTFd challenges, we skip Docker setup
# They are solved directly against the CTFd instance
try:
from agent.agent import Agent
from helper.ctf_challenge import CTFChallengeClient

agent = Agent(llm_manager)

# Create a simple client that submits to CTFd
def submit_to_ctfd(flag):
import requests
session = requests.Session()
session.headers.update({
'Authorization': f'Token {ctfd_token}',
'Content-Type': 'application/json'
})
try:
response = session.post(
f"{ctfd_url}/api/v1/challenges/attempt",
json={'challenge_id': challenge.id, 'submission': flag},
timeout=10
)
data = response.json().get('data', {})
return data.get('status') == 'correct'
except:
return False

# Store artifacts folder so it doesn't get cleaned up
temp_artifacts_dirs.append(challenge.artifacts_folder)

# Create CTFd client
# Let CTFChallengeClient copy from challenge.artifacts_folder to working_folder
working_folder = os.path.join(challenge_output_dir, "workdir")

# Check if this is a network-based challenge from CTFd
network_info = None
if challenge.services:
# Network challenge - extract connection_info
service = challenge.services[0]
connection_info = service.get('connection_info', '')
if connection_info:
network_info = {
'connection_info': connection_info,
'network_name': 'ctfd_network',
'is_ctfd_challenge': True
}

client = CTFChallengeClient(challenge, working_folder, submit_to_ctfd, network_info=network_info)

start_time = time.time()
flag = agent.solve_challenge(client)
end_time = time.time()

result_data = {
"challenge_name": challenge.name,
"success": flag is not None,
"submitted_flag": flag,
"start_time": datetime.fromtimestamp(start_time).isoformat(),
"end_time": datetime.fromtimestamp(end_time).isoformat(),
"duration": end_time - start_time,
}

with open(os.path.join(challenge_output_dir, "result.json"), "w") as f:
json.dump(result_data, f, indent=4)

results.append(result_data)
except Exception as e:
logging.error(f"Failed to evaluate {challenge_name}: {e}")

return results

challenge_name = os.path.basename(chal_dir)
logging.info(f"--- Running evaluation for challenge: {challenge_name} ---")

Expand Down Expand Up @@ -167,7 +261,11 @@ def run_evaluation(challenge_dirs, llm_manager):
for chal_dir in challenge_dirs:
try:
result = evaluate_challenge(chal_dir, llm_manager, run_output_dir, run_timestamp)
results.append(result)
# Handle both single result and list of results
if isinstance(result, list):
results.extend(result)
else:
results.append(result)
except Exception as exc:
logging.error(f'{chal_dir} generated an exception: {exc}')

Expand Down Expand Up @@ -202,12 +300,23 @@ def run_evaluation(challenge_dirs, llm_manager):


def main():
# Load environment variables from .env file
dotenv.load_dotenv()

parser = argparse.ArgumentParser(description="Evaluate CTF agent.")
parser.add_argument("--challenge", help="Specify a single challenge directory name to run.", type=str, default=None)
parser.add_argument("--ctfd", help="Use CTFd endpoint instead of local challenges.", action="store_true")
parser.add_argument("--ctfd-url", help="CTFd instance URL.", type=str, default=os.getenv("CTFD_URL", "http://129.213.16.86:8000"))
parser.add_argument("--ctfd-token", help="CTFd API token.", type=str, default=os.getenv("CTFD_TOKEN"))
args = parser.parse_args()

llm_manager = LiteLLMManager()
challenge_dirs = get_challenge_dirs(args.challenge)
challenge_dirs = get_challenge_dirs(
args.challenge,
use_ctfd=args.ctfd,
ctfd_url=args.ctfd_url,
ctfd_token=args.ctfd_token
)
if challenge_dirs:
run_evaluation(challenge_dirs, llm_manager)
else:
Expand Down
37 changes: 37 additions & 0 deletions helper/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""
Helper module for CTF Agent integration
Includes CTFd and CTFTime compatibility
"""

from helper.legacy_ctf_challenge import (
CTFChallenge,
CTFdChallenge,
CTFTimeChallenge,
CTFdClient,
CTFTimeHelper,
CTFChallenge,
ChallengeFiles,
ChallengeHint,
)

from helper.ctftime_helper import (
CTFTimeEndpoint,
CTFTimeHelper,
get_active_challenges_from_ctftime,
sync_ctftime_to_local,
)

__all__ = [
'CTFChallenge',
'CTFdChallenge',
'CTFTimeChallenge',
'CTFdClient',
'CTFTimeHelper',
'CTFChallenge',
'ChallengeFiles',
'ChallengeHint',
'CTFTimeEndpoint',
'get_active_challenges_from_ctftime',
'sync_ctftime_to_local',
]

Loading