오늘은 첫번째 SNS 웜으로 기록된 Samy 웜에 대해서 알아보겠습니다. 2005년 10월 4일, Samy라는 아이디를 쓰는 MySpace 사용자는 웜을 이용해서 하루만에 90만명이 넘는 친구(인증샷)를 확보했습니다.

MySpace 웜의 동작 방식

  1. MySpace는 많은 수의 태그를 차단하고 있다. <a>, <img>, <div>와 <embed> 등 몇 개의 태그만 허용되는 것 같다. <script>나 <body>, onClick이나 on으로 시작되는 어떤 것도, href에 자바스크립트를 넣는 것도 안 된다. 그렇지만 일부 브라우저에서는 CSS 태그에 자바스크립트를 쓸 수 있다. (IE나 사파리 일부 버전 등) 아무튼 자바스크립트가 동작해야 한다.

    예제: <div style="background:url('javascript:alert(1)')">
  2. 이미 작은 따옴표와 큰 따옴표를 다 써버렸기 때문에 자바스크립트 코딩하기가 굉장히 까다로워졌다. 이를 우회하기 위해 표현식에 자바스크립트를 저장해놓고 이름으로 불러서 실행하는 방법을 사용했다.

    예제: <div id="mycode" expr="alert('hah!')" style="background:url('javascript:eval(document.all.mycode.expr)')">
  3. 음 좋다. 이제 작은 따옴표와 자바스크립트를 사용할 수 있게 되었다. 그렇지만 MySpace는 javascript가 발견되는 족족 전부 날려버린다. 이를 우회하려면 일부 브라우저에서 "java\nscript"도 "javascript"로 인식한다는 점을 이용한다. (즉, java와 script 사이에 개행이 일어나게 하는 것이다.)

    예제: <div id="mycode" expr="alert('hah!')" style="background:url('java
    script:eval(document.all.mycode.expr)')">
  4. 됐다. 작은 따옴표도 쓸 수 있고, 가끔 큰 따옴표가 필요하긴 하겠지만 이스케이프 하면 된다. 음 그런데 MySpace는 모든 이스케이프된 따옴표를 날려버린다. 그렇지만 자바스크립트를 이용해서 정수를 아스키로 바꾸는 트릭으로 따옴표를 문자열에 붙이면 된다.

    예제: <div id="mycode" expr="alert('double quote: ' + String.fromCharCode(34))" style="background:url('java
    script:eval(document.all.mycode.expr)')">
  5. 내 프로필을 보는 사용자의 프로필에 코드를 집어넣으려면, 다른 사용자의 주소를 알아내야 한다. 아, document.body.innerHTML을 이용하면 페이지를 보고 있는 사용자의 ID를 얻어낼 수 있다. MySpace는 innerHTML 문자열도 다 날려버렸다. 또 돌아가면 된다. eval()을 이용해서 두 개의 문자열을 하나로 합치는 방식으로 "innerHTML"을 만들자.

    예제: alert(eval('document.body.inne' + 'rHTML'));
  6. 이제 다른 페이지를 가져와야 한다. iframe을 사용할 수도 있겠지만, iframe은 숨겨놓는다고 해도 사용자가 무슨 일이 일어나는지 눈치채기 더 쉽다. 그러니 대신 AJAX (XML-HTTP)를 사용해서 HTTP GET이나 POST 요청을 보내려고 한다. 그런데 또 MySpace는 XML-HTTP를 쓰는데 필요한 "onreadystatechange"를 날려버린다. 또 eval을 써야 한다. 그리고 여기에 덧붙여서 쿠키 정보도 같이 보내야 한다.

    예제: eval('xmlhttp.onread' + 'ystatechange = callback');
  7. 이제 다른 사용자의 프로필을 GET으로 가져올 때다. 여기서 현재 영웅 목록을 가져올 수 있다. 이 영웅 목록에 나를 살짝 끼워넣으려고 한다. 이건 뭐 XML-HTTP 요청을 보내기만 하면 되는 것이지만 내 프로필을 보고 있는 사용자의 friend ID가 필요하다. 위에서 했던 것처럼 내 프로필 페이지에서 찾으면 될텐데.. 문제는 검색할 때 내가 만들어 넣은 코드에서 같은 검색 문자열을 찾게 될거란 말이다. 가령 이 페이지가 'foo'를 포함하는 경우에 특정한 일을 하도록 하려고 하는데 검색해봐야 내가 넣은 코드의 검색 문자열 'foo'가 다시 나오게 되므로 항상 참이 되어버린다. 이 문제를 해결하려면 검색 문자열 자체도 쪼개놓고 실행할 때 붙여서 쓰면 된다.

    예제: var index = html.indexOf('frien' + 'dID');
  8. 이제 영웅 목록을 가지고 있는 상태이다. addFriends 페이지에 XML-HTTP POST 요청을 보내어 나를 친구로 추가하도록 하자. 읍, 그런데 제대로 동작하지 않는다. 이게 어찌 된 일인가? 내 프로필 페이지는 profile.myspace.com이고 POST 하는 페이지는 www.myspace.com이었다. 도메인 이름이 다르므로 XML-HTTP로 GET이나 POST 요청을 보낼 수가 없었다. 이를 우회하기 위해 www.myspace.com의 동일한 URL로 옮겼다. 어차피 www.myspace.com에서도 프로필을 볼 수 있기 때문이다. 만약 profile.myspace.com으로 프로필을 보러 왔다면 www.myspace.com으로 먼저 리다이렉트한다.

    예제: if (location.hostname == 'profile.myspace.com') document.location = 'http://www.myspace.com' + location.pathname + location.search;
  9. 드디어 POST가 된다! 그런데 요청을 보내긴 했지만 친구가 실제로 추가되진 않았다. 이건 MySpace가 POST에 사용할 랜덤한 해시 값을 만들어 놓기 때문이다. (역주: CSRF 공격을 막기 위한 것이죠.) POST에 올바른 해시 값이 같이 들어오지 않으면, 그냥 무시해버린다. 이를 우회하려면 브라우저인 척 해서 페이지를 받은 다음 파싱해서 해시 값을 얻어오면 된다.
  10. POST로 친구 추가가 끝나면 영웅으로도 추가하고 웜 코드도 복제해서 박아넣어야 한다. 해시 값을 새로 받아오는 것만 제외하면 영웅으로 추가하는 것도 POST 한 번으로 끝낼 수 있다. 자기 복제에 필요한 코드는 현재 프로필 페이지의 소스를 가져와서 코드 부분만 파싱한 다음 POST로 넘기는게 제일 쉬운 방법이다. 아, POST가 제대로 되려면 URL-인코드랑 이스케이프도 해야 한다. 어 그래도 이상하게 동작을 안 한다. 자바스크립트의 URL 인코딩과 escape() 함수는 제대로 이스케이프 처리가 안 되는 것 같다. 그래서 수작업으로 일부 이스케이프 처리를 해주었다. 자 이제 자기 복제가 가능한 웜을 만들어냈다!
  11. 길이 제한 등 다른 문제 때문에 코드를 빡세게 만들어야 한다. 불필요한 공백도 다 없애고 이름 길이도 최소한으로 하고 함수도 최대한 재사용 하는 등..

웜 코드 다운로드