Hace unos días conseguí finalmente contactar con el desarrollador del plugin «Z-URL Preview» donde le comentaba que tenia un Cross-site scripting en la versión 1.6.1.
Esta vulnerabilidad se encuentra en el parámetro «url» en el archivo»/wp-content/plugins/z-url-preview/class.zlinkpreview.php» que como se puede comprobar en la siguiente captura el constructor de la clase «ZLinkPreview» carece de los mecanismos necesarios para evitar la inyección de código.
<?php
class ZLinkPreview {
var $description;
var $title;
var $image = array();
var $url;
var $html;
var $parsemode;
var $curlerrno;
var $curlerr;
var $curlinf = array();
var $htmlblank;
function __construct($url) {
if (!preg_match("~^(?:f|ht)tps?://~i", $url)) {
$url = "http://" . $url . '/';
}
$this->url = $url;
$this->getHTML();
}
function setParseMode($m = "r") {
$this->parsemode = $m;
}
function getHTML() {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $this->url);
curl_setopt($ch, CURLOPT_NOBODY, false);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_CAINFO, dirname(__FILE__) . "/cacert.pem");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$this->html = curl_exec($ch);
$this->curlinf = curl_getinfo($ch);
$this->curlerr = curl_error($ch);
$this->curlerrno = curl_errno($ch);
if (!$this->html) {
//echo 'Error: ' . curl_error($ch);
//die();
$this->htmlblank = true;
}
curl_close($ch);
// $this->html = str_replace("<head>", "<head><base href=\"$this->url\">", $this->html);
}
function getcurlerrno() {
echo $this->curlerrno;
}
function getcurlerr() {
echo $this->curlerr;
}
function getcurlinf() {
print_r($this->curlinf);
}
function getDescription() {
if ($this->parsemode == "d") {
$res = "";
$dom = new DOMDocument();
@$dom->loadHTML($this->html);
foreach($dom->getElementsByTagName('meta') as $meta) { // prefer og:description
if ($meta->getAttribute('property') == "og:description") {
$res = $meta->getAttribute('content');
break;
}
}
if ($res == "") { // failback to basic description
foreach($dom->getElementsByTagName('meta') as $meta) {
if ($meta->getAttribute('name') == "description") {
$res = $meta->getAttribute('content');
break;
}
}
}
if ($res == "") { // failback to first p if meta's missing or blank
$res = $dom->getElementsByTagName('p')->item(0)->nodeValue;
}
echo $res;
} else {
if (preg_match_all('/<meta(?=[^>]*name="description")\s[^>]*content="([^>]*)"/si', $this->html, $matches)) {
foreach ($matches[1] as $key => $content) {
echo $content;
}
} else if (preg_match_all('/<meta(?=[^>]*name="og:description")\s[^>]*content="([^>]*)"/si', $this->html, $matches)) {
foreach ($matches[1] as $key => $content) {
echo $content;
}
}
}
}
function getTitle() {
if ($this->parsemode == "d") {
$title = "";
$dom = new DOMDocument();
@$dom->loadHTML($this->html);
foreach($dom->getElementsByTagName('meta') as $meta) { // prefer og:title
if ($meta->getAttribute('property') == "og:title") {
$title = $meta->getAttribute('content');
break;
}
}
if ($title == "") { // failback to title if og:title missing or blank
$title = $dom->getElementsByTagName('title')->item(0)->nodeValue;
}
if ($title == "") { // failback to h1 if title missing or blank
$title = $dom->getElementsByTagName('h1')->item(0)->nodeValue;
}
echo $title;
} else {
// if (preg_match("/<title>(.+)<\/title>/si", $this->html, $matches)) { // Changed due to issue with BBC news
if (preg_match("/<title>(.+)<\/title>/i", $this->html, $matches)) {
echo $matches[1];
} else {
$dom = new DOMDocument();
@$dom->loadHTML($this->html);
echo $dom->getElementsByTagName('title')->item(0)->nodeValue;
}
}
}
function getImage($multiple = false) {
if ($this->parsemode == "d") {
$res = "";
$dom = new DOMDocument();
@$dom->loadHTML($this->html);
foreach($dom->getElementsByTagName('meta') as $meta) { // prefer og:image
if ($meta->getAttribute('property') == "og:image") {
$res = $meta->getAttribute('content');
break;
}
}
if ($res == "") { // failback to first img if og:image missing or blank
$res = @$dom->getElementsByTagName('img')->item(0)->getAttribute('src');
}
if ($res != "") { // only try and clean up the url if an image was found
// we need the fqdn without the trailing /
$urlo = rtrim ($this->url,"/");
$res = preg_replace("/&#?[a-z0-9]{2,8};/i", "", $res);
if (substr($res, 0, 4) == "http") { // if the url starts with http we're done
$reso = $res;
} else {
if (substr($res, 0, 1) == "/") { // if url starts with / then it could be an absolute path
if (substr($res, 0, 2) == "//") { // ok, not absolute, but for dual-mode http(s) sites
$reso = "http:" . $res;
} elseif (substr($res, 0, 3) == "://") { // for dual-mode http(s) sites with :
$reso = "http" . $res;
} else { // absolute to prepend fqdn
$reso = $urlo . $res;
}
} else { // doesn't start with a / so a relative path - for now assume a / base path
$reso = $urlo . "/" . $res;
}
}
}
echo $reso;
} else {
/* First we will check if facebook opengraph image tag exist */
if (preg_match_all('/<meta(?=[^>]*property="og:image")\s[^>]*content="([^>]*)"/si', $this->html, $matches)) {
foreach ($matches[1] as $key => $content) {
$image[] = preg_replace("/&#?[a-z0-9]{2,8};/i", "", $content);
if ($key == 5)
break;
}
}
/* If not then we will get the first image from the html source */
else if (preg_match_all('/<img [^>]*src=["|\']([^"|\']+)/i', $this->html, $matches)) {
foreach ($matches[1] as $key => $value) {
if (strpos($value, 'http') === false) {
// If trailing slash is missing from domain AND image path does not start with slash, insert one - technically should check for base href, but later :-)
if ((substr($this->url, -1) != "/") && (substr($value, 0, 1) != "/")) {
$image[] = $this->url . '/' . preg_replace("/&#?[a-z0-9]{2,8};/i", "", $value);
} else {
$image[] = $this->url . preg_replace("/&#?[a-z0-9]{2,8};/i", "", $value);
}
} else {
$image[] = preg_replace("/&#?[a-z0-9]{2,8};/i", "", $value);
}
if ($key == 5)
break;
}
}
$image_index = (isset($_GET['image_no'])) ? $_GET['image_no'] - 1 : 0;
echo (!$multiple) ? $image[$image_index] : str_replace(array("\\", "\"", " "), array("", "", ""), json_encode($image));
}
}
}
$zlinkPreview = new ZLinkPreview($_GET['url']);
define('SHORTINIT', true);
require_once('../../../wp-load.php');
$linkmode = get_option('zurlpreview_linkmode');
switch ($linkmode) {
case "target-blank":
$linkmodehtml = ' target="_blank"';
break;
case "target-newwindow":
$linkmodehtml = ' target="newwindow"';
break;
case "rel-external":
$linkmodehtml = ' rel="external"';
break;
default:
$linkmodehtml = '';
}
$zlinkPreview->setParseMode(get_option('zurlpreview_parsemode'));
?>
<div id="at_zurlpreview">
<?php
if ($zlinkPreview->htmlblank == true) {
?>
<p class="imgd">Error No: <?php $zlinkPreview->getcurlerrno(); ?></p>
<p class="imgd">Error: <?php $zlinkPreview->getcurlerr(); ?></p>
<p class="imgd">Info: <?php $zlinkPreview->getcurlinf(); ?></p>
<?php
} else {
?>
<?php
if (get_option('zurlpreview_noheadtag') != "Yes") {
if (get_option('zurlpreview_linkheader') == "Yes") {
?>
<h2><a href="<?php echo $zlinkPreview->url; ?>" <?php echo $linkmodehtml; ?>><?php $zlinkPreview->getTitle(); ?></a></h2>
<?php
} else {
?>
<h2><?php $zlinkPreview->getTitle(); ?></h2>
<?php
}
}
?>
<h3 style="display:none;"><?php $zlinkPreview->getTitle(); ?></h3>
<?php
if (get_option('zurlpreview_noimage') != "Yes") {
if (get_option('zurlpreview_linkimage') == "Yes") {
?>
<p class="imgp"><a href="<?php echo $zlinkPreview->url; ?>" <?php echo $linkmodehtml; ?>><img data-src = "<?php $zlinkPreview->getImage(1); ?>" src="<?php $zlinkPreview->getImage(); ?>"></a></p>
<?php
} else {
?>
<p class="imgp"><img data-src = "<?php $zlinkPreview->getImage(1); ?>" src="<?php $zlinkPreview->getImage(); ?>"></p>
<?php
}
}
if (get_option('zurlpreview_nointro') != "Yes") {
?>
<p class="imgd"><?php $zlinkPreview->getDescription(); ?></p>
<?php
}
if (get_option('zurlpreview_titlelink') == "Yes") {
?>
<p class="imgs"><a href="<?php echo $zlinkPreview->url; ?>" <?php echo $linkmodehtml; ?>><?php echo htmlspecialchars($zlinkPreview->getTitle()); ?></a></p>
<?php
} else {
?>
<p class="imgs"><?php echo get_option('zurlpreview_linktxt'); ?> <a href="<?php echo $zlinkPreview->url; ?>" <?php echo $linkmodehtml; ?>><?php echo preg_replace('#^https?://#', '', $zlinkPreview->url); ?></a></p>
<?php
}
?>
<?php } ?>
</div>
En la siguiente captura se comprueba que la ejecución de código se realiza correctamente.

En la siguiente captura se comprueba el código dentro del cuerpo HTML.

- Agradecimiento del desarrollador:
https://wordpress.org/support/topic/z-url-preview-1-6-1-cross-site-scripting/ - Publicación en Packetstormsecurity:
https://packetstormsecurity.com/files/145218/WordPress-Z-URL-Preview-1.6.1-Cross-Site-Scripting.html