Parse — прекраснейший BaaS, позволяющий в кратчайшее время поднять полноценную серверную инфраструктуру для мобильного приложения. Возможно, именно из-за этой простоты многие разработчики и забывают о появляющихся проблемах безопасности и открывающихся уязвимостях.
Для тех, кто не знаком с сервисом, совершим небольшой экскурс в то, что он собой представляет. Parse предоставляет разработчику такие сервисы, как облачное хранилище данных, рассылку push-уведомлений, написание собственного API, сбор статистики, crash-логов и многое другое. В рамках этого исследования нас интересует именно хранилище данных, называемое Cloud Core.
Все данные в Parse хранятся в классах (по сути — таблицах), между записями которых можно устанавливать полноценные связи.
Для каждого из классов настраиваются клиентские права доступа, влияющие на возможность поиска, добавления новых записей, изменения существующих, и прочее. По умолчанию все действия разрешены. Конечно же, как водится, большинство разработчиков, один раз настроив нужные им таблицы, забывают о настройке клиентских разрешений.
Тесно столкнувшись на одном из рабочих проектов с Parse и повозившись с настройкой ACL, я решил поиграться и с чужими приложениями. Объект для исследования я выбрал прямо на parse.com/customers. Им стал Cubefree — сервис поиска мест для коворкинга.
Для подключения к аккаунту Parse в iOS приложении используется связка из двух ключей — Application ID и Client Key. Чтобы выполнять какие-либо действия над данными в Cloud Core первым делом нужно узнать эти данные. При помощи шикарной утилиты idb, автоматизирующей многие рутинные действия при пентестинге, расшифруем исполняемый файл приложения. Пока идет процесс, проверим NSUserDefaults — вполне вероятное место хранения интересующих нас ключей.
В этом случае все вполне безобидно — никаких конфиденциальных данных. Вернемся к расшифрованному бинарнику и скормим его дизассемблеру Hopper, специализирующемуся на реверс-инжиниринге приложений, написанных на Objective-C. Поиск ключей начнем с метода application:didFinishLaunchingWithOptions: в AppDelegate. Одна из замечательных возможностей Hopper — представление метода в виде псевдокода, который значительно понижает порог понимания расшифрованного кода.
Как и ожидалось, подключение к аккаунту Parse происходит именно здесь. Используя эти ключи, мы и будем анализировать структуру данных приложения и права доступа к ним.
Следующий шаг — поиск названий таблиц Parse. На самом деле, где их искать, становится понятно из этого же скриншота — сразу за подключением к серверу следует вызов методов registerSubclass у нескольких классов-наследников корневого PFObject. У каждого из них обязательно должен быть имплементирован метод parseClassName, отдающий интересующее нас имя таблицы на сервере.
Изучим структуру каждого из полученных таким образом классов:
PFQuery *query = [PFQuery queryWithClassName:@"ParseClassName"];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
NSLog(@"%@", objects);
}];
Тем не менее, знания одной лишь структуры недостаточно. Чтобы понять, каким образом мы можем повлиять на работу приложения, нужно определить права доступа ко всем классам Parse. Делается это достаточно просто — мы всего лишь выполняем соответствующие различным разрешениям запросы к серверу и анализируем их результат. Для упрощения этих рутинных действий я написал простенькую утилиту Parse Revealer, которая автоматически определяет уровни доступа ко всем известным классам.
На основании полученных нами данных можем построить таблицу:
Название класса | Структура данных | Права доступа |
---|---|---|
ChatRoom | chatId (String) user1 (User) user2 (User) |
GET: False FIND: True UPDATE: True CREATE: True DELETE: False ADD FIELDS: True |
Checkin | availableToShareTable (Bool) date (Date) invisible (Bool) statusCheckin (String) statusUser (String) user (User) workspace (Workspace) |
GET: True FIND: True UPDATE: True CREATE: True DELETE: True ADD FIELDS: True |
ChatMessage | chatId (String) Message (String) sender (User) unread (Bool) |
GET: False FIND: True UPDATE: True CREATE: True DELETE: False ADD FIELDS: True |
Notification | date (Date) sendUser (User) chekin (Cheking) status (Bool) type (Number) accepted (Bool) |
GET: True FIND: True UPDATE: True CREATE: True DELETE: False ADD FIELDS: True |
Review | date (Date) parkingStatus (Number) powerStatus (Number) soundStatus (Number) user (PFUser) wifiStatus (Number) workspace (Workspace) |
GET: True FIND: True UPDATE: False CREATE: True DELETE: False ADD FIELDS: True |
Workspace | address (String) cc (String) city (String) country (String) foursquareId (String) lat (String) lng (String) location (PFGeoPoint) name (String) postalCode (String) state (String) |
GET: True FIND: True UPDATE: True CREATE: True DELETE: False ADD FIELDS: True |
Как видно из полученных прав доступа, разработчики реализовали определенную политику безопасности, но, все ж таки, недостаточную. Покажем, каких результатов можно добиться, поиграв с классом ChatMessage.
Наиболее очевидная уязвимость — в любом из открытых чатов можно изменить как свои, так и чужие сообщения. После выполнения этого кода милое приветствие превращается в хабрасуицид:
PFQuery *query = [PFQuery queryWithClassName:@"ChatMessage"];
[query whereKey:@"message" equalTo:@"Привет, Хабр!"];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
PFObject *object = [objects firstObject];
object[@"message"] = @"Хабр, я тебя ненавижу!";
[object saveInBackground];
}];
Аналогичным образом можно добавлять и новые сообщения, достаточно лишь предоставить новому PFObject корректный chatId. Но стоит отметить, что Delete, установленный в false, не даст нам удалить ни одного из созданных объектов.
Гораздо более серьезная уязвимость заключается в некорректном маппинге данных, полученных из Parse. Если у свежесозданного объекта ChatMessage будет отсутствовать поле sender — приложение крашится. Таким образом, ничто не мешает нам пробежаться по всем когда либо созданным часам, добавить в них невалидное сообщение — и приложение будет вылетать у всех пользователей. Это уже чревато низкими рейтингами в App Store, оттоком пользователей и неудачей проекта в целом.
Подобные уязвимости есть и у остальных классов — но они уже находятся за рамками текущего исследования.
Что касается обеспечения безопасности — здесь все достаточно прозрачно. Нужно следовать лишь нескольким правилам:
- Всегда настраивайте уровни доступа для всех созданных классов.
- Для создаваемых пользователем данных используйте ACL, позволяя изменять их только определенному кругу лиц.
- Если клиенту необходимо изменять только одно из свойств (к примеру, флаг unread) — стоит задуматься о выделении его в отдельную таблицу. Таким образом, можно будет обойти возможность изменения других параметров объекта.
- Не стоит полагаться на то, что Parse всегда будет отдавать валидные данные — не забывайте встраивать соответствующие проверки.
- Не забывайте и о том, что, теоретически, applicationId и clientKey могут быть доступны любому злоумышленнику, и продумывайте политику безопасности, основываясь на этом знании.
- Предыдущее правило не означает, что нужно полностью забыть об обфускации строк в коде :)
- В особо сложных случаях не стесняйтесь использовать Cloud Code.
Если в этом исследовании вы увидите черты и своих приложений, не стоит ругать Parse — как я уже говорил, это отличный сервис, минимизирующий затраты на создание серверной части приложения. А все рассмотренные уязвимости лежат только на ответственности разработчиков приложения.
Полезные ссылки:
- Документация Parse
- Цикл мини-статей «Parse Security»
- Parse Revealer
- The Hopper Disassembler by Mike Ash
Другие материалы, посвященные обеспечению безопасности iOS приложений: