Продукты «Хакера»

  • Ежеквартальный «Хакер» #3

    Ежеквартальный «Хакер» #3

    Лучшие статьи квартала на бумаге

    2 000 ₽

    Заказать журнал
  • Годовая подписка на Xakep.ru

    Годовая подписка на Xakep.ru

    Полный доступ к статьям и архиву выпусков

    6 000 ₽ в год

    Подписаться
  • «Пентест WEB» для начинающих

    «Пентест WEB» для начинающих

    Совместный курс «Хакера» и лаборатории «Хаксет»

    7 000 ₽

    Купить курс
  • Спецвыпуск «Хакера» #4

    Спецвыпуск «Хакера» #4

    Лучшие статьи за 2021–2022 годы на бумаге

    2 000 ₽

    Заказать журнал
Со сто­роны кажет­ся, что мобиль­ные бан­ков­ские при­ложе­ния надеж­но защище­ны от любых попыток взло­ма, но порой ока­зыва­ется, что они содер­жат мно­жес­тво уяз­вимос­тей, выявить которые мож­но вдум­чивым тес­тирова­нием. Исто­рию пен­теста одно­го такого при­ложе­ния мы рас­ска­жем сегод­ня.

В один совер­шенно обыч­ный день нам при­шел зап­рос на пен­тест бан­ков­ско­го при­ложе­ния. Разоб­рались с бюрок­рати­ей, обсу­дили все детали. «Итак, NDA под­писан, ско­уп и дед­лайн сог­ласова­ны, мож­но начинать», — подума­ли мы и сра­зу же прис­тупили к работе. К кон­цу про­екта опи­сать наш сту­пор сло­вами было уже прак­тичес­ки невоз­можно. Хорошая новость зак­лючалась в том, что новые уяз­вимос­ти обна­ружи­вались очень быс­тро. Пло­хая в том, что их было мно­го. Пос­ле пер­вой уяз­вимос­ти мы подума­ли: «Быва­ет». Пос­ле вто­рой: «Не очень хорошо». Пос­ле пятой ста­ло оче­вид­но, что сло­во «быва­ет» здесь уже нес­коль­ко неумес­тно.

info

Наз­вание самого бан­ка, парамет­ры, заголов­ки, эндпо­инты замене­ны аль­тер­натив­ными в целях кон­фиден­циаль­нос­ти. Скрин­шоты мобиль­ного при­ложе­ния так­же были замене­ны сге­нери­рован­ными с помощью ИИ изоб­ражени­ями с дру­гим дизай­ном — по той же при­чине. Любые сов­падения с наз­вани­ями, интерфей­сами, парамет­рами или дру­гими деталя­ми, встре­чающи­мися в дру­гих бан­ках, слу­чай­ны. Все при­веден­ные в статье наз­вания и тех­ничес­кие дан­ные были изме­нены и подоб­раны исклю­читель­но ради ано­ними­зации исходной цели.

 

Валидация OTP

Ког­да поль­зователь пыта­ется залоги­нить­ся в свой акка­унт в бан­ков­ском при­ложе­нии, ему нуж­но ввес­ти ПИН (пер­сональ­ный иден­тифика­цион­ный номер) и серию пас­порта на началь­ной стра­нице. Пос­ле это­го при­ложе­ние перенап­равит поль­зовате­ля на новую activity, где ему пот­ребу­ется ввес­ти свой номер телефо­на для получе­ния одно­разо­вого кода (OTP).

Сло­во «свой» в абза­це выше выделе­но не прос­то так. Каж­дый раз, ког­да поль­зователь логинит­ся, он может ввес­ти любой номер телефо­на, вне зависи­мос­ти от того, при­вязан этот номер к его акка­унту или нет. OTP при­дет в любом слу­чае, и пос­ле его вво­да при­ложе­ние переки­нет поль­зовате­ля на сле­дующий этап.

Та­ким обра­зом, зло­умыш­ленник, который зна­ет ПИН и серию пас­порта жер­твы, может обой­ти эту валида­цию, исполь­зуя любой дру­гой номер телефо­на.

Пос­ле вво­да ПИНа, номера телефо­на и отправ­ки зап­роса, если такой кли­ент сущес­тву­ет, API-сер­вер вер­нет сле­дующий ответ:

HTTP/1.1 200 OK
Content-Type: application/json
{
"success": true,
"customerNumber": "123456"
}

Сох­раним номер кли­ента, он нам понадо­бит­ся в даль­нейшем.

На сле­дующей стра­нице поль­зовате­ля про­сят ввес­ти номер телефо­на. Раз уж при­ложе­ние никак не про­веря­ет при­над­лежность это­го номера, зло­умыш­ленник может ука­зать любой телефон, на который пос­тупит одно­разо­вый код.

Пос­ле вво­да номера телефо­на при­ложе­ние откры­вает стра­ницу, где поль­зователь дол­жен ввес­ти получен­ный одно­разо­вый код.

Ес­ли вве­ден кор­рек­тный код, откры­вает­ся сле­дующая стра­ница.

Та­ким обра­зом у нас получи­лось обой­ти про­вер­ку номера телефо­на и перей­ти на сле­дующий этап верифи­кации — KYC.

 

Корявый KYC

KYC — это, конеч­но, хорошо. Но было бы луч­ше, если бы он еще и работал кор­рек­тно.

В ходе тес­тирова­ния наша коман­да обна­ружи­ла, что если KYC поль­зовате­ля про­валит­ся, нап­ример его лицо не похоже на фотог­рафию в удос­товере­нии лич­ности, то сер­вер вер­нет нам 500 Internal Server Error. Если же все будет сов­падать, то нам будет воз­вра­щен код 200 с подоб­ной JSON-информа­цией:

HTTP/1.1 200 OK
Content-Type: application/json
{
"message": "success",
"data": {
"customerNumber": "555555"
}
}

Пом­нишь, я пред­лагал сох­ранить номер кли­ента, который воз­вра­щал нам сер­вер?

В этом кон­тек­сте он тоже нам воз­вра­щает­ся. Пос­ле получе­ния такого отве­та нас перенап­равля­ет на стра­ницу, где мы можем уста­новить ПИН‑код для при­ложе­ния.

Здесь мы задались воп­росом: а вдруг прог­рамма перенап­равля­ет поль­зовате­ля, толь­ко осно­выва­ясь на содер­жимом воз­вра­щаемо­го сер­вером отве­та? Поэто­му, вер­нувшись к началу про­цес­са KYC, мы умыш­ленно про­вали­ли про­вер­ку, что­бы сер­вер вер­нул нам ответ 500 и мы мог­ли про­тес­тировать HTTP Request Tampering.

К счастью для нас и к сожале­нию для бан­ка, мобиль­ное при­ложе­ние про­веря­ет толь­ко customerNumber и, уви­дев код 200 от API-сер­вера, перенап­равля­ет нас на сле­дующий этап. То есть KYC тоже мож­но без осо­бого тру­да обой­ти.

Пер­вым делом зло­умыш­ленник записы­вает у себя ответ сер­вера (код 200), при этом зна­чение customerNumber нуж­но будет поменять на номер кли­ента жер­твы. А этот номер, напом­ню, мож­но дос­тать из отве­та API-сер­вера, если знать ПИН и серию пас­порта жер­твы.

За­тем зло­умыш­ленник умыш­ленно про­вали­вает иден­тифика­цию лица. Генери­рует­ся зап­рос, где фотог­рафия зло­дея (ско­рее все­го, там будет не его нас­тоящее лицо) отправ­ляет­ся на сер­вер в фор­мате Base64 для про­вер­ки:

POST /api/v2/identity/liveness-test HTTP/1.1
Accept: application/json
Content-Type: application/json
{
"imageData": "/9j/4AAQSkZJRgABAQAAAQABAAD...<base64 картинка>...5ErkJggg=="
}

Пос­ле это­го сер­вер вер­нет зло­умыш­ленни­ку такой ответ:

HTTP/1.1 500 Internal Server Error
Content-Type: application/json

Зло­умыш­ленник, перех­ватив ответ до получе­ния его при­ложе­нием, пол­ностью меня­ет струк­туру:

HTTP/1.1 200 OK
Content-Type: application/json
{
"message": "success",
"data": {
"customerNumber": "<тут указывается номер клиента>"
}
}

Пос­ле того как мобиль­ное при­ложе­ние получит такой ответ, оно перенап­равит нас на стра­ницу нас­трой­ки ПИН‑кода для дос­тупа.

Ког­да ПИН‑код нас­тро­ен, при­ложе­ние воз­вра­щает поль­зовате­ля обратно на глав­ную стра­ницу.

 

По CVV на брата

В боль­шинс­тве бан­ков­ских при­ложе­ний при прос­мотре рек­визитов бан­ков­ской кар­ты CVV обыч­но по умол­чанию скрыт и, что­бы уви­деть его, нуж­но нажать на соот­ветс­тву­ющее поле. В иссле­дуемом нами при­ложе­нии в момент нажатия на поле CVV на сер­вер отправ­ляет­ся вот такой зап­рос:

POST /api/v2/cards/cvv HTTP/1.1
Accept: application/json
Authorization: Bearer <redacted>
Content-Type: application/json
{
"cardNumber": "4930264148157430"
}

В качес­тве отве­та сер­вер нам выда­ет сле­дующее:

HTTP/1.1 200 OK
Content-Type: application/json
{
"message": "success",
"data": {
"cvv": "123"
}
}

На пер­вый взгляд все выг­лядит впол­не логич­но: при­ложе­ние отправ­ляет номер кар­ты на сер­вер, сер­вер понима­ет, для какой имен­но кар­ты необ­ходимо получить CVV, про­веря­ет авто­риза­цию поль­зовате­ля и воз­вра­щает завет­ные три цифер­ки.

Продолжение доступно только участникам

Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».

Присоединяйся к сообществу «Xakep.ru»!

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии