@shinyaz

AWS Security Agent 検証 — オンデマンド・ペネトレーションテストの検出品質とコスト

目次

はじめに

2026年3月31日、AWS は AWS Security Agent のオンデマンド・ペネトレーションテストの一般提供(GA)を発表した。AI エージェントが自律的にアプリケーションの脆弱性を発見・検証する「フロンティアエージェント」で、従来の手動ペネトレーションテストを「数週間から数時間に短縮する」と謳っている。

AWS Security Agent は3つの機能を持つ:

  1. ペネトレーションテスト(Penetration Testing) — オンデマンドの AI 駆動ペネトレーションテスト($50/タスク時間)[GA]
  2. 設計レビュー(Design Security Review) — 設計ドキュメントのリアルタイムセキュリティレビュー(無料、月200回)[Preview]
  3. コードレビュー(Code Security Review) — PR に対する自動セキュリティレビュー(無料、月1,000回)[Preview]

今回 GA となったのはペネトレーションテストのみで、設計レビューとコードレビューは Preview のままである。本記事では GA となったペネトレーションテストを検証する。OWASP Top 10 だけでなくビジネスロジックの欠陥も検出できるとされるが、$50/タスク時間の価格に見合う検出品質なのか。意図的に5つの脆弱性を埋め込んだ Flask アプリに対してテストを実行し、検出率・Finding 品質・コストを定量的に評価する。公式ドキュメントは AWS Security Agent User Guide

前提条件:

  • AWS CLI v2(2.34.21 で動作確認済み)
  • IAM ロール作成権限、EC2 / VPC の操作権限
  • 検証リージョン: ap-northeast-1(東京)
  • 料金: $50/タスク時間(秒単位課金)。新規顧客は2ヶ月の無料トライアル(月200タスク時間)

対応リージョンは us-east-1、us-west-2、eu-west-1、eu-central-1、ap-southeast-2、ap-northeast-1 の6つ。

結果だけ見たい場合はまとめに進んでほしい。

検証 1: CLI セットアップ(Agent Space 〜 ペネトレーションテスト作成)

Security Agent のリソースモデルは3層構造である。Application はアカウント全体の Security Agent 設定で、1アカウントにつき1つのみ作成できる(コンソールでの初回セットアップ時に自動作成)。Agent Space はテスト対象をグルーピングする論理コンテナで、VPC・IAM ロール・Target Domain を紐付ける。Target Domain はテスト許可されたドメインの登録で、所有権の証明が必要である。

今回は Agent Space の作成からペネトレーションテスト作成までを CLI のみで実行した。なお、ステップ 3〜5 では EC2 のプライベート DNS や VPC 情報が必要なため、先に検証 2 の「EC2 デプロイ手順」を実行し、$VPC_ID$SUBNET_ID$SG_ID$PRIVATE_DNS を取得してから本セクションに戻ること。セットアップは以下の5ステップで構成される:

  1. IAM ロール作成(Security Agent サービスプリンシパル用)
  2. Agent Space 作成
  3. Target Domain 登録・検証(テスト対象のドメインの所有権証明)
  4. Agent Space に VPC・IAM ロール・Target Domain を関連付け
  5. ペネトレーションテスト作成
IAM ロール・Agent Space 作成手順
Terminal (IAM ロール作成)
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
REGION=ap-northeast-1
 
# Security Agent 用サービスロール
cat > securityagent-trust-policy.json << EOF
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "Service": "securityagent.amazonaws.com" },
    "Action": "sts:AssumeRole",
    "Condition": {
      "StringEquals": { "aws:SourceAccount": "${ACCOUNT_ID}" }
    }
  }]
}
EOF
 
aws iam create-role \
  --role-name SecurityAgentPentestRole \
  --assume-role-policy-document file://securityagent-trust-policy.json
 
# VPC アクセス用ポリシー(ENI 作成に必要)
aws iam attach-role-policy \
  --role-name SecurityAgentPentestRole \
  --policy-arn arn:aws:iam::aws:policy/AmazonVPCFullAccess
 
# CloudWatch Logs ポリシー(ドキュメント未記載だが必須)
cat > logs-policy.json << EOF
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
    "Resource": "arn:aws:logs:${REGION}:${ACCOUNT_ID}:log-group:/aws/securityagent/*"
  }]
}
EOF
 
aws iam put-role-policy \
  --role-name SecurityAgentPentestRole \
  --policy-name SecurityAgentLogsAccess \
  --policy-document file://logs-policy.json
Terminal (Agent Space 作成)
AGENT_SPACE_ID=$(aws securityagent create-agent-space \
  --name "pentest-verification" \
  --region $REGION \
  --query "agentSpaceId" --output text)
 
echo "Agent Space: $AGENT_SPACE_ID"
Target Domain 登録・検証

テスト対象のドメインを登録し、所有権を証明する必要がある。検証方法は DNS_TXT(DNS レコード追加)と HTTP_ROUTE(HTTP エンドポイントにトークン配置)の2種類。今回は HTTP_ROUTE を使用した。

Terminal (Target Domain 登録)
TARGET_DOMAIN_ID=$(aws securityagent create-target-domain \
  --target-domain-name "$PRIVATE_DNS" \
  --verification-method HTTP_ROUTE \
  --region $REGION \
  --query "targetDomainId" --output text)
 
# 検証トークンを取得
aws securityagent batch-get-target-domains \
  --target-domain-ids $TARGET_DOMAIN_ID \
  --region $REGION \
  --query "targetDomains[0].verificationDetails.httpRoute"
Output
{
  "token": "Vdm6-_8EVhQZR3Xf0hOwug",
  "routePath": ".well-known/aws/securityagent-domain-verification.json"
}

取得したトークンを http://<domain>/.well-known/aws/securityagent-domain-verification.json で返すよう設定し、verify-target-domain を実行する。

注意: verify-target-domain CLI コマンドの HTTP_ROUTE 検証はパブリックアクセスが必要である。 compute.internal のようなプライベート DNS の場合、CLI の検証ステータスは UNREACHABLE になった。ただし VPC Config を使ったペネトレーションテストでは、PREFLIGHT ステップ中に VPC 内部から別途ドメイン検証が行われ、こちらは正常に通過した(ログに「Verifying ownership of private network domains → Completed」と記録)。つまり CLI のドメイン検証が UNREACHABLE でも、VPC Config 経由のテスト実行には影響しない。

Agent Space 更新・ペネトレーションテスト作成
Terminal (Agent Space に VPC・IAM ロール・Target Domain を関連付け)
SERVICE_ROLE_ARN="arn:aws:iam::${ACCOUNT_ID}:role/SecurityAgentPentestRole"
 
aws securityagent update-agent-space \
  --agent-space-id $AGENT_SPACE_ID \
  --name "pentest-verification" \
  --aws-resources "{
    \"vpcs\": [{
      \"vpcArn\": \"$VPC_ID\",
      \"securityGroupArns\": [\"$SG_ID\"],
      \"subnetArns\": [\"$SUBNET_ID\"]
    }],
    \"iamRoles\": [\"$SERVICE_ROLE_ARN\"]
  }" \
  --target-domain-ids "$TARGET_DOMAIN_ID" \
  --region $REGION
Terminal (ペネトレーションテスト作成)
PENTEST_ID=$(aws securityagent create-pentest \
  --title "vulnerable-flask-app-pentest" \
  --agent-space-id $AGENT_SPACE_ID \
  --service-role $SERVICE_ROLE_ARN \
  --assets "{
    \"endpoints\": [{\"uri\": \"http://${PRIVATE_DNS}\"}]
  }" \
  --vpc-config "{
    \"vpcArn\": \"$VPC_ID\",
    \"securityGroupArns\": [\"$SG_ID\"],
    \"subnetArns\": [\"$SUBNET_ID\"]
  }" \
  --region $REGION \
  --query "pentestId" --output text)
 
echo "Pentest: $PENTEST_ID"

なお、--code-remediation-strategy AUTOMATIC を指定したところ 「Code remediation is not supported in ap-northeast-1」 というエラーが返った。コード修正自動生成は東京リージョンでは未対応である(2026年4月1日時点)。

セットアップ結果

ステップ所要時間
IAM ロール作成 + ポリシーアタッチ約8秒
Agent Space 作成約3秒
Target Domain 登録約2秒
Agent Space 更新(VPC・IAM・Target Domain)約3秒
ペネトレーションテスト作成約2秒
合計約18秒

CLI のみで完結し、18秒でテスト設定が完了する。ただし初回セットアップでは以下の制約に注意:

  • 1アカウント1 Application 制限(コンソールで初回セットアップ済みなら CLI からの作成は不要)
  • サービスロールは Agent Space の aws-resources.iamRoles に事前登録が必須
  • サービスロールに CloudWatch Logs 権限(logs:CreateLogGroup 等)が必須(ドキュメント未記載)
  • --code-remediation-strategy AUTOMATIC は東京リージョン未対応

検証 2: ペネトレーションテスト実行と Finding 品質分析

テスト対象アプリ

意図的に5つの脆弱性を埋め込んだ Flask アプリを EC2(t3.small)にデプロイした。本番環境ではアプリケーションがプライベートサブネットに配置されるケースが多いため、パブリックエンドポイントではなく VPC Config を使ったプライベートエンドポイントへのテストを選択した。Security Agent はプライベート DNS 経由で VPC 内からアクセスする。

脆弱性は「従来の DAST ツールでも検出できる定型パターン2つ」と「エンドポイントの発見や認証コンテキストの理解が必要なもの3つ」を意図的に混在させた。Security Agent の「コンテキスト認識型」アプローチが定型パターン以外でどこまで有効かを測るためである。

#脆弱性カテゴリ検出難易度(予想)
1SQL Injection(ログインフォーム)インジェクション低(定型パターン)
2Stored XSS(コメント機能)XSS低(定型パターン)
3IDOR(他ユーザーのプロフィール閲覧)アクセス制御中(認証コンテキスト必要)
4JWT 検証不備(署名未検証)認証中(ビジネスロジック)
5Path Traversal(ファイルダウンロード)インジェクション中(定型パターンだが HTML 上にリンクなし)
Flask アプリのソースコード(app.py 全文)
app.py
"""
Deliberately vulnerable Flask application for Security Agent penetration testing.
WARNING: Do NOT deploy in production environments.
"""
import os, sqlite3, hashlib, json, base64
from flask import Flask, request, jsonify, g, send_file
 
app = Flask(__name__)
app.config["SECRET_KEY"] = "hardcoded-secret-key-123"
DATABASE = "/tmp/vuln_app.db"
UPLOAD_DIR = "/tmp/uploads"
os.makedirs(UPLOAD_DIR, exist_ok=True)
 
def get_db():
    if "db" not in g:
        g.db = sqlite3.connect(DATABASE)
        g.db.row_factory = sqlite3.Row
    return g.db
 
@app.teardown_appcontext
def close_db(exception):
    db = g.pop("db", None)
    if db is not None:
        db.close()
 
def init_db():
    db = sqlite3.connect(DATABASE)
    db.executescript("""
        CREATE TABLE IF NOT EXISTS users (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            username TEXT UNIQUE NOT NULL,
            password TEXT NOT NULL,
            email TEXT, role TEXT DEFAULT 'user');
        CREATE TABLE IF NOT EXISTS comments (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            user_id INTEGER, content TEXT NOT NULL,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);
    """)
    db.execute("INSERT OR IGNORE INTO users (username, password, email, role) VALUES (?, ?, ?, ?)",
               ("admin", hashlib.md5(b"admin123").hexdigest(), "admin@example.com", "admin"))
    db.execute("INSERT OR IGNORE INTO users (username, password, email, role) VALUES (?, ?, ?, ?)",
               ("alice", hashlib.md5(b"password1").hexdigest(), "alice@example.com", "user"))
    db.execute("INSERT OR IGNORE INTO users (username, password, email, role) VALUES (?, ?, ?, ?)",
               ("bob", hashlib.md5(b"password2").hexdigest(), "bob@example.com", "user"))
    db.commit()
    db.close()
 
# --- Vulnerability 1: SQL Injection ---
@app.route("/api/login", methods=["POST"])
def login():
    data = request.get_json()
    username, password = data.get("username", ""), data.get("password", "")
    password_hash = hashlib.md5(password.encode()).hexdigest()
    query = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password_hash}'"
    try:
        user = get_db().execute(query).fetchone()
    except Exception as e:
        return jsonify({"error": str(e)}), 500
    if user:
        payload = base64.b64encode(json.dumps(
            {"user_id": user["id"], "username": user["username"], "role": user["role"]}
        ).encode()).decode()
        return jsonify({"token": f"header.{payload}.nosignature", "user_id": user["id"]})
    return jsonify({"error": "Invalid credentials"}), 401
 
# --- Vulnerability 2: Stored XSS ---
@app.route("/api/comments", methods=["GET"])
def get_comments():
    comments = get_db().execute(
        "SELECT c.id, c.content, c.created_at, u.username "
        "FROM comments c JOIN users u ON c.user_id = u.id ORDER BY c.created_at DESC"
    ).fetchall()
    return jsonify([dict(c) for c in comments])
 
@app.route("/api/comments", methods=["POST"])
def post_comment():
    token = request.headers.get("Authorization", "")
    user = decode_token(token)
    if not user:
        return jsonify({"error": "Unauthorized"}), 401
    content = request.get_json().get("content", "")
    get_db().execute("INSERT INTO comments (user_id, content) VALUES (?, ?)", (user["user_id"], content))
    get_db().commit()
    return jsonify({"message": "Comment posted"}), 201
 
# --- Vulnerability 3: IDOR ---
@app.route("/api/users/<int:user_id>", methods=["GET"])
def get_user_profile(user_id):
    token = request.headers.get("Authorization", "")
    user = decode_token(token)
    if not user:
        return jsonify({"error": "Unauthorized"}), 401
    profile = get_db().execute("SELECT id, username, email, role FROM users WHERE id = ?",
                               (user_id,)).fetchone()
    return jsonify(dict(profile)) if profile else (jsonify({"error": "Not found"}), 404)
 
# --- Vulnerability 4: JWT signature not verified ---
def decode_token(auth_header):
    try:
        token = auth_header.split("Bearer ")[1]
        payload = json.loads(base64.b64decode(token.split(".")[1] + "=="))
        return payload
    except Exception:
        return None
 
# --- Vulnerability 5: Path Traversal ---
@app.route("/api/files/download", methods=["GET"])
def download_file():
    filename = request.args.get("filename", "")
    filepath = os.path.join(UPLOAD_DIR, filename)
    return send_file(filepath) if os.path.exists(filepath) else (jsonify({"error": "Not found"}), 404)
 
@app.route("/health")
def health():
    return jsonify({"status": "ok"})
 
@app.route("/")
def index():
    comments = get_db().execute(
        "SELECT c.content, u.username FROM comments c JOIN users u ON c.user_id = u.id"
    ).fetchall()
    comments_html = "".join(f"<div class='comment'><b>{c['username']}</b>: {c['content']}</div>" for c in comments)
    return f"<!DOCTYPE html><html><head><title>Vulnerable App</title></head><body>"  \
           f"<h1>Comment Board</h1>{comments_html}</body></html>"
 
if __name__ == "__main__":
    init_db()
    app.run(host="0.0.0.0", port=80)
EC2 デプロイ手順

デフォルト VPC を使用する。Security Agent は並列に13カテゴリの攻撃を実行するため、Flask 開発サーバー(シングルスレッド)ではクラッシュする。gunicorn(マルチワーカー)が必要である。

Terminal (環境変数の準備)
REGION=ap-northeast-1
VPC_ID=$(aws ec2 describe-vpcs --filters "Name=isDefault,Values=true" \
  --query "Vpcs[0].VpcId" --output text --region $REGION)
VPC_CIDR=$(aws ec2 describe-vpcs --vpc-ids $VPC_ID \
  --query "Vpcs[0].CidrBlock" --output text --region $REGION)
SUBNET_ID=$(aws ec2 describe-subnets --filters "Name=vpc-id,Values=$VPC_ID" \
  --query "Subnets[0].SubnetId" --output text --region $REGION)
AMI_ID=$(aws ssm get-parameters \
  --names /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64 \
  --query "Parameters[0].Value" --output text --region $REGION)
Terminal (EC2 用 IAM ロール・インスタンスプロファイル作成)
# SSM 接続用ロール
cat > ec2-trust-policy.json << 'EOF'
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "Service": "ec2.amazonaws.com" },
    "Action": "sts:AssumeRole"
  }]
}
EOF
 
aws iam create-role --role-name SecurityAgentPentestEC2Role \
  --assume-role-policy-document file://ec2-trust-policy.json
aws iam attach-role-policy --role-name SecurityAgentPentestEC2Role \
  --policy-arn arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
aws iam create-instance-profile \
  --instance-profile-name SecurityAgentPentestEC2Profile
aws iam add-role-to-instance-profile \
  --instance-profile-name SecurityAgentPentestEC2Profile \
  --role-name SecurityAgentPentestEC2Role
 
sleep 10  # IAM 伝播待ち
Terminal (SG・EC2 起動)
# SG: VPC 内からの HTTP 80 のみ許可
SG_ID=$(aws ec2 create-security-group \
  --group-name security-agent-pentest-target \
  --description "Pentest target - HTTP from VPC only" \
  --vpc-id $VPC_ID --region $REGION --query "GroupId" --output text)
 
aws ec2 authorize-security-group-ingress \
  --group-id $SG_ID --protocol tcp --port 80 \
  --cidr $VPC_CIDR --region $REGION
 
# EC2 起動
INSTANCE_ID=$(aws ec2 run-instances \
  --image-id $AMI_ID --instance-type t3.small \
  --subnet-id $SUBNET_ID --security-group-ids $SG_ID \
  --iam-instance-profile Name=SecurityAgentPentestEC2Profile \
  --associate-public-ip-address \
  --tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=security-agent-pentest-target}]" \
  --region $REGION --query "Instances[0].InstanceId" --output text)
 
aws ec2 wait instance-running --instance-ids $INSTANCE_ID --region $REGION
 
PRIVATE_DNS=$(aws ec2 describe-instances --instance-ids $INSTANCE_ID \
  --query "Reservations[0].Instances[0].PrivateDnsName" --output text --region $REGION)
echo "Private DNS: $PRIVATE_DNS"
Terminal (アプリデプロイ — SSM 経由)
# SSM Agent オンライン待ち
while true; do
  STATUS=$(aws ssm describe-instance-information \
    --filters "Key=InstanceIds,Values=${INSTANCE_ID}" \
    --query "InstanceInformationList[0].PingStatus" --output text --region $REGION 2>/dev/null)
  [ "$STATUS" = "Online" ] && break
  echo "Waiting for SSM Agent..." && sleep 10
done
 
# app.py を EC2 に転送(base64 エンコード経由)
APP_B64=$(base64 -w0 app.py)
aws ssm send-command --instance-ids $INSTANCE_ID \
  --document-name "AWS-RunShellScript" \
  --parameters "{\"commands\":[
    \"sudo mkdir -p /opt/vulnerable-app\",
    \"echo '$APP_B64' | base64 -d | sudo tee /opt/vulnerable-app/app.py > /dev/null\"
  ]}" --region $REGION
 
# gunicorn インストール + DB 初期化 + 起動
aws ssm send-command --instance-ids $INSTANCE_ID \
  --document-name "AWS-RunShellScript" \
  --timeout-seconds 120 \
  --parameters '{"commands":[
    "sudo python3 -m ensurepip --upgrade",
    "sudo python3 -m pip install flask==3.1.0 gunicorn",
    "cd /opt/vulnerable-app && sudo python3 -c \"exec(open('app.py').read().split('if __name__')[0]); init_db()\"",
    "sudo systemd-run --unit=vuln-app --working-directory=/opt/vulnerable-app gunicorn --bind 0.0.0.0:80 --workers 4 --timeout 120 app:app"
  ]}' --region $REGION
 
sleep 5
# 動作確認
aws ssm send-command --instance-ids $INSTANCE_ID \
  --document-name "AWS-RunShellScript" \
  --parameters '{"commands":["curl -s http://localhost/health"]}' \
  --region $REGION

テスト実行

Terminal
aws securityagent start-pentest-job \
  --pentest-id $PENTEST_ID \
  --agent-space-id $AGENT_SPACE_ID \
  --region $REGION
Output
{
  "status": "IN_PROGRESS",
  "pentestJobId": "pj-d63d1394-8caa-455c-a966-e384eb266718"
}

進捗は batch-get-pentest-jobs で確認できる。テスト完了まで約2時間43分かかった。

Terminal (進捗確認)
aws securityagent batch-get-pentest-jobs \
  --agent-space-id $AGENT_SPACE_ID \
  --pentest-job-ids <pentestJobId> \
  --region $REGION \
  --query "pentestJobs[0].{status:status,steps:steps[*].{name:name,status:status}}"

テストは4つのステップで進行する:

  1. PREFLIGHT — VPC 内にコンテナ環境を構築し、エンドポイントの到達性を確認
  2. STATIC_ANALYSIS — エンドポイントのクロール、ネットワークスキャン、TLS スキャン
  3. PENTEST — 実際のペネトレーションテスト(多段階攻撃シナリオの実行)
  4. FINALIZING — 結果の集約、CVSS スコアリング、レポート生成

CloudWatch Logs(/aws/securityagent/<agent-space-name>/<pentest-id>)に詳細な実行ログが出力される。実際に観測された内部タスクは以下の通り:

タスク所要時間内容
Setup Testing Environment約26分VPC 内にコンピュート環境・ネットワーキング・コンテナを構築
Endpoint Accessibility Validation数秒エンドポイントへの HTTP 接続確認
SCANNER約7秒ネットワークスキャン(ポート・サービス検出)
TLS SCANNER約8秒TLS 設定のスキャン
CRAWLER約29分複数の AI エージェントがブラウザと Python スクリプトで並列にエンドポイントを探索
PENTEST(攻撃タスク群)約1時間37分13カテゴリの攻撃を並列実行 + VALIDATOR TASK で検証

特筆すべきは CRAWLER の動作である。CloudWatch Logs から確認できたのは、複数の AI エージェントがブラウザセッション(browse ツール)と Python の requests ライブラリを並列に使い、アプリのエンドポイントを自律的に発見・分析していることだ(1回目の実行ログでは少なくとも4つの異なる agent_id を確認)。フォーム構造の解析、未知パス(/admin/api/v1/graphql 等)のプローブも行っており、従来の DAST ツールのクローラーとは異なるアプローチである。

PENTEST フェーズでは OWASP Top 10 を中心とした13カテゴリの攻撃タスク(XSS、SQLi、IDOR、Path Traversal、SSRF、SSTI、Code Injection、Command Injection、LFI、XXE、JWT、Privilege Escalation、Arbitrary File Upload)が並列実行された。各攻撃タスクの完了後、VALIDATOR TASK が Finding の再現検証を行い、confidence を確定する。

なお、1回目の実行では Flask の開発サーバー(シングルスレッド)が並列攻撃リクエストに耐えられずクラッシュし、テストが FAILED になった。2回目は gunicorn(4ワーカー)に切り替えて正常完了した。ペンテスト対象のアプリケーションサーバーにはマルチワーカー構成が必要である。

結果分析

テスト完了後、list-findingsbatch-get-findings で検出結果を取得する。

Terminal (Finding 取得)
# Finding 一覧
aws securityagent list-findings \
  --agent-space-id $AGENT_SPACE_ID \
  --pentest-job-id <pentestJobId> \
  --region $REGION
 
# Finding 詳細(再現手順・CVSS・影響分析を含む)
aws securityagent batch-get-findings \
  --agent-space-id $AGENT_SPACE_ID \
  --finding-ids <findingId1> <findingId2> ... \
  --region $REGION

テスト全体の所要時間は約2時間43分(PREFLIGHT 約32分 + STATIC_ANALYSIS 約29分 + PENTEST 約1時間37分 + FINALIZING 約6分)。

脆弱性検出CVSSconfidence備考
SQL Injection9.8 CRITICALHIGH' OR '1'='1 で認証バイパスを再現。認証情報も admin:admin123 を発見
Stored XSS6.4 MEDIUMLOW<img onerror> ペイロードで JS 実行を確認。CSP ヘッダー未設定も指摘
IDOR6.5 MEDIUMHIGHalice のトークンで GET /api/users/1(admin)にアクセスし情報取得を再現
JWT 検証不備10.0 CRITICALHIGH署名なしトークンで任意の user_id/role を設定し特権昇格を再現
Path Traversal--未検出
想定外: Integer Overflow4.3 MEDIUMHIGHGET /api/users/99999999999999999 で 500 エラー。埋め込んでいない脆弱性

検出率: 4/5(80%)、偽陽性: 0件

Path Traversal が未検出だった理由は確定できないが、/api/files/download?filename= は HTML ページ上にリンクがなく、API ドキュメントやソースコードも提供していなかったため、CRAWLER がエンドポイント自体を発見できなかった可能性が高い。ソースコードをアーティファクトとして登録すれば CRAWLER の探索範囲が広がるが、それで検出されるかは未検証である。

一方で、埋め込んでいない Integer Overflow の脆弱性を追加で発見した点は評価できる。user_id パラメータに極端な値を入力することで 500 Internal Server Error が発生するという、入力バリデーション不備の問題である。

Finding の品質は高い。各 Finding には以下が含まれる:

  • 詳細な再現手順 — curl コマンド形式で、コピー&ペーストで再現可能
  • CVSS スコア — 9.8(SQL Injection)、10.0(JWT)など、妥当なスコアリング
  • 影響分析 — セッションハイジャック、データ漏洩等の具体的な影響を記述
  • 修正ガイダンス — パラメータ化クエリ、出力エスケープ等の具体的な修正方法(Finding の description 内)

コスト

上記の所要時間(約2時間43分)を $50/タスク時間で換算すると $136(秒単位課金)。新規顧客の2ヶ月無料トライアル(月200タスク時間)を利用すれば、この規模のテストは無料枠内で十分実行可能である。

まとめ — どのようなチームに向いているか

今回の検証では、小規模アプリ(6エンドポイント)に対して5つの既知脆弱性のうち4つを検出し、偽陽性は0件だった。定期テストの間を埋める継続的セキュリティテストとして実用的と判断できる。特にビジネスロジック脆弱性(IDOR、JWT 検証不備)を HIGH confidence で検出できた点は、従来の DAST ツールとの明確な差別化ポイントだ。

観点結果評価
CLI セットアップ18秒(IAM ロール〜ペンテスト作成)
PREFLIGHT(VPC 環境構築)約32分
テスト全体の所要時間約2時間43分(6エンドポイントの小規模アプリ)
検出率4/5(80%)+ 想定外の脆弱性1件
偽陽性0件
Finding 品質再現手順・CVSS・影響分析・修正ガイダンス付き
ビジネスロジック脆弱性IDOR・JWT 検証不備を HIGH confidence で検出
VPC 内テストVPC Config で対応
コード修正提案東京リージョン未対応
コスト$136(2時間43分 × $50/タスク時間)
  • 定期的な手動ペンテストの間を埋める用途に最適 — 手動ペンテスト(数十万〜数百万円/回)は年1-2回が一般的。Security Agent なら約$136/回で開発サイクルに合わせた頻度でテストでき、2ヶ月の無料トライアルで評価もできる
  • ソースコード / API ドキュメントのアーティファクト登録で検出率向上の余地あり — 今回は URL のみ提供で 4/5 の検出率だった。未検出の Path Traversal は HTML 上にリンクのないエンドポイントだったため、ソースコードを登録すれば CRAWLER の探索範囲が広がる可能性がある(未検証)
  • CI/CD に組み込む場合は PREFLIGHT の約32分を考慮 — VPC 内テストでは環境構築に時間がかかるため、デプロイ前ゲートではなく夜間バッチや週次スケジュールでの実行が現実的
  • テスト対象サーバーはマルチワーカー構成が必須 — 13カテゴリの攻撃が並列実行されるため、シングルスレッドサーバーでは負荷に耐えられない。本番環境では問題にならないが、検証環境では gunicorn 等の導入を忘れずに

実運用に向けた注意点

  1. VPC Config 利用時のドメイン検証 — CLI の verify-target-domain はパブリックからのアクセスのためプライベート DNS では UNREACHABLE になるが、VPC Config 経由のペンテストでは PREFLIGHT 中に VPC 内部検証が別途行われるため、テスト実行には影響しない。ただし Target Domain の登録自体は必要
  2. CloudWatch Logs 権限の追加を忘れずに — サービスロールに logs:CreateLogGroup 権限がないと PREFLIGHT で即失敗する。公式ドキュメントに記載がないため、IAM ポリシー作成時に忘れやすい
  3. Code Remediation は東京リージョン未対応--code-remediation-strategy AUTOMATIC は 東京リージョンでは未対応(対応リージョンは未確認)。修正提案が必要な場合はリージョンの選定が必要

クリーンアップ

リソース削除手順
Terminal
# ペネトレーションテスト削除
aws securityagent batch-delete-pentests \
  --agent-space-id $AGENT_SPACE_ID \
  --pentest-ids $PENTEST_ID \
  --region $REGION
 
# Target Domain 削除
aws securityagent delete-target-domain \
  --target-domain-id $TARGET_DOMAIN_ID \
  --region $REGION
 
# Agent Space 削除
aws securityagent delete-agent-space \
  --agent-space-id $AGENT_SPACE_ID \
  --region $REGION
 
# EC2 インスタンス終了
aws ec2 terminate-instances --instance-ids $INSTANCE_ID --region $REGION
aws ec2 wait instance-terminated --instance-ids $INSTANCE_ID --region $REGION
aws ec2 delete-security-group --group-id $SG_ID --region $REGION
 
# IAM ロール削除
aws iam detach-role-policy --role-name SecurityAgentPentestRole \
  --policy-arn arn:aws:iam::aws:policy/AmazonVPCFullAccess
aws iam delete-role-policy --role-name SecurityAgentPentestRole \
  --policy-name SecurityAgentLogsAccess 2>/dev/null
aws iam delete-role --role-name SecurityAgentPentestRole
 
aws iam detach-role-policy --role-name SecurityAgentPentestEC2Role \
  --policy-arn arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
aws iam delete-instance-profile --instance-profile-name SecurityAgentPentestEC2Profile
aws iam delete-role --role-name SecurityAgentPentestEC2Role

共有する

田原 慎也

田原 慎也

ソリューションアーキテクト @ AWS

AWS ソリューションアーキテクトとして金融業界のお客様を中心に技術支援をしており、クラウドアーキテクチャや AI/ML に関する学びをこのサイトで発信しています。このサイトの内容は個人の見解であり、所属企業の公式な意見や見解を代表するものではありません。

関連記事