PostMessage Vulnerabilities. Part II

Title

This is the second part of a two series of articles about postMessage vulnerabilities. The first part was an introduction to what is a postMessage, basic exploitation, detection and mitigation. This part is an analysis of real cases reported in Bug Bounty scenarios. Two disclossed Hackerone reports will be analyzed and a few tips to exploit/bypass postMessage Vulnerabilities will be shown.


DOM Based XSS in www.hackerone.com via PostMessage and Bypass (#398054 and #499030)

In #398054 report, a Dom XSS is exploited in Hackerone through an insecure message event listener in Marketo. The flow of the code could be seen in the following image:

Image Diagram 1

According to the report, If there is no error set in the response, it creates a variable named u and sets it to the return value of the findCorrectFollowUpUrl method. This performs some processing on a property named followUpUrl in the response object, which seemed to be a URL to redirect to after the form submission was complete.

This was not used by the HackerOne form, but by setting it to an absolute URL, it was possible to control the value of the u variable, which was later used to change the location.href of the window. When the following mktoResponse message was sent to the Hackerone window, the window was redirected to the JavaScript URI, and the code alert(document.domain) was executed.

To exploit this vulnerability, the following snippet could be used:

Image Exploit 1

In the snippet there are three parts:

  • 1. PostMessage with the first JSON element as mktoResponse, to invoke the function in:
else if (d.mktoResponse){
    onResponse(d.mktoResponse)
}
  • 2. Reaching this function, a JSON structure is needed with the following elements: for, error and data. If error is false, the function success will be called.
  var requestId = mktoResponse["for"];
  var request = inflight[requestId];
  if(request){
    if(mktoResponse.error){
      request.error(mktoResponse.data);
    }else{
      request.success(mktoResponse.data);
  • 3. In this function, the followUpUrl value will be associated to u, and passed to location.href. Therefore, a payload with javascript:alert(document.domain) will trigger JavaScript execution.
  var u = findCorrectFollowUpUrl(data);
  location.href = u;

After that, Hackerone team changed the OnMessage function to add an origin validation:

if (a.originalEvent && a.originalEvent.data && 0 === i.indexOf(a.originalEvent.origin)) {
    var b;
    try {
        b = j.parseJSON(a.originalEvent.data)
    } catch (c) {
        return
    }
    b.mktoReady ? f() : b.mktoResponse && e(b.mktoResponse)
}

The Bypass

@honoki reported a smart bypass in #499030.

The variable i resolves to https://app-sj17.marketo.com/ and indexOf checks if the origin is contained in the string. Therefore registering a marcarian domain .ma, the validation will be bypassed:

("https://app-sj17.marketo.com").indexOf("https://app-sj17.ma")
0

If the previous exploit is hosted in the registered domain https://app-sj17.ma, the XSS will be executed.


CVE-2020-8127: XSS by calling arbitrary method via postMessage in reveal.js (#691977)

In #691977, @s_p_q_r reported a DOM XSS exploited via PostMessage. The flow of the code could be seen in the following image:

Image Diagram 2

First, the setupPostMessage is invoked with the method addKeyBinding to define a JSON element with the malicious payload. After that, the function showHelp() is called to render in the browser the malicios payload defined in registeredKeyBindings[binding].description

To exploit this vulnerability, the following snippet could be used:

Image Exploit 2

In the snippet there are three parts:

  • 1. PostMessage with the first JSON element as "method" : "addKeyBinding", to call the method and apply the args:
if( data.method && typeof Reveal[data.method] === 'function' ) {
    Reveal[data.method].apply( Reveal, data.args );
  • 2. Reaching the function addKeyBinding with the arguments args, allows the construction of a JSON object with the value callback, key and description.
function addKeyBinding( binding, callback ) {

    if( typeof binding === 'object' && binding.keyCode ) {
        registeredKeyBindings[binding.keyCode] = {
            callback: callback,
            key: binding.key,
            description: binding.description
        };
    }
  • 3. The function toggleHelp() is invoked because renders the content of the previous JSON without validation, triggering the JavaScript execution.
function showHelp() {

    ...

    for( var binding in registeredKeyBindings ) {
        if( registeredKeyBindings[binding].key && registeredKeyBindings[binding].description ) {
            html += '<tr><td>' + registeredKeyBindings[binding].key + '</td><td>' + registeredKeyBindings[binding].description + '</td></tr>';
        }
    }

    ...

}

Tips/Bypasses in PostMessage vulnerabilities

  • If indexOf() is used to check the origin of the PostMessage event, remember that it can be bypassed if the origin is contained in the string as seen in The Bypass

  • @filedescriptor: Using search() to validate the origin could be insecure. According to the docs of String.prototype.search(), the method takes a regular repression object instead of a string. If anything other than regexp is passed, it will get implicitly converted into a regexp.

"https://www.safedomain.com".search(t.origin)

In regular expression, a dot (.) is treated as a wildcard. In other words, any character of the origin can be replaced with a dot. An attacker can take advantage of it and use a special domain instead of the official one to bypass the validation, such as www.s.afedomain.com.

  • @bored-engineer: If escapeHtml function is used, the function does not create a new escaped object, instead it over-writes properties of the existing object. This means that if we are able to create an object with a controlled property that does not respond to hasOwnProperty it will not be escaped.
// Expected to fail:
result = u({
  message: "'\"<b>\\"
});
result.message // "&#39;&quot;&lt;b&gt;\"
// Bypassed:
result = u(new Error("'\"<b>\\"));
result.message; // "'"<b>\"

File object is perfect for this exploit as it has a read-only name property which is used by our template and will bypass escapeHtml function.