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 - из другого namespace3. Повторной проверки прав на
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 в проверке привилегий) = полное чтение продакшен-базы одним запросом.