Этот небольшой, местами смешной, местами не очень отчёт поведает тебе о том, как за один вечер я умудрился найти три бага в сервисе IPScore Check и пополнить свой баланс на 5,000,000$.
Всё начинается с бага Ссылка на заголовок
Я часто пользуюсь ботом IPScore Check, когда встаёт вопрос о проверке IP-адресов. Он агрегирует бо́льшую часть общедоступной информации об адресах, да и разработчика я хорошо знаю.
Ну и как-то от нехрен делать я тыкнул на кнопку “Проверить мой IP” (я и раньше на неё тыкал, но было всё ок, читай дальше) и обнаружил странную вещь:
Бот по какой-то причине получал от WebView не мой адрес, а айпишник CloudFlare. Казалось бы, бэкенд берёт реальный адрес подключения вместо айпишника клиента в заголовках - но раньше-то всё работало, да и разраб меня убеждает, что на его машине всё ок… Несколько секунд гугления, и я вспоминаю, что совсем недавно настроил IPv6 на роутере:
If Pseudo IPv4 is set to Overwrite Headers - Cloudflare overwrites the existing Cf-Connecting-IP and X-Forwarded-For headers with a pseudo IPv4 address while preserving the real IPv6 address in CF-Connecting-IPv6 header.1
Однако, Pseudo IPv4 был отключен, да и видел я не псевдоадрес, а IP сервера CloudFlare, который менялся между запросами.
Манипулирование IP-адресом Ссылка на заголовок
И тут мой товарищ скидывает кусок кода с фразой “Может, баг где-то в коде? Ты видишь его?”
async def get_ip(self, request: Request):
if not request:
raise HTTPException(status_code=400, detail="Request object is required")
headers = request.headers
ipv4 = None
ipv6 = None
cf_ip = headers.get("CF-Connecting-IP")
if cf_ip:
if ':' in cf_ip:
ipv6 = cf_ip
else:
ipv4 = cf_ip
if not ipv4 or not ipv6:
x_forwarded_for = headers.get("X-Forwarded-For")
if x_forwarded_for:
ip_list = x_forwarded_for.split(',')
for ip in ip_list:
ip = ip.strip()
if ':' in ip:
ipv6 = ipv6 or ip
else:
ipv4 = ipv4 or ip
if not ipv4:
x_real_ip = headers.get("X-Real-IP")
if x_real_ip and ':' not in x_real_ip:
ipv4 = x_real_ip
client_ip = request.client.host
if not ipv4 and not ipv6:
if ':' in client_ip:
ipv6 = client_ip
else:
ipv4 = client_ip
ip = ipv4 if ipv4 else ipv6
return {
"status": "success",
"data": {
"ip": ip,
"ipv4": ipv4,
"ipv6": ipv6
}
}
Ну да, вижу. И даже не один…
- Первое - и самое главное - если запрос пришел с серверов CloudFlare, то вся функция должна свестись к одному единственному действию -
cf_ip = headers.get("CF-Connecting-IP")
, остальные хедеры проверять не нужно; - Второе - в случае, если запрос пришёл со сторонних адресов - использовать хедер
X-Real-IP
; - Третье - забыть навсегда (почти) про
request.client.host
иX-Forwarded-For
.
request.client.host
всегда будет содержать NAT-овский IP-адрес из частной подсети 172.16.0.0/12.
Реверс-прокси устанавливает хедер X-Real-IP
, содержащий реальный IP-адрес, с которого подключается клиент.Собственно, причина того, что вместо своего IPv6 адреса я видел IPv4 адрес CloudFlare - багованная (и небезопасная) логика обработки хедеров, содержащих IP-адреса, в частности бэкенд очень сильно хотел, чтобы у него был IPv4-адрес клиента, даже если его у него нет.
Также я сразу заметил, что здесь совершенно нет проверок на принадлежность реального IP-адреса подключения к диапазонам сетей CloudFlare, а микросервис API прослушивает порт на внешнем адресе, позволяя обращаться к нему напрямую (однако CloudFlare его фильтрует). И тут меня наводнила мысль:
- Если обращаться к API через реверс-прокси в обход CloudFlare, можно манипулировать IP-адресом посредством заголовка
CF-Connecting-IP
; - Если обращаться к API в обход реверс-прокси, можно использовать и заголовок
X-Real-IP
; - В коде отсутствует валидация IP-адреса - можно указывать произвольный текст.
PoC: Ссылка на заголовок
Impact: Ссылка на заголовок
- Скрытие адреса в логах и обход блокировок. Ситуация усугубляется тем, что порт API-сервиса доступен из вне, а сам сервис находится за NAT-ом, т.е. поймать реальный IP-адрес клиента внутри сервиса невозможно;
- Разработчик намекнул, что в некоторых местах возможно было триггернуть SSRF;
- При определенных условиях появляется возможность отправлять произвольные сообщения некоторым пользователям Telegram через официального бота.
Слабое шифрование и IDOR Ссылка на заголовок
И тут я вхожу во вкус, руки прям чешутся потыкать что-нибудь ещё, да и сам разработчик сказал, что, скорее всего, тут есть ещё дыры - часть API вообще опирается на старый код на пыхе, а руки переписать и избавиться от легаси у него ещё не дошли.
Тыкаюсь я на https://ipscore.me/, оттуда на https://docs.ipscore.me/ и нахожу клиентскую документацию на REST API сервиса. Залогинившись на основном домене через Telegram и потыкав апишку (https://api.ipscore.me), я обнаруживаю, что я нахожусь ровно под тем же юзером, под которым работает TG-бот на моём аккаунте. С одной стороны это очень удобно, но с другой стороны это прямо развязало мне руки:
- При работе с REST API я свободен от ограничений и защитных механизмов, реализованных в самом Telegram и в коде бота;
- Я значительно расширяю количество автоматизированных и ручных инструментов и утилит для исследования приложения;
- Взаимодействовать с API гораздо проще и удобнее, чем с ботом посредством Telegram;
- Я получаю возможность взаимодействовать с методами и параметрами, взаимодействие с которыми через бота проблематично либо ограничено.
Так замечательно, что аж плакать хочется. Бегаю я по разным методам, слушаю Never gonna give you up (попался?). Меня больше всего интересовали методы, принимающие данные от пользователя - а их там оказалось совсем немного, однако create_invoice
показался мне весьма интересным:
GET /create_invoice/Bitcoin/BTC/5000000/ HTTP/1.1
HTTP/1.1 200 OK
{
"status": "success",
"data": {
"invoice_id": "403F386D45"
}
}
GET /create_invoice/Bitcoin/BTC/1/ HTTP/1.1
HTTP/1.1 200 OK
{
"status": "success",
"data": {
"invoice_id": "403F386D44"
}
}
Вы тоже заметили? Последовательность invoice_id
уж больно легко вычисляется. Нет, она не линейная , но очень простая.
Первое, про что я подумал - IDOR, и не ошибся. Как оказалось, API в методах управления инвойсами не проверял авторизацию пользователя, но требовал её лишь для предоставления доступа к методам. По итогу появилась возможность просмотреть инвойсы всех пользователей сервиса, включая архивные записи:
{
"status": "success",
"data": {
"invoice_id": ***,
"merchant_name": "Ip Score Checker",
"merch_id": 20,
"logo_img": "https://ipscore.me/favicon.ico",
"transaction_id": ***,
"currency": 840,
"currency_code": "USD",
"coin": "USDT",
"network": "Tron",
"wallet_address": "T*********************************",
"wallet_id": ***,
"exchange_rate": 0.999845,
"amount": 10.0,
"client_amount": 9.99845,
"order_id": "****",
"description": "********* Top-up 10.0 USD",
"lang": "en",
"status": "success",
"confirmation_status": null,
"created_at": "2024-**-**T00:07:35",
"payment_due_date": "2024-**-**T02:07:39"
}
}
Кроме того выяснилось, что метод update_invoice
мог обновлять и чужие, и архивные инвойсы. С помощью этого метода можно изменить сеть и монету для оплаты и, как оказалось, обновить инвойсу payment_due_date
. Кстати, api_key
в параметрах запроса совершенно ничего не делает, как я понял. Возможно, это какой-то костыль, притянутый из старого легаси, когда авторизация была не Bearer по токенам, а по API-ключам (предполагаю).
Impact: Ссылка на заголовок
- Можно собрать информацию об инвойсах пользователей, посчитать доходы разработчика. Я посчитал, честно - хз на что он сервера содержит :(
- Инвойсы не содержат информации об адресах кошельков клиентов, но её без труда можно подтянуть из блокчейна;
- Можно оживить все архивные инвойсы пользователей;
- Исходит из 3: вероятность DoS в случае, если архивных инвойсов очень много. Как минимум время проверки транзакций увеличится (у шлюза подняты собственные ноды, кстати), повысится задержка ответа, т.к. шлюз начнёт в больших количествах генерировать адреса - а это не самое быстрое задание;
- Можно изменить адрес и/или сеть/монету в текущем инвойсе пользователя. Если обновление инвойса отключает старый адрес для оплаты, а пользователь успеет перевести монеты на него - оплата не пройдет. Коридор проверки оплаты у шлюза около 1 минуты.
5000000$ ценой в несколько центов Ссылка на заголовок
Во время исследования поведения API-методов работы с инвойсами я обнаружил довольно занятную вещь:
POST /update_invoice/ HTTP/1.1
{
"invoice_id": "403F386D45",
"network": "Tronium",
"coin": "TRahiX",
"api_key": "string"
}
HTTP/1.1 200 OK
{
"status": "success",
"data": {
"invoice_id": "403F386D45"
}
}
Оказывается, API не проверяет валидность значений сети и монеты, полученных от клиента. При создании инвойса - та же история. Давайте посмотрим, что стало с инвойсом:
GET /get_invoice/403F386D45/ HTTP/1.1
HTTP/1.1 200 OK
{
"status": "success",
"data": {
"invoice_id": ***,
"merchant_name": "Ip Score Checker",
"merch_id": 20,
"logo_img": "https://ipscore.me/favicon.ico",
"transaction_id": null,
"currency": 840,
"currency_code": "USD",
"coin": null,
"network": null,
"wallet_address": "bc1q**************************************",
"wallet_id": ***,
"exchange_rate": 0.0,
"amount": 5000000.0,
"client_amount": 0.0,
"order_id": "****",
"description": "********* Top-up 5000000.0 USD",
"lang": "en",
"status": "success",
"confirmation_status": null,
"created_at": "2024-08-29T01:08:12",
"payment_due_date": "2024-08-29T03:08:15"
}
}
Поля сеть, монета, курс и количество монет на отправку превратились в null и нули, но адрес для отправки средств и количество у.е. на зачисление остались прежними. И тут меня щёлкнуло - раз client_ammount
стал нулём - инвойс может оплатиться буквально любой транзакцией с любым количеством монет. Ну а дальше я перевожу несколько центов на адрес для оплаты и ловлю сообщение в боте об успешном пополнении счёта:
Your balance has been successfully replenished by 5000000 $ through Ugate!
PoC: Ссылка на заголовок
Impact: Ссылка на заголовок
- Я теперь $ мультимиллионер :)
В заключение Ссылка на заголовок
А не будет заключения, оно приняло Ислам. Вместо него будет немного мемов. Если уж очень сильно хочется воды в стиле “всегда тестируйте свои сервисы и API” - попросите ChatGPT это извергнуть. Да, кстати, я им не пользовался при написании этой статьи, а разработчик залатал все дырки на момент выхода статьи.
Список использованных инструментов aka Набор мазохиста Ссылка на заголовок
- Телефон под управлением ОС Android
- Kiwi Browser (apk)
- Restler (apk)
- Termux (apk)
- cURL (termux)
БагБаунти, ёпта, 100₽, лучше бы блечил Ссылка на заголовок
На самом деле скриншоты выше - просто смешная ситуация, никто ни на кого не в обиде, денег за этот Bounty: Райское наслаждение я не просил, не ждал и не брал, да и сам ресёрч организован был на энтузиазме в свободный вечер… Среды?
А вообще, IPScore - действительно полезный сервис, а разраб - реально чёткий поцык. В ближайшее время (которое должно было быть уже вчера, но вместо этого я заставил разраба латать дыры) планируется масштабное обновление, нацеленное на повышение производительности и оптимизацию инфраструктуры, а чуть позже будет расширен функционал инструмента “IP-Трекер” и внедрены другие полезные инструменты для работы с сетями. Так что давай, тыкай ссылочку, пользуйся, плоти нолог.
P.S. на самом деле не плоти - на этом зеркале скоро появится скидка вместо моего реферального процента. С ненавистью любовью, FazaN ❤️
-
Restoring original visitor IPs, CloudFlare. ↩︎