이번 포스팅은 SQL injection 공부중 공부할만한 sk에서 발간한 글이 있길래 공부겸 정리글을 써보고자 한다.

출처: https://www.skshieldus.com/download/files/download.do?o_fname=EQST%20insight_Special%20Report_202209.pdf&r_fname=20220926092447242.pdf

SQL Injection이란?

SQL Injection이란 설계된 SQL구문에서 사용자 입력값 검증이 미흡하여, 악의적으로 작동되는 쿼리를 삽입해서 공격하는것이다.

만약 공격자가 관리자의 아이디인 admin의 존재를 알고있지만, 비밀번호는 모를때 공격하는과정이다.

악성 클라이언트가
아이디: admin' -- //나머지는 주석처리
비밀번호: 1234 //주석처리되어 상관없음

Select * from member where id = admin --' and pw = '1234'

이런식으로 '--이후 주석처리로 인해 비밀번호 검증부분이 주석 처리된다.
이를 통해 admin계정에 접속이 가능하다.

공격 유형에 따른 분류

Union SQL Injection
Union 연산자는 두개 이상의 select문에 대한 결과를 하나로 묶어 데이터베이스에서 추출한다.

고로 기존의 select문 select * from member where id = 'candi'에다가 union절을 통해서 공격을 실행한다.

조건 1. Select문과 Union Select문의 칼럼수가 동일해야한다. => where 조건절에 Order By절을 이용하여 컬럼 수를 추출한다.(Order By절은 데이터 정렬시 사용되므로, select문의 컬럼갯수보다 많은 숫자를 조회하면 에러를 발생시키므로 컬럼의 갯수를 확인할 수 있다.)

SELECT id, name, email FROM users ORDER BY 1; -- 정상 실행
SELECT id, name, email FROM users ORDER BY 2; -- 정상 실행
SELECT id, name, email FROM users ORDER BY 3; -- 정상 실행
SELECT id, name, email FROM users ORDER BY 4; -- 오류 발생

조건 2. 컬럼은 각 순서별로 동일한 데이터형이어야 한다. => 데이터 형을 알기 위해서 컬럼의 갯수만큼 null문자를 입력해서 해당 컬럼의 데이터형이 숫자인지 문자인지 판단한다.

예를 들어 users에 3개의 컬럼 id,name,email이 있고 각각 int,varchar,varchar의 형태라면, 아까 조건 1을 통해서 컬럼갯수가 3개라는 사실을 파악할 수 있다.

이제 각 컬럼의 데이터 형을 추출하기 위해서 NULL값을 사용하여 테스트를 진행한다.

SELECT id, name, email FROM users
UNION
SELECT NULL, NULL, NULL; -- 정상 실행 (모든 컬럼이 NULL은 모든 데이터형과 호환)

//그 다음, 특정 컬럼에 NULL 대신 숫자 또는 문자열을 넣어 테스트 한다.
-- 첫 번째 컬럼이 숫자형인지 확인
SELECT id, name, email FROM users
UNION
SELECT 1, NULL, NULL; -- 정상 실행 (첫 번째 컬럼이 숫자형)

-- 두 번째 컬럼이 문자열인지 확인
SELECT id, name, email FROM users
UNION
SELECT NULL, 'test', NULL; -- 정상 실행 (두 번째 컬럼이 문자열)

-- 세 번째 컬럼이 문자열인지 확인
SELECT id, name, email FROM users
UNION
SELECT NULL, NULL, 'test@example.com'; -- 정상 실행 (세 번째 컬럼이 문자열)

해당 과정을 거치면, 컬럼갯수와 데이터형을 알아낼 수 있다.


기존의 select문에 union select절을 추가하면, 기존에 id,pw,email이 3개의 컬럼임을 알고 있으므로 id에는 null을 넣고, 나머지에 pw,email컬럼에 union절에 있는 id,pw를 출력시킬 수 있다.

Error Based SQL Injection
데이터베이스의 문법에 맞지 않는 쿼리문 입력 시 반환되는 에러 정보를 기반으로 공격하는 방법이다.

에러를 유발하는 함수중 하나인 CTXSYS.DRITHSX.SN을 사용하여 Member 테이블의 첫번째 컬럼의 패스워드를 조회한 결과 발생한 에러 메시지에서 비밀번호를 획득 할 수 있다.

자세한 사용법보다는 이런 함수를 통해서 유출이 될 수있다 정도로 생각하고 넘어가자.

Blind SQL Injection
참인 쿼리문과 거짓인 쿼리문 삽입시 반환되는 데이터를 비교하여서 데이터를 추출하는 공격이다.

일반적인 SQL Injection은 데이터를 바로 반환받아 공격자가 결과를 확인할 수 있지만, Blind Sql Injection은 데이터를 직접적으로 출력하는게 아니라 쿼리의 참,거짓 결과를 통해서 데이터를 노가다로 알아내는 방식이다.

예를 들어, users테이블에서 username을 추출하는 방식으로

SELECT username FROM users LIMIT 1 OFFSET 0;

해당 쿼리문을 통해서 alice라는 이름을 가져올 수 있다.

SELECT IF(SUBSTRING((SELECT username FROM users LIMIT 1 OFFSET 0), 1, 1) = 'a', 'true', 'false');

If절에는 alice가 나오고, alice의 첫번째 글자가 'a'인지 확인한다.
이떄 substring으로 alice에서 첫번째 글자를 짜른뒤에 확인한느것이다.

SELECT IF(SUBSTRING((SELECT username FROM users LIMIT 1 OFFSET 0), 2, 1) = 'l', 'true', 'false');

다음 sql에서는 2번째 l를 짜르고 'l'과 동일한지 비교한다.
이렇게 반복을 노가다로 계속하다보면 alice라는 이름을 추출할 수 있다.

단순히 대문자 소문자 52개니까 alice만해도 52^5이므로 굉장히 큰 경우의 수가 있으므로, 공격자가 일일히 수행하기보다는 도구를 사용하여 해당 과정을 자동화 한다.

해결방안

Prepared Statement
Prepared Statement는 SQl Injection을 방어하는 최선의 보안 대책이며, SQL구문이 미리 컴파일 되어 있어 입력값을 변수로 선언해 두고 필요에 따라 값을 대입하여 처리하는 방식이다.

Select문의 동작과정
Select문은 DBMS 내부적으로 4단계의 과정 Parse,Bind, Execute, Fetch를 거쳐 결과를 출력한다.

Parse를 통해서 select문을 거치면 아래와 같은 파싱트리가 생성된다.

일반적인 Statement는 Parse부터 Fetch까지 모든 과정을 매번 수행한다.
따라서, SQL에 악의적인 영향을 미치는 특수문자나 예약어가 들어간 경우 Parse과정에서 SQL구문의 일부로 작용하여 SQL Injection공격이 가능한다.


예를들어, Select * from member where id = admin --' and pw = '1234' 해당 구문에서 영향을 미치는 특수문자는 --이다. 왜냐하면 구문분석과정에서 --를 통해서 뒤에 pw 조건절은 무시하기 때문이다. 고로 --가 SQL구문의 일부로 작용하여 SQL Injection공격이 가능한것이다.

그러나 Prepared Statement의 동작과정의 경우 약간 다르다. 구문 분석과정을 최초 1회만 수행하여 생성된 결과를 메모리에 저장해 필요할 때마다 사용한다.

이를 통해 두가지 장점을 얻을 수 있다.

  1. 미리 구성된 파싱트리를 사용하기 때문에 Statment에 비해 시간을 단축할 수 있다.
  2. SQL구문이 미리 컴파일되어 사용자 입력값을 변수로 선언해 값을 대입하여 사용한다.

예시를 들어보자
취약한 Statment 코드

Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
...
String sql = "SELECT * FROM MEMBER WHERE ID = '"+param_id+"' AND PW = '"+param_ passwd+"'";
stmt = conn.createStatement();
rs = stmt.executeQuery(sql);

마약 역기서 param_id는 admin--'이 들어가고 parm_passd에 1234가 들어간다고 치자
그러면 완성된 sql문은 Select * from member where id = admin --' and pw = '1234'이게 되고 stmt.excuteQuery(sql)을 통해서 해당 쿼리문을 parse하고 --가 특수문자로 sql구문의 일부로 작용이된다.

안전한 Prepared Statment코드

String param_id=request.getParameter("id");
String param_passwd=request.getParameter("passwd");
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
…
String sql = "SELECT * FROM MEMBER WHERE ID = ? AND PW = ?";
pstmt.setString(1, param_id);
pstmt.setString(2, param_passwd);
rs = pstmt.executeQuery(sql);

여기서는 사용자의 입력값이 쿼리에 직접 삽입되지 않고, 파라미터 바인딩을 통해서 처리된다.

그래서 admin--'와 같이 입력을해도

SELECT * FROM MEMBER WHERE ID = 'admin--'' AND PW = '1234'

이런식으로 sql의 구문 일부분이 아니라, 바인딩을 통해서 그냥 문자열로 처리가 된다.

Prepared Statement를 사용하는게 불가능하다면?

언제나 Prepared Statement를 사용할 수 있는것은 아니다.
예를들어, Prepared Statement는 데이터를 파라미터로 전달하는 역할을 하기 때문에 Order By 절에서는 사용하지 않는다.
당연한게 Select from users where age= ? 여기에는 ?에 데이터값을 넣을수 있지만
Select
from users ORDER BY ? => 여기에는 정렬할 컬럼 이름을 넣어야하는데 컬럼 이름을 미리 알고있어야 동적으로 바인딩이 가능하기 때문이다.

또한, 실제 운영에서는 항상 Prepared Statment를 사용할 수 있는것은 아니다.

고로, WhiteList Filter, 입력값 정제등을 통해서 차선책을 고려해야한다.

WhiteList Filter
허용할 문자열을 제외한 모든 문자열을 필터링하는것이다.
입력값 정제
사용자 입력값이 SQL 구문에서 문법적인 의미를 갖지 못하도록 입력값을 다른값으로 치환하는 방법이다.
대표적으로 HashMap을 사용하는것인데, 사용자 입력값이 해시테이블의 값과 매핑을 통해서 정제되기 때문에 SQL구문에는 영향을 미치지 못한다.

또한, 에러 메시지 출력을 제한해야한다.
공격자가 악용할 가능성이 있는 에러 메시지가 노출 될경우 Error Based SQL Injection과 같은 공격이 가능하다.
따라서 Default 에러메시지가 아닌 사전에 정의한 에러 페이지를 반환하도록 대체해야한다.

profile
항상 Why?[왜썻는지] What?[이를 통해 무엇을 얻었는지 생각하겠습니다.]

0개의 댓글

관련 채용 정보

Powered by GraphCDN, the GraphQL CDN