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.
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:
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:
In the snippet there are three parts:
mktoResponse
, to invoke the function in:else if (d.mktoResponse){
onResponse(d.mktoResponse)
}
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);
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)
}
@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.
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:
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:
In the snippet there are three parts:
"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 );
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
};
}
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>';
}
}
...
}
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.
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 // "'"<b>\"
// 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.