|
| 1 | +#!/usr/bin/env bash |
| 2 | +# ============================================================================= |
| 3 | +# 01_health_recon.sh - Health endpoint reconnaissance |
| 4 | +# |
| 5 | +# Purpose: Probe all health/status endpoints and document what internal system |
| 6 | +# information is exposed (DB URLs, Redis addresses, service names, |
| 7 | +# version strings, environment names, etc.) |
| 8 | +# |
| 9 | +# Usage: |
| 10 | +# bash 01_health_recon.sh |
| 11 | +# bash 01_health_recon.sh 2>&1 | tee custom_output.txt |
| 12 | +# |
| 13 | +# Output: ../snapshots/health_recon_TIMESTAMP.txt |
| 14 | +# ============================================================================= |
| 15 | + |
| 16 | +set -euo pipefail |
| 17 | + |
| 18 | +# --------------------------------------------------------------------------- |
| 19 | +# Configuration |
| 20 | +# --------------------------------------------------------------------------- |
| 21 | +BASE_URL="http://localhost:3333" |
| 22 | +API="http://localhost:3333/api/v1" |
| 23 | +TIMESTAMP="$(date +%Y%m%d_%H%M%S)" |
| 24 | +SNAPSHOT_DIR="$(cd "$(dirname "$0")/../snapshots" 2>/dev/null && pwd || echo "/home/bullet/PROJETOS/prostaff-api/.pentest/snapshots")" |
| 25 | +OUTPUT_FILE="${SNAPSHOT_DIR}/health_recon_${TIMESTAMP}.txt" |
| 26 | + |
| 27 | +# --------------------------------------------------------------------------- |
| 28 | +# Color helpers (stdout only; file output is plain) |
| 29 | +# --------------------------------------------------------------------------- |
| 30 | +RED='\033[0;31m' |
| 31 | +GREEN='\033[0;32m' |
| 32 | +YELLOW='\033[1;33m' |
| 33 | +CYAN='\033[0;36m' |
| 34 | +BOLD='\033[1m' |
| 35 | +RESET='\033[0m' |
| 36 | + |
| 37 | +ok() { echo -e "${GREEN}[OK]${RESET} $*"; } |
| 38 | +finding() { echo -e "${RED}[!!]${RESET} $*"; } |
| 39 | +info() { echo -e "${CYAN}[*]${RESET} $*"; } |
| 40 | +header() { echo -e "\n${BOLD}${CYAN}=== $* ===${RESET}\n"; } |
| 41 | + |
| 42 | +# --------------------------------------------------------------------------- |
| 43 | +# Logging: write to both stdout and file (file gets plain text) |
| 44 | +# --------------------------------------------------------------------------- |
| 45 | +mkdir -p "${SNAPSHOT_DIR}" |
| 46 | +exec > >(tee -a "${OUTPUT_FILE}") 2>&1 |
| 47 | + |
| 48 | +log_separator() { |
| 49 | + echo "--------------------------------------------------------------------------------" |
| 50 | +} |
| 51 | + |
| 52 | +# --------------------------------------------------------------------------- |
| 53 | +# Probe a single endpoint and record everything |
| 54 | +# --------------------------------------------------------------------------- |
| 55 | +probe_endpoint() { |
| 56 | + local label="$1" |
| 57 | + local url="$2" |
| 58 | + local method="${3:-GET}" |
| 59 | + |
| 60 | + echo "" |
| 61 | + log_separator |
| 62 | + echo "ENDPOINT : ${label}" |
| 63 | + echo "URL : ${url}" |
| 64 | + echo "METHOD : ${method}" |
| 65 | + echo "TIME : $(date --iso-8601=seconds)" |
| 66 | + log_separator |
| 67 | + |
| 68 | + # Run curl, capture status + time + headers + body |
| 69 | + local tmp_headers |
| 70 | + tmp_headers="$(mktemp)" |
| 71 | + local tmp_body |
| 72 | + tmp_body="$(mktemp)" |
| 73 | + |
| 74 | + local http_code |
| 75 | + local total_time |
| 76 | + |
| 77 | + http_code=$(curl -s -o "${tmp_body}" \ |
| 78 | + -D "${tmp_headers}" \ |
| 79 | + -w "%{http_code}" \ |
| 80 | + --max-time 10 \ |
| 81 | + -X "${method}" \ |
| 82 | + "${url}" 2>/dev/null) || http_code="CURL_ERROR" |
| 83 | + |
| 84 | + total_time=$(curl -s -o /dev/null \ |
| 85 | + -w "%{time_total}" \ |
| 86 | + --max-time 10 \ |
| 87 | + -X "${method}" \ |
| 88 | + "${url}" 2>/dev/null) || total_time="N/A" |
| 89 | + |
| 90 | + echo "HTTP STATUS : ${http_code}" |
| 91 | + echo "RESPONSE TIME: ${total_time}s" |
| 92 | + echo "" |
| 93 | + |
| 94 | + echo "--- Response Headers ---" |
| 95 | + cat "${tmp_headers}" 2>/dev/null || echo "(no headers captured)" |
| 96 | + echo "" |
| 97 | + |
| 98 | + echo "--- Response Body ---" |
| 99 | + local body |
| 100 | + body="$(cat "${tmp_body}" 2>/dev/null || echo '(empty)')" |
| 101 | + if [ -z "${body}" ]; then |
| 102 | + echo "(empty body)" |
| 103 | + else |
| 104 | + # Pretty-print if JSON, otherwise raw |
| 105 | + echo "${body}" | python3 -m json.tool 2>/dev/null || echo "${body}" |
| 106 | + fi |
| 107 | + echo "" |
| 108 | + |
| 109 | + # ------------------------------------------------------------------------- |
| 110 | + # Findings analysis - look for sensitive data patterns |
| 111 | + # ------------------------------------------------------------------------- |
| 112 | + echo "--- Findings Analysis ---" |
| 113 | + |
| 114 | + local found_anything=0 |
| 115 | + |
| 116 | + # DB connection strings |
| 117 | + if echo "${body}" | grep -qiE '(postgres|mysql|mongodb|database_url|db_host|db_url|jdbc:)'; then |
| 118 | + finding "Possible database connection info in response body" |
| 119 | + found_anything=1 |
| 120 | + fi |
| 121 | + |
| 122 | + # Redis |
| 123 | + if echo "${body}" | grep -qiE '(redis://|redis_url|redis_host|:6379|:6380)'; then |
| 124 | + finding "Redis connection info leaked in response body" |
| 125 | + found_anything=1 |
| 126 | + fi |
| 127 | + |
| 128 | + # Internal hostnames / IPs |
| 129 | + if echo "${body}" | grep -qE '(127\.|10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.|localhost)'; then |
| 130 | + finding "Internal IP or localhost reference in response body" |
| 131 | + found_anything=1 |
| 132 | + fi |
| 133 | + |
| 134 | + # Service version strings |
| 135 | + if echo "${body}" | grep -qiE '(version|ruby|rails|rack|puma|unicorn|nginx|apache)'; then |
| 136 | + finding "Version or server technology disclosed in response body" |
| 137 | + found_anything=1 |
| 138 | + fi |
| 139 | + |
| 140 | + # Environment name |
| 141 | + if echo "${body}" | grep -qiE '(environment|env.*:.*production|env.*:.*staging|env.*:.*development|RAILS_ENV)'; then |
| 142 | + finding "Environment name disclosed in response body" |
| 143 | + found_anything=1 |
| 144 | + fi |
| 145 | + |
| 146 | + # API keys / secrets (partial) |
| 147 | + if echo "${body}" | grep -qiE '(api_key|secret|token|password|credential)'; then |
| 148 | + finding "Possible credential/key reference in response body" |
| 149 | + found_anything=1 |
| 150 | + fi |
| 151 | + |
| 152 | + # Stack traces |
| 153 | + if echo "${body}" | grep -qiE '(\.rb:|ActiveRecord|ActionController|app/|backtrace|stack trace|Traceback)'; then |
| 154 | + finding "Stack trace or Ruby internal path in response body" |
| 155 | + found_anything=1 |
| 156 | + fi |
| 157 | + |
| 158 | + # Server header disclosure |
| 159 | + local server_header |
| 160 | + server_header=$(grep -i '^server:' "${tmp_headers}" 2>/dev/null | head -1 || true) |
| 161 | + if [ -n "${server_header}" ]; then |
| 162 | + finding "Server header disclosed: ${server_header}" |
| 163 | + found_anything=1 |
| 164 | + fi |
| 165 | + |
| 166 | + # X-Powered-By |
| 167 | + local powered_by |
| 168 | + powered_by=$(grep -i '^x-powered-by:' "${tmp_headers}" 2>/dev/null | head -1 || true) |
| 169 | + if [ -n "${powered_by}" ]; then |
| 170 | + finding "X-Powered-By header disclosed: ${powered_by}" |
| 171 | + found_anything=1 |
| 172 | + fi |
| 173 | + |
| 174 | + # Meilisearch |
| 175 | + if echo "${body}" | grep -qiE '(meilisearch|meili)'; then |
| 176 | + finding "Meilisearch service reference in response body" |
| 177 | + found_anything=1 |
| 178 | + fi |
| 179 | + |
| 180 | + # Supabase |
| 181 | + if echo "${body}" | grep -qiE '(supabase|\.supabase\.co)'; then |
| 182 | + finding "Supabase reference in response body" |
| 183 | + found_anything=1 |
| 184 | + fi |
| 185 | + |
| 186 | + if [ "${found_anything}" -eq 0 ]; then |
| 187 | + ok "No obvious sensitive data detected in response" |
| 188 | + fi |
| 189 | + |
| 190 | + # Check security headers |
| 191 | + echo "" |
| 192 | + echo "--- Security Headers Check ---" |
| 193 | + local sec_headers=("X-Frame-Options" "X-Content-Type-Options" "Content-Security-Policy" "Strict-Transport-Security" "X-XSS-Protection") |
| 194 | + for hdr in "${sec_headers[@]}"; do |
| 195 | + local val |
| 196 | + val=$(grep -i "^${hdr}:" "${tmp_headers}" 2>/dev/null | head -1 || true) |
| 197 | + if [ -n "${val}" ]; then |
| 198 | + ok "Present: ${val}" |
| 199 | + else |
| 200 | + info "Missing: ${hdr}" |
| 201 | + fi |
| 202 | + done |
| 203 | + |
| 204 | + rm -f "${tmp_headers}" "${tmp_body}" |
| 205 | +} |
| 206 | + |
| 207 | +# =========================================================================== |
| 208 | +# MAIN |
| 209 | +# =========================================================================== |
| 210 | +header "HEALTH ENDPOINT RECONNAISSANCE" |
| 211 | +echo "Target : ${BASE_URL}" |
| 212 | +echo "Started : $(date --iso-8601=seconds)" |
| 213 | +echo "Output : ${OUTPUT_FILE}" |
| 214 | + |
| 215 | +info "Probing health and status endpoints..." |
| 216 | + |
| 217 | +# Probe all candidate endpoints |
| 218 | +probe_endpoint "Root health (Rails default)" "${BASE_URL}/up" |
| 219 | +probe_endpoint "Generic /health" "${BASE_URL}/health" |
| 220 | +probe_endpoint "Liveness probe" "${BASE_URL}/health/live" |
| 221 | +probe_endpoint "Readiness probe" "${BASE_URL}/health/ready" |
| 222 | +probe_endpoint "Detailed health" "${BASE_URL}/health/detailed" |
| 223 | +probe_endpoint "API v1 status" "${API}/status" |
| 224 | +probe_endpoint "API root" "${API}" |
| 225 | + |
| 226 | +# Also check for common info-disclosure paths |
| 227 | +probe_endpoint "Rails info (should be blocked in prod)" "${BASE_URL}/rails/info" |
| 228 | +probe_endpoint "Rails info properties" "${BASE_URL}/rails/info/properties" |
| 229 | +probe_endpoint "Sidekiq web UI" "${BASE_URL}/sidekiq" |
| 230 | +probe_endpoint "Cable endpoint" "${BASE_URL}/cable" |
| 231 | + |
| 232 | +echo "" |
| 233 | +log_separator |
| 234 | +header "RECONNAISSANCE SUMMARY" |
| 235 | +echo "Completed : $(date --iso-8601=seconds)" |
| 236 | +echo "Results saved to: ${OUTPUT_FILE}" |
| 237 | +echo "" |
| 238 | +echo "Review the [!!] findings above for sensitive disclosures." |
| 239 | +echo "Key questions to answer from this output:" |
| 240 | +echo " 1. Does /up or /health/detailed reveal DB, Redis, or service URLs?" |
| 241 | +echo " 2. Are Server/X-Powered-By headers disclosing tech stack?" |
| 242 | +echo " 3. Does /rails/info/properties return data (should 404 in prod)?" |
| 243 | +echo " 4. Is the Sidekiq dashboard accessible without auth?" |
| 244 | +echo " 5. Do any endpoints return stack traces or internal paths?" |
| 245 | +log_separator |
0 commit comments