모의해킹 스터디 복습

모의해킹 스터디 8주차(2): SQL Injection 대응 방법

whydontyoushovel 2024. 12. 11. 18:26

 

 

SQL Injection

서버에서 준비한 쿼리문에 악의적인 의도를 가진 SQL구문을 주입하는 해킹 기법.

 

 


 

 

SQL Injection이 SQL구문을 주입하는 해킹 기법이기 때문에 SQL Injection에 대한 보편적이고 확실한 대응은 서버에서 동적으로 할당되는 데이터를 어떻게 처리할 것인지에 달려있다.

 

여기서는 SQL Injection에 대응하는 방법 중 prepared statementwhitelist filtering에 대해 다뤄볼 것이다.

 

 

Prepared Statement

 

prepared statement의 가장 큰 특징은 준비된 쿼리문을 미리 컴파일 시켜둔다는 것이다.

가장 많이 접했던 select문을 예시로 살펴보자.

 

select id, pass, email from member where id = '___';

 

서버에 이런 쿼리문이 준비되어있는 상태라고 해보자.

사용자가 입력한 값을 where 조건절에 넣어 사용자 데이터를 식별하고 있다.

공격자는 SQL Injection을 위해 rara' and '1'='1 따위의 데이터를 입력해볼 것이다.

select id, pass, email from member where id = 'rara' and '1'='1';

 

 

1) prepared statement를 사용하지 않은 경우

 

prepared statement를 사용하지 않은 경우 사용자의 입력값을 서버의 쿼리문에 넣은 다음에 컴파일이 진행되기 때문에 SQL Injection을 위한 페이로드까지 쿼리문으로 인식되어 컴파일시킨다. 쿼리의 구조를 바꿀 수 있게 되는 것이다.

 

입력값 삽입 -> 기계어로 컴파일 -> 실행 -> 결과 출력

 

이런 식으로 진행되는 것이다.

 

 

2) prepared statement를 적용할 경우

 

prepared statement는 서버의 SQL 질의문을 미리 기계어로 컴파일해둔다.

 

그래서 공격자가 rara' and '1'='1 을 입력해도 ' and '1'='1 부분이 서버의 쿼리문 일부로 컴파일되는 것이 아니라 별개의 입력값으로 처리되는 것이다.

밥솥에서 대기 중인 물과 쌀에 내가 몰래 불려둔 콩을 넣어서 밥을 지으면 콩밥이 되지만 이미 따끈따끈하게 지어진 밥 위에 불려둔 콩을 뿌리면 수상할 정도로 축축한 콩이 올라간 밥이 되는 것이다. (이게 나의 최선이다...)

 

p-s적용 안시킴: 공격자가 뭘 넣어서 나쁜 쿼리문을 실행시켜버림.

p-s적용 시킴: 서버 쿼리문을 이미 컴파일시켜서 공격자가 뭘 넣든 그냥 입력값됨.

 

결국 쿼리문의 일부가 되느냐 마느냐가 포인트로 보인다.

 

  • 사용법

mysqli를 쓰느냐, pdo를 쓰느냐, 그리고 객체 지향 방식으로 쓰느냐 절차지향 방식으로 쓰느냐에 따라 정확한 소스코드는 달라지지만 아무튼 prepared statement를 쓰려면 사용할 쿼리문을 준비시켜야한다.

(여기선 mysqli를 적용한 예시를 적어둘 것이다.)

 

위의 예시에서 사용했던 아래의 쿼리를 사용한다고 쳐보자.

select id, pass, email from member where id = '___';

 

prepared statement에서는 사용자 입력값 부분에 플레이스 홀더(?)를 사용한다.

그럼 아래와 같이 될 것이다.

 select id, pass, email from member where id= ?;

 

물음표 부분만 쏙 빼놓고 기계어로 컴파일 될 것이고 나중에 사용자 입력값을 준비된 쿼리문에 넣을 때 저 위치에 쏙 집어넣게 된다.

 

 

이 쿼리문을 prepare() 의 괄호 안에 넣고 접근 권한($db_conn등에 저장한 DB 권한)을 이용해 컴파일해둔다.

컴파일한 데이터는 변수에 저장해둔다. 그럼 아래와 같은 코드가 된다.

$stmt = mysqli_prepare($db_conn, "select id, pass, email from member where id=?");

 

 

이후 플레이스홀더로 표시한 부분에 사용자 입력값을 넣어 쿼리를 실행시키는 과정을 수행해야한다.

이때 입력값을 쿼리의 플레이스홀더에 대입시키는 걸 바인딩한다고 표현한다.

 

그 과정에서 이런 코드가 쓰인다.

mysqli_stmt_bind_param($stmt, "s", $user_id);

 

첫번째 인자는 컴파일된 쿼리문이고

두번째 인자는 입력값의 자료형을 뜻한다. (s: 문자열, i: 정수, d: 실수, b: 바이너리) 

세번째 인자로 사용자 입력값이 들어간다.

 

 

$stmt변수에 사용자 입력값이 바인딩된 준비된 쿼리문이 저장되었다.

이제 쿼리문을 실행할 차례이다.

실행할 땐 아래와 같은 메서드를 사용한다.

mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt);

 

mysqli_stmt_execute메서드로 쿼리를 실행시키고 그 결과를 $result 변수에 저장하였다.

 

 

정리하자면

1) 쿼리문 기계어로 저장 (플레이스홀더로 입력값 위치 표시)

2) 입력값 바인딩

3) 입력값 바인딩된 쿼리 실행

4) 결과 저장

 

이런 식으로 미리 쿼리문을 기계어로 만들어두기 때문에 질의문 구조를 바꿀 수가 없게 되어 SQL Injection에 대응하는 방법이 된 것이다. 으아아아주 강력한 대응법이다!!!!

더하여 처리 속도도 높아진다고 하니 안 쓸 이유는 더더욱 없다.

그래서 안 쓰는 곳이 없다고 한다. 99.999%가 사용 중이라고 한다.

 

그럼 왜 배운 거임????

 

  1. prepared statement를 잘못 사용 중인 경우 SQLi 취약점이 일어날 수 있음.
  2. prepared statement가 적용되지 않는 부분이 있음.

1번은 예를 들어 물음표 문법을 쓰지 않고 입력값 저장 변수로 둔 경우가 그러할 수 있고

2번의 경우는 prepared statement의 한계이다. 얘는 컬럼명이나 테이블이름, order by 절엔 구멍을 뚫어둘 수 없다고 한다.

prepared statement는 컬럼명이나 테이블이름, order by 절은 SQL 구조의 일부로 간주하기 때문에 플레이스홀더로 처리할 수 없다는 것 같다.

그래서 예를 들어 where 조건절의 컬럼명이나 order by 절을 동적으로 할당하려면 이런 식으로 써야하는 것이다.

$query = "select * from board where $filter like '% ? %' order by $sort";

 

이렇게 만든 후 prepare($query) 처리하더라도 이미 where 절에 1=1 and title과 같은 SQL Injection이 실행되어 버린다.

공격자 입장에서는 그렇게 SQL Inejction을 시도하면 되겠지만 서버 입장에선 그 공격을 어떻게 막으면 될까?

여기서 whitelist filtering이 쓰일 수 있다.

 

 

 

Whitelist Filtering

 

필터링에는 두 가지 종류가 있다.

  1. blacklist filtering
  2. whitelist filtering

블랙리스트 필터링은 특정 문자는 쓰지 못하게 하는 것이고

화이트리스트 필터링은 특정 문자만 허용되게 하는 것이다.

 

블랙리스트 필터링은 사용자 입력값을 제한할 때 유용할 것이고

화이트리스트 필터링은 컬럼명과 같이 유효한 단어가 정해져있을 때 쓰기 좋을 것이다.

그래서 prepared statement로 막지 못하는 포인트는 화이트리스트 필터링을 이용하는 것이다.

 

화이트리스트 필터링을 적용한 코드의 흐름은 대충 이렇다.

 

1) 받은 값을 변수에 저장

2) 그 값이 유효한 단어 인지 검증

3) 유효하면 그 단어를 포함한 prepared statement 실행

4) 유효하지 않으면 디폴트로 선택되는 단어를 포함한 prepared statement 실행

 

$sort = $_POST['sort'];

if ($sort == 'title') {
	$stmt = $conn -> prepare("select * from board where title like '% ? %'");
} elif ($sort == 'username') {
	$stmt = $conn -> prepare("select * from board where username like '% ? %'");
} else {
	$stmt = $conn -> prepare("select * from board where username like '% ? %'");
}

$stmt -> bind_param("s", $search);
//이후 실행, 결과 저장 코드

 

혹은 입력값 검증 함수를 만들어서 컬럼명이나 order by 절의 파라미터를 받을 수 있다.

$sort = $_POST['sort'];

$order = sortValidate($sort);
//sortValidate에서 sort값이 유효하면 post로 받은 sort값 반환

$query = "select * from board where $sort like '% ? %'";

$stmt = $conn -> prepare($query);
//이후 값 바인딩 및 실행, 결과 저장

 

방법은 다양하다. 다만 어떤 방법을 사용하든 유효한 컬럼명을 파라미터로 받았는지 검증하는 과정이 필요할 뿐이다.

 

 

 

정리

 

지금까지 SQL Injection에 대한 대응 방법으로 두가지를 정리해보았다.

1) prepared statement

2) whitelist filtering

 

prepared statement는 서버에서 쿼리문을 미리 컴파일해 사용자 입력 데이터로 쿼리의 구조를 바꿀 수 없게 한다. 강력한 대응 방법이며 처리 속도가 빨라지기까지 해서 사용하지 않는 사이트가 없을 정도지만 prepared statement는 입력 데이터에 대한 보호만 가능하고 동적으로 할당되는 컬럼명이나 테이블명, order by 절의 정렬 기준 컬럼 등은 SQL구조로 판단해 적용하지 않는 단점이 있다.

이 단점을 보완하는 방법으로 whitelist filtering이 있다. whitelist filtering은 특정 문자만 사용 가능하도록 하는 필터링의 일종으로 동적으로 할당받은 컬럼명, 테이블명, order by 절의 데이터를 검증하여 유효하다면 해당 데이터를 사용하여 쿼리를 실행하고 유효하지 않다면 기본으로 적용되는 데이터를 사용하여 쿼리를 실행하게 만드는 방법이다.

이 두가지와 더불어 입력 수 제한까지 하면 공격자 입장에서 귀찮아서라도 해킹을 포기하지 않을까 싶다.