이번 시간은 인젝션(Injection) 이라는 주제로 진행하려고 한다. 굳이 보안의 수많은 주제 중, 인젝션을 맨 처음 이야기 하는 이유는 어찌 보면 보안에서 악성행위라고 할 수 있는 가장 기초적이면서도 본질적인 행위라고 생각하기 때문이다.
[목차]
1. 보안을 바라보는 방법
2. 보안에서의 코드 읽기
3. 인젝션(Injection) 살펴보기
4. 암복호화
5. 클라이언트 코드
6. 업로드, 다운로드
7. 설계 문제
8. API
9. 설정, 패치, 하드닝
10. 그밖의 사전식 주제들
11. 스캐너 vs 수동 테스트
12. 자동화 잡
13. 리버싱과 포렌식
14. 모니터링 문제
15. 악성코드
16. 보안과 데이터
1. 들어가면서
사실 OWASP TOP 10 이나 여러 보안가이드, 책에서는 취약점을 여러가지의 타입으로 나누어 설명하고는 있지만, 사실 현실에서는 그렇게 명확히 나눠지는 것은 아닌 것 같다. 여러가지가 복합적으로 연관되어 일어나는 경우도 있고, 때로는 특별한 취약점 타입이라고 하긴 힘든 비즈니스나 설계적 관점 등에서 일어나는 경우도 있기 때문이다. 반대로 다른 관점에서 보면 모든 취약점은 그 대상이 되는 기술을 이해한다는 가정하에서 몇가지 요소가 섞인 단순한 원리로 구성된 것 같아 보이기도 한다. 그래서 앞으로 보안을 이해하는데 중요하다고 생각되는 하나하나의 주제를 가지고 이야기를 할테지만, 때로는 다른 시간의 이야기와 겹칠 때도 있을 것 같다.
인젝션(Injection) 이란 OWASP TOP 10 – 2017 의 첫번째를 차지하는 항목으로, 보통 SQL 인젝션 이나, 커맨드 인젝션 같은 대표적인 주제로 인지가 되어진다. 인젝션을 묘사하는 이미지를 생각해 본다면, 아래의 주사를 놓는 그림과 같이 무언가 외부의 물질이 내부로 침투되는 행위라 할수 있다. 그렇게 침투된 물질이 바이러스가 몸 안의 생태계 및 면역 체계와 반응하여 나쁘거나 이상한 효과를 나타내는 것 같이, 프로그램 안의 로직에 영향을 미쳐 이상한 행동을 일으키게 하는 것이 보안에서의 인젝션이라고 보면 어떨까 싶다.
보통 인젝션이 되어 들어오는 형태는 문자열, 숫자, 바이너리 등 여러 형태이며(종종 페이로드라도 칭해지는 코드 덩어리도 들어온다), 해당 들어온 요소들에 대해서, 파서(Parser - 구문을 분석해 처리함. 파서에 대해서는 파이썬 글 웹페이지 파싱에서 설명을 했다), 또는 비슷한 의미지만 인터프리터(Interpreter - 특정 구문을 가져와 명시된 로직을 실행하는 행위)가 정상적인 데이터라고 안심하고(뭐 이부분은 프로그래머를 믿기 때문에...) 실행하는 경우라고 볼 수 있다.
SQL 문이 들어와 나쁜 문제를 일으키면 SQL 인젝션이 되는거고, 커맨드 명령어가 들어와 나쁜 문제를 일으키면 커맨드 인젝션이 되고, XML 구문이 들어와 나쁜 문제를 일으키면 XML 인젝션이 되는 어찌 보면 단순한 형태라 할수 있다.
인젝션을 명시적으로 무언가를 넣는 행위가 일어나는 경우라고 제한할 수도 있지만, 좀 더 넓은 의미로 바라보면, 우리가 컴퓨터나 모바일 게임을 할 때 세이브 파일을 조작해 능력치를 조작한거나 어려운 스테이지를 넘긴다든지, "show me the money" 같은 치트키를 넣거나, 게임이 실행되는 메모리 영역의 숫자를 추적해 체력 수치를 고정시킨다거나 하는 작업, 파이썬 시간에 설치해 봤던 피들러(fiddler) 같은 툴로 HTTP 통신을 가로채 보거나 조작한다거나, XSS, API 인자 조작, 악성코드 들이 컴퓨터에 들어와서 하는 행위 등 모든 일이 사실 모두 인젝션의 행위를 일으킨다고 할 수도 있다.
왜냐하면 해킹이나 악성코드 감염 등은 결국 무언가 정상적으로 돌아가는 시스템 안에 원하는 타입의 (나쁜) 이상을 일으키는 행위이기 때문에, 시스템에 영향을 주기 위해서 인젝션 행위는 필수적이기 때문이다. 다만 그렇게 넓게 보게 되면 이 시간에 앞에 얘기한 모든 주제들이 비슷비슷해죠 헷깔리기 때문에, 여기서는 SQL 인젝션 같은 전통적인 인젝션 영역에 한정해서 얘기를 하려고 한다. 다만 맨 앞에서 얘기했듯이, 취약점 타입이라는 것은 어찌보면 보안을 체계적이고 계단식으로 잘 설명하기 위해서 나눈 기준이라는 측면도 있다는 것을 염두에 두고, 좁은 범위로만 생각하지 않았음 하는 바램이 있다. 그래서 조금 공식적인 개념에 비해 혼란이 있더라도 광의적인 관점에서도 짚고 넘어가려 한다.
2. SQL Injection
우선 인젝션 중에서 가장 유명하고, 블로그나 책에서도 많이 다루는 SQL 인젝션에 대해서 얘기를 하려 한다. 사실 SQL 인젝션은 그만큼 유명한 인젝션 타입이기도 하지만 현대의 프로그래밍 언어 환경에서는 거의 멸종 위기에 도달한 타입같기도 하다. 왜냐하면 새로 나오는 언어들이나 기존 언어들의 업데이트 버전에서는 SQL 인젝션은 언어가 대처해야 되는 당연한 보안 위협이라고 인지하여, 디폴트로 보안 방어(Prepared Statement 나 파이썬 글 장고 파트에서 볼 수 있는 ORM 형식의 쿼리방식)를 제공하기 때문이다. 아마도 많은 개발자들은 해당 SQL 호출 함수를 쓰는 것이 SQL 인젝션을 방어하게 된다는 것을 인지도 못하고 사용할 가능성이 높다(보안이 개발프로세스에 들어간 가장 바람직한 예인것 같다).
하지만 개인적으로 생각했을 때, SQL 인젝션이란 것은 인젝션의 원리를 설명할 때 참 직관적인 샘플을 보여줄 수 있고, 또 예전 언어를 쓰는 레거시 시스템은 여전히 우리나라를 포함한 세계 곳곳에 남아 있기 때문에(마치 아직도 윈도우 XP 가 많이 남아있는 것처럼) 여전히 세계에서 가장 많이 일어나고, 보안에서 많이 언급되는 항목인 듯 싶다.
여기서는 SQL 인젝션의 예제와 원리를 보여주고, 인젝션의 방어로 많이 언급되는 스토어드 프로시저(Stored Procedure), Prepared Statement 가 나쁜 데이터가 어떻게 정화(Sanitized-원래는 사전을 찾아보면 살균의 의미에 가깝지만...)되는지를 실습하며 눈으로 확인해 보도록 하겠다. 다양한 예제를 보여주기 위해 스토어드 프로시저는 ASP, MSSQL을 이용하고 Prepared Statement 는 PHP, Mysql 로 시연하려고 한다.
먼저 ASP 와 MSSQL 설치는 파이썬 글에서 이미 설명했기 때문에 우선 MSSQL 설치와 실습용 테이블의 생성 부분은 파이썬 글 4교시 데이터 베이스 조회 부분을 참조해 설정하길 바란다. 현재 조금 바뀐 부분은 MSSQL 2017 Express 를 다운 받을 수 없고, MSSQL 2018 Express 버전을 다운받게 되는데, 기존 버전과 설치과정이 바뀐건 없으니 그대로 따라 진행 하면 된다. 설치 후 supermarket 테이블 생성까지 하면 되고, 최근에 답변을 하다 알았는데, MSSQL 2016~2018 은 윈도우 8 이상에만 설치가 되니, 구 버전을 구하지 않는 이상 윈도우 10 에만 설치할 수 있다. 현재 글도 윈도우 10 환경에서 진행 중이니 양해 부탁 드린다.
두 번째로 ASP 설정과 샘플 페이지 생성하여 확인하는 부분은 파이썬 글 18교시 Lagacy Web 부분을 참고해서 IIS를 설치하고 ASP 샘플 페이지를 실행하는 부분까지 설정해 주길 바란다. 웹을 잘 모른다면 18교시를 한번 전체적으로 읽고 오면 이해가 좀더 쉬울 듯 싶다.
2.1 IIS 추가 세팅
앞에서 세팅한 IIS(Internet Information Server) 세팅으로는 에러가 났을때 상세 내용이 보이지 않기 때문에(500 대 서버 에러만 보이게 된다. 이것도 뒤에 얘기하겠지만 보안적 디폴트 설정 때문에 그렇다). 인젝션 시도시 어떤 에러가 나는지 자세히 살펴보기 위해서 상세 에러가 나오도록 세팅을 하려한다.
먼저 앞에 했던 IIS 기본 설정에는 에러 모듈이 설치되 있지 않아서 IIS 세팅시에 사용했던 "Windows 기능 켜기/끄기" 창에서 "인터넷 정보 서비스" > "World Wide Web 서비스" > "일반적인 HTTP 기능" > "HTTP 오류" 를 체크하고, 확인 버튼을 누른다.
이후 커맨드 창에서, "inetmgr" 을 입력하여, "IIS(인터넷 정보 서비스) 관리자" 화면을 띄운 후, 왼쪽 트리에서 "Default Web Site" 를 선택하고, IIS 섹션에서 "ASP" 항목을 더블 클릭 한다.
이후 가운데에서 디버깅 속성 항목을 펼쳐서 "브라우저에 오류 전송", "클라이언트 쪽 디버깅 사용" 2가지 항목을 True 로 바꾼 후, 오른쪽의 작업 창에서 적용 버튼을 클릭한다.
다음엔 같은 식으로 "오류 페이지" 아이콘을 더블 클릭한다.
이후 오른쪽의 "기능 설정 편집..." 을 클릭 하여, "오류 페이지 설정 편집" 창이 나오면, "자세한 오류" 라디오 단추를 선택하고, 아래의 "확인" 버튼을 누른다.
이 상태에서 엣지 브라우저 같은 경우는 정상으로 에러를 볼수 있는데, IE 같은 경우는 하나를 더 해 주어야 한다. "도구(브라우저 우측 상단의 톱니 바퀴모양 아이콘)" > "인터넷 옵션" 으로 들어가 고급 탭을 클릭하고, "HTTP 오류 메시지 표시" 를 언체크 하고, 하단의 "확인" 버튼을 누른다(브라우저를 종료했다 다시 실행해야 반영된다).
2.2 테이블 추가 생성 및 ASP 쿼리 준비
기존에 파이썬 쪽에서 세팅해 놓은 상품 정보가 들어간 supermarket 테이블이 있긴 하지만, 조금이나마 더 현실에 가까운 샘플을 보이기 위해서 거래 관련 테이블을 하나 만들어 보보려 한다. SQL Server Management Studio 에서 supermarket 이 있는 mytest 디비에 아래와 같은 거래 테이블을 만든 후(파이썬 4교시 설명 같이 선택 후, F5 키를 누른다),
1
2
3
4
5
6
7
8 |
create table order_record
(
OrderNo int,
MemberId char(20),
ItemNo int,
BuyCount int,
InsDate datetime
) |
cs |
데이터를 입력한다.
1
2
3
4
5
6
7
8
9
10
11 |
insert into order_record
values (1000, 'tom' , 1, 3, '2018-01-01')
insert into order_record
values (1001, 'jerry' , 2, 1, '2018-01-02')
insert into order_record
values (1002, 'tom' , 3, 1, '2018-01-02')
insert into order_record
values (1003, 'tom' , 3, 2, '2018-01-03') |
cs |
이후 ASP 코드에서 호출 할 쿼리를 하나 만들어 실행해 확인해 본다
1
2
3
4
5 |
select o.memberid, s.Foodname, o.buycount, o.InsDate, s.price, (s.price * o.buycount) total_price from order_record o(nolock)
join supermarket s(nolock)
on s.itemno = o.itemno
where o.memberid = 'tom'
order by o.buycount desc |
cs |
2.3 인젝션이 일어나는 ASP 페이지 만들기
ASP 는 .NET 이 생기면서 유지보수가 거의 멈춰진 언어이기 때문에, 인터넷에 있는 많은 샘플이 SQL Injection 에 취약한 형태로 구현이 되어 있다(뒤에 보겠지만 계속 유지보수된 PHP 도 하위 호환성 때문에 예전 스타일 대로 코딩 하면 비슷하긴 하다). 자세한 부분은 ASP 문법이라 중요하진 않으니 대략적으로 보면, 위의 SQL 문에 대해서 구입한 사람(memberid)를 입력하면 해당 되는 회원이 구입한 물품들을 보여주는 페이지 소스는 아래와 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87 |
<%@ Language=VBScript %>
<%
' 컨넥션 스트링
strOrderList = "Provider=SQLOLEDB; Data Source=localhost; Initial Catalog=mytest; User Id=pyuser; Password=test1234"
Set objCnn = Server.CreateObject("ADODB.Connection")
objCnn.Open strOrderList
%>
<%
' 폼 값 받기
strBuyerID = request("txtBuyerID")
' 실행할 SQL + 인자
strSQL1 = "select o.memberid, s.Foodname, o.buycount, o.insdate, s.price, (s.price * o.buycount) total_price from order_record o(nolock) " & _
"join supermarket s(nolock) " & _
"on s.itemno = o.itemno " & _
"where o.memberid = '" & strBuyerID & "'" & _
" order by o.buycount desc"
Response.Write "<h3>날라가는 쿼리 표시 </h3>"
Response.Write strSQL1
Response.Write "<hr>"
%>
<html>
<head>
<title>손님 관리</title>
</head>
<body>
<form name = "BuyForm" method="get" action="injection_test.asp">
<table width = 600>
<tr>
<td class="graycolor_center_align" width = "100"> 구매자 조회</td>
<td class="content_left_align" width = "100"> <INPUT maxlength="200" name="txtBuyerID" size="15" style="font-size:12px;" type="text" value=""> </td>
<td class="content_left_align"> <input name=button type=submit value="조회"> </td>
</tr>
</table>
<br>
<%
' 쿼리 실행
Set rsList = objCnn.Execute(strSQL1)
%>
<table width="600">
<tr>
<td class="graycolor_center_align" width = "100">손님</td>
<td class="graycolor_center_align" width = "100">구매물품</td>
<td class="graycolor_center_align" width = "100">구매수량</td>
<td class="graycolor_center_align" width = "100">구매일</td>
<td class="graycolor_center_align" width = "100">단가</td>
<td class="graycolor_center_align" width = "100">총 금액</td>
</tr>
<%
'결과가 끝이 아닐 라면,
Do while Not rsList.EOF
%>
<tr>
<td class="content_center_align" width = "100"><%=rsList("memberid")%></td>
<td class="content_center_align" width = "100"><%=rsList("Foodname")%></td>
<td class="content_center_align" width = "100"><%=rsList("buycount")%></td>
<td class="content_center_align" width = "100"><%=rsList("InsDate")%></td>
<td class="content_center_align" width = "100"><%=rsList("price")%></td>
<td class="content_center_align" width = "100"><%=rsList("total_price")%></td>
</tr>
<%
rsList.MoveNext
Loop
%>
</table>
</form>
</body>
<%
'열었던 연결 닫기
objCnn.Close
Set ObjCnn=Nothing
%> |
cs |
위의 코드를 관리자 권한으로 실행한 메모장(파이썬 18교시 ASP 샘플 실행하는 부분에서 설명함)에 붙여 놓고, "c:\inetpub\wwwroot" 폴더에 "ANSI" 인코딩(한글이 안깨지려면 이렇게)으로 "injection_test.asp" 라고 저장한다.
브라우저 주소창에 "http://localhost/injection_test.asp" 라고 입력하면, 아래와 같이 원하는 구매자를 넣고, 조회를 누르면 해당 구매자가 구입한 물품이 나타나는 간단한 화면이 나오게 된다. 상단에는 인젝션이 일어나는 상황을 보여주기 위해서, 실제 데이터베이스로 날라가는 쿼리를 표시하게 했다. 상단의 쿼리를 보면 위의 ASP 에 있는 쿼리 조합 코드에 의해서 where 조건에 있는 "memberid" 에 사용자가 입력한 "tom" 이 결합 됨으로서, tom 이 구입한 물품을 가져오게 되는 것을 볼 수 있다(이 쿼리가 이해 안간다면 SQL 기초가 부족 한 거니, Select 문, Join 문, Union 문, Order by 문을 공부하길 바란다)
2.4 인젝션 시도해보기
앞에서 인젝션은 외부에서 들어온 구문을 해석하는 파서나 인터프리터의 문제라고 얘기했듯이, SQL 인젝션은 프로그램에서 던지는 SQL 문과 SQL 서버 사이의 문제이다. 하지만 여기서 순진한 SQL 서버는 보내준데로 실행해 주는 것일 뿐이니 별 잘못은 없다고 볼수 있고, 편파적이긴 하지만 ASP 프로그램이 나쁜 의도의 사용자가 입력한 내용을 잘못 해석해서 SQL 서버로 던져서 일어난다고 보는 것이 맞다. 결국 SQL 인젝션은 프로그램을 잘 속여 SQL 서버로 나쁜 쿼리를 던져 실행시키는 것이 목적이기 때문에, SQL 서버 종류별로 문법이 약간 달라서 악용되는 기법이 조금씩 틀린 부분이 있다.
그럼 모든 SQL 서버들에서 문법 문자로 사용하는 홑따옴표(')나 쌍따옴표(")가 기본적인 SQL 인젝션을 판단하기에 적절하다. 물론 블라인드 SQL 인젝션(Blind SQL Injection)이라는 한 단계 더 꼬여있는 분야도 있지만, SQL 의 미묘한 문법을 사용한다는 측면에서는 동일하다고 봐도 된다. 위의 화면에서 홑따옴표(') 하나만 넣어서 조회를 해보도록 한다.
원래 일반적인 웹 사이트는 에러가 사용자 친화적인 공통 에러 페이지("사용에 불편을 드려 죄송합니다" 같은)로 가도록 설정이 되기때문에, 위와 같은 자세한 에러 페이지를 만나기는 어렵다. 하지만 여기서는 SQL 인젝션의 원리를 설명하기 위해서 일부러 자세한 에러를 보이도록 앞에서 설정했었다. 위의 SQL 서버로 전달되는 쿼리를 보면 o.memberid =''' 식으로 입력되어, 홑따옴표가 3개가 되기 때문에, SQL 문법이 깨지게 된다. 실제 MSSQL 서버에서 반환된 에러메시지도, 따옴표의 짝이 안 맞는다는 에러가 나온다. 현실적으로 저런 경우라면 SQL 인젝션이 된다는 사실을 확인하고 여러 시도를 해볼 수 있게된다.
다음엔 입력 인자로 tom' 을 넣어본다. 여전히 쿼리를 보면 'tom'' 으로 홑 따옴표 수가 안 맞아 SQL 문법을 깨뜨리므로 동일한 에러가 나게 된다.
그 다음에는 MSSQL 에서 그 뒤에 나온 문장을 주석처리를 해주는 효과를 지닌 -- 문자를 이용해서, tom' -- 라고 넣어보자. 이번에는 -- 표시로 인해서 뒤의 ' order by o.buycount desc 문장이 모두 주석 처리되기 때문에 실제 유효한 쿼리는 ...'tom' 까지가 되어서 문법이 깨지지 않기 때문에 정상적인 쿼리가 되어 결과가 나오게 된다. 대신 주석 뒤의 order by 문장은 무시되어 정렬이 없어져 나온다. 궁금한 사람은 해당 문장을 복사해서, SSMS 에 복사해 실행해도 마찬가지 결과가 나오게 된다.
자 그러면 이제부터는 이것저것 문법을 조합하여 자유롭게 쿼리를 만들 수 있게 된다. tom' and o.buycount = 3 -- 값을 넣어서 구매 수량이 3개인 건만 가져올 수도 있다.
그러면 이 시점에서 "이게 뭐 어쨌다고?" 라고 생각할 수도 있을 것 같다. 원래 저 페이지는 해당 테이블들을 조회를 하는 쿼리였고, 인덱스를 좀 이상하게 타면 데이터베이스에 부하를 많이 줄 수 있긴 하겠지만(이건 DDOS 공격이 될수도 있겠지만), 보안적으로 정말 위험한 건가 하는 생각을 가질 수도 있다. 사실 이제부터는 case by case 라고 보면 된다. 운이 좋으면 심각한 문제가 일어나지 않을 수도 있지만(하지만 잠재적으로 큰일이 날수 있는 중요한 취약점이라 꼭 바로 수정해야 하긴 한다), 어쩌면 매우 큰일이 일어날 수도 있게된다.
우선 DB 나 보안 관련된 팀이 없는 많은 작은 회사들은, 어플리케이션에서 사용하는 데이터베이스 계정을 기본 계정인 admin 이나 DB Owner 계정으로 사용하고 있을 수 있다(권한을 세분화 하면 관리가 복잡해 지기 때문에). 추가해 해당 계정에 특정한 SQL 명령어 실행 제약이 없을 가능성도 높다(설정에 따라 SQL 문을 이용해 시스템 명령어도 실행할 수도 있다). 엎친데 덮친 격으로 해당 테이블과 같은 데이터베이스 내에 회원의 개인정보 테이블이 있을 경우도 있다(다른 데이터 베이스에 있더라도 접근을 못한다고 하긴 어렵지만 아무래도 환경과 난이도의 영향을 받게 된다).
최악의 경우를 상정하기 위해서, 개인정보 테이블을 하나 mytest 데이터베이스에 만들어 보도록 하다. SecretMember 테이블을 하나 만들고, 회원 정보를 하나 넣는다.
1
2
3
4
5
6
7 |
create table SecretMember(
MembID char(20),
TelNo char(20),
email char(30)
)
Insert SecretMember values('admin', '010-xxxx-2222', 'test@test.com') |
cs |
그럼 이제 SQL 문법 중에 인젝션에 단골로 사용되는 "union all" 이라는 문법을 써보자. 두 개의 서로 다른 테이블의 내용을 가져와서 select 문 뒤의 컬럼들의 데이터 형만 맞춰 주면, 마치 하나의 테이블에서 가져온 것처럼 결과를 반환하는 연산자이다..
[w3schools 사이트 - SQL UNION Operator]
https://www.w3schools.com/sql/sql_union.asp
그럼 tom' union all select membid + telno + email, '', 0, '2018-01-01', 1, 1 from secretmember(nolock) -- 문장을 넣고 조회를 하게 되면, 기존 프로그램에서 조회하려던 것과 전혀 상관없는 secretmember 테이블 로부터 데이터를 같이 조회해 와서, 화면에 뿌려주게 된다. 만약 해당 테이블에 개인정보가 있다면 SQL 서버는 안전하게 회사 내부에서 보호되고 있더라도, 개인정보가 모두 유출 되는 상황이 벌어질 수 있다(물론 어플리케이션 방화벽 같은 추가적인 방어 장비로 부터도 잘 피해가야 하겠지만. 아마 이런 취약점이 있는 회사는 그런 장비가 없거나 있어도 형식적으로 운영될 가능성이 높을 듯 하다).
한발 더 나아가 만약 별로 털어갈 테이블에 없어서 실망한 공격자가 아래와 같은 쿼리를 날린다고 해보면 tom' ; drop table secretmember -- 만약 해당 어플리케이션의 데이터베이스 계정 권한이 과도하게 있는 경우는 해당 테이블이 삭제되어 버리게 됩니다(";" 는 MSSQL 에서 두개의 명령어의 연결을 나타내서, 두개의 명령어가 같이 실행되어 버린다)
SSMS 에서 select top 10 * from secretmember(nolock) 를 해보면 테이블이 사라지고 없다.
2.5 SQL 인젝션 방어 전략
그럼 나름 무시무시한 SQL 인젝션을 방어하려면 어떻게 하는게 좋을까? 여러 블로그 등에도 많은 가이드가 나오지만 구글에 "owasp sql injection defense" 라고 검색하면, 아래의 OWASP 에서 가이드하는 내용이 나온다. 개인적으로 볼때 초보자를 위한 세부적 기술에 대한 설명은 부족해 아쉽지만, 내용 자체는 군더더기 없이 잘 정리되어 있다고 생각한다.
[OWASP 사이트 - SQL Injection Prevention Cheat Sheet]
https://www.owasp.org/index.php/SQL_Injection_Prevention_Cheat_Sheet#SQL_Server_Escaping
내용을 보면 순서대로 Prepared Statements(with 파라매터화된 쿼리), 스토어드 프로시저(Stored Procedures), 화이트 리스트 베이스의 입력 체크(White List Input Validation), 모든 사용자 입력의 이스케이핑 처리(Escaping All User-Supplied Input) 가 있다. 해당 페이지에도 명시됬듯이, 이스케이핑 처리는 정말 다른 방법이 없거나, 레거시 시스템이라서 수정할 범위가 넓어 사이트 이펙이 너무 커서 당장 전체를 수정하기 어려운 경우 임시적 조치로 사용하는 것이 맞는듯 싶고, 화이트 리스트 베이스로 입력 체크를 하는 것은 사실 수많은 쿼리에 대해서 경우의 수도 따져봐야 하고, 범용적인 화이트 리스트를 구축하기도 쉽지 않기 때문에 설명에 나온것처럼 2차적인 방어 요소로 생각하고 사용하면 좋을 듯 싶다. 여기서는 Prepared Statements 와 스토어드 프로시저 2개를 구현하면서 데이터를 어떻게 방어하는지 보도록 하겠다.
2.5.1 방어전략 1 - 스토어드 프로시저의 방어 살펴보기
우선 스토어드 프로시저가 무언지 알아보자. 스토어드 프로시저는 간단히 설명하면 SQL 서버내에 저장되어 있는 바깥에서 호출이 가능한 함수라고 생각하면 된다(물론 MSSQL 은 스토어드 프로시저와 별개로 진짜 함수라 불리는 문법도 있긴 하지만 개념적으로 외부에서 접근가능한 함수라고 생각하면 편할 듯 싶다). 함수와 마찬가지로 입력과 출력을 가지고 있으며, 인자가 있을 수도 없을 수도 있다. 프로그램에서 인자에 값을 넣어 스토어드 프로시저를 호출 하면, 해당 결과가 SQL 문을 실행 한 것과 마찬가지로 프로그램으로 반환된다. 실제 스토어드 프로시저 내에는 SQL 문법으로 이루어져 있다.
그럼 SSMS 에서 스토어드 프로시저를 하나 만들어 보자. 거의 SQL과 같지만, 앞에 create PROCEDURE 문이 있고 그 담에 입력인자 @memb_id 가 딸려온다. 일반적인 SQL 문의 뒤에 where 조건의 o.member_id = @memb_id 부분에서 스토어드 프로시저로 넘어온 인자를 쿼리와 합쳐 필요한 조건문을 만든다.
1
2
3
4
5
6
7
8 |
create PROCEDURE dbo.Select_Buy_Item
@memb_id char(20)
AS
select o.memberid, s.Foodname, o.buycount, o.insdate, s.price, (s.price * o.buycount) total_price from order_record o(nolock)
join supermarket s(nolock)
on s.itemno = o.itemno
where o.memberid = @memb_id
order by o.buycount desc |
cs |
이후 해당 스토어드 프로시저를 호출하는 ASP 페이지를 만들면 아래와 같다. 역시 적당히 흐름만 보자
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96 |
<%@ Language=VBScript %>
<!--METADATA TYPE= "typelib" NAME= "ADODB Type Library"
FILE="C:\Program Files\Common Files\SYSTEM\ADO\msado15.dll" -->
<%
' 컨넥션 스트링
strOrderList = "Provider=SQLOLEDB; Data Source=localhost; Initial Catalog=mytest; User Id=pyuser; Password=test1234"
Set objCnn = Server.CreateObject("ADODB.Connection")
objCnn.Open strOrderList
%>
<%
' 폼 값 받기
strBuyerID = request("txtBuyerID")
if(strBuyerID = "") then
strBuyerID = ""
end if
' 폼으로 받은 인자를 넘겨서, 스토어드 프로시저 실행
Set objCmd = Server.CreateObject("ADODB.Command")
with objCmd
.ActiveConnection = objCnn
.CommandText = "Select_Buy_Item"
.CommandType = adCmdStoredProc
.Parameters.Append .CreateParameter("@memb_id",adChar,adParamInput,20)
.Parameters("@memb_id") = strBuyerID
end with
Response.Write "<hr>"
%>
<html>
<head>
<title>손님 관리</title>
</head>
<body>
<form name = "BuyForm" method="get" action="injection_test_sp.asp">
<table width = 600>
<tr>
<td class="graycolor_center_align" width = "100"> 구매자 조회</td>
<td class="content_left_align" width = "100"> <INPUT maxlength="200" name="txtBuyerID" size="15" style="font-size:12px;" type="text" value=""> </td>
<td class="content_left_align"> <input name=button type=submit value="조회"> </td>
</tr>
</table>
<br>
<%
' 쿼리 실행
Set rsList = objCmd.execute
%>
<table width="600">
<tr>
<td class="graycolor_center_align" width = "100">손님</td>
<td class="graycolor_center_align" width = "100">구매물품</td>
<td class="graycolor_center_align" width = "100">구매수량</td>
<td class="graycolor_center_align" width = "100">구매일</td>
<td class="graycolor_center_align" width = "100">단가</td>
<td class="graycolor_center_align" width = "100">총 금액</td>
</tr>
<%
'결과가 끝이 아닐 라면,
Do while Not rsList.EOF
%>
<tr>
<td class="content_center_align" width = "100"><%=rsList("memberid")%></td>
<td class="content_center_align" width = "100"><%=rsList("Foodname")%></td>
<td class="content_center_align" width = "100"><%=rsList("buycount")%></td>
<td class="content_center_align" width = "100"><%=rsList("InsDate")%></td>
<td class="content_center_align" width = "100"><%=rsList("price")%></td>
<td class="content_center_align" width = "100"><%=rsList("total_price")%></td>
</tr>
<%
rsList.MoveNext
Loop
%>
</table>
</form>
</body>
<%
'열었던 연결 닫기
objCnn.Close
Set ObjCnn=Nothing
%> |
cs |
다른 부분은 앞의 예제와 비슷하고, "폼으로 받은 인자를 넘겨서, 스토어드 프로시저 실행" 주석 부분의 코드만 조금 다르다. 아까 SSMS 에서 만들었던 스토어드 프로시저인 Select_Buy_Item 를 호출 하면서 @memb_id 인자를 넘긴다. 그럼 c:\inetpub\wwwroot 에 ANSI 인코딩 형식으로 injection_test_sp.asp 라고 파일을 저장한다. http://localhost/injection_test_sp.asp 페이지를 로드해서 똑같이 tom' -- 을 입력해 보도록 해보자. 그럼 아무 에러도 나지 않고, 결과가 없는 화면이 나타나게 된다.
스토어드 프로시저의 효과는 프로시저를 이용해 안전하게 쿼리를 작성 한다면 Prepared Statement 하고 거의 비슷한 효과를 준다고 보는데, Prepared 라는 말은 쿼리의 실행계획을 세운 다는 의미라고 보면 될것 같다. 쿼리의 실행 계획(Query Plan)이라는 것은 비유하자면 우리가 프로그램을 컴파일하는 행위와 같다고 보면 된다. 우리가 프로그램 소스를 컴파일하고 빌드 할때 컴파일러가 가장 최적의 어셈블리코드를 설계하여 바이너리 파일로 만들어 주듯이, SQL 서버도 내부적으로는 쿼리에 최적화된 형태의 파일이 관리되는 프로그램이기 때문에, 사용자가 원하는 쿼리에 대해서, 어떤 전략을 가지고 결과를 얻기위해 실행할 것인지를 결정하는 과정이 있게 된다.
이렇게 전략이 결정되게 되면, 다음에 다시 전략을 계산하기 전에는 컴파일된 바이너리와 같이 고정된 전략을 가지게 된다. SQL 문에서 전략에 영향을 미치는 요소가 join, union 등의 연산자나 where 문에서 언급하는 컬럼이름 등의 구조에 영향을 주는 문법 요소들이기 때문에, 결국 SQL 문이 입력되는 상수들 빼고는 내부적인 문법 구조가 고정되어 버리는 효과를 가지게 된다.
그래서 바깥에서 아무리 문법 문자에 해당하는 문자열들을 내부에 인젝션 하더라도, 이미 고정된 구조는 유지한채 문법 요소에 영향을 주지 못하고, 입력되는 상수로만 취급 되게 된다. 사실 이건 최초에는 시간이 많이 걸리는 쿼리 실행 계획을 재사용 함으로서 성능적인 이점을 가지게 되는 데서 출발한것 같은데, 해당 방식이 SQL 인젝션을 근본적으로 예방 가능한 특징을 가지고 있기 때문에, SQL 인젝션의 1번째 방어 권고 요소가 된듯도 싶다(소발에 쥐 잡았다고 봐도 되려나...)
다만 스토어드 프로시저는 내부에서 받은 인수를 이용해 문자열을 합치는 식으로 조합해서(예를 들어 해당 인자를 스토어드 프로시저 내부에서 where 문장 뒤에 명시적으로 붙인다던지...) 새로운 쿼리를 만드는 방식으로도 사용할 수 있기 때문에, 내부에서 상수 형식으로 사용될 때에만 Prepared Statement 와 동일한 효과를 가지는 것 같다고 생각된다. 사실 위와 같이 쓰는 것은 스토어드 프로시저를 사용하는 의미를 잃게 하는 행위인듯도 싶다.
그럼 위의 화면의 결과만 가지고는 정말 내부에서 들어간 인자가 문법 문자가 아닌, 일반 문자열로만 취급됬는지 증명하기 힘들기 때문에, 페이지를 조금 변형하여 입력한 문자를 테이블안에 넣어 실제 어떤 형식으로 들어가는지 확인해 보도록 하겠다(개인적으로 첨에 정말 그런지 궁금해서 실험을 위해 해본 행동이다). 먼저 escape_test 라는 테이블을 하나 만들고 hey, hey', hey'' 3개의 문자를 입력해 보도록 하겠다. 여기서는 처음에 create table 을 먼저 선택해 실행 하고, 그 뒤에 insert 문을 하나씩 선택해서 실행해 본다. 실행 하다보면 2번째 insert 문은 hey 뒤에 홑따옴표가 두개 있어('') 에러가 나고, 3번째 insert 문은 세개의 홑 따옴표중 앞의 2개가 ' 문자를 나타내는 이스케이프 문자로 처리되어 hey' 문자가 입력이 된다.
1
2
3
4
5
6
7
8
9
10 |
create table escape_test
(
mytext char(30)
)
insert into escape_test values('hey')
insert into escape_test values('hey'')
insert into escape_test values('hey''') |
cs |
실제 들어간 데이터를 셀렉트 해보면 확인을 할 수 있다.
1 |
select * from escape_test(nolock) |
cs |
그럼 해당 테이블에 인자를 받아서 insert 하는 스토어드 프로시저를 하나 만들어 보겠다
1
2
3
4
5 |
create PROCEDURE dbo.insert_escape_text
@mytext char(30)
AS
insert into escape_test
values (@mytext) |
cs |
이후 해당 스토어드 프로시저를 호출하는 ASP페이지를 하나 만든다. 앞의 페이지와 거의 동일하고, 필요없는 코드 제거와, 스토어드 프로시저 호출 부분만 조금 바뀌었다. 역시 문법은 대충 보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62 |
<%@ Language=VBScript %>
<!--METADATA TYPE= "typelib" NAME= "ADODB Type Library"
FILE="C:\Program Files\Common Files\SYSTEM\ADO\msado15.dll" -->
<%
' 컨넥션 스트링
strOrderList = "Provider=SQLOLEDB; Data Source=localhost; Initial Catalog=mytest; User Id=pyuser; Password=test1234"
Set objCnn = Server.CreateObject("ADODB.Connection")
objCnn.Open strOrderList
%>
<%
' 폼 값 받기
strBuyerID = request("txtBuyerID")
if(strBuyerID = "") then
strBuyerID = ""
end if
Set objCmd = Server.CreateObject("ADODB.Command")
with objCmd
.ActiveConnection = objCnn
.CommandText = "insert_escape_text"
.CommandType = adCmdStoredProc
.Parameters.Append .CreateParameter("@mytext",adChar,adParamInput,20)
.Parameters("@mytext") = strBuyerID
end with
Response.Write "<hr>"
%>
<html>
<head>
<title>손님 관리</title>
</head>
<body>
<form name = "BuyForm" method="get" action="injection_test_insert.asp">
<table width = 600>
<tr>
<td class="graycolor_center_align" width = "100"> 구매자 입력</td>
<td class="content_left_align" width = "100"> <INPUT maxlength="200" name="txtBuyerID" size="15" style="font-size:12px;" type="text" value=""> </td>
<td class="content_left_align"> <input name=button type=submit value="입력"> </td>
</tr>
</table>
<br>
<%
' 쿼리 실행
Set rsList = objCmd.execute
'열었던 연결 닫기
objCnn.Close
Set ObjCnn=Nothing
%> |
cs |
관리자 권한으로 연 메모장에 내용을 붙여 넣고, c:\inetpub\wwwroot 폴더에 ANSI 인코딩으로 injection_test_insert.asp 이름으로 저장한다. tom' -- 를 넣고 입력 버튼을 누른다. 이후 SSMS 에서 escape_test 테이블을 조회하여 어떻게 내용이 들어갔나 확인해 본다.
실제로 들어간 내용을 보면 우리가 입력한 tom' -- 값이 insert 문법을 깨뜨리지 않고 문자열로 들어가(아마 입력한 그대로 홑따옴표가 들어간거 보면 내부 전달과정에서 이스케이프 처리도 같이 해주는 것으로 추측된다) 있는 것을 볼 수 있다. 이 과정을 통해 고정된 실행계획을 가지도록 정적 쿼리로 설계된 스토어드 프로시저가 Prepared Statements 비슷하게 입력한 문자열을 문법 요소가 아닌 상수로 취급하다는 것을 간접적으로 살펴볼수 있지 않았나 싶다.
2.5.2 방어전략 2 - Prepared Statements 방어 살펴보기
ASP 도 preapared statements 를 지원하는 것은 같지만 나중에도 사용하게 될 php 와 mysql을 가지고 해당 부분을 살펴보려고 한다. 그렇게 하려니 우선 mysql 와 php 를 설치해야되는 인형눈 끼는 작업이 필요하다. 해본적이 없는 사람들은 한번쯤 해볼만한 귀찮은 작업이다.
2.5.2.1 MySQL 설치
우선 mysql 먼저 설치한다. mysql 은 앞에 설치한 MSSQL 과 과정이 비슷하다.
MySQL 도 MSSQL처럼 서비스 기반의 데이터베이스이기 때문에 설치를 해야 한다. 구글에서 “mysql community windows installer “ 라고 검색을 한다(버전업이 참 빨리 되는 듯 하다)
[Download MySQL Installer – MySQL 공식 페이지]
https://dev.mysql.com/downloads/installer/
페이지 하단의 다운로드 경로에서 370메가 정도 되는 풀 설치 버전을 다운로드 하면, 아래와 같이 오라클 회원에 가입하라는 버튼이 보인다. 굳이 회원 가입까지는 필요 없고, 아래의 작은 링크인, “No thanks, just start my download” 를 클릭하면 로그인 없이 다운받을 수 있다.
다운 받을 파일을 실행하여 설치로 들어간다. 우선 라이선스에 동의 체크박스를 체크 후, “Next” 버튼을 누른다.
이후 설치 타입으로 “Developer Default” 라디오 박스를 선택 하고 “Next” 버튼을 누른다.
필수 프로그램을 체크하는 Check Requirement 항목에서는 그냥 “Next” 버튼을 누른다. 계속 진행 하겠냐고 묻는 창이 뜨면, “Yes”를 클릭한다.
설치될 프로그램 리스트 들이 나타나면, “Execute” 버튼을 클릭한다. 조금 시간이 걸리면서 각 요소들이 설치되게 되고, 설치가 완료 되면, “Execute” 버튼이 사라지며 “Next” 버튼으로 바뀌어 표시되면 클릭한다.
이제 세팅 화면이 시작 된다. 리눅스로 따지면 config 파일 수정하고 하는 작업 들이다. Product Configuration 창에서 “Next” 버튼을 누른다.
네트워킹 타입을 설정하는 창이며, 테스트 용도로 하나의 서버로 운영할 예정이므로 기본 값인 “standalone” 으로 두고, “Next” 버튼을 누른다.
다음 화면에서도 MySQL 디폴트 포트인 3306 상태로 그대로 “Next “버튼을 누른다.
하위 버전에는 없던 패스워드를 SHA256으로 저장하겠냐는 화면이 나오는데, 2018년 6월 현재 PHP 의 mysqli 모듈을 사용하여 쿼리를 날릴시, 위의 Sha256 옵션을 선택하면 에러가 난다(PDO 방식은 괜찮다는 얘기가 있지만, 지금 다시 해당 스타일로 소스를 고치기도 번거롭기도 해서 예전 옵션으로 가려고 한다^^). 아래의 "Use Lagacy ..." 선택하고 "Next" 버튼을 누른다(많이 쓰는 모듈이니 아마 한 두달 후쯤엔 지원하게 되지 않을까 싶다.).
이후 계정을 설정하는 화면에서 루트 계정의 패스워드를 설정하는 “MySQL Root Password”, “Repeat Password” 항목에 “test1234” 라고 적는다. 해당 값을 입력하면 패스워드 강도가 낮다고 나오는데 물론 실제 운영 시에는 충분히 복잡한 패스워드로 설정해야 한다. 오른쪽 하단의 “Add User” 버튼을 클릭 후, 사용자 등록 창이 나오면 Username 에 “pyuser”, Password 와 Confirm Password 에 “test1234” 를 동일하게 넣는다. 역할은 데이터베이스 관리자(“DB Admin”)이고, 모든 곳에서 연결할 수 있게 한다(“All Hosts”).. 이후 “OK” 버튼을 눌러 유저를 등록 시키고, “Next” 버튼을 누른다.
계속 실행 하기 위해서 윈도우 서비스로 등록하고, 부팅 시 시작하도록 옵션이 되어 있다. 웹 서버의 실행 계정은 기본 시스템 계정으로 돌아가게 되고, 그냥 “Next” 버튼을 누른다.
플러그인이나 확장 패키지는 설치할 필요가 없으므로, “Next” 를 누른다.
“Execute” 버튼을 눌러서 설정한 내용들을 적용 시킨다. 이후 버튼이 “Finish” 로 바뀌면 클릭 한다.
라우터 설정이 시작된다(길긴 길다 --;). ‘Next’ 버튼을 누른다.
라우팅 설정은 현재는 상관없으므로, “Finish” 버튼을 누른다.
연결 상태를 체크하는 최종 단계가 시작된다. “Next” 버튼을 누른다.
아까 입력한 root 패스워드인 “test1234” 를 넣고, “Check” 버튼을 클릭한다. 이후 잘 연결이 되면 “Next” 버튼이 활성화 된다. “Next” 버튼을 누른다.
최종으로 “Execute” 버튼을 누르고, 모든 설정이 적용된 후 “Finish” 버튼이 나오면 클릭한다.
완료 단계로, “Next” 버튼을 누른다.
쉘하고 워크벤치 실행이 체크된 상태에서 "Finish" 버튼을 누른다.
2.5.2.2 MySQL 테이블 및 데이터 설정
MySQL 을 관리할 수 있는 쉘 창과, 워크벤치(MSSQL SSMS 비슷한 클라이언트 이다)가 완료 후 실행 된다. 예전에는 HeidiSQL 같은 외부 클라이언트를 사용했는데, 새롭게 생긴 워크벤치도 괜찮아 보이니, 여기서는 워크벤치를 이용해서 데이터베이스 및 테이블을 설정하도록 하겠다.
설치 완료 후 실행된 MySQL워크벤치에서 “Database > Connect to Database” 메뉴를 선택 한다. 아래와 같이 연결 창이 나오면 Username 란 에 “pyuser” 를 입력하고 “Store in Vault” 버튼을 눌러서, Password란에 “test1234” 를 입력 하고 “OK” 버튼을 눌러서 패스워드 입력 창을 닫는다. 이후 “OK” 버튼을 눌러서 데이터베이스에 연결을 한다.
연결이 되고 나면, 아래와 같은 쿼리 입력이 가능한 창이 생기는데, 밑의 MySQL 에 맞춘 쿼리 내용을 복사해서, 쿼리 입력 창에 붙여 넣기 한다. 이후 해당 쿼리를 MSSQL 때처럼 마우스로 드래그 하여 선택하고, “CTRL+SHIFT+ENTER” 키를 누르거나, 상단 메뉴에서 “Query > Execute (All or Selection)” 메뉴를 실행 한다. 그러면 하단 아웃풋 창에 반영되었다는 결과 메시지가 나오게 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 |
CREATE DATABASE mytest;
use mytest;
CREATE TABLE supermarket(
Itemno int NULL,
Category char(20) NULL,
FoodName char(30) NULL,
Company char(20) NULL,
Price int NULL
);
insert into supermarket
values (1, '과일', '자몽', '마트', 1500);
insert into supermarket
values (2, '음료수', '망고주스', '편의점', 1000);
insert into supermarket
values (3, '음료수', '식혜', '시장', 1000);
insert into supermarket
values (4, '과자', '머랭', '조각케익가게', 3000); |
cs |
반영된 것을 확인 하기 위해서 아래의 쿼리를 복사하여 붙여 넣고, 다시 셀렉트 한 후 실행 해 본다. 아래와 같이 결과가 정상적으로 나오게 되면 된다.
1 |
select * from supermarket; |
cs |
2.5.2.3 PHP 설치(with Apache)
처음 PHP를 설정하는 사람들은 조금 설정이 복잡하다는 생각을 할 수 있다. 왜냐하면 PHP 만 설치하면 되는게 아니고, 아파치(Apache) 서버와 PHP 서버를 각각 설치하고, 여러가지 세팅들을 해야하기 때문이다.
파이썬 글의 플라스크와 장고 파트에서 얘기는 하긴 했지만 웹서버는 보통 2개의 역할로 나누어지게 된다. 사용자의 웹 요청 자체를 처리하는 순수 웹 서버 부분이 있고, 해당 사용자의 요청을 프로그래밍 로직에 맞춰 해석하여 결과를 생성해 반환하는 부분이 있다. 아파치는 웹서버 역할을 하고, PHP 언어가 해석기 역할을 하게 된다(비슷한 예로 JSP는 아파치 웹서버위에 올라가 있는 톰캣이란 모듈로 서비스가 된다. 반대로 PHP 를 앞에서 ASP 에서 사용했던 IIS 와 연결할수도 있다. IIS에 플라스크를 얹을 수도 있고 말이다).
그럼 우선 PHP 프로그램을 세팅하도록 해보자 구글에서 "php download"를 검색해서 공식 다운로드 페이지로 이동한다.
[PHP 다운로드 페이지 - PHP 공식 페이지]
"Windows Download" 링크를 클릭하고, "VC15 x64 Thread Safe" 버전(x64가 64비트 윈도우를 얘기하고, Thread Safe 버전을 선택해야 현재 새팅에 필요한 DLL 이 있다)을 다운로드 받는다. 그리고 c:\PHP7 폴더에 압축을 푼다(압축 해제시 아래와 같은 폴더 구조가 되어야 한다)
c:\PHP7\php.ini-production 파일을 php.ini 으로 이름을 변경 한후, 맨 뒤에 아래의 내용을 추가한다(헷까리는 사람들을 위해 이 글의 맨뒤에 PHP와 아파치 설정 파일 두개를 zip으로 첨부했다). 내용을 보면 우리가 사용할 mysqli 모듈을 읽어오는 부분이다(파이썬에서 import 하는 거랑 비슷한 행위라고 보면 될것 같다)
1
2 |
extension_dir = "C:\PHP7\ext"
extension=php_mysqli.dll |
cs |
2.5.2.4 아파치 세팅과 PHP 연결하기
이렇게 하면 현재 시간에 보여주기 위한 용도로는 PHP 세팅이 완료되었고, 아파치 서버 세팅을 하도록 하겠다. 우선 구글에 "apache windows binary" 를 검색해 아래 페이지로 이동한다.
[아파치 바이너리 다운로드 - Apache Lounge 사이트]
https://www.apachelounge.com/download/
"Apache 2.4.33 Win64" 링크(버전은 시간이 지나면 변할 수가 있다)를 클릭하여 zip 파일을 다운로드 받는다. c:\Apache24 에 압축을 푼다(아래와 같은 폴더 구조가 되어야 한다)
PHP와 마찬가지로 설정 파일을 수정해야 한다. c:\Apache24\conf\httpd.conf 파일을 열어 아래의 몇가지 설정을 확인하거나 추가한다.
1) ServerRoot 항목에서 서버 루트 경로를 체크한다. 이미 "c:\Apache24" 로 세팅되어 잇을 것이다(버전이 바뀌면 경로에 맞춰서 바꿔줘야 할수도 있다)
1 |
ServerRoot "c:/Apache24" |
cs |
2) DocumentRoot 항목에서 아파치의 웹 루트 경로를 체크한다. 역시 마찬가지로 세팅되어 있으며, htdocs 는 아파치에서 사용하는 디폴드 웹루트 경로라고 보면 된다(IIS 에서 wwwroot 를 사용하는 것과 비슷하다고 보면 된다).
1 |
DocumentRoot "c:/Apache24/htdocs" |
cs |
3) Listen 항목(사용자의 웹 요청에 귀기울이고 있다는 의미로 보면 된다)을 확인하면, 기본으로 80 포트를 사용하게 되어 있지만, 현재 ASP 서비스 하는 IIS 에서 80을 이미 사용하고 있기 때문에 충돌나지 않도록 9999 로 바꾼다(서로 다른 프로그램이 하나의 포트를 공유해 사용할 수는 없기 때문이다).
1 |
Listen 9999 |
cs |
4) 마지막으로 앞에 얘기한 PHP 와 아파치를 연결하는 작업을 해야한다. 좀 복잡해 보일지도 모르지만, 아파치에서 PHP 가 어디 있는지 알려주고, PHP7 모듈을 읽어오고, PHP 핸들러를 만든 후, .php 확장자를 해당 핸들러에 연결해 주는 자연스러운 과정이다. 이렇게 되면 웹서버에서 .php 파일이 호출 되면, 아파치가 PHP7 에게 해당 요청의 처리를 위임하게 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13 |
# PHP 설정 파일 경로
PHPIniDir "C:/PHP7"
# 사용할 PHP 모듈 읽기
LoadModule php7_module "C:/PHP7/php7apache2_4.dll"
# 웹서버가 받은 요청에 대해 해석을 할 핸들러(해당 데이터를 적절히 처리 한다는 의미에서)
AddHandler application/x-httpd-php .php
# .php 확장자가 들어오면 앞에서 설정한 PHP7 핸들러에게 전달함.
<FilesMatch \.php$>
SetHandler application/x-httpd-php
</FilesMatch> |
cs |
이후 매번 아파치 서버를 실행 시키지 않고 서비스로 등록하기 위해서, 관리자 권한으로 커맨드 창을 먼저 띄운다. “윈도우+x” 키를 눌러서, 왼쪽 메뉴에서 “검색”을 선택한다. 검색 창이 나오면 “CMD” 이라고 찾은 후, “명령 프롬프트” 아이콘이 나오면 마우스 오른쪽 버튼을 눌러서 컨텍스트 메뉴를 띄워 “관리자 권한으로 실행”을 선택한다
커맨드 창이 나오면 "cd c:\apache24\bin" 을 입력해 아파치 실행 파일 경로로 이동 후, "httpd.exe -k install" 을 입력해 아파치 웹 서버를 서비스로 등록한다(이후 부터는 재부팅이 되도 항상 IIS 처럼 실행되어 있게 된다)
이후 아파치를 기동 시키기 위해서 탐색기에서 "c:\apache24\bin\ApacheMonitor.exe" 파일을 실행 하면, 트레이 아이콘에 아파치 서비스 모니터링 아이콘이 생긴다(아파치 서버를 시작하거나 멈추거나, 재시작 할때 쓰는 프로그램 이다). 해당 아이콘을 더블 클릭한 후, "Apache Service Monitor" 창이 뜨면 "Start" 버튼을 누른다.
브라우저를 열어 "http://localhost:9999/" 를 입력하면 아래와 같은 정상적인 기동 화면이 나오게 된다(MySQL PHP, 아파치 설정이 끝났다)
2.5.2.5 SQL 인젝션 취약점이 있는 PHP 페이지 살펴보기.
이제 세팅이 끝났으니 PHP 페이지를 만들어 보려 한다. PHP 쪽에서 유명한 SQL 관련 모듈은 mysqli 와 pdo 두개가 있는 것 같은데, 여기서는 mysqli 를 이용한다. 바로 샘플을 보도록 하겠다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60 |
<?php
// 에러나면 표시하게 설정합니다.
error_reporting(E_ALL);
ini_set("display_errors", 1);
// 연결 문자열
$servername = "localhost";
$username = "pyuser";
$password = "test1234";
$dbname = "mytest";
// 연결을 합니다.
$conn = new mysqli($servername, $username, $password, $dbname);
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}
// 인자가 있으면 받아서
if(isset($_GET["name"])){
$foodname = $_GET["name"];
} else {
$foodname = "";
}
// 쿼리에 조합합니다.
$sql = "SELECT Itemno, Category, FoodName, Company, Price FROM supermarket where FoodName=\"" . $foodname . "\";";
print "<h3>쿼리 결과</h3>";
print $sql;
print "<hr>";
?>
<html>
<body>
<form action="mysql_test.php" method="get">
FoodName: <input type="text" name="name"><br>
<input type="submit">
</form>
</body>
</html>
<?php
// 쿼리 결과를 받습니다.
$result = mysqli_query($conn, $sql);
// 결과가 없을 때까지 루프를 돌리면서
if (mysqli_num_rows($result) > 0) {
while($row = mysqli_fetch_assoc($result)) {
echo "Itemno: " . $row["Itemno"]. ", Category: " . $row["Category"]. ", FoodName: " . $row["FoodName"]. "<br>";
}
} else {
echo "0 results";
}
mysqli_close($conn);
?> |
cs |
전체 구조는 대충보면 ASP 와 비슷하다(사실 ASP 코드를 생각하면서 구글을 찾아가며 만들어 봐서 그럴지도 --;). 연결 문자열을 세팅하여 연결하고, 퀴리를 조합하여 DB 쪽으로 쿼리를 보내고, 결과를 받아서 화면에 뿌린다. 역시 취약점이 있었던 ASP 와 비슷하게 쿼리를 직접 조합해서 만들게 된다. "c:\Apache24\htdocs\" 폴더에 UTF-8 인코딩(한글 입력시 에러가 안나게 된다. PHP 도 파이썬 같이 UTF-8이 기본 인코딩 인가 보다)으로 "mysql_test.php" 라고 저장한다.
브라우저를 열어 "http://localhost:9999/mysql_test.php" 를 호출하고, 입력 란에 "자몽"이라고 넣고, "쿼리 전송" 버튼을 누른다. 위쪽 쿼리를 살펴보면 SELECT Itemno, Category, FoodName, Company, Price FROM supermarket where FoodName="자몽"; 문이 실행 되어, mysql 에 넣은 4개의 데이터 중, "FoodName" 이 "자몽"인 데이터 한 건만 나오게 된다.
홑따옴표나 쌍따옴표 기호를 넣어보면 ASP 와는 조금 다르게 명시적으로 쿼리가 깨졌다는 에러는 안나오기 때문에(직접 한번 해보면 쌍따옴표는 그래도 문법이 깨졌다고 판단해 그런지-아마도 문법이 깨지는 쿼리를 날렸기 때문에 리턴값이 안 넘어왔을 같다-Warning 이 나오긴 한다), 조금 다른 쿼리를 만들어 쿼리가 동작한다는 것을 증명해 보도록 하겠다. 입력 창에 자몽" or "x"="x 라고 넣어본다.
위의 쿼리를 다시 보면 where 조건에 FoodName="자몽" or "x"="x" 가 입력 되어, "x"="x" 는 항상 참이기 때문에 FoodName="자몽" 조건에 관계 없이 전체 결과를 가져오는 SELECT Itemno, Category, FoodName, Company, Price FROM supermarket; 와 같은 쿼리가 되어 버린다. 이렇게 되면 SQL 문법의 구조를 바꾸는 인젝션 취약점이 존재한 다는 것을 확인 할 수 있게 된다.
2.5.2.6 Prepared Statement 방식으로 호출하기
모든 언어가 마찬가지지만 해당 방어 방식을 사용하려면 결국 사용하는 라이브러리에서 지원을 해 주어야 한다. 구글에서 mysqli 의 Prepared Statement 호출 방법을 찾아 구현한 코드가 아래의 코드이다. 전체적인 구조는 앞의 코드와 비슷하지만, $conn->prepare, bind_param 같은 Prepared Statement 호출을 이용하는 코드들이 보인다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58 |
<?php
// 에러나면 표시하게 설정 합니다.
error_reporting(E_ALL);
ini_set("display_errors", 1);
// 연결 문자열
$servername = "localhost";
$username = "pyuser";
$password = "test1234";
$dbname = "mytest";
// 연결 합니다.
$conn = new mysqli($servername, $username, $password, $dbname);
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}
// 인자가 있으면 받아서 변수에 세팅을 합니다.
if(isset($_GET["name"])){
$foodname = $_GET["name"];
} else {
$foodname = "";
}
?>
<html>
<body>
<form action="mysql_prepared.php" method="get">
FoodName: <input type="text" name="name"><br>
<input type="submit">
</form>
</body>
</html>
<?php
// Prepared Statement 방식으로 쿼리를 호출해 결과를 받습니다.
$arr = [];
$stmt = $conn->prepare("SELECT Itemno, Category, FoodName, Company, Price FROM supermarket where FoodName = ?");
$stmt->bind_param("s", $foodname);
$stmt->execute();
$result = $stmt->get_result();
// 결과가 없을 때까지 화면에 뿌립니다.
if (mysqli_num_rows($result) > 0) {
while($row = $result->fetch_assoc()) {
echo "Itemno: " . $row["Itemno"]. " - Category: " . $row["Category"]. " " . $row["FoodName"]. "<br>";
}
} else {
echo "0 results";
}
$stmt->close();
?> |
cs |
마찬가지로 "c:\Apache24\htdocs\" 폴더에 UTF-8 인코딩으로 "mysql_prepared.php" 라고 저장하고 브라우저에 "http://localhost:9999/mysql_prepared.php" 를 호출한 후, 입력란에 아까 SQL 인젝션을 확인 했던, 자몽" or "x"="x 를 넣어서 전송해 본다. 앞의 스토어드 프로시저에서 설명했던 것처럼, 문법 요소가 아닌 상수로 취급되어 에러가 나지 않고 결과만 없게 된다. 아까 Warning이 났던 쌍따옴표를 전송해 봐도, 마찬가지로 괜찮다.
좀 더 실험을 해보고 싶은 경우는 해당 페이지를 수정해서, 앞의 스토어드 프로시저 때 처럼 데이터를 넣어보는 샘플을 만들어 보면 더 확실하게 알 수 있을 것 같다. 예전 버전의 PHP 에서 Magic Quotes 라는 사용자가 입력한 문자 중 좀 위험해 보이는 문자들 앞에 백슬러시를 붙여주는 방어 옵션을 제공하였 었는데, 아무래도 강제로 모든 입력에 대해 이스케이핑 하는 경우는 호환성 같은 사이드 이펙을 많이 발생시켰을 것 같다. 현재 버전에서는 기본 옵션에서 빠져 있는 듯 하며, 자세한 부분은 아래의 글에 나와있다.
[why-magic-quotes-are-gone-in-php7 - thePHP.cc 사이트]
https://thephp.cc/news/2017/08/why-magic-quotes-are-gone-in-php7
2.5.3 SQL 인젝션 방어전략 정리
여기까지 결국 스토어드 프로시저와 Parepared Statement 를 살펴보고 데이터가 어떻게 악용되게 되고, 어떻게 방어가 되는지를 살펴봤다. 사실 인젝션이나, XSS 같은 문제들은 디비나 브러우저 종류에 따라서 공격 케이스가 달라지기 때문에, 경우의 수를 모두 고려해서 개발자나 보안 인력이 고안한 로직으로 필터링 하기에는 복잡한 요소들이 많다.(해당 인젝션의 대상이 되는 서로 다른 밴더의 파서의 동작을 세밀한 부분까지 잘 이해해야 하기 때문에) 그러므로 앞에서 보인 안정되고 공식적인 방어기법에 의존하는 방식이 좋은 것 같다.
다만 앞에서 얘기했듯이, 왜 방어가 되는지에 대해서 데이터 관점에서 이해해야지만, 혹시나 예외적인 상황이 생겼을 때, 데이터를 추적하고, 해당 이유를 찾아 적절한 방어를 할 수 있지 않을까 싶긴하다.
또한 직접적인 방어 말고도 인젝션에 추가로 영향을 미치는 요소들이 추가로 있다. 어플리케이션 실행 계정에 대한 필요한 최소 권한 관리나(필요한 테이블만 조회한다든지, 스키마 변경이나, 시스템 함수 기능등의 실행은 막는 다는지... 이 부분은 프로그램 뿐만 아니라 조회 권한을 가진 사람들의 악용까지도 최소한으로 만들게 된다), 각 입력 변수에 대한 정확한 설계 및 검증(예를 들어 숫자만 들어오는 변수는 숫자형으로만 만든다든지)등이 함께 이루어지면 더더욱 안전한 설계가 될듯 싶다. 해당 부분은 defense in depth 라는 개념에서 얘기하 듯 하나의 방어가 혹시나 뚤리더라도, 다른 방어가 막게되는 안전한 시스템을 지향하게 된다. 물론 이런 부분이 실제 사용성이나, 설계 변경의 용이성과도 출동이 나는 경우는 있기 때문에, 여러 부서들의 이해관계를 기반으로 적절한 선택을 해야할 듯도 싶다.
[Defense in depth]
https://www.owasp.org/index.php/Defense_in_depth
마지막으로 어느정도 규모가 있는 회사에서는 법적인 사항때문이라도 어플리케이션 외부의 네트워크 구간에서 데이터베이스에 연결하는 모든 쿼리를 감사하고 통제하는 디비 접근제어 솔루션을 사용하는 경우도 있다. 뭐 여하튼 어떤 경우라도 하나가 모든걸 다 해결해 주진 않는 건 사실이다.
2.5.4 Prepared Statement 사용 예외 사항
이 부분 다른 일을 하다가 생각이 나서 추가하게 됬다. 알다시피 Prepared Statement 가 좋은 SQL 방어 도구 이긴 하지만 앞서 말했듯이 원래 SQL 방어용이 아니고 정형화된 쿼리를 날리는 용도 이기 때문에 적용이 안되는 경우가 있다.
예를들어 SQL 서버 쪽에 입력 받은 테이블이나 컬럼명에 따라서 동적으로 쿼리를 날려주는 관리 툴을 만들 경우는 쿼리 요소 중 개념적으로는 테이블 명 이나 컬럼이 인자가 되게 된다. 하지만 이 경우 쿼리 플랜이라는게 테이블 구조나 컬럼 등에 따라서 달라지기 때문에 테이블이나 컬럼이 변경되는 쿼리인 경우 Prepared Steatement 를 사용해서 Parameterized 된 쿼리를 날릴수는 없다.
이 경우는 인자로 넘어오는 테이블이나 컬럼 변수를 입력값 체크를 통해 위험한 문자들을 제거하는 방식으로 할수 밖에 없을 듯 하다(개인적이라면 테이블 컬럼 생성 표준에 따라 허용되는 문자 -> 아마 거의 "_" 정도)와 아스키 문자, 숫자 정도만 whitelist 로 허용하는 건 어떨까 싶다. 뭐 한글이름으로 테이블을 만들거라면 그것도 추가하고 --;)
3. 그 외의 Injection 들
여기서 모든 인젝션을 찬찬히 다루어도 좋겠지만, 처음에 얘기했듯이 인젝션의 범위는 크게 보면 어플리케이션으로 들어오는 모든 데이터에 대한 이야기기 때문에 범위가 너무 넓기도 하고, 일부는 뒤에서 다른 주제를 얘기할때 다루게 될 것 같다. 아래의 OWASP 페이지를 보면 다양한 타입의 인젝션을 언급 하고 있다(가볍게 한번 읽어보기를...),
[Injection Theory - OWASP 사이트]
https://www.owasp.org/index.php/Injection_Theory
[Injection Prevension Cheat Sheet - OWASP 사이트]
https://www.owasp.org/index.php/Injection_Prevention_Cheat_Sheet
해당 페이지에서는 SQL, LDAP, 시스템 명령어, HTML, XML, JSON, HTTP Header, File Path, URL, 프로그램에 따라 다른 특수한 스트링 포맷 등 여러가지를 얘기하고 있으며, 해당 요소들이 의미가 있는 이유는 보통 프로그램들이 내부나 외부에서 데이터(메시지)를 교환할때 위와 같은 포맷을 통해서 데이터를 전달해서, 내부에서 파싱해 처리하는 부분이 많기 때문이라고 생각하면 될듯 하다.
해당 부분을 방어하기 위해서는 역시 SQL 인젝션 방어로 사용된 스토어드 프로시저나 Prepared Statement 와 비슷하게 각 인젝션 타입에 특화된 방어 방식이 있다면 해당 방어 모듈, 프레임워크 등을 채용하고(1순위), 화이트 리스트를 기반으로 검증된 데이터만을 통과시키거나(2순위), 검증된 이스케이프 함수(URL encoding, HTML encoding 등)를 사용하거나(3순위), 마땅한 함수가 없거나 데이터의 자유도에 대한 요구사항이 높아서 화이트 리스트 방식을 사용하기 힘들다면 문법을 깨뜨릴 수 있는 문장이나 특수 문자를 삭제하는 커스텀 필터를 만들거나(4순위)하는 게 맞지 않을까 싶다.
그러한 바탕위에 다른 측면에서 SQL 인젝션에서 얘기한 권한 최소화(웹서버 실행 권한은 필요한 권한만 가지도록 설계한 전용 계정으로 실행하거나, 명령어 실행이 필요할 시 범용 함수보다는 디렉토리 생성이라든지 하는 특정한 용도로만 실행하는 함수를 사용한다거나)의 여러 측면을 설계와 운영 부분에 적용하는 것이 맞지 않을까 싶다.
SQL 인젝션 예제를 통해서 인젝션의 일반적인 특성에 대해서는 설명되었다고 생각되고, JSON 이나 HTTP Header, File Path 등에 대해서는 다른 시간에도 비슷한 관점으로 다루게 될 것이기 때문에, 여기에서 내용을 마치려고 한다(약간 늪에 빠지기 전에 도망치는 느낌은 있다^^).
4. 마무리 하면서
사실 인젝션을 설명하다보면 약간은 모순된 느낌에 빠지게 된다. 한 측면에서 보면 인젝션이 보안의 모든 요소에 영향을 주는 아주 중요한 위치를 차지하고 있지만, 사실 인젝션을 이해하는 중요한 열쇠는 보안적인 패턴 보다는 각 인젝션의 목적이 되는 데이터베이스, 대상 시스템 같은 도메인이나, 해당 현상이 일어나는 실제 장소인 프로그래밍 언어 환경에 있다고 본다. 만약 그러한 대상이 되는 기술 자체를 이해하지 못하는 상태에서 인젝션에 대해서 논하는 것은 (아마도) 탁상공론이 되기 쉬울 것이다.
또 다른 측면에서 보게되면 세부적인 보안 방식은 대상이 되는 기술에 종속적이기는 하지만, 전체적인 맥락에서 인젝션이라는 것은 데이터가 외부로부터 프로그램 안으로 들어가는 모든 행위를 나타내기 때문에, 프로그래밍 언어 그 자체의 이슈 인것 같은 부분도 있다. 어째든 처음엔 어려워 보이는 대상이 잘 알게 되면 생각보다 별 것 아닌 경우도 많고, 그렇게 이해하여 별 것 아니게 보였던 대상도 다시 자세히 따져 보려하면 어려워 보이는 경우가 종종 있는듯 싶다. 그 둘 사이에서 균형을 잘 유지하는 것도 중요할 것 같다.
[PHP7, 아파치 컨피그 파일 첨부]
2018.6.17 by 자유로운설탕 |
cs |
'보안' 카테고리의 다른 글
구글로 공부하는 보안 - 6교시 (업로드, 다운로드) (0) | 2019.02.24 |
---|---|
구글로 공부하는 보안 - 5교시 (클라이언트 코드) (0) | 2018.11.11 |
구글로 공부하는 보안 - 4교시 (암호화) (0) | 2018.07.15 |
구글로 공부하는 보안 - 2교시 (보안에서의 코드 읽기) (2) | 2018.01.01 |
구글로 공부하는 보안 - 1교시 (보안을 바라보는 방법) (6) | 2017.12.02 |