אתגר 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'
הפעלת הסביבה¶
שלב 1 - מיפוי סאבדומיינים ומציאת שרת dev נשכח¶
המשימה¶
מפו את כל הסאבדומיינים של secureapp.lab ומצאו סאבדומיין של סביבת פיתוח שנשכחה.
רמזים¶
- התחילו מסריקת 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>
-
בדקו את הסאבדומיינים שמצאתם - איזה מהם מחזיר תוכן של סביבת פיתוח?
-
בסביבת dev, חפשו נקודות קצה שחושפות מידע רגיש
הדגל¶
מצאו את הדגל שמסתתר בתצורת סביבת הפיתוח.
שלב 2 - ניתוח JavaScript של האתר הראשי¶
המשימה¶
נתחו את קובצי ה-JavaScript של האתר הראשי כדי למצוא:
- נתיבי API נסתרים
- מפתחות וטוקנים
- כתובות שרתים פנימיים
רמזים¶
- גלשו לאתר הראשי ומצאו את קובצי ה-JS
- בדקו האם יש Source Map זמין
- אם מצאתם Source Map - שחזרו את קוד המקור
- חפשו הערות TODO/FIXME, מפתחות, וטוקנים
מה לחפש¶
- כתובות API
- מפתחות Firebase
- הגדרות אותנטיקציה
- נתיבי admin
- הערות מפתחים עם מידע רגיש
הדגל¶
מצאו את הדגל שמסתתר בתוך ה-Source Map.
שלב 3 - גילוי API לא מתועד עם סכמה מלאה¶
המשימה¶
מתוך המידע שגיליתם ב-JavaScript, מצאו ונתחו את ה-API:
- מצאו את קובץ ה-Swagger
- בצעו אינטרוספקציה של GraphQL
- גשו לנקודות קצה פנימיות
רמזים¶
- מה-JavaScript שניתחתם, אתם יודעים שיש תיעוד Swagger ונקודת קצה GraphQL
- נסו לגשת לתיעוד
- בצעו שאילתת אינטרוספקציה ב-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 } } } } } }"}'
- חפשו שדות רגישים בסכמה של GraphQL
- נסו לגשת ל-
/api/internal/configול-/api/v1/users
הדגל¶
מצאו דגלים בנקודות הקצה הפנימיות וב-GraphQL.
שלב 4 - חילוץ סודות מ-Source Maps ושימוש בטוקנים¶
המשימה¶
השתמשו במידע שחילצתם מכל השלבים הקודמים כדי לגשת לנקודות קצה מוגנות.
רמזים¶
- ב-Source Map מצאתם טוקן debug - השתמשו בו:
- ב-Source Map מצאתם גם admin token - נסו אותו:
curl -s "http://api.secureapp.lab:8092/api/admin/dashboard" \
-H "X-Admin-Token: <TOKEN_FROM_SOURCE_MAP>"
- השוו בין
/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 עם טוקן |
טכניקות שנדרשו¶
- מיפוי סאבדומיינים - סריקת vhosts למציאת dev.secureapp.lab
- ניתוח JavaScript - חילוץ נתיבי API, מפתחות, וטוקנים מקוד ה-bundle
- שחזור Source Maps - שחזור קוד מקור מלא עם הערות וסודות
- חקירת API - שימוש ב-Swagger, אינטרוספקציית GraphQL, ובדיקת גרסאות ישנות
- ניצול מידע - שימוש בטוקנים שנחשפו לגישה לנקודות קצה מוגנות
מעבדות PortSwigger קשורות¶
לתרגול נוסף, בצעו את המעבדות הבאות:
- Information disclosure in error messages - חשיפת מידע בהודעות שגיאה
- Information disclosure on debug page - חשיפת מידע בדפי debug
- Source code disclosure via backup files - חשיפת קוד מקור דרך קבצי גיבוי
- 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.