SQL Injection이란?
SQL injection이란 관계형 DBMS에서 사용하는 질의 구문인 SQL을 삽입하는 공격입니다. 여기서 DBMS란 DataBase Management System의 약자로 데이터베이스를 관리하는 애플리케이션을 말합니다. 이 DBMS는 테이블 형식으로 데이터가 저장되는 관계형 DBMS와 키-값 형태로 데이터가 저장되는 비관계형 DBMS가 있습니다. 이번 포스팅에서 다루게 될 SQL Injection은 관계형 DBMS인 RDBMS에서 사용되는 공격으로 공격 발생 시 조작된 쿼리로 인증을 우회하거나, 데이터베이스의 정보를 유출할 수 있는 해킹 기법입니다.
SQL Injection은 공격 방법에 따라 여러 종류가 존재합니다.
이번 포스팅에서는 아래의 4가지 종류의 SQL Injection을 설명하도록 하겠습니다.
- Classic SQL Injection
- Error based SQL Injection
- Blind SQL Injection
- Union based SQL Injection
Classic SQL Injection
Classic SQL Injection은 로그인 양식, 검색 상자, URL 매개 변수와 같은 입력 필드를 악용하여 데이터베이스 쿼리에 직접적으로 악의적인 코드를 삽입해 쿼리의 결과가 데이터베이스에 비정상적인 명령을 수행하도록 하는 방법입니다.
위와 같은 로그인 양식에서는 아래와 같은 SQL 쿼리를 통해 데이터베이스에 저장된 userid와 userpw가 모두 일치한 경우 로그인이 이루어집니다.
SELECT * FROM member where userid='$userid' and userpw='$userpw'
하지만 만약 ' -- 을 userid 뒤에 입력한다면?
SELECT * FROM member where userid='admin'-- ' and userpw='$userpw'
' 로 아이디 쿼리가 닫히고, MySQL의 주석으로 쓰이는 -- 으로 인해 비밀번호를 검증하는 부분이 주석처리가 됩니다.
이런 경우 비밀번호와 상관없이 데이터베이스에 해당 아이디값이 존재한다면 로그인이 가능해지는데요.
이처럼 아이디가 admin인 계정의 정보를 반환하는 것을 볼 수 있습니다.
Error based SQL Injection
Error based SQL Injection은 데이터 베이스 서버에서 반환하는 에러 메시지를 이용하여 데이터베이스의 구조나 데이터를 노출시키는 공격 기법으로 Get, Post 요청 필드 또는 HTTP 헤더 값, 쿠키값 등에 특수문자를 삽입 시 SQL 관련 에러가 출력되는 경우 사용할 수 있습니다.
이 공격은 문법 에러가 아닌 논리 에러를 발생시켜 데이터를 알아내는 것으로 논리 에러를 유발하기 위한 Updatexml 또는 extractvalue와 같은 함수를 사용하게 됩니다. 두 함수를 설명하기 위해 일단 XML에 대해서 간단히 설명하자면 XML은 HTML과 같은 마크업 언어로, 태그를 사용하여 데이터의 구조와 의미를 정의하는 역할을 합니다. 주로 웹에서 데이터를 전송하기 위해 만들어진 문서이며 MySQL에서도 데이터를 처리하기 위해 Updatexml이나 extractvalue와 같은 함수를 사용하게 되는 것입니다.
하지만 이를 악용하여 두 함수를 쓸 때 XPath라는 XML 문서의 노드를 정의하기 위한 경로식을 쓰게 되는데 이 경로식을 잘못된 방법으로 작성하여 에러 메시지를 유도할 수 있습니다. 저는 Updatexml 함수를 이용해 실습을 해보았습니다.
보시는 것처럼 일단 1을 입력 후 작은따옴표로 닫아 첫 조건이 참이 되도록 해주어 이후 쿼리가 실행되도록 하였고, updatexml 함수에서 XML 문서를 입력하는 첫 인자와 XML문서에 업데이트할 값을 입력하는 마지막 인자에는 NULL을 입력하여 실제 XML문서가 전달되지 않도록 해준 후, 두 번째 인자값에 와야 하는 XPath의 경로식을 concat 함수로 16진수 콜론과 test라는 문자열이 합하여 에러가 발생하도록 하였습니다. 마지막으로 항상 참이 되도록 and ‘1’=’1을 입력해 주어 쿼리의 논리적인 일관성을 유지하였고, 이를 통해 에러 메시지에 특정 정보를 포함하여 출력시키도록 할 수 있다는 것을 알게 되었습니다.
Concat 함수
문자열을 결합해 주는 함수로 이 함수 없이 단순히 원하는 내용을 입력하게 될 경우 문자열이 아닌 올바른 XML 형식의 데이터로 인식하여 에러 메시지가 출력되지 않을 수 있기 때문에 사용하였습니다.
이제 앞서 주입한 SQL 쿼리의 내용 중 test가 들어갔던 부분에 database 함수를 가져와 현재 테이블이 사용 중인 데이터베이스의 이름을 출력시키도록 해주겠습니다.
그렇게 데이터베이스의 이름이 DB라는 것을 알게 되었습니다.
다음으로는 테이블의 이름을 알아낼 것인데 이때 Limit이라는 함수가 사용됩니다. 이 함수는 Limit n, m 형식으로 사용되는데 n번째 데이터부터 m개만큼을 출력하는 함수입니다.
SELECT * FROM member where userid='1' and updatexml(null,concat(0x3a,(SELECT table_name from information_schema.tables where table_schema='DB' limit 0,1)),null) and '1'='1' and userpw='$userpw'
작성된 쿼리문을 보시면, updatexml과 concat는 데이터베이스 이름을 알아낼 때와 똑같이 사용하고 concat 함수 안에서 사용된 서브 쿼리로 테이블 이름을 가져오는데 이때 information_schema.tables이라는 mysql 시스템 데이터베이스에 포함된 테이블에서 table_schema라는 열에 데이터베이스 이름이 DB인 table_name을 조회하여 그 이름을 출력하는 것입니다. 이 때 서브 쿼리가 여러 행을 반환하면 단일값으로 처리할 수 없기 때문에 Limit 함수를 이용해 테이블 이름을 하나씩 반환하는 것 입니다.
그렇게 총 2개의 테이블의 존재와 이름을 알게 되었고 이렇게 알아낸 테이블 이름으로 컬럼명을 알아낼 수 있습니다.
SELECT * FROM member where userid='1' and updatexml(null,concat(0x3a,(SELECT column_name from information_schema.columns where table_name='member' limit 0,1)),null) and '1'='1' and userpw='$userpw'
이번에는 Information_schema.columns이라는 컬럼에 대한 정보를 담은 테이블에서 테이블 이름이 member인 곳의 컬럼을 하나씩 반환하여 출력해 주었습니다.
알아낸 컬럼의 이름을 통해 member 테이블에서 해당 컬럼의 이름을 통해 데이터들을 불러올 수 있었습니다.
SELECT * FROM member where userid='1' and updatexml(null,concat(0x3a,(SELECT userpw from member limit 0,1)),null) and '1'='1' and userpw='$userpw'
Blind SQL Injection
Blind SQL Injection은 참과 거짓에 따라 달라지는 결과를 통해 정보를 추출하는 공격 기법입니다.Blind SQL Injection에는 Boolean 기반과 Time 기반이 있습니다.
Boolean SQL Injection
SQL 쿼리의 결과가 참일 때 정상적인 응답을 반환하고, 거짓일 때 에러 응답을 반환하는 것을 통해 정보를 유추하는 방법입니다.
예를 들어 아이디와 비밀번호를 틀린 경우에는 로그인 정보가 없다는 경고창이 뜨고, 올바르게 입력했을 경우 해당 계정으로 로그인이 되는 경우가 있습니다.
비밀번호의 길이를 먼저 알아내기 위해 Length 함수를 이용하여 ' or length(userpw) = [숫자] 를 비밀번호 창에 입력하게 될 경우 이와 같이 쿼리문이 조작되는데 내용을 살펴보면 member 테이블에서 userid가 admin이고 userpw가 일치하거나 userpw의 길이가 해당 숫자일 경우 참을 반환하게 되는 형식인데요. 올바른 길이를 입력할 경우 실제 로그인 창에서는 로그인이 됩니다.
그렇게 비밀번호의 길이를 알았다면 이제 한 글자 한 글자씩 값을 대입하여 참과 거짓의 결과로 해당 문자열 몇 번째 자리의 문자가 무엇인지를 하나씩 알아낼 수 있는데요. substr 함수와 ascii 함수를 함께 사용하여 userpw의 첫 번째 문자열에서부터 한 글자의 값이 10진수로 표현된 아스키 값과 같다면 참을 반환하고 아닐 경우 거짓을 반환하는 방법입니다.
이 경우에 수동으로 하나씩 대입해 보는 것은 너무 비효율적이라 파이썬으로 자동화 코드를 작성하여 확인할 수 있는데요. 아래처럼 password라는 변수를 빈값으로 설정하고 비밀번호를 입력하는 곳에 미리 작성한 페이로드를 넣었을 때 응답 값에 location이 index.php로 넘어간다면 해당 자릿수에 해당 문자를 password라는 변수에 넣어 출력하는 방식입니다. 그렇게 비밀번호를 알아낼 수 있었습니다.
import requests
url='http://localhost:8888/member/login_ok.php'
password=""
for i in range(1,6):
for j in range(32,127):
payload=f"' or ascii(substr(userpw,{i}, 1))={j}-- "
data={'userid':'admin','userpw':payload}
response=requests.post(url, data=data)
if response.url.endswith("/index.php"):
print(f"{i}번째 문자 : {chr(j)}")
password+=chr(j)
break
print(f"비밀번호 : {password}")
Time based SQL Injection
SELLP과 같이 SQL 쿼리 결과에 따라 응답을 지연시키는 함수를 통해서도 알아낼 수 있습니다. SELLP 함수는 SELLP(초) 형식으로 사용되며 해당 쿼리 결과의 값이 참이라면 지정한 시간만큼의 지연시간을 갖게 하는 함수입니다. 보시는 것 처럼 10진수로 표현된 아스키 값이 비밀번호의 해당 문자와 일치한다면 2초의 지연시간을 갖게하는 쿼리를 입력했을 때 참이면 2초의 지연시간을 갖고 거짓이라면 바로 값이 나오게 되는 것입니다.
Union based SQL Injection
Union based SQL Injection은 Union 연산자를 사용하여 원래 쿼리 결과에 추가적인 선택 결과를 결합하는 방식인데요 이때 사용하는 Union 연산자란 두 개 이상의 SELECT 문들의 결과 집합을 단일 결과 집합으로 결합하고 결합 시 중복된 데이터는 제거해 주는 연산자로 두 개의 쿼리문의 결과를 하나의 테이블로 보여주게 됩니다.
검색창에서 사용하기 위해 일단 sql 쿼리가 적용되는지 확인을 해보았고
일단 검색 결과가 출력되는 이름, 제목, 날짜, 조회수가 몇 번째 컬럼인지를 알기 위해 먼저 컬럼의 개수를 알아보았는데요. order by 연산자를 사용하여 존재하는 게시글 제목인 Mysql이라는 문구를 포함하는 게시글을 찾고 그 결과를 첫 번째 컬럼을 기준으로 정렬하였을 때 결과가 나오는 것을 볼 수 있습니다. 그렇게 order by 뒤의 숫자를 하나씩 늘려 6번째 열까지는 존재함을 알았지만, 7번째 컬럼은 존재하지 않기때문에 검색 결과가 없다고 뜨는 것을 볼 수 있었습니다.
터미널 창에서 해당 쿼리를 입력하였을 때는 아래와 같은 결과가 나오게 됩니다.
이렇게 알아낸 컬럼의 개수를 바탕으로 몇 번째 컬럼이 어느 부분에 출력되는지를 확인하여야 원하는 데이터 베이스 정보의 내용을 추출하여 확인할 수 있기 때문에 앞서 설명한 Union 연산자를 사용해 select 문으로 1부터 6까지의 숫자를 단순히 반환하여 MySQL을 포함하는 게시물과 반환된 숫자 중 출력되는 숫자를 통해 출력되는 컬럼의 자리를 알 수 있습니다. 따라서 2, 3, 5, 6열의 내용이 페이지에 출력되는 것을 확인할 수 있었습니다.
출력되는 컬럼을 확인하였으니 해당 컬럼의 자리에 현재 테이블이 저장된 데이터베이스를 출력해 주는 database()라는 함수를 사용하여 select문으로 불러와 제목이 출력되던 3번 자리에 출력시켜 주었습니다. 실제로 터미널을 통해 DB라는 데이터 베이스에 해당 테이블이 존재하고 있는 것을 볼 수 있고
SELECT * FROM board WHERE title LIKE '%1' UNION SELECT 1,2,database(),4,5,6 --
이제 알아낸 데이터베이스의 이름으로 해당 데이터 베이스에 존재하는 모든 테이블을 불러보도록 하겠습니다. 첫 번째 select문에서 반환되는 값이 없어도 두 번째 select문에서 값이 반환되기 때문에 앞에는 1과 작은 따옴표를 입력하여 두번째 select문의 결과만 출력되도록 해주었으며 두 번째 select문에서는 3번째 자리에 테이블 이름이 출력되도록 하는데 앞서 에러 기반 sql injection에서 사용한 information_schema.tables는 mysql 시스템 데이터베이스에 포함된 테이블에서 table_schema 열의 데이터 베이스가 DB인 table_name을 조회하도록 하여 board와 member이라는 이름의 테이블이 출력되는 것을 볼 수 있습니다.
SELECT * FROM board WHERE title LIKE '%1' UNION SELECT 1,2,table_name,4,5,6 from information_schema.tables WHERE table_schema='DB' --
이제 테이블 이름을 통해서 이렇게 6개의 컬럼명이 존재함을 알았고,
SELECT * FROM board WHERE title LIKE '%1' UNION SELECT 1,2,column_name,4,5,6 from information_schema.columns WHERE table_name='member' --
아까 확인한 컬럼 명을 출력되는 숫자 대신 입력하고, 불러올 테이블도 입력해 주면 해당 데이터를 추출할 수 있게 됩니다.
대응 방안
Prepared Satement
현재 SQL Injection을 방어하는 최선의 보안 대책으로 Prepared Satement를 사용하고 있습니다. 이 방법을 이해하기 위해서는 SQL 쿼리를 입력하였을 때 처리되는 내용을 알아야 합니다.
MySQL에서 데이터를 처리할 때 parse->bind->execut->patch 순서로 진행이 되는데 parse 단계에서 쿼리문을 검사하게 됩니다. Prepared Satement를 사용하게 되면 해당 단계를 한 번만 수행하고 메모리에 저장해 두어 필요할 때마다 꺼내 사용하는 것입니다. 이 때 바인딩 변수를 미리 지정해두어 비밀번호나 아이디와 같은 내용을 매번 입력받을 수 있습니다. 그렇게 입력받는 바인딩 변수는 데이터로만 인식되기 때문에 SQL 쿼리로 작동하지 않게되는 것 입니다.
$conn=mysqli_connect("localhost","keshu","1234","DB");
$userid=$_POST['userid'];
$userpw=$_POST['userpw'];
$stmt=mysqli_prepare($conn,'SELECT * FROM member WHERE userid=? AND userpw=?');
mysqli_stmt_bind_param($stmt,'ss',$userid,$userpw);
mysqli_stmt_execute($stmt);
$result=mysqli_stmt_get_result($stmt);
입력 값 검증
mysql_real_escape_string와 같은 함수를 사용해 화이트리스트 또는 블랙리스트 필터링을 할 수 있습니다. 두 방법 모두 사용 가능하지만 보안상 블랙리스트는 우회 가능성이 높아 화이트 리스트 필터링을 사용하는 것이 더 좋습니다.
Error 메시지 출력 제한
Error sql injection과 같이 사용될 수 있는 에러 메시지를 띄우지 않는 것이 보안에 좋습니다. 에러가 난 경우 미리 만들어둔 에러 페이지 또는 경고창이 뜨도록 하고, 사전에 확인을 위해 적어둔 에러 메시지 출력의 경우 실제 시스템에서는 사용하지 않는 것이 좋습니다.
이 외에도 웹 방화벽을 설정해 주거나, 저장 프로시저 사용, 최소 권한 유저로 DB 운영, ORM(Object-Relational Mapping) 사용 등과 같은 방법을 함께 사용하여 보안성을 높일 수 있습니다.
'Web > Web Hacking' 카테고리의 다른 글
NoSQL Injection (0) | 2024.09.07 |
---|---|
XXE External Entities (0) | 2024.09.06 |
File Download Vulnerability (0) | 2024.09.06 |
File Upload Vulnerability (0) | 2024.09.06 |
XSS (Cross Site Scripting) (0) | 2024.09.06 |