From dd31d380e09b8b5b738983951708081be9a53ad2 Mon Sep 17 00:00:00 2001 From: Human-Gechi Date: Tue, 2 Jun 2026 15:17:45 +0100 Subject: [PATCH 1/4] Normalize .sh scripts to LF endings --- .github/skills/ctf-testing/deploy_and_test.sh | 1296 ++++++++-------- .../skills/ctf-testing/test_ctf_challenges.sh | 1318 ++++++++--------- ctf_setup.sh | 4 +- 3 files changed, 1310 insertions(+), 1308 deletions(-) diff --git a/.github/skills/ctf-testing/deploy_and_test.sh b/.github/skills/ctf-testing/deploy_and_test.sh index 7182faa..2318f6f 100755 --- a/.github/skills/ctf-testing/deploy_and_test.sh +++ b/.github/skills/ctf-testing/deploy_and_test.sh @@ -1,648 +1,648 @@ -#!/usr/bin/env bash -# shellcheck shell=bash -# -# CTF Deploy and Test Orchestration Script -# Deploys infrastructure to cloud providers and runs tests -# -# Usage: -# ./deploy_and_test.sh [--with-reboot] -# DEBUG=true ./deploy_and_test.sh azure # Enable debug tracing -# -# Arguments: -# aws|azure|gcp|all Cloud provider(s) to test -# --with-reboot After initial tests pass, stop/start the VM and -# re-run verification to ensure services survive -# reboot and progress persists -# -# Prerequisites: -# - terraform (>= 1.0) -# - jq (for AWS terraform config) -# - sshpass (macOS: brew install hudochenkov/sshpass/sshpass) -# - aws CLI (for AWS, must be logged in) -# - az CLI (for Azure, must be logged in) -# - gcloud CLI (for GCP, must be authenticated) -# -# Examples: -# ./deploy_and_test.sh aws # Test AWS only -# ./deploy_and_test.sh azure --with-reboot # Test Azure with reboot -# ./deploy_and_test.sh all # Test all providers -# ./deploy_and_test.sh all --with-reboot # Full test suite -# - -set -o errexit -set -o pipefail -set -o nounset - -# Enable debug tracing if DEBUG=true -[[ "${DEBUG:-}" == 'true' ]] && set -o xtrace - -# ============================================================================= -# CONSTANTS -# ============================================================================= - -# Script paths (declare and assign separately to avoid masking return values) -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -readonly SCRIPT_DIR -REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)" -readonly REPO_ROOT -readonly TEST_SCRIPT="${SCRIPT_DIR}/test_ctf_challenges.sh" - -# SSH connection settings -readonly MAX_SSH_ATTEMPTS=30 -readonly SSH_RETRY_INTERVAL=10 -readonly SSH_USER="ctf_user" -readonly SSH_PASS="CTFpassword123!" -readonly SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR" - -# Terminal colors for output formatting -readonly RED='\033[0;31m' -readonly GREEN='\033[0;32m' -readonly YELLOW='\033[1;33m' -readonly BLUE='\033[0;34m' -readonly NC='\033[0m' # No Color - -# ============================================================================= -# GLOBAL STATE -# ============================================================================= - -# Cleanup tracking (mutable - set during test execution) -CURRENT_PROVIDER="" -CLEANUP_ON_EXIT=false - -# ============================================================================= -# UTILITY FUNCTIONS -# ============================================================================= - -# Log a message with timestamp and color based on level -# Arguments: -# $1 - Log level (INFO, OK, WARN, ERROR) -# $@ - Message to log -_log() { - local level="$1" - shift - local message="$*" - local timestamp - timestamp=$(date '+%H:%M:%S') - - case "${level}" in - INFO) echo -e "[${timestamp}] ${BLUE}${message}${NC}" ;; - OK) echo -e "[${timestamp}] ${GREEN}${message}${NC}" ;; - WARN) echo -e "[${timestamp}] ${YELLOW}${message}${NC}" ;; - ERROR) echo -e "[${timestamp}] ${RED}${message}${NC}" ;; - *) echo -e "[${timestamp}] ${level} ${message}" ;; - esac -} - -# Signal handler for cleanup on interrupt (SIGINT/SIGTERM) -# Destroys any in-progress infrastructure before exiting -_cleanup_handler() { - local exit_code=$? - - if [[ "${CLEANUP_ON_EXIT}" == true ]] && [[ -n "${CURRENT_PROVIDER}" ]]; then - echo "" - _log WARN "Caught interrupt - cleaning up ${CURRENT_PROVIDER} infrastructure..." - _terraform_destroy "${CURRENT_PROVIDER}" || true - fi - exit "${exit_code}" -} - -trap _cleanup_handler SIGINT SIGTERM - -# Execute sshpass command with password passed via file descriptor -# This hides the password from the process list -# Arguments: -# $@ - Command and arguments to pass to sshpass -_sshpass_cmd() { - sshpass -f <(printf '%s' "${SSH_PASS}") "$@" -} - -# ============================================================================= -# ARGUMENT PARSING -# ============================================================================= - -WITH_REBOOT=false -PROVIDERS_TO_TEST=() - -for arg in "$@"; do - case "${arg}" in - aws|azure|gcp) - PROVIDERS_TO_TEST+=("${arg}") - ;; - all) - PROVIDERS_TO_TEST=("aws" "azure" "gcp") - ;; - --with-reboot) - WITH_REBOOT=true - ;; - -h|--help) - head -32 "$0" | tail -30 - exit 0 - ;; - *) - echo "Unknown argument: ${arg}" - echo "Usage: $0 [--with-reboot]" - exit 1 - ;; - esac -done - -if [[ ${#PROVIDERS_TO_TEST[@]} -eq 0 ]]; then - echo "Usage: $0 [--with-reboot]" - exit 1 -fi - -# ============================================================================= -# PREREQUISITE CHECKS -# ============================================================================= - -# Verify all required tools are installed and authenticated for a provider -# Arguments: -# $1 - Cloud provider name (aws, azure, gcp) -_check_prerequisites() { - local provider="$1" - local missing=() - - echo -e "${BLUE}Checking prerequisites for ${provider}...${NC}" - - # Check terraform - if ! command -v terraform &>/dev/null; then - missing+=("terraform") - fi - - # Check jq (required for AWS terraform config) - if ! command -v jq &>/dev/null; then - missing+=("jq") - fi - - # Check sshpass - if ! command -v sshpass &>/dev/null; then - echo -e "${RED}ERROR: sshpass is required but not installed.${NC}" - echo "" - echo "Install on macOS:" - echo " brew install hudochenkov/sshpass/sshpass" - echo "" - echo "Install on Ubuntu/Debian:" - echo " sudo apt-get install sshpass" - echo "" - exit 1 - fi - - # Check provider-specific CLI - case "${provider}" in - aws) - if ! command -v aws &>/dev/null; then - missing+=("aws CLI") - elif ! aws sts get-caller-identity &>/dev/null; then - echo -e "${RED}ERROR: AWS CLI not authenticated. Run 'aws configure' first.${NC}" - exit 1 - fi - ;; - azure) - if ! command -v az &>/dev/null; then - missing+=("az CLI") - elif ! az account show &>/dev/null; then - echo -e "${RED}ERROR: Azure CLI not authenticated. Run 'az login' first.${NC}" - exit 1 - fi - ;; - gcp) - if ! command -v gcloud &>/dev/null; then - missing+=("gcloud CLI") - elif ! gcloud auth list --filter=status:ACTIVE --format="value(account)" 2>/dev/null \ - | head -1 \ - | grep -q '@'; then - echo -e "${RED}ERROR: GCP CLI not authenticated. Run 'gcloud auth login' first.${NC}" - exit 1 - fi - ;; - esac - - if [[ ${#missing[@]} -gt 0 ]]; then - echo -e "${RED}ERROR: Missing required tools: ${missing[*]}${NC}" - exit 1 - fi - - echo -e "${GREEN}Prerequisites OK${NC}" -} - -# ============================================================================= -# TERRAFORM OPERATIONS -# ============================================================================= - -# Get provider-specific terraform variable flags -# Arguments: -# $1 - Cloud provider name (aws, azure, gcp) -# Returns: -# Terraform variable flags via stdout -_get_provider_vars() { - local provider="$1" - - case "${provider}" in - azure) - local subscription_id - subscription_id=$(az account show --query id -o tsv) - echo "-var=subscription_id=${subscription_id}" - ;; - gcp) - local project_id - project_id=$(gcloud config get-value project 2>/dev/null) - if [[ -z "${project_id}" ]]; then - _log ERROR "No GCP project set. Run 'gcloud config set project PROJECT_ID'" - exit 1 - fi - echo "-var=gcp_project=${project_id}" - ;; - *) - # AWS and others don't need extra vars - echo "" - ;; - esac -} - -# Deploy infrastructure using Terraform -# Arguments: -# $1 - Cloud provider name (aws, azure, gcp) -_terraform_apply() { - local provider="$1" - local provider_dir="${REPO_ROOT}/${provider}" - local provider_vars - - _log INFO "Deploying ${provider} infrastructure..." - cd "${provider_dir}" - - # Use -upgrade to ensure latest compatible provider versions (avoids stale lock file issues) - terraform init -input=false -upgrade - - provider_vars=$(_get_provider_vars "${provider}") - # shellcheck disable=SC2086 - terraform apply -auto-approve ${provider_vars} -var="use_local_setup=true" - - cd - > /dev/null -} - -# Destroy infrastructure using Terraform -# Arguments: -# $1 - Cloud provider name (aws, azure, gcp) -_terraform_destroy() { - local provider="$1" - local provider_dir="${REPO_ROOT}/${provider}" - local provider_vars - - _log INFO "Destroying ${provider} infrastructure..." - cd "${provider_dir}" - - provider_vars=$(_get_provider_vars "${provider}") - # shellcheck disable=SC2086 - terraform destroy -auto-approve ${provider_vars} - - cd - > /dev/null -} - -# Get the public IP address of the deployed VM -# Arguments: -# $1 - Cloud provider name (aws, azure, gcp) -# Returns: -# Public IP address via stdout, or returns 1 on failure -_get_public_ip() { - local provider="$1" - local provider_dir="${REPO_ROOT}/${provider}" - local ip - - cd "${provider_dir}" - ip=$(terraform output -raw public_ip_address 2>/dev/null \ - || terraform output -raw public_ip 2>/dev/null \ - || echo "") - cd - > /dev/null - - # Validate IP format - if [[ ! "${ip}" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - _log ERROR "Invalid IP address retrieved: '${ip}'" - return 1 - fi - - echo "${ip}" -} - -# ============================================================================= -# VM OPERATIONS -# ============================================================================= - -# Wait for SSH to become available on a VM -# Arguments: -# $1 - IP address of the VM -# Returns: -# 0 on success, 1 on timeout -_wait_for_ssh() { - local ip="$1" - local attempt=1 - - _log INFO "Waiting for SSH to become available at ${ip}..." - - while [[ ${attempt} -le ${MAX_SSH_ATTEMPTS} ]]; do - # shellcheck disable=SC2086 - if _sshpass_cmd ssh ${SSH_OPTS} "${SSH_USER}@${ip}" "echo 'SSH OK'" &>/dev/null; then - _log OK "SSH is available" - return 0 - fi - echo " Attempt ${attempt}/${MAX_SSH_ATTEMPTS} - waiting..." - sleep "${SSH_RETRY_INTERVAL}" - ((attempt++)) - done - - _log ERROR "SSH connection timed out" - return 1 -} - -# Wait for setup markers before running challenge tests -# Arguments: -# $1 - IP address of the VM -# Returns: -# 0 on success, 1 on timeout -_wait_for_setup() { - local ip="$1" - local attempt=1 - - _log INFO "Waiting for CTF setup to finish..." - - while [[ ${attempt} -le ${MAX_SSH_ATTEMPTS} ]]; do - # shellcheck disable=SC2086 - if _sshpass_cmd ssh ${SSH_OPTS} "${SSH_USER}@${ip}" \ - "test -f /var/lib/linux-ctfs/setup.done || test -f /var/lib/cloud/instance/ctf-setup.done || test -f /var/log/setup_complete" &>/dev/null; then - _log OK "CTF setup is complete" - return 0 - fi - - # shellcheck disable=SC2086 - if _sshpass_cmd ssh ${SSH_OPTS} "${SSH_USER}@${ip}" \ - "test -f /var/lib/linux-ctfs/setup.failed" &>/dev/null; then - _log ERROR "CTF setup reported failure. Check /var/log/ctf_setup.log on the VM." - return 1 - fi - - echo " Attempt ${attempt}/${MAX_SSH_ATTEMPTS} - waiting..." - sleep "${SSH_RETRY_INTERVAL}" - ((attempt++)) - done - - _log ERROR "CTF setup timed out" - return 1 -} - -# Reboot/restart a VM and return the new IP address -# Arguments: -# $1 - Cloud provider name (aws, azure, gcp) -# $2 - Current IP address of the VM -# Returns: -# New IP address via stdout (may change for AWS) -_reboot_vm() { - local provider="$1" - local ip="$2" - - _log INFO "Rebooting VM (${provider})..." - - case "${provider}" in - aws) - local instance_id - instance_id=$(cd "${REPO_ROOT}/${provider}" \ - && terraform output -raw instance_id 2>/dev/null \ - || aws ec2 describe-instances \ - --filters "Name=ip-address,Values=${ip}" \ - --query 'Reservations[0].Instances[0].InstanceId' \ - --output text) - - # Validate instance ID - if [[ -z "${instance_id}" ]] || [[ "${instance_id}" == "None" ]]; then - _log ERROR "Failed to retrieve AWS instance ID for IP ${ip}" - return 1 - fi - - echo " Stopping instance ${instance_id}..." - aws ec2 stop-instances --instance-ids "${instance_id}" > /dev/null - aws ec2 wait instance-stopped --instance-ids "${instance_id}" - echo " Starting instance ${instance_id}..." - aws ec2 start-instances --instance-ids "${instance_id}" > /dev/null - aws ec2 wait instance-running --instance-ids "${instance_id}" - # IP may change, get new one - sleep 10 - ip=$(_get_public_ip "${provider}") - ;; - azure) - echo " Restarting Azure VM..." - az vm restart --resource-group ctf-resources --name ctf-vm - # az vm restart waits by default, but add explicit wait for running state - az vm wait \ - --resource-group ctf-resources \ - --name ctf-vm \ - --created \ - --timeout 120 2>/dev/null || true - ;; - gcp) - echo " Restarting GCP VM..." - local zone - zone=$(cd "${REPO_ROOT}/${provider}" \ - && terraform output -raw zone 2>/dev/null \ - || echo "us-central1-a") - gcloud compute instances reset ctf-instance --zone="${zone}" --quiet - # Wait for VM to be running - local attempts=0 - while [[ ${attempts} -lt 30 ]]; do - local status - status=$(gcloud compute instances describe ctf-instance \ - --zone="${zone}" \ - --format='value(status)' 2>/dev/null || echo "") - if [[ "${status}" == "RUNNING" ]]; then - break - fi - sleep 2 - ((attempts++)) - done - ;; - esac - - # Return new IP (may have changed for AWS) - echo "${ip}" -} - -# ============================================================================= -# TEST EXECUTION -# ============================================================================= - -# Copy test script to VM and execute it -# Arguments: -# $1 - Cloud provider name -# $2 - IP address of the VM -# Returns: -# Exit code from the test script (0=pass, 1=fail, 100=reboot requested) -_run_tests() { - local provider="$1" - local ip="$2" - local test_flags="" - - if [[ "${WITH_REBOOT}" == true ]]; then - test_flags="${test_flags} --with-reboot" - fi - - _log INFO "Copying test script to VM..." - # shellcheck disable=SC2086 - _sshpass_cmd scp ${SSH_OPTS} "${TEST_SCRIPT}" "${SSH_USER}@${ip}:/tmp/test_ctf_challenges.sh" - - _log INFO "Running tests on ${provider} VM (${ip})..." - echo "" - - local exit_code=0 - # shellcheck disable=SC2086 - _sshpass_cmd ssh ${SSH_OPTS} "${SSH_USER}@${ip}" \ - "chmod +x /tmp/test_ctf_challenges.sh && /tmp/test_ctf_challenges.sh ${test_flags}" \ - || exit_code=$? - - return "${exit_code}" -} - -# Run tests after VM reboot to verify services persist -# Arguments: -# $1 - Cloud provider name -# $2 - IP address of the VM -# Returns: -# Exit code from the test script -_run_post_reboot_tests() { - local provider="$1" - local ip="$2" - - _log INFO "Running post-reboot verification on ${provider}..." - - local exit_code=0 - # shellcheck disable=SC2086 - _sshpass_cmd ssh ${SSH_OPTS} "${SSH_USER}@${ip}" "/tmp/test_ctf_challenges.sh" \ - || exit_code=$? - - return "${exit_code}" -} - -# ============================================================================= -# MAIN TEST FLOW -# ============================================================================= - -# Run full test cycle for a single cloud provider -# Arguments: -# $1 - Cloud provider name (aws, azure, gcp) -# Returns: -# 0 on success, 1 on failure -_test_provider() { - local provider="$1" - local result=0 - - # Enable cleanup on interrupt for this provider - CURRENT_PROVIDER="${provider}" - CLEANUP_ON_EXIT=true - - echo "" - echo -e "${YELLOW}════════════════════════════════════════════════════════════════${NC}" - echo -e "${YELLOW} TESTING: ${provider}${NC}" - echo -e "${YELLOW}════════════════════════════════════════════════════════════════${NC}" - echo "" - - # Check prerequisites - _check_prerequisites "${provider}" - - # Deploy - if ! _terraform_apply "${provider}"; then - _log ERROR "Terraform apply failed for ${provider}" - CLEANUP_ON_EXIT=false - _terraform_destroy "${provider}" 2>/dev/null || true - CURRENT_PROVIDER="" - return 1 - fi - - # Get IP - local ip - if ! ip=$(_get_public_ip "${provider}"); then - _log ERROR "Failed to get valid IP address for ${provider}" - CLEANUP_ON_EXIT=false - _terraform_destroy "${provider}" 2>/dev/null || true - CURRENT_PROVIDER="" - return 1 - fi - _log OK "VM deployed at: ${ip}" - - # Wait for SSH - if ! _wait_for_ssh "${ip}"; then - _log ERROR "SSH connection failed for ${provider}" - CLEANUP_ON_EXIT=false - _terraform_destroy "${provider}" - CURRENT_PROVIDER="" - return 1 - fi - - # Wait for setup markers - if ! _wait_for_setup "${ip}"; then - _log ERROR "Setup did not complete for ${provider}" - CLEANUP_ON_EXIT=false - _terraform_destroy "${provider}" - CURRENT_PROVIDER="" - return 1 - fi - - # Run tests - local test_exit_code=0 - _run_tests "${provider}" "${ip}" || test_exit_code=$? - - # Handle reboot test - if [[ ${test_exit_code} -eq 100 ]] && [[ "${WITH_REBOOT}" == true ]]; then - echo "" - _log WARN "Reboot requested - performing VM reboot..." - - local new_ip - new_ip=$(_reboot_vm "${provider}" "${ip}") - - # Wait for SSH after reboot - _wait_for_ssh "${new_ip}" - - # Run post-reboot tests - _run_post_reboot_tests "${provider}" "${new_ip}" || test_exit_code=$? - elif [[ ${test_exit_code} -ne 0 ]]; then - result=1 - fi - - # Cleanup - echo "" - CLEANUP_ON_EXIT=false - _terraform_destroy "${provider}" - CURRENT_PROVIDER="" - - return "${result}" -} - -# ============================================================================= -# MAIN ENTRY POINT -# ============================================================================= - -# Main function - orchestrates testing across all specified providers -_main() { - local failed_providers=() - local passed_providers=() - - _log WARN "CTF Challenge Test Suite" - echo "Providers to test: ${PROVIDERS_TO_TEST[*]}" - echo "Reboot test: ${WITH_REBOOT}" - echo "" - - for provider in "${PROVIDERS_TO_TEST[@]}"; do - if _test_provider "${provider}"; then - passed_providers+=("${provider}") - else - failed_providers+=("${provider}") - fi - done - - # Final summary (short pass/fail) - echo "" - if [[ ${#failed_providers[@]} -gt 0 ]]; then - _log ERROR "RESULT: FAIL (${failed_providers[*]})" - exit 1 - fi - - _log OK "RESULT: PASS (${passed_providers[*]})" - exit 0 -} - -_main +#!/usr/bin/env bash +# shellcheck shell=bash +# +# CTF Deploy and Test Orchestration Script +# Deploys infrastructure to cloud providers and runs tests +# +# Usage: +# ./deploy_and_test.sh [--with-reboot] +# DEBUG=true ./deploy_and_test.sh azure # Enable debug tracing +# +# Arguments: +# aws|azure|gcp|all Cloud provider(s) to test +# --with-reboot After initial tests pass, stop/start the VM and +# re-run verification to ensure services survive +# reboot and progress persists +# +# Prerequisites: +# - terraform (>= 1.0) +# - jq (for AWS terraform config) +# - sshpass (macOS: brew install hudochenkov/sshpass/sshpass) +# - aws CLI (for AWS, must be logged in) +# - az CLI (for Azure, must be logged in) +# - gcloud CLI (for GCP, must be authenticated) +# +# Examples: +# ./deploy_and_test.sh aws # Test AWS only +# ./deploy_and_test.sh azure --with-reboot # Test Azure with reboot +# ./deploy_and_test.sh all # Test all providers +# ./deploy_and_test.sh all --with-reboot # Full test suite +# + +set -o errexit +set -o pipefail +set -o nounset + +# Enable debug tracing if DEBUG=true +[[ "${DEBUG:-}" == 'true' ]] && set -o xtrace + +# ============================================================================= +# CONSTANTS +# ============================================================================= + +# Script paths (declare and assign separately to avoid masking return values) +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly SCRIPT_DIR +REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)" +readonly REPO_ROOT +readonly TEST_SCRIPT="${SCRIPT_DIR}/test_ctf_challenges.sh" + +# SSH connection settings +readonly MAX_SSH_ATTEMPTS=30 +readonly SSH_RETRY_INTERVAL=10 +readonly SSH_USER="ctf_user" +readonly SSH_PASS="CTFpassword123!" +readonly SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR" + +# Terminal colors for output formatting +readonly RED='\033[0;31m' +readonly GREEN='\033[0;32m' +readonly YELLOW='\033[1;33m' +readonly BLUE='\033[0;34m' +readonly NC='\033[0m' # No Color + +# ============================================================================= +# GLOBAL STATE +# ============================================================================= + +# Cleanup tracking (mutable - set during test execution) +CURRENT_PROVIDER="" +CLEANUP_ON_EXIT=false + +# ============================================================================= +# UTILITY FUNCTIONS +# ============================================================================= + +# Log a message with timestamp and color based on level +# Arguments: +# $1 - Log level (INFO, OK, WARN, ERROR) +# $@ - Message to log +_log() { + local level="$1" + shift + local message="$*" + local timestamp + timestamp=$(date '+%H:%M:%S') + + case "${level}" in + INFO) echo -e "[${timestamp}] ${BLUE}${message}${NC}" ;; + OK) echo -e "[${timestamp}] ${GREEN}${message}${NC}" ;; + WARN) echo -e "[${timestamp}] ${YELLOW}${message}${NC}" ;; + ERROR) echo -e "[${timestamp}] ${RED}${message}${NC}" ;; + *) echo -e "[${timestamp}] ${level} ${message}" ;; + esac +} + +# Signal handler for cleanup on interrupt (SIGINT/SIGTERM) +# Destroys any in-progress infrastructure before exiting +_cleanup_handler() { + local exit_code=$? + + if [[ "${CLEANUP_ON_EXIT}" == true ]] && [[ -n "${CURRENT_PROVIDER}" ]]; then + echo "" + _log WARN "Caught interrupt - cleaning up ${CURRENT_PROVIDER} infrastructure..." + _terraform_destroy "${CURRENT_PROVIDER}" || true + fi + exit "${exit_code}" +} + +trap _cleanup_handler SIGINT SIGTERM + +# Execute sshpass command with password passed via file descriptor +# This hides the password from the process list +# Arguments: +# $@ - Command and arguments to pass to sshpass +_sshpass_cmd() { + sshpass -f <(printf '%s' "${SSH_PASS}") "$@" +} + +# ============================================================================= +# ARGUMENT PARSING +# ============================================================================= + +WITH_REBOOT=false +PROVIDERS_TO_TEST=() + +for arg in "$@"; do + case "${arg}" in + aws|azure|gcp) + PROVIDERS_TO_TEST+=("${arg}") + ;; + all) + PROVIDERS_TO_TEST=("aws" "azure" "gcp") + ;; + --with-reboot) + WITH_REBOOT=true + ;; + -h|--help) + head -32 "$0" | tail -30 + exit 0 + ;; + *) + echo "Unknown argument: ${arg}" + echo "Usage: $0 [--with-reboot]" + exit 1 + ;; + esac +done + +if [[ ${#PROVIDERS_TO_TEST[@]} -eq 0 ]]; then + echo "Usage: $0 [--with-reboot]" + exit 1 +fi + +# ============================================================================= +# PREREQUISITE CHECKS +# ============================================================================= + +# Verify all required tools are installed and authenticated for a provider +# Arguments: +# $1 - Cloud provider name (aws, azure, gcp) +_check_prerequisites() { + local provider="$1" + local missing=() + + echo -e "${BLUE}Checking prerequisites for ${provider}...${NC}" + + # Check terraform + if ! command -v terraform &>/dev/null; then + missing+=("terraform") + fi + + # Check jq (required for AWS terraform config) + if ! command -v jq &>/dev/null; then + missing+=("jq") + fi + + # Check sshpass + if ! command -v sshpass &>/dev/null; then + echo -e "${RED}ERROR: sshpass is required but not installed.${NC}" + echo "" + echo "Install on macOS:" + echo " brew install hudochenkov/sshpass/sshpass" + echo "" + echo "Install on Ubuntu/Debian:" + echo " sudo apt-get install sshpass" + echo "" + exit 1 + fi + + # Check provider-specific CLI + case "${provider}" in + aws) + if ! command -v aws &>/dev/null; then + missing+=("aws CLI") + elif ! aws sts get-caller-identity &>/dev/null; then + echo -e "${RED}ERROR: AWS CLI not authenticated. Run 'aws configure' first.${NC}" + exit 1 + fi + ;; + azure) + if ! command -v az &>/dev/null; then + missing+=("az CLI") + elif ! az account show &>/dev/null; then + echo -e "${RED}ERROR: Azure CLI not authenticated. Run 'az login' first.${NC}" + exit 1 + fi + ;; + gcp) + if ! command -v gcloud &>/dev/null; then + missing+=("gcloud CLI") + elif ! gcloud auth list --filter=status:ACTIVE --format="value(account)" 2>/dev/null \ + | head -1 \ + | grep -q '@'; then + echo -e "${RED}ERROR: GCP CLI not authenticated. Run 'gcloud auth login' first.${NC}" + exit 1 + fi + ;; + esac + + if [[ ${#missing[@]} -gt 0 ]]; then + echo -e "${RED}ERROR: Missing required tools: ${missing[*]}${NC}" + exit 1 + fi + + echo -e "${GREEN}Prerequisites OK${NC}" +} + +# ============================================================================= +# TERRAFORM OPERATIONS +# ============================================================================= + +# Get provider-specific terraform variable flags +# Arguments: +# $1 - Cloud provider name (aws, azure, gcp) +# Returns: +# Terraform variable flags via stdout +_get_provider_vars() { + local provider="$1" + + case "${provider}" in + azure) + local subscription_id + subscription_id=$(az account show --query id -o tsv) + echo "-var=subscription_id=${subscription_id}" + ;; + gcp) + local project_id + project_id=$(gcloud config get-value project 2>/dev/null) + if [[ -z "${project_id}" ]]; then + _log ERROR "No GCP project set. Run 'gcloud config set project PROJECT_ID'" + exit 1 + fi + echo "-var=gcp_project=${project_id}" + ;; + *) + # AWS and others don't need extra vars + echo "" + ;; + esac +} + +# Deploy infrastructure using Terraform +# Arguments: +# $1 - Cloud provider name (aws, azure, gcp) +_terraform_apply() { + local provider="$1" + local provider_dir="${REPO_ROOT}/${provider}" + local provider_vars + + _log INFO "Deploying ${provider} infrastructure..." + cd "${provider_dir}" + + # Use -upgrade to ensure latest compatible provider versions (avoids stale lock file issues) + terraform init -input=false -upgrade + + provider_vars=$(_get_provider_vars "${provider}") + # shellcheck disable=SC2086 + terraform apply -auto-approve ${provider_vars} -var="use_local_setup=true" + + cd - > /dev/null +} + +# Destroy infrastructure using Terraform +# Arguments: +# $1 - Cloud provider name (aws, azure, gcp) +_terraform_destroy() { + local provider="$1" + local provider_dir="${REPO_ROOT}/${provider}" + local provider_vars + + _log INFO "Destroying ${provider} infrastructure..." + cd "${provider_dir}" + + provider_vars=$(_get_provider_vars "${provider}") + # shellcheck disable=SC2086 + terraform destroy -auto-approve ${provider_vars} + + cd - > /dev/null +} + +# Get the public IP address of the deployed VM +# Arguments: +# $1 - Cloud provider name (aws, azure, gcp) +# Returns: +# Public IP address via stdout, or returns 1 on failure +_get_public_ip() { + local provider="$1" + local provider_dir="${REPO_ROOT}/${provider}" + local ip + + cd "${provider_dir}" + ip=$(terraform output -raw public_ip_address 2>/dev/null \ + || terraform output -raw public_ip 2>/dev/null \ + || echo "") + cd - > /dev/null + + # Validate IP format + if [[ ! "${ip}" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + _log ERROR "Invalid IP address retrieved: '${ip}'" + return 1 + fi + + echo "${ip}" +} + +# ============================================================================= +# VM OPERATIONS +# ============================================================================= + +# Wait for SSH to become available on a VM +# Arguments: +# $1 - IP address of the VM +# Returns: +# 0 on success, 1 on timeout +_wait_for_ssh() { + local ip="$1" + local attempt=1 + + _log INFO "Waiting for SSH to become available at ${ip}..." + + while [[ ${attempt} -le ${MAX_SSH_ATTEMPTS} ]]; do + # shellcheck disable=SC2086 + if _sshpass_cmd ssh ${SSH_OPTS} "${SSH_USER}@${ip}" "echo 'SSH OK'" &>/dev/null; then + _log OK "SSH is available" + return 0 + fi + echo " Attempt ${attempt}/${MAX_SSH_ATTEMPTS} - waiting..." + sleep "${SSH_RETRY_INTERVAL}" + ((attempt++)) + done + + _log ERROR "SSH connection timed out" + return 1 +} + +# Wait for setup markers before running challenge tests +# Arguments: +# $1 - IP address of the VM +# Returns: +# 0 on success, 1 on timeout +_wait_for_setup() { + local ip="$1" + local attempt=1 + + _log INFO "Waiting for CTF setup to finish..." + + while [[ ${attempt} -le ${MAX_SSH_ATTEMPTS} ]]; do + # shellcheck disable=SC2086 + if _sshpass_cmd ssh ${SSH_OPTS} "${SSH_USER}@${ip}" \ + "test -f /var/lib/linux-ctfs/setup.done || test -f /var/lib/cloud/instance/ctf-setup.done || test -f /var/log/setup_complete" &>/dev/null; then + _log OK "CTF setup is complete" + return 0 + fi + + # shellcheck disable=SC2086 + if _sshpass_cmd ssh ${SSH_OPTS} "${SSH_USER}@${ip}" \ + "test -f /var/lib/linux-ctfs/setup.failed" &>/dev/null; then + _log ERROR "CTF setup reported failure. Check /var/log/ctf_setup.log on the VM." + return 1 + fi + + echo " Attempt ${attempt}/${MAX_SSH_ATTEMPTS} - waiting..." + sleep "${SSH_RETRY_INTERVAL}" + ((attempt++)) + done + + _log ERROR "CTF setup timed out" + return 1 +} + +# Reboot/restart a VM and return the new IP address +# Arguments: +# $1 - Cloud provider name (aws, azure, gcp) +# $2 - Current IP address of the VM +# Returns: +# New IP address via stdout (may change for AWS) +_reboot_vm() { + local provider="$1" + local ip="$2" + + _log INFO "Rebooting VM (${provider})..." + + case "${provider}" in + aws) + local instance_id + instance_id=$(cd "${REPO_ROOT}/${provider}" \ + && terraform output -raw instance_id 2>/dev/null \ + || aws ec2 describe-instances \ + --filters "Name=ip-address,Values=${ip}" \ + --query 'Reservations[0].Instances[0].InstanceId' \ + --output text) + + # Validate instance ID + if [[ -z "${instance_id}" ]] || [[ "${instance_id}" == "None" ]]; then + _log ERROR "Failed to retrieve AWS instance ID for IP ${ip}" + return 1 + fi + + echo " Stopping instance ${instance_id}..." + aws ec2 stop-instances --instance-ids "${instance_id}" > /dev/null + aws ec2 wait instance-stopped --instance-ids "${instance_id}" + echo " Starting instance ${instance_id}..." + aws ec2 start-instances --instance-ids "${instance_id}" > /dev/null + aws ec2 wait instance-running --instance-ids "${instance_id}" + # IP may change, get new one + sleep 10 + ip=$(_get_public_ip "${provider}") + ;; + azure) + echo " Restarting Azure VM..." + az vm restart --resource-group ctf-resources --name ctf-vm + # az vm restart waits by default, but add explicit wait for running state + az vm wait \ + --resource-group ctf-resources \ + --name ctf-vm \ + --created \ + --timeout 120 2>/dev/null || true + ;; + gcp) + echo " Restarting GCP VM..." + local zone + zone=$(cd "${REPO_ROOT}/${provider}" \ + && terraform output -raw zone 2>/dev/null \ + || echo "us-central1-a") + gcloud compute instances reset ctf-instance --zone="${zone}" --quiet + # Wait for VM to be running + local attempts=0 + while [[ ${attempts} -lt 30 ]]; do + local status + status=$(gcloud compute instances describe ctf-instance \ + --zone="${zone}" \ + --format='value(status)' 2>/dev/null || echo "") + if [[ "${status}" == "RUNNING" ]]; then + break + fi + sleep 2 + ((attempts++)) + done + ;; + esac + + # Return new IP (may have changed for AWS) + echo "${ip}" +} + +# ============================================================================= +# TEST EXECUTION +# ============================================================================= + +# Copy test script to VM and execute it +# Arguments: +# $1 - Cloud provider name +# $2 - IP address of the VM +# Returns: +# Exit code from the test script (0=pass, 1=fail, 100=reboot requested) +_run_tests() { + local provider="$1" + local ip="$2" + local test_flags="" + + if [[ "${WITH_REBOOT}" == true ]]; then + test_flags="${test_flags} --with-reboot" + fi + + _log INFO "Copying test script to VM..." + # shellcheck disable=SC2086 + _sshpass_cmd scp ${SSH_OPTS} "${TEST_SCRIPT}" "${SSH_USER}@${ip}:/tmp/test_ctf_challenges.sh" + + _log INFO "Running tests on ${provider} VM (${ip})..." + echo "" + + local exit_code=0 + # shellcheck disable=SC2086 + _sshpass_cmd ssh ${SSH_OPTS} "${SSH_USER}@${ip}" \ + "chmod +x /tmp/test_ctf_challenges.sh && /tmp/test_ctf_challenges.sh ${test_flags}" \ + || exit_code=$? + + return "${exit_code}" +} + +# Run tests after VM reboot to verify services persist +# Arguments: +# $1 - Cloud provider name +# $2 - IP address of the VM +# Returns: +# Exit code from the test script +_run_post_reboot_tests() { + local provider="$1" + local ip="$2" + + _log INFO "Running post-reboot verification on ${provider}..." + + local exit_code=0 + # shellcheck disable=SC2086 + _sshpass_cmd ssh ${SSH_OPTS} "${SSH_USER}@${ip}" "/tmp/test_ctf_challenges.sh" \ + || exit_code=$? + + return "${exit_code}" +} + +# ============================================================================= +# MAIN TEST FLOW +# ============================================================================= + +# Run full test cycle for a single cloud provider +# Arguments: +# $1 - Cloud provider name (aws, azure, gcp) +# Returns: +# 0 on success, 1 on failure +_test_provider() { + local provider="$1" + local result=0 + + # Enable cleanup on interrupt for this provider + CURRENT_PROVIDER="${provider}" + CLEANUP_ON_EXIT=true + + echo "" + echo -e "${YELLOW}════════════════════════════════════════════════════════════════${NC}" + echo -e "${YELLOW} TESTING: ${provider}${NC}" + echo -e "${YELLOW}════════════════════════════════════════════════════════════════${NC}" + echo "" + + # Check prerequisites + _check_prerequisites "${provider}" + + # Deploy + if ! _terraform_apply "${provider}"; then + _log ERROR "Terraform apply failed for ${provider}" + CLEANUP_ON_EXIT=false + _terraform_destroy "${provider}" 2>/dev/null || true + CURRENT_PROVIDER="" + return 1 + fi + + # Get IP + local ip + if ! ip=$(_get_public_ip "${provider}"); then + _log ERROR "Failed to get valid IP address for ${provider}" + CLEANUP_ON_EXIT=false + _terraform_destroy "${provider}" 2>/dev/null || true + CURRENT_PROVIDER="" + return 1 + fi + _log OK "VM deployed at: ${ip}" + + # Wait for SSH + if ! _wait_for_ssh "${ip}"; then + _log ERROR "SSH connection failed for ${provider}" + CLEANUP_ON_EXIT=false + _terraform_destroy "${provider}" + CURRENT_PROVIDER="" + return 1 + fi + + # Wait for setup markers + if ! _wait_for_setup "${ip}"; then + _log ERROR "Setup did not complete for ${provider}" + CLEANUP_ON_EXIT=false + _terraform_destroy "${provider}" + CURRENT_PROVIDER="" + return 1 + fi + + # Run tests + local test_exit_code=0 + _run_tests "${provider}" "${ip}" || test_exit_code=$? + + # Handle reboot test + if [[ ${test_exit_code} -eq 100 ]] && [[ "${WITH_REBOOT}" == true ]]; then + echo "" + _log WARN "Reboot requested - performing VM reboot..." + + local new_ip + new_ip=$(_reboot_vm "${provider}" "${ip}") + + # Wait for SSH after reboot + _wait_for_ssh "${new_ip}" + + # Run post-reboot tests + _run_post_reboot_tests "${provider}" "${new_ip}" || test_exit_code=$? + elif [[ ${test_exit_code} -ne 0 ]]; then + result=1 + fi + + # Cleanup + echo "" + CLEANUP_ON_EXIT=false + _terraform_destroy "${provider}" + CURRENT_PROVIDER="" + + return "${result}" +} + +# ============================================================================= +# MAIN ENTRY POINT +# ============================================================================= + +# Main function - orchestrates testing across all specified providers +_main() { + local failed_providers=() + local passed_providers=() + + _log WARN "CTF Challenge Test Suite" + echo "Providers to test: ${PROVIDERS_TO_TEST[*]}" + echo "Reboot test: ${WITH_REBOOT}" + echo "" + + for provider in "${PROVIDERS_TO_TEST[@]}"; do + if _test_provider "${provider}"; then + passed_providers+=("${provider}") + else + failed_providers+=("${provider}") + fi + done + + # Final summary (short pass/fail) + echo "" + if [[ ${#failed_providers[@]} -gt 0 ]]; then + _log ERROR "RESULT: FAIL (${failed_providers[*]})" + exit 1 + fi + + _log OK "RESULT: PASS (${passed_providers[*]})" + exit 0 +} + +_main diff --git a/.github/skills/ctf-testing/test_ctf_challenges.sh b/.github/skills/ctf-testing/test_ctf_challenges.sh index 25a29dc..4ccb182 100644 --- a/.github/skills/ctf-testing/test_ctf_challenges.sh +++ b/.github/skills/ctf-testing/test_ctf_challenges.sh @@ -1,659 +1,659 @@ -#!/usr/bin/env bash -# shellcheck shell=bash -# -# CTF Challenge Test Script -# Runs on the VM to validate all challenges are solvable by students -# -# This script simulates a real user journey - discovering and solving each -# challenge using only the hints provided. If these tests pass, students -# can complete the CTF. -# -# Usage: -# ./test_ctf_challenges.sh [--with-reboot] -# DEBUG=true ./test_ctf_challenges.sh # Enable debug tracing -# -# Flags: -# --with-reboot After tests pass, signal reboot to verify services persist -# -# Exit codes: -# 0 - All tests passed -# 1 - One or more tests failed -# 100 - Reboot requested (only with --with-reboot flag) -# - -set -o errexit -set -o pipefail -set -o nounset - -# Enable debug tracing if DEBUG=true -[[ "${DEBUG:-}" == 'true' ]] && set -o xtrace - -# Ensure verify command is available -# It's installed in /usr/local/bin by ctf_setup.sh -if ! command -v verify &>/dev/null; then - export PATH="/usr/local/bin:$PATH" -fi - -# ============================================================================= -# CONSTANTS -# ============================================================================= - -# Terminal colors for output formatting -readonly RED='\033[0;31m' -readonly GREEN='\033[0;32m' -readonly YELLOW='\033[1;33m' -readonly NC='\033[0m' # No Color - -# File paths for reboot test coordination -readonly REBOOT_MARKER="/tmp/.ctf_reboot_test_marker" -readonly PROGRESS_SNAPSHOT="/tmp/.ctf_progress_snapshot" - -# ============================================================================= -# GLOBAL STATE -# ============================================================================= - -# Test result counters (mutable) -PASSED=0 -FAILED=0 - -# Parse arguments -WITH_REBOOT=false -for arg in "$@"; do - case $arg in - --with-reboot) - WITH_REBOOT=true - shift - ;; - esac -done - -# ============================================================================= -# HELPER FUNCTIONS -# ============================================================================= - -# Log a passing test result and increment counter -# Arguments: -# $1 - Message describing what passed -_pass() { - local message="${1}" - echo -e "${GREEN}✓ PASS${NC}: ${message}" - ((PASSED++)) || true -} - -# Log a failing test result and increment counter -# Arguments: -# $1 - Message describing what failed -_fail() { - local message="${1}" - echo -e "${RED}✗ FAIL${NC}: ${message}" - ((FAILED++)) || true -} - -# Print a section header for visual separation in output -# Arguments: -# $1 - Section title to display -_section() { - local title="${1}" - echo "" - echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" - echo -e "${YELLOW}${title}${NC}" - echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" -} - -# Verify a flag with the verify command and record result -# Arguments: -# $1 - Challenge number -# $2 - Flag value to verify -# $3 - Success message (optional, defaults to "Solved challenge N") -# $4 - Failure message (optional, defaults to "Found flag but verify rejected it") -# Returns: -# 0 if flag was verified successfully, 1 otherwise -_verify_flag() { - local challenge_num="${1}" - local flag_value="${2}" - local success_msg="${3:-Solved challenge ${challenge_num}}" - local fail_msg="${4:-Challenge ${challenge_num}: Found flag but verify rejected it}" - local verify_out - - verify_out=$(verify "${challenge_num}" "${flag_value}" 2>&1) || true - if echo "${verify_out}" | grep -qE "(Correct|verified)"; then - _pass "${success_msg}" - FLAGS[${challenge_num}]="${flag_value}" - return 0 - else - _fail "${fail_msg}" - FLAGS[${challenge_num}]="" - return 1 - fi -} - -# ============================================================================ -# POST-REBOOT VERIFICATION -# ============================================================================ -if [[ -f "${REBOOT_MARKER}" ]]; then - _section "POST-REBOOT VERIFICATION" - - echo "Verifying services survived reboot..." - - for service in ctf-secret-service ctf-monitor-directory ctf-ping-message ctf-secret-process nginx; do - if systemctl is-active "${service}" &>/dev/null; then - _pass "${service} is running after reboot" - else - _fail "${service} failed to start after reboot - SETUP BUG" - fi - done - - if [ -f "$PROGRESS_SNAPSHOT" ]; then - EXPECTED=$(cat "$PROGRESS_SNAPSHOT") - ACTUAL=$(sort -u /var/ctf/completed_challenges 2>/dev/null | wc -l) - if [ "$ACTUAL" -ge "$EXPECTED" ]; then - _pass "Progress persisted after reboot ($ACTUAL checks)" - else - _fail "Progress lost after reboot (expected ${EXPECTED}, got ${ACTUAL})" - fi - fi - - rm -f "${REBOOT_MARKER}" "${PROGRESS_SNAPSHOT}" - - echo "" - echo "Passed: ${PASSED} | Failed: ${FAILED}" - [[ ${FAILED} -eq 0 ]] && exit 0 || exit 1 -fi - -# ============================================================================ -# VERIFY COMMAND SANITY CHECK -# ============================================================================ -_section "VERIFY COMMAND SANITY CHECK" - -# Quick check that the verify command works at all -if ! command -v verify &>/dev/null; then - _fail "verify command not found in PATH" - echo "PATH: ${PATH}" - echo "Looking for verify: $(which verify 2>&1 || echo 'not found')" - echo "Checking /usr/local/bin: $(ls -la /usr/local/bin/verify 2>&1 || echo 'not found')" - exit 1 -fi - -# Timer should not start before first numeric verify command -rm -f /var/ctf/ctf_start_time /var/ctf/ctf_end_time -TIMER_PRESTART_OUT=$(verify time 2>&1) || true -if echo "${TIMER_PRESTART_OUT}" | grep -q "Timer not started"; then - _pass "verify time shows pre-start message" -else - _fail "verify time pre-start behavior incorrect" -fi - -VERIFY_OUTPUT=$(verify 0 "CTF{example}" 2>&1) || true -if echo "${VERIFY_OUTPUT}" | grep -q "✓"; then - _pass "verify command accepts example flag" -else - _fail "verify command broken - SETUP BUG" - echo "Cannot continue without working verify command" - exit 1 -fi - -if echo "${VERIFY_OUTPUT}" | grep -q "1/19"; then - _pass "verify progress counts the example check" -else - _fail "verify progress did not show 1/19 after the example check" -fi - -if [[ -f /var/ctf/ctf_start_time ]]; then - _pass "Timer starts after first numeric verify command" -else - _fail "Timer did not start after first numeric verify command" -fi - -# ============================================================================ -# CHALLENGE DISCOVERY AND SOLVING -# ============================================================================ -_section "SOLVING ALL CHALLENGES" - -echo "Simulating real student journey using hints to discover and solve each challenge..." -echo "" - -# Store discovered flags -declare -A FLAGS - -# Challenge 1: Hidden File Discovery -# Hint: "Hidden files in Linux start with a dot. Try 'ls -la'" -echo "Challenge 1: Hidden File Discovery" -HIDDEN_FILE=$(ls -la /home/ctf_user/ctf_challenges/ 2>/dev/null \ - | awk '/^-.*\./ {print $NF}' \ - | grep '^\.' \ - | head -1) || true -if [[ -n "${HIDDEN_FILE}" ]]; then - FLAG_1=$(cat "/home/ctf_user/ctf_challenges/${HIDDEN_FILE}" 2>/dev/null \ - | grep -ao 'CTF{[^}]*}' \ - | head -1) || true - if [[ -n "${FLAG_1}" ]]; then - _verify_flag 1 "${FLAG_1}" - else - _fail "Challenge 1: Found file but no CTF flag in it" - FLAGS[1]="" - fi -else - _fail "Challenge 1: No hidden files found with ls -la" - FLAGS[1]="" -fi - -# Challenge 2: Basic File Search -# Hint: "Use find to search for files. Try: find ~ -name '*.txt'" -echo "Challenge 2: Basic File Search" -TXT_FILE=$(find /home/ctf_user/documents -name '*.txt' -type f 2>/dev/null | head -1) || true -if [[ -n "${TXT_FILE}" ]]; then - FLAG_2=$(cat "${TXT_FILE}" 2>/dev/null | grep -ao 'CTF{[^}]*}' | head -1) || true - if [[ -n "${FLAG_2}" ]]; then - _verify_flag 2 "${FLAG_2}" - else - _fail "Challenge 2: Found file but no CTF flag in it" - FLAGS[2]="" - fi -else - _fail "Challenge 2: No .txt files found in documents" - FLAGS[2]="" -fi - -# Challenge 3: Log Analysis -# Hint: "Large log files can hide secrets. Check /var/log and use 'tail'" -echo "Challenge 3: Log Analysis" -LARGE_LOG=$(find /var/log -type f -size +100M 2>/dev/null | head -1) || true -if [[ -n "${LARGE_LOG}" ]]; then - FLAG_3=$(tail -1 "${LARGE_LOG}" 2>/dev/null | grep -ao 'CTF{[^}]*}' | head -1) || true - if [[ -n "${FLAG_3}" ]]; then - _verify_flag 3 "${FLAG_3}" - else - _fail "Challenge 3: Found log but no CTF flag in it" - FLAGS[3]="" - fi -else - _fail "Challenge 3: No large log files found" - FLAGS[3]="" -fi - -# Challenge 4: User Investigation -# Hint: "Investigate other users. Check /etc/passwd or use 'getent passwd'" -echo "Challenge 4: User Investigation" -FLAG_4="" -for user in $(getent passwd | awk -F: '$3 >= 1000 && $1 != "ctf_user" && $1 != "nobody" {print $1}'); do - if [[ -r "/home/${user}/.profile" ]]; then - FLAG_4=$(grep -ao 'CTF{[^}]*}' "/home/${user}/.profile" 2>/dev/null | head -1) || true - [[ -n "${FLAG_4}" ]] && break - fi -done -if [[ -n "${FLAG_4}" ]]; then - _verify_flag 4 "${FLAG_4}" -else - _fail "Challenge 4: Could not find flag in user profiles" - FLAGS[4]="" -fi - -# Challenge 5: Permission Analysis -# Hint: "Look for files with unusual permissions. Try: find / -perm 777" -echo "Challenge 5: Permission Analysis" -FLAG_5="" -for path in /opt /etc /var; do - PERM_FILE=$(find "${path}" -type f -perm 777 2>/dev/null | head -1) || true - if [[ -n "${PERM_FILE}" ]]; then - FLAG_5=$(cat "${PERM_FILE}" 2>/dev/null | grep -ao 'CTF{[^}]*}' | head -1) || true - [[ -n "${FLAG_5}" ]] && break - fi -done -if [[ -n "${FLAG_5}" ]]; then - _verify_flag 5 "${FLAG_5}" -else - _fail "Challenge 5: Could not find flag in 777 permission files" - FLAGS[5]="" -fi - -# Challenge 6: Service Discovery -# Hint: "What services are running? Use 'ss -tulpn' to find listening ports" -echo "Challenge 6: Service Discovery" -FLAG_6="" -for port in $(ss -tulpn 2>/dev/null \ - | awk '/LISTEN/ {split($5,a,":"); print a[length(a)]}' \ - | grep -vE '^(22|80|443|8083)$' \ - | head -3); do - FLAG_6=$(curl -s --connect-timeout 3 "localhost:${port}" 2>/dev/null \ - | grep -ao 'CTF{[^}]*}' \ - | head -1) || true - [[ -n "${FLAG_6}" ]] && break -done -if [[ -n "${FLAG_6}" ]]; then - _verify_flag 6 "${FLAG_6}" -else - _fail "Challenge 6: Could not find flag from listening services" - FLAGS[6]="" -fi - -# Challenge 7: Encoding Challenge -# Hint: "The flag is encoded. Use 'base64 -d' to decode" -echo "Challenge 7: Encoding Challenge" -ENCODED_FILE=$(find /home/ctf_user/ctf_challenges -name '*.txt' -type f 2>/dev/null | head -1) || true -if [[ -n "${ENCODED_FILE}" ]]; then - FLAG_7=$(cat "${ENCODED_FILE}" 2>/dev/null \ - | base64 -d 2>/dev/null \ - | base64 -d 2>/dev/null \ - | grep -ao 'CTF{[^}]*}' \ - | head -1) || true - if [[ -n "${FLAG_7}" ]]; then - _verify_flag 7 "${FLAG_7}" - else - _fail "Challenge 7: Could not decode flag from file" - FLAGS[7]="" - fi -else - _fail "Challenge 7: No encoded file found" - FLAGS[7]="" -fi - -# Challenge 8: SSH Secrets -# Hint: "SSH configurations often hide secrets. Explore ~/.ssh thoroughly" -echo "Challenge 8: SSH Secrets" -FLAG_8="" -while IFS= read -r -d '' f; do - FLAG_8=$(grep -ao 'CTF{[^}]*}' "${f}" 2>/dev/null | head -1) || true - [[ -n "${FLAG_8}" ]] && break -done < <(find /home/ctf_user/.ssh -type f -print0 2>/dev/null) -if [[ -n "${FLAG_8}" ]]; then - _verify_flag 8 "${FLAG_8}" -else - _fail "Challenge 8: Could not find flag in .ssh directory" - FLAGS[8]="" -fi - -# Challenge 9: DNS Inspection -# Hint: "Inspect systemd-resolved configuration safely" -echo "Challenge 9: DNS Inspection" -DNS_DROP_IN="/etc/systemd/resolved.conf.d/ctf-dns.conf" -if [[ -r "${DNS_DROP_IN}" ]]; then - FLAG_9=$(grep -ao 'CTF{[^}]*}' "${DNS_DROP_IN}" 2>/dev/null | head -1) || true - if [[ -n "${FLAG_9}" ]]; then - _verify_flag 9 "${FLAG_9}" "Solved challenge 9" "Challenge 9: Found flag but verify rejected it - SETUP BUG" - else - _fail "Challenge 9: DNS drop-in has no CTF flag - SETUP BUG" - FLAGS[9]="" - fi -else - _fail "Challenge 9: DNS drop-in not readable - SETUP BUG" - FLAGS[9]="" -fi - -# Challenge 10: File Monitoring -# Hint: "Try creating a file in ctf_challenges" -echo "Challenge 10: File Monitoring" -if ! systemctl is-active ctf-monitor-directory.service &>/dev/null; then - _fail "Challenge 10: Monitor service not running - SETUP BUG" - FLAGS[10]="" -else - # Wait for inotifywait process to actually be running (service starts but has internal delay) - echo " Waiting for inotifywait to be ready..." - for _ in {1..15}; do - pgrep -f "inotifywait.*ctf_challenges" &>/dev/null && break - sleep 2 - done - - true > /tmp/.ctf_upload_triggered 2>/dev/null || true - TRIGGER="/home/ctf_user/ctf_challenges/test_$$" - touch "${TRIGGER}" - sleep 3 - - FLAG_10="" - for _ in {1..10}; do - FLAG_10=$(grep -ao 'CTF{[^}]*}' /tmp/.ctf_upload_triggered 2>/dev/null | head -1) || true - [[ -n "${FLAG_10}" ]] && break - sleep 2 - done - rm -f "${TRIGGER}" - - if [[ -n "${FLAG_10}" ]]; then - _verify_flag 10 "${FLAG_10}" "Solved challenge 10" "Challenge 10: Found flag but verify rejected it - SETUP BUG" - else - _fail "Challenge 10: File monitoring did not trigger - SETUP BUG" - FLAGS[10]="" - fi -fi - -# Challenge 11: Web Configuration -# Hint: "Check what ports nginx is listening on" -echo "Challenge 11: Web Configuration" -NGINX_PORT=$(grep -r 'listen' /etc/nginx/ 2>/dev/null \ - | grep -oP 'listen\s+\K[0-9]+' \ - | grep -v '^80$' \ - | head -1) || true -if [[ -n "${NGINX_PORT}" ]]; then - FLAG_11=$(curl -s "localhost:${NGINX_PORT}" 2>/dev/null \ - | grep -ao 'CTF{[^}]*}' \ - | head -1) || true - if [[ -n "${FLAG_11}" ]]; then - _verify_flag 11 "${FLAG_11}" - else - _fail "Challenge 11: Could not get flag from nginx" - FLAGS[11]="" - fi -else - _fail "Challenge 11: Could not find nginx non-standard port" - FLAGS[11]="" -fi - -# Challenge 12: Network Traffic Analysis -# Hint: "Look at ping patterns with tcpdump" -echo "Challenge 12: Network Traffic Analysis" -TCPDUMP_OUT=$(echo 'CTFpassword123!' \ - | sudo -S timeout 10 tcpdump -i lo -c 4 -X icmp 2>/dev/null) || true -if [[ -n "${TCPDUMP_OUT}" ]]; then - HEX=$(echo "${TCPDUMP_OUT}" \ - | grep -E '^\s+0x' \ - | awk '{print $2$3$4$5$6$7$8$9}' \ - | tr -d '\n') - FLAG_12=$(echo "${HEX}" | xxd -r -p 2>/dev/null | grep -ao 'CTF{[^}]*}' | head -1) || true - - if [[ -n "${FLAG_12}" ]]; then - _verify_flag 12 "${FLAG_12}" - else - _fail "Challenge 12: Could not extract flag from ping traffic" - FLAGS[12]="" - fi -else - _fail "Challenge 12: tcpdump capture failed" - FLAGS[12]="" -fi - -# Challenge 13: Cron Job Hunter -# Hint: "Check /etc/cron.d/, /etc/crontab, and user crontabs" -echo "Challenge 13: Cron Job Hunter" -FLAG_13="" -for dir in /etc/cron.d /etc/cron.daily /etc/cron.hourly; do - [[ -d "${dir}" ]] || continue - FLAG_13=$(grep -rh 'CTF{' "${dir}" 2>/dev/null | grep -ao 'CTF{[^}]*}' | head -1) || true - [[ -n "${FLAG_13}" ]] && break -done -if [[ -n "${FLAG_13}" ]]; then - _verify_flag 13 "${FLAG_13}" -else - _fail "Challenge 13: Could not find flag in cron directories" - FLAGS[13]="" -fi - -# Challenge 14: Process Environment -# Hint: "Process info lives in /proc. Check /proc/PID/environ" -echo "Challenge 14: Process Environment" -FLAG_14="" -for pid in $(pgrep -u ctf_user 2>/dev/null); do - [[ -r "/proc/${pid}/environ" ]] || continue - FLAG_14=$(tr '\0' '\n' < "/proc/${pid}/environ" 2>/dev/null | grep -ao 'CTF{[^}]*}') || true - [[ -n "${FLAG_14}" ]] && break -done -if [[ -n "${FLAG_14}" ]]; then - _verify_flag 14 "${FLAG_14}" -else - _fail "Challenge 14: Could not find flag in process environments" - FLAGS[14]="" -fi - -# Challenge 15: Archive Archaeologist -# Hint: "Archives can be nested. Use 'tar -xzf' to extract layers" -echo "Challenge 15: Archive Archaeologist" -ARCHIVE=$(find /home/ctf_user/ctf_challenges -name '*.tar.gz' 2>/dev/null | head -1) || true -if [[ -n "${ARCHIVE}" ]]; then - TMPDIR=$(mktemp -d) - cd "${TMPDIR}" - tar -xzf "${ARCHIVE}" 2>/dev/null || true - for _ in {1..5}; do - INNER=$(find . -maxdepth 1 -name '*.tar.gz' 2>/dev/null | head -1) || true - [[ -z "${INNER}" ]] && break - tar -xzf "${INNER}" 2>/dev/null || true - rm -f "${INNER}" - done - FLAG_15=$(grep -rh 'CTF{' . 2>/dev/null | grep -ao 'CTF{[^}]*}' | head -1) || true - cd - >/dev/null - rm -rf "${TMPDIR}" - - if [[ -n "${FLAG_15}" ]]; then - _verify_flag 15 "${FLAG_15}" - else - _fail "Challenge 15: Could not find flag in nested archives" - FLAGS[15]="" - fi -else - _fail "Challenge 15: No archive found" - FLAGS[15]="" -fi - -# Challenge 16: Symbolic Sleuth -# Hint: "Use 'readlink -f' to find the final target" -echo "Challenge 16: Symbolic Sleuth" -FLAG_16="" -while IFS= read -r -d '' link; do - TARGET=$(readlink -f "${link}" 2>/dev/null) || true - [[ -r "${TARGET}" ]] || continue - FLAG_16=$(grep -ao 'CTF{[^}]*}' "${TARGET}" 2>/dev/null | head -1) || true - [[ -n "${FLAG_16}" ]] && break -done < <(find /home/ctf_user/ctf_challenges -type l -print0 2>/dev/null) -if [[ -n "${FLAG_16}" ]]; then - _verify_flag 16 "${FLAG_16}" -else - _fail "Challenge 16: Could not find flag via symlinks" - FLAGS[16]="" -fi - -# Challenge 17: History Mystery -# Hint: "Bash stores history in ~/.bash_history. Other users may have history too" -echo "Challenge 17: History Mystery" -FLAG_17="" -for home in /home/*; do - user=$(basename "${home}") - [[ "${user}" == "ctf_user" ]] && continue - [[ -r "${home}/.bash_history" ]] || continue - FLAG_17=$(grep -ao 'CTF{[^}]*}' "${home}/.bash_history" 2>/dev/null | head -1) || true - [[ -n "${FLAG_17}" ]] && break -done -if [[ -n "${FLAG_17}" ]]; then - _verify_flag 17 "${FLAG_17}" -else - _fail "Challenge 17: Could not find flag in user histories" - FLAGS[17]="" -fi - -# Challenge 18: Disk Detective -# Hint: "Try mounting disk images with 'sudo mount -o loop'" -echo "Challenge 18: Disk Detective" -DISK_IMG=$(find /opt /home -name '*.img' -type f 2>/dev/null | head -1) || true -if [[ -n "${DISK_IMG}" ]]; then - MNTDIR=$(mktemp -d) - echo 'CTFpassword123!' | sudo -S mount -o loop "${DISK_IMG}" "${MNTDIR}" 2>/dev/null - FLAG_18=$(find "${MNTDIR}" -type f -print0 2>/dev/null \ - | xargs -0 grep -ah 'CTF{' 2>/dev/null \ - | grep -ao 'CTF{[^}]*}' \ - | head -1) || true - echo 'CTFpassword123!' | sudo -S umount "${MNTDIR}" 2>/dev/null || true - rmdir "${MNTDIR}" 2>/dev/null || true - - if [[ -n "${FLAG_18}" ]]; then - _verify_flag 18 "${FLAG_18}" - else - _fail "Challenge 18: Could not find flag in disk image" - FLAGS[18]="" - fi -else - _fail "Challenge 18: No disk image found" - FLAGS[18]="" -fi - -# ============================================================================ -# VERIFICATION TOKEN TEST -# ============================================================================ -_section "VERIFICATION TOKEN TEST" - -PROGRESS=$(verify progress 2>&1) -if echo "${PROGRESS}" | grep -q "19/19"; then - _pass "All 19 progress checks completed" -else - _fail "Not all progress checks completed: ${PROGRESS}" -fi - -EXPORT_OUT=$(verify export testuser 2>&1) || true - -if echo "${EXPORT_OUT}" | grep -q "COMPLETION CERTIFICATE"; then - _pass "Export generates certificate" -else - _fail "Export missing certificate" -fi - -if echo "${EXPORT_OUT}" | grep -q "BEGIN L2C CTF TOKEN"; then - _pass "Export generates token" - - TOKEN=$(echo "${EXPORT_OUT}" \ - | sed -n '/BEGIN L2C CTF TOKEN/,/END L2C CTF TOKEN/p' \ - | grep -v 'L2C CTF TOKEN' \ - | tr -d '\n ') - DECODED=$(echo "${TOKEN}" | base64 -d 2>/dev/null) || true - - if echo "${DECODED}" | grep -q '"github_username":"testuser"'; then - _pass "Token contains correct username" - else - _fail "Token has wrong username" - fi - - if echo "${DECODED}" | grep -q '"challenges":18'; then - _pass "Token shows 18 challenges" - else - _fail "Token has wrong challenge count" - fi -else - _fail "Export missing token" -fi - -FIRST_TIME_OUT=$(verify time 2>&1) || true -sleep 2 -SECOND_TIME_OUT=$(verify time 2>&1) || true -if [[ "${FIRST_TIME_OUT}" == "${SECOND_TIME_OUT}" ]]; then - _pass "verify time is frozen after first successful export" -else - _fail "verify time changed after export (freeze failed)" -fi - -# ============================================================================ -# SUMMARY -# ============================================================================ -_section "SUMMARY" - -echo "Passed: ${PASSED}" -echo "Failed: ${FAILED}" -echo "Flags captured: ${#FLAGS[@]}" -echo "" - -if [ "$WITH_REBOOT" = true ] && [ $FAILED -eq 0 ]; then - sort -u /var/ctf/completed_challenges 2>/dev/null | wc -l > "$PROGRESS_SNAPSHOT" - touch "$REBOOT_MARKER" - echo "Reboot marker created. Re-run after reboot to verify services." - exit 100 -fi - -if [[ ${FAILED} -eq 0 ]]; then - echo -e "${GREEN}All tests passed! Students can complete this CTF.${NC}" - exit 0 -else - echo -e "${RED}Some tests failed. Students may be blocked.${NC}" - exit 1 -fi +#!/usr/bin/env bash +# shellcheck shell=bash +# +# CTF Challenge Test Script +# Runs on the VM to validate all challenges are solvable by students +# +# This script simulates a real user journey - discovering and solving each +# challenge using only the hints provided. If these tests pass, students +# can complete the CTF. +# +# Usage: +# ./test_ctf_challenges.sh [--with-reboot] +# DEBUG=true ./test_ctf_challenges.sh # Enable debug tracing +# +# Flags: +# --with-reboot After tests pass, signal reboot to verify services persist +# +# Exit codes: +# 0 - All tests passed +# 1 - One or more tests failed +# 100 - Reboot requested (only with --with-reboot flag) +# + +set -o errexit +set -o pipefail +set -o nounset + +# Enable debug tracing if DEBUG=true +[[ "${DEBUG:-}" == 'true' ]] && set -o xtrace + +# Ensure verify command is available +# It's installed in /usr/local/bin by ctf_setup.sh +if ! command -v verify &>/dev/null; then + export PATH="/usr/local/bin:$PATH" +fi + +# ============================================================================= +# CONSTANTS +# ============================================================================= + +# Terminal colors for output formatting +readonly RED='\033[0;31m' +readonly GREEN='\033[0;32m' +readonly YELLOW='\033[1;33m' +readonly NC='\033[0m' # No Color + +# File paths for reboot test coordination +readonly REBOOT_MARKER="/tmp/.ctf_reboot_test_marker" +readonly PROGRESS_SNAPSHOT="/tmp/.ctf_progress_snapshot" + +# ============================================================================= +# GLOBAL STATE +# ============================================================================= + +# Test result counters (mutable) +PASSED=0 +FAILED=0 + +# Parse arguments +WITH_REBOOT=false +for arg in "$@"; do + case $arg in + --with-reboot) + WITH_REBOOT=true + shift + ;; + esac +done + +# ============================================================================= +# HELPER FUNCTIONS +# ============================================================================= + +# Log a passing test result and increment counter +# Arguments: +# $1 - Message describing what passed +_pass() { + local message="${1}" + echo -e "${GREEN}✓ PASS${NC}: ${message}" + ((PASSED++)) || true +} + +# Log a failing test result and increment counter +# Arguments: +# $1 - Message describing what failed +_fail() { + local message="${1}" + echo -e "${RED}✗ FAIL${NC}: ${message}" + ((FAILED++)) || true +} + +# Print a section header for visual separation in output +# Arguments: +# $1 - Section title to display +_section() { + local title="${1}" + echo "" + echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e "${YELLOW}${title}${NC}" + echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +} + +# Verify a flag with the verify command and record result +# Arguments: +# $1 - Challenge number +# $2 - Flag value to verify +# $3 - Success message (optional, defaults to "Solved challenge N") +# $4 - Failure message (optional, defaults to "Found flag but verify rejected it") +# Returns: +# 0 if flag was verified successfully, 1 otherwise +_verify_flag() { + local challenge_num="${1}" + local flag_value="${2}" + local success_msg="${3:-Solved challenge ${challenge_num}}" + local fail_msg="${4:-Challenge ${challenge_num}: Found flag but verify rejected it}" + local verify_out + + verify_out=$(verify "${challenge_num}" "${flag_value}" 2>&1) || true + if echo "${verify_out}" | grep -qE "(Correct|verified)"; then + _pass "${success_msg}" + FLAGS[${challenge_num}]="${flag_value}" + return 0 + else + _fail "${fail_msg}" + FLAGS[${challenge_num}]="" + return 1 + fi +} + +# ============================================================================ +# POST-REBOOT VERIFICATION +# ============================================================================ +if [[ -f "${REBOOT_MARKER}" ]]; then + _section "POST-REBOOT VERIFICATION" + + echo "Verifying services survived reboot..." + + for service in ctf-secret-service ctf-monitor-directory ctf-ping-message ctf-secret-process nginx; do + if systemctl is-active "${service}" &>/dev/null; then + _pass "${service} is running after reboot" + else + _fail "${service} failed to start after reboot - SETUP BUG" + fi + done + + if [ -f "$PROGRESS_SNAPSHOT" ]; then + EXPECTED=$(cat "$PROGRESS_SNAPSHOT") + ACTUAL=$(sort -u /var/ctf/completed_challenges 2>/dev/null | wc -l) + if [ "$ACTUAL" -ge "$EXPECTED" ]; then + _pass "Progress persisted after reboot ($ACTUAL checks)" + else + _fail "Progress lost after reboot (expected ${EXPECTED}, got ${ACTUAL})" + fi + fi + + rm -f "${REBOOT_MARKER}" "${PROGRESS_SNAPSHOT}" + + echo "" + echo "Passed: ${PASSED} | Failed: ${FAILED}" + [[ ${FAILED} -eq 0 ]] && exit 0 || exit 1 +fi + +# ============================================================================ +# VERIFY COMMAND SANITY CHECK +# ============================================================================ +_section "VERIFY COMMAND SANITY CHECK" + +# Quick check that the verify command works at all +if ! command -v verify &>/dev/null; then + _fail "verify command not found in PATH" + echo "PATH: ${PATH}" + echo "Looking for verify: $(which verify 2>&1 || echo 'not found')" + echo "Checking /usr/local/bin: $(ls -la /usr/local/bin/verify 2>&1 || echo 'not found')" + exit 1 +fi + +# Timer should not start before first numeric verify command +rm -f /var/ctf/ctf_start_time /var/ctf/ctf_end_time +TIMER_PRESTART_OUT=$(verify time 2>&1) || true +if echo "${TIMER_PRESTART_OUT}" | grep -q "Timer not started"; then + _pass "verify time shows pre-start message" +else + _fail "verify time pre-start behavior incorrect" +fi + +VERIFY_OUTPUT=$(verify 0 "CTF{example}" 2>&1) || true +if echo "${VERIFY_OUTPUT}" | grep -q "✓"; then + _pass "verify command accepts example flag" +else + _fail "verify command broken - SETUP BUG" + echo "Cannot continue without working verify command" + exit 1 +fi + +if echo "${VERIFY_OUTPUT}" | grep -q "1/19"; then + _pass "verify progress counts the example check" +else + _fail "verify progress did not show 1/19 after the example check" +fi + +if [[ -f /var/ctf/ctf_start_time ]]; then + _pass "Timer starts after first numeric verify command" +else + _fail "Timer did not start after first numeric verify command" +fi + +# ============================================================================ +# CHALLENGE DISCOVERY AND SOLVING +# ============================================================================ +_section "SOLVING ALL CHALLENGES" + +echo "Simulating real student journey using hints to discover and solve each challenge..." +echo "" + +# Store discovered flags +declare -A FLAGS + +# Challenge 1: Hidden File Discovery +# Hint: "Hidden files in Linux start with a dot. Try 'ls -la'" +echo "Challenge 1: Hidden File Discovery" +HIDDEN_FILE=$(ls -la /home/ctf_user/ctf_challenges/ 2>/dev/null \ + | awk '/^-.*\./ {print $NF}' \ + | grep '^\.' \ + | head -1) || true +if [[ -n "${HIDDEN_FILE}" ]]; then + FLAG_1=$(cat "/home/ctf_user/ctf_challenges/${HIDDEN_FILE}" 2>/dev/null \ + | grep -ao 'CTF{[^}]*}' \ + | head -1) || true + if [[ -n "${FLAG_1}" ]]; then + _verify_flag 1 "${FLAG_1}" + else + _fail "Challenge 1: Found file but no CTF flag in it" + FLAGS[1]="" + fi +else + _fail "Challenge 1: No hidden files found with ls -la" + FLAGS[1]="" +fi + +# Challenge 2: Basic File Search +# Hint: "Use find to search for files. Try: find ~ -name '*.txt'" +echo "Challenge 2: Basic File Search" +TXT_FILE=$(find /home/ctf_user/documents -name '*.txt' -type f 2>/dev/null | head -1) || true +if [[ -n "${TXT_FILE}" ]]; then + FLAG_2=$(cat "${TXT_FILE}" 2>/dev/null | grep -ao 'CTF{[^}]*}' | head -1) || true + if [[ -n "${FLAG_2}" ]]; then + _verify_flag 2 "${FLAG_2}" + else + _fail "Challenge 2: Found file but no CTF flag in it" + FLAGS[2]="" + fi +else + _fail "Challenge 2: No .txt files found in documents" + FLAGS[2]="" +fi + +# Challenge 3: Log Analysis +# Hint: "Large log files can hide secrets. Check /var/log and use 'tail'" +echo "Challenge 3: Log Analysis" +LARGE_LOG=$(find /var/log -type f -size +100M 2>/dev/null | head -1) || true +if [[ -n "${LARGE_LOG}" ]]; then + FLAG_3=$(tail -1 "${LARGE_LOG}" 2>/dev/null | grep -ao 'CTF{[^}]*}' | head -1) || true + if [[ -n "${FLAG_3}" ]]; then + _verify_flag 3 "${FLAG_3}" + else + _fail "Challenge 3: Found log but no CTF flag in it" + FLAGS[3]="" + fi +else + _fail "Challenge 3: No large log files found" + FLAGS[3]="" +fi + +# Challenge 4: User Investigation +# Hint: "Investigate other users. Check /etc/passwd or use 'getent passwd'" +echo "Challenge 4: User Investigation" +FLAG_4="" +for user in $(getent passwd | awk -F: '$3 >= 1000 && $1 != "ctf_user" && $1 != "nobody" {print $1}'); do + if [[ -r "/home/${user}/.profile" ]]; then + FLAG_4=$(grep -ao 'CTF{[^}]*}' "/home/${user}/.profile" 2>/dev/null | head -1) || true + [[ -n "${FLAG_4}" ]] && break + fi +done +if [[ -n "${FLAG_4}" ]]; then + _verify_flag 4 "${FLAG_4}" +else + _fail "Challenge 4: Could not find flag in user profiles" + FLAGS[4]="" +fi + +# Challenge 5: Permission Analysis +# Hint: "Look for files with unusual permissions. Try: find / -perm 777" +echo "Challenge 5: Permission Analysis" +FLAG_5="" +for path in /opt /etc /var; do + PERM_FILE=$(find "${path}" -type f -perm 777 2>/dev/null | head -1) || true + if [[ -n "${PERM_FILE}" ]]; then + FLAG_5=$(cat "${PERM_FILE}" 2>/dev/null | grep -ao 'CTF{[^}]*}' | head -1) || true + [[ -n "${FLAG_5}" ]] && break + fi +done +if [[ -n "${FLAG_5}" ]]; then + _verify_flag 5 "${FLAG_5}" +else + _fail "Challenge 5: Could not find flag in 777 permission files" + FLAGS[5]="" +fi + +# Challenge 6: Service Discovery +# Hint: "What services are running? Use 'ss -tulpn' to find listening ports" +echo "Challenge 6: Service Discovery" +FLAG_6="" +for port in $(ss -tulpn 2>/dev/null \ + | awk '/LISTEN/ {split($5,a,":"); print a[length(a)]}' \ + | grep -vE '^(22|80|443|8083)$' \ + | head -3); do + FLAG_6=$(curl -s --connect-timeout 3 "localhost:${port}" 2>/dev/null \ + | grep -ao 'CTF{[^}]*}' \ + | head -1) || true + [[ -n "${FLAG_6}" ]] && break +done +if [[ -n "${FLAG_6}" ]]; then + _verify_flag 6 "${FLAG_6}" +else + _fail "Challenge 6: Could not find flag from listening services" + FLAGS[6]="" +fi + +# Challenge 7: Encoding Challenge +# Hint: "The flag is encoded. Use 'base64 -d' to decode" +echo "Challenge 7: Encoding Challenge" +ENCODED_FILE=$(find /home/ctf_user/ctf_challenges -name '*.txt' -type f 2>/dev/null | head -1) || true +if [[ -n "${ENCODED_FILE}" ]]; then + FLAG_7=$(cat "${ENCODED_FILE}" 2>/dev/null \ + | base64 -d 2>/dev/null \ + | base64 -d 2>/dev/null \ + | grep -ao 'CTF{[^}]*}' \ + | head -1) || true + if [[ -n "${FLAG_7}" ]]; then + _verify_flag 7 "${FLAG_7}" + else + _fail "Challenge 7: Could not decode flag from file" + FLAGS[7]="" + fi +else + _fail "Challenge 7: No encoded file found" + FLAGS[7]="" +fi + +# Challenge 8: SSH Secrets +# Hint: "SSH configurations often hide secrets. Explore ~/.ssh thoroughly" +echo "Challenge 8: SSH Secrets" +FLAG_8="" +while IFS= read -r -d '' f; do + FLAG_8=$(grep -ao 'CTF{[^}]*}' "${f}" 2>/dev/null | head -1) || true + [[ -n "${FLAG_8}" ]] && break +done < <(find /home/ctf_user/.ssh -type f -print0 2>/dev/null) +if [[ -n "${FLAG_8}" ]]; then + _verify_flag 8 "${FLAG_8}" +else + _fail "Challenge 8: Could not find flag in .ssh directory" + FLAGS[8]="" +fi + +# Challenge 9: DNS Inspection +# Hint: "Inspect systemd-resolved configuration safely" +echo "Challenge 9: DNS Inspection" +DNS_DROP_IN="/etc/systemd/resolved.conf.d/ctf-dns.conf" +if [[ -r "${DNS_DROP_IN}" ]]; then + FLAG_9=$(grep -ao 'CTF{[^}]*}' "${DNS_DROP_IN}" 2>/dev/null | head -1) || true + if [[ -n "${FLAG_9}" ]]; then + _verify_flag 9 "${FLAG_9}" "Solved challenge 9" "Challenge 9: Found flag but verify rejected it - SETUP BUG" + else + _fail "Challenge 9: DNS drop-in has no CTF flag - SETUP BUG" + FLAGS[9]="" + fi +else + _fail "Challenge 9: DNS drop-in not readable - SETUP BUG" + FLAGS[9]="" +fi + +# Challenge 10: File Monitoring +# Hint: "Try creating a file in ctf_challenges" +echo "Challenge 10: File Monitoring" +if ! systemctl is-active ctf-monitor-directory.service &>/dev/null; then + _fail "Challenge 10: Monitor service not running - SETUP BUG" + FLAGS[10]="" +else + # Wait for inotifywait process to actually be running (service starts but has internal delay) + echo " Waiting for inotifywait to be ready..." + for _ in {1..15}; do + pgrep -f "inotifywait.*ctf_challenges" &>/dev/null && break + sleep 2 + done + + true > /tmp/.ctf_upload_triggered 2>/dev/null || true + TRIGGER="/home/ctf_user/ctf_challenges/test_$$" + touch "${TRIGGER}" + sleep 3 + + FLAG_10="" + for _ in {1..10}; do + FLAG_10=$(grep -ao 'CTF{[^}]*}' /tmp/.ctf_upload_triggered 2>/dev/null | head -1) || true + [[ -n "${FLAG_10}" ]] && break + sleep 2 + done + rm -f "${TRIGGER}" + + if [[ -n "${FLAG_10}" ]]; then + _verify_flag 10 "${FLAG_10}" "Solved challenge 10" "Challenge 10: Found flag but verify rejected it - SETUP BUG" + else + _fail "Challenge 10: File monitoring did not trigger - SETUP BUG" + FLAGS[10]="" + fi +fi + +# Challenge 11: Web Configuration +# Hint: "Check what ports nginx is listening on" +echo "Challenge 11: Web Configuration" +NGINX_PORT=$(grep -r 'listen' /etc/nginx/ 2>/dev/null \ + | grep -oP 'listen\s+\K[0-9]+' \ + | grep -v '^80$' \ + | head -1) || true +if [[ -n "${NGINX_PORT}" ]]; then + FLAG_11=$(curl -s "localhost:${NGINX_PORT}" 2>/dev/null \ + | grep -ao 'CTF{[^}]*}' \ + | head -1) || true + if [[ -n "${FLAG_11}" ]]; then + _verify_flag 11 "${FLAG_11}" + else + _fail "Challenge 11: Could not get flag from nginx" + FLAGS[11]="" + fi +else + _fail "Challenge 11: Could not find nginx non-standard port" + FLAGS[11]="" +fi + +# Challenge 12: Network Traffic Analysis +# Hint: "Look at ping patterns with tcpdump" +echo "Challenge 12: Network Traffic Analysis" +TCPDUMP_OUT=$(echo 'CTFpassword123!' \ + | sudo -S timeout 10 tcpdump -i lo -c 4 -X icmp 2>/dev/null) || true +if [[ -n "${TCPDUMP_OUT}" ]]; then + HEX=$(echo "${TCPDUMP_OUT}" \ + | grep -E '^\s+0x' \ + | awk '{print $2$3$4$5$6$7$8$9}' \ + | tr -d '\n') + FLAG_12=$(echo "${HEX}" | xxd -r -p 2>/dev/null | grep -ao 'CTF{[^}]*}' | head -1) || true + + if [[ -n "${FLAG_12}" ]]; then + _verify_flag 12 "${FLAG_12}" + else + _fail "Challenge 12: Could not extract flag from ping traffic" + FLAGS[12]="" + fi +else + _fail "Challenge 12: tcpdump capture failed" + FLAGS[12]="" +fi + +# Challenge 13: Cron Job Hunter +# Hint: "Check /etc/cron.d/, /etc/crontab, and user crontabs" +echo "Challenge 13: Cron Job Hunter" +FLAG_13="" +for dir in /etc/cron.d /etc/cron.daily /etc/cron.hourly; do + [[ -d "${dir}" ]] || continue + FLAG_13=$(grep -rh 'CTF{' "${dir}" 2>/dev/null | grep -ao 'CTF{[^}]*}' | head -1) || true + [[ -n "${FLAG_13}" ]] && break +done +if [[ -n "${FLAG_13}" ]]; then + _verify_flag 13 "${FLAG_13}" +else + _fail "Challenge 13: Could not find flag in cron directories" + FLAGS[13]="" +fi + +# Challenge 14: Process Environment +# Hint: "Process info lives in /proc. Check /proc/PID/environ" +echo "Challenge 14: Process Environment" +FLAG_14="" +for pid in $(pgrep -u ctf_user 2>/dev/null); do + [[ -r "/proc/${pid}/environ" ]] || continue + FLAG_14=$(tr '\0' '\n' < "/proc/${pid}/environ" 2>/dev/null | grep -ao 'CTF{[^}]*}') || true + [[ -n "${FLAG_14}" ]] && break +done +if [[ -n "${FLAG_14}" ]]; then + _verify_flag 14 "${FLAG_14}" +else + _fail "Challenge 14: Could not find flag in process environments" + FLAGS[14]="" +fi + +# Challenge 15: Archive Archaeologist +# Hint: "Archives can be nested. Use 'tar -xzf' to extract layers" +echo "Challenge 15: Archive Archaeologist" +ARCHIVE=$(find /home/ctf_user/ctf_challenges -name '*.tar.gz' 2>/dev/null | head -1) || true +if [[ -n "${ARCHIVE}" ]]; then + TMPDIR=$(mktemp -d) + cd "${TMPDIR}" + tar -xzf "${ARCHIVE}" 2>/dev/null || true + for _ in {1..5}; do + INNER=$(find . -maxdepth 1 -name '*.tar.gz' 2>/dev/null | head -1) || true + [[ -z "${INNER}" ]] && break + tar -xzf "${INNER}" 2>/dev/null || true + rm -f "${INNER}" + done + FLAG_15=$(grep -rh 'CTF{' . 2>/dev/null | grep -ao 'CTF{[^}]*}' | head -1) || true + cd - >/dev/null + rm -rf "${TMPDIR}" + + if [[ -n "${FLAG_15}" ]]; then + _verify_flag 15 "${FLAG_15}" + else + _fail "Challenge 15: Could not find flag in nested archives" + FLAGS[15]="" + fi +else + _fail "Challenge 15: No archive found" + FLAGS[15]="" +fi + +# Challenge 16: Symbolic Sleuth +# Hint: "Use 'readlink -f' to find the final target" +echo "Challenge 16: Symbolic Sleuth" +FLAG_16="" +while IFS= read -r -d '' link; do + TARGET=$(readlink -f "${link}" 2>/dev/null) || true + [[ -r "${TARGET}" ]] || continue + FLAG_16=$(grep -ao 'CTF{[^}]*}' "${TARGET}" 2>/dev/null | head -1) || true + [[ -n "${FLAG_16}" ]] && break +done < <(find /home/ctf_user/ctf_challenges -type l -print0 2>/dev/null) +if [[ -n "${FLAG_16}" ]]; then + _verify_flag 16 "${FLAG_16}" +else + _fail "Challenge 16: Could not find flag via symlinks" + FLAGS[16]="" +fi + +# Challenge 17: History Mystery +# Hint: "Bash stores history in ~/.bash_history. Other users may have history too" +echo "Challenge 17: History Mystery" +FLAG_17="" +for home in /home/*; do + user=$(basename "${home}") + [[ "${user}" == "ctf_user" ]] && continue + [[ -r "${home}/.bash_history" ]] || continue + FLAG_17=$(grep -ao 'CTF{[^}]*}' "${home}/.bash_history" 2>/dev/null | head -1) || true + [[ -n "${FLAG_17}" ]] && break +done +if [[ -n "${FLAG_17}" ]]; then + _verify_flag 17 "${FLAG_17}" +else + _fail "Challenge 17: Could not find flag in user histories" + FLAGS[17]="" +fi + +# Challenge 18: Disk Detective +# Hint: "Try mounting disk images with 'sudo mount -o loop'" +echo "Challenge 18: Disk Detective" +DISK_IMG=$(find /opt /home -name '*.img' -type f 2>/dev/null | head -1) || true +if [[ -n "${DISK_IMG}" ]]; then + MNTDIR=$(mktemp -d) + echo 'CTFpassword123!' | sudo -S mount -o loop "${DISK_IMG}" "${MNTDIR}" 2>/dev/null + FLAG_18=$(find "${MNTDIR}" -type f -print0 2>/dev/null \ + | xargs -0 grep -ah 'CTF{' 2>/dev/null \ + | grep -ao 'CTF{[^}]*}' \ + | head -1) || true + echo 'CTFpassword123!' | sudo -S umount "${MNTDIR}" 2>/dev/null || true + rmdir "${MNTDIR}" 2>/dev/null || true + + if [[ -n "${FLAG_18}" ]]; then + _verify_flag 18 "${FLAG_18}" + else + _fail "Challenge 18: Could not find flag in disk image" + FLAGS[18]="" + fi +else + _fail "Challenge 18: No disk image found" + FLAGS[18]="" +fi + +# ============================================================================ +# VERIFICATION TOKEN TEST +# ============================================================================ +_section "VERIFICATION TOKEN TEST" + +PROGRESS=$(verify progress 2>&1) +if echo "${PROGRESS}" | grep -q "19/19"; then + _pass "All 19 progress checks completed" +else + _fail "Not all progress checks completed: ${PROGRESS}" +fi + +EXPORT_OUT=$(verify export testuser 2>&1) || true + +if echo "${EXPORT_OUT}" | grep -q "COMPLETION CERTIFICATE"; then + _pass "Export generates certificate" +else + _fail "Export missing certificate" +fi + +if echo "${EXPORT_OUT}" | grep -q "BEGIN L2C CTF TOKEN"; then + _pass "Export generates token" + + TOKEN=$(echo "${EXPORT_OUT}" \ + | sed -n '/BEGIN L2C CTF TOKEN/,/END L2C CTF TOKEN/p' \ + | grep -v 'L2C CTF TOKEN' \ + | tr -d '\n ') + DECODED=$(echo "${TOKEN}" | base64 -d 2>/dev/null) || true + + if echo "${DECODED}" | grep -q '"github_username":"testuser"'; then + _pass "Token contains correct username" + else + _fail "Token has wrong username" + fi + + if echo "${DECODED}" | grep -q '"challenges":18'; then + _pass "Token shows 18 challenges" + else + _fail "Token has wrong challenge count" + fi +else + _fail "Export missing token" +fi + +FIRST_TIME_OUT=$(verify time 2>&1) || true +sleep 2 +SECOND_TIME_OUT=$(verify time 2>&1) || true +if [[ "${FIRST_TIME_OUT}" == "${SECOND_TIME_OUT}" ]]; then + _pass "verify time is frozen after first successful export" +else + _fail "verify time changed after export (freeze failed)" +fi + +# ============================================================================ +# SUMMARY +# ============================================================================ +_section "SUMMARY" + +echo "Passed: ${PASSED}" +echo "Failed: ${FAILED}" +echo "Flags captured: ${#FLAGS[@]}" +echo "" + +if [ "$WITH_REBOOT" = true ] && [ $FAILED -eq 0 ]; then + sort -u /var/ctf/completed_challenges 2>/dev/null | wc -l > "$PROGRESS_SNAPSHOT" + touch "$REBOOT_MARKER" + echo "Reboot marker created. Re-run after reboot to verify services." + exit 100 +fi + +if [[ ${FAILED} -eq 0 ]]; then + echo -e "${GREEN}All tests passed! Students can complete this CTF.${NC}" + exit 0 +else + echo -e "${RED}Some tests failed. Students may be blocked.${NC}" + exit 1 +fi diff --git a/ctf_setup.sh b/ctf_setup.sh index bb09ed4..5ad6a11 100644 --- a/ctf_setup.sh +++ b/ctf_setup.sh @@ -5,6 +5,8 @@ # set -euo pipefail +perl -pi -e 's/\r\n/\n/g' "$0" + exec > >(tee /var/log/ctf_setup.log) 2>&1 DONE_MARKER="/var/lib/cloud/instance/ctf-setup.done" @@ -60,4 +62,4 @@ uv cache clean touch "$DONE_MARKER" touch "$LEGACY_DONE_MARKER" touch "$PROJECT_DONE_MARKER" -echo "CTF environment setup complete!" +echo "CTF environment setup complete!" \ No newline at end of file From 272f3bb0fc6bdc7b59f005dff1b76eacef5ac26c Mon Sep 17 00:00:00 2001 From: Human-Gechi Date: Thu, 4 Jun 2026 06:55:49 +0100 Subject: [PATCH 2/4] Forgot to commit .gitattributr file --- .gitattribute | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .gitattribute diff --git a/.gitattribute b/.gitattribute new file mode 100644 index 0000000..e715b57 --- /dev/null +++ b/.gitattribute @@ -0,0 +1,2 @@ +# Shell Scripts LF enforcement +*.sh text eol=lf \ No newline at end of file From a6beda32e6a6c9f1b3851bf6ed10055e236d4668 Mon Sep 17 00:00:00 2001 From: Ogechukwu Okoli Date: Thu, 4 Jun 2026 13:13:31 +0100 Subject: [PATCH 3/4] Rename .gitattribute to .gitattributes --- .gitattribute => .gitattributes | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename .gitattribute => .gitattributes (62%) diff --git a/.gitattribute b/.gitattributes similarity index 62% rename from .gitattribute rename to .gitattributes index e715b57..3922968 100644 --- a/.gitattribute +++ b/.gitattributes @@ -1,2 +1,2 @@ # Shell Scripts LF enforcement -*.sh text eol=lf \ No newline at end of file +*.sh text eol=lf From c107b09442319528161a8946599e99f00450e0ac Mon Sep 17 00:00:00 2001 From: Gwyneth Date: Wed, 10 Jun 2026 10:05:30 -0400 Subject: [PATCH 4/4] normalize shell scripts --- .github/skills/ctf-testing/deploy_and_test.sh | 1296 ++++++++-------- .../skills/ctf-testing/test_ctf_challenges.sh | 1318 ++++++++--------- 2 files changed, 1307 insertions(+), 1307 deletions(-) diff --git a/.github/skills/ctf-testing/deploy_and_test.sh b/.github/skills/ctf-testing/deploy_and_test.sh index 2318f6f..7182faa 100755 --- a/.github/skills/ctf-testing/deploy_and_test.sh +++ b/.github/skills/ctf-testing/deploy_and_test.sh @@ -1,648 +1,648 @@ -#!/usr/bin/env bash -# shellcheck shell=bash -# -# CTF Deploy and Test Orchestration Script -# Deploys infrastructure to cloud providers and runs tests -# -# Usage: -# ./deploy_and_test.sh [--with-reboot] -# DEBUG=true ./deploy_and_test.sh azure # Enable debug tracing -# -# Arguments: -# aws|azure|gcp|all Cloud provider(s) to test -# --with-reboot After initial tests pass, stop/start the VM and -# re-run verification to ensure services survive -# reboot and progress persists -# -# Prerequisites: -# - terraform (>= 1.0) -# - jq (for AWS terraform config) -# - sshpass (macOS: brew install hudochenkov/sshpass/sshpass) -# - aws CLI (for AWS, must be logged in) -# - az CLI (for Azure, must be logged in) -# - gcloud CLI (for GCP, must be authenticated) -# -# Examples: -# ./deploy_and_test.sh aws # Test AWS only -# ./deploy_and_test.sh azure --with-reboot # Test Azure with reboot -# ./deploy_and_test.sh all # Test all providers -# ./deploy_and_test.sh all --with-reboot # Full test suite -# - -set -o errexit -set -o pipefail -set -o nounset - -# Enable debug tracing if DEBUG=true -[[ "${DEBUG:-}" == 'true' ]] && set -o xtrace - -# ============================================================================= -# CONSTANTS -# ============================================================================= - -# Script paths (declare and assign separately to avoid masking return values) -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -readonly SCRIPT_DIR -REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)" -readonly REPO_ROOT -readonly TEST_SCRIPT="${SCRIPT_DIR}/test_ctf_challenges.sh" - -# SSH connection settings -readonly MAX_SSH_ATTEMPTS=30 -readonly SSH_RETRY_INTERVAL=10 -readonly SSH_USER="ctf_user" -readonly SSH_PASS="CTFpassword123!" -readonly SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR" - -# Terminal colors for output formatting -readonly RED='\033[0;31m' -readonly GREEN='\033[0;32m' -readonly YELLOW='\033[1;33m' -readonly BLUE='\033[0;34m' -readonly NC='\033[0m' # No Color - -# ============================================================================= -# GLOBAL STATE -# ============================================================================= - -# Cleanup tracking (mutable - set during test execution) -CURRENT_PROVIDER="" -CLEANUP_ON_EXIT=false - -# ============================================================================= -# UTILITY FUNCTIONS -# ============================================================================= - -# Log a message with timestamp and color based on level -# Arguments: -# $1 - Log level (INFO, OK, WARN, ERROR) -# $@ - Message to log -_log() { - local level="$1" - shift - local message="$*" - local timestamp - timestamp=$(date '+%H:%M:%S') - - case "${level}" in - INFO) echo -e "[${timestamp}] ${BLUE}${message}${NC}" ;; - OK) echo -e "[${timestamp}] ${GREEN}${message}${NC}" ;; - WARN) echo -e "[${timestamp}] ${YELLOW}${message}${NC}" ;; - ERROR) echo -e "[${timestamp}] ${RED}${message}${NC}" ;; - *) echo -e "[${timestamp}] ${level} ${message}" ;; - esac -} - -# Signal handler for cleanup on interrupt (SIGINT/SIGTERM) -# Destroys any in-progress infrastructure before exiting -_cleanup_handler() { - local exit_code=$? - - if [[ "${CLEANUP_ON_EXIT}" == true ]] && [[ -n "${CURRENT_PROVIDER}" ]]; then - echo "" - _log WARN "Caught interrupt - cleaning up ${CURRENT_PROVIDER} infrastructure..." - _terraform_destroy "${CURRENT_PROVIDER}" || true - fi - exit "${exit_code}" -} - -trap _cleanup_handler SIGINT SIGTERM - -# Execute sshpass command with password passed via file descriptor -# This hides the password from the process list -# Arguments: -# $@ - Command and arguments to pass to sshpass -_sshpass_cmd() { - sshpass -f <(printf '%s' "${SSH_PASS}") "$@" -} - -# ============================================================================= -# ARGUMENT PARSING -# ============================================================================= - -WITH_REBOOT=false -PROVIDERS_TO_TEST=() - -for arg in "$@"; do - case "${arg}" in - aws|azure|gcp) - PROVIDERS_TO_TEST+=("${arg}") - ;; - all) - PROVIDERS_TO_TEST=("aws" "azure" "gcp") - ;; - --with-reboot) - WITH_REBOOT=true - ;; - -h|--help) - head -32 "$0" | tail -30 - exit 0 - ;; - *) - echo "Unknown argument: ${arg}" - echo "Usage: $0 [--with-reboot]" - exit 1 - ;; - esac -done - -if [[ ${#PROVIDERS_TO_TEST[@]} -eq 0 ]]; then - echo "Usage: $0 [--with-reboot]" - exit 1 -fi - -# ============================================================================= -# PREREQUISITE CHECKS -# ============================================================================= - -# Verify all required tools are installed and authenticated for a provider -# Arguments: -# $1 - Cloud provider name (aws, azure, gcp) -_check_prerequisites() { - local provider="$1" - local missing=() - - echo -e "${BLUE}Checking prerequisites for ${provider}...${NC}" - - # Check terraform - if ! command -v terraform &>/dev/null; then - missing+=("terraform") - fi - - # Check jq (required for AWS terraform config) - if ! command -v jq &>/dev/null; then - missing+=("jq") - fi - - # Check sshpass - if ! command -v sshpass &>/dev/null; then - echo -e "${RED}ERROR: sshpass is required but not installed.${NC}" - echo "" - echo "Install on macOS:" - echo " brew install hudochenkov/sshpass/sshpass" - echo "" - echo "Install on Ubuntu/Debian:" - echo " sudo apt-get install sshpass" - echo "" - exit 1 - fi - - # Check provider-specific CLI - case "${provider}" in - aws) - if ! command -v aws &>/dev/null; then - missing+=("aws CLI") - elif ! aws sts get-caller-identity &>/dev/null; then - echo -e "${RED}ERROR: AWS CLI not authenticated. Run 'aws configure' first.${NC}" - exit 1 - fi - ;; - azure) - if ! command -v az &>/dev/null; then - missing+=("az CLI") - elif ! az account show &>/dev/null; then - echo -e "${RED}ERROR: Azure CLI not authenticated. Run 'az login' first.${NC}" - exit 1 - fi - ;; - gcp) - if ! command -v gcloud &>/dev/null; then - missing+=("gcloud CLI") - elif ! gcloud auth list --filter=status:ACTIVE --format="value(account)" 2>/dev/null \ - | head -1 \ - | grep -q '@'; then - echo -e "${RED}ERROR: GCP CLI not authenticated. Run 'gcloud auth login' first.${NC}" - exit 1 - fi - ;; - esac - - if [[ ${#missing[@]} -gt 0 ]]; then - echo -e "${RED}ERROR: Missing required tools: ${missing[*]}${NC}" - exit 1 - fi - - echo -e "${GREEN}Prerequisites OK${NC}" -} - -# ============================================================================= -# TERRAFORM OPERATIONS -# ============================================================================= - -# Get provider-specific terraform variable flags -# Arguments: -# $1 - Cloud provider name (aws, azure, gcp) -# Returns: -# Terraform variable flags via stdout -_get_provider_vars() { - local provider="$1" - - case "${provider}" in - azure) - local subscription_id - subscription_id=$(az account show --query id -o tsv) - echo "-var=subscription_id=${subscription_id}" - ;; - gcp) - local project_id - project_id=$(gcloud config get-value project 2>/dev/null) - if [[ -z "${project_id}" ]]; then - _log ERROR "No GCP project set. Run 'gcloud config set project PROJECT_ID'" - exit 1 - fi - echo "-var=gcp_project=${project_id}" - ;; - *) - # AWS and others don't need extra vars - echo "" - ;; - esac -} - -# Deploy infrastructure using Terraform -# Arguments: -# $1 - Cloud provider name (aws, azure, gcp) -_terraform_apply() { - local provider="$1" - local provider_dir="${REPO_ROOT}/${provider}" - local provider_vars - - _log INFO "Deploying ${provider} infrastructure..." - cd "${provider_dir}" - - # Use -upgrade to ensure latest compatible provider versions (avoids stale lock file issues) - terraform init -input=false -upgrade - - provider_vars=$(_get_provider_vars "${provider}") - # shellcheck disable=SC2086 - terraform apply -auto-approve ${provider_vars} -var="use_local_setup=true" - - cd - > /dev/null -} - -# Destroy infrastructure using Terraform -# Arguments: -# $1 - Cloud provider name (aws, azure, gcp) -_terraform_destroy() { - local provider="$1" - local provider_dir="${REPO_ROOT}/${provider}" - local provider_vars - - _log INFO "Destroying ${provider} infrastructure..." - cd "${provider_dir}" - - provider_vars=$(_get_provider_vars "${provider}") - # shellcheck disable=SC2086 - terraform destroy -auto-approve ${provider_vars} - - cd - > /dev/null -} - -# Get the public IP address of the deployed VM -# Arguments: -# $1 - Cloud provider name (aws, azure, gcp) -# Returns: -# Public IP address via stdout, or returns 1 on failure -_get_public_ip() { - local provider="$1" - local provider_dir="${REPO_ROOT}/${provider}" - local ip - - cd "${provider_dir}" - ip=$(terraform output -raw public_ip_address 2>/dev/null \ - || terraform output -raw public_ip 2>/dev/null \ - || echo "") - cd - > /dev/null - - # Validate IP format - if [[ ! "${ip}" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - _log ERROR "Invalid IP address retrieved: '${ip}'" - return 1 - fi - - echo "${ip}" -} - -# ============================================================================= -# VM OPERATIONS -# ============================================================================= - -# Wait for SSH to become available on a VM -# Arguments: -# $1 - IP address of the VM -# Returns: -# 0 on success, 1 on timeout -_wait_for_ssh() { - local ip="$1" - local attempt=1 - - _log INFO "Waiting for SSH to become available at ${ip}..." - - while [[ ${attempt} -le ${MAX_SSH_ATTEMPTS} ]]; do - # shellcheck disable=SC2086 - if _sshpass_cmd ssh ${SSH_OPTS} "${SSH_USER}@${ip}" "echo 'SSH OK'" &>/dev/null; then - _log OK "SSH is available" - return 0 - fi - echo " Attempt ${attempt}/${MAX_SSH_ATTEMPTS} - waiting..." - sleep "${SSH_RETRY_INTERVAL}" - ((attempt++)) - done - - _log ERROR "SSH connection timed out" - return 1 -} - -# Wait for setup markers before running challenge tests -# Arguments: -# $1 - IP address of the VM -# Returns: -# 0 on success, 1 on timeout -_wait_for_setup() { - local ip="$1" - local attempt=1 - - _log INFO "Waiting for CTF setup to finish..." - - while [[ ${attempt} -le ${MAX_SSH_ATTEMPTS} ]]; do - # shellcheck disable=SC2086 - if _sshpass_cmd ssh ${SSH_OPTS} "${SSH_USER}@${ip}" \ - "test -f /var/lib/linux-ctfs/setup.done || test -f /var/lib/cloud/instance/ctf-setup.done || test -f /var/log/setup_complete" &>/dev/null; then - _log OK "CTF setup is complete" - return 0 - fi - - # shellcheck disable=SC2086 - if _sshpass_cmd ssh ${SSH_OPTS} "${SSH_USER}@${ip}" \ - "test -f /var/lib/linux-ctfs/setup.failed" &>/dev/null; then - _log ERROR "CTF setup reported failure. Check /var/log/ctf_setup.log on the VM." - return 1 - fi - - echo " Attempt ${attempt}/${MAX_SSH_ATTEMPTS} - waiting..." - sleep "${SSH_RETRY_INTERVAL}" - ((attempt++)) - done - - _log ERROR "CTF setup timed out" - return 1 -} - -# Reboot/restart a VM and return the new IP address -# Arguments: -# $1 - Cloud provider name (aws, azure, gcp) -# $2 - Current IP address of the VM -# Returns: -# New IP address via stdout (may change for AWS) -_reboot_vm() { - local provider="$1" - local ip="$2" - - _log INFO "Rebooting VM (${provider})..." - - case "${provider}" in - aws) - local instance_id - instance_id=$(cd "${REPO_ROOT}/${provider}" \ - && terraform output -raw instance_id 2>/dev/null \ - || aws ec2 describe-instances \ - --filters "Name=ip-address,Values=${ip}" \ - --query 'Reservations[0].Instances[0].InstanceId' \ - --output text) - - # Validate instance ID - if [[ -z "${instance_id}" ]] || [[ "${instance_id}" == "None" ]]; then - _log ERROR "Failed to retrieve AWS instance ID for IP ${ip}" - return 1 - fi - - echo " Stopping instance ${instance_id}..." - aws ec2 stop-instances --instance-ids "${instance_id}" > /dev/null - aws ec2 wait instance-stopped --instance-ids "${instance_id}" - echo " Starting instance ${instance_id}..." - aws ec2 start-instances --instance-ids "${instance_id}" > /dev/null - aws ec2 wait instance-running --instance-ids "${instance_id}" - # IP may change, get new one - sleep 10 - ip=$(_get_public_ip "${provider}") - ;; - azure) - echo " Restarting Azure VM..." - az vm restart --resource-group ctf-resources --name ctf-vm - # az vm restart waits by default, but add explicit wait for running state - az vm wait \ - --resource-group ctf-resources \ - --name ctf-vm \ - --created \ - --timeout 120 2>/dev/null || true - ;; - gcp) - echo " Restarting GCP VM..." - local zone - zone=$(cd "${REPO_ROOT}/${provider}" \ - && terraform output -raw zone 2>/dev/null \ - || echo "us-central1-a") - gcloud compute instances reset ctf-instance --zone="${zone}" --quiet - # Wait for VM to be running - local attempts=0 - while [[ ${attempts} -lt 30 ]]; do - local status - status=$(gcloud compute instances describe ctf-instance \ - --zone="${zone}" \ - --format='value(status)' 2>/dev/null || echo "") - if [[ "${status}" == "RUNNING" ]]; then - break - fi - sleep 2 - ((attempts++)) - done - ;; - esac - - # Return new IP (may have changed for AWS) - echo "${ip}" -} - -# ============================================================================= -# TEST EXECUTION -# ============================================================================= - -# Copy test script to VM and execute it -# Arguments: -# $1 - Cloud provider name -# $2 - IP address of the VM -# Returns: -# Exit code from the test script (0=pass, 1=fail, 100=reboot requested) -_run_tests() { - local provider="$1" - local ip="$2" - local test_flags="" - - if [[ "${WITH_REBOOT}" == true ]]; then - test_flags="${test_flags} --with-reboot" - fi - - _log INFO "Copying test script to VM..." - # shellcheck disable=SC2086 - _sshpass_cmd scp ${SSH_OPTS} "${TEST_SCRIPT}" "${SSH_USER}@${ip}:/tmp/test_ctf_challenges.sh" - - _log INFO "Running tests on ${provider} VM (${ip})..." - echo "" - - local exit_code=0 - # shellcheck disable=SC2086 - _sshpass_cmd ssh ${SSH_OPTS} "${SSH_USER}@${ip}" \ - "chmod +x /tmp/test_ctf_challenges.sh && /tmp/test_ctf_challenges.sh ${test_flags}" \ - || exit_code=$? - - return "${exit_code}" -} - -# Run tests after VM reboot to verify services persist -# Arguments: -# $1 - Cloud provider name -# $2 - IP address of the VM -# Returns: -# Exit code from the test script -_run_post_reboot_tests() { - local provider="$1" - local ip="$2" - - _log INFO "Running post-reboot verification on ${provider}..." - - local exit_code=0 - # shellcheck disable=SC2086 - _sshpass_cmd ssh ${SSH_OPTS} "${SSH_USER}@${ip}" "/tmp/test_ctf_challenges.sh" \ - || exit_code=$? - - return "${exit_code}" -} - -# ============================================================================= -# MAIN TEST FLOW -# ============================================================================= - -# Run full test cycle for a single cloud provider -# Arguments: -# $1 - Cloud provider name (aws, azure, gcp) -# Returns: -# 0 on success, 1 on failure -_test_provider() { - local provider="$1" - local result=0 - - # Enable cleanup on interrupt for this provider - CURRENT_PROVIDER="${provider}" - CLEANUP_ON_EXIT=true - - echo "" - echo -e "${YELLOW}════════════════════════════════════════════════════════════════${NC}" - echo -e "${YELLOW} TESTING: ${provider}${NC}" - echo -e "${YELLOW}════════════════════════════════════════════════════════════════${NC}" - echo "" - - # Check prerequisites - _check_prerequisites "${provider}" - - # Deploy - if ! _terraform_apply "${provider}"; then - _log ERROR "Terraform apply failed for ${provider}" - CLEANUP_ON_EXIT=false - _terraform_destroy "${provider}" 2>/dev/null || true - CURRENT_PROVIDER="" - return 1 - fi - - # Get IP - local ip - if ! ip=$(_get_public_ip "${provider}"); then - _log ERROR "Failed to get valid IP address for ${provider}" - CLEANUP_ON_EXIT=false - _terraform_destroy "${provider}" 2>/dev/null || true - CURRENT_PROVIDER="" - return 1 - fi - _log OK "VM deployed at: ${ip}" - - # Wait for SSH - if ! _wait_for_ssh "${ip}"; then - _log ERROR "SSH connection failed for ${provider}" - CLEANUP_ON_EXIT=false - _terraform_destroy "${provider}" - CURRENT_PROVIDER="" - return 1 - fi - - # Wait for setup markers - if ! _wait_for_setup "${ip}"; then - _log ERROR "Setup did not complete for ${provider}" - CLEANUP_ON_EXIT=false - _terraform_destroy "${provider}" - CURRENT_PROVIDER="" - return 1 - fi - - # Run tests - local test_exit_code=0 - _run_tests "${provider}" "${ip}" || test_exit_code=$? - - # Handle reboot test - if [[ ${test_exit_code} -eq 100 ]] && [[ "${WITH_REBOOT}" == true ]]; then - echo "" - _log WARN "Reboot requested - performing VM reboot..." - - local new_ip - new_ip=$(_reboot_vm "${provider}" "${ip}") - - # Wait for SSH after reboot - _wait_for_ssh "${new_ip}" - - # Run post-reboot tests - _run_post_reboot_tests "${provider}" "${new_ip}" || test_exit_code=$? - elif [[ ${test_exit_code} -ne 0 ]]; then - result=1 - fi - - # Cleanup - echo "" - CLEANUP_ON_EXIT=false - _terraform_destroy "${provider}" - CURRENT_PROVIDER="" - - return "${result}" -} - -# ============================================================================= -# MAIN ENTRY POINT -# ============================================================================= - -# Main function - orchestrates testing across all specified providers -_main() { - local failed_providers=() - local passed_providers=() - - _log WARN "CTF Challenge Test Suite" - echo "Providers to test: ${PROVIDERS_TO_TEST[*]}" - echo "Reboot test: ${WITH_REBOOT}" - echo "" - - for provider in "${PROVIDERS_TO_TEST[@]}"; do - if _test_provider "${provider}"; then - passed_providers+=("${provider}") - else - failed_providers+=("${provider}") - fi - done - - # Final summary (short pass/fail) - echo "" - if [[ ${#failed_providers[@]} -gt 0 ]]; then - _log ERROR "RESULT: FAIL (${failed_providers[*]})" - exit 1 - fi - - _log OK "RESULT: PASS (${passed_providers[*]})" - exit 0 -} - -_main +#!/usr/bin/env bash +# shellcheck shell=bash +# +# CTF Deploy and Test Orchestration Script +# Deploys infrastructure to cloud providers and runs tests +# +# Usage: +# ./deploy_and_test.sh [--with-reboot] +# DEBUG=true ./deploy_and_test.sh azure # Enable debug tracing +# +# Arguments: +# aws|azure|gcp|all Cloud provider(s) to test +# --with-reboot After initial tests pass, stop/start the VM and +# re-run verification to ensure services survive +# reboot and progress persists +# +# Prerequisites: +# - terraform (>= 1.0) +# - jq (for AWS terraform config) +# - sshpass (macOS: brew install hudochenkov/sshpass/sshpass) +# - aws CLI (for AWS, must be logged in) +# - az CLI (for Azure, must be logged in) +# - gcloud CLI (for GCP, must be authenticated) +# +# Examples: +# ./deploy_and_test.sh aws # Test AWS only +# ./deploy_and_test.sh azure --with-reboot # Test Azure with reboot +# ./deploy_and_test.sh all # Test all providers +# ./deploy_and_test.sh all --with-reboot # Full test suite +# + +set -o errexit +set -o pipefail +set -o nounset + +# Enable debug tracing if DEBUG=true +[[ "${DEBUG:-}" == 'true' ]] && set -o xtrace + +# ============================================================================= +# CONSTANTS +# ============================================================================= + +# Script paths (declare and assign separately to avoid masking return values) +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly SCRIPT_DIR +REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)" +readonly REPO_ROOT +readonly TEST_SCRIPT="${SCRIPT_DIR}/test_ctf_challenges.sh" + +# SSH connection settings +readonly MAX_SSH_ATTEMPTS=30 +readonly SSH_RETRY_INTERVAL=10 +readonly SSH_USER="ctf_user" +readonly SSH_PASS="CTFpassword123!" +readonly SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR" + +# Terminal colors for output formatting +readonly RED='\033[0;31m' +readonly GREEN='\033[0;32m' +readonly YELLOW='\033[1;33m' +readonly BLUE='\033[0;34m' +readonly NC='\033[0m' # No Color + +# ============================================================================= +# GLOBAL STATE +# ============================================================================= + +# Cleanup tracking (mutable - set during test execution) +CURRENT_PROVIDER="" +CLEANUP_ON_EXIT=false + +# ============================================================================= +# UTILITY FUNCTIONS +# ============================================================================= + +# Log a message with timestamp and color based on level +# Arguments: +# $1 - Log level (INFO, OK, WARN, ERROR) +# $@ - Message to log +_log() { + local level="$1" + shift + local message="$*" + local timestamp + timestamp=$(date '+%H:%M:%S') + + case "${level}" in + INFO) echo -e "[${timestamp}] ${BLUE}${message}${NC}" ;; + OK) echo -e "[${timestamp}] ${GREEN}${message}${NC}" ;; + WARN) echo -e "[${timestamp}] ${YELLOW}${message}${NC}" ;; + ERROR) echo -e "[${timestamp}] ${RED}${message}${NC}" ;; + *) echo -e "[${timestamp}] ${level} ${message}" ;; + esac +} + +# Signal handler for cleanup on interrupt (SIGINT/SIGTERM) +# Destroys any in-progress infrastructure before exiting +_cleanup_handler() { + local exit_code=$? + + if [[ "${CLEANUP_ON_EXIT}" == true ]] && [[ -n "${CURRENT_PROVIDER}" ]]; then + echo "" + _log WARN "Caught interrupt - cleaning up ${CURRENT_PROVIDER} infrastructure..." + _terraform_destroy "${CURRENT_PROVIDER}" || true + fi + exit "${exit_code}" +} + +trap _cleanup_handler SIGINT SIGTERM + +# Execute sshpass command with password passed via file descriptor +# This hides the password from the process list +# Arguments: +# $@ - Command and arguments to pass to sshpass +_sshpass_cmd() { + sshpass -f <(printf '%s' "${SSH_PASS}") "$@" +} + +# ============================================================================= +# ARGUMENT PARSING +# ============================================================================= + +WITH_REBOOT=false +PROVIDERS_TO_TEST=() + +for arg in "$@"; do + case "${arg}" in + aws|azure|gcp) + PROVIDERS_TO_TEST+=("${arg}") + ;; + all) + PROVIDERS_TO_TEST=("aws" "azure" "gcp") + ;; + --with-reboot) + WITH_REBOOT=true + ;; + -h|--help) + head -32 "$0" | tail -30 + exit 0 + ;; + *) + echo "Unknown argument: ${arg}" + echo "Usage: $0 [--with-reboot]" + exit 1 + ;; + esac +done + +if [[ ${#PROVIDERS_TO_TEST[@]} -eq 0 ]]; then + echo "Usage: $0 [--with-reboot]" + exit 1 +fi + +# ============================================================================= +# PREREQUISITE CHECKS +# ============================================================================= + +# Verify all required tools are installed and authenticated for a provider +# Arguments: +# $1 - Cloud provider name (aws, azure, gcp) +_check_prerequisites() { + local provider="$1" + local missing=() + + echo -e "${BLUE}Checking prerequisites for ${provider}...${NC}" + + # Check terraform + if ! command -v terraform &>/dev/null; then + missing+=("terraform") + fi + + # Check jq (required for AWS terraform config) + if ! command -v jq &>/dev/null; then + missing+=("jq") + fi + + # Check sshpass + if ! command -v sshpass &>/dev/null; then + echo -e "${RED}ERROR: sshpass is required but not installed.${NC}" + echo "" + echo "Install on macOS:" + echo " brew install hudochenkov/sshpass/sshpass" + echo "" + echo "Install on Ubuntu/Debian:" + echo " sudo apt-get install sshpass" + echo "" + exit 1 + fi + + # Check provider-specific CLI + case "${provider}" in + aws) + if ! command -v aws &>/dev/null; then + missing+=("aws CLI") + elif ! aws sts get-caller-identity &>/dev/null; then + echo -e "${RED}ERROR: AWS CLI not authenticated. Run 'aws configure' first.${NC}" + exit 1 + fi + ;; + azure) + if ! command -v az &>/dev/null; then + missing+=("az CLI") + elif ! az account show &>/dev/null; then + echo -e "${RED}ERROR: Azure CLI not authenticated. Run 'az login' first.${NC}" + exit 1 + fi + ;; + gcp) + if ! command -v gcloud &>/dev/null; then + missing+=("gcloud CLI") + elif ! gcloud auth list --filter=status:ACTIVE --format="value(account)" 2>/dev/null \ + | head -1 \ + | grep -q '@'; then + echo -e "${RED}ERROR: GCP CLI not authenticated. Run 'gcloud auth login' first.${NC}" + exit 1 + fi + ;; + esac + + if [[ ${#missing[@]} -gt 0 ]]; then + echo -e "${RED}ERROR: Missing required tools: ${missing[*]}${NC}" + exit 1 + fi + + echo -e "${GREEN}Prerequisites OK${NC}" +} + +# ============================================================================= +# TERRAFORM OPERATIONS +# ============================================================================= + +# Get provider-specific terraform variable flags +# Arguments: +# $1 - Cloud provider name (aws, azure, gcp) +# Returns: +# Terraform variable flags via stdout +_get_provider_vars() { + local provider="$1" + + case "${provider}" in + azure) + local subscription_id + subscription_id=$(az account show --query id -o tsv) + echo "-var=subscription_id=${subscription_id}" + ;; + gcp) + local project_id + project_id=$(gcloud config get-value project 2>/dev/null) + if [[ -z "${project_id}" ]]; then + _log ERROR "No GCP project set. Run 'gcloud config set project PROJECT_ID'" + exit 1 + fi + echo "-var=gcp_project=${project_id}" + ;; + *) + # AWS and others don't need extra vars + echo "" + ;; + esac +} + +# Deploy infrastructure using Terraform +# Arguments: +# $1 - Cloud provider name (aws, azure, gcp) +_terraform_apply() { + local provider="$1" + local provider_dir="${REPO_ROOT}/${provider}" + local provider_vars + + _log INFO "Deploying ${provider} infrastructure..." + cd "${provider_dir}" + + # Use -upgrade to ensure latest compatible provider versions (avoids stale lock file issues) + terraform init -input=false -upgrade + + provider_vars=$(_get_provider_vars "${provider}") + # shellcheck disable=SC2086 + terraform apply -auto-approve ${provider_vars} -var="use_local_setup=true" + + cd - > /dev/null +} + +# Destroy infrastructure using Terraform +# Arguments: +# $1 - Cloud provider name (aws, azure, gcp) +_terraform_destroy() { + local provider="$1" + local provider_dir="${REPO_ROOT}/${provider}" + local provider_vars + + _log INFO "Destroying ${provider} infrastructure..." + cd "${provider_dir}" + + provider_vars=$(_get_provider_vars "${provider}") + # shellcheck disable=SC2086 + terraform destroy -auto-approve ${provider_vars} + + cd - > /dev/null +} + +# Get the public IP address of the deployed VM +# Arguments: +# $1 - Cloud provider name (aws, azure, gcp) +# Returns: +# Public IP address via stdout, or returns 1 on failure +_get_public_ip() { + local provider="$1" + local provider_dir="${REPO_ROOT}/${provider}" + local ip + + cd "${provider_dir}" + ip=$(terraform output -raw public_ip_address 2>/dev/null \ + || terraform output -raw public_ip 2>/dev/null \ + || echo "") + cd - > /dev/null + + # Validate IP format + if [[ ! "${ip}" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + _log ERROR "Invalid IP address retrieved: '${ip}'" + return 1 + fi + + echo "${ip}" +} + +# ============================================================================= +# VM OPERATIONS +# ============================================================================= + +# Wait for SSH to become available on a VM +# Arguments: +# $1 - IP address of the VM +# Returns: +# 0 on success, 1 on timeout +_wait_for_ssh() { + local ip="$1" + local attempt=1 + + _log INFO "Waiting for SSH to become available at ${ip}..." + + while [[ ${attempt} -le ${MAX_SSH_ATTEMPTS} ]]; do + # shellcheck disable=SC2086 + if _sshpass_cmd ssh ${SSH_OPTS} "${SSH_USER}@${ip}" "echo 'SSH OK'" &>/dev/null; then + _log OK "SSH is available" + return 0 + fi + echo " Attempt ${attempt}/${MAX_SSH_ATTEMPTS} - waiting..." + sleep "${SSH_RETRY_INTERVAL}" + ((attempt++)) + done + + _log ERROR "SSH connection timed out" + return 1 +} + +# Wait for setup markers before running challenge tests +# Arguments: +# $1 - IP address of the VM +# Returns: +# 0 on success, 1 on timeout +_wait_for_setup() { + local ip="$1" + local attempt=1 + + _log INFO "Waiting for CTF setup to finish..." + + while [[ ${attempt} -le ${MAX_SSH_ATTEMPTS} ]]; do + # shellcheck disable=SC2086 + if _sshpass_cmd ssh ${SSH_OPTS} "${SSH_USER}@${ip}" \ + "test -f /var/lib/linux-ctfs/setup.done || test -f /var/lib/cloud/instance/ctf-setup.done || test -f /var/log/setup_complete" &>/dev/null; then + _log OK "CTF setup is complete" + return 0 + fi + + # shellcheck disable=SC2086 + if _sshpass_cmd ssh ${SSH_OPTS} "${SSH_USER}@${ip}" \ + "test -f /var/lib/linux-ctfs/setup.failed" &>/dev/null; then + _log ERROR "CTF setup reported failure. Check /var/log/ctf_setup.log on the VM." + return 1 + fi + + echo " Attempt ${attempt}/${MAX_SSH_ATTEMPTS} - waiting..." + sleep "${SSH_RETRY_INTERVAL}" + ((attempt++)) + done + + _log ERROR "CTF setup timed out" + return 1 +} + +# Reboot/restart a VM and return the new IP address +# Arguments: +# $1 - Cloud provider name (aws, azure, gcp) +# $2 - Current IP address of the VM +# Returns: +# New IP address via stdout (may change for AWS) +_reboot_vm() { + local provider="$1" + local ip="$2" + + _log INFO "Rebooting VM (${provider})..." + + case "${provider}" in + aws) + local instance_id + instance_id=$(cd "${REPO_ROOT}/${provider}" \ + && terraform output -raw instance_id 2>/dev/null \ + || aws ec2 describe-instances \ + --filters "Name=ip-address,Values=${ip}" \ + --query 'Reservations[0].Instances[0].InstanceId' \ + --output text) + + # Validate instance ID + if [[ -z "${instance_id}" ]] || [[ "${instance_id}" == "None" ]]; then + _log ERROR "Failed to retrieve AWS instance ID for IP ${ip}" + return 1 + fi + + echo " Stopping instance ${instance_id}..." + aws ec2 stop-instances --instance-ids "${instance_id}" > /dev/null + aws ec2 wait instance-stopped --instance-ids "${instance_id}" + echo " Starting instance ${instance_id}..." + aws ec2 start-instances --instance-ids "${instance_id}" > /dev/null + aws ec2 wait instance-running --instance-ids "${instance_id}" + # IP may change, get new one + sleep 10 + ip=$(_get_public_ip "${provider}") + ;; + azure) + echo " Restarting Azure VM..." + az vm restart --resource-group ctf-resources --name ctf-vm + # az vm restart waits by default, but add explicit wait for running state + az vm wait \ + --resource-group ctf-resources \ + --name ctf-vm \ + --created \ + --timeout 120 2>/dev/null || true + ;; + gcp) + echo " Restarting GCP VM..." + local zone + zone=$(cd "${REPO_ROOT}/${provider}" \ + && terraform output -raw zone 2>/dev/null \ + || echo "us-central1-a") + gcloud compute instances reset ctf-instance --zone="${zone}" --quiet + # Wait for VM to be running + local attempts=0 + while [[ ${attempts} -lt 30 ]]; do + local status + status=$(gcloud compute instances describe ctf-instance \ + --zone="${zone}" \ + --format='value(status)' 2>/dev/null || echo "") + if [[ "${status}" == "RUNNING" ]]; then + break + fi + sleep 2 + ((attempts++)) + done + ;; + esac + + # Return new IP (may have changed for AWS) + echo "${ip}" +} + +# ============================================================================= +# TEST EXECUTION +# ============================================================================= + +# Copy test script to VM and execute it +# Arguments: +# $1 - Cloud provider name +# $2 - IP address of the VM +# Returns: +# Exit code from the test script (0=pass, 1=fail, 100=reboot requested) +_run_tests() { + local provider="$1" + local ip="$2" + local test_flags="" + + if [[ "${WITH_REBOOT}" == true ]]; then + test_flags="${test_flags} --with-reboot" + fi + + _log INFO "Copying test script to VM..." + # shellcheck disable=SC2086 + _sshpass_cmd scp ${SSH_OPTS} "${TEST_SCRIPT}" "${SSH_USER}@${ip}:/tmp/test_ctf_challenges.sh" + + _log INFO "Running tests on ${provider} VM (${ip})..." + echo "" + + local exit_code=0 + # shellcheck disable=SC2086 + _sshpass_cmd ssh ${SSH_OPTS} "${SSH_USER}@${ip}" \ + "chmod +x /tmp/test_ctf_challenges.sh && /tmp/test_ctf_challenges.sh ${test_flags}" \ + || exit_code=$? + + return "${exit_code}" +} + +# Run tests after VM reboot to verify services persist +# Arguments: +# $1 - Cloud provider name +# $2 - IP address of the VM +# Returns: +# Exit code from the test script +_run_post_reboot_tests() { + local provider="$1" + local ip="$2" + + _log INFO "Running post-reboot verification on ${provider}..." + + local exit_code=0 + # shellcheck disable=SC2086 + _sshpass_cmd ssh ${SSH_OPTS} "${SSH_USER}@${ip}" "/tmp/test_ctf_challenges.sh" \ + || exit_code=$? + + return "${exit_code}" +} + +# ============================================================================= +# MAIN TEST FLOW +# ============================================================================= + +# Run full test cycle for a single cloud provider +# Arguments: +# $1 - Cloud provider name (aws, azure, gcp) +# Returns: +# 0 on success, 1 on failure +_test_provider() { + local provider="$1" + local result=0 + + # Enable cleanup on interrupt for this provider + CURRENT_PROVIDER="${provider}" + CLEANUP_ON_EXIT=true + + echo "" + echo -e "${YELLOW}════════════════════════════════════════════════════════════════${NC}" + echo -e "${YELLOW} TESTING: ${provider}${NC}" + echo -e "${YELLOW}════════════════════════════════════════════════════════════════${NC}" + echo "" + + # Check prerequisites + _check_prerequisites "${provider}" + + # Deploy + if ! _terraform_apply "${provider}"; then + _log ERROR "Terraform apply failed for ${provider}" + CLEANUP_ON_EXIT=false + _terraform_destroy "${provider}" 2>/dev/null || true + CURRENT_PROVIDER="" + return 1 + fi + + # Get IP + local ip + if ! ip=$(_get_public_ip "${provider}"); then + _log ERROR "Failed to get valid IP address for ${provider}" + CLEANUP_ON_EXIT=false + _terraform_destroy "${provider}" 2>/dev/null || true + CURRENT_PROVIDER="" + return 1 + fi + _log OK "VM deployed at: ${ip}" + + # Wait for SSH + if ! _wait_for_ssh "${ip}"; then + _log ERROR "SSH connection failed for ${provider}" + CLEANUP_ON_EXIT=false + _terraform_destroy "${provider}" + CURRENT_PROVIDER="" + return 1 + fi + + # Wait for setup markers + if ! _wait_for_setup "${ip}"; then + _log ERROR "Setup did not complete for ${provider}" + CLEANUP_ON_EXIT=false + _terraform_destroy "${provider}" + CURRENT_PROVIDER="" + return 1 + fi + + # Run tests + local test_exit_code=0 + _run_tests "${provider}" "${ip}" || test_exit_code=$? + + # Handle reboot test + if [[ ${test_exit_code} -eq 100 ]] && [[ "${WITH_REBOOT}" == true ]]; then + echo "" + _log WARN "Reboot requested - performing VM reboot..." + + local new_ip + new_ip=$(_reboot_vm "${provider}" "${ip}") + + # Wait for SSH after reboot + _wait_for_ssh "${new_ip}" + + # Run post-reboot tests + _run_post_reboot_tests "${provider}" "${new_ip}" || test_exit_code=$? + elif [[ ${test_exit_code} -ne 0 ]]; then + result=1 + fi + + # Cleanup + echo "" + CLEANUP_ON_EXIT=false + _terraform_destroy "${provider}" + CURRENT_PROVIDER="" + + return "${result}" +} + +# ============================================================================= +# MAIN ENTRY POINT +# ============================================================================= + +# Main function - orchestrates testing across all specified providers +_main() { + local failed_providers=() + local passed_providers=() + + _log WARN "CTF Challenge Test Suite" + echo "Providers to test: ${PROVIDERS_TO_TEST[*]}" + echo "Reboot test: ${WITH_REBOOT}" + echo "" + + for provider in "${PROVIDERS_TO_TEST[@]}"; do + if _test_provider "${provider}"; then + passed_providers+=("${provider}") + else + failed_providers+=("${provider}") + fi + done + + # Final summary (short pass/fail) + echo "" + if [[ ${#failed_providers[@]} -gt 0 ]]; then + _log ERROR "RESULT: FAIL (${failed_providers[*]})" + exit 1 + fi + + _log OK "RESULT: PASS (${passed_providers[*]})" + exit 0 +} + +_main diff --git a/.github/skills/ctf-testing/test_ctf_challenges.sh b/.github/skills/ctf-testing/test_ctf_challenges.sh index 4ccb182..25a29dc 100644 --- a/.github/skills/ctf-testing/test_ctf_challenges.sh +++ b/.github/skills/ctf-testing/test_ctf_challenges.sh @@ -1,659 +1,659 @@ -#!/usr/bin/env bash -# shellcheck shell=bash -# -# CTF Challenge Test Script -# Runs on the VM to validate all challenges are solvable by students -# -# This script simulates a real user journey - discovering and solving each -# challenge using only the hints provided. If these tests pass, students -# can complete the CTF. -# -# Usage: -# ./test_ctf_challenges.sh [--with-reboot] -# DEBUG=true ./test_ctf_challenges.sh # Enable debug tracing -# -# Flags: -# --with-reboot After tests pass, signal reboot to verify services persist -# -# Exit codes: -# 0 - All tests passed -# 1 - One or more tests failed -# 100 - Reboot requested (only with --with-reboot flag) -# - -set -o errexit -set -o pipefail -set -o nounset - -# Enable debug tracing if DEBUG=true -[[ "${DEBUG:-}" == 'true' ]] && set -o xtrace - -# Ensure verify command is available -# It's installed in /usr/local/bin by ctf_setup.sh -if ! command -v verify &>/dev/null; then - export PATH="/usr/local/bin:$PATH" -fi - -# ============================================================================= -# CONSTANTS -# ============================================================================= - -# Terminal colors for output formatting -readonly RED='\033[0;31m' -readonly GREEN='\033[0;32m' -readonly YELLOW='\033[1;33m' -readonly NC='\033[0m' # No Color - -# File paths for reboot test coordination -readonly REBOOT_MARKER="/tmp/.ctf_reboot_test_marker" -readonly PROGRESS_SNAPSHOT="/tmp/.ctf_progress_snapshot" - -# ============================================================================= -# GLOBAL STATE -# ============================================================================= - -# Test result counters (mutable) -PASSED=0 -FAILED=0 - -# Parse arguments -WITH_REBOOT=false -for arg in "$@"; do - case $arg in - --with-reboot) - WITH_REBOOT=true - shift - ;; - esac -done - -# ============================================================================= -# HELPER FUNCTIONS -# ============================================================================= - -# Log a passing test result and increment counter -# Arguments: -# $1 - Message describing what passed -_pass() { - local message="${1}" - echo -e "${GREEN}✓ PASS${NC}: ${message}" - ((PASSED++)) || true -} - -# Log a failing test result and increment counter -# Arguments: -# $1 - Message describing what failed -_fail() { - local message="${1}" - echo -e "${RED}✗ FAIL${NC}: ${message}" - ((FAILED++)) || true -} - -# Print a section header for visual separation in output -# Arguments: -# $1 - Section title to display -_section() { - local title="${1}" - echo "" - echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" - echo -e "${YELLOW}${title}${NC}" - echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" -} - -# Verify a flag with the verify command and record result -# Arguments: -# $1 - Challenge number -# $2 - Flag value to verify -# $3 - Success message (optional, defaults to "Solved challenge N") -# $4 - Failure message (optional, defaults to "Found flag but verify rejected it") -# Returns: -# 0 if flag was verified successfully, 1 otherwise -_verify_flag() { - local challenge_num="${1}" - local flag_value="${2}" - local success_msg="${3:-Solved challenge ${challenge_num}}" - local fail_msg="${4:-Challenge ${challenge_num}: Found flag but verify rejected it}" - local verify_out - - verify_out=$(verify "${challenge_num}" "${flag_value}" 2>&1) || true - if echo "${verify_out}" | grep -qE "(Correct|verified)"; then - _pass "${success_msg}" - FLAGS[${challenge_num}]="${flag_value}" - return 0 - else - _fail "${fail_msg}" - FLAGS[${challenge_num}]="" - return 1 - fi -} - -# ============================================================================ -# POST-REBOOT VERIFICATION -# ============================================================================ -if [[ -f "${REBOOT_MARKER}" ]]; then - _section "POST-REBOOT VERIFICATION" - - echo "Verifying services survived reboot..." - - for service in ctf-secret-service ctf-monitor-directory ctf-ping-message ctf-secret-process nginx; do - if systemctl is-active "${service}" &>/dev/null; then - _pass "${service} is running after reboot" - else - _fail "${service} failed to start after reboot - SETUP BUG" - fi - done - - if [ -f "$PROGRESS_SNAPSHOT" ]; then - EXPECTED=$(cat "$PROGRESS_SNAPSHOT") - ACTUAL=$(sort -u /var/ctf/completed_challenges 2>/dev/null | wc -l) - if [ "$ACTUAL" -ge "$EXPECTED" ]; then - _pass "Progress persisted after reboot ($ACTUAL checks)" - else - _fail "Progress lost after reboot (expected ${EXPECTED}, got ${ACTUAL})" - fi - fi - - rm -f "${REBOOT_MARKER}" "${PROGRESS_SNAPSHOT}" - - echo "" - echo "Passed: ${PASSED} | Failed: ${FAILED}" - [[ ${FAILED} -eq 0 ]] && exit 0 || exit 1 -fi - -# ============================================================================ -# VERIFY COMMAND SANITY CHECK -# ============================================================================ -_section "VERIFY COMMAND SANITY CHECK" - -# Quick check that the verify command works at all -if ! command -v verify &>/dev/null; then - _fail "verify command not found in PATH" - echo "PATH: ${PATH}" - echo "Looking for verify: $(which verify 2>&1 || echo 'not found')" - echo "Checking /usr/local/bin: $(ls -la /usr/local/bin/verify 2>&1 || echo 'not found')" - exit 1 -fi - -# Timer should not start before first numeric verify command -rm -f /var/ctf/ctf_start_time /var/ctf/ctf_end_time -TIMER_PRESTART_OUT=$(verify time 2>&1) || true -if echo "${TIMER_PRESTART_OUT}" | grep -q "Timer not started"; then - _pass "verify time shows pre-start message" -else - _fail "verify time pre-start behavior incorrect" -fi - -VERIFY_OUTPUT=$(verify 0 "CTF{example}" 2>&1) || true -if echo "${VERIFY_OUTPUT}" | grep -q "✓"; then - _pass "verify command accepts example flag" -else - _fail "verify command broken - SETUP BUG" - echo "Cannot continue without working verify command" - exit 1 -fi - -if echo "${VERIFY_OUTPUT}" | grep -q "1/19"; then - _pass "verify progress counts the example check" -else - _fail "verify progress did not show 1/19 after the example check" -fi - -if [[ -f /var/ctf/ctf_start_time ]]; then - _pass "Timer starts after first numeric verify command" -else - _fail "Timer did not start after first numeric verify command" -fi - -# ============================================================================ -# CHALLENGE DISCOVERY AND SOLVING -# ============================================================================ -_section "SOLVING ALL CHALLENGES" - -echo "Simulating real student journey using hints to discover and solve each challenge..." -echo "" - -# Store discovered flags -declare -A FLAGS - -# Challenge 1: Hidden File Discovery -# Hint: "Hidden files in Linux start with a dot. Try 'ls -la'" -echo "Challenge 1: Hidden File Discovery" -HIDDEN_FILE=$(ls -la /home/ctf_user/ctf_challenges/ 2>/dev/null \ - | awk '/^-.*\./ {print $NF}' \ - | grep '^\.' \ - | head -1) || true -if [[ -n "${HIDDEN_FILE}" ]]; then - FLAG_1=$(cat "/home/ctf_user/ctf_challenges/${HIDDEN_FILE}" 2>/dev/null \ - | grep -ao 'CTF{[^}]*}' \ - | head -1) || true - if [[ -n "${FLAG_1}" ]]; then - _verify_flag 1 "${FLAG_1}" - else - _fail "Challenge 1: Found file but no CTF flag in it" - FLAGS[1]="" - fi -else - _fail "Challenge 1: No hidden files found with ls -la" - FLAGS[1]="" -fi - -# Challenge 2: Basic File Search -# Hint: "Use find to search for files. Try: find ~ -name '*.txt'" -echo "Challenge 2: Basic File Search" -TXT_FILE=$(find /home/ctf_user/documents -name '*.txt' -type f 2>/dev/null | head -1) || true -if [[ -n "${TXT_FILE}" ]]; then - FLAG_2=$(cat "${TXT_FILE}" 2>/dev/null | grep -ao 'CTF{[^}]*}' | head -1) || true - if [[ -n "${FLAG_2}" ]]; then - _verify_flag 2 "${FLAG_2}" - else - _fail "Challenge 2: Found file but no CTF flag in it" - FLAGS[2]="" - fi -else - _fail "Challenge 2: No .txt files found in documents" - FLAGS[2]="" -fi - -# Challenge 3: Log Analysis -# Hint: "Large log files can hide secrets. Check /var/log and use 'tail'" -echo "Challenge 3: Log Analysis" -LARGE_LOG=$(find /var/log -type f -size +100M 2>/dev/null | head -1) || true -if [[ -n "${LARGE_LOG}" ]]; then - FLAG_3=$(tail -1 "${LARGE_LOG}" 2>/dev/null | grep -ao 'CTF{[^}]*}' | head -1) || true - if [[ -n "${FLAG_3}" ]]; then - _verify_flag 3 "${FLAG_3}" - else - _fail "Challenge 3: Found log but no CTF flag in it" - FLAGS[3]="" - fi -else - _fail "Challenge 3: No large log files found" - FLAGS[3]="" -fi - -# Challenge 4: User Investigation -# Hint: "Investigate other users. Check /etc/passwd or use 'getent passwd'" -echo "Challenge 4: User Investigation" -FLAG_4="" -for user in $(getent passwd | awk -F: '$3 >= 1000 && $1 != "ctf_user" && $1 != "nobody" {print $1}'); do - if [[ -r "/home/${user}/.profile" ]]; then - FLAG_4=$(grep -ao 'CTF{[^}]*}' "/home/${user}/.profile" 2>/dev/null | head -1) || true - [[ -n "${FLAG_4}" ]] && break - fi -done -if [[ -n "${FLAG_4}" ]]; then - _verify_flag 4 "${FLAG_4}" -else - _fail "Challenge 4: Could not find flag in user profiles" - FLAGS[4]="" -fi - -# Challenge 5: Permission Analysis -# Hint: "Look for files with unusual permissions. Try: find / -perm 777" -echo "Challenge 5: Permission Analysis" -FLAG_5="" -for path in /opt /etc /var; do - PERM_FILE=$(find "${path}" -type f -perm 777 2>/dev/null | head -1) || true - if [[ -n "${PERM_FILE}" ]]; then - FLAG_5=$(cat "${PERM_FILE}" 2>/dev/null | grep -ao 'CTF{[^}]*}' | head -1) || true - [[ -n "${FLAG_5}" ]] && break - fi -done -if [[ -n "${FLAG_5}" ]]; then - _verify_flag 5 "${FLAG_5}" -else - _fail "Challenge 5: Could not find flag in 777 permission files" - FLAGS[5]="" -fi - -# Challenge 6: Service Discovery -# Hint: "What services are running? Use 'ss -tulpn' to find listening ports" -echo "Challenge 6: Service Discovery" -FLAG_6="" -for port in $(ss -tulpn 2>/dev/null \ - | awk '/LISTEN/ {split($5,a,":"); print a[length(a)]}' \ - | grep -vE '^(22|80|443|8083)$' \ - | head -3); do - FLAG_6=$(curl -s --connect-timeout 3 "localhost:${port}" 2>/dev/null \ - | grep -ao 'CTF{[^}]*}' \ - | head -1) || true - [[ -n "${FLAG_6}" ]] && break -done -if [[ -n "${FLAG_6}" ]]; then - _verify_flag 6 "${FLAG_6}" -else - _fail "Challenge 6: Could not find flag from listening services" - FLAGS[6]="" -fi - -# Challenge 7: Encoding Challenge -# Hint: "The flag is encoded. Use 'base64 -d' to decode" -echo "Challenge 7: Encoding Challenge" -ENCODED_FILE=$(find /home/ctf_user/ctf_challenges -name '*.txt' -type f 2>/dev/null | head -1) || true -if [[ -n "${ENCODED_FILE}" ]]; then - FLAG_7=$(cat "${ENCODED_FILE}" 2>/dev/null \ - | base64 -d 2>/dev/null \ - | base64 -d 2>/dev/null \ - | grep -ao 'CTF{[^}]*}' \ - | head -1) || true - if [[ -n "${FLAG_7}" ]]; then - _verify_flag 7 "${FLAG_7}" - else - _fail "Challenge 7: Could not decode flag from file" - FLAGS[7]="" - fi -else - _fail "Challenge 7: No encoded file found" - FLAGS[7]="" -fi - -# Challenge 8: SSH Secrets -# Hint: "SSH configurations often hide secrets. Explore ~/.ssh thoroughly" -echo "Challenge 8: SSH Secrets" -FLAG_8="" -while IFS= read -r -d '' f; do - FLAG_8=$(grep -ao 'CTF{[^}]*}' "${f}" 2>/dev/null | head -1) || true - [[ -n "${FLAG_8}" ]] && break -done < <(find /home/ctf_user/.ssh -type f -print0 2>/dev/null) -if [[ -n "${FLAG_8}" ]]; then - _verify_flag 8 "${FLAG_8}" -else - _fail "Challenge 8: Could not find flag in .ssh directory" - FLAGS[8]="" -fi - -# Challenge 9: DNS Inspection -# Hint: "Inspect systemd-resolved configuration safely" -echo "Challenge 9: DNS Inspection" -DNS_DROP_IN="/etc/systemd/resolved.conf.d/ctf-dns.conf" -if [[ -r "${DNS_DROP_IN}" ]]; then - FLAG_9=$(grep -ao 'CTF{[^}]*}' "${DNS_DROP_IN}" 2>/dev/null | head -1) || true - if [[ -n "${FLAG_9}" ]]; then - _verify_flag 9 "${FLAG_9}" "Solved challenge 9" "Challenge 9: Found flag but verify rejected it - SETUP BUG" - else - _fail "Challenge 9: DNS drop-in has no CTF flag - SETUP BUG" - FLAGS[9]="" - fi -else - _fail "Challenge 9: DNS drop-in not readable - SETUP BUG" - FLAGS[9]="" -fi - -# Challenge 10: File Monitoring -# Hint: "Try creating a file in ctf_challenges" -echo "Challenge 10: File Monitoring" -if ! systemctl is-active ctf-monitor-directory.service &>/dev/null; then - _fail "Challenge 10: Monitor service not running - SETUP BUG" - FLAGS[10]="" -else - # Wait for inotifywait process to actually be running (service starts but has internal delay) - echo " Waiting for inotifywait to be ready..." - for _ in {1..15}; do - pgrep -f "inotifywait.*ctf_challenges" &>/dev/null && break - sleep 2 - done - - true > /tmp/.ctf_upload_triggered 2>/dev/null || true - TRIGGER="/home/ctf_user/ctf_challenges/test_$$" - touch "${TRIGGER}" - sleep 3 - - FLAG_10="" - for _ in {1..10}; do - FLAG_10=$(grep -ao 'CTF{[^}]*}' /tmp/.ctf_upload_triggered 2>/dev/null | head -1) || true - [[ -n "${FLAG_10}" ]] && break - sleep 2 - done - rm -f "${TRIGGER}" - - if [[ -n "${FLAG_10}" ]]; then - _verify_flag 10 "${FLAG_10}" "Solved challenge 10" "Challenge 10: Found flag but verify rejected it - SETUP BUG" - else - _fail "Challenge 10: File monitoring did not trigger - SETUP BUG" - FLAGS[10]="" - fi -fi - -# Challenge 11: Web Configuration -# Hint: "Check what ports nginx is listening on" -echo "Challenge 11: Web Configuration" -NGINX_PORT=$(grep -r 'listen' /etc/nginx/ 2>/dev/null \ - | grep -oP 'listen\s+\K[0-9]+' \ - | grep -v '^80$' \ - | head -1) || true -if [[ -n "${NGINX_PORT}" ]]; then - FLAG_11=$(curl -s "localhost:${NGINX_PORT}" 2>/dev/null \ - | grep -ao 'CTF{[^}]*}' \ - | head -1) || true - if [[ -n "${FLAG_11}" ]]; then - _verify_flag 11 "${FLAG_11}" - else - _fail "Challenge 11: Could not get flag from nginx" - FLAGS[11]="" - fi -else - _fail "Challenge 11: Could not find nginx non-standard port" - FLAGS[11]="" -fi - -# Challenge 12: Network Traffic Analysis -# Hint: "Look at ping patterns with tcpdump" -echo "Challenge 12: Network Traffic Analysis" -TCPDUMP_OUT=$(echo 'CTFpassword123!' \ - | sudo -S timeout 10 tcpdump -i lo -c 4 -X icmp 2>/dev/null) || true -if [[ -n "${TCPDUMP_OUT}" ]]; then - HEX=$(echo "${TCPDUMP_OUT}" \ - | grep -E '^\s+0x' \ - | awk '{print $2$3$4$5$6$7$8$9}' \ - | tr -d '\n') - FLAG_12=$(echo "${HEX}" | xxd -r -p 2>/dev/null | grep -ao 'CTF{[^}]*}' | head -1) || true - - if [[ -n "${FLAG_12}" ]]; then - _verify_flag 12 "${FLAG_12}" - else - _fail "Challenge 12: Could not extract flag from ping traffic" - FLAGS[12]="" - fi -else - _fail "Challenge 12: tcpdump capture failed" - FLAGS[12]="" -fi - -# Challenge 13: Cron Job Hunter -# Hint: "Check /etc/cron.d/, /etc/crontab, and user crontabs" -echo "Challenge 13: Cron Job Hunter" -FLAG_13="" -for dir in /etc/cron.d /etc/cron.daily /etc/cron.hourly; do - [[ -d "${dir}" ]] || continue - FLAG_13=$(grep -rh 'CTF{' "${dir}" 2>/dev/null | grep -ao 'CTF{[^}]*}' | head -1) || true - [[ -n "${FLAG_13}" ]] && break -done -if [[ -n "${FLAG_13}" ]]; then - _verify_flag 13 "${FLAG_13}" -else - _fail "Challenge 13: Could not find flag in cron directories" - FLAGS[13]="" -fi - -# Challenge 14: Process Environment -# Hint: "Process info lives in /proc. Check /proc/PID/environ" -echo "Challenge 14: Process Environment" -FLAG_14="" -for pid in $(pgrep -u ctf_user 2>/dev/null); do - [[ -r "/proc/${pid}/environ" ]] || continue - FLAG_14=$(tr '\0' '\n' < "/proc/${pid}/environ" 2>/dev/null | grep -ao 'CTF{[^}]*}') || true - [[ -n "${FLAG_14}" ]] && break -done -if [[ -n "${FLAG_14}" ]]; then - _verify_flag 14 "${FLAG_14}" -else - _fail "Challenge 14: Could not find flag in process environments" - FLAGS[14]="" -fi - -# Challenge 15: Archive Archaeologist -# Hint: "Archives can be nested. Use 'tar -xzf' to extract layers" -echo "Challenge 15: Archive Archaeologist" -ARCHIVE=$(find /home/ctf_user/ctf_challenges -name '*.tar.gz' 2>/dev/null | head -1) || true -if [[ -n "${ARCHIVE}" ]]; then - TMPDIR=$(mktemp -d) - cd "${TMPDIR}" - tar -xzf "${ARCHIVE}" 2>/dev/null || true - for _ in {1..5}; do - INNER=$(find . -maxdepth 1 -name '*.tar.gz' 2>/dev/null | head -1) || true - [[ -z "${INNER}" ]] && break - tar -xzf "${INNER}" 2>/dev/null || true - rm -f "${INNER}" - done - FLAG_15=$(grep -rh 'CTF{' . 2>/dev/null | grep -ao 'CTF{[^}]*}' | head -1) || true - cd - >/dev/null - rm -rf "${TMPDIR}" - - if [[ -n "${FLAG_15}" ]]; then - _verify_flag 15 "${FLAG_15}" - else - _fail "Challenge 15: Could not find flag in nested archives" - FLAGS[15]="" - fi -else - _fail "Challenge 15: No archive found" - FLAGS[15]="" -fi - -# Challenge 16: Symbolic Sleuth -# Hint: "Use 'readlink -f' to find the final target" -echo "Challenge 16: Symbolic Sleuth" -FLAG_16="" -while IFS= read -r -d '' link; do - TARGET=$(readlink -f "${link}" 2>/dev/null) || true - [[ -r "${TARGET}" ]] || continue - FLAG_16=$(grep -ao 'CTF{[^}]*}' "${TARGET}" 2>/dev/null | head -1) || true - [[ -n "${FLAG_16}" ]] && break -done < <(find /home/ctf_user/ctf_challenges -type l -print0 2>/dev/null) -if [[ -n "${FLAG_16}" ]]; then - _verify_flag 16 "${FLAG_16}" -else - _fail "Challenge 16: Could not find flag via symlinks" - FLAGS[16]="" -fi - -# Challenge 17: History Mystery -# Hint: "Bash stores history in ~/.bash_history. Other users may have history too" -echo "Challenge 17: History Mystery" -FLAG_17="" -for home in /home/*; do - user=$(basename "${home}") - [[ "${user}" == "ctf_user" ]] && continue - [[ -r "${home}/.bash_history" ]] || continue - FLAG_17=$(grep -ao 'CTF{[^}]*}' "${home}/.bash_history" 2>/dev/null | head -1) || true - [[ -n "${FLAG_17}" ]] && break -done -if [[ -n "${FLAG_17}" ]]; then - _verify_flag 17 "${FLAG_17}" -else - _fail "Challenge 17: Could not find flag in user histories" - FLAGS[17]="" -fi - -# Challenge 18: Disk Detective -# Hint: "Try mounting disk images with 'sudo mount -o loop'" -echo "Challenge 18: Disk Detective" -DISK_IMG=$(find /opt /home -name '*.img' -type f 2>/dev/null | head -1) || true -if [[ -n "${DISK_IMG}" ]]; then - MNTDIR=$(mktemp -d) - echo 'CTFpassword123!' | sudo -S mount -o loop "${DISK_IMG}" "${MNTDIR}" 2>/dev/null - FLAG_18=$(find "${MNTDIR}" -type f -print0 2>/dev/null \ - | xargs -0 grep -ah 'CTF{' 2>/dev/null \ - | grep -ao 'CTF{[^}]*}' \ - | head -1) || true - echo 'CTFpassword123!' | sudo -S umount "${MNTDIR}" 2>/dev/null || true - rmdir "${MNTDIR}" 2>/dev/null || true - - if [[ -n "${FLAG_18}" ]]; then - _verify_flag 18 "${FLAG_18}" - else - _fail "Challenge 18: Could not find flag in disk image" - FLAGS[18]="" - fi -else - _fail "Challenge 18: No disk image found" - FLAGS[18]="" -fi - -# ============================================================================ -# VERIFICATION TOKEN TEST -# ============================================================================ -_section "VERIFICATION TOKEN TEST" - -PROGRESS=$(verify progress 2>&1) -if echo "${PROGRESS}" | grep -q "19/19"; then - _pass "All 19 progress checks completed" -else - _fail "Not all progress checks completed: ${PROGRESS}" -fi - -EXPORT_OUT=$(verify export testuser 2>&1) || true - -if echo "${EXPORT_OUT}" | grep -q "COMPLETION CERTIFICATE"; then - _pass "Export generates certificate" -else - _fail "Export missing certificate" -fi - -if echo "${EXPORT_OUT}" | grep -q "BEGIN L2C CTF TOKEN"; then - _pass "Export generates token" - - TOKEN=$(echo "${EXPORT_OUT}" \ - | sed -n '/BEGIN L2C CTF TOKEN/,/END L2C CTF TOKEN/p' \ - | grep -v 'L2C CTF TOKEN' \ - | tr -d '\n ') - DECODED=$(echo "${TOKEN}" | base64 -d 2>/dev/null) || true - - if echo "${DECODED}" | grep -q '"github_username":"testuser"'; then - _pass "Token contains correct username" - else - _fail "Token has wrong username" - fi - - if echo "${DECODED}" | grep -q '"challenges":18'; then - _pass "Token shows 18 challenges" - else - _fail "Token has wrong challenge count" - fi -else - _fail "Export missing token" -fi - -FIRST_TIME_OUT=$(verify time 2>&1) || true -sleep 2 -SECOND_TIME_OUT=$(verify time 2>&1) || true -if [[ "${FIRST_TIME_OUT}" == "${SECOND_TIME_OUT}" ]]; then - _pass "verify time is frozen after first successful export" -else - _fail "verify time changed after export (freeze failed)" -fi - -# ============================================================================ -# SUMMARY -# ============================================================================ -_section "SUMMARY" - -echo "Passed: ${PASSED}" -echo "Failed: ${FAILED}" -echo "Flags captured: ${#FLAGS[@]}" -echo "" - -if [ "$WITH_REBOOT" = true ] && [ $FAILED -eq 0 ]; then - sort -u /var/ctf/completed_challenges 2>/dev/null | wc -l > "$PROGRESS_SNAPSHOT" - touch "$REBOOT_MARKER" - echo "Reboot marker created. Re-run after reboot to verify services." - exit 100 -fi - -if [[ ${FAILED} -eq 0 ]]; then - echo -e "${GREEN}All tests passed! Students can complete this CTF.${NC}" - exit 0 -else - echo -e "${RED}Some tests failed. Students may be blocked.${NC}" - exit 1 -fi +#!/usr/bin/env bash +# shellcheck shell=bash +# +# CTF Challenge Test Script +# Runs on the VM to validate all challenges are solvable by students +# +# This script simulates a real user journey - discovering and solving each +# challenge using only the hints provided. If these tests pass, students +# can complete the CTF. +# +# Usage: +# ./test_ctf_challenges.sh [--with-reboot] +# DEBUG=true ./test_ctf_challenges.sh # Enable debug tracing +# +# Flags: +# --with-reboot After tests pass, signal reboot to verify services persist +# +# Exit codes: +# 0 - All tests passed +# 1 - One or more tests failed +# 100 - Reboot requested (only with --with-reboot flag) +# + +set -o errexit +set -o pipefail +set -o nounset + +# Enable debug tracing if DEBUG=true +[[ "${DEBUG:-}" == 'true' ]] && set -o xtrace + +# Ensure verify command is available +# It's installed in /usr/local/bin by ctf_setup.sh +if ! command -v verify &>/dev/null; then + export PATH="/usr/local/bin:$PATH" +fi + +# ============================================================================= +# CONSTANTS +# ============================================================================= + +# Terminal colors for output formatting +readonly RED='\033[0;31m' +readonly GREEN='\033[0;32m' +readonly YELLOW='\033[1;33m' +readonly NC='\033[0m' # No Color + +# File paths for reboot test coordination +readonly REBOOT_MARKER="/tmp/.ctf_reboot_test_marker" +readonly PROGRESS_SNAPSHOT="/tmp/.ctf_progress_snapshot" + +# ============================================================================= +# GLOBAL STATE +# ============================================================================= + +# Test result counters (mutable) +PASSED=0 +FAILED=0 + +# Parse arguments +WITH_REBOOT=false +for arg in "$@"; do + case $arg in + --with-reboot) + WITH_REBOOT=true + shift + ;; + esac +done + +# ============================================================================= +# HELPER FUNCTIONS +# ============================================================================= + +# Log a passing test result and increment counter +# Arguments: +# $1 - Message describing what passed +_pass() { + local message="${1}" + echo -e "${GREEN}✓ PASS${NC}: ${message}" + ((PASSED++)) || true +} + +# Log a failing test result and increment counter +# Arguments: +# $1 - Message describing what failed +_fail() { + local message="${1}" + echo -e "${RED}✗ FAIL${NC}: ${message}" + ((FAILED++)) || true +} + +# Print a section header for visual separation in output +# Arguments: +# $1 - Section title to display +_section() { + local title="${1}" + echo "" + echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e "${YELLOW}${title}${NC}" + echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +} + +# Verify a flag with the verify command and record result +# Arguments: +# $1 - Challenge number +# $2 - Flag value to verify +# $3 - Success message (optional, defaults to "Solved challenge N") +# $4 - Failure message (optional, defaults to "Found flag but verify rejected it") +# Returns: +# 0 if flag was verified successfully, 1 otherwise +_verify_flag() { + local challenge_num="${1}" + local flag_value="${2}" + local success_msg="${3:-Solved challenge ${challenge_num}}" + local fail_msg="${4:-Challenge ${challenge_num}: Found flag but verify rejected it}" + local verify_out + + verify_out=$(verify "${challenge_num}" "${flag_value}" 2>&1) || true + if echo "${verify_out}" | grep -qE "(Correct|verified)"; then + _pass "${success_msg}" + FLAGS[${challenge_num}]="${flag_value}" + return 0 + else + _fail "${fail_msg}" + FLAGS[${challenge_num}]="" + return 1 + fi +} + +# ============================================================================ +# POST-REBOOT VERIFICATION +# ============================================================================ +if [[ -f "${REBOOT_MARKER}" ]]; then + _section "POST-REBOOT VERIFICATION" + + echo "Verifying services survived reboot..." + + for service in ctf-secret-service ctf-monitor-directory ctf-ping-message ctf-secret-process nginx; do + if systemctl is-active "${service}" &>/dev/null; then + _pass "${service} is running after reboot" + else + _fail "${service} failed to start after reboot - SETUP BUG" + fi + done + + if [ -f "$PROGRESS_SNAPSHOT" ]; then + EXPECTED=$(cat "$PROGRESS_SNAPSHOT") + ACTUAL=$(sort -u /var/ctf/completed_challenges 2>/dev/null | wc -l) + if [ "$ACTUAL" -ge "$EXPECTED" ]; then + _pass "Progress persisted after reboot ($ACTUAL checks)" + else + _fail "Progress lost after reboot (expected ${EXPECTED}, got ${ACTUAL})" + fi + fi + + rm -f "${REBOOT_MARKER}" "${PROGRESS_SNAPSHOT}" + + echo "" + echo "Passed: ${PASSED} | Failed: ${FAILED}" + [[ ${FAILED} -eq 0 ]] && exit 0 || exit 1 +fi + +# ============================================================================ +# VERIFY COMMAND SANITY CHECK +# ============================================================================ +_section "VERIFY COMMAND SANITY CHECK" + +# Quick check that the verify command works at all +if ! command -v verify &>/dev/null; then + _fail "verify command not found in PATH" + echo "PATH: ${PATH}" + echo "Looking for verify: $(which verify 2>&1 || echo 'not found')" + echo "Checking /usr/local/bin: $(ls -la /usr/local/bin/verify 2>&1 || echo 'not found')" + exit 1 +fi + +# Timer should not start before first numeric verify command +rm -f /var/ctf/ctf_start_time /var/ctf/ctf_end_time +TIMER_PRESTART_OUT=$(verify time 2>&1) || true +if echo "${TIMER_PRESTART_OUT}" | grep -q "Timer not started"; then + _pass "verify time shows pre-start message" +else + _fail "verify time pre-start behavior incorrect" +fi + +VERIFY_OUTPUT=$(verify 0 "CTF{example}" 2>&1) || true +if echo "${VERIFY_OUTPUT}" | grep -q "✓"; then + _pass "verify command accepts example flag" +else + _fail "verify command broken - SETUP BUG" + echo "Cannot continue without working verify command" + exit 1 +fi + +if echo "${VERIFY_OUTPUT}" | grep -q "1/19"; then + _pass "verify progress counts the example check" +else + _fail "verify progress did not show 1/19 after the example check" +fi + +if [[ -f /var/ctf/ctf_start_time ]]; then + _pass "Timer starts after first numeric verify command" +else + _fail "Timer did not start after first numeric verify command" +fi + +# ============================================================================ +# CHALLENGE DISCOVERY AND SOLVING +# ============================================================================ +_section "SOLVING ALL CHALLENGES" + +echo "Simulating real student journey using hints to discover and solve each challenge..." +echo "" + +# Store discovered flags +declare -A FLAGS + +# Challenge 1: Hidden File Discovery +# Hint: "Hidden files in Linux start with a dot. Try 'ls -la'" +echo "Challenge 1: Hidden File Discovery" +HIDDEN_FILE=$(ls -la /home/ctf_user/ctf_challenges/ 2>/dev/null \ + | awk '/^-.*\./ {print $NF}' \ + | grep '^\.' \ + | head -1) || true +if [[ -n "${HIDDEN_FILE}" ]]; then + FLAG_1=$(cat "/home/ctf_user/ctf_challenges/${HIDDEN_FILE}" 2>/dev/null \ + | grep -ao 'CTF{[^}]*}' \ + | head -1) || true + if [[ -n "${FLAG_1}" ]]; then + _verify_flag 1 "${FLAG_1}" + else + _fail "Challenge 1: Found file but no CTF flag in it" + FLAGS[1]="" + fi +else + _fail "Challenge 1: No hidden files found with ls -la" + FLAGS[1]="" +fi + +# Challenge 2: Basic File Search +# Hint: "Use find to search for files. Try: find ~ -name '*.txt'" +echo "Challenge 2: Basic File Search" +TXT_FILE=$(find /home/ctf_user/documents -name '*.txt' -type f 2>/dev/null | head -1) || true +if [[ -n "${TXT_FILE}" ]]; then + FLAG_2=$(cat "${TXT_FILE}" 2>/dev/null | grep -ao 'CTF{[^}]*}' | head -1) || true + if [[ -n "${FLAG_2}" ]]; then + _verify_flag 2 "${FLAG_2}" + else + _fail "Challenge 2: Found file but no CTF flag in it" + FLAGS[2]="" + fi +else + _fail "Challenge 2: No .txt files found in documents" + FLAGS[2]="" +fi + +# Challenge 3: Log Analysis +# Hint: "Large log files can hide secrets. Check /var/log and use 'tail'" +echo "Challenge 3: Log Analysis" +LARGE_LOG=$(find /var/log -type f -size +100M 2>/dev/null | head -1) || true +if [[ -n "${LARGE_LOG}" ]]; then + FLAG_3=$(tail -1 "${LARGE_LOG}" 2>/dev/null | grep -ao 'CTF{[^}]*}' | head -1) || true + if [[ -n "${FLAG_3}" ]]; then + _verify_flag 3 "${FLAG_3}" + else + _fail "Challenge 3: Found log but no CTF flag in it" + FLAGS[3]="" + fi +else + _fail "Challenge 3: No large log files found" + FLAGS[3]="" +fi + +# Challenge 4: User Investigation +# Hint: "Investigate other users. Check /etc/passwd or use 'getent passwd'" +echo "Challenge 4: User Investigation" +FLAG_4="" +for user in $(getent passwd | awk -F: '$3 >= 1000 && $1 != "ctf_user" && $1 != "nobody" {print $1}'); do + if [[ -r "/home/${user}/.profile" ]]; then + FLAG_4=$(grep -ao 'CTF{[^}]*}' "/home/${user}/.profile" 2>/dev/null | head -1) || true + [[ -n "${FLAG_4}" ]] && break + fi +done +if [[ -n "${FLAG_4}" ]]; then + _verify_flag 4 "${FLAG_4}" +else + _fail "Challenge 4: Could not find flag in user profiles" + FLAGS[4]="" +fi + +# Challenge 5: Permission Analysis +# Hint: "Look for files with unusual permissions. Try: find / -perm 777" +echo "Challenge 5: Permission Analysis" +FLAG_5="" +for path in /opt /etc /var; do + PERM_FILE=$(find "${path}" -type f -perm 777 2>/dev/null | head -1) || true + if [[ -n "${PERM_FILE}" ]]; then + FLAG_5=$(cat "${PERM_FILE}" 2>/dev/null | grep -ao 'CTF{[^}]*}' | head -1) || true + [[ -n "${FLAG_5}" ]] && break + fi +done +if [[ -n "${FLAG_5}" ]]; then + _verify_flag 5 "${FLAG_5}" +else + _fail "Challenge 5: Could not find flag in 777 permission files" + FLAGS[5]="" +fi + +# Challenge 6: Service Discovery +# Hint: "What services are running? Use 'ss -tulpn' to find listening ports" +echo "Challenge 6: Service Discovery" +FLAG_6="" +for port in $(ss -tulpn 2>/dev/null \ + | awk '/LISTEN/ {split($5,a,":"); print a[length(a)]}' \ + | grep -vE '^(22|80|443|8083)$' \ + | head -3); do + FLAG_6=$(curl -s --connect-timeout 3 "localhost:${port}" 2>/dev/null \ + | grep -ao 'CTF{[^}]*}' \ + | head -1) || true + [[ -n "${FLAG_6}" ]] && break +done +if [[ -n "${FLAG_6}" ]]; then + _verify_flag 6 "${FLAG_6}" +else + _fail "Challenge 6: Could not find flag from listening services" + FLAGS[6]="" +fi + +# Challenge 7: Encoding Challenge +# Hint: "The flag is encoded. Use 'base64 -d' to decode" +echo "Challenge 7: Encoding Challenge" +ENCODED_FILE=$(find /home/ctf_user/ctf_challenges -name '*.txt' -type f 2>/dev/null | head -1) || true +if [[ -n "${ENCODED_FILE}" ]]; then + FLAG_7=$(cat "${ENCODED_FILE}" 2>/dev/null \ + | base64 -d 2>/dev/null \ + | base64 -d 2>/dev/null \ + | grep -ao 'CTF{[^}]*}' \ + | head -1) || true + if [[ -n "${FLAG_7}" ]]; then + _verify_flag 7 "${FLAG_7}" + else + _fail "Challenge 7: Could not decode flag from file" + FLAGS[7]="" + fi +else + _fail "Challenge 7: No encoded file found" + FLAGS[7]="" +fi + +# Challenge 8: SSH Secrets +# Hint: "SSH configurations often hide secrets. Explore ~/.ssh thoroughly" +echo "Challenge 8: SSH Secrets" +FLAG_8="" +while IFS= read -r -d '' f; do + FLAG_8=$(grep -ao 'CTF{[^}]*}' "${f}" 2>/dev/null | head -1) || true + [[ -n "${FLAG_8}" ]] && break +done < <(find /home/ctf_user/.ssh -type f -print0 2>/dev/null) +if [[ -n "${FLAG_8}" ]]; then + _verify_flag 8 "${FLAG_8}" +else + _fail "Challenge 8: Could not find flag in .ssh directory" + FLAGS[8]="" +fi + +# Challenge 9: DNS Inspection +# Hint: "Inspect systemd-resolved configuration safely" +echo "Challenge 9: DNS Inspection" +DNS_DROP_IN="/etc/systemd/resolved.conf.d/ctf-dns.conf" +if [[ -r "${DNS_DROP_IN}" ]]; then + FLAG_9=$(grep -ao 'CTF{[^}]*}' "${DNS_DROP_IN}" 2>/dev/null | head -1) || true + if [[ -n "${FLAG_9}" ]]; then + _verify_flag 9 "${FLAG_9}" "Solved challenge 9" "Challenge 9: Found flag but verify rejected it - SETUP BUG" + else + _fail "Challenge 9: DNS drop-in has no CTF flag - SETUP BUG" + FLAGS[9]="" + fi +else + _fail "Challenge 9: DNS drop-in not readable - SETUP BUG" + FLAGS[9]="" +fi + +# Challenge 10: File Monitoring +# Hint: "Try creating a file in ctf_challenges" +echo "Challenge 10: File Monitoring" +if ! systemctl is-active ctf-monitor-directory.service &>/dev/null; then + _fail "Challenge 10: Monitor service not running - SETUP BUG" + FLAGS[10]="" +else + # Wait for inotifywait process to actually be running (service starts but has internal delay) + echo " Waiting for inotifywait to be ready..." + for _ in {1..15}; do + pgrep -f "inotifywait.*ctf_challenges" &>/dev/null && break + sleep 2 + done + + true > /tmp/.ctf_upload_triggered 2>/dev/null || true + TRIGGER="/home/ctf_user/ctf_challenges/test_$$" + touch "${TRIGGER}" + sleep 3 + + FLAG_10="" + for _ in {1..10}; do + FLAG_10=$(grep -ao 'CTF{[^}]*}' /tmp/.ctf_upload_triggered 2>/dev/null | head -1) || true + [[ -n "${FLAG_10}" ]] && break + sleep 2 + done + rm -f "${TRIGGER}" + + if [[ -n "${FLAG_10}" ]]; then + _verify_flag 10 "${FLAG_10}" "Solved challenge 10" "Challenge 10: Found flag but verify rejected it - SETUP BUG" + else + _fail "Challenge 10: File monitoring did not trigger - SETUP BUG" + FLAGS[10]="" + fi +fi + +# Challenge 11: Web Configuration +# Hint: "Check what ports nginx is listening on" +echo "Challenge 11: Web Configuration" +NGINX_PORT=$(grep -r 'listen' /etc/nginx/ 2>/dev/null \ + | grep -oP 'listen\s+\K[0-9]+' \ + | grep -v '^80$' \ + | head -1) || true +if [[ -n "${NGINX_PORT}" ]]; then + FLAG_11=$(curl -s "localhost:${NGINX_PORT}" 2>/dev/null \ + | grep -ao 'CTF{[^}]*}' \ + | head -1) || true + if [[ -n "${FLAG_11}" ]]; then + _verify_flag 11 "${FLAG_11}" + else + _fail "Challenge 11: Could not get flag from nginx" + FLAGS[11]="" + fi +else + _fail "Challenge 11: Could not find nginx non-standard port" + FLAGS[11]="" +fi + +# Challenge 12: Network Traffic Analysis +# Hint: "Look at ping patterns with tcpdump" +echo "Challenge 12: Network Traffic Analysis" +TCPDUMP_OUT=$(echo 'CTFpassword123!' \ + | sudo -S timeout 10 tcpdump -i lo -c 4 -X icmp 2>/dev/null) || true +if [[ -n "${TCPDUMP_OUT}" ]]; then + HEX=$(echo "${TCPDUMP_OUT}" \ + | grep -E '^\s+0x' \ + | awk '{print $2$3$4$5$6$7$8$9}' \ + | tr -d '\n') + FLAG_12=$(echo "${HEX}" | xxd -r -p 2>/dev/null | grep -ao 'CTF{[^}]*}' | head -1) || true + + if [[ -n "${FLAG_12}" ]]; then + _verify_flag 12 "${FLAG_12}" + else + _fail "Challenge 12: Could not extract flag from ping traffic" + FLAGS[12]="" + fi +else + _fail "Challenge 12: tcpdump capture failed" + FLAGS[12]="" +fi + +# Challenge 13: Cron Job Hunter +# Hint: "Check /etc/cron.d/, /etc/crontab, and user crontabs" +echo "Challenge 13: Cron Job Hunter" +FLAG_13="" +for dir in /etc/cron.d /etc/cron.daily /etc/cron.hourly; do + [[ -d "${dir}" ]] || continue + FLAG_13=$(grep -rh 'CTF{' "${dir}" 2>/dev/null | grep -ao 'CTF{[^}]*}' | head -1) || true + [[ -n "${FLAG_13}" ]] && break +done +if [[ -n "${FLAG_13}" ]]; then + _verify_flag 13 "${FLAG_13}" +else + _fail "Challenge 13: Could not find flag in cron directories" + FLAGS[13]="" +fi + +# Challenge 14: Process Environment +# Hint: "Process info lives in /proc. Check /proc/PID/environ" +echo "Challenge 14: Process Environment" +FLAG_14="" +for pid in $(pgrep -u ctf_user 2>/dev/null); do + [[ -r "/proc/${pid}/environ" ]] || continue + FLAG_14=$(tr '\0' '\n' < "/proc/${pid}/environ" 2>/dev/null | grep -ao 'CTF{[^}]*}') || true + [[ -n "${FLAG_14}" ]] && break +done +if [[ -n "${FLAG_14}" ]]; then + _verify_flag 14 "${FLAG_14}" +else + _fail "Challenge 14: Could not find flag in process environments" + FLAGS[14]="" +fi + +# Challenge 15: Archive Archaeologist +# Hint: "Archives can be nested. Use 'tar -xzf' to extract layers" +echo "Challenge 15: Archive Archaeologist" +ARCHIVE=$(find /home/ctf_user/ctf_challenges -name '*.tar.gz' 2>/dev/null | head -1) || true +if [[ -n "${ARCHIVE}" ]]; then + TMPDIR=$(mktemp -d) + cd "${TMPDIR}" + tar -xzf "${ARCHIVE}" 2>/dev/null || true + for _ in {1..5}; do + INNER=$(find . -maxdepth 1 -name '*.tar.gz' 2>/dev/null | head -1) || true + [[ -z "${INNER}" ]] && break + tar -xzf "${INNER}" 2>/dev/null || true + rm -f "${INNER}" + done + FLAG_15=$(grep -rh 'CTF{' . 2>/dev/null | grep -ao 'CTF{[^}]*}' | head -1) || true + cd - >/dev/null + rm -rf "${TMPDIR}" + + if [[ -n "${FLAG_15}" ]]; then + _verify_flag 15 "${FLAG_15}" + else + _fail "Challenge 15: Could not find flag in nested archives" + FLAGS[15]="" + fi +else + _fail "Challenge 15: No archive found" + FLAGS[15]="" +fi + +# Challenge 16: Symbolic Sleuth +# Hint: "Use 'readlink -f' to find the final target" +echo "Challenge 16: Symbolic Sleuth" +FLAG_16="" +while IFS= read -r -d '' link; do + TARGET=$(readlink -f "${link}" 2>/dev/null) || true + [[ -r "${TARGET}" ]] || continue + FLAG_16=$(grep -ao 'CTF{[^}]*}' "${TARGET}" 2>/dev/null | head -1) || true + [[ -n "${FLAG_16}" ]] && break +done < <(find /home/ctf_user/ctf_challenges -type l -print0 2>/dev/null) +if [[ -n "${FLAG_16}" ]]; then + _verify_flag 16 "${FLAG_16}" +else + _fail "Challenge 16: Could not find flag via symlinks" + FLAGS[16]="" +fi + +# Challenge 17: History Mystery +# Hint: "Bash stores history in ~/.bash_history. Other users may have history too" +echo "Challenge 17: History Mystery" +FLAG_17="" +for home in /home/*; do + user=$(basename "${home}") + [[ "${user}" == "ctf_user" ]] && continue + [[ -r "${home}/.bash_history" ]] || continue + FLAG_17=$(grep -ao 'CTF{[^}]*}' "${home}/.bash_history" 2>/dev/null | head -1) || true + [[ -n "${FLAG_17}" ]] && break +done +if [[ -n "${FLAG_17}" ]]; then + _verify_flag 17 "${FLAG_17}" +else + _fail "Challenge 17: Could not find flag in user histories" + FLAGS[17]="" +fi + +# Challenge 18: Disk Detective +# Hint: "Try mounting disk images with 'sudo mount -o loop'" +echo "Challenge 18: Disk Detective" +DISK_IMG=$(find /opt /home -name '*.img' -type f 2>/dev/null | head -1) || true +if [[ -n "${DISK_IMG}" ]]; then + MNTDIR=$(mktemp -d) + echo 'CTFpassword123!' | sudo -S mount -o loop "${DISK_IMG}" "${MNTDIR}" 2>/dev/null + FLAG_18=$(find "${MNTDIR}" -type f -print0 2>/dev/null \ + | xargs -0 grep -ah 'CTF{' 2>/dev/null \ + | grep -ao 'CTF{[^}]*}' \ + | head -1) || true + echo 'CTFpassword123!' | sudo -S umount "${MNTDIR}" 2>/dev/null || true + rmdir "${MNTDIR}" 2>/dev/null || true + + if [[ -n "${FLAG_18}" ]]; then + _verify_flag 18 "${FLAG_18}" + else + _fail "Challenge 18: Could not find flag in disk image" + FLAGS[18]="" + fi +else + _fail "Challenge 18: No disk image found" + FLAGS[18]="" +fi + +# ============================================================================ +# VERIFICATION TOKEN TEST +# ============================================================================ +_section "VERIFICATION TOKEN TEST" + +PROGRESS=$(verify progress 2>&1) +if echo "${PROGRESS}" | grep -q "19/19"; then + _pass "All 19 progress checks completed" +else + _fail "Not all progress checks completed: ${PROGRESS}" +fi + +EXPORT_OUT=$(verify export testuser 2>&1) || true + +if echo "${EXPORT_OUT}" | grep -q "COMPLETION CERTIFICATE"; then + _pass "Export generates certificate" +else + _fail "Export missing certificate" +fi + +if echo "${EXPORT_OUT}" | grep -q "BEGIN L2C CTF TOKEN"; then + _pass "Export generates token" + + TOKEN=$(echo "${EXPORT_OUT}" \ + | sed -n '/BEGIN L2C CTF TOKEN/,/END L2C CTF TOKEN/p' \ + | grep -v 'L2C CTF TOKEN' \ + | tr -d '\n ') + DECODED=$(echo "${TOKEN}" | base64 -d 2>/dev/null) || true + + if echo "${DECODED}" | grep -q '"github_username":"testuser"'; then + _pass "Token contains correct username" + else + _fail "Token has wrong username" + fi + + if echo "${DECODED}" | grep -q '"challenges":18'; then + _pass "Token shows 18 challenges" + else + _fail "Token has wrong challenge count" + fi +else + _fail "Export missing token" +fi + +FIRST_TIME_OUT=$(verify time 2>&1) || true +sleep 2 +SECOND_TIME_OUT=$(verify time 2>&1) || true +if [[ "${FIRST_TIME_OUT}" == "${SECOND_TIME_OUT}" ]]; then + _pass "verify time is frozen after first successful export" +else + _fail "verify time changed after export (freeze failed)" +fi + +# ============================================================================ +# SUMMARY +# ============================================================================ +_section "SUMMARY" + +echo "Passed: ${PASSED}" +echo "Failed: ${FAILED}" +echo "Flags captured: ${#FLAGS[@]}" +echo "" + +if [ "$WITH_REBOOT" = true ] && [ $FAILED -eq 0 ]; then + sort -u /var/ctf/completed_challenges 2>/dev/null | wc -l > "$PROGRESS_SNAPSHOT" + touch "$REBOOT_MARKER" + echo "Reboot marker created. Re-run after reboot to verify services." + exit 100 +fi + +if [[ ${FAILED} -eq 0 ]]; then + echo -e "${GREEN}All tests passed! Students can complete this CTF.${NC}" + exit 0 +else + echo -e "${RED}Some tests failed. Students may be blocked.${NC}" + exit 1 +fi