1. SQL Injection 공격 원리
- SQL Injection 공격을 이해하기 위해서는 데이터가 어떤 과정으로 저장되고 사용되는지 알아야 한다.
- 게시판에서 회원가입을 할 때 아이디와 패스워드를 입력한 정보는 모두 어디에 저장이 되었을까?
- 이 게시판에서는 회원가입 정보를 board 데이터베이스의 member 테이블에 저장한다.
- 아이디는 bId 열에, 패스워드는 bPass 열에 각각 저장되어있다.
- 로그인 페이지 파일명은 default.htm이다.
- 등록된 사용자의 아이디와 패스워드를 입력 받아 member 테이블에서 동일한 아이디와 패스워드를 가진 레코드를 찾아 사용자가 게시판에 접근하는 것을 허용한다.
- 계정이 존재하지 않거나 패스워드가 올바르지 않으면 잘못된 로그인임을 알려줄 것이다.
- 아래 소스를 보면 login_ck.asp 파일로 정보를 보내고 있음을 확인할 수 있다.
- login_ck.asp 파일을 열어보면 아이디(memberid)와 패스워드(pass1)를 받아서 SQL 쿼리문(strSQL)을 만들고 실행시켜 member 테이블의 정보를 가져온다.
- 입력된 아이디와 패스워드에 대한 결과 값이 있다면, 즉 실제로 등록된 사용자라면 board_list.asp가 실행되고, 아이디와 패스워드를 잘못 입력하거나 등록되지 않은 사용자라면 잘못된 로그인입니다라는 메시지를 보낸다.
2. 공격 방법
▶ 인증 우회 ( Authentication Bypass )
⊙ 인증 우회
- 가장 널리 이용되는 SQL Injection 방법은 OR 연산을 사용하는 것이다.
- 입력 폼에 `or '1'='1 문을 입력하여 결과 값을 참으로 만들어 우회하는 방법이다.
- 예를 들어, 로그인 화면에서 아이디는 TEST_ID로 하고, 패스워드는 test로 해서 로그인을 시도할 경우 패스워드가 틀려서 잘못된 로그인이라는 메시지를 받게 된다.
- 하지만, 패스워드에 test' or '1'='1을 입력하면 login_ck.asp 파일에서 회원 로그인을 처리하기 위해 데이터베이스에 요청하는 쿼리문을 최종적으로 select * from member where bId = 'TEST_ID' and bPass = 'test' or '1'='1'과 같이 전달되고 이 때 where 절 뒤쪽에 임의로 삽입된 oR 조건에 의해서 OR 앞이나 뒤의 두 조건 중에 하나라도 참이면 참이 되어 결과 값을 반환한다. 여기에서 1과 1은 같은 값이므로 앞에 어떠 값이 오더라도 항상 참.
- 여기서 한 가지 더 주의할 점은 홑따옴표(')의 사용이다. 아래의 소스 코드에서 알 수 있듯이 아이디와 패스워드로 SQL 문을 생성
- 이 때, TEST_ID와 test은 홑따옴표 안에 들어간다. 이미 알고 있는 것처럼 문법상 홑따옴표는 2개를 쌍으로 사용해야 한다.
- 따라서, SQL 문을 조작할 때도 에러가 나지 않도록 주의해야 한다.
- test' or '1'='1을 입력하면 아래와 같은 SQL 문이 만들어진다.
- 모든 사이트가 동일한 형식으로 쿼리문을 생성하는 것은 아니다. 여기에서도 test' or '1'='1 뿐만 아니라 'or 1=1도 공격에 이용할 수 있다.
⊙ 패스워드 우회
- 패스워드가 틀려도 로그인에 성공할 수 있는지 확인하기 위해 로그인 창에 쿼리문을 입력해보자
- 로그인에 성공하였지만 TEST_ID가 아니고 admin으로 로그인 되었을까..
- SQL 문을 한번 더 보게 되면, 연산자 우선순위로 볼 때, AND는 OR보다 먼저 계산된다.
- 따라서, (bId='TEST_ID' and bPass='test')가 먼저 계산되어 그 결과가 FALSE로 되고, FALSE or (1=1)이 그 다음에 계산된다.
- 즉, FALSE or TRUE가 되어 결과적으로 TRUE가 되고 결국 4)와 같이 member 테이블에서 모든 레코드 값을 가져오게된다.
- 가져올 아이디 값이 정해지지 않았기 때문에, 가장 위쪽의 레코드를 읽어온다.
- 따라서, member 테이블에는 아래와 같이 2개의 계정만 존재하고, TEST_ID보다 admin이 위에 있다.
- 따라서, 가장 위에 있는 레코드의 admin 값을 가져오게 되어 admin으로 로그인에 성공한 것이다.
- 많은 경우에 관리자(admin)가 가장 위에 존재하기 때문에 이런 방법으로 관리자로 로그인 할 수도 있다.
⊙ 주석문을 이용한 admin 로그인
- 1)의 SQL 문을 2)처럼 바꾸면, 주석문(--) 이후의 문자열은 주석 처리되어 무시되므로 결과적으로 3)의 SQL 문이 된다.
- 이것을 로그인 페이지에서 입력해보자. ( 이 때도 홑따옴표를 잊지 말자.)
- admin으로 로그인에 성공했다. 이제 이 게시판의 관리자 권한을 갖게 된 것이다.
▶ 시스템 명령어 실행
- xp_cmdshell은 MS-SQL DB에 잇는 master DB의 확장 프로시저로서, 이것을 이용하면 시스템 명령어(OS Call)를 실행할 수 있다.
- SQL 쿼리 분석기 창에서 xp_cmdshell을 사용하기 위해 등록할 떄는 다음과 같은 명령어를 실행하면 된다.
등록 |
sp_addextendedproc 'xp_cmdshell', 'xplog70.dll |
제거 |
sp_dropextendedproc 'xp_cmdshell' 또는 C:\Program Files\Microsoft SQL Server\MSSQL\Binn에서 xplog70.dll 삭제 |
⊙ 두가지 이상의 쿼리문 실행
- 두 가지 이상의 명령을 연속해서 사용할 때는, 세미콜론(;) 표시를 이용하여 사용할 수 있다.
- 먼저 member 테이블의 내용을 확인하고, 자기 자신의 시스템에 ping 명령을 수행하는 ping 127.0.0.1을 실핼하였다.
⊙ 시스템에 폴더 생성하기
- 앞서 테스트한 ping 127.0.0.1이 포함된 SQL injection 명령을 게시판에서 실행하면, 공격이 성공하여 잘 실행이 되어도 결과를 확인할 방법이 없다. 그렇기 때문에 폴더를 만들어 시스템 명령어가 잘 실행되었는지 확인하자.
- 폴더를 생성하는 명령은 다음과 같다.
- 로그인 페이지의 아이디 입력 폼에 위의 쿼리문을 입력하고 싶어도 할 수 없을 것이다.
- 왜냐하면 아래의 예제에서 그 내용을 확인할 수 있듯이 입력 값의 길이가 15로 제한되어 있기 때문이다.
- 이렇게 길이에 제한이 있어 원하는 SQL Injection 문을 직접 입력할 수 없는 경우도 있다.
- 이럴 때는 프록시 툴을 이용하여 길이 제약을 피할 수 있다.
- 예로들면 오디세우스 프록시 툴을 이용하여 SQL Injection 문을 서버로 전달할 것이다.
- 먼저 admin/admin 계정으로 로그인할 때 전송되는 내용을 확인해보자.
- memberid=admin 부분을 아래와 같이 조작하여 전송하면 길이 제한에 상관없이 시스템 명령어를 실행할 수 잇다.
- URL 인코딩을 위해 공백은 %20으로 변경해야 한다.
- admin 계정으로 로그인되었지만 게시판 리스트만 보일 뿐 특별한 내용은 없다.
- 명령어가 제대로 실행되었는지 실제로 폴더를 확인해보면 C:\board 아래에 test 폴더가 생성된 것을 확인할 수 있다.
▶ 테이블 정보 열람하기
⊙ 집계함수
- 집계는 이미 계산된 것들을 모아서 계산하는 것을 말한다.
- SQL의 집계 함수에는 avg(), sum(), count() 등이 있다.
- 즉, 이미 계산된 결과를 어떤 기준으로 그룹화 하는 함수들이다.
- 그룹화하는 기준을 집계 키라고 하는데, GROUP BY 절의 칼럼이나 연산식을 집계 키로 사용할 수 있다.
- GROUP BY 절에 사용된 집계 키들만 SELECT 목록에 사용할 수 있다는 것이다. 그렇지 않으면 에러가 발생!
- 예를 들어, 쿼리 분석기에서 아래의 명령을 실행. 여기에서 member.bId는 테이블명.필드명으로 특정 필드를 가리키는 표현 방식
- 아래와 같이 SELECT 절에는 bId, bPass, bName, bMail이 있고 GROUP BY절에는 member.bId만 있을 경우에 에러가 발생한다.
- 왜냐하면 bPass, bName, bMail은 GROUP BY에서 사용하는 집계 키가 아니라서 SELECT 절에 사용할 수 없기 때문이다.
⊙ 테이블 명 획득
- GROUP BY 절이 어떤 칼럼을 기준으로 그룹을 만들 때 HAVING는 결과를 다시 한번 더 필터링 하는 역할을 한다.
- 이것은 GROUP BY 절에 대한 조건, 즉 일반적인 SELECT 절의 WHERE와 비슷하다.
- HAVING는 단독으로 쓰일 수 없으며 GROUP BY 절과 함께 사용되어야 한다.
- 이 점을 이용하여 다음과 같은 명령을 검색어로 입력하면 에러가 발생하는 것을 확인 할 수 있다.
- 에러는 'bbs.idx' 열이 집계 함수에 없고, GROUP BY 절이 없으므로 SELECT 목록에서 사용할 수 없다는 내용이다.
- 이 에러 메시지에는 bbs라는 테이블 명과, idx라는 필드명을 언급함으로써 중요한 정보를 노출하고 있다.
⊙ 필드 명 획득
- 이전에 획득한 테이블 명과 필드 명을 다시 이용하여 검색 쿼리문에서 사용하면 bbs 테이블에 있는 다른 필드 명도 알아낼 수 있다.
- 입력 값으로 group by 조건에 idx를 추가하였다.
- 이렇게 하면 집계 키에 idx가 있으므로 그 다음에 있는 tId를 볼 수 있다.
- 하지만 이것은 집계 키에 없으므로 bbs.tId를 사용할 수 없다는 에러를 발생시킨다.
- 이런 방식으로 계속해서 아래와 같이 반복하면 bbs 테이블의 전체 필드 명을 알 수 있다.
●
●
●
- 이 과정을 통해 bbs라는 테이블에 idx, tId, tName, tMail, tTitile, tContent, tFilename, tfilepath, tRead, tDate라는 10개의 열리 존재한다는 것을 알 수 있다.
- 로그인 페이지에도 위와 동일한 방식으로 테이블의 필드 명을 알아낼 수 있다.
- SQL 쿼리 분석기에서 로그인 페이지에 대해 위의 과정을 실행하여 결과를 확인해보자.
- 쿼리 분석기에는 에러가 발생해도 끝까지 실행하기 때문에 전체 에러 메시지를 확인할 수 있다.
- 결국, member 테이블에 idx, bId, bPass, bName, bPost, bAssr1, bAddr2, bPhone, bMail, bDate 열이 있으며 로그인 페이지에는 이것을 이용하여 쿼리를 하고 있다는 것을 짐작할 수 있다.
⊙ 필드 타입 획득
- 이제 UNION과 sum()함수를 이욯아여 필드 타입을 알아보자
- sum() 함수에는 숫자형만 사용할 수 있고 UNION을 사용하기 위해서는 두 테이블의 필드 수가 동일해야 한다.
- admin으로 로그인을 한 후 게시판 리스트 하단에 게시글을 검색하는 입력 폼에 테스트
- 아래의 board_search.asp 파일을 열어 확인해보면 검색어(Keyword)를 받아, Select * From bbs Where tTitle like '%keyword%'을 생성
- 쿼리문이 제대로 실행되면 제목에 keyword가 포함되어 있는 게시글을 찾아서 보여줄 것이다.
- 검색어에 union을 사용하여 member 테이블과 bbs 테이블의 필드 타입을 알아낼 수 있다.
- sum() 함수는 varchar 타입을 사용할 수 없기 때문에 에러가 발생하게 되고 bId가 varchar타입인 것을 알 수 있다.
- 동일한 방법으로 member 테이블의 bId, bPass, bName, bpost, bAddr1, bAddr2, bPhone, bMail과 bbs 테이블의 tId, tName, tMail, tTitle, tContent, tfilename, ffilepath가 varchar 타입을 사용한다는 것을 확인할 수 있다.
- 그리고 member.bDate와 bbs.tDate가 datetime 데이터 형식을 사용한다는 것도 확인하였다.
- 이제 member.idx와 bbs.idx, bbs.tRead만 확인하면 된다.
- 아래의 결과는 UNION에서 필드의 개수를 맞추지 않았기 때문에 에러가 발생했다는 것이며, 이는 데이터 형식에 대한 에러가 아니기 때문에 idx가 숫자형이라는 것을 유추할 수 있다.
- 실제 우리가 사용하는 테이블과 그 내용이 동일함을 알 수 있다.
member 테이블 |
bbs 테이블 | ||
필드 명 |
필드 타입 |
필드 명 |
필드 타입 |
idx |
숫자 |
idx |
숫자 |
bId |
varchar |
tId |
varchar |
bPass |
varchar |
tName |
varchar |
bName |
varchar |
tMail |
varchar |
bPost |
varchar |
tTitile |
varchar |
bAddr1 |
varchar |
tContent |
varchar |
bAddr2 |
varchar |
tfilename |
varchar |
bPhone |
varchar |
tfilepath |
varchar |
bMail |
varchar |
tRead |
숫자 |
bDate |
datetime |
tDate |
datetime |