לדלג לתוכן

אתגר CTF - סריקות

מבוא

אתגר זה משלב את כל הטכניקות שלמדנו בפרק הסריקות והחקירה המתקדמת. האתגר מורכב מארבעה שלבים, כל אחד בונה על הקודם. תצטרכו להשתמש בכלים ובטכניקות מהשיעורים הקודמים:
- מיפוי סאבדומיינים ו-Subdomain Takeover
- ניתוח JavaScript סטטי
- גילוי נקודות קצה API
- חקירת API ואינטרוספקציה

האתגר מדמה תרחיש אמיתי של חקירת אתר ווב של חברה מדומה.


רקע

חברת "SecureApp Inc." מפעילה אתר בכתובת secureapp.lab. במסגרת תוכנית באג באונטי, נדרשתם לבדוק את משטח התקיפה שלהם. כל מה שיש לכם הוא שם הדומיין.


הכנת סביבת המעבדה

הקמת הסביבה

# docker-compose-ctf-recon.yml
version: "3.8"

services:
  # אתר ראשי
  main-site:
    image: nginx:latest
    ports:
      - "8090:80"
    volumes:
      - ./ctf/main:/usr/share/nginx/html
      - ./ctf/nginx/main.conf:/etc/nginx/conf.d/default.conf

  # סאבדומיין dev
  dev-site:
    image: node:18-slim
    ports:
      - "8091:3000"
    volumes:
      - ./ctf/dev:/app
    working_dir: /app
    command: node server.js

  # שרת API
  api-server:
    image: node:18-slim
    ports:
      - "8092:4000"
    volumes:
      - ./ctf/api:/app
    working_dir: /app
    command: node server.js

צרו את הקבצים הבאים:

קובץ ראשי של האתר:

<!-- ctf/main/index.html -->
<html>
<head>
    <title>SecureApp Inc.</title>
    <script src="/static/js/app.bundle.js"></script>
</head>
<body>
    <h1>Welcome to SecureApp Inc.</h1>
    <p>We build secure applications.</p>
    <a href="/about">About</a>
    <a href="/contact">Contact</a>
</body>
</html>

קובץ JavaScript עם רמזים:

// ctf/main/static/js/app.bundle.js
!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n(0)}({0:function(e,t,n){var r=n(1);r.init()},1:function(e,t){
// API Configuration
var API_BASE="http://api.secureapp.lab:8092";
var DEV_API="http://dev.secureapp.lab:8091";
var INTERNAL_ENDPOINTS=["/api/v1/users","/api/v2/users","/api/admin/dashboard","/api/internal/config","/api/debug/logs","/api/v1/export"];
var FIREBASE_CONFIG={apiKey:"AIzaSyFakeKeyForCTF123456789",authDomain:"secureapp-dev.firebaseapp.com",projectId:"secureapp-dev"};
// TODO: Remove debug endpoint before production
// FIXME: /api/internal/config exposes database credentials
var ADMIN_ROUTES=["/admin","/admin/users","/admin/settings","/admin/sql-console","/dev/feature-flags"];
e.exports={init:function(){console.log("App initialized");fetch(API_BASE+"/api/health").then(function(r){return r.json()}).then(function(d){console.log("API Status:",d)})}}
}});
//# sourceMappingURL=app.bundle.js.map

קובץ Source Map:

// ctf/main/static/js/app.bundle.js.map
{
  "version": 3,
  "sources": [
    "src/config/api.js",
    "src/config/firebase.js",
    "src/services/auth.js",
    "src/services/admin.js",
    "src/utils/debug.js"
  ],
  "sourcesContent": [
    "// API Configuration\nconst API_BASE = 'http://api.secureapp.lab:8092';\nconst API_KEY = 'sk_live_CTF_FLAG_STEP4_SourceMapSecret';\n\nexport const endpoints = {\n  users: `${API_BASE}/api/v2/users`,\n  admin: `${API_BASE}/api/admin/dashboard`,\n  config: `${API_BASE}/api/internal/config`,\n  debug: `${API_BASE}/api/debug/logs`,\n  export: `${API_BASE}/api/v1/export`,\n  graphql: `${API_BASE}/graphql`,\n  swagger: `${API_BASE}/api-docs.json`\n};",
    "// Firebase Config\nconst firebaseConfig = {\n  apiKey: 'AIzaSyFakeKeyForCTF123456789',\n  authDomain: 'secureapp-dev.firebaseapp.com',\n  projectId: 'secureapp-dev'\n};",
    "// Auth Service\nconst AUTH_SECRET = 'jwt_secret_for_development_only';\n// Default admin credentials for dev: admin@secureapp.lab / CTF_D3v_P4ssw0rd!",
    "// Admin Service\n// Internal admin API: http://api.secureapp.lab:8092/api/admin/\n// Requires X-Admin-Token: super_secret_admin_token_123",
    "// Debug Utils\n// Debug mode: add ?debug=true to any page\n// Log viewer: /api/debug/logs?token=debug_token_456"
  ]
}

שרת dev:

// ctf/dev/server.js
const http = require("http");

const server = http.createServer((req, res) => {
    res.setHeader("Content-Type", "application/json");

    if (req.url === "/" || req.url === "/index.html") {
        res.writeHead(200, {"Content-Type": "text/html"});
        res.end("<h1>SecureApp Dev Portal</h1><p>Internal use only</p>");
    } else if (req.url === "/api/health") {
        res.writeHead(200);
        res.end(JSON.stringify({status: "ok", environment: "development", version: "2.1.0-beta"}));
    } else if (req.url === "/api/config") {
        res.writeHead(200);
        res.end(JSON.stringify({
            database: "mongodb://devuser:devpass123@db.internal:27017/secureapp",
            redis: "redis://cache.internal:6379",
            flag: "CTF_FLAG_STEP2_DevConfigExposed"
        }));
    } else {
        res.writeHead(404);
        res.end(JSON.stringify({error: "Not found"}));
    }
});

server.listen(3000, () => console.log("Dev server on port 3000"));

שרת API:

// ctf/api/server.js
const http = require("http");
const url = require("url");

const USERS = [
    {id: 1, name: "Admin", email: "admin@secureapp.lab", role: "admin", password_hash: "$2b$10$fake_hash"},
    {id: 2, name: "User", email: "user@secureapp.lab", role: "user", password_hash: "$2b$10$fake_hash2"},
];

const SWAGGER_DOC = {
    openapi: "3.0.0",
    info: {title: "SecureApp API", version: "2.0.0"},
    paths: {
        "/api/v2/users": {get: {summary: "List users", security: [{bearerAuth: []}]}},
        "/api/v2/users/{id}": {get: {summary: "Get user by ID"}},
        "/api/admin/dashboard": {get: {summary: "Admin dashboard", security: [{adminToken: []}]}},
        "/api/internal/config": {get: {summary: "Internal config (restricted)"}},
        "/api/debug/logs": {get: {summary: "Debug logs"}},
        "/graphql": {post: {summary: "GraphQL endpoint"}},
    }
};

const GQL_SCHEMA = {
    data: {
        __schema: {
            types: [
                {name: "Query", kind: "OBJECT", fields: [
                    {name: "users", type: {name: null, kind: "LIST", ofType: {name: "User"}}, args: []},
                    {name: "user", type: {name: "User", kind: "OBJECT", ofType: null}, args: [{name: "id", type: {name: "Int"}}]},
                    {name: "flag", type: {name: "String", kind: "SCALAR", ofType: null}, args: [{name: "token", type: {name: "String"}}]},
                    {name: "secretConfig", type: {name: "Config", kind: "OBJECT", ofType: null}, args: []},
                ]},
                {name: "User", kind: "OBJECT", fields: [
                    {name: "id", type: {name: "Int", kind: "SCALAR", ofType: null}, args: []},
                    {name: "name", type: {name: "String", kind: "SCALAR", ofType: null}, args: []},
                    {name: "email", type: {name: "String", kind: "SCALAR", ofType: null}, args: []},
                    {name: "role", type: {name: "String", kind: "SCALAR", ofType: null}, args: []},
                    {name: "apiKey", type: {name: "String", kind: "SCALAR", ofType: null}, args: []},
                ]},
                {name: "Config", kind: "OBJECT", fields: [
                    {name: "dbConnection", type: {name: "String", kind: "SCALAR", ofType: null}, args: []},
                    {name: "adminSecret", type: {name: "String", kind: "SCALAR", ofType: null}, args: []},
                    {name: "flag", type: {name: "String", kind: "SCALAR", ofType: null}, args: []},
                ]},
            ]
        }
    }
};

const server = http.createServer((req, res) => {
    const parsedUrl = url.parse(req.url, true);
    const path = parsedUrl.pathname;

    res.setHeader("Access-Control-Allow-Origin", "*");
    res.setHeader("Content-Type", "application/json");

    if (path === "/api/health") {
        res.writeHead(200);
        res.end(JSON.stringify({status: "ok", version: "2.0.0"}));
    }
    else if (path === "/api-docs.json" || path === "/swagger.json") {
        res.writeHead(200);
        res.end(JSON.stringify(SWAGGER_DOC));
    }
    else if (path === "/api/v2/users") {
        res.writeHead(200);
        res.end(JSON.stringify(USERS.map(u => ({id: u.id, name: u.name, email: u.email, role: u.role}))));
    }
    else if (path === "/api/v1/users") {
        // גרסה ישנה חושפת מידע נוסף
        res.writeHead(200);
        res.end(JSON.stringify(USERS));
    }
    else if (path === "/api/internal/config") {
        res.writeHead(200);
        res.end(JSON.stringify({
            db: "mysql://root:r00tP4ss!@db.internal:3306/production",
            secret: "jwt_production_secret_do_not_share",
            flag: "CTF_FLAG_STEP3_InternalConfigAccess"
        }));
    }
    else if (path === "/api/debug/logs") {
        const token = parsedUrl.query.token;
        if (token === "debug_token_456") {
            res.writeHead(200);
            res.end(JSON.stringify({logs: ["Login attempt: admin", "Config reload", "DB backup started"], flag: "CTF_FLAG_BONUS_DebugAccess"}));
        } else {
            res.writeHead(403);
            res.end(JSON.stringify({error: "Invalid debug token"}));
        }
    }
    else if (path === "/graphql" && req.method === "POST") {
        let body = "";
        req.on("data", chunk => body += chunk);
        req.on("end", () => {
            try {
                const parsed = JSON.parse(body);
                const query = parsed.query || "";

                if (query.includes("__schema") || query.includes("__type")) {
                    res.writeHead(200);
                    res.end(JSON.stringify(GQL_SCHEMA));
                }
                else if (query.includes("secretConfig")) {
                    res.writeHead(200);
                    res.end(JSON.stringify({data: {secretConfig: {
                        dbConnection: "mysql://root:r00tP4ss!@db.internal:3306",
                        adminSecret: "super_secret_admin_token_123",
                        flag: "CTF_FLAG_STEP3_GraphQLSecretConfig"
                    }}}));
                }
                else if (query.includes("users")) {
                    res.writeHead(200);
                    res.end(JSON.stringify({data: {users: USERS.map(u => ({...u, apiKey: "ak_" + u.id + "_secret123"}))}}));
                }
                else {
                    res.writeHead(200);
                    res.end(JSON.stringify({data: null}));
                }
            } catch(e) {
                res.writeHead(400);
                res.end(JSON.stringify({error: "Invalid query"}));
            }
        });
    }
    else {
        res.writeHead(404);
        res.end(JSON.stringify({error: "Not found"}));
    }
});

server.listen(4000, () => console.log("API server on port 4000"));

הגדרת DNS מקומי

# הוסיפו לקובץ /etc/hosts:
sudo sh -c 'cat >> /etc/hosts << EOF
127.0.0.1 secureapp.lab
127.0.0.1 dev.secureapp.lab
127.0.0.1 api.secureapp.lab
127.0.0.1 staging.secureapp.lab
127.0.0.1 old.secureapp.lab
EOF'

הפעלת הסביבה

# צרו את כל התיקיות והקבצים, ואז:
docker compose -f docker-compose-ctf-recon.yml up -d

שלב 1 - מיפוי סאבדומיינים ומציאת שרת dev נשכח

המשימה

מפו את כל הסאבדומיינים של secureapp.lab ומצאו סאבדומיין של סביבת פיתוח שנשכחה.

רמזים

  1. התחילו מסריקת vhosts:
ffuf -u "http://secureapp.lab:8090" -w /usr/share/wordlists/subdomains-top1million-5000.txt \
     -H "Host: FUZZ.secureapp.lab" -mc all -fs <BASELINE_SIZE>
  1. בדקו את הסאבדומיינים שמצאתם - איזה מהם מחזיר תוכן של סביבת פיתוח?

  2. בסביבת dev, חפשו נקודות קצה שחושפות מידע רגיש

הדגל

מצאו את הדגל שמסתתר בתצורת סביבת הפיתוח.


שלב 2 - ניתוח JavaScript של האתר הראשי

המשימה

נתחו את קובצי ה-JavaScript של האתר הראשי כדי למצוא:
- נתיבי API נסתרים
- מפתחות וטוקנים
- כתובות שרתים פנימיים

רמזים

  1. גלשו לאתר הראשי ומצאו את קובצי ה-JS
  2. בדקו האם יש Source Map זמין
  3. אם מצאתם Source Map - שחזרו את קוד המקור
  4. חפשו הערות TODO/FIXME, מפתחות, וטוקנים

מה לחפש

  • כתובות API
  • מפתחות Firebase
  • הגדרות אותנטיקציה
  • נתיבי admin
  • הערות מפתחים עם מידע רגיש

הדגל

מצאו את הדגל שמסתתר בתוך ה-Source Map.


שלב 3 - גילוי API לא מתועד עם סכמה מלאה

המשימה

מתוך המידע שגיליתם ב-JavaScript, מצאו ונתחו את ה-API:
- מצאו את קובץ ה-Swagger
- בצעו אינטרוספקציה של GraphQL
- גשו לנקודות קצה פנימיות

רמזים

  1. מה-JavaScript שניתחתם, אתם יודעים שיש תיעוד Swagger ונקודת קצה GraphQL
  2. נסו לגשת לתיעוד
  3. בצעו שאילתת אינטרוספקציה ב-GraphQL:
curl -s -X POST "http://api.secureapp.lab:8092/graphql" \
     -H "Content-Type: application/json" \
     -d '{"query": "{ __schema { types { name kind fields { name type { name kind ofType { name } } args { name type { name } } } } } }"}'
  1. חפשו שדות רגישים בסכמה של GraphQL
  2. נסו לגשת ל-/api/internal/config ול-/api/v1/users

הדגל

מצאו דגלים בנקודות הקצה הפנימיות וב-GraphQL.


שלב 4 - חילוץ סודות מ-Source Maps ושימוש בטוקנים

המשימה

השתמשו במידע שחילצתם מכל השלבים הקודמים כדי לגשת לנקודות קצה מוגנות.

רמזים

  1. ב-Source Map מצאתם טוקן debug - השתמשו בו:
curl -s "http://api.secureapp.lab:8092/api/debug/logs?token=<TOKEN_FROM_SOURCE_MAP>"
  1. ב-Source Map מצאתם גם admin token - נסו אותו:
curl -s "http://api.secureapp.lab:8092/api/admin/dashboard" \
     -H "X-Admin-Token: <TOKEN_FROM_SOURCE_MAP>"
  1. השוו בין /api/v1/users ל-/api/v2/users - מה החשיפה בגרסה הישנה?

הדגל

מצאו את כל הדגלים שנותרו.


סיכום האתגר

רשימת דגלים

שלב דגל מקור
1 CTF_FLAG_STEP2_DevConfigExposed תצורת שרת dev
2 CTF_FLAG_STEP4_SourceMapSecret קוד מקור ב-Source Map
3 CTF_FLAG_STEP3_InternalConfigAccess נקודת קצה פנימית
3 CTF_FLAG_STEP3_GraphQLSecretConfig שאילתת GraphQL
בונוס CTF_FLAG_BONUS_DebugAccess גישה לנקודת debug עם טוקן

טכניקות שנדרשו

  1. מיפוי סאבדומיינים - סריקת vhosts למציאת dev.secureapp.lab
  2. ניתוח JavaScript - חילוץ נתיבי API, מפתחות, וטוקנים מקוד ה-bundle
  3. שחזור Source Maps - שחזור קוד מקור מלא עם הערות וסודות
  4. חקירת API - שימוש ב-Swagger, אינטרוספקציית GraphQL, ובדיקת גרסאות ישנות
  5. ניצול מידע - שימוש בטוקנים שנחשפו לגישה לנקודות קצה מוגנות

מעבדות PortSwigger קשורות

לתרגול נוסף, בצעו את המעבדות הבאות:

  1. Information disclosure in error messages - חשיפת מידע בהודעות שגיאה
  2. Information disclosure on debug page - חשיפת מידע בדפי debug
  3. Source code disclosure via backup files - חשיפת קוד מקור דרך קבצי גיבוי
  4. Authentication bypass via information disclosure - עקיפת אותנטיקציה דרך מידע שנחשף

קישור: https://portswigger.net/web-security/information-disclosure


אתגרי HackTheBox מומלצים

  • מכונות בקטגוריית Web בדרגת קושי Medium שדורשות חקירה מתקדמת
  • אתגרי Web בקטגוריית Recon
  • אתגרים שכוללים ניתוח JavaScript ומציאת API נסתרים

בדקו את האתגרים הזמינים ב-https://www.hackthebox.com/ תחת Challenges > Web.