블로그 이미지
자유로운설탕

calendar

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

Notice

2019. 3. 2. 22:32 보안

  사실 스크립트 문제는 명시적으로 클라이언트나 서버에서 돌아가는 모든 스크립트가 문제이긴 하지만, 여기서는 클라이언트 쪽(정확히는 브라우저)의 자바스크립트 만을 대상으로 얘기하려 한다.

 

  일반적으로 자바스크립트 문제로 얘기되어 지는 XSS(Cross Site Script)는 인젝션 문제와 기본적으로 유사한 형태라고 생각된다. 다만 브라우저와 밀접하게 움직이는 클라이언트 코드인 자바스크립트의 특성 때문에, 최악의 상황에는 브라우저를 뒤에서 조정한다고도 할 수 있는 비동기 루프까지 일으킬 수 있기 때문에 더 특별히 다뤄지는게 아닐까도 싶다. 여튼 상황에 따라 단발성이 아닌 살아 있는 것 같이 사용자를 계속 괴롭힐 수 있는(물론 브라우저를 끄기 전까지에 한정되 있지만) 특이한 요소이다.

 

 

 

[목차]

1. 보안을 바라보는 방법

2. 보안에서의 코드 읽기

3. 인젝션(Injection) 살펴보기 #1, #2

4. 암복호화

5. 클라이언트 코드

6. 업로드, 다운로드

7. 스크립트 문제

8. API

9. 설정, 패치, 하드닝

10. 설계 문제

11. 스캐너 vs 수동 테스트

12. 자동화 잡

13. 리버싱과 포렌식

14. 모니터링 문제

15. 악성코드

16. 보안과 데이터

 

 

 

1. 들어가면서

  자바스크립트 문제에 영향을 줄 수 있는 요소들은 브라우저, 프레임워크 특성, 요구사항 정도 일거 같다.

 

  먼저 브라우저는 워낙 다양한 브라우저가 존재하고, 해당 브라우저 마다 XSS 시도에 대해서 서로 다른 디폴트 대응을 하고 있다(자바스크립트를 이용해 웹 프로그램을 만들어 보면 브라우저 호환성이 얼마나 속을 썩이는 지를 알수 있다). 어떤 브라우저는 모든 XSS 공격 시도에 대해서 허용하고 있고, 어떤 브라우저는 특정의 XSS 공격 시도에 대해 명시적으로 방어해 주기도 한다. 다만 상황에 따라 특정 자바스크립트 코드의 실행이 XSS 시도인지 브라우저 쪽에서 판단하는 것은 참 애매한 일이다. 잘못하면 정상적인 자바 스크립트 코드의 실행을 막을 수도 있기 때문이다. 해당 부분은 관련된 코드를 직접 보면서 한번 생각해 보도록 하자.

 

 

  두 번째로 프레임워크 특성 부분이다. 보통 요즘의 프로그래밍 언어들은 웹프로그래밍을 위해 제공하는 웹프레임워크 들이 있다. 해당 프레임워크마다 권고하는 설계가 있는데, 해당 설계를 따라 프로그램을 만들었을때, 자동으로 어디까지 XSS 로 의심되는 시도가 막히게 되느냐에 대한 문제가 있다. 어떤 언어는 해당 부분을 특정한 라이브러를 제공해서 개발자에게 명시적으로 해당 라이브러리를 사용해 방어코드를 넣도록 하는 경우도 있을테고, 어떤 경우는 프레임워크 자체에서 모든 전달된 변수에 대해 해당 방어 매커니즘을 자동으로 반영하는 경우도 있을 것이다. 그런데 여기서도 마찬가지로 지나친 안전을 보장하게 되면 모든 코드를 XSS 위험요소가 있는 것같이 다루게 되어 기능의 유연성이 떨어지는 문제가 생기게 될수도 있다. 이 부분도 뒤에서 플라스크 코드를 통해서 살펴보도록 하자. 

 

 

  마지막으로 요구사항 측면도 많은 영향을 미칠 수 있다. 예를 들어 게시판을 하나 만든다고 생각해 보자. 해당 게시판의 컨텐츠의 구성요소가 텍스트 기반이냐, HTML 기반이냐, 자바스크립트도 포함하느냐에 따라서 XSS 방어의 구현 난이도는 많이 차이가 나게 된다.

 

  텍스트 기반인 경우는 XSS 로 악용이 될 수 있는 모든 이상한 문자들을 HTML 인코딩 또는 제거를 통해서 나름 안전하게 데이터를 필터링 할 수 있다. 하지만 블로그와 같은 HTML 코드의 허용이 필요해버리면, 어떤 HTML 태그나 요소는 허용하고, 어떤 것은 제한 시킬지에 대해서 정교한 정책의 설계와 보안 테스트가 되어야 한다. 게다가 해당 부분은 위에 얘기한 브라우저 별로 검토되어야 하며, 흔히 여러 환경에서의 호환 테스트가 그렇듯이 그렇게 될경우 테스트 케이스의 수는 컨트롤 하기 힘들 정도로 계속 증식되게 된다.

 

  아래 링크에서 자바스크립트 체크 회피를 시도하는 XSS 공격에 악용 될수 있는 HTML 태그나 이벤트, 쿠키, 스타일 태그 등의 예제를 보여주고 있다. 보기만 해도 머리가 아파지지 않나 싶다. 만약 요구사항에 관리자들에게 필요하다며 게시판 작성시 자바스크립트 기능도 넣어달라고 해버리면 머리는 더 아파진다. 이런 경우 자바스크립트 방어를 포기하고 사용자 권한 관리 쪽에 포커스를 두거나, 커스텀한 편집 마법사를 만들어서, 사용가능한 자바스크립트 코드 기능을 제한하면서, 사용자가 직접 자바스크립트 편집을 하지 않게 하고 실제 코드의 생성 및 수정 처리는 서버단에서 하게 하는 방법도 있을 듯 싶다(공수는 엄청 들겠지만 말이다).

 

[XSS Filter Evasion Check Sheet - OWASP]

https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet

 

 

  특정 사이트 들에서는 BBCODE 라는 게시판용 커스텀 스크립트 언어를 쓴다고도 하지만, 아무래도 일반성 부분에서의 HTML 호환성을 포기하는 대가가 있을 것은 같다. 그리고 아마 저런 새로운 스크립트가 유명해지고, 자바스크립트 만큼 기능성이 확장된다면 역시 복잡도에 기반한 악용을 하는 시도는 늘어날 것이기 때문에 현실에 완벽한 해답은 없어 보인다.

 

[BBCode - WIKI]

https://ko.wikipedia.org/wiki/BBCode

 

 

 

 

2. 자바스크립트 공격의 종류

  보통 웹을 찾아보면 자바스크립트 공격은 3가지 종류로 나눌 수 있게 되는데, Reflected(반사), Stored(저장), DOM Based(돔 기반) 정도로 나누어지고, 부록으로 CSRF(Cross Site Request Forgery) 같이 연관된 항목이 있다.

 

  개인적으로 Reflected 와 DOM Based XSS 는 비슷한 종류라고 보고 싶다. 왜냐하면 두 항목의 차이는 브라우저 외부에서 입력된 스크립트 조각이 프로그래밍 언어의 출력 명령(PHP의 echo 같은)을 통해 화면에 표시되느냐, HTML 코드내에 포함된(또는 외부 링크일수도 있지만) 기존 자바스크립트에 의해서 표시되느냐의 차이일 뿐이기 때문이다. 여튼 두 가지는 클라이언트 코드, 인젝션 시간에 얘기한 브라우저 파서가 외부에서 들어온 자바스크립트를 어떻게 처리하느냐의 측면이라고 보면 될 듯 싶다.

 

 Stored 의 경우는 보통 Reflected 보다 심각한 문제라고 보는데 해당 부분에는 2가지 측면이 있다. 첫 째는 보통 해당 데이터가 게시판의 게시글 같이 DB에 저장된 상태로 보관되어 게시글을 보는 불특정 다수에게 노출이 된다는 부분이다. 둘째는 사실 이 글에서 얘기하고 싶은 요점이기도 한데 XSS 를 적극적으로 막아주는 브라우저도 이 타입의 스크립트에 대해서는 쉽게 선뜻 악용의 여부를 판단하기 힘들어 소극적이라는 부분이다. 왜냐하면 이 타입은 사실 Stored 란 이름 때문에 저장된 타입이라는 관점에서 보는 일이 많지만, 프로그램의 관점에서 보면 프로그램 내부에서 생성한 자바스크립트를 브라우저가 막을수 있겠냐는 딜레마의 측면이 있기 때문이다.

 

  마지막으로 CSRF 는 이름만 보면 난해해 보이지만, Forgery 가 '위조'라는 의미므로 요청(Request)을 위조한다는 의미라서 보통 인증에 사용하는 쿠키를 보유한 사용자가 모르는 사이에 XSS 공격을 이용해서, 대상 쿠키를 만들어낸 사이트의 주요 URL을 호출해 원하는 기능을 실행(예를 들어 패스워드 변경, 주소지 변경)하게 하는 요소로서 사이트의 주요한 기능에 대해서 쿠키 이외의 제대로된 추가 인증을 하고 있느냐가 문제가 되는 부분이라서, 사실 XSS 와는 관계가 있으면서도 관계가 없는 부분이기도 하다.

 

  그럼 주요 요소인 Reflected 와 Stored 공격에 대해서 브라우저들이 어떻게 반응을 하고 있는지 코드와 함께 살펴 보도록 하자.

 

 

 

 

3. Reflected vs Stored 공격 살펴보기

  아무래도 옛날 웹 프로그램 언어들이 이런 보안 부분에 아무 관심이 없는 편이므로, 3교시에 봤던 ASP 코드로 관련 코드를 만들어 본다.

 

 

 

3.1 POC 코드 만들고 엣지브라우저 또는 IE 에서 실행해보기

   해당 코드는 아래와 같다. 간단한 코드로 하나의 페이지에서 폼 전송과, 받기가 동시에 이루어진다. 위 쪽으로 보면 strFromGet 이란 변수는 폼에서 전송된 fromGet 이름을 가진 사용자가 입력한 input 박스 내용을 받고, strFromDatabase 변수는 원래는 데이터베이스를 갔다와야 하지만, 어차피 쿼리 결과를 변수에 담는거니 저런 스크립트 코드 데이터가 데이터베이스 안에 저장되어 있었다고 가정하자(이 부분이 앞서 얘기한 프로그램 내부에서 생성한 데이터 측면이 된다). 하단을 보면 Response.Write(PHP 의 echo 와 동일하다고 보면 된다)를 이용해서 해당 두 변수를 웹 화면에 뿌려준다.

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
<%
    ' 폼 값 받기
    strFromGet = request("fromGet")
    strFromDatabase = "<script>alert('stored xss run')</script>"
%>
 
<html>
   <head>
      <title>XSS 시연</title>
   </head>
 
<body>
     <form name = "Custom" method="get" action="show_xss.asp">
        <table width = 500>
            <tr>        
                <td> ID </td>
                <td> <INPUT maxlength="100" name="fromGet" size="50" type="text"> </td>
                <td> <input name=button type=submit value="ID 보내기"> </td>
            </tr>      
        </table>   
    </form>      
    
    <hr>
    <br><b>결과</b>
    <br><br>
        
<%
    ' 두 개의 변수 표시    
    Response.Write "Reflected XSS : " & strFromGet & "<br><br>"
    Response.Write "Stored XSS : " & strFromDatabase & "<br><br>"
%>
 
</body>
</html>
cs

[show_xss.asp]

 

  그럼 위의 코드를 c:\inetpub\wwwroot 에 ANSI 포맷으로 show_xss.asp 라고 저장한다. 이 후 우선 엣지브라우저를 열어 http://localhost/show_xss.asp 라고 입력한다(윈도우 10 기준이고, IE11 이나 엣지 브라우저 모두 같은 결과를 가져오니 아무거나 해도 된다).

 

 

  처음에 뜨는 Stored XSS Run 이라는 alert 은 일단 무시하고, ID 입력 창에 "<script>alert('reflected xss run')</script>" 라고 적은 후,  "ID 보내기" 버튼을 누른다.

 

 

 그렇게 되면 처음에 입력한 값을 웹 화면에 표시했기 때문에 페이지에서 자바스크립트가 실행되어 "reflected xss run" 이라는 alert 이 뜨고, 확인을 누르면 다시 "stored xss run" 이라는 alert 이 뜬다.

 

 

 

  F12 를 눌러 개발자 도구에서 페이지 소스를 보면 아래와 같이 2개의 alert 을 띄우는 스크립트 코드가 내부안에 들어가 있다.

 

 

 

3.2 크롬에서 실행해 보기 

  이번엔 IE 가 아닌 크롬에서 같은 페이지를 열어 시연해 보자. 현재 크롬 최신 버전은 72 버전이다. 크롬에서 실행해 보면 처음 "stored xss run" 얼랏이 뜨는건 마찬가지지만, 입력 박스에 스크립트를 넣어 전송하게 되면 아래와 같이 XSS_AUDITOR 에 의해 에러가 났다는 페이지가 뜨면서 실행이 막히게 된다.

 

 

  마우스 오른쪽 버튼을 클릭해 소스 보기를 하면 크롬이 페이지를 막은 부분의 스크립트가 표시된다. 크롬이 자체적으로 외부에서 들어온 악성 스크립트가 실행된다고 판단해서 실행을 막은 것이다.

 

 

 

3.3 브라우저 입장에서 생각해 보기.

  자 그럼 브라우저 입장에서 한번 해당 코드를 생각해 보자. 우선 위의 코드 실행 결과를 보고 어떤 브라우저가 더 안전하구나 생각하는 건 사실 좀 애매한 관점이다. 그것보다는 XSS 보안에 대해서 어떤 접근법을 취하느냐에 대해 살펴보는게 더 정확할 듯 하다.

 

 

  Reflected XSS 경우 브라우저는 해당 현상이 일어나고 있다는 것을 누구 보다 잘 알고 있다. 왜냐하면 해당 스크립트 코드 조각을 보낸 것이 바로 자기 자신이고(사용자가 입력한 값을 브라우저가 전달했으니), 응답 받은 코드에 보낸 코드 조각이 똑같이 있으므로(일단 브라우저는 요청된 결과를 소스로 받아서 이리저리 해석하니까 처음 받는건 스크립트가 포함된 HTML 소스이다), 자기가 보낸 코드가 반사(reflected) 되었다는 사실을 알수 있다(사실 다른 시간에 다시 얘기하겠지만 웹 취약점 스캐너의 주요 원리도 대충 이런 식이다).

 

  그래서 refelected XSS 의 경우는 엣지나 크롬 모두 잘 인지하고 있지만, 엣지는 해당 부분은 통과시켜주고, 크롬은 막고 있다.

 

  ※ 엣지 브라우저에 XSS 필터가 제거된 것은 2018년 7월 인거 같다. 자세한 것은 아래 글 같은 부분을 참고하면 될 듯 하다. Content Security Policy 로 보호가 대체 될수 있다고 얘기하고, 여기서도 XSS 필터가 reflected XSS 만 막을 수 있다는 부분을 얘기하고 있다.

[edge to remove xss auditor - scotthelme 사이트]

https://scotthelme.co.uk/edge-to-remove-xss-auditor/

 

 

  문제는 XSS 취약점 중 영향이 크다고 얘기되고, 실제로 영향이 큰 경우는 stroed XSS 라는 것이다. 위의 예제에서 봤듯이 두 브라우저 모두 해당 코드를 실행 시켜준 이유는, 이게 해킹이 된 내부 데이터로 부터 날라온건지, 원래 프로그램이 실행시키기를 원해 만든 정상적인 스크립트 인지를 판단하는 것이 종합적인 배경지식이 없다면 현실적으로 불가능 하다는 점이다(저 위의 Content Security Policy 라는 나름 이런 부분을 정의하여 접근을 구분하겠다는 것으로 보이긴 하지만, 모든 브라우저가 지원해야 되고, 페이지 마다 권한을 정의해야 되니 설계가 많이 복잡해지긴 할 듯 싶다.

 

  사실 그런 종합적인 배경지식은 페이지를 설계한 기획이나 개발자가 제일 잘 알고 있다. 그래서 어느 정도 브라우저에도 의존할 수도 있고, 해당 스크립트 코드가 들어간 링크를 누군가 명시적으로 클릭하기 전에는(물론 인지 못하고 클릭할 가능성이 높지만) 일어날 수 없는 reflected XSS 와는 달리 stored XSS 는 100% 의도적인 보안 설계로 풀어야 한다. 

 

 

 

 

4. flask 방어 코드 예제 보기

   XSS 필터를 이용한 예제도 괜찮겠지만, flask 코드에서 자동 방어해주는 부분을 한번 보도록 하자. 정확히 얘기하면 플라스크에서 사용하는 jinja2 템플릿에서 지원해준다고 보면 될것 같다. 역시 간단한 코드를 보도록 하겠다(flask 동작에 대해 잘 모르겠으면 파이썬 19교시를 보고 오자).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from flask import Flask, make_response, render_template
 
# flask 웹서버를 실행 합니다.
app = Flask(__name__)
 
@app.route("/xss", methods=['GET'])
def xss():
 
    xss_string = "<script>alert('flask reflected xss run')</script>"
    return render_template('xss.html', name = xss_string)
 
 
# 이 웹서버는 127.0.0.1 주소를 가지면 포트 5000번에 동작하며, 에러를 자세히 표시합니다 
if __name__ == "__main__":
    app.run(host='127.0.0.1',port=5000,debug=True)
cs

[flask_xss.py]

 

  위의 코드를 c:\python\code 에 UTF-8 포맷으로 flask_xss.py 이름으로 저장한다. 

 

 

  이후 템플릿 파일을 만들어 보자. 한줄짜리 간단한 html 이다.

1
<td>{{ name }}</td>
cs

 

  위의 코드를 c:\python\code\templates 폴더에 UTF-8 포맷으로 xss.html 로 저장한다. 이후 플라스크 웹서버를 실행 시킨다.

 

c:\Python\code>python flask_xss.py
 * Serving Flask app "flask_xss" (lazy loading)
 * Running on
http://127.0.0.1:5000/ (Press CTRL+C to quit)

 

  이후 엣지 브라우에서 http://localhost:5000/xss 주소를 열어본다. 아래와 같이 스크립트가 실행되지 않고, 일반 문자로 표시된다. html escape 처리가 된 것이다.

 

 

  실제 이스케이프 처리가 된 것을 보기위에 소스보기를 하면 아래와 같이 이스케이프 처리가 자동으로 된 것을 볼 수 있다. 

 

 

  그럼 여기서 이런 생각이 들 수 있다. "그런데 만약 자바스크립트를 flask 로직쪽에서 생성해서 동적으로 HTML 페이지 쪽에 랜더링하는 경우라면 어떻게 할까?" 이다. 이 경우 해당 필터를 제거하는 방법은 간단하다. 아래와 같이 xss.html 파일에서 받은 변수의 뒤에 "|safe" 문장을 추가하여 저장하면 된다. 

1
<td>{{ name|safe }}</td>
cs

 

 

  이후 다시 같은 url 을 실행하면, 자바스크립트가 이스케이프 처리가 안되고 그대로 실행되는 것을 볼 수 있다. 다만 이 경우는 무조건 좋아할 건 아니고, 스크립트를 무효화 하는 기능이 해제된거니, 본인이 직접 해당 넘어온 코드의 안전성을 보장하는 부분을 고민해야 하는 문제가 생긴다. 세상에 완전한 공짜 점심은 없다. 

 

 

 

 

5. 응용편 - 비동기 호출과의 만남.

  추가로 공격의 XSS 의 최악의 상황을 묘사해 보기 위해서, 극단적이긴 하지만 비동기 코드에서 일어나는 상황을 한번 만들어 보자.

 

 

  우선 flask 코드는 아래와 같다. 보게 되면 2개의 라우팅 경로가 있는데, /xss_ajax 는 기본 인젝션 페이지로 stored XSS 가 전달되고 있고(reflected 보다 POC 코드 만들기가 편해서 이렇게 했다^^;), xss_ajax.html 을 랜더링 패이지로 쓴다.

 

  밑의 /getData 경로는 웹페이지에서 AJAX 호출을 하는 경로인데, 내용을 보면 단순하게 id 값 인자를 받아서 "hello, 받은 id" 이라는 값을 조합해서 반환한다. 랜더링 때 인젝션 되는 스크립트 코드를 보면 setInterval 함수를 이용해서 매 3초(3000)마다 GetData() 함수를 반복 호출하는 코드이다.

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
from flask import Flask, make_response, render_template, request
 
# flask 웹서버를 실행 합니다.
app = Flask(__name__)
 
@app.route("/xss_ajax", methods=['GET'])
def xss_ajax():
    
    xss_string = """<script>
                       setInterval(function() {
                          GetData();
                       }, 3000);
                    </script>"""
    return render_template('xss_ajax.html', name = xss_string)
    
    
 
@app.route("/getData", methods=['GET'])
def getData():
    your_id = request.args.get('id')
    return "hello, " + str(your_id)
 
 
# 이 웹서버는 127.0.0.1 주소를 가지면 포트 5000번에 동작하며, 에러를 자세히 표시합니다 
if __name__ == "__main__":
    app.run(host='127.0.0.1',port=5000,debug=True)
cs

 [flask_xss_ajax.py]

 

  일단 c:\python\code 폴더에 UTF-8 포맷으로 flask_xss_ajax.py 라고 저장을 한다.

 

 

 다음은 템플릿 소스이다(기본적인 AJAX 코드 동작을 모르는 경우는 클라이언트 코드 편을 보고 온다). 스크립트를 보면 단순하게 페이지가 로딩이 완료(document.ready)되는 순간에 GetData 함수를 호출 한다. GetData 함수는 my_id 인자에 tom 이라는 값을 넣어, flask 의 getData 라우팅 주소를 호출한다. 그리고 반환된 결과를 아래의 span 태그 안에 표시한다. 그 밑의 {{ name|safe }} 는 일부러 XSS 취약점을 일으키게 위해 HTML 인코딩을 제거했다.

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
<html>
    <head>
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
        <title>AJAX XSS Sample</title>
    </head>
    <body>
        <script type="text/javascript">
 
            $(document).ready(function(){
                GetData();
            });
 
            function GetData(){
                var my_id = "tom"
                $.ajax({
                    url : '/getData',
                    type : 'GET',
                    data : {id: my_id},
        
                    success: function(res){
                        $("#spanName").html(res);
                    }
                });
            }
        </script>
                        
        <span id="spanName"></span>
        <td>{{ name|safe }}</td>
    </body>
</html>
cs

[xss_ajax.html]

 

  해당 소스를 c:\python\code\templates 폴더에 UTF-8 포맷으로 xss_ajax.html 이름으로 저장한다. 이후 페이지를 한번 실행해보자. XSS 공격만 없다면 최초 페이지를 로딩했을때, getData 경로를 한번 호출하는 ajax 코드를 이용해 결과를 받아, 페이지에 hello, tom 이라고 표시해주는 작업을 하게 되는 페이지이다. 이후 플라스크 웹서버를 실행 시킨다.

 

c:\Python\code>python flask_xss_ajax.py
 * Serving Flask app "flask_xss_ajax" (lazy loading)
 * Running on
http://127.0.0.1:5000/ (Press CTRL+C to quit)

 

 

  엣지나 크롬 브라우저에 http://localhost:5000/xss_ajax 경로를 실행 하면, 아래와 같이 평범한 페이지가 실행이 된다. 사용자 입장에서는 특별한 일이 없는 것으로 보인다.

 

 

  하지만 피들러를 띄운 상태에서 해당 페이지를 띄워보면 전혀 다른 상황이 되어 버린다. 아래와 같이 한번 호출 하고 말아야 했던 /getData 경로가 3초에 한번씩 무한으로 실행되고 있다(브라우저를 끄면 종료된다). 이론상 스크립트를 어떻게 작성하고 어떤 AJAX 기능을 호출하냐에 따라서 마음대로 할수 있다(이렇게 되면 SQL Injection 이 일어났을때 공격자가 마음대로 쿼리를 만들어 내는 거나, 업로드 취약점으로 웹쉘이 올라갔을때와 비슷한 느낌이 된다). 인증된 토큰이나 쿠키는 이용하지 않지만, 위의 예제를 CSRF 공격의 AJAX 버전이라고 봐도 될듯 하고 말이다.

 

 

  보안 교육 할때 브라우저를 마음대로 조정한다고 많이 시연되는 Beef 라는 툴도 결국은 이와 비슷한 원리로 잘 짜여진 ajax 스크립트가 기반일 것이다.

 

[beef project]

https://beefproject.com/

 

 

 

 

6. XSS 방어 설계

  그럼 우리가 생각했던 여러 포인트 들을 정리해서 XSS 이슈에 어떻게 접근해야 될지 한번 고민해 보자. 우선 XSS 의 패턴은 너무 많다. 그러므로 가장 바람직한 경우는 영향을 최소화 하기 위해서 스크립트나 HTML 을 표시하는 기능을 필요한 만큼만 최소화해서 구현 하는 것이다. 사용할 수 있는 태그 제한이 가능하다면 가능한 태그와 문법만 허용하는 화이트 박스 방식으로 체크하면 좋을 서이다.(이런 이상적인 경우는 드물긴 하겠지만...)

 

 

  reflected XSS 는 브라우저가 인지해서 어느정도 방어할 수는 있지만, 세상 사람들이 가진 브라우저는 워낙 종류와 버전이 다양하므로 브라우저만의 방어를 믿을 수는 없다. 앞에서 보듯이 IE 계열은 그대로 실행도 시켜주고 있다. 그래서 적절한 방어 코드를 넣어야 하는데, 원칙상으로는 페이지의 자바스크립트나 프로그래밍 언어에서 참조하는 외부 모든 인자에 대해서(HTTP 헤더의 모든 값 포함) 사용 전에 안전하게 필터링 해줘야 한다. 

 

  그런 부분을 충분히 설계하기 힘들다고 판단된다면, 아래와 같은 신뢰성 있는 기관에서 만든 필터를 이용하는 것이 현실적이다. 하지만 기본적인 XSS 에 대한 이해와, 자바스크립트의 이해, 방어의 한계 등을 잘 인지하고 있어야지 문제가 있을때 대처 할 수 있다는 것은 잊으면 안될것 같다(때로는 과하게 막아 프로그램과의 호환성을 위해서 해당 필터 라이브러리의 일부를 커스터마이즈 해야 될 수도 있을 듯 싶다). 또 위의 flask 예에도 보았듯이 괜찮은 웹 프레임워크나, 여러 자바스크립트 라이브러리를 쓰는 것도 해결책일 거 같다. 해당 라이브러리에 문제가 생긴다면 그쪽에서 빠르게 패치를 제공할 것이기 때문에 말이다(사실 보안은 어느정도 이렇게 신뢰성 있는 외부 프로젝트 들에 많이 의지하는 것 같다. 회사에서 구입해 사용하는 상용 백신 솔루션 같은 경우도 어찌보면 백신 회사의 지적인 프로젝트를 믿고 의지하는 것이라고 볼수 있다).

 

[Cross Site Scripting Prevention Cheat Sheet - OWASP]

https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.md

 

[XSS 나무위키] - 4.1.1.2 에 네이버에서 제공되는 필터가 언급되어 있다.

https://namu.wiki/w/XSS

 

 

  reflected XSS 의 경우는 웹서버로 전달되는 모든 인자를 체크해야 하는 부분이지만, 사실 명확히 얘기하면, 프로그램이나 자바스크립트에서 실제 사용하는 인자가 실제 대상이다(뭐 미래에 사용 안하던 다른 인자가 사용될 수도 있지만 그때는 적절한 개발 단계의 보안 점검을 통해서 변화를 인지하고 팔로우업해야 될듯 하다).

 

  또 브라우저가 reflected XSS 를 인지할 수 있다는 것은 브라우저에 들어가는 입력과(여러 GET, POST 인자와 HTTP 헤더) 출력(서버로부터 전달되어 화면에 뿌려지는 HTTP 소스)을 알 수 있다면, reflected XSS 가 일어나는 후보 값들을 쉽게 발견할 수 있다는 것이다. 이 부분이 웹 취약점 스캐너가 XSS 를 찾는 원리라고 볼 수 있고, reflected XSS 또는 DOM based XSS 영역의 취약점을 점검하는데 많은 도움이 된다. 스캐너는사람이 하면 거의 불가능한 모든 인자를 리스트업 해서, 스크립트를 입력하고 결과 HTML 에서 해당 스크립트가 어떻게 되었는지 확인하여 판단을 해준다. 다만 이경우 앞에서 얘기한 실제 사용안하는 외부 값까지도 모두 검사하는 낭비 측면이 있지만 뭐 그건 자동화의 단점이자 장점인 듯 싶다.

 

  반대로 얘기하면 좀더 민감한 stored 같은 경우는 운이 좋지 않은 이상 스캐너로 발견하기 무척 힘들다. 해당 데이터에 우연히 접근되거나, 해당 데이터를 전송한 후에 바로 확인이 가능해야 한다. 전략적으로 스캐너를 이용해 stored XSS 테스트에 접근한다면, 기존 기능을 기반으로 입력 후 바로 디비에 저장된 결과를 보여주는 테스트 페이지를 만들어 스캐너를 돌린다든지 하는 작업이 필요할 듯 하다. 추가로 스캐너는 문제를 발견하는 것이지 문제가 없음을 100% 증명하는 툴은 아니라는 것을 인지하자. 스캐너가 사용한 수많은 XSS 패턴들이 모든 현존하는 XSS 패턴을 커버한다는 보장은 할수 없기 때문에, 스캐너 검사에 통과 되었더라도 여러 다양한 요소에 해당하는 취약점 패턴이 숨어있을 수도 있다.

 

 

 

 

7. 마무리 하면서

  추가로 위의 예에서는 유입 변수에 <script> 태그가 들어간 예제만 보았지만, 실제로는 해당 태그가 없어도 스크립트 동작이 가능한 경우가 많다. 적절한 코딩 방법은 아니지만 <script> 태그내의 스크립트 내용 중 일부를 외부 값을 사용하는 경우다(실제 이렇게 코딩하는 개발자들이 가끔 있다). 위의 플라스크 템플릿을 예를 들면 아래의 코드 형태일 것이다.

1
2
3
4
5
<script>
 
var a = {{ name|safe }};
....
</script>
cs

 

 

  name 변수에 "1;setInterval(function() {GetData();}, 3000);" 을 넣는다면 아래와 같은 반복 실행을 하는 문법이 똑같이 만들어질 것이다.  

1
2
3
4
5
<script>
 
var a = 1setInterval(function() {GetData();}, 3000);
....
</script>
cs

 

 

  뭐 여튼 결론적으로 스크립트 문제 및 인젝션 문제에 대해서는 요구사항이나 설계의 자유도가 높아질 수록 점점 방어하기는 복잡하게 되어 버린다. 모든 보안 분야가 마찬가지로 자유도를 높게 줄수록 방어하기는 더 어려워지며 포기해야할 트레이드 오프도 생길 수 있다. 결국 해당 부분의 영향을 최소화 하기 위해서는 대상이 되는 시스템과 요구사항, 기술, 프로세스, 관련된 사람들을 잘 이해하는 방법 밖에는 없을듯 싶다. 생각보다 실제에서는 보안은 명확하지 않은 애매한(놓여진 코드나, 요구사항, 여러 외부 환경에 따라 달라지는) 영역들이 상호 작용하는 분야이기도 한거 같다. 그나마 어플리케이션 보안 부분이 제일 명확한 면이 있어 보이는 듯 싶다. 점점 진행될 수록 앞의 다른 챕터의 얘기들이 조금씩 반복되곤 하는데 맨 처음 얘기한 듯이 보안의 원리가 결국 비슷비슷하기 때문이다. 어플리케이션 보안은 프로그램과 데이터 간의 술래잡기 같은 측면이 있다.

 

 

2019.3.3 by 자유로운설탕
cs

 

 

 

 

posted by 자유로운설탕
2019. 2. 24. 23:38 보안

  이번 시간에는 웹 어플리케이션에서 자주 이슈가 되는 업로드, 다운로드 문제를 살펴보려고 한다. 이 부분은 내부에 돌아가는 원리와 패턴을 이해하기만 하면 해법이 아주 명확한 편인 부분이기도 하다. 각각 실제 돌아가는 코드를 한번 살펴본 후, 해당 문제를 보안적으로 바라보는 관점을 설명하려고 한다.

 

 

[목차]

1. 보안을 바라보는 방법

2. 보안에서의 코드 읽기

3. 인젝션(Injection) 살펴보기 #1, #2

4. 암복호화

5. 클라이언트 코드

6. 업로드, 다운로드

7. 스크립트 문제

8. API

9. 설정, 패치, 하드닝

10. 설계 문제

11. 스캐너 vs 수동 테스트

12. 자동화 잡

13. 리버싱과 포렌식

14. 모니터링 문제

15. 악성코드

16. 보안과 데이터

 

 

 

1. 들어가면서

  사실 컴퓨터안의 데이터라는 것은 사용자나 프로그래머를 위해서 많이 추상화 되긴 했지만, 변하지 않는 사실은 0과 1로 이루어진 일련의 숫자를 로직과 표준이라는 임의의 규칙에 맞춰 구분한 것이라고 볼 수 있다. 물론 이 0과 1로 이루어진 숫자를 보고 실제 환경과 동작을 알수 있는 사람은 현대에는 거의 없을 것 같긴하지만, 추상적인 상위 OS 에서 하위로 파고 내려가는 리버싱이나 포렌식, 악성코드 분석 같은 부분을 보면 그런 하위로 내려가는 노력도 필요한 분야는 여전히 건재한 듯 하다.

 

  보안 쪽에서는 그런 부분 이외에도 파일의 특정 공간에 숨겨진 데이터 라든지, 프로토콜 안에 숨겨진 데이터 등등 보통 밖에서 보기엔 완전하게 하나의 개체로 추상화된 데이터의 조작된 내부를 봐야 하는 경우가 많은 것 같다. 이러한 해체적 관점(좀 과장은 됬다)에서 한번 업로드, 다운로드를 살펴보려고 한다.

 

 

 

2. 다운로드 문제

  일단 파이썬으로 이루어진 엑셀 다운로드 기능을 하나 보자.

 

 

2.1 모듈 설치

  우선 필요한 모듈을 설치한다. 어떤 글을 봤냐에 따라 이미 설치된 모듈도 있을 것이다.

C:\Python\code>pip install pandas

C:\Python\code>pip install openpyxl

C:\Python\code>pip install numpy

C:\Python\code>pip install flask

 

 

2.2 엑셀 다운로드 구현

  예전에 파이썬 때 만들었던 pandas 코드와 구글 검색으로 찾은 flask 코드를 대충 합치면 아래와 같다. (플라스크 구현 부분이 이해가 안되면 파이썬 글 19교시 flask 편을 보고 오면 된다)

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
from flask import Flask, make_response
import pandas as pd
import numpy as np
import io
 
# flask 웹서버를 실행 합니다.
app = Flask(__name__)
 
@app.route("/excel_down", methods=['GET'])
def excel_down():
   # pandas 객체를 하나 만든다.
   df = pd.DataFrame({'A''fruit drink cookie fruit'.split(),
                      'B''orange Coke chocopie mango'.split(),
                      'C': np.arange(4)})
 
   # 메모리에 pandas 객체를 이용해 엑셀을 만들고 저장한다.
   output = io.BytesIO()
   writer = pd.ExcelWriter(output)
   df.to_excel(writer, 'Tab1')
   writer.save()
 
   # 엑셀 형태로 HTTP 응답을 주어 브라우저가 파일로 저장하게 유도한다.
   resp = make_response(output.getvalue())
   resp.headers['Content-Disposition'= 'attachment; filename=output.xlsx'
   resp.headers["Content-type"= "text/csv"
   return resp
 
# 이 웹서버는 127.0.0.1 주소를 가지면 포트 5000번에 동작하며, 에러를 자세히 표시합니다 
if __name__ == "__main__":
    app.run(host='127.0.0.1',port=5000,debug=True)
cs

[flask_excel_down.py]

 

   c:\python\code 폴더에 "flask_excel_down.py" 이름으로 UTF-8 인코딩으로 저장 후 실행하면 플라스크 웹 서버가 실행 된다.

C:\Python\code>python flask_excel_down.py
....
 * Running on
http://127.0.0.1:5000/ (Press CTRL+C to quit)

 

 

  이후 브라우저 주소창에 http://127.0.0.1:5000/excel_down 이라고 적으면 아래와 같이 pandas 데이터가 저장된 엑셀 파일이 다운된다.

 

 

2.3 코드 살펴보기

  이제 동작은 확인했으니 위의 코드를 한번 살펴보자. 첨에 테이블 형태의 pandas 객체가 만들어지고, io.BytesIO() 라는 바이트 형태의 파일 스트림이 하나 만들어 진 후(output = io.BytesIO()), 해당 스트림안에 pandas 를 사용하여 엑셀 데이터를 만들어 채워 넣게 된다(writer = pd.ExcelWriter(output)).

 

  이후 해당 파일 스트림을 저장 후에, HTTP 형태로 응답을 생성하면서, 엑셀 파일의 전달에 맞는 헤더를 설정한다(resp.headers['Content-Disposition'] = 'attachment; filename=output.xlsx', resp.headers["Content-type"] = "text/csv"). 브라우저는 해당 응답을 받아 엑셀 파일 형태임을 인지하고, 사용자에게 엑셀 파일로 저장하라는 선택 창을 보여주게 된다.

 

 

2.4 피들러로 살펴보기

  이제 네크워크 단에서 전송 되는 부분을 살펴보기 위해서 피들러를 통해 봐보자. 피들러의 resonse 섹션에서 raw 탭을 클릭해 보면(이제 기존 글을 봤다면 피들러 사용 법은 어느 정도 익숙해 졌다고 생각해서 자세한 스탭은 생략한다. 잘 모르는 경우는 파이썬 글 WhoIS API 편에 설명되어 있으니 그 쪽을 보고 오면 된다), 아래와 같이 raw 탭을 클릭해 보면 헤더 값 뒤에 암호화 된 것만 같은 엑셀 파일 내용을 인코딩한 데이터가 브라우저로 전달되고 있다.  

 

 

  헤더 부분 탭만 클릭해 따로 보게 되면 아래와 같이 text/csv 라는 content-type 으로 지정되어, 브라우저가 이게 엑셀 파일이구나를 명시적으로 알게되어 파일로 저장하도록 유도한다.

 

 

2.5 다운로드 문제 생각해 보기

  위의 예제를 보면 우리가 일반적으로 받아들이는 것과는 조금 다르게 내부적으로는 실제 무언가가 다운된다는 행위가 실제로는 엑셀 파일 형태의 일련의 데이터 스트림이 서버에서 만들어져 브라우저 쪽으로 복사되어 전달되는 것이라는 것을 볼 수 있다. 정확하게 얘기하면 복사(또는 반사)에 가깝다. 이렇게 보면 사실 다운로드의 보안 문제는 복사된 데이터가 넘어올때, (누군가가) 원하지 않은 복사된 데이터가 넘어오는 경우라고 볼 수 있다.

 

  그럼 어떤 경우 원하지 않는 데이터가 복사되어 올까? 사실 취약점이란 공격자에게 이점을 주는 관점에서 명명되는 것이기 때문에, 기대하지 않았던 이상한 데이터가 사용자가 넘어오는 부분은 보통 버그지 보안 쪽에서 초점을 두는 부분이 아닐 것이다. 그래서 보통 다운로드 취약점하면 서버 쪽에서 보내길 원하지 않는 데이터(파일, 데이터)가 전달 될때 문제가 된다. 

 

 

  웹에서의 다운로드는 굳이 분류하자면 4가지 정도 타입으로 나눠 볼수 있는데, 첫 번째는 우리가 브라우저에서 이미지 URL 을 입력했을때 이미지가 다운되서 보이게 되는 것 같은거, 두 번째는 서버쪽 프로그램에서 임의의 URL 을 조합해 생성해서 우리한데 해당 경로의 파일이나 이미지를 전달해 주는 경우, 세 번째는 위의 엑셀 다운 프로그램과 같이 서버쪽에서 조합된 데이터(보통 관리자 화면 같은데서 데이터베이스에서 조회한 데이터를 엑셀 형태로 만들어 다운로드가 되는 경우가 많다)를 다운로드 하는 경우, 마지막으로 가장 특수한 경우긴 하지만 사용자가 자신이 올린 파일이나 권한 있는 파일만 다운로드 해야 할 경우가 있는 경우이다.

 

  첫 번째 URL 을 입력하는 경우는 사실 뭐 거의 막을 수 없는 경우인데, 중요한 파일이 직접 URL 을 입력해서 다운로드가 되는 부분이 문제이므로, 중요한 파일은 웹 루트 바깥의 경로에 두어 URL 로 다운로드가 되지 않게 하거나, 웹 서버 설정에서 접근 가능 확장자 필터링을 하여 다운로드가 불가능하게 하는 방법 밖에는 없다. 또한 디렉토리 리스팅 등의 웹폴더 내부의 파일 이름을 볼수 있는 등의 힌트가 되는 경우를 막고(사실 요즘 웹서버들은 기본적으로 disable 되어있긴 하지만) 근본적으로는 중요한 파일이라면, 뒤에 얘기할 네 번째 경우와 같이  설계를 해야된다.

 

  두 번째 서버 쪽 프로그램에서 사용자가 전송한 인자나 header 의 특정 값 요소에 따라 임의의 URL 을 조합해서 해당 파일을 전달해 주는 경우인데, 이 경우는 5교시에서 얘기한 클라이언트에서 날라간 인자를 믿는 클라이언트 코드 문제라고 볼 수 있다. 이 경우 부터는 사실 좀 애매모호하게 흘러가는데, 보통 Directory Traveral(디렉토리 트레버셜) 문제인 ../../ 같은 상위 디렉토리를 거슬러 가거나 하는 문자를 파일 이름이나, 경로로 사용될 인자에 넣어 악용해 생기는 문제이므로, 밑의 업로드 예제에서 보이게될 werkzeug.utils 모듈의 secure_filename 같은 함수를 사용하여 인자에서 해당 문자들을 제거하거나 하는 방법이 있긴하다. 하지만 보통 이 부분이 레거시 코드를 수정하는 경우는 디렉토리를 만드는 규칙이나, 파일 이름의 생성 등과 대치되는 상황이 생길수도 있다(뭐 새로 코드를 다시 만든다고 해도 기존에 이미 있는 파일은 어떻게 해냐하는 마이그레이션 문제가 생길 수 있다)

 

[디렉토리 접근 공격 - H@ck.pe.kr]

https://hack.pe.kr/91

 

  대부분 이런 경우는 요구사항 구조상 파일 이름이나 번호 등을 인자로 받아 지정한 파일을 전달할 수 밖에 없는 경우므로, 개인적으로 추천하는 방법은 디렉토리 경로는 고정(상수나 config 값으로)시키고, 파일이름만 넘겨 받아 위에 언급한 secure_filename 와 비슷하게 각 언어에서 지원해주는 이름에서 위험한 문자들을 제거해 주는 메서드를 적용하는 것이다. 물론 이렇게 하려면 업로드 할때도 같은 함수를 파일 이름에 적용하거나, 아예 알파벳과 숫자로만 제한된 파일 이름으로 저장해야 호환성 문제가 발생 안한다.

 

  세 번째는 사실 위와 같이 허용되지 않는 파일을 다운로드 하는 측면의 문제라기 보다는, 허용되지 않는 데이터를 조회하는 측면에 가깝다(그 조회된 내용이 엑셀 형태로 브라우저로 전달될 뿐이다). 이 경우 실제 파일이 다운로드 되는게 아니고 데이터베이스를 조회한 데이터가 엑셀로 만들어 지는 일이 일어날 뿐이므로, 데이터베이스에 조회하는 인자들의 SQL Injection 이라든가, 또는 비슷하게 데이터를 만들어 낼때 악용될 수 있는 클라이언트 인자의 조작 부분을 방어하면 될 듯하다.

 

  마지막으로 네번째는 가장 엄격한 형태라고 볼수 있는데, 사용자의 이력서가 올라가는 사이트라든지, 해당 회원만 다운받을 수 있는 파일이라든지(예를 들어 웹툰 미리보기?) 같은 문제이다. 이 경우는 보통 권한을 관리해 주는 추가적인 데이터베이스 설계가 필요한데, 파일의 리스트를  관리하면서 해당 파일들을 다운로드 할 수 있는 사용자 권한을 지정하는 형태이다. 사용자가 해당 파일의 다운로드를 요청했을때, 테이블의 다운로드 권한을 조회해 해당 파일이나, 데이터의 로드(사용자에게 보내기 위한)을 결정하게 된다.

 

  그런 파일들은 보통 웹경로로는 다운 받을 수 없는 위치에 있고, 어플리케이션은 해당 사용자의 토큰을 통해 권한을 획득한 후, 가져와 메모리 상태에서 사용자 브라우저에 전달하게 되며, 개인정보 파일이나 아주 중요한 파일의 경우는 저장 할때도 암호화하여 저장하여 일반적인 권한과 루트로는 접근을 하지 못하도록 해야 된다. 다만 이 부분은 모든 다운로드 가능 파일의 사용권한을 체크하는 번거로움이 있고, 첫번째나 두번째 경우같이 일반적인 웹 서버의 GET 요청에 의해서 파일을 가져가지 못하므로 서버하 하는 일이 많아 조금 더 힘들어할 거 같은 부분이 있긴 하다.

 

  뭐 여튼 위와 같이 여러 상황과 니즈가 생길 수 있으므로 그때그때 마다 적절히 설계를 하면 될것 같긴하지만, 중요한거는 처음 얘기한 것과 같이 다운로드 문제는 원하지 않는 데이터가 복제되어 나가는 상황이라는 것을 잊지않는 것이다. 해당 이해를 기반으로 다운로드가 필요한 어플리케이션 형태를 고려해 찬찬히 고민해 본다면 적절한 설계를 할 수 있게 될 것 같다.

 

 

 

 

3. 업로드 설계

  이번에는 업로드 동작을 살펴 보도록 하겠다. 마찬가지로 파이썬 flask 로 구현하고, 딱히 추가로 설치할 모듈은 없다.

 

 

3.1 파일 업로드 구현

  구글을 찾아보니 아래 샘플을 이용하면 될 것 같다. 오타를 수정하고, 내용을 적당히 바꾼 소스를 보면 아래와 같다.

 

[Flask – File Uploading - Tutorialspoint 사이트]

https://www.tutorialspoint.com/flask/flask_file_uploading.htm

 

 

  우선 템플릿을 하나 만든다.

1
2
3
4
5
6
7
8
9
<html>
   <body>
      <form action = "http://localhost:5000/upload_process" method = "POST" 
         enctype = "multipart/form-data">
         <input type = "file" name = "upfile" />
         <input type = "submit" value = "파일 업로드" />
      </form>
   </body>
</html>
cs

[upload.html]

 

실습을 하는 c:\python\code 폴더에 templates 폴더를 만들고, 해당 폴더안에 upload.html 이라고 UTF-8 포맷으로 저장한다. c:\python\code\templates\upload.html 파일이 된다.

 

 

  이후 실제 로직을 처리할 flask 코드를 만든다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from flask import Flask, render_template, request
from werkzeug import secure_filename
 
# flask 웹서버를 실행 합니다.
app = Flask(__name__)
 
# 업로드 창을 보여줍니다. 
@app.route('/upload')
def upload():
   return render_template('upload.html')
   
# 실제 업로드 처리를 합니다.
@app.route('/upload_process', methods = ['GET''POST'])
def upload_process():
   if request.method == 'POST':
      f = request.files['upfile']
      f.save(secure_filename(f.filename))
      return '파일 업로드 완료'
 
# 이 웹서버는 127.0.0.1 주소를 가지면 포트 5000번에 동작하며, 에러를 자세히 표시합니다 
if __name__ == "__main__":
    app.run(host='127.0.0.1',port=5000,debug=True)
cs

[flask_upload.py]

 

 

  해당 파일을 c:\python\code 폴더에 "flask_upload.py" 이름으로 UTF-8 인코딩으로 저장 후 실행하면 플라스크 웹 서버가 실행 된다. 

C:\Python\code>python flask_upload.py
....
 * Running on
http://127.0.0.1:5000/ (Press CTRL+C to quit)

 

 

  이후 브라우저에서 http://127.0.0.1:5000/upload 입력하면 아래와 같이 업로드할 파일을 선택하는 창이 뜬다.

 

 

  다른 폴더에 있는 그림 파일을 하나 선택한 후, "파일 업로드" 버튼을 누르면 아래와 같이 업로드가 완료되었다는 화면이 나온다. 플라스크를 실행한 c:\python\code 안을 보면 업로드한 이미지가 올라가 있다. 

 

 

3.2 코드 살펴보기

  마찬가지로 위의 업로드 코드를 살펴 보자. /upload 호출에서 업로드 upload.html 파일을 랜더링해 화면에 보여준다. 해당 HTML 소스를 보면 파일 형태를  보내기 위해 전송방식이 POST 로 되어있고, 파일 업로드시 내용이 깨지지 않게 인코딩을 해달라는 (enctype = "multipart/form-data") 인코딩 방식이 정의되어 있다(앞의 암호화 파트에서 본 Base64 인코딩을 생각하며 된다)

 

  실제 올라온 파일을 처리하는 /upload_process 코드를 보면 POST 로 넘어온 값 중에 이름이 upfile 인 폼 요소 값을 가져와서 파일을 저장하는 객체(f)에 담고, 해당 넘어온 파일의 원래 이름을 f.filename 기준으로 플라스크 실행 폴더 루트에 저장을 한다. secure_filename 이라는 함수는 라이브러리에서 넘어오는 보통 업로드나 다운로드 등에서 많이 얘기되는 ../ 같은 악용문자들을 제거해주는 함수 이다. 위에 다운로드 설명할때 언급했었다.

 

 

3.3 피들러로 살펴보기

  다운로드와 동일하게 업로드 파일이 올라가는 순간의 모습을 피들러로 봐보자. 버튼을 눌렀을때 호출되는 http://localhost:5000/upload_process 호출의 Request 부분에서 Raw 탭을 마찬가지로 보면, 다운로드와 비슷하게, 헤더 쪽에는 데이터의 종류를 알려주는 content-type 이나, 이미지 데이터가 언제 끝나는지 알려주는 boundary 값(하단으로 스크롤 하면 맨 마지막에 저 값이 있다), 폼 요소 이름(name="upfile")이나, 파일 이름(IMG_0071.JPG), 파일의 종류(Content-Type: image/jpeg) 도 있다. 그 밑에 글짜가 깨진거 같이 보이는 이미지 파일이 인코딩된 데이터가 있게 된다.

 

  형태를 보게되면 앞의 다운로드 코드를 본 것과 많이 비슷하며, 전달되는 여러 정보들이 이번에는 브라우저가 아닌, 서버쪽 처리 프로그램을 위한 것이라는 부분만 상이하다고 봐도 될것 같다.

 

 

3.4 업로드 문제 생각해 보기

  결국 업로드도 다운로드와 비슷하게 전달된 데이터의 올바른 처리 문제라고 볼수 있다. 다만 조금 다른 측면은 이번엔 보내는 쪽이 사용자 쪽이므로, 데이터 형태가 비교적 정형화됬던 다운로드와 별개로 적극적으로 공격자의 조작이 가능하다는 것이다.

 

  정상적인 파일의 헤더 같은 부분에 악성 코드를 살짝 숨겨 놓을 수도 있고(관련 해석기의 취약점이 있다면 영향을 줄수도 있다), 리눅스 쉘을 사용하듯이 특정한 명령어를 원격에서 웹 인터페이스를 통해 실행할 수 있게 해주는 웹쉘(WebShell)이라고 불리우는 악성 코드를 업로드 하려고 시도하거나, CMD Injection 취약점 처럼 특정 파서와 만나서 실행되는 코드를 파일내에 삽입하거나, 확장자나, 저장되는 경로를 속이려는 여러 시도를 할 수 있다.

 

 

  업로드는 보통 2가지 정도의 타입으로 분류할 수 있는데, 첫번째는 엑셀이나 텍스트 파일을 업로드해 일괄 처리를 하는 형태가 있고, 두 번째는 아까 얘기한 이미지나, 이력서 파일 등을 서버로 올려서 해당 형태 그대로 저장해 게시물이나, 증빙 목적으로 열람하는 형태가 있다.

 

  첫번째의 경우는 보통 업로드된 파일을 굳이 서버의 특정 경로에 저장해서 일어나는 경우라고 볼 수 있는데 종종 장애 등의 디버깅 상황을 위해서 그런 경우도 있고, 구글 등 검색 엔진에서 노출되는 레거시 코드를 참조해 만들다가 그런 경우도 있는듯 싶다(경험상 예전 프로그램 코드들이 좀 이런 형태가 많은 듯 하다). 요즘은 아예 자바스크립트에서 파싱해서 데이터만 JSON 형태로 보내거나, 서버에서 바로 메모리상에서 처리하고 저장하지 않는 경우도 많아 잘 발생하지 않는 듯도 싶다. 해당 언어에서도 그렇게 가이드 하고 말이다. 

 

  파일을 저장하는 레거시 코드인 경우 같은 경우는 코드 전체를 뜯어고치기 힘들다면, 저장 경로를 웹 서버 바깥으로 저장하고, 처리 후 삭제를 하게하는 것이 보안적이나, 웹서버에 쓸데없는 파일이 남게 되는 것을 방지하는 차원에서도 좋다. 물론 이 경우에도 뜻하지 않던 경로를 가리키도록 조작하여 접근하는 이슈는 남을 수 있어서, 뒤에 얘기할 부분들도 함께 적용을 고려해야만 한다.

 

  두번째의 경우가 보통 웹의 업로드 문제에서 자주 일어나는 부분인데, 보통 웹 형태로 된 게시판 등에서 이미지를 업로드 하거나, PDF 등의 문서 등을 업로드 하는 경우이다. 이 경우 확장자를 체크한다던지, MIME 타입을 체크한다든지, 이름과 경로가 될 인자에서 특수문자를 제거한다든지, 실제 업로드된 이미지 같은 파일이 실제로 그런 종류의 파일인지 체크하는 등 여러 방법을 혼용해서 가이드 하긴 하지만, 개인적으로 가장 안전한 방법은 저장 경로나 이름을 결정할 때 클라이언트 코드를 참고 안 하는 거라고 생각한다.

 

  예를 들어 서버 쪽의 저장 폴더 경로를 고정된 상수나 config 값으로 정해 놓은 후, 파일 이름을 날짜와 해시값, 난수 등을 조합한 서버를 기반한 고유 값으로 생성하고, 검증이 끝난 특정 확장자를 명시적으로(예를 들어 exe = ".jpg" 후) 확장자로 연결해 저장한다면, 외부에서 공격을 하고 싶어도 공격할 수 있는 부분이 안 생기게 된다(이 부분은 클라이언트 코드 편에서도 얘기를 했다). 만약 실제의 포맷이 다른 이상한 파일이 올라가는게 탐탁치 않은 경우는(예로서 .jpg 인데 안의 내용은 스크립트) 서버쪽에서 저장하기 전에 실제 해당 포맷의 파일이 맞는지를 확인하는 메서드를 쓰면 될듯 싶다. 물론 업로드 폴더의 스크립트 실행권한을 제거한다든지, 상황에 따라 업로드 폴더를 웹 경로 바깥으로 뺀다든지 하는 부분도 기본적으로 필요하다.

 

  다만 이러한 설계라해도 100% 안전한다고는 볼수는 없다. 예를 들어 웹서버 파싱 모듈에 취약점이 있어서 스크립트로  내용이 구성된 .jpg 확장자의 파일이, 호출시 스크립트를 실행한다는 경우가 생길 수도 있겠지만, 현실적으로 일어나긴 힘들고 해당 경우는 일어나더라도 해당 해석 모듈의 패치로 해결하는게 정석일 거 같다. 뭐 여튼 버그건 취약점이건 간에 앞에 얘기했듯이 컴퓨터의 모든건 0과 1의 세상이기 때문에 발생할 가능성이 zero 라고는 절대 말하진 못한다. 

 

 

 

 

4. 마무리 하면서

  이번에는 방어코드 보다는 구현코드 위주로 구성한게 조금 걸리긴 하지만, 어차피 업로드, 다운로드 취약점에 대한 가이드 들은 구글을 찾아보면 많이 나오고, 그 세세한 내용 보다는 그 글들이 왜 이런 방어를 하자고 얘기하고, 그 경우 생길 수 있는 blind spot 을 생각해 보자는 의미에서, 동작 구조를 이해할 수 있는 코드를 위주로 설명했다. 사실 공식화 되어 있는 듯한 다른 보안 이슈들도 이런 모호한 측면들이 언제든 현실에서 발생할 수 있다. 그래서 계속적으로 관련된 프로그램의 구조나 환경을 이해하려 해야 되는 것이고 말이다.

 

  또 하나 더 강조하고 싶은건 결국 모든 컴퓨터 내를 구성하는 요소들은 0과 1들을 임의의 로직대로 추상화해 놓은 것이라는 것을 마음속 한 구석에 담아두길 바라는 맘도 있었다. 예전에 파이썬 글에서도 소개했었지만 마음의 여유가 있으신 분들은 아래 책을 꼭 한번 읽어보기를 추천한다. 결국 스스로에게 갇히지 않기 위해서는 다양한 관점에서 같은 대상을 바라보는 훈련도 계속 필요한 듯은 싶다. 

 

CODE 코드 : 하드웨어와 소프트웨어에 숨어 있는 언어(이건 프로그래밍 공부가 의미 없게 느껴지는 분들을 위해서 덤으로~ 불빛 신호에서 컴퓨터가 만들어지기 까지의 과정을 설명한다. 중간에서 길을 잃어 필름이 끊기더라도 읽어볼 가치가 있다고 본다. 비 전공자한테는 생각보다 어려울지도 모르지만, 해당 책의 저자가 비전공자도 읽을 수 있도록 노력해 쓴 책이고 파이썬 공부를 하고자 하는 의지 정도만 있음 가능할 듯 싶다.)

 

  그럼 즐거운 공부되길 바라며, 아직 어떻게 표현하면 좋을지 감이 안잡히는 주제들도 있지만 금년엔 가능한 자주 올려 이 시리즈를 마쳐볼 수 있도록 노력해 보려 한다 --;

 

2019.2.25 by 자유로운설탕
cs

 

posted by 자유로운설탕
2019. 2. 4. 22:31 프로그래밍

  보안글이 어느정도 정리되면(아직 갈길은 좀 멀어보이긴 하지만--;;) 그 동안에 개인적으로 업데이트된 부분들을 기반으로 파이썬 부록 편을 몇 개 작성하고 싶었는데, 마침 그 부분 중 하나에 대해서 콕 찝어 문의해 주신 "독자(닉네임)"님이 있으셔서 간단하게 정리를 해보려고 한다. 물론 구글을 잘 찾으면 있긴 한 내용이지만, 아무래도 처음 하게되면 개념도 잘 안잡히고, 버전이 달라서 헤멨었던 경험이 있어서 한번 더 모아 정리하는 것도 웹 공해는 안될 것 같았기 때문이다. 기본적으로 책 기준인 파이썬 3.6.4 에 IIS 7.5(윈도우즈 10 기준), Apache 2.4 를 기준으로 세팅해 보려고 한다.

 

 

 

 

1. 들어가면서

  플라스크(flask)던 장고(django)든 굳이 IIS, Apache 같은 웹서버와 연결해야 하는 이유는 예제에서 보였던 CMD 창에서 띄우는 방식은 CMD 창이 닫혀 버리면 웹사이트 자체로 사라져 버린 다는 것 때문이다. 특히 사용자가 로그 아웃을 하게 되면 모든 CMD 창은 사라지기 때문에, 다른 사람이 해당 페이지를 원격에서 접속해서 지속적으로 사용할 수 없게 된다. 물론 서비스 형태로 등록해 본다던가 하는 꽁수는 있을 것 같긴한데, 아무래도 정상적인 절차는 예전 플라스크 시간에 얘기했던 WSGI(Web Server Gateway Interface) 방식을 사용해, 사용자 요청은 안정적인 웹 서버에서 전담하고, 로직 처리만 WSGI 를 통해서 파이썬으로 전달해 처리하여 다시 웹 서버를 통해서 사용자 브라우저에 전달해 주는 방식일 것이다. 윈도우즈 쪽에서는 Fast-CGI 라고 보통 얘기하는 듯하고, 아파치 쪽에서는 WSGI 라고 얘기하는 거 같은데, 뭐 두가지 사이에 설계적 차이는 있다고는 하는데, 딱히 선택의 여지도 없으니까 무시해도 될 갭인 것은 같다.

 

[불곰님 블로그 - WSGI, WAS, GgI 이해]

https://brownbears.tistory.com/350

 

 

 

 

2. Windows 10 + IIS 설정 하기

 기본적으로 구글에 "windows 10 IIS flask" 라고 검색하면 몇 개의 링크가 나오는데, 몇 몇 시행착오를 겪어본 결과 가장 현재 시점에 맞춰 간략히 잘 정리된 페이지는 아래의 링크들 같다. 이 블로그의 글은 해당 글들을 여기서 연재했던 샘플에 맞춰 좀더 쉽게 풀어 정리한 버전이라고 봐도 될듯 하다.

 

[github - Python Flask on IIS with wfastcgi]

https://gist.github.com/bparaj/ac8dd5c35a15a7633a268e668f4d2c94 

 

[Medium.com - Deploying Python web app (Flask) in Windows Server (IIS) using FastCGI]

https://medium.com/@bilalbayasut/deploying-python-web-app-flask-in-windows-server-iis-using-fastcgi-6c1873ae0ad8

 

  간단하게 흐름을 설명하자면, 예전 설명글들을 보다보면, 여러가지 IIS  환경 변수를 직접 설정해야했는데(사실 좀 이런 오래된 리눅스 스타일의 하나하나 설정하는 작업은 요즘엔 좀 번거롭게 느껴지는 일 같기는 하다), wfastcgi 는 모듈이 점점 업데이트 되면서 wfastcgi-enable.exe 파일을 같이 제공해서, 해당 작업들을 한꺼번에 자동으로 해준다. 이 후 우리는 적당히 웹서버를 하나 만들고(디폴트 웹서버를 써도 되고), 웹서버에 대한 설정 파일인 web.config 에 연결된 fastcgi 환경과 연결된 파이썬 게이트웨이 파일을 정의하는 작업을 하면 된다. 여기서는 19교시에서 진행한 DB에서 가져온 테이블을 보여주는 "myweb.py" 파일을 기준으로 설명한다.

 

 

2.1 IIS 설치하기

  18교시 레거시웹 편을 참조하여 IIS 를 설치하고 기본적인 동작을 확인 한다. 추가로 하나 더 해야 할 일이 있는데, "Windows 기능 켜기/끄기" 에서 "인터넷 정보 서비스" > "World Wide Web 서비스" > "응용 프로그램 개발 기능" > "CGI" 를 추가로 선택해 설치해 줘야 한다(그래야 fastcgi 기능을 쓸수 있다).

 

 

2.2 관리자 권한으로 cmd 창 실행

  뒤의 설정 파일 때문에 관리자 권한으로 cmd 창을 실행해야 된다. 아니면 권한 에러가 난다.

 

 

2.3 wfastcgi 설치

C:\Python\code>pip install wfastcgi
Collecting wfastcgi
...
Successfully installed wfastcgi-3.0.0

 

 

2.4 설정 자동화 파일 실행 하기

C:\Python\code>wfastcgi-enable
구성 커밋 경로 "MACHINE/WEBROOT/APPHOST"에서 "MACHINE/WEBROOT/APPHOST"의 "system.webServer/fastCgi" 섹션에 구성 변경을 적용했습니다.
"c:\python\python.exe|c:\python\lib\site-packages\wfastcgi.py" can now be used as a FastCGI script processor

 

  이렇게 설정하면 다른 설명 글들에서 얘기하는 여러가지 IIS fastcgi 설정이 자동으로 된다. 

 

 

2.5 IIS 루트 폴더내에 파일 복사하기

  19교시에서 예전에 예제로 만들었던 SQL 서버에서 데이터를 가져와 HTML 테이블로 표시하던 myweb.py 파일과 templetes 폴더안에 있는 myweb.html 파일을 IIS 루트 디렉토리인 c:\inetpub\wwwroot 에 복사한다(관리자 권한으로 해야된다). 아래와 같은 구조가 될것 이다.

1
2
3
4
5
wwwroot/
    myweb.py
    templetes/
        myweb.html
        web.config
cs

 

 

2.6 web.config 수정하기

  이제 IIS 의 사이트별 config 파일인 web.config 파일을 아래와 같이 수정하자. 관리자 권한이 있어야 수정되기 때문에 메모장을 관리자 권한으로 실행해서 해당 파일을 읽어 들여 수정해야 한다.

 

 대충 내용을 보면, 사용자 요청을 처리할 핸들러를 정의하고(<handler> 섹션), 핸들러 메인 파일이나, 루트 패스, 로그 위치 등(<appSettings> 섹션)을 설정한다. WSGI_LOG 폴더를 실습용 폴더인 c:\python\code\ 안으로 잡은거는 기본 로그 폴더인 c:\inetpub\logs 폴더가 기본적으로 IIS 사용자 권한으로 쓰기가 안되서 권한 있는 폴더로 잡은 것이다(좀 이상한건 IIS 로그 폴던데 원래 이런건진 모르겠다--;). 만약 logs 폴더를 사용하고 싶다면 해당 폴더의 속성의 보안 권한에서 IIS_IUSRS 사용자에게 쓰기 권한을 주면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <system.webServer>
        <handlers>
            <remove name="Python27_via_FastCGI" />
            <remove name="Python34_via_FastCGI" />
            <add name="Python FastCGI" path="*" verb="*" modules="FastCgiModule" scriptProcessor="C:\Python\python.exe|C:\Python\Lib\site-packages\wfastcgi.py" resourceType="Unspecified" requireAccess="Script" />
        </handlers>
        <httpErrors errorMode="Detailed" />
    </system.webServer>
 
    <appSettings>
        <!-- Required settings -->
        <add key="WSGI_HANDLER" value="myweb.app" />
        <add key="PYTHONPATH" value="C:\inetpub\wwwroot" />
        <add key="WSGI_LOG" value="C:\python\code\my_app.log" />
    </appSettings>
</configuration>
cs

 

 

2.7 기능 위임 부분 수정 하기

  아마도 이 오류는 windows 10 타입(Home, Pro 나 정책 설정?)에 따라서 다른거 같긴한데, 현재 실습 환경인 Windows 10 Home 에서는 위와 같이 모든 설정을 다하고 "http://localhost/sqltable" 을 실행하게 되면, 아래와 같은 에러가 발생한다.

 

HTTP 오류 500.19 - Internal Server Error
요청된 페이지와 관련된 구성 데이터가 잘못되어 해당 페이지에 액세스할 수 없습니다.

이 구성 섹션은 이 경로에서 사용할 수 없습니다. 섹션이 부모 수준에서 잠겨 있는 경우 이 오류가 발생합니다.

 

  구글을 찾아보면 c:\windows\system32\inetsrv\config\applicationHost.config 에서 핸들러의 겹쳐쓰기(overide) 모드를 allow 로 바꾸라던데, 막상 저 폴더에 가면 해당 파일이 없어 좀 당황을 했다. 조금 더 다른 문서를 찾다보니 이제는 저 파일을 바로 수정하려 할 수는 없고 관리화면에 기능 위임 부분을 수정하라길래, IIS 관리 모듈로 들어가 보니 아래와 같이 왼쪽 트리에서 가장 위의 항목을 선택하면 오른쪽의 관리 섹션에 기능 위임 아이콘이 있다.

 

  해당 아이콘을 더블 클릭해 들어간 후, 아래와 같이 처리기 매핑 항목을 "읽기" 에서 "읽기/쓰기" 로 바꾸어 준다.

 

 

2.8 샘플 실행해 보기

  이제 설정이 완료된 듯 하니 "http://localhost/sqltable" 이라고 치면 아래와 같이 IIS 환경을 통해서 flask 페이지가 나오게 된다(첨엔 많이 신기했음;).

 

 

 

 

3. Windows 10 + Apache 설정하기

  다음으로는 아파치에 설정하는 부분을 똑같이 해보려 한다. 아파치 설정하는 부분은 다른 시리즈 글인 "구글로 공부하는 보안 3교시" 뒤쪽을 보면 PHP 설정하는 곳에 놓여있다. 거길 그대로 따라해서 "It Works!" 샘플페이지 까지 봤다고 가정하고 연결해서 쓴다(PHP 설정도 그대로 해도 된다. 단 현재 설정을 config 파일에 하게되면 php 해석기는 안 돌아가긴 한다)

 

  구글을 찾아보니 아래 2개의 사이트를 주로 참조하면 될듯 하다.

 

[ZEIZ님 블로그 - Windows 10 64bit에서 Apache 64bit에 Flask with Python 3.6 배포]

https://zeiz.com/post/158725706804/windows-10-64bit%EC%97%90%EC%84%9C-apache-64bit%EC%97%90-flask-with

 

[STOREHUBS 사이트]

https://www.storehubs.com/Blog/deploy-python-flask-application-apache-windows-server/

 

 

3.1 mode_wsgi 모듈 설치하기

C:\Python\code>pip install mod_wsgi
Installing collected packages: mod-wsgi

 

  다른 환경에서는 VS 컴파일러 에러가 나서 wheel 파일을 설치하기도 한거 같은데, 아마 실습을 쭉 해왔다면 이미 깔려있어서 괜찮은 것 같다. wheel 파일을 설치해도 되고, VS 컴파일러를 설치해도 되는거 같다. 뭐 일단 잘 되니 패스.

 

 

3.2 mod_wsgi.cp36-win_amd64.pyd 파일을 복사

  mod_wsgi 모듈을 설치하게 되면 파이썬의 모듈 폴더안에 c:\Python\Lib\site-packages\mod_wsgi\server\mod_wsgi.cp36-win_amd64.pyd 파일이 설치되는데, 이게 IIS 에 있던 wfastcgi.py 와 비슷한 역할을 한다고 보면 될듯 하다. 뭐 참조 사이트에 있는 것처럼 직접 해당 경로를 지정하도 되지만 경로가 조금더 단순해 설정이 쉬워 보이길 바라는 맘에서 mod_wsgi.cp36-win_amd64.pyd 파일을 c:\Apache24\modules 에 복사한다. 그 안을 보면 다 .so 확장자 파일만 있는데, 예전엔 mod_wsgi 도 .so 확장자로 지원했다가 요즘은 .pyd 확장자로 지원하는 거 같다(요 부분도 좀 그래서 구글을 참조하다보면 헷깔린다)

 

 

3.3 파이썬 용 폴더 만들고 파일 복사하기

  IIS 와 비슷하게 기존에 세팅한 httpd 폴더에 설정해도 될듯 하나, php 도 나중에 다시 써야하고, flask 웹서비스용 폴더를 한번 분리해 보고 싶어서 c:\test 폴더를 만들고, IIS 때와 마찬가지로 myweb.py 파일과 templetes 폴더안에 있는 myweb.html 파일을 복사한다. 아래와 같은 구조가 될것 이다.

1
2
3
4
test/
    myweb.py
    templetes/
        myweb.html
cs

 

 

 

3.4 WSGI 게이트웨이 파일 호출 하기

  mod_wsgi 설계 구조상 IIS 처럼 해당 파일을 직접 매칭 시키지는 못하는 것 같고, 중계하는 게이트웨이 파일을 하나 만들어야 한다. 설명에는 .wsgi 확장자로 만들긴 하지만 .py 로 만들어도 괜찮길래 "gateway.py" 란 파일로 c:\test 폴더에 만들어 본다. 내용을 보면 application 이란 호칭으로 myweb.py 파일 안에 있는 app 라는 이름의 플라스크 객체를 가져온다. 

1
2
3
4
import sys
 
sys.path.append('c:/test/')
from myweb import app as application
cs

[gateway,py]

 

  폴더 구조를 보면 아래와 같이 하나의 파일이 추가되었다.

1
2
3
4
5
test/
    gateway.py
    myweb.py
    templetes/
        myweb.html
cs

 

 

3.5 config 파일 수정하기

  이제 IIS 에서 햇던 것처럼 이것저것을 수정해야 되는데 결론적으로 아파치 설정 파일인 httpd.config 파일만 수정하면 된다. 해당 파일을 열어 맨 아래에 아래 값들을 추가한다. 여러 글들에 보면 글마다 옵션이 좀 다른데, 하나씩 빼보면서 샘플에서 에러가 안나는 최소 값으로 추려봤다.

 

<httpd.config 에 추가>

# 사용하려는 test 폴더에 모두 허용하는 것으로 설정한다(운영에서는 뭔가 고민할 부분이 있을 수도)

<Directory "c:/test/">
    Require all granted
</Directory>

 

# 복사했던 모듈 파일을 읽어오고

LoadModule wsgi_module modules/mod_wsgi.cp36-win_amd64.pyd
# flask 게이트웨이 파일 위치를 알려준다

WSGIScriptAlias / "c:/test/gateway.py"

 

 

3.6 아파치 재시작

  아파치를 재 시작해야 config 파일이 반영되므로 예전 글에서 설명한 트레이 아이콘 이용해 재시작한다. 뭔가 수정이 잘못됬음 재시작시 에러가 날텐데 해당 경우는 c:\Apache24\logs\error.log 로그를 보고 원인을 해결해야 한다.

 

 

3.7 동작 확인해보기

  "http://localhost:9999/sqltable" 를 호출해보면 IIS 때와 같은 테이블 결과를 볼 수 있다.

 

 

 

 

4. 에러 추적 및 웹서버 구성 시 차이점

  마지막으로 WSGI 방식으로 서버를 구성하면서 CMD 창에서는 잘 돌아가던 코드가 안돌아갔거나 구성 설계를 바꾸거나 해야될 필요가 있던 부분에 대해서 얘기하려 한다.

 

 

4.1 에러 추적

  저렇게 연결했을때 제일 속터지는 상황이, CMD 창에서는 잘 실행이 되는데 fastcgi나 mod_wsgi 를 연결하게 되면 에러가 날때 이다. 에러 원인을 찾아 해결하는 것도 힘든 일이지만 처음 만나게 되면 일단 화면에서 아래와 같이 에러의 상세 내용 없는 500 대 화면만 보게 되기 때문이다. 

Internal Server Error

The server encountered an internal error or misconfiguration and was unable to complete your request.

 

  아파치 같은 경우는 앞에서 얘기한 c:\Apache24\logs\error.log 를 보게 되면 우리가 CMD 창에서 봤던 에러를 똑같이 볼 수 있는 것 같고, IIS 의 경우는 따로 남기는 에러는 없는 것 같아(아시면 댓글 좀_ _;), 아래의 스택오버플로우 글에서 제시한 코드를  app = Flask(__name__) 뒤에 추가해서 500에러 메시지를 보이게 하는게 현재까지로는 제일 난 것 같다.

 

[스택 오버플로 - python flask login form throws 500 error on iis]

https://stackoverflow.com/questions/31804800/python-flask-login-form-throws-500-error-on-iis

1
2
3
4
5
6
7
8
9
10
11
12
app = Flask(__name__)
 
@app.errorhandler(500)
def internal_error(exception):
    app.logger.exception(exception)
    file_handler = RotatingFileHandler('C:\inetpub\wwwroot\logs.log''a'1 * 1024 * 102410)
    file_handler.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'))
    app.logger.setLevel(logging.INFO)
    file_handler.setLevel(logging.INFO)
    app.logger.addHandler(file_handler)
    app.logger.info('microblog startup')
    return render_template('500.html'), 500
cs

 

 

  해당 코드를 넣으면 아래와 같이 나름 상세한 에러가 브라우저 화면에 보이게 된다.

Error occurred: Traceback (most recent call last): File "c:\python\lib\site-packages\flask\app.py", line 2292, ..... , in make_response 'The view function did not return a valid response.

 

 

4.2 CMD 개발과 웹서버 연결시 차이점.

  불행하게도 잘 돌아가던 flask 코드를 IIS 나 Apache 와 연결하게 되면 에러가 발생하는 경우가 종종 생기는 것 같다. 개인적으로 기억 나는 일은 SQLite 를 사용하는 경우와 flask-excel 이라는 엑셀 다운로드 모듈을 사용하는 경우였다. SQLite는 IIS 에서 돌리니 기본적으로 웹서버 계정에 로컬 파일을 쓰는 권한이 없어서 에러가 났고, flask-excel 은 excel 초기화 함수가 if __name__ == "__main__": 안에 정의해 놓으니 WSGI 모듈이 해당 코드를 무시해 버려서 초기화 작업이 안되서 에러가 나게 됬다.

 

  Flask 로 샘플 페이지를 만들다 보면, 웹프로그램이라는걸 깜빡 잊고 일반 파이썬 어플리케이션 처럼 파일도 읽고 쓰면서 개발하게 될 경우가 많은데, 막상 웹 서버로 배포하려 하다보면, 기본적으로 로컬에 있는 파일을 왠만하면 접근 못하도록 막아놓은 웹 서버 설계를 무시하고(보통 보안적으로 이렇게 되는 부분이 안전하기도 하다) 로컬 파일을 읽고 쓰는 방식으로 만드는 게 맞나 하는 생각이 들게 된다. 그래서 SQLite 쓰는 부분을 DB 서버 자체가 권한을 관리하는 일반 SQL 로 변경하거나 하는 일도 필요할 수 있다. 이게 좀 자유롭게 취향대로 쓸 수 있는 플라스크의 조심스러운 측면인것도 같긴하다.

 

  뭐 이런 연결방식에 따른 코드나 설계의 차이는 시행착오를 겪으면서 하나하나 다른 사람들의 경험에도 도움을 받으면서 해결 할 수 밖에는 없을 듯 싶긴하다.

 

 

 

 

5. 마치면서

  뭔가 간략히 정리하려 했는데, 이왕 정리하는거 하다보니 꽤 긴 글이 되긴 했다. IIS 와 아파치 설정 부분이 다른 글에 이미 정리되어 있어서 그나마 다행이라고 생각되는 상황이였다. --; 아마 Flask, WSGI, IIS, Apache 버전이 서로 계속 변하면서 또 상황은 바뀔 수도 있겠지만, 여기서 설명한 개념만 잘 잡고 있다면 그리 많이 변하는 건 없을 거 같긴하다. 그럼 이렇게 파이썬 게릴라 포스팅을 마치려고 한다~

 

 

2019.2.4 by 자유로운설탕
cs

 

 

 

 

 

posted by 자유로운설탕
2018. 11. 11. 20:36 보안

 이번 시간에는 인젝션과 비슷한 무게로 어플리케이션 보안을 이해하는 데 필요한 클라이언트 코드에 대해서 살펴보려고 한다. 개인적으로 인젝션이 "행위"라면, 클라이언트 코드는 그 행위가 일어나는 "환경"이라고 보면 어떨까 싶다. 이 시간에는 웹(이건 좀 자세하게)과 윈도우, 모바일 환경(이건 간단한 예를 들어서)에서 클라이언트 코드가 어떤 범위를 가지고, 어떤 의미를 가지고 있는지 설명해 보려고 한다. 

 

P.S.: 너무 점잖은 말투로 블로그를 적다보니 왠지 블로그 글 같지도 않고, 쓰는데도 부담이 많이 되서 다시 평상어로 적으려고 합니다~ 틀리면 말고 모드로 막 적겠습니다. 앞에 미리 쓴 글들도 여유가 생김 같이 바꾸려 해요. 이해 부탁 드려요. ㅎㅎ

 

 

[목차]

1. 보안을 바라보는 방법

2. 보안에서의 코드 읽기

3. 인젝션(Injection) 살펴보기 #1, #2

4. 암복호화

5. 클라이언트 코드

6. 업로드, 다운로드

7. 스크립트 문제

8. API

9. 설정, 패치, 하드닝

10. 설계 문제

11. 스캐너 vs 수동 테스트

12. 자동화 잡

13. 리버싱과 포렌식

14. 모니터링 문제

15. 악성코드

16. 보안과 데이터

 

 

 

1. 들어가면서

  우선 클라이언트 코드(Client Code)의 의미는 무엇일까? 개인적으로 해당 의미를 "조작 가능한 코드"라고 말하고 싶다. 추가로 해당 범위는 코드 자체만을 가리킨다기 보다는 클라이언트 코드에 영향을 줄 수 있는 모든 행위와 매체(하드웨어 포함)를 포함하는게 맞지 않을까 생각한다. 

 

  예를 들어 아주 예전 코인 오락기에 전기 충격을 주어 코인을 늘리려고 했던 장치나, 게임을 조작하는 매크로 및 기계 장치들 부터 시작해, 로컬에 설치된 SQLite3 파일을 조작한다 거나, 쿠키 수정, 메모리 수정, 패킷 수정, 전통적인 웹 사이트 공격 기법 등이 모두 포함된다. 서버를 침투하려는 여러 시도의 경우도 서버 사이드 측면의 공격이라고 생각할 수도 있지만, 반대로 생각하면 필요없는 서비스를 실수로 오픈한 경우를 빼고는 모두 클라이언트(서버 끼리의 서비스라고 해도 넓은 범위에서는 마찬가지로 외부의 사람이나 프로그램등 클라이언트를 위한 거니까)를 위한 인터페이스를 제공하기 있기 때문에 클라이언트 측면의 공격이라고 우길 수도 있을 것  같다. 사실상 내부로 들어갈 수 있는 인터페이스가 없다면 공격은 불가능 한게 맞을테니까 말이다.

 

  또 비교적 안전하다고 생각하는 모바일 환경의 어플리케이션 등도 탈옥 등의 적절한 상황을 만나게 되면 웹, PC내의 프로그램과 마찬가지로 자유롭게 조작할 수 있다고 보는게 맞을 듯하다(물론 그러한 조작을 할 수 있는 능력이나 툴이 있느냐는 별개의 문제겠지만 말이다).

 

[오락실 딱딱이 - 나무위키]

https://namu.wiki/w/%EC%98%A4%EB%9D%BD%EC%8B%A4%20%EB%94%B1%EB%94%B1%EC%9D%B4

 

[자동사냥 - 나무위키]

https://namu.wiki/w/%EC%9E%90%EB%8F%99%EC%82%AC%EB%83%A5

 

  개인적으로는 이 클라이언트 코드를 이해하느냐의 문제가 어플리케이션 보안 문제의 70% 정도는 차지하지 않을까 하는 중요한 주제라고 생각된다. 이 클라이언트 코드를 제대로 이해하기 위해서, 프로그래밍 언어를 공부하고, 네트워크를 공부하고, 이런저런 프로토콜과, 웹, 컴퓨터 내부의 동작등을 이해하기 위해서 계속 노력해야 되지 않을까 싶다. 

 

   반대 관점에서 서버 사이드 코드는 서버안에 존재하고, 조작이 불가능하게 설계가 보장되어 있는 로직을 의미한다. 이 서버 사이드 코드를 안전하게 설계하기 위해서는 여러가지 노력이 필요하지만, 모순되게도 안전한 설계를 위해서는 클라이언트 코드가 어떤 범위와 의미를 가지느냐를 정확히 이해하고 있어야 하는 것 같다. 물론 외부의 변수를 받지 않는 폐쇄적인 로직을 구현한다면 가능하지도 모르지만, 현대의 프로그램 환경은 외부의 데이터와 연결 없이 프로그램 자체만 돌아가는 경우는 거의 없을 듯하기 때문에 불가능한 희망이라고 볼 수 있다.

 

  그럼 이 글에서 얼마나 해당 부분을 표현할 수 있을지 모르겠지만, 개인적으로 가지고 있는 관점을 이해시키기 위해 노력해 보려 한다. 물론 앞에도 얘기했지만 저는 딱 넘 못하지도 잘하지도 않는 중간 정도 레벨이라고 생각하기 때문에, 더 상위 레벨의 관점을 가진 사람들은 다르게 생각할 수도 있을 것 같지만... 제 블로그니까 뭐 제 맘대로...

 

 

 

 

2. 웹에서의 클라이언트 코드

  보안 공부하는 사람들이 많이들 하는 얘기중 하나가 웹 보안이 보안에 입문하기에 가장 쉽다고 하는 얘기다. 보안 공부를 가이드하는 글들을 찾다보면, 웹 보안을 먼저하고 점점 윈도우나 리눅스나 모바일 같은 보안으로 가는 식으로 얘기가 되서, 웹이 엄청 난이도가 낮은 것처럼 얘기되지만 해당 부분에는 좀 오해가 있다고 생각한다. 사실 웹 쪽 보안이 좀더 쉽게 느껴지는 이유는 기존 사람들의 노력과 표준적인 HTTP 프로토콜, 브라우저라는 제한된 환경 때문이라고 생각한다.

 

  만약 우리가 쿠키도 조작할수 없고, 버프, 피들러, 파로스 같은 좋은 프록시툴도 없고, 브라우저의 개발자 도구 같은 것도 없다면, 웹에 대한 보안의 난이도는 많이 높아질 것이다. 아마 제한된 소수의 사람만이 직접 http 패킷과 브라우저의 메모리 공간을 어렵게 조작하면서 웹 보안에 대한 지식을 독점했을 것 같다. 옛날에 SQL 인젝션 같은 기초적인 취약점들이 웹사이트 들에 엄청 퍼져 있던 것처럼 말이다.

 

  그리고 사실 웹은 서버사이드 코드로 들어가게 되면, SQL 이나 여러 외부 시스템과 맞물리게 된다. 물론 비교적 단순히 돌아가는 시스템도 많긴 한지만, 내부에 무엇이든 만들어 연결할 수 있다는 면에서 일반 어플리케이션과 딱히 틀린 점이 있나 싶은 측면도 있다. 요즘 같이 모든 기술이 점점 연결되어 가고, 데이터를 공유하는 상황에서는 웹에 대한 보안을 이해하기 위해서 서버 깊숙히 숨은 어플리케이션 부터 부터 전달되고 나오는 데이터를 처리하는 부분들에 대해서도 모두 이해해야 될 필요가 있기 때문에, 사실 꽤 오래전부터 해당 경계는 깨어져 버렸다고 생각한다. 그래서 웹에 대한 보안 공부를 하면서 웹에 제한된 관점으로만 공부하는 것은 잘못된 것이라고 생각한다. 어차피 모든 프로그램의 보안 문제는 원리적으로 보면 비슷비슷한 것도 같다.

 

  그래서 여기에서 얘기하는 웹이라는 것은 웹 환경의 전부가 아니라, 브라우저에 제한된 관점에서의 웹만을 얘기한다는 것을 우선 전제하고 싶다.

 

 

 

2.1 웹의 1세대 - 창고

  웹을 세대를 나누는게 맞을까 싶기도 하지만, 웹이 어떻게 진행되었느냐를 설명하기 위해서 개인적인 관점에서 나누자면, 1세대는 문서의 보관소 같은 창고 같은 역활을 하는 정적인 페이지로 이루어진 웹이라고 볼 수 있다.  

 

HTML 은 Hypertext Markup Language 의 약자로 HyperText 는 지금은 너무 평범하게 보이는 웹페이지에서 다른 페이지로 이동할 수 있는 링크를 얘기한다. 예전에 HTML 이 널리 퍼지기전 테스트 파일들만을 가진 환경에서 컴퓨터를 써보지 않았다면 해당 링크 기능이 얼마나 유용한 건지 알기 힘들 수도 있다. 하나의 문서를 보다가 연결된 다른 문서를 보기위해서는 편집기에 여러개의 텍스트 파일을 동시에 띄워놓고, 전체 검색을 통해서 열려진 문서를 왔다갔다 할수 밖에 없었다.  현재 비슷한 상황을 생각해 본다면, IDE 에서 메서드가 정의된 위치를 자동으로 연결해 이동해 찾는 것과, 파일 매니저를 통해 여러개의 소스 파일들을 뒤져서 해당 호출 위치를 찾는 것과 비슷한 차이가 있다.

 

  Markup Language 는 페이지의 구조를 정의 한다는 의미이다. 우리가 HTML 문서 안에서 보는 여러 태그(<html>,<body>,<td>,...)들이 그런 역활을 한다. 하얀 종이에 글을 적어서 의미있는 문장을 만들듯이, 미리 약속된 이런 태그와 속성들을 이용해서, 브라우저는 그림파일과 함께 배치해서 우리가 보는 네이버, 다음 같은 웹 페이지들처럼 예쁘게 화면을 표시한다. 사실 웹은 우리가 미적인 관점으로 화면을 보면서 이해하기 때문에 공감각적 의미가 있는 것이지, 내부적으로는 이런 마크업 언어들이 돌아다니는 세상이라고 보면 될듯 하다. 보안이나 개발에서는 이러한 세상을 주로 보면서 살고있다고 보면 될테고 말이다.

 

  웹의 1세대에에서는 우리가 URL(브라우저의 주소창에 우리가 적는 http://~~ 하는 사이트 주소들)경로를 호출하면 해당 웹사이트를 찾아가 원하는 위치에서 원하는 파일을 가져오는 역활에 제한 되었다. 해당 HTML 로 구성되어있는 문서를 가져다가(물론 꼭 HTML 문서만 있는건 아니고, 텍스트든 그림 파일이든 웹서버 폴더에 있는 파일들을 모두 지정해서 가져올 수 있다), 브라우저 엔진에서 구조적으로 표시해주는 역할을 할 뿐이였다. 이러한 브라우저 엔진이 IE냐 크롬이냐 사파리냐에 따라서 화면에 표시해 주는게 미묘하게 달라서 호환성 문제가 발생했으며, 이것은 다음에 설명할 자바스크립트에서도 마찬가지로 일어나서 XSS 취약점같은 경우 브라우저 종류 및 버전별로 해당 현상이 일어나기도 하고 안 일어나기도 하는 애매한 상황을 가져오기도 한다.

 

 

2.2 웹의 2세대 - 동적 페이지

  웹의 2세대(다시 말하지만 개인적인 분류다^^)에서는 페이지가 움직이고 체계를 잡기 시작했다고 보면 될것 같다.

 

  프로그래밍 코드와 디자인 코드를 분리하기 위해서 디자인 부분만을 담당하는 CSS 란 언어가 나왔고, 자바스크립트가 나와서 사용자 행동에 반응을 하며, 브라우저의 DOM(Document Object Model) 구조를 이용하여, 사용자에 행동에 따라 움직이는 것같이 생각되게 하는 동적인 웹 페이지를 지원하게 되었다. 또한 단순히 페이지 주소만을 호출하던 1세대와는 다르게 클라이언트와 서버사이에 Form 이라는 HTML 요소를 가지고 데이터를 전달(GET-HTTP 헤더, POST-HTTP 바디 내부)하였으며, 서버 쪽도 단순히 원하는 페이지를 찾아 전달해 주는 것을 벗어나 사용자의 입력을 통해 폼으로 전달해진 데이터를 기반으로 CGI(Common Gateway Interface), ASP, JSP 같은 로직을 이용해 처리함으로서 로직을 가지기 시작했다.

 

  그리고 가공된 데이터를 데이터베이스 안에 저장함으로서 1회적인 행동에서 벗어나, 사용자의 여러 행위나 상태등을 저장하여, 지금 우리가 사용하고 있는 여러가지 포털 사이트나, 은행 등의 사이트들이 돌아갈 수 있는 기반 구조가 만들어 졌다고 보면 될 듯 하다.

 

 

  HTTP 메시지는 아래와 같이 크게 헤더와 바디로 나누어져 있는데, 여기에서의 헤더, 바디는 HTML 에서의 문법에서의 헤더, 바디와는 조금은 다른 의미이다.

 

  나중에 피들러로 살펴보겠지만, HTTP Body 부분에 우리가 알고 있는 HTML 소스와(<header>, <body> 를 포함한 모두), 폼 전송시의 Post 데이터가 들어가게 된다(이미지 업로드시 MIME 인코딩된 데이터도 마찬가지이다). HTTP Headers 부분에 들어가 있는 데이터들이 웹 프로그래밍을 공부 하다보면 자주 보게 되는 사용자 에이전트 정보(user agent), 페이지를 어떤 언어로 보여줄것인가 하는 인코딩(encoding), 바로 이전 호출됬던 페이지를 나타내 주는 레퍼러(referer), http only 같은 쿠키 옵션들이 들어가게 된다.

 

 

  그럼 샘플 페이지를 피들러를 이용해 살펴 보면 HTTP 헤더, 바디 부분과 쿠키가 만들어지는 과정을 봐보도록 하자. ASP 코드의 실행 방법은 3교시 글을 참고하면 된다.

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
<%
    ' 폼과 쿠키 값 받기
    strCustomID = request("txtCustomID")
    strLoginUser = Request.Cookies("LoginUserName")
%>
 
 
<html>
   <head>
      <title>쿠키 만들기</title>
   </head>
 
<body>
     <form name = "Custom" method="get" action="MakeCookie.asp">
        <table width = 600>
            <tr>        
                <td width = "60"> 아이디 </td>
                <td width = "100"> <INPUT maxlength="10" name="txtCustomID" size="10" type="text" value=<%=strCustomID%></td>
                <td>&nbsp;<input name=button type=submit value="쿠키 만들기"></td> 
            </tr>      
        </table>   
    </form>      
        <hr>
        <br><b>결과</b><br><br>
        
<%
    ' 1) 전달된 쿠키 표시    
    Response.Write "서버로 들어온 쿠키는 : " & strLoginUser & "<br><br>"
        
 
    ' 2) 등록된 회원일 경우 DB 에 저장하고, 쿠키를 생성함
    Select Case strCustomID
        Case "tom"
            Response.Write("tom 의 쿠키를 만듬"& "<br><br>"
            Response.Cookies("LoginUserName"= "tom"
        Case Else
            Response.Write("잘못된 ID 임"& "<br><br>"
            Response.end
    End Select
%>
 
</body>
</html>
cs

[makecookie.asp]

 

  편의상 하나의 페이지로 만들어 좀 헷깔리겠지만, 코드를 보면 맨 위에서, form 값인 txtCustomID 와 쿠키 값인 LoginUserName 을 받는다. 처음 페이지를 열게 되면 물론 쿠키 값이나 폼 값은 넘어오지 않으니 표시된 내용이 없을 것이다. 이후 코드를 보면 폼안에 txtCustomID 이름을 가진 입력 박스가 있고, 밑의 ASP 코드(VBScript 문법이다)를 보면 전달 되어온 쿠키 값을 표시하고, 아이디가 tom인 경우 LoginUserName 가 tom 인 쿠키를 만들어 낸다.

 

 

  해당 파일을 makecookie.asp 이름으로, c:\inetpub\wwwroot에 복사후, 피들러를 띄우고, 브라우저 주소창에 http://localhost/makecookie.asp 를 입력하여 이동하면, 아래와 같이 아이디를 입력하는 화면이 나오게 된다. 아직 쿠키나, ID가 전달 안 되었기 때문에, 밑에는 빈 칸으로 표시되거나 잘못된 아이디라고 나온다. 

 

 

  이때 피들러 쪽을 보면 아래와 같이 표시된다. 위쪽의 Request 된 항목을 보면 GET 으로 makecookie.asp 를 요청했고, 밑의 Response 된 헤더를 보면 SET-Cookie 라는 명령어와 함께 ASP 에서 디폴트로 만들어 주는 세션 쿠키가 생성된다.

 

 

  이 후 아이디 입력 란에 "tom" 을 넣고, "쿠키 만들기" 버튼을 누른다. 브라우저 주소창을 보면 ? 뒤에 인자로 txtCustomID=tom 이 날라가는게 보이고, 출력 화면을 보면 "tom 의 쿠키를 만듬" 이라는 메시지가 보인다. 서버 응답 쪽에서는 쿠키를 만들어 달라고 클라이언트 브라우저 쪽에 요청을 해서 브라우저는 쿠키를 만든 상태지만, 우리가 "tom" 을 넣고 전송한 과거 시점의 데이터는 아직 해당 쿠키가 없는 상태이기 때문에, 서버에서 인지한 쿠키 내용은 여전히 비어 있다. 

 

 

  피들러 쪽에서 보면, 위 쪽 request 에서는 아까와 조금 다르게 GET 부분에 txtCustomID=tom 이라는 인자가 날라가는 것이 보인다(WebForms 탭에서는 GET, POST 값을 모두 보여 주기 때문에 거기서 봐도 된다). 쿠키 쪽도 보면, 맨 처음 빈 페이지를 호출할 때와 다르게 아까 처음 페이지를 열때 서버 쪽에서 요청되온 ASP 세션 쿠키를 생성해서 날리고 있다. 밑의 response 부분을 보면, 현재 페이지를 만들어서 보여 주고 싶어한 부분이 보이게 되는데, "Set-Cookie" 라는 명령어 뒤에 우리가 ASP 페이지에서 쿠키를 만드는 코드에서 지정했던 "LoginUserName=tom" 이라는 쿠키를 만들어 달라는 명령어가 response 헤더를 통해 전달되게 된다.

 

 

  이 후에 다시 한번 jerry 같은 의미 없는 값을 넣고, 쿠키 만들기 버튼을 누르면, 헤더에는 아까 만들어진 LoginUserName=tom 이라는 쿠키가 전달되게 된다. 여기서는 브라우저 쪽을 보면, 아래와 같이 쿠키가 서버로 날라갔기 때문에, tom 이라는 쿠키를 표시하게 된다(jerry 로 쿠키를 만드는 코드는 없으니 잘못된 ID 라고 나오게 된다)

  

 

  해당 부분을 피들러로 보게 되면 이제 request 되는 쿠키 값에 "LoginUserName=tom" 이라는 항목이 날라가게 된다. 해당 쿠키는 다른 사이트를 호출할 때는 날라가지 않지만(예를 들어 네이버를 호출해 본다든지), 브라우저의 메모리 안에 계속 존재하며 언제든지 http://localhost 사이트를 호출 할때는 헤더에 첨부되어 날라가게 된다. 반대로 얘기하면 브라우저를 끄게 되면 사라지게 된다. 그래서 대부분의 사이트에서 ID/PASSWORD 를 통해 로그인 한 후, 브라우저를 모두 닫아 버리게 되면 해당 사이트에서 브라우저에게 만들게 했던 사용자 신원을 보장하는 쿠키가 날라가 버리게 되어서 로그아웃 되게 되어 버린다(만약 로그아웃이 안되는 사이트가 있다면, 로컬 파일 쿠키를 사용한다든지 하는 바람직하지 않은 경우일 것이다. 다만 모바일 기기의 경우는 같은 로컬이라도 좀더 소유기반으로 안전하게 보기 때문에, 로컬의 안전한 장소에 특정기간 인증코드를 남겨놓는 것은 약간은 별개의 문제이긴 한것 같다)  

 

  이 단순해 보이는 쿠키 생성을 통해, 사이트는 사용자의 신원을 보증하고 로그인 상태에서 사이트를 사용할 수 있게 한다. 정상적인 사이트의 경우는 저렇게 아이디만 던지는 경우는 없을 것이고 보통 아이디, 패스워드 쌍을 던져서, 사이트의 내부 디비에 저장한 아이디와 해시되어 보관된 패스워드 값과 비교한 후 인증이 된 경우만 쿠키를 생성하며, 일반적으로 쿠키 재사용을 방지하고 특정 시점의 만료처리를 할수 있는, 시간 및 기타 값들과 믹스하여 안전한 알고리즘으로으로 암호화 하여 관리한다. 

 

  이후 브라우저는 해당 값을 모든 브라우저가 닫히기 전에는 메모리에 안전하게 보관하면서, 해당 사이트의 페이지를 요청할때 위와 같이 헤더에 포함해 보내게 된다. 서버는 해당 쿠키 값을 받은 후 암호화를 해제해서 안의 사용자의 신원을 나타내는 고유값을 기반으로 사용자가 정상적으로 로그인 되었다고 믿고 작업을 하게 된다. 이 부분이 stateless 라는 http 통신에서 세션을 유지하게 되는 쿠키의 역할이며, 웹 어플리케이션 보안 쪽에서 많이 얘기하는 권한 상승, 세션 탈취 등의 어려운 용어가 저기에서 시작된다고 보면 된다. 해당 부분은 다른 측면도 있긴 하지만 주로 저렇게 인증을 위한 쿠키가 만들어 지는 부분을 파고들어 여러가지로 악용해 보려는 시도라고 봐도 될 듯 하다.

 

 

  앞의 얘기한 부분을 요약 하면 아래의 그림이라고 보면 될듯 하다.

 

 

 

 

2.3 웹의 3세대 - 비동기 호출 페이지(AJAX)

  개인적으로 3세대라고 얘기하고 싶고, 사실 현재 웹이 실질적으로 머무르고 있는 단계라고 생각하는 부분이 비동기 호출 페이지의 등장이다. 비동기 호출 이라는 것은 우리가 앞에서 봤던 것처럼 명시적으로 폼을 전송하는 방식이 아니라, 사용자 입장에서는 멈춰 있는 페이지의 뒷단에서, 자바스크립트 라이브러리인 AJAX 를 통해서 데이터를 주고 받는 것을 이야기 한다.

 

  아마도 2세대까지의 정식으로 HTML 페이지가 요청되고, 받아지는 부분과 상이한 움직이라고 해서, 비동기라고 지칭되긴 했지만, 사실상 엄밀히 따지면 요청하고 받는 행위가 페이지의 뒷단에서 벌어지는 것 뿐이므로 사전적 의미에서의 비동기 라는 말이 적합한가도 싶긴하다. 여하튼 페이지가 멈춰진 상태에서, 보통 사용자의 액션이나 주기적인 스케줄링에 기반해 트리거 되어 API 등을 호출해 데이터만을 교환하며, 가져온 데이터를 화면에 업데이트 하거나, 사용자가 인지 못하는 뒷단에서 광고, 사용성 추적 등을 목적으로 서버로 데이터를 전송하는데 사용하기도 한다.

 

 

  위의 그림에서 보듯 클라이언트와 서버 사이에 데이터의 교환만이 있기 때문에, 2세대와 같이 폼이 왔다갔다 하지 않는다. 아마도 초기에는 스트링 형태로 전송되다가 복잡한 데이터를 구조적으로 전송하기 위하여 XML 을 시용하다가, 점점 실용적으로 프로그램 간에서 교환할 수 있는 JSON 으로 변환되어 왔다고 보며, 현재는 거의 XML 과 JSON 으로 통일되어 데이터가 교환되는 것 같다.

 

  JSON 을 살펴보게 되면 항상 Serialize, Deserialize 라는 낯선 용어가 언급되게 되는데, Serailize 는 "일반 문자열로 만들기", Deserialize 는 "프로그램에서 사용하는 객체 형태의 데이터로 만들기" 정도로 해석하면 어떨까 싶다. 더 궁금하면 파이썬 19, 20교시에서 예제를 보면 된다. 

 

  클라이언트 프로그램 내(자바스크립트)에서 사용하는 딕셔너리나, 리스트 구조를 웹으로 전송하기 위해 {}, 등의 문자로 이루어진 문자열 형태로 압축해서 만들고, 네트워크 상에서는 문자열 형태로 전송 된다. 다시 서버 쪽의 언어에서 해당 값을 받은 후 다시 프로그램내에서 사용하는 자료구조인 리스트나 딕셔너리 구조로 바꾸어 다차원적인 데이터 형태를 무너뜨리지 않고 자연스럽게 데이터를 교환하는 형태이다. 뭐 만화의 한 장면 같은 예를 들자면 냉장고를 접어서 서류봉투안에 넣어 배달하고 다시 꺼내어 펼치면 냉장고가 되는 느낌이라고 할까. 그래서 단순한 교환을 위한 데이터 형태인 XML 보다, 프로그램에서 자주 사용하는 데이터를 유지할 수 있는 JSON 이 인기 인것 같다(물론 XML 도 같은 역활을 해주는 라이브러리가 있지만 두 개의 언어를 써보면 JSON 이 압도적으로 실용적인것 같다. 반대로 얘기하면 데이터 정합성에 엄격한 분야에서는 아직도 XML 을 선호할 것도 같다). 물론 여기서 보안적으로 중요한 사실은 전송되는 형태가 평범한 "문자열" 이라는 것이다.

 

 

  Ajax 에 대한 코드 샘플은 파이썬 글 18교시를 보거나, 뒤의 피들러로 시연하는 샘플을 통해 보면 될것 같고, 여기서는 일상속에서 많이 보이는 예제 하나를 보여주고 마무리 하려 한다. 아래는 구글의 검색어 추천 로직으로, 사용자가 c, a, t 즉 cat 이라고 적을 때마다 하단 박스에 해당되는 추천 검색어가 나오게 된다. 그럼 이 부분을 피들러를 켠 채로 실행시키고 피들러로 잡힌 호출을 보도록 하겠다.   

 

 

  피들러를 보게되면(요즘은 웹 뒤에서 몰래 돌아가는 호출이 많아서, 잡다한 호출 등은 가독성을 위해 정리했다), 아래와 같이 /complete/search 라는 url 이 3번 호출됬다. 상단 request 섹션의 Webforms 탭을 클릭해보면, 사용자가 입력하는 글자는 q 라는 이름을 가진 인자와 매핑되어 있다. 위의 요청 부터 하나하나 클릭해 보면 q 값이 c, ca, cat 로 변해가는 것을 볼 수있다. 하단 response 탭의 JSON 탭을 보면, 피들러가 왔다갔다 하는 데이터를 JSON 형태로 해석한 부분이 보인다.

 

  거기에 보면 트리 형태로(원래는 아까 얘기했듯이 {}, 등으로 구성된 문자열을 피들러가 해석해 트리 모양으로 보여주는 것이다) "cat", "cat<b>egory</b>" 등의 상단 그림에서 봤던 추천 검색어 들이 구성되어 있다, 아마도 구글의  메인 페이지를 찬찬히 뜯어보면 저 /complete/search url 을 AJAX 방식으로 호출하고, 결과 값인 JSON 데이터를 받아, DOM 구조를 이용해 숨켜놨던 네모 박스를 보여주면서, 그 안에 가져온 데이터를 뿌려주는 로직이 있을 것이다.

 

  그러면 평범한 사용자들은 해당 액션을 보고 내부에 이런 왔다갔다 함이 있음을 인지하지 못하고, 내가 입력한 값에 대해서 사이트가 신기하게도 추천 검색어를 뿌려주는 거나 생각을 할 것이다. 다만 보안적인 입장에서는 화면보다는 이 뒷단의 데이터로 주로 봐야하는 것이 다를 뿐이다(그러고 보면 웹은 거대한 눈 속임수 같은 느낌도 있지만 그건 뭐 디지탈화된 영상이나 사진 등도 실상은 마찬가지니...)

 

 

 

 

2.4 클라이언트 vs 서버 코드

  위의 장황한 이야기의 결론은 아래의 그림을 표시하기 위해서였다. 우리는 여러가지 예제를 피들러를 통해 살펴 봄으로써 HTML 코드나, CSS, 자바스크립트 등을 평문으로 볼수 있고(피들러의 Response 섹션의 TextView 탭을 보면 된다), 날라가는 폼 값도 피들러로 볼수 있고(이건 WebForms 탭), HTTP 헤더내에 있는 여러 쿠키나, 사용자 에이전트, referer 등도 볼수 있었다(이건 Headers 탭). 또 한 한발 더 나아가 브라우저의 뒷면에서 남 모르게 날라가는 AJAX 데이터의 입력과 출력도 하나하나 볼수 있었다. 결국 이러한 데이터들을 피들러로 볼수 있다는 얘기는 피들러로 "조작"이 가능하다는 얘기가 된다. 다음 섹션에서 이러한 코드를 하나하나 피들러로 조작하는 예제를 보여주면서 해당 부분을 살펴보려 한다.  

 

  반대로 우리가 피들러로 볼수 없어 조작할 수 없는 코드는 서버 사이드에서 돌아가는 로직 들이다. C++(레거시 CGI 에서 사용), C#, JAVA, PHP 같은 언어로 구성된 코드들은 우리는 내용을 볼 수도 조작할 수도 없다. 하지만 불행하게도 서버 뒷단의 로직은 사용자의 액션에 따라 특정한 로직을 지원하기 위해 생성된 경우가 대부분이기 때문에, 자바스크립트나 HTML 을 잘 분석하거나 폼이나 AJAX 의 입출력 인자를 살펴보거나, 히든 필드를 보거나, 비즈니스 로직을 살펴서 많은 유추가 가능하다.

 

  그리고 서버 뒷단의 로직은 결국 클라이언트에서 날라간 폼 값이나 AJAX 데이터 값을 기반으로 동작하기 때문에 해당 로직을 조작하여, 코드에 영향을 미치려는 시도를 할 수 있다(이게 웹 해킹의 많은 부분을 차지한다고 본다). 마지막으로 서버가 탈취 당했을 때도 공격자의 역량에 따라 서버 사이트 코드에 영향을 미칠 수도 있다(요즘은 빌드된 언어가 많아서, 스크립트 코드보다는 빌드된 형태의 코드를 원하는 데로 조작해야 되서 난이도가 더 높을 듯은 싶다) 

 

 

 

 

 

2.5 피들러로 클라이언트 코드 조작해보기

  그럼 앞에 설명한 클라이언트 예제들을 하나하나 조작하는 것을 시연해 보면서, 각 행위가 웹 어플리케이션의 보안적 측면에서 어떤 의미가 살펴보도록 하자. 앞에 만든 쿠키 페이지에 코드를 추가해 샘플 페이지로 만든 것이 아래와 같다.

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
<script>
function validateForm() {
    var x = document.forms["Custom"]["outMoney"].value;
    var y = document.forms["Custom"]["customID"].value;
    
    if (x > 10000) {
        alert("10000원 초과 출금 안됨");
        return false;
    }
    else if (x == null || x == "") {
        alert("찾으실 금액을 넣어주세요");
        return false;
    }
    
    if (y == null || y == "") {
        alert("아이디를 넣어주세요");
        return false;
    }
    
}
</script>
 
<%
    ' 폼과 쿠키 값 받기
    strCustomID = request("customID")
    strOutPut = request("outMoney")
    strLoginUserID = Request.Cookies("LoginUserID")
%>
 
<html>
   <head>
      <title>인출 하기</title>
   </head>
 
<body>
     <form name = "Custom" method="get" action="GiveMe.asp" onsubmit="return validateForm()">
        <table width = 600>
            <tr>        
                <td width = "60"> 아이디 </td>
                <td width = "100"> <INPUT maxlength="4" name="customID" size="10" type="text" value=<%=strCustomID%></td>
                <td> | </td>
                <td width = "130"> output money </td>
                <td width = "100"> <INPUT maxlength="10" name="outMoney" size="10" type="text" value=<%=strOutPut%></td>
                <td>&nbsp;<input name=button type=submit value="현금 인출하기"></td> 
            </tr>      
        </table>   
    </form>      
    
    <hr>
    <br><b>결과</b>
    <br><br>
        
<%
    ' 1) 전달된 쿠키 표시    
    Response.Write "1. 서버로 들어온 쿠키는 : " & strLoginUserID & "<br><br>"
        
 
    ' 2) 등록된 회원일 경우 쿠키를 생성함
    Select Case strCustomID
        Case "tom"
            Response.Write("2. tom의 쿠키를 만듬"& "<br><br>"
            Response.Cookies("LoginUserID"= "tom"
        Case "lucy"
            Response.Write("2. lucy의 쿠키를 만듬"& "<br><br>"
            Response.Cookies("LoginUserID"= "jerry"
        Case "secret_code"
            Response.Write("2. secret_code의 쿠키를 만듬"& "<br><br>"
            Response.Cookies("LoginUserID"= "secret_code"
        Case Else
            Response.Write("2. 인식 못하는 에러 발생"& "<br><br>"
            Response.end
    End Select
    
    '3) 금액을 인출 해줌.
    Response.Write "3. 인출 금액 : " & strOutPut & " 원" & "<br><br>"
%>
 
</body>
</html>
cs

[GiveMe.asp]

 

  코드를 보면 맨 위에 자바 스크립트로 입력 값이 빈칸인지를 간단히 체크하고, 인출 하려는 금액이 10000원을 초과하면 alert 을 띄우는 체크 코드가 있다. 그 밑에 보면 넘어온 폼 내의 아이디와 금액 값과, LoginUserID 라는 쿠키 값을 받아서 프로그램 내부 변수에 담는다.

 

  그 아래의 HTML 코드는 사용자에게 값을 넣게 하거나, 처리 결과를 보여주게 하는 페이지이다(복잡성을 낮추기 위해서 하나의 페이지에서 입력과 출력을 동시에 하게 했다).

 

  그 다음 VBSCRIPT 코드(ASP 는 해당 VBSCRIPT 문법임)에서 1) 서버에서 인지한 쿠키를 표시하고, 2) 맞는 아이디가(tom, lucy, secret_code)가 들어왔을때 해당 쿠키를 만들어 준다. secret_code는 사실 이 페이지에서는 입력할 수 없는 디버그 아이디로 HTML 코드에 보면 custID 라는 name 속성을 가진 인풋 박스의 maxlength 값이 4로 되어 있어 페이지에서는 4글자 이상이 입력되지 않고 있다. 3) 마지막으로 아이디가 잘 맞는 경우 해당 입력한 금액을 인출해 준다(사실 이 부분도 해당 아이디에 지불할 금액이 있는지 등등의 체크를 하기 위해서 디비를 다녀오고 그래야 하지만 예제의 간결성을 위해서 과감히 생략했다).

 

 

  c:\inetpub\wwwroot 폴더에 GiveMe.asp 라고 저장하고, http://localhost/GiveMe.asp 를 호출해 실제 페이지가 뜬 화면을 보면 아래와 같다.

 

 

 

2.5.1 전송하는 폼 값 바꾸기

  첫 번째로 전송하는 폼 값을 바꿔 보는 예제를 보기로 하자. 현재는 자바스크립트로 막혀있어 10000원을 초과하는 금액을 인출할 수 없는데 1,000,000원을 인출해 보기로 한다. 우선 피들러를 키고 빈 페이지를 호출 한다(http://localhost/GiveMe.asp). 피들러의 숨은 기능 중 하나가 IDE 같이 호출 중간에 디버깅 기능을 걸수 있는데, Burf 나 Paros 같은 프록시 툴에서 하나하나 페이지를 보면서 넘기는 것과 비슷하다(피들러가 동작하는 원리는 파이썬 글 10교시에서 설명했으니 궁금하면 보고 오면 된다)

 

 

  피들러 상단 메뉴에서 "Rules > Automatic Breakpoints > Before Requests" 를 체크 한다.

(이 상태에서는 피들러가 브라우저의 모든 호출을 잡아버리기 때문에 작업을 끝내면 꼭 피들러를 끄거나 해당 값을 "Disabled" 로 바꾸어야 웹 페이지 들이 정상으로 호출된다)

 

 

  이후 GiveMe.asp 페이지에서 lucy 를 입력하고, 1000000 을 입력한 후, "현금 인출하기" 버튼을 클릭 한다. "1000000원 출금 안됨" 이라는 alert이 뜰 것이다. 자바스크립트에서 막은 것이기 때문에 브라우저 프로세스 내에서 일어난 일이며 실제 서버로 요청은 안 간것이기 때문에 피들러에는 아무것도 잡히지 않는다.

 

 

  이제 금액을 1000 으로 조정 한후, "현금 인출하기" 버튼을 클릭한다. 브라우저 화면을 보면 탭 이름 부분의 앞이 빙글 빙글 돌아가는 표시가 보이며 localhost 서버의 응답을 기다리는 중이라는 메시지가 표시된다. 현재 상태는 브라우저가 사용자가 입력한 폼 값을 서버로 전송했지만(여기서는 로컬 서버이긴 하지만), 피들러가 중간에서 잡고 서버한테 전달을 안하면서 땡깡을 부리는 중이라고 볼수 있다.

 

 

  땡깡을 부리고 있는 피들러 쪽으로 가보면 기존에 못보던 "Run to Completion" 이라는 버튼이 오른쪽 상단의 Request 섹션에 생겨 있다. "WebForms" 탭을 선택하면 우리가 입력한 lucy 와 1000 이라는 숫자가 있다. 아래 그림과 같이 outMoney 에 있는 값을 1000000 으로 살포시 바꿔보자. 이후 "Run to Completion" 버튼을 누른다.

 

 

  이렇게 되면 브라우저가 체크해 주던 자바스크립트의 방어 구역을 벗어나 네트워크 상에서 피들러가 해당 값을 수정한 것이기 때문에 서버에는 해당 값이 그대로 전달되어 버리게 된다. 서버의 페이지를 보면 아래와 같이 인출 되지 말아야될 lucy 의 돈 백만원이 인출 되고 만다.

 

  해당 케이스가 아주 간단해 보이지만, 거의 모든 가격 및 사용조작에 영향을 미치는 큰 행위의 원리가 되는 행위이다. 해당 부분은 브라우저의 사용자 인터페이스로 제한된 한계를 넘을 수 있느냐 없느냐를 인지하는 부분이기 때문이다.

 

  예를 들어 주문서 페이지의 HTML 과 자바스크립트 구조를 분석하여 서버로 날라가는 여러 값들을 잘 정리할 수 있다면(이 변수는 가격, 이 변수는 적립금, 이 변수는 쿠폰, 이 변수는 할인 금액, 이 변수는 본인인증 여부 등등) 해당 값들이 왔다갔다하는 적절한 타이밍에 값을 바꿔치기 해주면 원하는 값을 조작할 수도 있다. 신문에 가끔 나는 해커가 몇백만원 짜리 물건을 몇백원에 결제해서 악용하다 감옥에 갔다는 얘기는 사실 이 단순해 보이는 행위에서 시작된다. 물론 페이지를 분석해 관련 로직을 이해하고 원하는 요소들을 찾아내는 능력은 별개로 필요하긴 하지만 말이다. 

 

 

  그럼 이렇게 큰일에 대해서 어떻게 대비를 하냐 얘기한다면, 해결 방법은 클라이언트 코드를 신뢰하지 않음 된다고 얘기하고 싶다. 위의 ASP 코드 중 인출 부분을 방어하는 코드로 바꿔본다면, 아래와 같이 폼으로 넘어온 값을 한번 더 체크하는 수정을 하면 된다.

1
2
3
4
5
6
    '3) 금액을 인출 해줌.
    If strOutPut > 10000 Then
       Response.Write("3. 너무 많은 금액임<br><br>")
    Else     
       Response.Write "3. 인출 금액 : " & strOutPut & " 원" & "<br><br>"
    End If
cs

 

 

  해당 코드를 수정 한후 저장 후, 같은 행위를 하게 되면 아래와 같이 서버에서 너무 많은 금액이라고 막히는 화면이 나온다(확인 후에는 시연을 위해서 다시 예전 코드로 돌리거나 주석을 하자^^. 브레이크 포인트도 disabled 시키는거 잊지 말고..)

 

  이 아주 작은 차이 하나가 사이트를 취약하게 하느냐 취약하지 않게 하느냐의 차이를 만든다는 사실은 조금 우습기도 하지만, 사실 이 차이는 개발자가 클라이언트 코드와 서버 사이드 코드의 차이점을 정확하게 이해하고, 클라이언트 코드를 믿지않고 서버 사이드에 최종 방어 코드를 넣어두었냐는 큰 차이기도 하다. 기능 구현과 성능과 버그에 집중하고 있는 개발자가 보안적인 관점에서 코드와 코드가 돌아가는 시스템 환경을 보는 것은 어느날 쉽게 얻을 수 있는 부분은 아닌 것도 같다.

 

 

 

2.5.2 HTML DOM 구조 바꾸기

  두 번째는 HTML 소스 구조를 바꾸는 부분이다. 이 부분은 개발자 도구를 이용해서, 브라우저 메모리 상에서 바꿔치기 할수도 있지만, 여기서는 피들러를 통해서 네트워크 상에서 바꾸는 예제를 보려한다. 시나리오는 아까 아이디 입력 부분이 maxlength 로 제한되어 4글자 이상 안들어 가고 있는 상황에서, 숨겨진 아이디인 secret_code 를 넣어서 돈을 인출 하게 하는 것이다. 물론 앞에 있던 전송되는 폼 값에서 아이디를 바꿔치는 방법도 있겠지만, 여기서는 다른 측면을 보려고 한다.

 

  이번엔 피들러의 브레이크 포인트를 아까와는 반대로 "Rules > Automatic Breakpoints > After Reponses" 를 선택하여 건다(아까의 아래 메뉴니 굳이 화면은 첨부 안해도 될듯 하다)

 

  피들러가 reponse 를 잡는 부분을 확실히 보기 위해서 브라우저를 모두 종료한 후 다시 띄운 후 빈 창에서 http://localhost/GiveMe.asp 페이지를 호출 한다. 그럼 다시 아래와 같이 페이지가 안 뜨고 완전히 빈 화면만 나오게 된다(브라우저에 따라서 아까 처럼 빙글빙글 도는 표시가 있을 수도 없을 수도 있다). 현재는 브라우저가 서버 쪽에 요청을 한 후, 서버 쪽은 브라우저에게 표시할 페이지에 대한 HTML 을 전달해 줬는데 피들러가 잡고 브라우저에게 아직 안 주고 있는 상황이다.

 

 

  피들러 화면으로 가면 이번에 하단 reponse 쪽에 "Run to Completion" 버튼이 보이고 있다. "TextView" 탭을 클릭하면 브라우저로 전달될 HTML 코드가 보이고 있다(참고로 인코딩이 뭔가 호환이 안되는지 한글은 깨져보인다). 해당 소스에서 name=custumID 인 인풋 박스를 찾아서 maxlength 를 4에서 30으로 바꾸어 본다. 이후 "Run to Completion" 버튼을 누른다(이제 브레이크 포인트는 풀어보자)

 

  이제 브라우저로 돌아가면 피들러가 이제야 전달해준 소스가 보이게 된다(한글이 깨지지만 신경쓰진 말자). 이제 인풋 박스 제한이 30글자가 되어 secret_id 가 입력이 되며, 실제 브라우저 소스 보기를 하면 30으로 조정된 값이 보인다. secret_id를 넣어 전송하는 것은 뻔하니까 굳이 시연하진 않는다.

 

 

  이 부분도 그래서? 라고 생각할 수도 있지만, 서버에서 클라이언트가 사용하기를 원해 전달했던 HTML 소스를 마음대로 변경할 수 있다는 부분에서 큰 의미를 가지게 된다. 그런 일은 없겠지만 마치 XSS 코드가 외부에서 들어온것 처럼 커스텀 코드가 들어갈 수 있으며(물론 자바스크립트 문법은 준수해야 페이지가 깨지지 않고 동작한다). 여러 설정해 놓은 히든 값이나, 숨겨놓은 코드, 더 나아가면 다른 페이지의 코드를 가져다가 붙이는 행위도 할 수 있다(원래는 특정 아이디나 조건에서만 쓸수 있는 결제 화면을 넣는다든지). 이 부분은 공격하는 사람의 상상력에 따라 얼마든 창의적일 수 있으며, 브라우저는 서버가 전달해준 코드라고 생각하기 때문에 XSS 공격같이 방어해주는 일도 없다(설사 방어해줘도 의미는 없겠지만 말이다).

 

 

 

2.5.3 자바 스크립트 조작하기

  이번에는 아까 백만원을 입력할 수 있었던 예제를 자바 스크립트를 조작해서 시연해 보도록 한다. 아까는 request 의 폼인자를 조작했다면 이번엔 response 를 조작해 본다. 3.5.2 와 마찬가지로 피들러의 브레이크 포인트를 "Rules > Automatic Breakpoints > After Reponses" 를 선택하여 건다.

 

  이후 똑같이 브라우저를 모두 끄고, GiveMe.asp 페이지를 호출 한다. 역시 마찬가지로 Response 섹션의 "TextView" 탭으로 가면 아까 ASP 소스내에서 코딩해 놨던 자바 스크립트 코드가 보인다. 해당 코드를 선택해서  함수 이름과 {} 스크립트 뼈대만 남기고 모두 지운다(뼈대를 남기는 이유는 아까도 얘기했지만 문법을 깨뜨리지 않기 위해서다. 문법을 깨뜨리면 브라우저가 동적으로 동작을 안하게 된다. 물론 저 함수를 지우고 HTML 쪽 이벤트를 수정해도 되지만 이게 더 편해 보인다). 이후 "Run to Completion" 버튼을 누르고 1000000 이상의 금액을 넣고 전송하면 자바스크립트 체크로직이 없어져서 전송이 된다.

1
2
3
4
<script>
function validateForm() {
}
</script>
cs

 

 

  여기에서 사실 자바스크립트는 페이지의 일부 이기 때문에 3.5.2 와 같이 당연히 조작 가능하긴 하지만,  보통 프로그램에서 자바스크립트 파일은 따로 떨어져 있는 경우가 많은데 해당 부분도 역시 마찬가지이다. 해당 현상을 이용해서 여러가지 브라우저 단에서 체크하는 검증 로직들을 이론적으로는 모두 우회할 수 있다고 보면 될것 같다(물론 서버쪽에 최종으로 검증하는 설계가 없다는 가정하에지만 말이다). 

 

 

 

2.5.4 쿠키 조작

  이번엔 지금까지 서버가 돈을 지불해야 하는 사람을 판단하는데 사용했던 쿠키 값를 수정해 보려고 한다. 브라우저에서 GiveMe.asp 페이지를 연 후 lucy, 100 을 입력 후, "현금 인출하기" 버튼을 클릭해 쿠키를 생성한다. 이후 그대로 한번 더 "현금 인출하기" 버튼을 클릭해 만들어진 쿠키를 서버로 전송해 쿠키가 만들어진 아래 화면을 확인 한다.

 

 

  그리고 피들러에 가서 3.5.1 에서 했던 것처럼 "Rules > Automatic Breakpoints > Before Requests" 를 체크 한다. 이후 다시 "현금 인출하기" 버튼을 누르면 피들러가 전송되는 요청을 잡게 된다.

 

  피들러 화면으로 가서 상단 request 섹션에서 Headers 탭을 선택하게 되면 HTTP 헤더로 전송되는 여러 데이터를 볼 수 있다. 그중 쿠키 부분의 값을 선택하고, 마우스 오른쪽 버튼을 클릭해서 컨텍스트 메뉴를 띄워 "Edit Header..." 메뉴를 클릭한다.

 

 

  편집 화면이 나오면 Values 부분에서 LoginUserID 를 lucy 에서 tom 으로 바꾼다. 이후 Save 버튼을 눌러 저장한다. 이후 앞에서와 마찬가지로 "Run to Completion" 버튼을 클릭하여 전송한다.

 

 

  이제 브라우저 쪽을 체크해 보면 넘어온 쿠키 값이 tom 으로 바뀌어서, 서버쪽에서 tom 쿠키를 출력해 주게 된다.  

 

  물론 실제 현실에서는 이렇게 쿠키를 평문 상태로 사용하는 경우는 별로 없지만, 암호화 하더라도 해당 값을 바꿔치기 할 수 있다는 사실은 변하지 않는다. 4교시에도 얘기했지만 암호화 하더라도 내부의 값의 고유성은 변경되지 않거나 혹은 1회성 토큰이 아닌 경우는 특정 시간 동안은 유효하기 때문에 때문에 relpay 등에 재사용 되는 경우는 있기 때문이다.

 

 

 

2.5.5 비동기 호출 조작(AJAX, API)

  마지막으로 AJAX 등의 비동기 호출을 조작하는 방법에 대해서 알아보자. 이 부분은 비동기 적인 호출 뿐만 아니라 API 등의 호출도 마찬가지이다(API 는 파이썬 글 10교시, AJAX 는 파이썬 글 18교시에 비교적 자세히 설명되어 있다)

 

  AJAX 예제를 보여주기 위해서 위의 페이지를 조금 수정 하고 getMoneyInfo.asp 라는 AJAX 호출시 호출할 페이지를 추가로 만든다

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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
<script>
function validateForm() {
    var x = document.forms["Custom"]["outMoney"].value;
    var y = document.forms["Custom"]["customID"].value;
    
    if (x > 10000) {
        alert("10000원 초과 출금 안됨");
        return false;
    }
    else if (x == null || x == "") {
        alert("찾으실 금액을 넣어주세요");
        return false;
    }
    
    if (y == null || y == "") {
        alert("아이디를 넣어주세요");
        return false;
    }
    
}
</script>
 
<script>
function showMoneyInfo() {
  var xhttp;
  var loginUserCookie = getCookie('LoginUserID');
 
  xhttp = new XMLHttpRequest();
  xhttp.onreadystatechange = function() {
    if (xhttp.readyState == 4 && xhttp.status == 200) {
      document.getElementById("txtMoneyInfo").innerHTML = xhttp.responseText+"원";
    }
  }
  xhttp.open("GET""getMoneyInfo.asp?myID="+loginUserCookie, true);
  xhttp.send();
}
 
 
function getCookie(name) {
  var value = "; " + document.cookie;
  var parts = value.split("; " + name + "=");
  if (parts.length == 2return parts.pop().split(";").shift();
}
 
 
</script>
 
<%
    ' 폼과 쿠키 값 받기
    strCustomID = request("customID")
    strOutPut = request("outMoney")
    strLoginUserID = Request.Cookies("LoginUserID")
%>
 
<html>
   <head>
      <title>인출 하기</title>
   </head>
 
<body>
     <form name = "Custom" method="get" action="GiveMe_Ajax.asp" onsubmit="return validateForm()">
        <table width = 600>
            <tr>        
                <td width = "60"> 아이디 </td>
                <td width = "100"> <INPUT maxlength="4" name="customID" size="10" type="text" value=<%=strCustomID%></td>
                <td> | </td>
                <td width = "130"> 출금 금액 </td>
                <td width = "100"> <INPUT maxlength="10" name="outMoney" size="10" type="text" value=<%=strOutPut%></td>
                <td>&nbsp;<input name=button type=submit value="현금 인출하기"></td>
            </tr>
            <tr>
            <td colspan=6>
                내 잔고: <input type="button" value="잔고 보기" onclick="showMoneyInfo()">
                : <span id="txtMoneyInfo"></span>
            </td>
        </tr>  
        </table>   
    </form>      
    
    <hr>
    <br><b>결과</b>
    <br><br>
        
<%
    ' 1) 전달된 쿠키 표시    
    Response.Write "1. 서버로 들어온 쿠키는 : " & strLoginUserID & "<br><br>"
        
 
    ' 2) 등록된 회원일 경우 쿠키를 생성함
    Select Case strCustomID
        Case "tom"
            Response.Write("2. tom의 쿠키를 만듬"& "<br><br>"
            Response.Cookies("LoginUserID"= "tom"
        Case "lucy"
            Response.Write("2. lucy의 쿠키를 만듬"& "<br><br>"
            Response.Cookies("LoginUserID"= "lucy"
        Case "secret_code"
            Response.Write("2. secret_code의 쿠키를 만듬"& "<br><br>"
            Response.Cookies("LoginUserID"= "secret_code"
        Case Else
            Response.Write("2. 인식 못하는 에러 발생"& "<br><br>"
            Response.end
    End Select
    
    '3) 금액을 인출 해줌.
    'If strOutPut > 10000 Then
       'Response.Write("3. 너무 많은 금액임<br><br>")
    'Else     
       Response.Write "3. 인출 금액 : " & strOutPut & " 원" & "<br><br>"
    'End If
%>
 
</body>
</html>
cs

[GiveMe_AJAX.asp]

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<%@ Language=VBScript %>
 
 
<%
    ' 폼 값 받기
    customID = request("myID")
    
 
    '등록된 회원을 검색해 잔고를 얻어옴
    Select Case customID
        Case "tom"
            Response.Write("100,000")
        Case "lucy"
            Response.Write("200,000")
        Case "secret"
            Response.Write("100,000,000")
        Case Else
            Response.Write("error")
    End Select
%>
cs

[getMoneyInfo.asp]

 

  기존 자바 스크립트 코드 아래를 보면 showMoneyInfo 라는 새로운 메서드가 만들어져 있으며, 그 안에 보면 특정 쿠키 값을 받고(var loginUserCookie = getCookie('LoginUserID');), 해당 쿠키 값을 AJAX 호출을 통해 myID 라는 인자로 getMoneyInfo.asp 페이지에 전달해  잔고를 받아오며, 해당 응답 값을 받아오는게 성공 한다면 가져온 값을 HTML 소스에 추가한 txtMoneyInfo 라는 span 태그의 값으로 치환한다.

 

 

  그래서 아래와 같이 우리가 잔고보기 버튼을 눌렀을때, showMoneyInfo 메서드를 호출해서 해당 아이디의 잔고를 받아와 화면에 표시하게 된다. 조금 복잡해 보이는 AJAX 코드는 스트레스 받지 말고 그 라이브러리를 설계한 사람이 그렇게 만든거니 그렇게 쓰면 되려니 생각하면 된다.

 

 

  피들러를 켜고 위의 그림과 똑같이 lucy 와 100을 입력하고 현금 인출하기 버튼을 2번(쿠키 값을 페이지에 표시하기 위해 한번 더 눌렀다) 클릭한다. 이후 "잔고 보기" 버튼을 눌러보자. 그럼 페이지가 리프레쉬 되지 않고도 200,000원 이라는 잔고를 가져와 화면에 뿌려주게 된다(참고로 AJAX 호출을 할때, 같은 값이 인자로 넘어가게 되면 브라우저는 실제 호출을 안하고 캐시된 값을 보여주게 된다. 그래서 같은 쿠키 ID 값으로 다시 AJAX 호출이 되게 하고 싶으면 인자에 랜덤 값을 추가로 첨부하거나 브라우저를 껐다 켜면 된다)

 

 

  이 상황에서 tom 의 잔고를 알려면 어떻게 해야할까? 앞의 예제들을 생각해 보면 전송되는 쿠키 값을 바꿔치기 한 후 페이지에 들어와 "잔고 보기" 버튼을 눌러도 될것 같다. 그런데 조금 더 쉽고 직접 적인 방법이 있다. 피들러 화면으로 가면 "잔고 보기" 버튼을 눌렀을때, 우리가 만들어 놓은 로직에 의해 getMoneyInfo.asp 페이지(사실 여기서는 API 같은 역활이다)를 호출한 내역이 있다. request 의 WebFroms 부분을 보면 쿠키 값인 lucy 가 myID 에 담겨 날라가고 있고, response 의 TextView 를 보면 200,000 이라는 숫자가 넘어오는 것이 보인다(잘 시연할 수 있을듯 해서 스크린샷은 생략한다).

 

  그럼 피들러의 다른 기능인 Replay 기능을 이용해 보도록 하겠다. 해당 기능은 기존에 호출한 URL을 다시 똑같이 호출해 주게 되는데, 비단 URL 뿐만 아니라 우리가 앞에서 살펴본 쿠키등이 담겨있는 HTTP 헤더를 포함해 통채로 보내주게 된다. 그래서 만약 인증에 사용되는 쿠키나 토큰이 만료되지만 않았다면, 이전에 실행시켰던 환경과 똑같이 로그인 한 듯이 재생을 하게 해주는 것이다.

 

  추가로 아래 화면 처럼 마우스 오른쪽 버튼을 눌러서 Replay > Reissue and Edit 메뉴를 선택하면 재생 하면서 날라가는 값을 임의로 편집 할 수 있게 해준다 

 

 

  팝업 창이 뜨면 위쪽 request 쪽의 WebForms 탭에서 lucy 를 tom 으로 바꾼다. 이 후 "Run to Completion" 버튼을 누르면 우리가 조작한 값이 getMoneyInfo.asp 페이지로 넘어간다.

 

 

  이후 response 의 TextView 을 보면 tom 의 잔고인 100,000 원이 보이게 된다.

 

  이 사용자에게 숨겨진 AJAX 호출의 문제 중 하나는 개발자 들이 이 호출이 브라우저 상에서 보여지지 되지 않기 때문에, 서버 단에서 호출되는 것으로 착각하는 경우가 있다는 것이다. 그래서 이 호출을 조작 불가능한 호출이라고 생각하는 경우가 많다. 물론 서버 사이에서 서버끼리 API 를 호출하는 경우도 실제  있지만, 해당 경우도 전달되는 인자가 클라이언트에서 전달되는 값의 영향을 받는 다면, 마찬가지로 인젝션에서 자유로워 질수는 없다(결국 클라이언트 코드의 조작도 넓게 보면 인젝션의 일종이라고 볼수 있다)

 

  요즘은 많은 페이지들이 MVC 타입의 패턴을 채용하고 있다(이 부분이 궁금하면 파이썬 19, 20 교시를 보면 된다). 페이지는 화면에 뿌려주는 기능을 주로 맡고, 대부분의 데이터를 API 형태의 다른 서버에 질의해 가져오게 된다. 문제는 그러한 경우가 대부분 AJAX 코드로 이루어져 있고, 피들러로 보게되면 이렇게 잘 보이고 조작이 가능하다는 것이다. 뒷단에서 호출되는 AJAX 페이지나 API 들이 적절한 사용자 권한 체크를 하지 않는다면(예를 들면 이 종류의 데이터를 해당 사용자에게 전달해 줘도 되는가?), 합법적인(?) 경로를 통해서 시스템의 중요 데이터를 웹을 통해 외부에서 가져갈 수 있게 된다. 이런 합법적인 호출은 하루에도 다른 사용자 들에 의해서 수없이 호출되기 때문에, 운이 좋지 않은 이상 이러한 부분을 모니터링 하기도 무척 힘들다. 실제로 이런 사고들이 종종 나고 있고, 앞으로도 종종 날 것으로 예상된다.

 

 보통 일반적으로 개발자나 테스터는 시스템이 올바르게 돌아가는 측면에 대부분의 노력을 쏟기 때문에, 이렇게 페이지 뒤에서 돌아가는 상황에 대한 자세한 고찰을 하지 않는 면이 많다. 하지만 반대로 어플리케이션 보안을 생각하는 사람들은 이렇게 프로그램의 어두컴컴한 뒷골목의 쓰레기통을 뒤져야 하는 경향이 좀 있다. 

 

  수많은 개발자들이 만들어놓은 코드와 페이지를 소수의 사람들이 이렇게 삿삿이 뒤지는 것은 사실 시간이나 리소스상 어려운 일이고 그래서 열심히 여러 부가적인 활동을 통해서(설계 단계에서의 참여라든지, 소스 리뷰라든지, 가이드 라든지, 개발자 교육이라든지) 개발자의 도움을 받으려고 하는 노력들이 있다. 물론 스캐너 같은 자동화 툴도 일부 도움은 되지만 해당 부분에 대한 유용한 점과 한계에 대해서는 뒤의 다른 시간에 찬찬히 살펴보려고 한다. 

 

 

 

2.5.6 모든것에 대한 믹스 및 정리

  위의 시연한 예제를 기반으로 생각해 보면, 우리가 HTTP 통신을 사용할때 사용하는 폼, 히든 필드, HTML 소스, 자바스크립트, 쿠키 및 referer 등의 헤더 값들은 모두 피들러로 볼수 있었고, 결국 피들러로 수정 하여 조작할 수 있다고 볼 수 있다. 반대로 우리가 처음 1000000원 인출을 막았을 때 ASP 내에 넣었던 서버사이드 코드는 클라이언트 코드에서 영향을 미칠 수는 없다(다만 코드 설계에 취약점이 있다면 교묘하게 조작을 통해 우회만 할수 있을 뿐이다).  공격자는 사이트의 로직에 대해서 외부의 비즈니스와 내부의 볼수 있는 코드(스크립트, HTML)를 분석하여 공격 시나리오를 만들 수 있다. 웹 어플리케이션의 불행한 점 중 하나는 대부분의 방어를 위한 내부 로직들이 사용자 에러를 막기위해 자바스크립트 단에도 똑같이 복사되어 있어 내부 로직을 파악하기에 일반 어플리케이션 보다 좀 편하다는 것이다.  

 

  해당 클라이언트 쪽의 조작을 막을 수 있는 방법 중 하나로는 인자 값의 암호화 같은 수단이 있을텐데, 클라이언트 쪽에서 자바스크립트 등을 이용해 암호화 하는 것은 어차피 공격자도 훤히 볼 수 있는 상황이기 때문에 별 소용이 없고(자바스크립트 난독화를 한다고 해도 능숙한 공격자에게는 사실 시간끌기 퀴즈 풀이 정도 밖에 안된다고 본다. 현실에선 항상 히어로가 빌런보다 강한건 아니니까). 서버 쪽에서 암호화를 해서 클라이언트에 주더라도 난수, 시간 등의 더미 값을 넣어 항상 다른 값으로 만들어 재사용이 힘들게 하고, 상황에 따라 적절히 만료 관리를 해줘야 하는 부분이 있다. 실제로 전달되는 값이 암호화 되었다고 안심하다가 돌 맞는 경우도 있으니, 해당 부분은 설계 부분에서 잘 고려해야 한다. 결국은 클라이언트 코드를 조작하는 부분에 대해 최선을 다해 막는것이 맞겠지만, 한편으로는 어느 정도 마음을 비우고 서버사이드 설계와 사용자 액션에 따른 데이터에 기반한 모니터링에 초점을 맞춰야 하는것 같다. 이 보안적으로 안전한 설계 부분도 나중에 이 글의 마지막 시간 쯤에 함 다뤘음 좋겠다.

 

 

 

2.6 클라이언트 코드의 OWASP TOP 10 에서의 의미

  그럼 웹 클라이언트 코드의 마지막으로 지금 것 다뤄온 클라이언트 코드들이 실제 의미가 있는 요소인지를 보기 위해서 OWASP TOP 10 항목을 한번 보도록 하겠다. OWASP 는 자원자들이 모인 커다란 보안 커뮤니티로 2년에 한번 정도 빈도수와 중요도에 따라 세계에서 가장 많이 일어나는 10개의 웹 취약점을 정리해서 발표한다.

 

[OWASP 2017 한글판]

https://www.owasp.org/images/b/bd/OWASP_Top_10-2017-ko.pdf

 

  

  A1인 Injection 은 3교시에서 설명한 것 같이 외부에서 악의적인 코드를 실행하는 인자가 들어와서 이리저리 돌아다니다가 해당 코드를 해석하는 파서와 만나게 되면 실행되어 문제를 일으키는 SQL 인젝션, CMD 인젝션 등등을 얘기하고, A2 Broken Authentication 은 위에서 본 평문 ID 로 계정 관리를 한다든지, 암호화 하지만 만료 정책이 적절하지 않다든지, 패스워드 찾기에 취약점이 있다든지 하는 인증에 대한 설계가 외부에서 봤을때 악용할 수 있는 부분이 있을 경우를 얘기한다.

 

  A3 Sensitive Data Exposure 는 평문으로 중요 데이터를 전송해 공공 네트워크에서 사용했을때 피들러 비슷한 네트워크 툴들로 패킷이 해석되어 내용이 노출된다든지, 와이파이 등에서 암호화 방식이 부실해 내용을 크랙할 수 있다든지 하는 부분이고, A4 XML External Entities 는 XML 코드안에 이상한게 들어가서 문제가 나는 XSS 의 XML 버전이라고 봐도 될듯 하다.

 

  A5 Broken Access Control 은 허용되지 않은 방식으로 API 나 AJAX 를 호출한다거나, 인자를 바꾼다거나, 권한 없는 사용자로 권한 있는 데이터를 얻으려 한다던가 하는 부분이다. A6 Security Misconfiguration 은 9, 10교시에서 다룰게 될 내용으로, 이런저런 웹서버나 서버, 어플리케이션의 세팅 중, 나름 보안에 적절한 수학의 정석 같은 설정 항목들을 잘못 세팅한 경우이다.

 

  A7 Cross-Site Scripting 은 자바스크립트가 들어와 브라우저에게 영향을 주어 원하는 이득을 얻으려 하는 시도로 동기적, 비동기적으로 일어 날수 있으며(저장되어 영구적이냐 인자로 임시적이냐는 측면도 있다), A8 Insecure Deserialization 은 사실 A5의 하위 부분에 포함되어야 할 항목이라고 생각되는데, JSON 을 인자로 받아들이는 프로그램에서 조작에 대한 방비가 안되 있어 당하는 것이라서, AJAX 가 대중화된 요즘 워낙 자주 일어나는 일이라서 좀더 주의를 기울게 하기 위해서 따로 번호를 분리한게 아닐까 싶다.   

 

  A9 Using Components with Known Vulnerabilities 는 취약점이 있는 컴포넌트를 사용하는 것으로 네이버, 제로보드 같은 웹 게시판의 취약 버전을 패치 안하고 사용한다든지 취약할 가능성이 있는 오픈 소스를 사용하는 것인데, 사실 최신 버전을 사용해도 취약점이 없다는 보장을 100% 할순 없고(모든 최신은 또 조금만 지나면 과거기도 하고, 기능 변경등에 의해서 새로운 취약점이 나올 수도 있고 하니), 요즘 오픈 소스들은 사실 워낙 많은 라이브러리들을 공유해 사용하는 거 같아서, 하나의 핵심적인 모듈만 걸리면 우르르 걸리는 문제가 되는 부분이라서 참 애매한 것 같다. 여튼 상용으로 오랫동안 많은 회사에서 사용되거나, 소스가 공개되어 사람들이 많이 사용하고, 유지보수 하고, 안정화 된 오픈 소스를 사용하는 것이 최대한 현실적인 관점일 것 같다. 뭐 능력이 된다면 전체적으로 소스를 검토하고 쓰면 더 좋을 것 같고 말이다.

 

  A10 Insuffient Loging&Monitoring 은 2017년에 새롭게 나온 항목(내용이 새롭다긴 보다 항목이)으로 보안 팀들이 점점 데이터의 분석에 관심을 많이 기울이고 있다는 것을 증명하는 것 같다. 사용성과 보안은 반비례 한다고 하던 과거의 관점에서, 점점 두 가지를 양립시켜야 살아남을 수 있다는 관점의 변화와, 점점 살펴봐야 할 시스템 들의 다양함에 따른 복잡성과 상이한 데이터가 늘어나고 있는 상황에서는 어쩔 수 없는 부분인 것 같다. 하지만 여전히 개발자 입장에서 보안성에 필요한 데이터라는 주제는 피부에 닿기 힘든 부분이며(보통 개발자가 모니터링에 필요한 데이터를 개발 때 명시적으로 저장해 줘야 쉽게 가져갈 수 있지만, 기능에 필요없는 데이터가 저장되긴 참 힘든 것 같다), 정말 어떤 데이터가 모니터링에 필수적인 데이터인지를 찾아내는 작업도 쉽진 않기 때문에 꽤 어려운 주제 같긴 하다.

 

 

  음 이렇게 보면, 세계에서 제일 중요하다고 하는 10개의 취약점 중 앞에 별이 표시된 7개의 주제가 앞에서 설명한 클라이언트 코드에 대한 이슈라고 볼 수 있다. 그래서 앞에 설명한 간단한 예제들이 꽤 보안적으로 중요한 개념들이라고 말하고 싶다. 물론 자꾸 반복해 말하지만 해당 클라이언트 코드에 대해 제대로 이해하려면 시스템, 프로그램, 네트워크를 데이터의 흐름 관점에서 잘 이해해야 하고, 그 부분을 기반으로 새로 개발에 사용되는 주요한 시스템들을 계속 배워나가며 한다고 생각한다. 게다가 이 부분은 PC 나 모바일 코드로 넘어가게 되면 또 다른 측면의 이해가 필요하게 된다. 하지만 웹이든 OS 환경이든 클라이언트 코드라는 측면에서는 두 개의 타입과 원리는 비슷하다고도 말하고 싶다(물론 관련 기술을 이해할수 있다면 말이다). 그래서 보안 분야를 제대로 이해하고 싶다면 시스템과 데이터를 이해하려고 끝없이 노력해야 하는것 같다(물론 기술 이외에도 가끔 더 현실로 느껴지는 프로세스나 법률적, 관리적 측면에 대한 이해도 균형이 맞춰져야 하겠지만 말이다)

 

 

 

 

3. 모바일, PC 에서의 클라이언트 코드

  이 부분은 간단히 하나의 현실 예제를 보여주면서, 해당 영역에서의 클라이언트 코드의 의미와 가능한 방어 전략에 대해서 생각해 보려고 한다.

 

  앞에서 얘기한 것과 같이 이러한 일반 어플리케이션 레벨에서의 조작은 웹에 비해서 진입 장벽은 높은 것 같다. 일단 HTTP 같이 표준되는 규약으로 투명하게 데이터 흐름이 이루어진 것도 아니고, 피들러 같이 쉽게 특정 주제를 간단히 조작할 수 있는 툴은 드물다. PC 는 거의 디버거나 헥사 에디터의 도움을 통해서 분석 및 조작해야 하는듯 하고, 모바일은 디컴파일러나(보통 공통 가상 환경에서 돌아가는 코드들이 이 부분이 대부분 가능한 것 같다) 마찬가지로 디버거를 사용해서 코드를 해석하거나 조작 해야하는 것 같다. 그래서 기본적으로 어셈블리나 해당 저수준 API 레벨에서의 프로그래밍 지식에 익숙해져 있지 않으면, 관련 툴이 있어도손가락만 빨고 있을 수 밖에 있다. 또 프로그램들은 그러한 분석을 어렵게 하기위해 여러 디버깅 방지 로직이나, 난독화등을 적용하고 있다. 또한 플랫폼에 따라 OS 나 언어도 다양하기도 하지만, 무언가를 자세히 분석하는데에 그렇게 호의적인 환경을 제공하는 것 같지도 않다.

 

  또한 해당 프로그램 및 시스템의 지식은 낮은 레벨의 동작 분석에 집중되어 있기 때문에 파이썬 같은 다방면으로 쓸수 있는 언어보다 일반적인 관점에서 투자한 시간에 비해 유용성이 떨어지기 때문에, 해당 분야의 잡을 가지기 원하는 사람이 아닌 이상 시간을 많이 투여하기가 망설여지는 측면도 있다. 하지만 잘 경험해 놓는다면 남들이 가지지 못한 독특한 다른 시각을 하나 보유하게 되는 것은 부정하기 힘들 듯 싶다.

 

 

3.1 Flex2 살펴보기

  그럼 본론으로 들어가자. 일반적으로 모바일 어플 조작 방지를 위한 가이드를 보면 탈옥, 루팅시 설치되는 파일의 위치를 체크, 시스템 함수를 호출해 사용자의 권한을 체크, 바이너리 해시 크기를 체크, 프로세스를 체크하는 등 공식화 된 방어 방법을 권고 한다. 실제로 은행 등의 중요한 앱들은 이러한 부분들을 의무적으로 적용하고 있다. 여기에서는 해당 방식의 방어 로직들이 정교한 레벨의 클라이언트 코드 방어이긴 하지만, 공격자 입장에서는 완벽한 방어는 아니라는 부분을 주장해 보려 한다.

 

  아래의 어플은 flex2 라는 IOS 탈옥을 했을 경우 시디아라는 탈옥용 웹스토어 같은데서 구입할 수 있는 제한된 어플이다. 현재는 3버전이 나온 것으로 안다. 해당 어플은 탈옥 환경에서만 돌아가며 실행 중인 프로그램이 사용하고 있는 메서드 들을 열람할 수 있으며, 원하는 메서드를 선택 하여 지정한 값을 항상 반환하게 할 수 있다. 

 

  밑의 어플은 중국에서 서비스하는 스트리밍 음악을 들을 수 있는 무료 음원 어플이다. 아직 중국은 음원에 대한 무료 어플들이 일부 있지만 점점 저작권이 강해지는 추세라서(아마 글로벌 기업들이 중국 시장의 미래 수익를 고려하여 중국 정부와 협의해 특정 기간의 유예 특례를 준듯하다), 중국 내부의 IP 에서는 들을 수 있지만 그 외의 IP 에서는 재생을 막는 제약이 걸려있다. 아래 항목은 누군가 해당 부분의 코드 실행을 회피하게 만들어 flex2 에서 바로 다운 받을 수 있는 클라우드 환경에 올려놓은 회피 코드를 다운 받은 것이다. 

 

 

  해당 부분을 클릭하여 들어가게 되면 프로그램 내부에서 사용하는 중국내 IP 체크에 사용하는 여러 메서드 들이 보이게 된다. 물론 해당 패치 파일을 올린 사람은 실제로 디버거나 디컴파일러로 해당 프로그램을 분석해서, IP 체크를 수행한다고 판단되는 메서드 들을 한땀 한땀(이 표현이 맞지 않을까--;) 정리했을 가능성이 높다.  

 

 

  그 중 하나의 항목인 isChinaUser 항목을 클릭해 들어가 보면 "Return Value" 값에 1을 강제로 리턴하도록 설정 하고 있다. 해당 패치를 활성화 한 상태에서 조작하려는 특정 음악 어플을 실행하면 실행 시점에서 중국내 IP 를 체크하는 몇개의 메서드들의 반환 값이 강제되어 버려, 중국 외부에서도 중국 내부 처럼 음악을 들을 수 있게 된다(보안 교육 목적이지만 블로그에 올리기엔 불법적인 사항이라 판단하여 가능한 블라인드 처리를 많이 했다)

 

  실제로 이 어플을 이용해서 은행의 탈옥 감지등을 우회하는 예제도 종종 공유되고 있다. 우회 원리는 위와 마찬가지로 아까 얘기한 탈옥 경로 체크, 프로세스 체크 등의 여러 방어 기법들이 결국은 사용자 스마트폰에 설치되어 있는 모바일 바이너리 안에서 특정 API 를 호출하는 메서드로 구현되기 때문에, 해당 메서드를 정확히 분석하여 탈옥이 안됬다는 거짓된 값만을 리턴한다면 가능한 듯 싶다(제가 이 부분을 잘못 이해하고 있다면 누군가 잘 아시는 분이 교정 댓글 좀 부탁드려요^^) 

 

 

  물론 앞에서 얘기했듯이 해당 코드를 발견하는 부분은 어플리케이션의 난독화, 새로운 보안적 개발언어, 디버그 탐지 코드 등으로 더 어려워 질수는 있지만, 해당 트릭을 풀수 있는 능력을 가진 사람이 관심을 가진다면(금전적 이익을 얻을 수 있는 앱이라면 더 더욱 가능성이 높다고 본다), 파이썬 21교시에서 얘기한것 처럼 그러한 개인의 지식은 소프트웨어나 데이터 형태로 구현되어 일반적인 사람이 쉽게 사용할 수 있는 형태로 제공 될 수도 있다.

 

  결국 방어하는 입장에서는 강력해 보이는 바이너리 기반의 방어 방법이, 환경적 한계로 클라이언트 상의 방어일수 밖에 없기 때문에(OS 단에서 제공해 주는 보안 기능도 외부 공격자에게는 강력하지만, 내부 시스템 어드민 권한을 가진 악의적인 사용자의 조작에는 생각보다 취약할 수 있을 것 같다), 해당 방어 부분에 100% 기대지 말고, 프로그램이 탈옥, 루팅이 되어 많은 부분을 회피가능하다고 가정하고, 해당 상황에서도 최대한 방어할 수 있는 서버 연동 기반의 설계를 하는게 맞을 듯 싶다. 

 

  반면에 사용자 인증 측면에서는 한번 소유기반의 기반한 안정성에 이렇게 의심을 가지게 되면, 많은 민감한 부분들이 과도하게 위험하게 느껴지는 모순이 생기는 듯 싶기도 하다. 하지만 선의의 사용자와 악의의 사용자가 같은 무기를 지닐 수 있을 때는 악의의 사용자에게 촛점을 맞추는게 방어자 입장에서는 맞을 것은 같다. 이런 모호한 상황때문에 OWASP 같은 데서도 점점 데이터에 기반한 모니터링을 강조하는지도 모르겠다. 

 

  또한 웹 쪽과 비교하자면 서버 역할과 비슷한 부분은 OS 환경인것 같다. 백신이 상위권한을 가지고 엑셀을 열기전에 가로채 파일을 검사하는 것과 마찬가지로, 언제든지 OS의 취약점이 있어 어플리케이션의 입출력이 악용되거나 중간 통신을 가로챌 수 있기 때문에 클라이언트 코드 측면의 방어를 너무 믿으면 안 된다고 생각한다. 또한 현재의 많은 어플리케이션의 경우 껍데기는 해당 OS 의 프레임워크를 사용하지만, 백단에서는 실제로는 외부 API 서버와 HTTP 기반의 통신을 하는 경우가 많기 때문에, 피들러 같은 툴을 프록시 서버로 설정해 전송되는 데이터를 살펴보거나 조작할 수 있다. 이런 타입의 어플리케이션의 경우는 웹 프로그램의 관점에서 좀더 깊게 체크 하는 것이 더 맞을 듯 싶다.

 

  앞 부분에서 flex2 어플을 샘플로 보여주며 설명한 행위를 실제 코드 관점에서 설명하고 있는 글이 아래 블로그 글 일듯 싶다.

[IOS Hooking#2(Frida) - Bach's Bolg]

http://bachs.tistory.com/entry/iOS-Hooking2Frida?category=892887

 

 

 

 

4. 마무리 하면서

  여기 까지 오면서 웹 코드로 예시를 들면서, 클라이언트 코드에 대해 개인적으로 가지고 있는 생각을 전달하려 했는데, 얼마나 공감을 얻었을지 모르겠다. 개인적으로는 웹이나 어플리케이션이나 기술의 결이 다를 뿐이지 프로그래밍 보안이란 관점에서는 그리 큰 차이는 없는 듯 싶다. 모든 걸 깊게 잘 알고 있는 건 아니기 때문에 이런 종류의 의견엔 크게  자신은 없기만, 공부를 시작하는 사람들 한테는 이런 부분에 대한 소개도 의미가 있진 않을까 싶어서 글로 남겨 본다.

 

  개인적으로 점점 시간이 지날 수록 보안 뿐만 아니라 IT 전체에 있는 많은 분야에서 하는 일들이  결국은 다른 관점에서 시스템과 데이터를 이해하는 본질적으로는 같은 업무가 아닌가 하는 생각이 들곤 한다. 그 덕분에 왠지 알고 있던 사실들이 갑자기 무의미 해지기도 하고, 쉽게 생각했던 분야들이 어렵게 느껴지거나 그 반대의 일들도 종종 일어나고 있다.

 

 

2018.12.16 by 자유로운설탕
cs

 

  

 

posted by 자유로운설탕
2018. 7. 15. 20:18 보안

  이 시간에는 보안에서는 거의 필수라고 할수 있는 몇 가지 타입의 암호화와 복호화에 대한 얘기를 해보려고 한다. 다만 RSA 나 AES 와 같은 암호화 방식에 대한 수학적 전개에 대한 얘기를 하려는 것은 아니고(해당 부분은 많은 책이나 인터넷에서 자세히 설명하고 있다. 쉽게 이해가 잘 안되서 문제긴 하지만...^^), 보안 및 IT 쪽에서 암복호화가 어떤 의미로 쓰이고 있는지와 이해가 필요한 미묘한 점에 대해서 아는 범위 내에서 얘기해 보려 한다.

 

 

[목차]

1. 보안을 바라보는 방법

2. 보안에서의 코드 읽기

3. 인젝션(Injection) 살펴보기

4. 암복호화

5. 클라이언트 코드

6. 업로드, 다운로드

7. 스크립트 문제

8. API

9. 설정, 패치, 하드닝

10. 설계 문제

11. 스캐너 vs 수동 테스트

12. 자동화 잡

13. 리버싱과 포렌식

14. 모니터링 문제

15. 악성코드

16. 보안과 데이터

 

 

1. 들어가면서

  사실 암호화 라는 것은 현실과 비교하자면 우리가 주민등록증을 가지고 다닐 때, 남들이 다 볼 수 있도록 손바닥위에 놓고 다니지 않고, 지갑에 넣은후 주머니에 넣거나 가방에 넣어 가지고 다니는 것과 비슷하다고 볼 수 있다. 보통의 사람들 한테는(뭐 초능력자나 어벤저스가 있다면 모르겠지만) 그것을 투시할 수 있는 능력이 없기 때문에  남에게 보이지 않고 안전하게 다닐 수 있게 된다. 

 

  과거의 암호들은 셜록홈즈 소설에서 나오는 그림 암호 같은 형태로 숨겨진 특정 규칙에 의해서 문자를 뒤섞거나 매칭되어 있기 때무네, 그 숨겨진 규칙이 암호화 방식 차체가 되는 방식이였지만, 현대에서는 수학적 도메인을 이용하여 관련 계산식은 모두 공개하고, 몇 몇 변수 값을 감추어 어렵게 풀어야 하는 수학적 문제로 바뀌게 되었다. 그렇게 된 이유 중의 하나는 아마도 우리가 컴퓨터에 저장한 데이터 들이 결국은 2진수의 형태로 저장되어 있기 때문일 것 같다. 해당 저장된 수를 적당히 잘라서(블록화) 뒤섞거나 암호키인 숫자와 연산하여 의미가 없어 보이는 다른 숫자의 형태로 만들 수 있게 되었기 때문이라고 생각한다(인간이 인지할 수 있는 텍스트나 실행이 되는 exe 파일들도 디스크 상의 저장된 형태를 기반으로는 무척 큰 2진수나 블록으로 잘라진 작은 2진수 묶음 들에 불과하다). 결국 그러한 숫자를 뒤섞는 수학적 로직들이 AES, DES, RSA, ECC, SHA(해시) 등의 이름으로 암호화 알고리즘 이라고 불리우게 된다(머신러닝에서 통계적 기법들이 각각 하나의 머신러닝 알고리즘을 차지하는 거랑 비슷한 듯도 싶다). 

 

 

[셜록홈즈 춤추는 인형 - Sherlock님 블로그]

https://m.blog.naver.com/thdgnstjs/50077677206

 

 

  뭐 개인적으로 나눠본다면 암호를 1세대, 2세대, 3세대, 특수 형태, 제3자 인증 형태의 5가지로 나눠보고 싶다.

 

   1세대는 위의 셜록홈즈의 그림 암호같이 다른 매칭되는 매체들(예를 들어 이미지)과 연관되서나, 문자나 비트 등을 특정한 규칙에 의해 교환하거나 뒤 섞어 만드는 방식을 얘기한다. 예를 들면 프로그래밍에서 많이 쓰이는 Base64 인코딩이나, HTML 인코딩, URL 인코딩 등이 그러한 타입이라고 볼수 있다.

 

  2세대는 엑셀이나 zip 파일에 암호를 걸때 "1234" 를 입력해 암호를 걸었다면, 해당 문서나 zip 파일을 열때 같은 암호인 "1234" 를 넣어야 되는 것과 마찬가지인 타입이다. DES 나 AES 같은 대칭키 암호화 알고리즘이 이런한 타입이라고 볼수 있다.

 

  3세대는 우리가 계약서에 날인 할때 인감 도장을 두 개의 나눠가질 계약서 사이에 찍는 간인(間印)과 비슷하다고 봐도 될듯 하다. 한 쪽 계약서에 찍힌 반쪽의 도장은 다른 한쪽의 계약서에 찍힌 도장과 매칭이 되어, 두 개의 계약서가 서로 유효하다는 것을 증명하게 된다. 보통 양쪽 당사자의 도장을 다 찍기 때문에 상호 증명이 된다. 이러한 쌍이되는 키를 수학적으로 구현한 것이 RSA 나 ECC 라고 불리우는 공개키, 개인키를 가지는 비대칭키 암호화 알고리즘이다. 아래와 같은 증표의 나눠 가지는 것도 비슷하다고 봐도 될 것 같다.

 

[메데이아와 테세우스, 배신하는 딸들의 운명 - 윤단우. 그녀의 시선 블로그]

http://blog.yes24.com/blog/blogMain.aspx?blogid=iconblue&artSeqNo=6998225

 

  특수 형태는 개인적으로 프로그래밍을 공부하다 처음 접했을때 이해하기 힘들었던 해시 함수 이다. 해시 함수는 비유 하자면 사람이 지닌 지문이나 홍채와 비슷하다. 지문 또는 홍채가 해당 개인 자체는 아니지만, 개인만의 고유한 특징을 소유하여 한 개인의 신원을 증명을 할 수 있는 것 처럼 컴퓨터에 저장된 특정한 문장, 파일, 숫자에 대해서(앞에서도 얘기했지만 내부적으로는 결국 숫자로 이루어져 있기 때문에) 수학적 연산을 통해서 일정한 길이를 가진 고유의 숫자로 함축해서 표현하게 된다. 최종적으로 만들어진 것은 우리가 발급받은 주민번호와 같아서 정말 아주 특별한 경우가 아니라면 현실적으로는 겹치지 않는다(해당 부분은 홍채나 지문도 마찬가지이다). 실제 우리가 입력한 홍채, 지문, 패스워드 등의 정보는 숫자 형태로 변환한 후(디지털화 라고 한다), 해시함수를 통해서 고유 값으로 변환하여 데이터베이스나 OS에서 관리하는 비밀 장소에 저장하게 된다.

 

  마지막으로 제3자 인증 형태는 일반적인 관점에서는 암호화의 한 종류라고는 할수 없지만, 암호화의 쓰임과 등가거나 밀접히 연결되어 있기 때문에 여기서 연관하여 같이 설명하려고 한다. 제3자 인증 형태는 신원의 증명을 위해서 외부의 기관이나, 장치, 서버 등을 신뢰하는 것을 말한다. 현실로 따지면 계약시 동사무소에서 발급 받은 인감 증명서에 의해 계약이 효력이 생기는 것 같이, 신뢰할 수 있는(물론 해당 부분은 국가와 비슷하게 내부의 구성원들이 약속이나 이익이나, 법률등에 기반해 신뢰하는 것과 마찬가지로 절대적인 것은 아니다. 다만 믿지 않으면 현실적으로 개인이 살아갈 수 없거나, 많은 사람이 동의 하는 것이기 때문에 믿는 것이라고 볼수 있다. 우리가 현실의 돈이나 게임 내의 아이템, 비트코인 등을 교환 가능한 가치로 인정하듯이 말이다).

 

  이러한 부분들은 응용적인 측면이 강하기 때문에 앞의 4가지 타입의 암호화 와도 같이 맞물려 사용된다. 공개키-개인키를 이용한 전자서명의 기반이 되는 인증서의 존재를 특정한 상위 기관이 증명한다거나 하는 일도 벌어지고, 휴대폰, ARS 인증같은 소유기반의 인증이나, OTP 와 같은 소유와 서버 인증이 동시에 이루어지는 경우도 있고, 패스워드나 토큰 기반의 인증도 마찬가지로 서버 기반의 인증이라고 볼수 있다. 꼭 서버가 아니라더라도 모바일 폰에서 특정 횟수의 패스워드의 틀림이 있을때, 폰을 잠가버리는 행위도 제3자 인증 형태가 일부 차용된 것이라고 볼수 있을 것 같다.

 

  왜 암호화에 대한 얘기에 이런 제3자 인증을 끼워넣느냐고 생각할 수도 있지만, 암호화 라는 것은 키 측면에서 보면 해당 키를 가진 사람만이 암호화된 정보에 접근할 수 있는 인증의 개념도 포함되고 있기 때문이다. 마치 바위에 박혀있는 엑스칼리버를 아서왕 만이 뽑을 수 있는 것처럼 말이다. 여러 인증에서 사용하는 서버 쪽 만이 알고 있는 특별한 검증 정보 및 인증 로직은 암호화 알고리즘과 비슷한 측면이 있다. 뭐 여튼 암호와의 응용 측면까지 포함하려 생각하다보니 해당 개념을 빼고는 너무 사전적인 설명이 될것 같아서 포함하게 되었다.

 

  다만 앞에도 얘기했듯이 이 분류는 개인적으로 글을 구성하다 보니 개념적으로 나눠보게 된 것이고, 일반적인 보안 지식이나, 자격증 지식의 측면에는 타당한 분류는 아니라는 것을 잊진 말았음 한다. 그럼 각 요소들에 대해서 조금 더 자세히 들여다 보도록 하겠다.

 

 

 

2. 1세대

  1세대 암호화는 아주 옛날에는 풀기 불가능했던 암호화일지는 모르지만, 여러가지 수학적, 과학적 툴들이나 컴퓨터 프로그래밍이 발달한 현재에는 약점을 쉽게 드러낼 가능성이 높은 방식이다. 물론 특정 물리적 개체의 특성과 연관짓는다든지 하는 특수한 내부집단에서만 공유되는 비전서 적인 방법도 있을지는 모르겠지만, 보통 키 자체 또는 암호화 로직이 엄청 복잡해 지는 경우일 것이기 때문에 엄청 비효율 적일 것이다(예를 들어 특정 책의 본문 내용과 암호화 할 대상을 1:1로 섞는다든지 하는 방식이 있을 듯 싶다. 우리가 강력한 암호를 선택할 때 책의 좋아하는 문장을 적절한 특수문자와 섞어 암호로 사용한다든지 하는 것도 비슷할 것 같고 말이다). 이 쪽에서 보안 측면에서 생각할 문제는 이러한 방식에 대해서 유효한 암호화 라고 생각하는 보안적 측면에 경험이 적은 개발자 들을 종종 볼 수 있다는 것이다. 코드 안에서 AES 암호화를 수행 해야될 위치에 베이스64 함수를 이용해 인코딩을 한다든지 하는 식으로 말이다(쿠키에 베이스64로 인코딩된 인증 쿠키를 숨겨 놓는 등의 설계를 할 수 있다).

 

 

2.1 Base64

  세상에는 수많은 인코딩이 있지만(유니코드나, 각 언어별 인코딩, 더 나아가 스스로 만든 인코딩도 있을 수 있다) 몇 가지 보안적 측면에서 많이 거론되는 인코딩을 본다면 우선 "베이스64(BASE64)"는 어떠한 문자라도 웹을 통해 전달할 수 있는 아스키 형태로 변환 하는것을 말한다. 예를 들면 그림 파일을 업로드시 바이너리의 내용(이것도 앞에서 얘기했지만 어치피 특정한 숫자의 연속이다)이 베이스64식으로 인코딩 되서 HTTP Body 에 첨부되어 날라가게 된다(아마 뒤에서 업로드 코드를 시연할 때 실제로 보게 될것 같다).

 

[베이스64 - 위키백과]

https://ko.wikipedia.org/wiki/%EB%B2%A0%EC%9D%B4%EC%8A%A464

 

  실제로 베이스64 인코딩이 되는 부분을 보기 위해서, 여러 인터넷에 있는 툴이나 페이지를 이용해도 되지만, 여기에서는 다음 클라이언트 코드 시간에 사용하게 될 피들러를 설치한 후, 내장된 유틸리티인 TextWizard를 이용해 보도록 하겠다. TextWizard는 여러 종류의 인코딩 디코딩을 하게 해주는 유틸리티 이다. 해당 부분도 파이썬 10교시 WhoIS API 글에서 설명을 했기 때문에 그쪽에 있는 피들러 설치와 설정 방법을 참조해서 세팅하기 바란다. 현재 버전은 바뀌었지만 설치과정은 거의 비슷하다. 피들러를 실행하고 상단에서 Tools > TextWizard 나 TextWizard 아이콘을 클릭하면 아래와 같은 유틸리티 창이 나오게 된다.

 

  기본적으로 Transform 부분에 "To Base64"가 선택되어 있을 것이다. 위의 입력란에 넣은 텍스트를 베이스64로 인코딩 해준다는 뜻이다. 그럼 위쪽에 "안녕하세요." 이라고 넣게 되면, 하단에 베이스64로 인코딩 된, "7JWI64WV7ZWY7IS47JqULg==" 값이 나오게 된다.

 

 

 그럼 반대로 "7JWI64WV7ZWY7IS47JqULg==" 를 복사해서 위쪽 칸에 넣고, Transform 드롭박스에서 "From Baset64" 를 선택해 본다. 베이스64로 인코딩된 문자열로부터 평문을 만든다는 뜻으로, 다시 하단에 "안녕하세요."라는 문장이 나오게 된다.

 

 

2.2 URL Encoding

  2번째로 URL 인코딩은 브라우저의 주소창에서 사용되는 부분인데, 영어가 아닌 한글 같은 문자나, 공백 문자 등을 %OO 의 형태로 바꾸어 URL에서 사용할 수 있도록 하는 것을 말한다. 서버는 저 %OO으로 변환된 URL을 받아서 원래의 문자로 바꾸어 처리하게 된다.

 

[URL - Encoding URL - Regular Motion님의 블로그]

http://regularmotion.kr/url-encoding-url/

 

  이 부분도 TextWizrd 를 통해서 실제로 보도록 하겠다. 위쪽의 베이스 64위키 링크였던  "https://ko.wikipedia.org/wiki/%EB%B2%A0%EC%9D%B4%EC%8A%A464" 를 복사해서, TextWizard 에 복사하고, Transform 드롭박스에서 "URLDecode"를 선택 한다. 그럼 URL 뒤쪽에 암호와 같이 있었던 "%EB%B2%A0%EC%9D%B4%EC%8A%A464" 부분이 "베이스64" 라는 것을 알수 있게된다. 자세히 보면 뒤의 숫자 64는 아스키 코드이기 때문에 따로 인코딩이 되지 않고 한글 부분만 인코딩이 되어있다.

 

 

2.3 HTML Encoding

  마지막으로 HTML 인코딩은 설명하려 한다. <table>, <b>, <script> 와 같은 태그들은 원래 브라우저에서 문법 문자로 해석이 되어 테이블 형태, 볼드체 표기 등의 UI적 측면을 표시하거나 자바스크립트를 실행 하게 된다. 그런데 만약 해당 태그를 HTML 인코딩을 하게 되면, 태그 그 자체의 글자로 브라우저 화면에 표시되게 한다. 이러한 원리를 이용해서, XSS 공격을 방어하기도 한다.

 

[difference between url encode and html encode - 스택오버플로]

https://stackoverflow.com/questions/1812473/difference-between-url-encode-and-html-encode

 

  이것도 실제 보기 위해서 아래의 테이블을 보여주는 HTML 파일을 보도록 하겠다.

 

  가장 간단한 구조의 테이블 예제는 아래와 같다. <table> 태그가 맨 밖에 있고, 선(border) 굵기가 1 사이즈를 가진다. <th> 태그안에 제목인 '과자'와 '초콜릿'이  들어있고, 줄을 나타내는<tr> 태그가 두개 있고, 하나에는 '파이, 카카오45%' 가, 나머지 하나에는 '머랭, 카카오100%'  가 각각 <td> 태그안에 나눠 담겨 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<table border=1>
  <tr> 
     <th>과자</th>    
     <th>초콜릿</th>  
  </tr>  
  <tr>    
     <td>파이</td>
     <td>카카오45%</td>
  </tr>  
  <tr>   
     <td>머랭</td>
     <td>카카오100%</td>  
  </tr>
</table>
cs

 

 

  아무 폴더에나  파일형식을 "모든 파일"로 선택하고 "table_sample1.html" 이라고 저장한다. 이후 탐색기에서 해당 파일을 더블클릭해 실행하면 브라우저가 뜨면서 아래와 같이 테이블이 표시된다.

 

 

  이제 HTML 인코딩이 되는 경우를 보기 위해서, 해당 소스를 복사해서 TextWizard에 붙여 넣습니다. 이후 Transform 드롭박스에서 "HTML Encode" 를 선택한다.

 

   위와 같이 변환된 결과가 나오면, 아래의 결과를 드래그해 복사 후, 아무 폴더에나  파일형식을 "모든 파일"로 선택하고 "table_sample2.html" 이라고 저장한다. 이후 탐색기에서 해당 파일을 더블클릭해 실행하면 브라우저가 뜨면서 아래와 같이 랜더링된 테이블이 아닌 테이블 소스 자체의 문자로 표시 된다. 저 내용 안에 <script> 태그가 있다해도 일반적인 글자로 취급되서 실행이 안되니(마치 이전 SQL Injection 에서 Prepared Statement 호출시 인자가 문법 문자로 취급되지 않는 것과 원리가 비슷합니다), XSS 공격의 방어 수단으로 사용되기도 한다(이것도 좀 인젝션의 스토어드 프로시저나, Prepared Statement 처럼 원래 만들때의 의도는 XSS 방어를 생각하고 만들어진 것은 아니였을 것 같다)

 

 

  이렇게 위의 예제 3개를 보면 인코딩 이란것은 변환 로직에 기반한 암호화 이기 때문에 중요한 문자열을 저런 타입의 인코딩으로 변화해 보호하는 것은 의미 없는 행위라는 것을 이해할 수 있다. 이 세가지 인코딩을 확실히 이해하지 못하면 보안 공부할때 헷깔리는 경우가 종종 있으니 꼭 이해하고 넘어가길 바란다.

 

 

 

3. 2세대

  2세대가 되면서 암호화에 중요한 진전이 나타나는 데, 그것은 수학을 본격적으로 이용한다는 것이다. 그래서 2세대 이후의 암호화 방식은 수학 문제를 푸는 것과 같은데, 로직을 숨겨 놓은 1세대 암호화와는 다르게 2세대 이후는 전체 로직을 모두에게 공개 한다. 다만 그 수학문제를 풀 때 힌트인 암호화키를 모를 경우는 풀이 난이도가 엄청 상승되게 되어, 하나하나 숫자 대입 방식으로 풀려면 컴퓨터로 몇백년 만큼의 시간이 걸린다는 점이다.

 

  2세대 암호화는 대칭키의 성질을 이용한 암호화이다. 위에서 얘기했듯이 엑셀이나 zip파일에 암호화를 할 때 사용하는 방식으로, 암호화를 사용할때 사용한 키가 복호화 할때도 동일하게 사용된다.

 

 

  어찌보면 현실의 자물쇠-열쇠 조합과 비슷해 아주 직관적이라 암호화 계의 모범생이라고 보면 될것 같다. 그래서 현실에서 복사한 열쇠를 가족들과 나눠갖는 것과 마찬가지로 복사한 열쇠를 얼마나 조심히 잘 전달하고, 관리하는 지가 중요하다. 한 사람이라도 어수룩한 사람이 있게되면 자물쇠는 어처구니 없이 열리게 된다(물론 보안과 마찬가지로 중요한 자물쇠가 있는 장소 앞을 여러가지 물리보안이나, 추가적인 논리적 보안 수단을 통해서 보호해야 해야한다. 집문 앞에 CCTV 가 있다든지, 추가로 홍채 인식을 해야한다든지 말이다).  

 

 대칭키 암호화는 엑셀, zip 같은거 이외에도 우리가 잘 알고 있는 로그인이나, Gmail, 은행 등에서 사용하고 있는 HTTPS(SSL) 연결에서 키 교환 후 실제 데이터를 주고 받을 때도 사용하고, VPN 에서 VPN 장비와 컴퓨터 사이에서의 모든 데이터(여기서 암호화되어 전달되는 데이터 흐름을 터널링이라고 표현합니다)도 마찬가지 이다. 모바일 폰 내의 데이터 암호화도 사용자 패스워드에 기반한 키를 생성하여 대칭키 방식으로 저장하는 것으로 알고 있다. 그래서 패스워드를 모르면 데이터를 복구할 수 없다는 얘기가 나오게 된다. 또는 여러 회사나 단체에서 고객의 정보를 저장할 때, 중요한 개인정보나 카드 정보 같은 경우는 실제 프로그램 내부에서 불러와 사용해야 하기 때문에, 데이터베이스에 저장할때는 대칭키로 암호화해 저장하고, 필요할때 적절한 권한을 획득하여 복호화 된 평문 정보를 얻게된다.

 

  대칭키는 암호화 키에 풀이가 의존적인 수학 문제기 때문에 암호화키를 사용하지 않고 숫자들을 하나하나 대입할 경우 언제 쯤 답을 찾을 수 있는지가 중요하게 된다. 해당 시간이 짧거나, 많은 컴퓨터를 병렬로 연결하여 계산하거나, 몇 십년후의 컴퓨터에서는 금방 찾을 수 있는 수준이라면 나중에 현재 데이터를 암호화 해서 보호하고 있는 기술들이 모두 무용지물 되는 상황이 생길 수 있게된다. 그래서 여러가지 안전한 암호화 방식이 권장되고 있지만 가장 잘 알려지고 널리 쓰이고 있는 것은 AES 방식이다(보통 128, 256 정도의 키 길이를 가지며 키길이가 길어질 수록 2의 N승으로 풀이 난이도가 올라가게 된다).

 

 

  대칭키를 쓰다보면 몇 가지 정도의 주의해야 하는 이슈가 있는데, 그 중 하나가 키의 안전한 전달(교환)과 보관, 관리 이다. 만약 두 사람이 인터넷에서 특정한 대칭키를 기반으로 암호화해서 데이터를 보내기로 하면 최초 한쪽에서 만든 키를 전달해 줘야한다. 전화로 불러주거나, 직접 만나서 얘기해주거나, 종이로 적어 전해주거나, 암호화 해서 보낸후 구두로 암호화 키를 알려주거나 하는 많은 방법의 경우 "낮말은 새가 듣고, 밤말은 쥐가 듣는다"는 속담을 피해갈 수는 없게 된다. 또 암호화 키라는게 보통 의미 없는 숫자로 길게 만들어지기 때문에 가독성이나 암기하기에도 좋지 않다. 그래서 3세대에 설명할 비대칭 키와 인증이라는 개념을 이용해서 보통 현실적인 키교환을 하게 된다. 해당 부분은 비대칭 키 부분에서 설명하도록 하겠다(하지만 어쩌면 현실적으로는 사람이 와서 주위에 아무도 없는 것(도청장치 포함)을 확인한 후에 외운 내용을 불러주고 가는게 더 안전할 수도 있을 것도 같다).

 

  추가로 보관하는데 있어서도 암호키와 키를 사용하는 프로그램이 분리되어야 하는 이슈가 있게 된다(해당 부분은 법적인 이슈이기도 하다). 왜 그래야 하는지 생각해 본다면, 집문 앞의 화분뒤에 열쇠를 숨겨놓는 것과 같은 행위이기 때문이다. 물론 해당 집 문앞까지 가는 행위가 IT 로 이루어진 세상에서는 해당 서버내의 프로그램 실행 경로까지 침투해야되는 어려운 일이긴 할테지만, 일단 행위가 일어나게 되면 키의 탈취가 아주 쉬워지기 때문이다. 그래서 많은 프로그램 예제에 있듯이(이 글의 예제도 마찬가지 입니다) 소스 내부나, 컨피그 파일내에 암호화키가 덩그라니 들어있는 것은 옳지 않은 일이다.

 

  JAVA 등의 컴파일 되는 언어라도 바이너리를 분석석하여 끄집어내거나, 디버그 툴 등으로 해당 키 기능을 호출 할 수 있기 때문에, 집 앞에 구덩이를 파서 몰래 열쇠를 숨겨놓는 정도 밖에 안전하지 않다고 본다. 그래서 사실 여러가지 키관리 솔루션이나 설계를 이용하여, 권한 제어를 통해 키를 외부에 숨겨서 사용하긴 하지만 그것도 100% 안전한 방법이라고는 하긴 힘들것 같고(보안적으로 100% 안전하다는 것은 사실 좀 거짓말인듯도 싶다), 결국은 분리된 키관리와 함께 문앞에 낯선 사람이 맘대로 오지 않게하고, 혹시 오더라도 수상한 행동을 하지 않나 모니터링이 되도록 defense in depth 측면의 설계를 택하는게 현실적인 것 같다.

 

  마지막으로 한번 정해진 암호화 방식이나, 키를 바꾸는 것은 상당히 어렵기 때문에(프로그램 변경 이외에도 이미 기존 방식으로 암호화된 데이터가 있기 때문에, 해당 데이터를 모두 복호화 해서 평문으로 복구 후, 다시 새 방식으로 암호화를 하는 데이터마이그레이션을 해줘야 한다. 게다가 패스워드 등을 저장한 해시 같은 일방향 암호화 같은 경우는 원래 평문 값을 알 수 없기 때문에, 다시 사용자에게 값을 받기 전에는 기존 것을 없애고 새로 만드는 마이그레이션 자체가 불가능 하기도 하다), 처음 설계부터 이런 쪽은 제대로 표준과 법률에 맞추어 설계하는 것이 맞을듯 싶다.

 

 

  AES256 에 대한 예제는 파이썬에서 있던 예제를 가져와 보려한다. 우선 파이썬 3.7이 c:\Python 폴더에 설치되 있다는 가정에서 시작한다. 설치 및 실행 방법을 모른다면 파이썬 글 2교시를 참조하면 된다.

 

  파이썬 3.7에서는 pycrypto 의 상위 호환버전인 pycryptodome 의 한글 인코딩 문제가 해결되어 그대로 쓰면 될것 같다. 커맨드 창을 열고 PIP 명령어를 이용해 설치한다.

c:\Python\code>pip install pycryptodome

Collecting pycryptodome
...
Successfully installed pycryptodome-3.6.4

 

  예제는 아래와 같다.

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
# 모듈을 불러옵니다.
import base64
import hashlib
from Crypto import Random
from Crypto.Cipher import AES
 
# 암호화할 문자열을 일정 크기로 나누기 위해서, 모자란 경우 크기를 채워줍니다.
BS = 16
pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS).encode()
unpad = lambda s : s[0:-s[-1]]
 
 
# 암호화를 담당할 클래스 입니다.
class AESCipher:
 
    # 클래스 초기화 - 전달 받은 키를 해시 값으로 변환해 키로 사용합니다.
    def __init__( self, key ):
        self.key = key
        self.key = hashlib.sha256(key.encode()).digest()
 
    # 암호화 - 전달받은 평문을 패딩 후, AES 256 으로 암호화 합니다.
    def encrypt( self, raw ):
        raw = raw.encode()
        raw = pad(raw)
        iv = Random.new().read( AES.block_size )
        cipher = AES.new( self.key, AES.MODE_CBC, iv )
        return base64.b64encode( iv + cipher.encrypt( raw ) ).decode()
    
    # 복호화 - 전달 받은 값을 복호화 한후, 언패딩해 원문을 전달합니다.
    def decrypt( self, enc ):
        enc = base64.b64decode(enc)
        iv = enc[:16]
        cipher = AES.new(self.key, AES.MODE_CBC, iv )
        return unpad(cipher.decrypt( enc[16:] )).decode()
 
 
# 암호화 클래스를 이용해 cipherinstance 객체를 만들면서, 암호화키를 넣습니다.
cipherinstance = AESCipher('mysecretpassword')
 
# 암호화를 합니다.
encrypted = cipherinstance.encrypt('감추고 싶은 말')
 
# 암호화 한 값을 다시 복호화 합니다.
decrypted = cipherinstance.decrypt(encrypted)
 
# 암호화 한 값과 복호화 한 값을 출력 합니다.
print('암호화된 값 : ' + encrypted)
print('복호화된 값 : ' + decrypted)
cs

[파이썬 소스 - aes_example_in_python.py]

 

   그럼 위의 파일을 aes_example_in_python.py 로 UTF-8 포맷으로 저장하고 실행 한다.

c:\Python\code>python pycrypto_aes256_sample.py

암호화된 값 : C0u2hrCZrAca69P7D3VFnSitOkTlPZbCxat9vMvZXN2cPSJUMu+Et3Ca+nZoYEO9
복호화된 값 : 감추고 싶은 말

 

  실행된 내용을 보면 같은 키로 암호화와 복호화를 하는 것을 볼 수 있다.

 

 

 

4. 3세대

  3세대는 비대칭 키라고 하며 공개키와 개인키라는 특이한 개념의 수학적 도구를 사용하게 된다. 2세대와 틀린 부분은 키가 하나가 아니라 2개로 이루어진 한 쌍이라는 것이다. 개인키로 암호화된 값은 공개키로 복호화 될수 있고, 공개키로 암호화된 값은 개인키로 복호화 될수 있다. 개인키를 가진 쪽이 좀더 빨리 복호화가 된다고 하는 것 같으나, 가장 주요한 차이는 공개키 쪽이 열쇠의 역활도 하지만 자물쇠의 역활도 한다는 것입니다(물론 개인키도 전자서명의 측면에서는 자물쇠의 역활도 한다고 볼수 있지만, 주로 하는 역활은 열쇠라고 보는게 맞을 듯 싶다)

 

  보통 알고리즘으로는 RSA 와 좀더 최근에 나온 ECC(elliptic curve cryptography-타원곡선암호) 를 사용하게 되는데 일반적으로 ECC가 같은 수준에서 키가 더 짧아 간결하다고 하고, 자세한 차이점을 정말 이해하려면 수학적 차이일 테니 양쪽의 수학적 원리를 이해해야 할듯하다.

 

[Which one is better: elliptic curve cryptography or RSA algorithm and why? - Quora 사이트]

https://www.quora.com/Which-one-is-better-elliptic-curve-cryptography-or-RSA-algorithm-and-why

 

 

  가장 흔히 쓰이는 분야가 앞서 얘기한 HTTPS(SSL)의 경우인데, 최초 브라우저와 서버가 서로 사용할 대칭키를 비밀스럽게 교환하기 위해서 사용한다. 다만 여기서 현실적인 딜레마가 생기게 되는데, 서버가 보내준 공개키를 어떻게 믿고 클라이언트 쪽의 브라우저가 암호화에 사용할 대칭키를 해당 서버의 공개키로 암호화해 보내느냐 이다.

 

  이러한 딜레마를 해결하기 위해 인증서의 개념이 생기게 됬는데, 사실상 해결책이 신뢰할 수 있는 국제 기관이나 국내 기관의 공개키를 클라이언트쪽 OS(예:윈도우즈, 리눅스)가 인지하여 관리하고, 해당 기관의 개인키를 이용해서, 서버 사이트의 공개키를 보증 하는 방식이다. 뭔가 절대적으로 안전해 보이진 않지만 마치 우리가 은행이나 국가를 믿듯, 일단은 OS나 저러한 (루트) 인증 기관을 믿는것이 전제로 되어있다(앞에도 얘기했지만 보안은 100%인 것은 없어 보인다. 사실 우리가 사는 현실자체가 100% 는 없기 때문에 당연할 것을 수도 있다). 

 

 여기서 또 전자서명이라는 개념이 추가로 들어오게 되는데, 하나의 개인키로 특정한 값을 암호화 해서 만들어 내면, 공개키를 가진 사람이 해당 값을 복호화 하게 되서 기대했던 일련의 값들을 얻을 수 있다면, 해당 복호화된 내용은 해당 개인키를 가진 사람이 작성했다는 것을 증명하게 된다는 것이다(왜냐하면 개인키는 그 사람만 가지고 있어야만 하는 것이기 때문이다-물론 해킹을 당하는 경우는 아니겠지만, 이런 아닌 경우는 비트코인 지갑이 털리거나 할 때 일어나는 일이 된다).

 

 

  예를 들자면 특정 A 회사가 서버가 추가되서 SSL 통신을 해야되는 일이 생기면, 베리사인 같은 루트 인증 기관에 1년에 특정한 금액을 내고 인증서를 발급 받게 된다. 인증서 안에는 A 회사 도메인의 주소, 인증서 유효기간 등등의 정보와, A 사의 공개키가 포함되어 있고, 그 모든 정보를 적절히 믹스하여 루트인증기관의 개인키로 암호화한 전자서명이 들어있다. 사용자의 브라우저에서는 해당 인증서를 받아서, 알고 있는 루트인증기관의 공개키로 전자서명을 풀어, 복호화된 내용이 인증서에 기재한 내용들과 일치하는지 확인한다. 내용이 일치하게 되면 A 사의 공개키는 믿을 수 있으므로, A사의 공개키로 HTTPS 통신에 사용할 대칭키를 암호화해 보낸다. A 사의 서버는 해당 암호화된 내용을 받아 개인키로 풀어 대칭키를 얻어낸 후, 이후 통신 부터는 해당 대칭키로 암호화해 내용을 주고 받게 된다(공개키-개인키 방식이 연산 비용이 많이 들어 대칭키로 실제 암복호화를 한다고 한다).

 

  개인이 받는 공인인증서도 비슷하다. 공개키를 증명하는 인증서를 신용평가 회사에서 받게 된다. 조금 다른 부분은 개인키 또한 같이 발급받아 보관하면서, 개인키는 유출이 되면 큰일이 나니까 따로 대칭키 방식을 이용해서 개인키를 암호화 하여 보관한다(공인인증서를 발급 받을때 입력하는 개인의 비밀번호가 해당 대칭 암호키 이다). 그럼 아마도 확실한 내부 동작은 모르겠지만, 은행 사이트에 접속하면 은행 쪽에서 개인의 인증서를 가져가 신용평가 회사의 공개키로 풀어서 내용을 확인해, 신용평가사에서 해당 개인 계좌주인에게 발급한 공개키라는 것을 확인하할 것이다. 이후에는 그 공개키를 이용해서 거래에 관련된 정보를 암호화해 보내거나, 같이 저장된 개인키를 이용해서 거래를 증명하는 전자서명을 보내거나, HTTPS 에서 사용할 대칭키를 보내거나 할것 같다.  

 

[공인인증서 비밀번호의 안정성 - CPUU의 DayDreamin']

https://cpuu.postype.com/post/177466

 

[네이버 애플리케이션의 전사 서명 원리 - 네이버 D2]

https://d2.naver.com/helloworld/744920

 

  실제로 보안 분야 뿐만 아니라(물론 안전하게 인증을 하면서 편하게 사용하기 위한 보안적 측면을 위해 도입된 것이겠지만), 비트코인, GIT, SSH, PGP, 빅데이터 시스템 쪽에서도 이러한 개념을 가져다 사용한다. 비트코인은 코인주소를 나타내는데 공개키를, 지갑 열쇠를 나타내는데 개인키를 사용하고, GIT 또한 SSH(Secure Shell) 연결을 위해 공개키-개인키 쌍을 생성하여 깃서버와 개인 컴퓨터가 나눠가져 해당 키 인증으로 사용을 한다. 리눅스도 마찬가지로 id/password 대신 공개키 인증을 통해 로그인을 하여 사용할 수 있다(그게 id/password 보다 더 안전하냐는 별개의 문제겠지만).

 

  PGP 도 각각의 공개키를 공개키 서버에서 얻어 공개키로 메일 내용을 암호화 안전하게 보내는 것이라고 보면되고, 하둡 등의 여러 클러스터링 설정 시에도 아마 주키퍼 등에서 공개키, 개인키를 이용해 서버간의 작업 수행에 대한 권한 인증을 하기도 했던 것으로 기억한다(앞의 공인인증서의 개인키와 마찬가지로 추가로 패스워드를 지정하기도 한다). 공개키, 개인키 쌍으로 로그인을 하는 것과 같은 경우는, 공개키가 본인만의 자물쇠를 나타내며, 등록한 자체가 해당 공개키를 믿는 것을 의미하는 것이기 때문에(이 자물쇠를 연다면 들여보내줘...), 마냥 공개적인 것만은 아닌 측면도 살짝 보이는 것 같다. 그래서 공개키-개인키의 이름에 넘 칩착하면 뭔가 꼬여 버릴 수도 있다. 공개키는 쓰임에 따라서는 마냥 공개된 키만이 아니라는 것을 염두에 두어야 한다. 

 

 

  그럼 뭔가 예제가 없으면 섭섭할 것 같아서, 조촐하지만 파이썬을 이용해서 RSA 공개키와 개인키 쌍을 하나 만들어 보도록 하겠다. 구글에서 "python generate rsa key" 라고 찾으면 아래의 깃허브 페이지가 나온다.

 

[Python PyCrypto: Generate RSA Keys Example.py - lkdocs 깃허브]

https://gist.github.com/lkdocs/6519378

 

  밑의 댓글하고 섞어서 RSA2048 키를 만들도록 적당히 주석을 달았다. PEM 포맷은 공개키, 개인키를 교환할때 사용하는 표준 포맷 중 하나이다(편하게 위의 인코딩+형식을 정의한 규칙이라고 봐도 될 듯 하다)

1
2
3
4
5
6
7
8
9
10
11
12
13
from Crypto.PublicKey import RSA 
 
# 새로운 RSA 생성 
new_key = RSA.generate(2048)
 
# 공개키를 얻어 옵니다.
public_key = new_key.publickey().exportKey("PEM"
 
# 개인키를 얻어 옵니다.
private_key = new_key.exportKey("PEM"
 
print (private_key)
print (public_key)
cs

 

   그럼 위의 파일을 aes_example_in_python.py 로 UTF-8 포맷으로 저장하고 실행한다.

c:\Python\code>python rsa_gen.py

b'-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEAkhB0qnHrNpFO5rlyiMzBkU55pvIjj5Zgk3TfPEFXzMvyORW/\nuuiL8mIfmIaouPUj+eEPvfG7Zdk2rV2rf6bUL1Of9 .....

 

 

 

5. 특수형태 - 해시

  해시는 보안에 들어오기전 처음 프로그래밍 공부할때 봤을때 참 신기한 기능을 하는 함수라고 생각이 들었었다. 사실 해시의 유용함은 보안적 관점을 추가해 봤을때 가장 잘 이해되는 것도 같다. 

 

  해시 또한 수학적 개념으로 아무리 큰 숫자라도 해시라는 함수를 적용하게 되면 고정 길이를 가진(SHA256 같은 경우 256bit 길이의 숫자가 나온다는 의미이다) 숫자로 변환되게 된다. 해시가 유용한 것은 서로 다른 숫자라면 같은 해시 값이 나올 가능성이 거의 없고, 만약 있더라도 같은 해시값을 가지는 서로 다른 숫자를 의도적으로 만들기에는 너무 많은 시간이 걸린다는 성격을 가지고 있다. 또한 해시값으로 부터 원래 값을 유추하기가 아주 많이 힘들다는 특징도 가지고 있다(이것은 2, 3 세대 암호화와 비슷한 부분이다)  

 

 

  해시는 보통 검증을 위한 측면과, 정체를 들키지 않기 위해 숨기려는 2가지 측면을 동시에 가지고 있다. 검증하기 위한 측면은 백신 프로그램에서 특정 악성코드 파일의 해시 값을 비교 하거나, 게임이나 어플의 패치시 패치파일의 유효성이나, 설치된 어플이 조작되지 않았다는 것을 체크하는데도 해시 값을 사용한다. 또는 여러 파일 공유 사이트에서 불법, 저작권 파일 등을 체크해 내기 위해서도 파일의 해시값을 비교하곤 한다. 파일의 내용이 1비트만 바뀌더라도 파일을 구성하는 숫자가 달라지는 것이기 때문에, 새로운 해시값이 만들어지는 특징을 이용한 검사 이다. 비트 코인 같은데서도 채굴 및 블록의 검증을 하는 경우에도 사용하고 있다.

 

  또한 가입한 회원들의 패스워드를 저장할때도, 법적으로 해시화 해서 저장하도록 되어 있다. 사람들은 보통 여러 사이트에 동일한 패스워드를 사용하기 때문에, 특정 회사에서 저장한 패스워드의 평문을 복구할 수 있다면 위험 부담이 생길 수 있을 것이다. 그래서 혹시나 인터넷의 수상한 사이트(?)를 회원가입을 통해서만 사용할 수 있다면, 첫째는 사용하지 않는 것이 제일 좋을 테고, 어쩔수 없이 사용해야할 때에는 다른 주요 사이트에서 사용하는 id/password 로는 등록하지 않는 것이 좋아보인다.  그 사이트에서 패스워드를 해시를 해서 저장할지, 평문으로 저장할지도 모르고, 그것과 상관없이 중간에서 빼내어 다른데에 나쁘게 쓸지는 아무도 모르니까 말이다. 또 내부 직원은 풀수 있는 양방향 암호화로 저장하거나 하는 실수를 할 수도 있고 말이다.

 

  해시로 저장한 경우 원본 값을 알 수 없기 때문에,  사이트에서는 사용자가 입력한 패스워드를 받아서 해시함수로 넘긴 후, 결과에 나온 해시 값을 회원 가입때 입력했던 패스워드 해시값과 비교해서 검증하게 된다(그래서 사이트에서 비밀번호 찾기 할때 무조건 새로운 비밀번호로 바꾸지, 원래 비밀번호를 알려주진 못하게 된다. 만약 잊어버린 비밀번호를 알려주는 사이트라면 평문으로 저장됬거나 양방향으로 잘못 저장된 경우라고 논리적으로 확신할 수 있다).

 

 

  반대로 해시값을 숨겨야 하는 측면도 있다. 여기서 숨긴다는 의미는 레인보우 테이블 같은 시도로부터 정체를 숨긴다는 의미이다. 레인보우 테이블은 평문을 해시함수로 만들면 항상 일정한 값이 나온다는 것에 착안해서, 해시와 평문의 매칭 테이블을 만든다고 한다. 뭐 단순하게 1:1 매칭으로 테이블을 만드는건 아니고 빠른 검색을 위해 이런저런 구조의 최적화를 한다고는 한다. 이런 문제 때문에 현실에서의 외모의 변장과 같은 일들을 해시값을 만들어 낼때 추가로 해야한다.

 

  첫번째는 솔트(salt)로 요리에 소금을 치듯이 사용자가 입력한 패스워드에 특정한 랜덤 문자열을 더한 후 해시 함수를 적용해서, 만들어진 해시 값을 다르게 한다. 패스워드마다 서로 다른 솔트 값을 만들어 내야 하는 이유는, 안 그러면 하나의 패스워드만 알아내게 되면, 이후 고정된 솔트값을 알수 있게 되기 때문에 해당 솔트가 가미된 새로운 레인보우 테이블도 만들수 있게 될 것이기 때문이다. 두 번째는 해시를 몇번씩 다시 해싱을 해서 전혀 다른 값을 만든다는 것인데, 이건 마치 악성 코드들이 코드를 안 들키기 위해서 여러번 포장을 하는 것과 비슷해 보인다(가끔 방어 기법과 공격기법이 마찬가지인 경우가 있다. 랜섬웨어 처럼 말이다). 이 경우 값도 달라지고 brute force 방식으로 진행시 한번 계산하는 시간을 많이 걸리게 해서 찾기 힘들 게 만든다고 한다. 뭐 여튼 이것도 100% 완벽하다고는 못하겠지만 안 그런 데이터를 공격할 때 보다는 공격자의 ROI가 확실히 안 나오게는 만들 순 있을 것 같다. 

 

[패스워드 털려서 써보는 패스워드 암호화 - 장모님 브런치]

https://brunch.co.kr/@jangcnet/11

 

[안전한 패스워드 저장 - 네이버 D2]

https://d2.naver.com/helloworld/318732

 

 

  그럼 python.exe 파일을 열어 SHA256 해시값을 얻는 예제를 하나 보겠다. 구글에서 "python file sha256" 라고 검색하여 제일 처음의 github 샘플을 봅니다.

 

[simple SHA256 hashing example, written in Python using hashlib - rji님 깃허브]

https://gist.github.com/rji/b38c7238128edf53a181

 

  역시 적당히 한글 주석과 함께 python.exe 파일을 가져오도록 편집을 해보면 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import hashlib
import sys
 
# 파일 읽어들일 블록 사이즈
block_size = 65536
 
sha256 = hashlib.sha256()
with open("c:\python\python.exe"'rb') as f:
    for block in iter(lambda: f.read(block_size), b''):
        # 읽어 들인 값을 계속 해시객체에 추가한다.
        sha256.update(block)
    # 최종 해시 계산
    print (sha256.hexdigest())
 
cs

 

  그럼 위의 파일을 "aes_example_in_python.py" 로 "UTF-8" 인코딩으로 저장하고 실행 한다. 아래와 같이 해시 값이 나온다. 여러번 실행해도 계속 같은 값이 나오는 것을 볼 수 있다.

c:\Python\code>python sha256_gen.py
e964f2e498ab3141d86da5f0d6e135be82986c66aaafef356b6e7751b779a796

 

 

 

 

6. 제3자 인증 형태 - 설계적 인증

  이제 마지막 부분이다. 이 부분은 앞에서 얘기한 베리사인같은 CA 기관이나 신용 정보회사 같은데서 하는 서비스하는 공개키의 대한 신원의 보증같은 측면도 있지만, 추가로 언급하고 싶은 부분은 설계적 인증 형태라고 보는 것이 어떨까 싶다. 이 부분을 암호화 글에 넣어 설명하는 경우가 맞을까는 싶지만(뒤에서도 아마 계속 언급되어질 주제같다), 암호화와 연관지어 설명해야 가장 맘에 닿을 것 같기 때문에 일단 이 곳에서 우선 언급하려 한다.

 

  1번째는 서버 측면의 로직이다. 패스워드 추측 공격을 피하기 위해서 시도 횟수를 제한하거나,  특정 횟수 후 캡챠를 걸거나, 입력 할때마다 인증 받는 시간을 증가시키거나 하는 경우도 안전한 암호화를 돕는 방법이 될 것 같다.

 

  2번째로 핸드폰, 태그 방식의 도어키, OTP 등 소유에 대한 안정성을 기반으로 한 인증도 역시 마찬가지로 암호화와 동등한 레벨의 방어를 가지게 된다(defense in depth 측면이기도 하지만, 법적으로 암호화 행위와, 전용선의 사용이 등가적인 보호수단으로 취급되는 것도 이런 측면이라고 생각한다).

  

  세 번째로 I-PIN 같은 기술이 주민번호를 대체하기 위해 나와서, 이전 같으면 주민번호가 양방향 암호화 되서 저장될 수 있는 부분을 개인정보와 관련있는 임의의 고유키의 저장으로 대체하게 되었다(물론 I-PIN 회사 내부에서는 주민번호와 해당 CI, DI 값의 연결고리를 알고 있겠지만, CI나 DI 값만을 사용하는 일반 회사들은 해당 주민번호를 알 순 없다). OTP 도 개인만 아는 패스워드로 인증 받는 체계를, 소유 기반 인증에 더해서, 서버만이 알고 있는 임의의 패스워드(OTP 생성번호)로 대체했다고 보면 될것 같다.

 

  네 번째로 쿠키 등의 특정 장소에 보관된 암호화된 인증 값에 대한 재사용 방지 이슈도 있다. 특정 값을 암호화 했더라도 해시나, AES 같은 경우는 해당 값의 고유성이 사라지진 않는다. 그래서 해당 데이터를 그대로 복제해 사용할 경우 인증을 할 수 있는 재사용 이슈가 있다. 해당 부분에 대해서는 랜덤 값인 시간, 거래 고유 번호 등 항상 달라지는 서버 측면 요소들을 더해 암호함으로서 항상 값이 달라지게 만들고, 해당 달라지는 값들을 서버 측면에서 검증(시간이 5분이상 지났다든지, 거래 고유 번호 등의 값이 맞는다 든지)해 사용해야 되는 이슈가 있다. 뭐 이렇게 특정 값을 unvalidate 시키는 것은, 토큰인, 비트 코인등 다른 쪽에서도 마찬가지 인것 같다.

 

 

 

7. 마무리 하면서

  써 놓은 글을 뒤돌아 보니, 요약해 보면 간단한 암호화에 대한 얘기를 너무 거창하게 펼쳐 놓지 않았나도 싶다. 하지만 앞에서 봤듯이 암호화 부분이 정말 많은 기술 분야 안에 기초 구조로 숨어 있기 때문에, 해당 측면들을 인지하지 못한다면, 어플리케이션의 취약점에 대해 불완전하게 이해할 가능성이 높다. 아마 다음 시간에 얘기할 클라이언트 코드 쪽 보안에 대한 얘기도 불완전하게 될 가능성이 높고 말이다.

 

  그리고 전공이 맞아 천천히 공부해서 수학적으로 깊게 이해한 사람들도 있겠지만, 그렇지 못한 사람에게는 논리적으로라도 해당 개념을 정확하게 잡고 있는 것이 중요한 듯 싶다. 안 그럼 자꾸 상상의 나래를 펼치게 되어 잘못된 의견을 가지게 될 수도 있으니 말이다. 그럼 이렇게 이번 시간을 마무리를 하도록 하겠다. 

 

 

2018.7.22 by 자유로운설탕
cs

 

 

posted by 자유로운설탕
2018. 6. 6. 19:36 보안

  이번 시간은 인젝션(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' , 13'2018-01-01')
 
insert into order_record
values (1001'jerry' , 21'2018-01-02')
 
insert into order_record
values (1002'tom' , 31'2018-01-02')
 
insert into order_record
values (1003'tom' , 32'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"> &nbsp; <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"> &nbsp; <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"> &nbsp; <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(20NULL,
    FoodName char(30NULL,
    Company char(20NULL,
    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 공식 페이지]

http://php.net/downloads.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, 아파치 컨피그 파일 첨부]

config_files.zip

 

 

 

2018.6.17 by 자유로운설탕
cs

 

posted by 자유로운설탕
prev 1 2 3 4 5 6 ··· 9 next