대응 방안 개요
일반적인 방법
- 시큐어 코딩 → 근본적인 대응 방안
- 인라인 보안솔루션(IPS,WAF 등) → 한계가 있다. (우회 할 수 있음)
시큐어 코딩
1. Prepared Statement 사용
2. 사용자 입력 값 타입에 따른 입력 값 검증 로직 구현
3. 길이 제한
Prepared Statement 가 적용되어 있다면 굳이 2,3번을 적용할 필요가 없다.
3번 길이제한은 단독으로 사용하면 안된다. → 1번 or 2번과 같이 적용해야 한다.
대응 방안(1) - Prepared Statement 사용
Prepared Statement 는 SQL Injection에 대한 방어 뿐만 아니라 성능적인 이점에 의해서 많이 사용된다.
하지만 적절하게 사용하지 않는 경우 Prepared Statement 또한 취약할 수 있다.
취약한 소스코드
- Statement 를 사용하여 질의하는 방식
안전한 소스코드
- Prepared Statement 를 사용하여 프리 컴파일 후에 사용자 입력값을 바인딩해서 결과를 얻어온다.
- 프리 컴파일: 코드에 삽입된 SQL문을 DB와 연결하는 작업 수행(컴파일 전에 수행하는 작업)
SQL 구문 실행 단계
1. 구문 분석 및 정규화
2. 컴파일
3. 쿼리 최적화
4. 캐시
5. 실행
구문 분석 및 정규화
→ 쿼리 구문 및 의미를 검사
→ 쿼리에 사용된 테이블, 컬럼이 존재하는지 확인
컴파일
→ 쿼리에 사용되었던 SELECT, FROM, WHERE 절 등을 컴퓨터가 이해할 수 있는 형식으로 변환
→ 사람이 이해하는 언어에서 컴퓨터가 이해하는 언어로 변환
쿼리 최적화
→ 쿼리를 실행할 수 있는 방법을 찾기 위한 의사결정 트리가 만들어짐
→ 쿼리 실행 시 최고 효율로 실행하는 방법을 찾는다
캐시
→ 위의 3단계 결과를 캐시에 저장
→ 동일한 쿼리가 캐시에 들어올때마다 캐시에 저장된 기존 쿼리를 실행
→ 중복된 쿼리를 불필요하게 거칠 필요가 없음, 성능적 이점이 있다.
실행
→ 제공된 쿼리가 실행되고 결과를 반환
Prepared Statement 가 안전한 이유
String query = "select * from board where content like ?";
→ 쿼리가 완성되지 않은 상태에서 질의를 한다.
물음표(?)는 사용자 입력값과 치환된다.
1단계를 거칠 때는 물음표(?)는 단순 문자열이다, 아무런 역할도 없다.
2단계를 거칠 때는 물음표(?)가 나중에 사용자 입력값과 치환이 되기 위해서 PlaceHolder의 기능을 하게끔 특수처리가 된다. (컴퓨터가 알 수 있는 형식으로 특정 값으로 변환된다.)
3단계를 거치고 4단계에서 캐시에 저장이 된다. → pstmt = conn.prepareStatement(query);
컴파일된 쿼리가 캐시에 저장이 된다.
사용자가 입력한 데이터가 세팅이 될때, 미리 컴파일된 쿼리가 캐시에서 선택이 된다. (컴파일된 쿼리가 캐시에 저장되어 있음)
PlaceHolder 부분이 setString 메소드에 의해 치환이 이루어진다.
1~4단계를 거치면서 이미 컴파일을 진행하였기 때문에, 사용자 입력값(PlaceHolder)에 입력된 데이터는 순수 문자열로 처리 된다. 설령 ' and 1=1 등의 SQL 쿼리를 입력하더라도 SQL문으로 인식하지 않게 된다.
컴파일을 거치는 이유는 사람이 알 수 있는 언어에서 컴퓨터가 알 수 있는 언어로 변환하기 때문에 컴파일 이후에 입력된 SQL 구문은 컴퓨터가 이해할 수 없게 된다.
→ SQL 구문을 컴퓨터가 해석하려면 컴파일이 되어야하는데, 이미 컴파일된 구문에 사용자 입력값(PlaceHolder)만 추가됨
4단계 캐시에서는 단순히 사용자 입력값을 이미 컴파일된 쿼리에 넣기 때문에, 컴퓨터가 사용자 입력값에 포함된 SQL 쿼리를 이해하지 못하므로 SQL Injection이 발생할 수 없다.
사용자 입력값 데이터 타입에 따른 메소드
setString → 문자
setInt → 숫자
request.getParameter("idx")
→ 원래 문자형인데, Integer.parseInt 를 사용하여 String타입의 숫자를 int타입으로 변환해준다.
Prepared Statement를 안전하게 사용해야한다. 잘못 사용하면 취약할 수 있음
프리 컴파일 이전에 사용자 입력값을 바인딩하는 경우 SQL Injection에 취약하게 된다.
pstmt = conn.prepareStatement(query); 구문 이전에 "select * from board where content like '%" + keyword + "%'"; 구문을 사용하여 사용자 입력값이 바인딩되는 경우, SQL Injection에 취약하게 된다.
keyword 부분에 ' and 1=1 등의 SQL 구문이 삽입되면 컴파일되어 SQL Injection 공격이 가능해진다.
안전하게 사용하기 위해서는 PlaceHolder 를 사용하여 프리 컴파일 이후에 사용자 입력값을 바인딩해야 한다.
전통적인 자바 웹 환경에서 JDBC를 통해서 DB를 연결했는데, 오늘날의 자바 웹 환경은 프레임워크를 사용하게 된다.
iBatis, myBatis 환경에서 $,# 문자열은 다음과 같은 의미를 가진다.
$ → Statement
# → Prepared Statement
$를 #으로 변경해줘야 안전하게 사용할 수 있다.
Hibenate 같은 경우에도 콜론 : 문자를 이용하면 안전하게 사용할 수 있다.
사용자 입력 값을 통해 테이블/컬럼명을 입력 받을 경우 왜 Prepared Statement 사용이 불가능할까?
→ 테이블/컬럼명은 컴파일하여 컴퓨터가 알 수 있는 언어로 변환해야하는데, Prepared Statement를 사용하게 되면 컴파일 이후에 캐시에서 사용자 입력값(테이블/컬럼명)을 받아서 쿼리를 실행하게 되어 테이블/컬럼명은 단순 문자열로 처리되어 컴퓨터가 인식하지 못하게 된다. 그러므로 정상적인 SQL 질의가 불가능해진다.
→ 테이블/컬럼명이 컴파일되지 못하고 나중에 사용자 입력값으로 받기 때문에 컴퓨터가 인식하지 못하므로 Prepared Statement 사용이 불가능하다.
select * from ? where content like ?
select * from ㅁ where content like ㅁ
→ 어떤 테이블에서 데이터를 조회해야하는지 알 수 없음
Prepared Statement에서 setString 을 사용하는 것은 순수한 사용자 입력값만 가능하다.
키워드, 명령절 등 컴파일이 필요한(컴퓨터에 명령을 내려야하는) 부분에는 사용이 불가능하다.
사용자 입력 값을 통해 테이블/컬럼명을 입력 받을 경우 Prepared Statement 사용이 불가능하며, 이때는 사용자 입력 값 타입에 따른 입력 값 검증 로직을 구현하여 안전하게 사용할 수 있다.
대응 방안(2) - 사용자 입력 값 타입에 따른 입력 값 검증 로직 구현
사용자 입력 값 타입
- 숫자 → 숫자만 입력
- 문자 → 싱글 쿼터에 대한 검증
- 테이블/컬럼 → board, title, idx
- 키워드 → asc, desc
select * from board where title like '%test%' order by idx desc
테이블/컬럼, 키워드는 문자형 검증 하듯이 대응하면 취약하다.
싱글 쿼터 없이도 공격이 가능하다.
^ → 문자열 시작
$ → 문자열 끝
Pattern.matches → 지정된 정규식을 컴파일하고 지정된 입력과 일치시키려고 시도한다.
is_numeric → 주어진 값이 숫자 또는 숫자 문자열인지 확인하는 함수, 주어진 값이 숫자면 True 아니면 False 반환
JAVA 소스코드는 숫자일 경우에는 True, 숫자가 아닐 경우에는 False를 반환하며, if(!flag)에 의해 숫자일 경우에는 정상적으로 쿼리가 작성되며, 숫자가 아닌 경우에는 에러메시지가 표시된다.
PHP 소스코드는 숫자일 경우에는 True, 숫자가 아닐 경우에는 False를 반환하며, if(!is_numeric($seq))에 의해 숫자일 경우에는 정상적으로 쿼리가 작성되며, 숫자가 아닌 경우에는 에러메시지가 표시된다.
문자검증 소스코드(JAVA), MS-SQL, ORACLE 대응방안
' → '' 싱글쿼터 2개
" → "" 더블쿼터 2개
replace → 싱글쿼터는 싱글쿼터 2개로, 더블쿼터는 더블쿼터 2개로 바꾼다.
MySQL 같은 경우 싱글쿼터를 이스케이프 처리한다.
싱글쿼터는 특정 기능을 하는 문자인데, 만약 사용자가 싱글쿼터를 검색하고 싶으면, 싱글쿼터를 이스케이프 처리해야 한다.
싱글쿼터 2개를 입력하면 순수 사용자 데이터로 싱글쿼터 1개가 인식된다.
MySQL에서는 싱글쿼터 2개 '' , \' 를 입력하면 싱글쿼터 1개로 인식된다.
" , \' → '
만약 MySQL에서 위와 같이 싱글쿼터1개가 싱글쿼터 2개로 치환되는 대응방안을 적용했다는 가정하에,
공격자는 \'를 입력하면 SQL Injection 공격이 가능하다.
문자검증 소스코드(JAVA), MySQL 대응방안
' → '\
\ → \\
PHP 에서는 real_escape_string 을 사용하면 된다.
real_escape_string
- NULL(\x00), \n, \r, \, ', " 문자 앞(왼쪽)에 \를 붙여 사용자의 입력에 의해 악의적인 쿼리문이 실행되는 것을 막는다.
- 해당 함수가 호출되면 특정 문자를 이스케이프 처리한 후의 문자열을 반환한다.
테이블/컬럼으로 사용자 입력값을 받을 경우, 정규표현식을 활용하여 화이트리스트 형식으로 검색에 필요한 문자,숫자, 특수문자만 입력받는다.
여기에 길이제한 방식을 추가하면 SQL Injection이 더 어려워지게 된다.
테이블명에 들어가는 특수문자가 어떤게 있는지 파악한 상태에서 특정 특수문자만 허용하는 방식을 사용한다.
만약 테이블/컬럼명에 특수문자나, 숫자가 없다면 특수문자와 숫자는 제외한다.
테이블/컬럼명에 있는 문자만 허용하는 화이트리스트 방식을 사용한다.
특정 키워드는 ASC,DESC 등으로 한정되어 있다.
위의 검증은 equals 메소드를 사용하여 값을 비교하여 사용자 입력값이 바인딩되는것이 아닌, Server-Side에서 작성된게 바인딩되는 형태로 SQL Injection에 안전하다.
equals → 두 인스턴스의 주소값을 비교하여 같은 인스턴스인지 확인, 같으면 True 다르면 False 반환
대응 방안(3) - 길이 제한
사용자 입력값에 대해서 들어오는 문자의 길이를 검증하는 방법이다.
문자의 길이값이 초과되면 검증 로직에 의해 에러 메시지를 출력하는 소스코드이다.
SQL Injection 취약점은 길이 제한에 민감하다.
길이 제한 검증 방법은 일종의 보험으로 사용한다. (무조건 사용은 아니지만 할 수 있으면 한다.)
단독으로 사용하지 않고 Prepared Statement 또는 사용자 입력값 검증 로직과 같이 사용한다.
실습2-10 취약 환경 시큐어 코딩 적용 실습
시큐어 코딩 실습 전에 새로운 실습 환경을 추가한다.
→ 웹 사이트는 htdocs 폴더에 넣고 MySQL에서 DB 및 테이블을 추가하는 방식
강의 내 취약 환경 구축 자료 다운로드 후 C:\APM_Setup\htdocs 폴더 내에 압축 해제한다.
폴더명은 insecure_website2 로 변경한다. (경로: C:\APM_Setup\htdocs\insecure_website2)
압축 해제 후 common.php 내 mysql 패스워드를 test로 변경, db는 pentest2로 변경해준다. (기존에 pentest DB가 존재하므로)
insecure_website2 폴더 내 query.txt 쿼리 중 DB 생성 및 사용 쿼리를 변경해준다.
create database pentest2;
use pentest2;
create와 use 부분을 변경 후 전체 명령어를 복사해서 MySQL CMD 창에 그대로 붙여넣는다.
쿼리 복붙 후 DB, 테이블 생성 및 데이터 입력이 완료되면 insecure 홈페이지에 접속을 시도한다.
insecure_website2 에 정상적으로 접속된다.
기존의 insecure_website는 시큐어 코딩에 사용하며, 새로운 insecure_website2는 백업용으로 놔둔다.
SQL Injection에 취약한 어플리케이션 기능은 대표적으로 게시판에서 사용하는 기능들로 볼 수 있다. 검색, 게시글, 로그인, 회원가입 기능 등등
데이터베이스와 연결되어 있는 기능들은 잠재적인 SQL Injection 위협에 노출되어 있다고 볼 수 있다.
여러 페이지 중 로그인 페이지에 대한 시큐어 코딩을 진행한다.
login.php 파일을 연다. 보통 Form 페이지와 Action 페이지가 따로 있지만 login.php에는 같이 존재한다.
< login.php 의 Action 페이지 >
<?
$db_conn = mysql_conn();
if(!empty($_SESSION["id"])) {
echo "<script>location.href='index.php';</script>";
exit();
}
$id = $_POST["id"];
$password = $_POST["password"];
if(!empty($id) && !empty($password)) {
$password = md5($password);
$query = "select * from members where id='{$id}' and password='{$password}'";
$result = $db_conn->query($query);
$num = $result->num_rows;
if($num != 0) {
$row = $result->fetch_assoc();
$_SESSION["id"] = $row["id"];
$_SESSION["name"] = $row["name"];
echo "<script>location.href='index.php';</script>";
} else {
echo "<script>alert('아이디 혹은 패스워드가 틀렸습니다.');location.href='index.php?page=login';</script>";
exit();
}
}
패스워드는 MD5 해시함수가 적용되어 있어 따로 검증이 필요없어 보인다.
$id 입력값에 대해서 시큐어코딩을 적용한다.
코드의 위치에 따라 달라지는데 $query 뒤에 코드를 작성하게 되면 $id 값 내에 싱글쿼터가 존재한다면, 해당 구문을 종료시키는 로직으로 코드를 작성해야 한다.
$query 이전에 코드를 작성하게 되면 $id 값 내에 싱글쿼터가 존재한다면 싱글쿼터를 이스케이프 처리(싱글쿼터 기능이 아닌 단순 문자열로 인식하도록 처리)를 해야한다.
$db_conn → DB 객체 생성
real_escape_string → 문자열 이스케이프 처리
$id = $db_conn->realescape_string($id); 를 추가한다.
< login.php 의 Action 페이지 - $id 에 시큐어코딩 적용 >
<?
$db_conn = mysql_conn();
if(!empty($_SESSION["id"])) {
echo "<script>location.href='index.php';</script>";
exit();
}
$id = $_POST["id"];
$password = $_POST["password"];
if(!empty($id) && !empty($password)) {
$id = $db_conn->real_escape_string($id);
$password = md5($password);
$query = "select * from members where id='{$id}' and password='{$password}'";
$result = $db_conn->query($query);
$num = $result->num_rows;
if($num != 0) {
$row = $result->fetch_assoc();
$_SESSION["id"] = $row["id"];
$_SESSION["name"] = $row["name"];
echo "<script>location.href='index.php';</script>";
} else {
echo "<script>alert('아이디 혹은 패스워드가 틀렸습니다.');location.href='index.php?page=login';</script>";
exit();
}
}
?>
시큐어 코딩 전에는 로그인 시 admin'# 입력 후 임의의 비밀번호를 입력하면 admin 계정으로 로그인이 가능했다.
로그인 시 서버에서 DB로 아래와 같은 SQL 구문을 요청할 것으로 예상된다.
select * from board where id='admin'#' and password=1234
시큐어 코딩이 적용된 후 싱글쿼터 ' 가 더 이상 기능을 하지 못하고 단순 문자열로 인식되므로 admin 계정에 대한 무단 로그인이 실패하게 된다.
싱글쿼터가 SQL 구문으로 삽입이 불가하므로 login.php 에 큐어 코딩이 완료되었다.
MySQL 에서 문자형일때는 \' 로 이스케이프 처리를 한다.
MSSQL, ORACLE 에서 문자형일때는 '' 더블쿼터로 이스케이프 처리를 한다.
MySQL → \'
MSSQL, ORACLE → ''
join 페이지도 취약하므로 시큐어코딩을 적용해본다.
< join.php 의 Action 페이지 >
<?
$db_conn = mysql_conn();
$id = $_POST["id"];
$password1 = $_POST["password1"];
$password2 = $_POST["password2"];
$name = $_POST["name"];
$email = $_POST["email"];
$company = $_POST["company"];
if(!empty($id) && !empty($password1) && !empty($password2) && !empty($name) && !empty($email)) {
if($password1 != $password2) {
echo "<script>alert('패스워드가 일치하지 않습니다.');history.back(-1);</script>";
exit();
}
$password = md5($password1);
$query = "insert into members(id, password, name, email, company) values('{$id}', '{$password}', '{$name}', '{$email}', '{$company}')";
$result = $db_conn->query($query);
echo "<script>alert('회원가입이 완료되었습니다.');location.href='index.php?page=login';</script>";
exit();
}
?>
$id, $name, $email, $company 변수가 아무런 검증 로직이 없는 것으로 보아 모두 취약하다는 것을 알 수 있다.
$password는 MD5 해시함수가 적용되어 있어 안전하다.
real_escape_string 을 각 변수에 적용해준다.
$db_conn → DB 객체 생성
real_escape_string → 문자열 이스케이프 처리
$id = $db_conn->real_escape_string($id);
$name = $db_conn->real_escape_string($name);
$email = $db_conn->real_escape_string($email);
$company = $db_conn->real_escape_string($company);
< join.php 의 Action 페이지 - 시큐어코딩 적용 >
<?
$db_conn = mysql_conn();
$id = $_POST["id"];
$password1 = $_POST["password1"];
$password2 = $_POST["password2"];
$name = $_POST["name"];
$email = $_POST["email"];
$company = $_POST["company"];
if(!empty($id) && !empty($password1) && !empty($password2) && !empty($name) && !empty($email)) {
if($password1 != $password2) {
echo "<script>alert('패스워드가 일치하지 않습니다.');history.back(-1);</script>";
exit();
}
$id = $db_conn->real_escape_string($id);
$name = $db_conn->real_escape_string($name);
$email = $db_conn->real_escape_string($email);
$company = $db_conn->real_escape_string($company);
$password = md5($password1);
$query = "insert into members(id, password, name, email, company) values('{$id}', '{$password}', '{$name}', '{$email}', '{$company}')";
$result = $db_conn->query($query);
echo "<script>alert('회원가입이 완료되었습니다.');location.href='index.php?page=login';</script>";
exit();
}
?>
시큐어 코딩 적용 후 join 페이지에서 회원가입을 시도해본다.
정상적으로 회원가입이 되는 것을 확인할 수 있다.
로그인 페이지와 회원가입(join) 페이지는 SQL Injection에 안전한 시큐어 코딩 적용이 완료되었다.
mypage 도 시큐어 코딩 적용을 해야한다.
id 부분에 te' 'st1 를 입력하니 그대로 test1 사용자의 mypage가 확인된다.
MySQL 에서 공백은 연결연산자로 서버에서 DB로 아래와 같은 SQL 구문 요청을 할 것을 예상된다.
select * from mypage where id='te' 'st1'
< mypage.php 의 Action 페이지 >
<?
include_once("./common.php");
$db_conn = mysql_conn();
$id = $_GET["id"];
$gubun = $_POST["gubun"];
if($gubun == "action") {
$name = $db_conn->real_escape_string($_POST["name"]);
$email = $db_conn->real_escape_string($_POST["email"]);
$company = $db_conn->real_escape_string($_POST["company"]);
$password = $_POST["password"];
if(!empty($password)) {
$password = md5($password);
$query = "update members set name='{$name}', email='{$email}', company='{$company}', password='{$password}' where id='{$id}'";
$result = $db_conn->query($query);
} else {
$query = "update members set name='{$name}', email='{$email}', company='{$company}' where id='{$id}'";
$result = $db_conn->query($query);
}
echo "<script>alert('회원정보 수정완료');</script>";
}
$query = "select * from members where id='{$id}'";
$result = $db_conn->query($query);
$num = $result->num_rows;
?>
$id 변수에 real_escape_string 을 추가해준다.
< mypage.php 의 Action 페이지 - 시큐어코딩 적용 >
<?
include_once("./common.php");
$db_conn = mysql_conn();
$id = $db_conn->real_escape_string($_GET["id"]);
$gubun = $_POST["gubun"];
if($gubun == "action") {
$name = $db_conn->real_escape_string($_POST["name"]);
$email = $db_conn->real_escape_string($_POST["email"]);
$company = $db_conn->real_escape_string($_POST["company"]);
$password = $_POST["password"];
if(!empty($password)) {
$password = md5($password);
$query = "update members set name='{$name}', email='{$email}', company='{$company}', password='{$password}' where id='{$id}'";
$result = $db_conn->query($query);
} else {
$query = "update members set name='{$name}', email='{$email}', company='{$company}' where id='{$id}'";
$result = $db_conn->query($query);
}
echo "<script>alert('회원정보 수정완료');</script>";
}
$query = "select * from members where id='{$id}'";
$result = $db_conn->query($query);
$num = $result->num_rows;
?>
id에 te' 'st1 를 입력하니 이전과는 다르게 존재하지 않는 사용자로 확인된다.
싱글쿼터가 이스케이프 처리되어 순수 문자열로 인식되었기 때문에 te' 'st1 사용자를 찾게되는 것이다.
te' 'st1 사용자는 존재하지 않으므로 아래와 같이 결과를 확인할 수 있다.
게시판 검색 기능도 시큐어 코딩을 적용해야 한다.
test 게시글이 작성된 상태에서 te' 'st를 검색하면 test 게시글이 검색된다.
→ 싱글쿼터가 SQL 구문으로 인식되어 검색됨, SQL Injection 취약점이 존재함
te'a'st를 검색하면 에러가 발생하여 아무 게시글도 검색되지 않는다.
서버에서 DB로 SQL 질의는 아래와 같이 검색하는 것으로 추정된다.
select * from board where title like '%te' 'st%';
실제 MySQL에서 해당 게시글을 찾을 때 아래와 같이 검색된다.
select * from insecure_board where title like '%te' 'st'%;
select * from insecure_board where title like '%te'a'st%';
시큐어 코딩 적용을 위해 list.php 파일을 연다.
< list.php 의 Action 페이지 >
<?
@include_once("./common.php");
$db_conn = mysql_conn();
$page = $_SERVER['REQUEST_URI'];
# Search Logic
$search_type = $_POST["search_type"];
$keyword = $_POST["keyword"];
if(empty($search_type) && empty($keyword)) {
$query = "select * from {$tb_name}";
} else {
if($search_type == "all") {
$query = "select * from {$tb_name} where title like '%{$keyword}%' or writer like '%{$keyword}%' or content like '%{$keyword}%'";
} else {
$query = "select * from {$tb_name} where {$search_type} like '%{$keyword}%'";
}
}
# Sort Logic
$sort = $_GET["sort"];
$sort_column = $_GET["sort_column"];
if(empty($sort_column) && empty($sort)) {
$query .= " order by idx desc";
} else {
$query .= " order by {$sort_column} {$sort}";
}
$result = $db_conn->query($query);
$num = $result->num_rows;
?>
SQL Injection 실습 할때는 keyword 부분만 실습을 했었고, search_type 과 sort_column 부분은 실습을 진행하지 않았다.
하지만 다른 부분들도 SQL Injection 취약점이 존재하므로 시큐어 코딩 적용을 해야한다.
keyword 부분은 사용자 입력값을 문자 형태로 받기 때문에 이스케이프 처리를 하면 된다.
→ real_escape_string
< list.php 의 Action 페이지 - $Keyword 이스케이프 처리 >
<?
@include_once("./common.php");
$db_conn = mysql_conn();
$page = $_SERVER['REQUEST_URI'];
# Search Logic
$search_type = $_POST["search_type"];
$keyword = $db_conn->real_escape_string($_POST["keyword"]);
if(empty($search_type) && empty($keyword)) {
$query = "select * from {$tb_name}";
} else {
if($search_type == "all") {
$query = "select * from {$tb_name} where title like '%{$keyword}%' or writer like '%{$keyword}%' or content like '%{$keyword}%'";
} else {
$query = "select * from {$tb_name} where {$search_type} like '%{$keyword}%'";
}
}
# Sort Logic
$sort = $_GET["sort"];
$sort_column = $_GET["sort_column"];
if(empty($sort_column) && empty($sort)) {
$query .= " order by idx desc";
} else {
$query .= " order by {$sort_column} {$sort}";
}
$result = $db_conn->query($query);
$num = $result->num_rows;
?>
이스케이프 처리 후 다시 te' 'st 로 검색하니 검색결과가 나오지 않았다.
real_escape_string을 사용하니 싱글쿼터 ' 를 순수 문자열로 인식하여 검색되지 않은것이다. ( te' 'st 라는 게시글을 검색 )
$search_type 과 $sort_column 을 시큐어 코딩 적용을 해본다.
위 두 부분에 사용자 입력값이 들어오는 경우, 특수문자나 공백 등 허용되지 않은 문자들이 입력되지 않도록 검증해야 한다.
혀용한 문자만 입력받게끔 정규표현식을 사용해서 코드를 작성하면 된다.
PHP 는 preg_match를 사용하고, JAVA는 Pattern.matches 를 사용하면 된다.
empty를 사용하여 빈값이 입력될 수 있는 조건을 추가한다.
코드 작성 시 주의할 점은 꼭 exit 를 해줘야 해당 입력값 검증이 가능하다는 점이다.
< list.php 의 Action 페이지 - $search_type, $sort_column 정규표현식 패턴 적용 >
<?
@include_once("./common.php");
$db_conn = mysql_conn();
$page = $_SERVER['REQUEST_URI'];
# Search Logic
$search_type = $_POST["search_type"];
$keyword = $db_conn->real_escape_string($_POST["keyword"]);
# Sort Logic
$sort = $_GET["sort"];
$sort_column = $_GET["sort_column"];
if((!preg_match("/^[a-zA-Z]*$/", $search_type) && !empty($search_type)) || (!preg_match("/^[a-zA-Z]*$/", $sort_column) && !empty($sort_column))){
echo "<script>alert('허용된 문자가 아닙니다.');history.back(-1);</script>";
exit();
}
if(empty($search_type) && empty($keyword)) {
$query = "select * from {$tb_name}";
} else {
if($search_type == "all") {
$query = "select * from {$tb_name} where title like '%{$keyword}%' or writer like '%{$keyword}%' or content like '%{$keyword}%'";
} else {
$query = "select * from {$tb_name} where {$search_type} like '%{$keyword}%'";
}
}
if(empty($sort_column) && empty($sort)) {
$query .= " order by idx desc";
} else {
$query .= " order by {$sort_column} {$sort}";
}
$result = $db_conn->query($query);
$num = $result->num_rows;
?>
시큐어 코딩 적용 후 sort_column에 공백과 괄호가 들어가니 허용된 문자가 아니라는에러 메시지가 확인된다.
이를 통해 혀용된 영어 알파벳만 사용이 가능한 것을 알 수 있다.
버스스위트를 실행해서 공백이 입력되는지 확인해본다.
버프스위트 인터셉트 On 후 title 을 기준으로 test 를 검색해본다.
공백은 URL 인코딩 시 + 를 입력되므로, 공백을 입력하고 Forward 후 인터셉트를 Off 하면, 허용된 문자열이 아니라는 에러가 확인된다.
이로써 $sort_column 은 시큐어 코딩이 적용되었다고 볼 수 있다.
검증없이 사용자 입력값이 $sort 에 들어가므로 SQL Injection 에 취약하므로 시큐어 코딩을 적용해줘야 한다.
게시판은 기본값으로 내림차순(DESC) 정렬이 되어 있다.
sort는 오름차순과 내림차순으로 데이터를 정렬해주는 기능을 하기 때문에, ASC와 DESC를 비교만하고 Server-Side에서 하드코딩 시켜주면 된다.
< list.php 의 Action 페이지 - $sort 시큐어코딩 적용>
<?
@include_once("./common.php");
$db_conn = mysql_conn();
$page = $_SERVER['REQUEST_URI'];
# Search Logic
$search_type = $_POST["search_type"];
$keyword = $db_conn->real_escape_string($_POST["keyword"]);
# Sort Logic
$sort = $_GET["sort"];
$sort_column = $_GET["sort_column"];
if((!preg_match("/^[a-zA-Z]*$/", $search_type) && !empty($search_type)) || (!preg_match("/^[a-zA-Z]*$/", $sort_column) && !empty($sort_column))){
echo "<script>alert('허용된 문자가 아닙니다.');history.back(-1);</script>";
exit();
}
if(empty($search_type) && empty($keyword)) {
$query = "select * from {$tb_name}";
} else {
if($search_type == "all") {
$query = "select * from {$tb_name} where title like '%{$keyword}%' or writer like '%{$keyword}%' or content like '%{$keyword}%'";
} else {
$query = "select * from {$tb_name} where {$search_type} like '%{$keyword}%'";
}
}
if(empty($sort_column) && empty($sort)) {
$query .= " order by idx desc";
} else {
$sort = strtoupper($sort);
if($sort == "ASC") {
$query .= " order by {$sort_column} ASC";
} else {
$query .= " order by {$sort_column} DESC";
}
}
$result = $db_conn->query($query);
$num = $result->num_rows;
?>
이로써 $sort 는 SQL Injection 에 안전하다고 볼 수 있다.
list.php 소스코드 확인 시 $tb_name은 사용자 입력값을 받아서 테이블을 구성하는 상태가 아니고, common.php 에서 불러오는 상태이기 때문에 SQL Injection에 안전하다고 볼 수 있다.
DB 컬럼명의 길이를 어느정도 알기 때문에 컬럼명의 길이값을 제한하는 검증 코드를 추가한다.
MySQL 접속하여 insecure_board 테이블 구조를 확인해본다.
컬럼명을 보니 10글자를 넘지 않는 것이 확인된다.
이를 통해 컬럼명 길이를 제한하는 검증 코드를 추가한다.
strlen → 문자열 길이값을 반환하는 함수
< list.php 의 Action 페이지 - 컬럼명 길이값 검증 코드 추가>
<?
@include_once("./common.php");
$db_conn = mysql_conn();
$page = $_SERVER['REQUEST_URI'];
# Search Logic
$search_type = $_POST["search_type"];
$keyword = $db_conn->real_escape_string($_POST["keyword"]);
# Sort Logic
$sort = $_GET["sort"];
$sort_column = $_GET["sort_column"];
if(strlen($search_type) > 10 || strlen($sort_column) > 10 ) {
echo "<script>alert('허용된 길이를 초과하였습니다.');history.back(-1);</script>";
exit();
}
if((!preg_match("/^[a-zA-Z]*$/", $search_type) && !empty($search_type)) || (!preg_match("/^[a-zA-Z]*$/", $sort_column) && !empty($sort_column))){
echo "<script>alert('허용된 문자가 아닙니다.');history.back(-1);</script>";
exit();
}
if(empty($search_type) && empty($keyword)) {
$query = "select * from {$tb_name}";
} else {
if($search_type == "all") {
$query = "select * from {$tb_name} where title like '%{$keyword}%' or writer like '%{$keyword}%' or content like '%{$keyword}%'";
} else {
$query = "select * from {$tb_name} where {$search_type} like '%{$keyword}%'";
}
}
if(empty($sort_column) && empty($sort)) {
$query .= " order by idx desc";
} else {
$sort = strtoupper($sort);
if($sort == "ASC") {
$query .= " order by {$sort_column} ASC";
} else {
$query .= " order by {$sort_column} DESC";
}
}
$result = $db_conn->query($query);
$num = $result->num_rows;
?>
sort_column 부분에 특정 문자열을 여러번 입력하니 10글자를 초과하게되어 다음과 같은 에러메시지가 발생한다.
게시판 상세 보기 페이지에도 SQL Injection이 취약한 부분이 있으므로 시큐어 코딩을 적용해야 한다.
view.php 소스코드를 확인한다.
< view.php 의 Action 페이지 >
<?
$db_conn = mysql_conn();
$idx = $_REQUEST["idx"];
$password = $_POST["password"];
if(empty($password)) {
$query = "select * from {$tb_name} where idx={$idx} and secret='n'";
} else {
$query = "select * from {$tb_name} where idx={$idx} and password='{$password}'";
}
$result = $db_conn->query($query);
$num = $result->num_rows;
?>
현재 $idx와 $password 부분에는 입력값 검증이 되어 있지 않아 SQL Injection이 취약하다.
$password는 문자형이므로 이스케이프 처리를 한다. → real_escape_string
$idx는 숫자형이므로 is_numeric 함수를 사용하여 검증을 한다. → 숫자가 아닐 경우 에러메시지 출력
is_numeric → 주어진 값이 숫자 또는 숫자 문자열인지 확인하는 함수, 주어진 값이 숫자면 True 아니면 False 반환
< view.php 의 Action 페이지 - 시큐어코딩 적용 >
<?
$db_conn = mysql_conn();
$idx = $_REQUEST["idx"];
$password = $db_conn->real_escape_string($_POST["password"]);
if(!is_numeric($idx)) {
echo "<script>alert('숫자 값만 가능합니다.');history.back(-1);</script>";
exit();
}
if(empty($password)) {
$query = "select * from {$tb_name} where idx={$idx} and secret='n'";
} else {
$query = "select * from {$tb_name} where idx={$idx} and password='{$password}'";
}
$result = $db_conn->query($query);
$num = $result->num_rows;
?>
수정 페이지 또한 idx 가 SQL Injection 에 취약하므로 시큐어 코딩을 적용해야 한다.
modify.php 를 열고 소스코드를 확인한다.
< modify.php 의 Action 페이지 >
<?
include_once("./common.php");
$db_conn = mysql_conn();
$idx = $_GET["idx"];
$query = "select * from {$tb_name} where idx={$idx}";
$result = $db_conn->query($query);
$num = $result->num_rows;
?>
$idx 에 입력 검증 로직을 추가하여 SQL Injection에 안전한 코드로 만든다.
$idx 에 숫자만 입력할 수 있도록 is_numeric 함수를 사용하여 검증한다.
< modify.php 의 Action 페이지 - 시큐어코딩 적용 >
<?
include_once("./common.php");
$db_conn = mysql_conn();
$idx = $_GET["idx"];
if(!is_numeric($idx)) {
echo "<script>alert('숫자 값만 가능합니다.');history.back(-1);</script>";
exit();
}
$query = "select * from {$tb_name} where idx={$idx}";
$result = $db_conn->query($query);
$num = $result->num_rows;
?>
수정 페이지의 $idx에는 숫자값 이외의 값은 입력이 불가하므로 SQL Injection에 안전하게 되었다.
마지막으로 action 페이지 부분에 시큐어 코딩을 적용한다.
aciton 페이지는 취약한 부분이 많은 페이지이다.
< action.php 소스코드 >
<?
@session_start();
header("Content-Type: text/html; charset=UTF-8");
include ( './common.php' );
$mode = $_REQUEST["mode"];
$db_conn = mysql_conn();
if($mode == "write") {
$title = $_POST["title"];
$id = $_SESSION["id"];
$writer = $_SESSION["name"];
$password = $_POST["password"];
$content = $_POST["content"];
$secret = $_POST["secret"];
$uploadFile = "";
if(empty($title) || empty($password) || empty($content)) {
echo "<script>alert('빈칸이 존재합니다.');history.back(-1);</script>";
exit();
}
if(!empty($_FILES["userfile"]["name"])) {
$uploadFile = $_FILES["userfile"]["name"];
$uploadPath = "{$upload_path}/{$uploadFile}";
if(!(@move_uploaded_file($_FILES['userfile']['tmp_name'], $uploadPath))) {
echo("<script>alert('파일 업로드를 실패 하셨습니다.');history.back(-1);</script>");
exit;
}
}
if($secret == "on") {
$secret = "y";
} else {
$secret = "n";
}
$content = str_replace("\r\n", "<br>", $content);
$query = "insert into {$tb_name}(title, id, writer, password, content, file, secret, regdate) values('{$title}', '{$id}', '{$writer}', '{$password}', '{$content}', '{$uploadFile}', '{$secret}', now())";
$db_conn->query($query);
} else if($mode == "modify") {
$idx = $_POST["idx"];
$title = $_POST["title"];
$password = $_POST["password"];
$content = $_POST["content"];
$secret = $_POST["secret"];
$uploadFile = $_POST["oldfile"];
if(empty($idx) || empty($title) || empty($password) || empty($content)) {
echo "<script>alert('빈칸이 존재합니다.');history.back(-1);</script>";
exit();
}
# Password Check Logic
$query = "select * from {$tb_name} where idx={$idx} and password='{$password}'";
$result = $db_conn->query($query);
$num = $result->num_rows;
if($num == 0) {
echo "<script>alert('패스워드가 일치하지 않습니다.');history.back(-1);</script>";
exit();
}
if(!empty($_FILES["userfile"]["name"])) {
$uploadFile = $_FILES["userfile"]["name"];
$uploadPath = "{$upload_path}/{$uploadFile}";
if(!(@move_uploaded_file($_FILES['userfile']['tmp_name'], $uploadPath))) {
echo("<script>alert('파일 업로드를 실패 하셨습니다.');history.back(-1);</script>");
exit;
}
}
if($secret == "on") {
$secret = "y";
} else {
$secret = "n";
}
$content = str_replace("\r\n", "<br>", $content);
$query = "update {$tb_name} set title='{$title}', content='{$content}', file='{$uploadFile}', secret='{$secret}', regdate=now() where idx={$idx}";
$db_conn->query($query);
} else if($mode == "delete") {
$idx = $_POST["idx"];
$password = $_POST["password"];
# Password Check Logic
$query = "select * from {$tb_name} where idx={$idx} and password='{$password}'";
$result = $db_conn->query($query);
$num = $result->num_rows;
if($num == 0) {
echo "<script>alert('패스워드가 일치하지 않습니다.');history.back(-1);</script>";
exit();
}
$query = "delete from {$tb_name} where idx={$idx}";
$db_conn->query($query);
}
echo "<script>location.href='index.php';</script>";
$db_conn->close();
?>
$mode는 작성,수정,삭제를 선택하는 변수로 SQL 구문에 영향을 받지 않으므로 상관없다.
SQL 구문에 영향을 받는 파라미터를 확인해서 시큐어 코딩을 적용해야 한다.
$title, $id, $writer, $pasword, $content 등
문자열이 입력되는 변수에는 real_escape_string을 사용하여 시큐어 코딩을 적용한다.
$secret은 on 인지 확인만하는 파라미터로(비밀글 여부) 값이 Server-Side에서 대입되기 때문에 SQL Injection에 취약한 부분은 아니다.
$uploadFile은 파일이 업로드 될때 특정 파일명에 대해 이스케이프 처리를 해야한다. → real_escape_string
$idx는 숫자값을 받아오므로 is_numeric 함수를 사용하여 숫자값이 아닌 경우 에러 메시지를 출력하도록 한다.
< action.php 소스코드 - 시큐어코딩 적용 >
<?
@session_start();
header("Content-Type: text/html; charset=UTF-8");
include ( './common.php' );
$mode = $_REQUEST["mode"];
$db_conn = mysql_conn();
if($mode == "write") {
$title = $db_conn->real_escape_string($_POST["title"]);
$id = $db_conn->real_escape_string($_SESSION["id"]);
$writer = $db_conn->real_escape_string($_SESSION["name"]);
$password = $db_conn->real_escape_string($_POST["password"]);
$content =$db_conn->real_escape_string($_POST["content"]);
$secret = $_POST["secret"];
$uploadFile = "";
if(empty($title) || empty($password) || empty($content)) {
echo "<script>alert('빈칸이 존재합니다.');history.back(-1);</script>";
exit();
}
if(!empty($_FILES["userfile"]["name"])) {
$uploadFile = $_FILES["userfile"]["name"];
$uploadPath = "{$upload_path}/{$uploadFile}";
if(!(@move_uploaded_file($_FILES['userfile']['tmp_name'], $uploadPath))) {
echo("<script>alert('파일 업로드를 실패 하셨습니다.');history.back(-1);</script>");
exit;
}
}
if($secret == "on") {
$secret = "y";
} else {
$secret = "n";
}
$uploadFile = $db_conn->real_escape_string($uploadFile);
$content = str_replace("\r\n", "<br>", $content);
$query = "insert into {$tb_name}(title, id, writer, password, content, file, secret, regdate) values('{$title}', '{$id}', '{$writer}', '{$password}', '{$content}', '{$uploadFile}', '{$secret}', now())";
$db_conn->query($query);
} else if($mode == "modify") {
$idx = $_POST["idx"];
$title = $db_conn->real_escape_string($_POST["title"]);
$password = $db_conn->real_escape_string($_POST["password"]);
$content = $db_conn->real_escape_string($_POST["content"]);
$secret = $_POST["secret"];
$uploadFile = $_POST["oldfile"];
if(empty($idx) || empty($title) || empty($password) || empty($content)) {
echo "<script>alert('빈칸이 존재합니다.');history.back(-1);</script>";
exit();
}
if(!is_numeric($idx)) {
echo "<script>alert('숫자 값만 가능합니다.');history.back(-1);</script>";
exit();
}
# Password Check Logic
$query = "select * from {$tb_name} where idx={$idx} and password='{$password}'";
$result = $db_conn->query($query);
$num = $result->num_rows;
if($num == 0) {
echo "<script>alert('패스워드가 일치하지 않습니다.');history.back(-1);</script>";
exit();
}
if(!empty($_FILES["userfile"]["name"])) {
$uploadFile = $_FILES["userfile"]["name"];
$uploadPath = "{$upload_path}/{$uploadFile}";
if(!(@move_uploaded_file($_FILES['userfile']['tmp_name'], $uploadPath))) {
echo("<script>alert('파일 업로드를 실패 하셨습니다.');history.back(-1);</script>");
exit;
}
}
if($secret == "on") {
$secret = "y";
} else {
$secret = "n";
}
$content = str_replace("\r\n", "<br>", $content);
$uploadFile = $db_conn->real_escape_string($uploadFile);
$query = "update {$tb_name} set title='{$title}', content='{$content}', file='{$uploadFile}', secret='{$secret}', regdate=now() where idx={$idx}";
$db_conn->query($query);
} else if($mode == "delete") {
$idx = $_POST["idx"];
$password = $db_conn->real_escape_string($_POST["password"]);
if(!is_numeric($idx)) {
echo "<script>alert('숫자 값만 가능합니다.');history.back(-1);</script>";
exit();
}
# Password Check Logic
$query = "select * from {$tb_name} where idx={$idx} and password='{$password}'";
$result = $db_conn->query($query);
$num = $result->num_rows;
if($num == 0) {
echo "<script>alert('패스워드가 일치하지 않습니다.');history.back(-1);</script>";
exit();
}
$query = "delete from {$tb_name} where idx={$idx}";
$db_conn->query($query);
}
echo "<script>location.href='index.php';</script>";
$db_conn->close();
?>
전체 페이지에 대한 SQL Injection 시큐어 코딩 적용이 완료되었다.
참고
정규표현식 참고
'웹 해킹 > 웹 해킹 및 시큐어 코딩 기초' 카테고리의 다른 글
XXE Injection (2) | 2025.01.04 |
---|---|
OS Command Injection (2) | 2025.01.03 |
SQL Injection (1) | 2024.12.11 |
버프 스위트(Burp Suite) 설치 및 사용법 (0) | 2024.11.26 |
웹 해킹에 대한 이해 (0) | 2024.11.23 |