Uway.com 은 대부분의 대학에서 이용하는 원서 접수 대리 사이트이다. 또한 물리 올림피아드 , 수학 올림피아드 등 각종 올림피아드의 수상자 발표 서비스 및 성적 컨설팅 서비스도 제공한다.

사용자 삽입 이미지

때문에 굉장히 많은 학생들의 신상 정보를 포함하기 마련이다. 사실 대한민국의 모든 수험생의 신상 정보를 담고 있다고 해도 과언이 아니다.

지난 8월 13일 한국 물리올림피아드의 수상자가 온라인으로 발표되었다. 발표 페이지는 Uway에서 제작한 대행 서비스를 사용하고 있었다. 주소는 http://ipsi9.uway.com/ksi/kps/pass/1/index.htm 이며, 이 포스트를 작성할 때는 서비스를 중단한 상태였다.

페이지는 단순했다. 성명과 주민등록번호를 입력하고 확인을 누르면 수상 정보를 알려 주는 것이었다. HTML 코드를 분석해 본 결과 form1이라는 폼에 namekor, juminno라는 이름의 Text field와 mode라는 이름의 hidden field가 있었다. method는 POST 방식을 사용하고 있었으며 onsubmit에는 값 검사 함수가 할당되어 있어 각 필드의 유효성을 검사했다.

여기서 두 가지 흥미로운 점을 발견할 수 있었다. 즉, form1의 action 페이지가 설정되어있지 않았다는 점mode라는 hidden field의 값은 check로 설정된 채 바뀌지 않는다는 점이었다. action에 주소가 할당되지 않으면 submit시 현재 페이지가 리로딩되며 데이터가 전달된다. 즉, action의 기본값은 현재 페이지이다. 따라서 이 페이지는 한 페이지에서 값의 입력과 출력을 모두 담당하며, 입력 페이지를 표시할 것인지 출력 페이지를 표시할 것인지는 mode라는 변수가 check라는 값을 가지고 있는지를 이용해 판단할 것임을 추측할 수 있다.

기본적으로 POST 방식으로 전송되는 데이터는 변조가 쉽지 않다. 따라서 입력 필드를 통해서만 공격을 해야 되기 때문에 공격이 많이 제한될 수밖에 없다. 특히 필드 검사 함수가 있는 상황에서는 더욱 힘들다. 그러나 만약 GET 방식의 파라미터를 받아들일 수 있다면, 입력 검사 함수를 회피하여 더욱 다양한 형태의 공격이 가능하다. 혹시나 하는 기대를 품고 주소 표시줄에 다음을 입력했다.

(생략)/index.htm?mode=check&namekor=홍길동

mode=check은 입력 페이지가 아닌 출력 페이지를 호출하기 위함이며, namekor 필드를 GET 형식으로 넘기고 있다. 결과는 놀라웠다. 데이터 에러가 아닌 '수상내역 없음' 페이지가 떴다. 알고 있는 수상자의 이름을 입력하여 보았더니 수험 번호와 이름, 주민등록번호, 상급, 여름학교 참가 여부가 정상적으로 표시되었다!

이 공격에서 POST로는 어떠한 값도 넘기지 않았다. 심지어 GET으로도 주민등록번호는 넘기지 않았다. 그러나 이름만 가지고도 정상적인 출력을 했으며, 이는 매우 위험하다.

결과를 통해 이 페이지가 범하고 있는 실수들을 추측해보면 다음과 같다.


  1. POST value와 GET value를 구분하지 않고 사용한다.

    서버사이드 언어를 PHP로 사용했을 경우를 예로 들어 보자. PHP에서 POST value와 GET value에 접근하기 위해서는 각각 $_POST['varname'], $_GET['varname']과 같이 사용해야 한다. 그러나 PHP 배포판의 기본 설정상, POST value와 GET value 모두 단순히 $varname으로도 접근할 수 있다. 만약 POST value에 접근하기 위해 $varname을 사용하였는데 공격자가 POST value 대신 GET value를 전달한다면, 프로그램은 GET value를 POST value로 인식하고 사용할 것이다. 물리 올림피아드 수상자 발표 페이지에 대한 공격이 성공한 결정적인 이유는, POST value 대신 GET value를 넘길 수 있었기 때문이다. 만약 두 값이 구분되었다면, mode를 인위적으로 check로 바꾸기는 힘들었을 것이다.


  2. 폼 값을 검사하지 않는다.

    웹 페이지를 작성하며 가장 하기 쉬운 잘못은 바로 값을 전달할 때 송신자와 수신자 중 한 쪽에서만 검사를 한다는 것이다. 로컬로 실행되는 프로그램은 상대적으로 값이 조작될 가능성이 낮기 때문에 값 전달시 한 쪽에서만 검사하는 것도 큰 문제가 되지 않는다. 그러나 웹 프로그래밍에서 값은 언제 어디서 어떻게 조작될 지 알 수 없으므로 언제나 값을 사용하기 전에는 검사해야 한다. 즉, 폼 값을 전송하기 전에 Javascript를 이용하여 유효성 검사를 했다고 하더라도, 서버사이드에서 이 값을 사용할 때에는 다시 유효성 검사를 해야 한다는 것이다. 물리 올림피아드 수상자 발표 페이지가 서버사이드에서 다시 유효성 검사를 했더라면, juminno 필드가 비어 있는 것을 발견하고 에러를 냈을 것이다.


  3. 적절한 Query문을 작성하지 않는다.

    주민등록번호가 전달되지 않으므로 juminno=""라고 예상할 수 있다. 만약 Query가 적절히 작성되었다면, 주민등록번호가 ""인 데이터는 검색되지 않아야 한다. 예상컨대 Query를 작성할 때 해당 필드가 빈 문자열이 아닐 때에만 조건을 추가하도록 한 것으로 보이며, 이는 이 코드가 재활용되기 때문일 것이다. 어떤 수상자 발표 페이지에서는 이름과 수험번호를, 어떤 페이지에서는 수험 번호와 주민등록번호를 요구하는 등 서로 다른 필드를 요구하기 때문에 빈 문자열이 아닌 경우에만 조건을 추가하도록 한 것이 문제의 원인으로 보인다.


만약 모든 가능한 이름에 대하여 Brute-force 공격(마구잡이 대입 공격)을 시도한다면 1초에 10개의 이름을 처리할 수 있다는 가정 하에 약 900시간만에 모든 이름에 대해 공격을 시도할 수 있으며 한국인의 이름은 주요 성씨에 대부분이 모여 있다는 점을 이용하면 훨씬 짧은 시간 안에도 대다수의 이름에 대해 공격을 시도할 수 있다. 이름만으로 주민등록번호를 포함한 수상 정보를 얻을 수 있다는 것은 매우 위험한 일이다.

9월 초에는 한국 수학올림피아드의 수상자가 또 Uway의 페이지를 통해 발표되었다. 코드를 재활용할 것이라는 예상과 같이, 수학 올림피아드와 물리 올림피아드의 수상자 발표 페이지는 출력 형식을 제외하고는 거의 100% 일치했다.

사용자 삽입 이미지

단일 페이지에서 mode 변수를 이용해 출력을 컨트롤하는 점, POST value와 GET value를 필터링하지 않는 점 등 모든 보안 문제가 그대로였기 때문에 정확히 같은 방법으로 공격을 할 수 있다. 다른 점이라면 출력 페이지에 주민등록번호가 표시되지 않는다는 것이다. 따라서 이름과 주민등록번호를 연결시켜 개인 정보를 얻기 위해서는 이름이 아닌 주민등록번호에 대해 Brute-force 공격을 시도해야 한다.

가장 큰 문제는 보안 의식이다. Uway에서 발생한 문제는 절대 기술적인 문제가 아니다. 공격법이 새로운 것도 아니고, 그렇다고 복잡한 것도 아니다. 심지어 기본적인 PHP 입문서에나 나올 법한 보안 예제이다. 아주 단순하고 초보적인 공격에 개인 주민등록번호를 노출하고 만 것이다. 조금만 보안에 관심을 가졌다면 쉽게 발견하고 고칠 수 있는 문제이다.

문제는 이러한 보안 문제가 이 페이지에만 국한된 것이 아니라는 것이다. 얼마 전 전자신문 에 '학교에서 주민번호가 줄~줄 샌다 '라는 기사가 떴다. Uway와 같이 온라인 서비스가 주된 사업 영역이고, 그 규모도 동종 기업 중 1위라고 자부하는 회사에서조차 너무나 허술한 보안의식으로 심각한 보안 문제가 나타났는데, 학교야 어련하랴 싶은 마음이 들었다.

이제 프로그램을 개발할 때 보안에 신경을 쓰는 것은 필수이다. 프로그래머가 보안을 담당할 수 없다면, 보안 기술자를 고용하는 것이 현명할 것이다. 이 공격법은 주민등록번호 노출에서 그쳤지만, 이와 같은 보안 의식으로 설계한 프로그램이라면 다른 허점도 존재할 것이고, 이는 더 심각한 문제를 야기할 수 있다. 이러한 사태를 맞는 것보다는 보안 분야에 예산을 더 투입하는 것이 훨씬 경제적인 판단이다.

복잡하고 교묘한 공격을 막기는 힘들더라도, 기초적 보안 의식 결여에 의한 보안 허점은 이제 사라지길 바라며 마친다.


2008년 1월 19일 이후 작성된 모든 글에 대해서 퍼가는 것을 금지합니다.
퍼가고자 하시는 분은 링크를 달아 주시기 바랍니다.
Posted by Harry

필자는 10월 1일부터 Apec에서 주최하고 대만 교육부에서 주관하는 Apec Cyber Academy(이하 ACA)에 참가한다. 물론 국제대회이고, 상급은 1등상이 대만 교육부장관상일 만큼 꽤 큰 대회이고, 올해가 6회 대회로, 역사가 짧은 대회도 아니다.

이 대회에 참가하기 위해서는 팀을 구성해야 하는데, 올해부터 팀 구성에 한 가지 관문이 생겼다. 바로 Survivor Online이라는 게임을 통과해야 하는 것이다. 게임이라고는 하지만 게임이라기보다는 퀴즈에 가까운 컨텐츠로, 인터넷 개인정보 보호(Safe)와 정보 불법 도용 및 복제 방지(Honest)의 두 가지 주제에 대한 인식 검사 정도로 보면 된다. 쉬워 보일 수 있으나, 퀴즈를 푸는 도중 한 문제라도 틀리면 처음으로 돌아가는데다가 문제마다 플래시 애니메이션이 있어서 시간이 상당히 소요되기 때문에 통과하기가 매우 힘든 것이 사실이다.

그래서 어떻게 편법을 써서 이를 통과할 방법이 없을까를 고민했다. 가장 먼저 생각해 본 것은, 맨 마지막 페이지에서 지금까지 맞은 문제의 개수와 틀린 문제의 개수를 집계하여 이를 이용해 통과 여부를 결정할 테니, 그 중간의 맞은 문제 개수 전달 과정을 잡아 내어 조작하면 될 것이라는 것이었다. 그래서 혹시 Form 내부의 Input을 Hidden으로 해 놓고 이에 숨겨 놓지는 않았나 검사해 보았다. 하지만 소스 코드는 너무나 깨끗했고, 어디에서도 맞은 문제의 개수가 저장되어 있는 Form을 찾을 수는 없었다.

그래서 일단 성실히 문제를 풀어 나가서 맨 마지막 페이지의 주소를 알아내었다. 수고롭게도, 주소 표시줄의 주소를 변경시키지 않고 페이지만 이동하도록 설계된 사이트여서 페이지 이동시 주소는 상태표시줄에 로딩 메시지가 잠깐 뜰 때 이를 캡쳐하여 볼 수밖에 없었다. 알아낸 주소는, /lwf/so/intro_safe_q.asp?t=1&s=20이었다. 그런데, 의외의 수확이 있었다. 바로 주소 뒤에 get 형식으로 인자 두 개가 따라오는 것을 볼 수 있었기 때문이었다.

사용자 삽입 이미지

t는 어떤 역할을 하는지는 전혀 유추가 되지 않았기 때문에 신경 쓰지 않기로 하고 s라는 변수의 기능만 알아내면 될 터였다. 그 때 상황이 필자가 10문제 중 2문제를 맞히고 마지막 페이지에 도착한 상태였고, 이를 이용해 s를 유추해 내면 대략 100점 만점의 점수(score)를 나타내는 변수라고 생각할 수 있었다.

그리하여 즉시 /lwf/so/intro_safe_q.asp?t=1&s=100 이라는 주소로 접속을 해 보았더니 여전히 'Try Again' 메시지가 떴다.
사용자 삽입 이미지

혹시나 하여 t를 0으로 주어 접속을 해 보았다. 그랬더니...
사용자 삽입 이미지

한 문제라도 틀렸을 때 나오던 Try Again 메시지는 없어지고 반가운 Pass 메시지가 떴다.

물론 서버상 정보에도 나는 이 문제를 모두 통과한 것으로 기록되어 있다. 국제 대회에서 이러한 문제가 발생한 이유는 페이지 설계에서 보안을 무시했던 탓이다. POST나 GET이나 그게 그거겠지 하는 생각이나 세션 관리의 허술, 자바스크립트의 지나친 노출 등이 모두 사이트 설계의 헛점이다. 이 사이트의 경우 세션 등으로 처리해야 할 데이터를 GET으로 넘겨줌으로써 이를 사용자가 너무나 쉽게 조작할 수 있게 했다는 것이다. 퀴즈이니까 이런 문제들이 그냥 넘어갈 수 있지, 만약 개인정보가 이러한 방식으로 날아다닌다면 심각한 문제가 될 것이다.

이러한 예를 통해서 자신이 페이지 설계시에는 이러한 점들을 최대한 고려하여 이렇게 '어이없게' 뚫리는 일이 없도록 하자.

(덕분에 우리 팀원들은 문제를 풀지 않고 원터치로 모든 문제를 패스할 수 있게 되었다.)




2008년 1월 19일 이후 작성된 모든 글에 대해서 퍼가는 것을 금지합니다.
퍼가고자 하시는 분은 링크를 달아 주시기 바랍니다.
Posted by Harry