Уязвимость ВКонтакте: отправляем сообщение с кодом восстановления страницы на чужой номер
Уязвимость ВКонтакте: отправляем сообщение с кодом восстановления страницы на чужой номер
:extract_focal()/https%3A%2F%2Fhabrastorage.org%2Fweb%2Fce1%2Ff8f%2Fc02%2Fce1f8fc0222f4b3db640ccbb3838d858.png)
Обычным весенним днем, занимаясь «подготовкой» к ЕГЭ по информатике, наткнулся на статью об уязвимости Facebook, позволявшей взломать все аккаунты в социальной сети, за которую выплатили 15000$. Суть уязвимости заключалась в переборе кодов восстановления на тестовом домене компании. Я подумал, а чем собственно ВКонтакте хуже? И решил попробовать провернуть подобный трюк у них. Зная, что веб-версия уже достаточно хорошо исследована, жертвой должен был стать Android клиент, а что из этого вышло можно прочитать под катом.
Смотрим трафик
Первым делом я захотел узнать, какую информацию приложение передает в сеть во время процесса восстановления страницы. Помощником в этом деле выступил Fiddler, я настроил его и Android устройство, как написано в официальной документации. Таким образом в Fiddler становятся доступны все HTTP/HTTPS запросы c устройства. Теперь, в приложении, смело выходим из аккаунта ВКонтакте и нажимаем на кнопку «Забыли пароль?». После ввода номера телефона приложение отправляет 2 HTTPS запроса. Особую ценность представляет второй, потому что именно он отвечает за отправку SMS с кодом восстановления.
:extract_focal()/https%3A%2F%2Fhabrastorage.org%2Fweb%2F70c%2F8c4%2F886%2F70c8c488624049e296401558a18657bc.png)
Особое внимание стоит обратить на некоторые параметры запроса:
phone — номер на который отправляется SMSПопытка отправить запрос изменив его не увенчалась успехом. Мешает параметр «signature», который выступает в роли «подписи», как она генерируется разберемся немного позже.
session_id — рандомно генерирующаяся сессия операции восстановления
Для последующего анализа, в приложении, введём случайный код восстановления и продолжим наблюдать за сетевой активностью. Видим следующий запрос, он проверяет правильность введенного кода. Так как код был случайным — проверка не пройдена.
:extract_focal()/https%3A%2F%2Fhabrastorage.org%2Fweb%2F164%2F304%2F0cc%2F1643040cc91a4b979abbb69ce4fae1a7.png)
Честно говоря на этом моменте мне хотелось начать перебирать коды восстановления, меняя значение параметра «code». К сожалению, и этот запрос защищен от изменения с помощью «signature». Придётся разобраться, как генерируется эта подпись.
Реверс инжиниринг: декомпиляция
Для первоначального анализа можно попробовать декомпилировать приложение ВКонтакте. Так можно получить некоторые части исходного кода на Java.
Открываем в jd-gui все полученные .jar файлы. И не долго думая, делаем поиск по строке «signature».
:extract_focal()/https%3A%2F%2Fhabrastorage.org%2Fweb%2F120%2Fcf7%2Fab1%2F120cf7ab1f144403a58dd53d9b0c0bdb.png)
Библиотека libverify за авторством Mail.Ru явно выпадает из общего списка найденного. Смотрим и не ошибаемся, формируемая строка очень похожа на url из предыдущих запросов.
localObject3 = String.format(Locale.US, "%s%s?%s&signature=%s", new Object[] { d(), e(), localObject3, URLEncoder.encode(ru.mail.libverify.utils.m.b(f() + (String)localObject4 + ru.mail.libverify.utils.m.c(a.b())), "UTF-8") });
Эта библиотека сделана в лучших традициях security through obscurity, весь код надежно обфусцирован. Поэтому, через jd-gui мне удалось узнать только то, что за «signature» прячется MD5-хэш от неизвестной строки.
:extract_focal()/https%3A%2F%2Fhabrastorage.org%2Fweb%2F09a%2F1b0%2F65a%2F09a1b065a5454bd494b19b8236fcab48.png)
Реверс инжиниринг: дизассемблирование
Мне требовалось узнать, что за строка поступает в функцию ru.mail.libverify.utils.m.b(). Самый простой способ сделать это — немного изменить код приложения. Ну что ж попробуем. Для начала используем apktool, с командой:
apktool.jar d vk.apk -r
(ключ -r для игнорирования ресурсов)
Теперь, в папках с smali-кодом находим файл в котором происходит генерация MD5. В моем случае путь был такой: smali_classes3\ru\mail\libverify\utils\m.smali. Переходим к нужному методу:
...
.method public static b(Ljava/lang/String;)Ljava/lang/String;
.locals 8
.param p0 # Ljava/lang/String;
.annotation build Landroid/support/annotation/NonNull;
.end annotation
.end param
:try_start_0
const-string/jumbo v0, "UTF-8"
invoke-virtual {p0, v0}, Ljava/lang/String;->getBytes(Ljava/lang/String;)[B
:try_end_0
.catch Ljava/io/UnsupportedEncodingException; {:try_start_0 .. :try_end_0} :catch_2
move-result-object v0
:try_start_1
const-string/jumbo v1, "MD5"
invoke-static {v1}, Ljava/security/MessageDigest;->getInstance(Ljava/lang/String;)Ljava/security/MessageDigest;
move-result-object v1
invoke-virtual {v1}, Ljava/security/MessageDigest;->reset()V
invoke-virtual {v1, v0}, Ljava/security/MessageDigest;->update([B)V
invoke-virtual {v1}, Ljava/security/MessageDigest;->digest()[B
move-result-object v0
...
Строка, которую требовалось узнать передавалась в функцию в первом параметр-регистре (p0). Поэтому, чтобы получить ее, следует куда-нибудь вывести параметр, например, в Logcat. Добавляем в код несколько строк:
...
.method public static b(Ljava/lang/String;)Ljava/lang/String;
.locals 8
.param p0 # Ljava/lang/String;
.annotation build Landroid/support/annotation/NonNull;
.end annotation
.end param
# PATCH
# String v0 = "vk-research";
const-string/jumbo v0, "vk-research"
# Log.d(v0, p0), где p0 параметр метода
invoke-static {v0, p0}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
:try_start_0
const-string/jumbo v0, "UTF-8"
...
После сохранения изменений собираем приложение с помощью apktool:
apktool.jar b vk -o newvk.apk
Теперь нужно подписать apk, я использовалAPK Signer.
После этого, предварительно удалив оригинальное приложение, можно установить и запустить наш измененный клиент ВКонтакте. Для получения logcat с устройства воспользуемся Android Debug Bridge. Подключаем Android-устройство по USB и последовательно выполняем команды:
adb devices
adb logcat
Как только присоединились к устройству и получили возможность смотреть логи, снова нажимаем на «Забыли пароль?» и вводим номер телефона. В окне adb появляется запись:
:extract_focal()/https%3A%2F%2Fhabrastorage.org%2Fweb%2Fe23%2F705%2Feb7%2Fe23705eb793a466492db75014d0277ab.png)
Становится понятно, что хэшируемая строка состоит из последовательно склеенных: части url, параметров запроса и еще одной строки-хэша (506e786f377863526a7558536c644968). И теперь, зная алгоритм генерации «signature» можем начать отправлять свои «подписанные» запросы.
Исследование
Для исследования я написал простую программу на C#, которая отправляла запрос на отправку SMS и делала попытки ввода кода. Воспользовавшись ей, я вводил случайные коды восстановления. Но ожидаемо уперся в лимит попыток:
:extract_focal()/https%3A%2F%2Fhabrastorage.org%2Fweb%2F2ff%2F6e0%2F161%2F2ff6e0161bad417ca3229fc3dd66f442.png)
Пытался повторно отправить смс, в надежде, что после этого пропадет лимит. К моему сожалению, сообщение приходило, но вводить код я все так же не мог:
:extract_focal()/https%3A%2F%2Fhabrastorage.org%2Fweb%2Fa25%2F570%2Fba9%2Fa25570ba957d41b2b735129b00772e38.png)
Решил отправлять запросы с разными session_id, приходили смс с другими кодами восстановления, но я все еще упирался в лимит, теперь уже не в «ATTEMPTLIMIT», а в «RATELIMIT».
:extract_focal()/https%3A%2F%2Fhabrastorage.org%2Fweb%2Fd70%2F47b%2F01c%2Fd7047b01c76e44eeb52b1d3771c0ec75.png)
Я пытался обойти лимиты всеми возможными способами. Менял IP-адреса, параметры запроса, номера телефонов, но сделать больше 5 попыток ввода кода у меня не получалось.
И тут, почти случайно, я решил отправить код восстановления на два разных номера, но используя один и тот же session_id. Моему удивлению не было придела, когда я увидел одинаковую SMS на обоих телефонах. Вот как это работало:
Таким образом получалась атака:
- Отправляем запрос на отправку SMS абоненту A с session_id C, ему приходит код 1234
- Отправляем запрос на отправку SMS абоненту B с session_id C, ему приходит код 1234
- Теперь, если абонент A знает номер телефона абонента B, он может восстановить его страницу. Восстанавливать можно было только ту страницу, на номер которой пришла последняя SMS с сходным session_id.
Вывод
Сразу после обнаружения уязвимости я написал репорт на HackerOne. Уже через 17 часов уязвимость была устранена. Спустя несколько дней мне выплатили 2000$. Данная уязвимость позволяла взломать большинство аккаунтов в социальной сети, в безопасности были только аккаунты с двухфакторной авторизацией (у них нельзя делать восстановление по номеру телефона). Репорт.
P.S. ЕГЭ по информатике сдал на 97 баллов. К сожалению, остальные предметы не так успешно.
UPD: Библиотека libverify так же использовалась в ICQ, следовательно уязвимость существовала и там. В программе bug-bounty ICQ мне была сделана доплата в размере 1000$.Репорт.
Коментарі
Дописати коментар
Олег Мічман в X: «Donations and support for media resources, bloggers, projects, and individuals. https://t.co/HPKsNRd4Uo https://t.co/R6NXVPK62M» / X
https://twitter.com/olukawy/status/1703876551505309973