云安全 STS Policy注入
参考:【云安全】万字剖析STS Policy 注入在对象存储直传与攻击中的利用链:从 STS 越权到 Unicode 二次绕过-先知社区
STS是什么
关于STS,很多人挖洞看到的通常是AK、SK、还有一个Token,比如长这个样子。

然后测试的时候一般会尝试能不能../../../或者直接调用一些现有的安全工具对这个下发的sts安全凭证进行测试
看一下腾讯云对STS的描述

简单来说,就是由可信后端保管长期凭证,或者通过 RAM 角色获取调用 STS 的权限(这里用的比较多的就是PutObject和GetObject),再针对当前这一次上传签发一组临时凭证。这组凭证的有效期可以很短,权限也可以通过会话 Policy 进一步收窄,比如只允许向指定存储桶、指定目录,甚至指定对象上传文件。这样一来,长期 AK/SK 就不需要进入前端;即使临时凭证意外泄露,攻击者能够利用的时间和权限范围也都比较有限。
输入流入侵控制流
这里我们需要先去创建一个RAM 用户,拿到AccessKey ID 和 AccessKey Secret
| |
在权限编辑页面,我们可以选择对角色权限进行编辑
| |
为什么不能通过目录穿越进行任意文件读取
在传统文件系统中: exampledir/../../target.txt 会被路径解析函数归一化为: ../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 的对象。

这里可以看到,前端请求中存在一个 fileKey 参数,它的值类似于:/2025-05-21/xxxx.xlsx,如果服务端没有自行生成 Object Key,而是直接使用前端提交的 fileKey 构造 STS 会话 Policy,那么我们就可以尝试把其中的2025-05-21 修改成其他日期,例如:2025-05-20/xxx.xlsx ,如果后端没有校验这个日期前缀是否属于当前用户,就可能据此生成下面这段权限:acs:oss:::这里是存储桶的地域,不用管,是场景方自己配置的/2025-05-20/* , 假设 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 拒绝。
至于防御,厂商有禁止同名文件覆盖的参数,OSS 的 PutObject 支持:x-oss-forbid-overwrite: true
输入流入侵控制流再到云Policy注入
首先是后端写法,我们在真实业务场景中一般不会对Policy生成的STS做List Object权限,一般是给Put和Get权限
| |
这里我们的前端可以控制上传的文件名来拿到STS,同时通过STS和文件内容,来对文件进行上传,通过获取到的STS也是不能直接对OSS Browser拥有登录权限的
这里作者写的靶场代码在获取/api/v1/sts路由过来的fulPathFid参数后,发送给了AssumeRole(fullPathFid),然后继续传递到了下一层的provider.go
这里的第一个参数为bucket的值,是不可控的。第二个是fullpath的值,也就是文件名,此时文件名可控,所以最后的数据流转过程就是
| |
然后我们可以用代码拼接注入的思想看我们前面配置的策略
| |
这里我们只需要构造payload如下
| |
传入时Payload 会被放进原始 Policy 模板中第二个 %s 对应的位置
最终得到
| |
这里我们依旧需要先理解几个基本概念。前面已经提到,Action 代表允许执行的操作,Resource 代表这些操作可以作用的资源。原本的代码只给 Policy 配置了:
| |
我们注入后有列桶的权限,这里出现了重复键 Action。绝大多数 JSON 解析器对重复键采用"后者覆盖",于是这条 Statement 的 Action 最终是 ["*",""],Resource 是 [bucket/*, *]——一条 Statement、Statement 数还是 1,但权限已经被放大到"任意动作、任意资源"(受角色上限约束)。
其他云的policy注入
上面已阿里云为例,下面腾讯云也是类似
| |
只需要构造
| |
注入后变成
| |
这里需要注意:腾讯云会拒绝重复键,因此,腾讯云的做法不是在同一条 Statement 中重复写一个 action,然后等待后者覆盖前者,而是先把原来的 Statement 关闭,再追加一条全新的 Statement。statement 本身就是一个数组,数组里的每个对象就是一条独立的授权语句,所以第二个 { “effect”: “allow” … } 直接作为 statement 数组的第二个元素即可。
