flawopen.com/ssrf/Csharp

● CWE-918 · Hoch
Sicherheitsforschung · FlawOpen

Server-Side Request Forgery in C#

How HttpClient allows SSRF in ASP.NET Core, and how to enforce IP boundary assertions in SocketsHttpHandler.

💡 Einfache Erklärung (ELI5)

Stellen Sie sich vor, Sie schicken einen Boten, um ein Paket in einem öffentlichen Geschäft abzuholen, geben ihm aber die Adresse des Tresors im Büro des Geschäftsführers. Da der Bote einen internen Ausweis besitzt, öffnet er den Tresor und händigt Firmengeheimnisse aus.

Kernkonzepte & Begriffe

Web Application Security
Kernkomponente der Architektur, die von CWE-918 betroffen ist.
CWE-918
Standardmäßige Common Weakness Enumeration (CWE)-Klassifizierung für ssrf-csharp.
Defense-in-Depth
Mehrschichtige technische Verifikation und Laufzeit-Grenzenisolation.

Schritt-für-Schritt Angriffsablauf

Step 1

Eingabe Nicht Vertrauenswürdiger URLs

Die Anwendung akzeptiert externe URL-Parameter für Webhooks, Avatare oder PDF-Voransichten.

Step 2

Adressierung Interner Netzwerke

Der Angreifer übergibt eine Ziel-URL auf Cloud-Metadaten oder interne Loopback-Adressen (z. B. http://169.254.169.254/latest/meta-data/).

Step 3

Ungefilterter Socket-Ausgang

Der HTTP-Client des Servers sendet die Anfrage aus der geschützten VPC, ohne die Ziel-IP gegen private Subnetzbereiche abzugleichen.

Step 4

Exfiltration von Cloud-Zugangsdaten

Der interne Metadatendienst vertraut der Server-Anfrage und liefert temporäre IAM-Schlüssel und Tokens zurück.

Quellcode: Verwundbar vs. Sicher

✕ VERWUNDBARE IMPLEMENTIERUNG
// VULNERABLE: Naive HttpClient usage with user-supplied URL
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/[controller]")]
public class WebhookController : ControllerBase {
    private readonly HttpClient _client = new HttpClient();

    [HttpGet("test")]
    public async Task<IActionResult> TestWebhook([FromQuery] string url) {
        // Attacker input: http://169.254.169.254/metadata/v1/
        var response = await _client.GetAsync(url);
        var content = await response.Content.ReadAsStringAsync();
        return Ok(content);
    }
}
✓ GEHÄRTETER SICHERHEITS-PATCH
// HARDENED: SocketsHttpHandler with ConnectCallback IP validation
using System;
using System.Net;
using System.Net.Http;
using System.Net.Sockets;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/[controller]")]
public class WebhookController : ControllerBase {
    private static readonly HttpClient SafeClient;

    static WebhookController() {
        var handler = new SocketsHttpHandler {
            AllowAutoRedirect = false, // Block redirect bypasses
            ConnectTimeout = TimeSpan.FromSeconds(3),
            ConnectCallback = async (context, cancellationToken) => {
                var entry = await Dns.GetHostEntryAsync(context.DnsEndPoint.Host, cancellationToken);
                foreach (var ip in entry.AddressList) {
                    if (IPAddress.IsLoopback(ip) || isPrivateOrLinkLocal(ip)) {
                        throw new SocketException((int)SocketError.AccessDenied);
                    }
                }
                var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
                await socket.ConnectAsync(entry.AddressList[0], context.DnsEndPoint.Port, cancellationToken);
                return new NetworkStream(socket, ownsSocket: true);
            }
        };
        SafeClient = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(5) };
    }

    private static bool isPrivateOrLinkLocal(IPAddress ip) {
        if (ip.IsIPv4MappedToIPv6) ip = ip.MapToIPv4();
        byte[] bytes = ip.GetAddressBytes();
        if (bytes[0] == 10) return true; // 10.0.0.0/8
        if (bytes[0] == 172 && bytes[1] >= 16 && bytes[1] <= 31) return true; // 172.16.0.0/12
        if (bytes[0] == 192 && bytes[1] == 168) return true; // 192.168.0.0/16
        if (bytes[0] == 169 && bytes[1] == 254) return true; // 169.254.0.0/16 Link-Local
        return false;
    }

    [HttpGet("test")]
    public async Task<IActionResult> TestWebhook([FromQuery] string url) {
        if (!Uri.TryCreate(url, UriKind.Absolute, out var uri) || (uri.Scheme != "http" && uri.Scheme != "https")) {
            return BadRequest("Invalid URL scheme");
        }

        try {
            var response = await SafeClient.GetAsync(uri);
            var content = await response.Content.ReadAsStringAsync();
            return Ok(content);
        } catch (Exception) {
            return Forbid("Access to private/internal network rejected");
        }
    }
}

Checkliste für Engineering & Systemsicherheit

References