IT/웹 보안

[NoSQL Injection & Blind NoSQL Injection]

kykyky 2024. 6. 8. 08:39

NoSQL Injection

: 이용자의 입력값이 쿼리에 포함되어, 이용자가 원하는 요청을 실행하는 취약점 ㅡ SQL Injection과 공격 목적 및 방법이 매우 유사

 

SQL과 달리, NoSQL은 사용하는 DBMS에 따라 요청 방식과 구조가 다른데

이 글에서는 MongoDB에서의 NoSQL Injection만 알아볼 것임

 

MongoDB에서는 SQL이 저장할 수 있는 데이터의 자료형인 문자열, 정수, 날짜, 실수 외에도 오브젝트, 배열 타입을 사용할 수 있는데,

오브젝트 타입의 입력값을 처리할 때에는 쿼리 연산자를 사용할 수 있어,

이를 통해 다양한 행위가 가능

 

>> Node.js의 express 프레임워크로 짜인 소스코드 예시

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');
});

 

위 코드는 이용자의 입력값과 이것의 타입을 출력하는데,

req.query의 타입이 문자열로 지정되어 있지 않아서 문자열 외의 타입이 입력될 수 있음.

 

>> 각 request url에 따른 data와 type 결과 

오브젝트 타입도 삽입할 수 있는 것을 확인

 

예시 상황 1

>> 입력된 아이디와 패스워드에 해당하는 데이터를 찾아 출력하는 소스코드

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');
});

위 코드는 user 컬렉션에서 이용자가 입력한 uid와 upw에 해당하는 데이터를 찾아 출력하는데,
이용자의 입력값에 대해 타입을 검증하지 않아서, 오브젝트 타입의 값을 입력할 수 있음.

 

※ GET 메서드를 이용

사용자가 전달한, 즉 GET 요청의 데이터는 URL의 query string에 포함되고, 이는 요청의 URL 끝에 ?로 시작하는 부분에 있음 

eg. http://localhost:3000/query?uid=user123&upw=pass123

 

공격

오브젝트 타입의 값을 입력할 수 있다면 연산자를 사용할 수 있다.

 

$ne 연산자 = not equal = 입력한 데이터와 일치하지 않는 데이터를 반환

-> 공격자는 계정 정보를 모르더라도 아래와 같이 입력해 해당 정보를 알아낼 수 있음 

 

>> uid와 upw가 "a"가 아닌 데이터를 조회하는 공격 쿼리와 실행 결과

 

 

예시 상황 2

>> 입력된 아이디와 패스워드에 해당하는 데이터를 찾아 출력하는 소스코드 예시

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');
});

※ POST 메서드를 이용

일반적으로, 사용자가 폼을 제출하거나 자바스크립트를 통해 요청을 보내는 방식이기 때문에, 사용자가 직접 URL을 입력하지 않고 인터페이스를 통해 데이터를 요청 본문에 포함하여 전송

 

공격

목표: 데이터베이스에 존재하는 “admin” 계정의 비밀번호 획득하기

 

upw에서 $ne(not equal)연산자를 이용하여 upw값에 상관없이 uid가 "admin"인 데이터를 조회할 수 있다.

 

 

 

Blind NoSQL Injection

: 데이터 조회 후 결과를 직접적으로 확인할 수 없는 경우 사용될 수 있는 NoSQL Injection 공격 기법 ㅡ Blind SQL 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.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']);
        }else{
            res.send('undefined');
        }
    })
});
const server = app.listen(80, function(){
    console.log('app.listen');
});

위 코드는 로그인에 성공할 경우 uid를 출력

 

 

배경지식: 연산자를 활용한 공격

 

▶ $regex

정규식을 사용해 식과 일치하는 데이터를 조회

 

>> upw에서 각 문자로 시작하는 데이터를 조회하는 쿼리

 

▶ $where

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

해당 연산자는 field에서 사용할 수 없음 

substring

이 연산자로 Javascript 표현식을 입력하면, Blind SQL Injection에서 한 글자씩 비교했던 것과 같이 데이터를 알아낼 수 있음

 

>> upw의 첫 글자를 비교해 데이터를 알아내는 쿼리

 


Sleep 함수를 통한 Time based Injection

sleep을 표현식과 함께 사용하면 지연 시간을 통해 참/거짓 결과를 확인할 수 있음

 

>> upw의 첫 글자를 비교하고, 해당 표현식이 참을 반환할 때 sleep 함수를 실행하는 쿼리



Error based Injection

올바르지 않은 문법을 입력해 고의로 에러를 발생시켜 데이터를 알아냄

upw의 첫 글자가 'g' 문자인 경우, 올바르지 않은 문법인 asdf(아무말임)을 실행하여 에러 발생시킴

 

실제 공격

목표: 데이터베이스에 존재하는 "admin" 계정의 비밀번호 획득하기

 

i) 비밀번호 길이 획득

비밀번호 자체를 알아내기 전에 길이부터 알아내자.

 

정규식 ㅡ .{5}와 .{6}을 활용

.{5} 식의 경우 uid를 반환하지만, .{6} 식에서는 undefined임

-> upw의 길이가 5임을 의미


ii) 비밀번호 획득

$regex 연산자와 정규식을 통해 한 글자씩 비교하고, 반환 결과를 통해 비밀번호의 한 글자씩을 알아냄

※ 정규식 중 ^는 입력의 ^ㅣ작부분을 나타냄