862K записей из "защищённой" блокчейн-платформы одним curlWeb3-платформа децентрализованной идентификации. 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 bypassuser.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 в проверке привилегий) = полное чтение продакшен-базы одним запросом.