Category: wordpress

Passwort-Reset in WordPress deaktivieren

09.12.2015 yahe code legacy security wordpress

Immer wieder findet man vermeindliche Kleinigkeiten, mit denen man sich noch besser gegen Angreifer schützen kann. Einer dieser Blind Spots war bei mir bisher die Passwort-zurücksetzen-Funktion meiner Webseiten. WordPress liefert hier leider keine einfache Konfigurationseinstellung, um sie zu deaktivieren. Da ich in meinem Fall neben dem WordPress-Nutzer auch gleichzeitig der Serveradmin bin und mir direkt auf der Kommandozeile ein neues Passwort verpassen kann, benötige ich den Weg über eine Passwort-Reset-E-Mail gar nicht und kann diese Funktion auch genauso gut abschalten. So besteht nicht mehr die Gefahr, dass meine Webseiten durch gehijackte E-Mail-Accounts kompromittiert werden können.

Hierfür notwendig ist ein winziges Plugin, das man sich schnell selbst zusammenschreiben kann:

<?php
  /*
    Plugin Name: KillPWReset
    Description: Disables the Password Reset in WordPress.
    Version: 0.1c1
  */

function killpwreset_disable($userid) {
  return false;
}

function killpwreset_errortext($errortext) {
  return "Login failed.";
}

add_filter("allow_password_reset", "killpwreset_disable");
add_filter("login_errors",         "killpwreset_errortext");
?>

Der Code sorgt dafür, dass für sämtliche Nutzer der Passwort-Reset deaktiviert wird. Weiterhin sorgt er dafür, dass eine generische Fehlermeldung erzeugt wird, wenn ein Login fehlschlägt. Typischerweise ist in der Fehlermeldung die Passwort-Reset-Funktion mit verlinkt. So wird der Fehlertext samt Link einfach überschrieben.


WordPress mit TLS Client Authentication absichern

27.04.2015 yahe administration code legacy security wordpress

Vor einiger Zeit hatte ich einmal gezeigt, wie man WordPress gegen eine größere Menge von Zero-Day-Exploits absichern kann, indem man verschiedene Datenbanknutzer einführt. Dadurch verwenden normale Webseitenbesucher einen Datenbanknutzer mit geringeren Privilegien und Nutzer des Backends einen Datenbanknutzer mit höheren Privilegien. Bei Softwarefehlern, die schreibenden Zugriff auf die Datenbank ermöglichen, können so Schreibversuche unterbunden werden.

Leider ist den Entwicklern von WordPress nicht sonderlich daran gelegen, die Datenbank so zu strukturieren, dass bestimmte Datenbankzugriffe für gewisse Nutzer nicht notwendig sind. So wurde vor einiger Zeit ein neues Session-Management eingeführt, das es zwar ermöglicht, Sitzungen auf anderen Rechnern zu beenden, dafür jedoch Daten in eine Tabelle schreibt, in der auch wichtige Konfigurationsinformationen abgelegt sind. Das führte zu Problemen bei der Trennung der Datenbanknutzer. Ich überlegte deshalb, wie ich die Trennung der Datenbanknutzer beibehalten und gleichzeitig flexibler bestimmen kann, wann ein höher privilegierter Datenbanknutzer benötigt wird.

Ebenfalls vor einiger Zeit hatte ich einmal gezeigt, wie man in NGINX TLS-Clientzertifikate einsetzen kann. Meine Idee war nun, die Steigerung der Privilegien innerhalb der WordPress-Datenbank an die Nutzung von TLS-Clientzertifikaten zu koppeln. So hat man eine Art Zwei-Faktor-Authentisierung: Mit seinem Zertifikat schaltet man die Nutzerrechte in der Datenbank frei und mit seinem Passwort loggt man sich im Backend ein.

Mein erster Versuch war, mit optionalen Clientzertifikaten zu arbeiten. Der Server fragt die Clientauthentisierung beim Browser an. Wenn der Browser kein Clientzertifikat zurückliefert, wird trotzdem mit der Seitendarstellung mit geringeren Priviliegen fortgefahren. Allerdings stellte sich heraus, dass der Safari-Browser bei jedem einzelnen Seitenaufruf nach dem Clientzertifikat fragt, sobald man eines hinterlegt. Eigentlich sendet der Server das Zertifikat der CA mit, von der das passende Clientzertifikat ausgestellt sein muss. Browser können diese Information nutzen, um die Clientzertifikate herauszufiltern, die nicht verwendet werden können. Doch leider sieht das Apple anders und so fragt der Safari den Nutzer ständig nach dem zu verwendenden Clientzertifikat, selbst, wenn es kein passendes gibt. Also musste eine nutzerverträglichere Lösung her.

In der neuen Lösung ruft man eine separate Seite auf, für die man zwingend eine Clientauthentisierung durchführen muss. Diese überprüft die Authentizität des Zertifikats und leitet dann auf einen Teil der eigentlichen, zu schützenden Seite weiter. Dieser Teil startet die Session, in der hinterlegt wird, dass die höheren Privilegien genutzt werden dürfen. Der Aufruf dieses Teils ist durch ein zeit- und passwortbasiertes Challenge-Response-Verfahren geschützt. Das spannende an diesem Ansatz ist, dass es prizipiell auch dafür geeignet ist, ein Single-Sign-On-Portal für verschiedenste Anwendungen aufzubauen. Kern der Anwendung ist die Datei "auth.php", in der sich die grundlegende Logik befindet:

<?php
  define("SUCCESS_CONTENT", "SUCCESS"); // defines what text should be in TLS_SUCCESS

  define("CHALLENGE_ALGO",   "sha256"); // defines the algorithm used for response HMAC
  define("CHALLENGE_LENGTH", 32);       // defines the length of the random challenge

  define("PARAM_START",   "?"); // should be static
  define("PARAM_VALUE",   "="); // should be static
  define("PARAM_COMBINE", "&"); // should be static

  define("CHALLENGE_PARAM", "challenge"); // name of challenge GET parameter
  define("RESPONSE_PARAM",  "response");  // name of response GET parameter

  define("DN_PARAM",      "TLS_DN");      // name of DN SERVER parameter
  define("SUCCESS_PARAM", "TLS_SUCCESS"); // name of success SERVER parameter

  define("LOGINSESSION_LOGGEDIN", "loginsession_loggedin");

  function getChallenge() {
    return openssl_random_pseudo_bytes(CHALLENGE_LENGTH);
  }

  function getResponse($challenge, $previous = false) {
    // prepare a delta time value for cases where we missed the variance window
    $delta = ($previous) ? CHALLENGE_VARIANCE : 0;

    // use the current time as a replay prevention mechanism
    $time_challenge = dechex((int)((time()-$delta) / CHALLENGE_VARIANCE));

    return hash_hmac(CHALLENGE_ALGO, $challenge.$time_challenge, CHALLENGE_KEY);
  }

  function check_GET() {
    $result = false;

    // check if the parameters are set
    if (isset($_GET[CHALLENGE_PARAM]) && isset($_GET[RESPONSE_PARAM])) {
      // convert challenge parameter to binary
      $challenge = hex2bin($_GET[CHALLENGE_PARAM]);
      // check if the challenge parameter was a hex
      if (false !== $challenge) {
        // calculate response from challenge parameter
        $response = getResponse($challenge, false);
        // check if response calculation was possible
        if (false !== $response) {
          // check if the calculated response matches the response parameter
          $result = (0 === strcasecmp($response, $_GET[RESPONSE_PARAM]));
        }

        // maybe we just passed the time variance window, test the previous one
        if (!$result) {
          // calculate response from challenge parameter
          $response = getResponse($challenge, true);
          // check if response calculation was possible
          if (false !== $response) {
            // check if the calculated response matches the response parameter
            $result = (0 === strcasecmp($response, $_GET[RESPONSE_PARAM]));
          }
        }
      }
    }

    return $result;
  }

  function check_SERVER() {
    $result = false;

    // check if the necessary TLS server values have been set
    if ((isset($_SERVER[DN_PARAM])) && (isset($_SERVER[SUCCESS_PARAM]))) {
      // check if the TLS server values contained the required content
      $result = ((0 === strcasecmp(DN_CONTENT,      $_SERVER[DN_PARAM])) &&
                 (0 === strcasecmp(SUCCESS_CONTENT, $_SERVER[SUCCESS_PARAM])));
    }

    return $result;
  }

  function createSession() {
    // open a session
    session_start();

    // session should exist now
    if (PHP_SESSION_ACTIVE === session_status()) {
      // prevent session fixation
      session_regenerate_id(true);

      $_SESSION[LOGINSESSION_LOGGEDIN] = true;
    }
  }

  function destroySession() {
    // check if there actually is a session to destroy
    if ((isset($_COOKIE[session_name()])) || (isset($_GET[session_name()]))) {
      // open the existing session
      session_start();

      // session should be open now
      if (PHP_SESSION_ACTIVE === session_status()) {
        // remove loggedin value from session
        if (isset($_SESSION[LOGINSESSION_LOGGEDIN])) {
          unset($_SESSION[LOGINSESSION_LOGGEDIN]);
        }

        // check if the session is empty
        if (0 === count($_SESSION)) {
          // destroy the session if it is
          session_unset();
          session_destroy();

          // unset the session cookie
          if (isset($_COOKIE[session_name()])) {
            $params = session_get_cookie_params();
            setcookie(session_name(), "", time()-3600, $params["path"], $params["domain"], $params["secure"], $params["httponly"]);
          }
        } else {
          // prevent session fixation
          session_regenerate_id(true);
        }
      }
    }
  }

  function doRedirect($target) {
    header("HTTP/1.1 302 Moved Temporarily");
    header("Location: ".$target);
  }

  function triggerChallengeResponse() {
    // generate new challenge
    $challenge = getChallenge();
    // check if challenge generation worked
    if (false !== $challenge) {
      // calculate response from challenge
      $response = getResponse($challenge, false);
      // check if response calucation worked
      if (false !== $response) {
        // convert challenge from binary to hex
        $challenge = bin2hex($challenge);

        // redirect to challenge-response
        doRedirect(PARAM_PATH.PARAM_START.CHALLENGE_PARAM.PARAM_VALUE.$challenge.PARAM_COMBINE.RESPONSE_PARAM.PARAM_VALUE.$response);
      }
    }
  }
?>

Die eigentliche Aufgabe, das Clientzertifikat zu prüfen, zur eigentlichen Webseite zu redirecten und dort die Session zu starten, übernimmt die Datei "logon.php". Diese gibt es zwei Mal, einmal auf dem Server, der die TLS-Clientauthentisierung übernimmt und einmal auf dem Server, auf dem WordPress installiert ist. Die in der Datei enthaltene Konfiguration sollte auf beiden Servern identisch sein:

<?php
  define("DN_CONTENT", "/cn=example.com"); // defines what text should be in TLS_DN

  define("CHALLENGE_VARIANCE", 300); // defines how long the challenge will be usable
  define("CHALLENGE_KEY",      "V3RY 5TR0NG P455W0RD"); // defines the password used for the response HMAC

  define("PARAM_PATH", "https://example.com/auth/logon.php"); // redirect path

  define("FINAL_PATH", "https://example.com/wp-login.php");

  require_once(dirname(__FILE__)."/auth.php");

  if (check_GET()) {
    createSession();
    print("<a href=\"".FINAL_PATH."\">Logon</a>");
  } else {
    if (check_SERVER()) {
      triggerChallengeResponse();
      exit();
    }
  }
?>

Es kann auch sinnvoll sein, die erhaltenen Privilegien wieder abzugeben, indem man die Session beendet. Diese Aufgabe übernimmt die Datei "logoff.php", die ebenfalls auf beiden Servern vorhanden und dieselbe Konfiguration enthalten sollte:

<?php
  define("DN_CONTENT", "/cn=example.com"); // defines what text should be in TLS_DN

  define("CHALLENGE_VARIANCE", 300); // defines how long the challenge will be usable
  define("CHALLENGE_KEY",      "V3RY 5TR0NG P455W0RD"); // defines the password used for the response HMAC

  define("PARAM_PATH", "https://example.com/auth/logoff.php"); // redirect path

  require_once(dirname(__FILE__)."/auth.php");

  if (check_GET()) {
    destroySession();
    print("logoff");
  } else {
    if (check_SERVER()) {
      triggerChallengeResponse();
      exit();
    }
  }
?>

In der eigentlichen Anwendung kann dann anhand der Session zwischen den Datenbanknutzern unterschieden werden. Im Falle von WordPress habe ich das in der Konfigurationsdatei "wp-config.php" getan:

$loginsession_evaluated = false;
// open the session if there is one
if ((isset($_COOKIE[session_name()])) || (isset($_GET[session_name()]))) {
  session_start();

  if ((isset($_SESSION["loginsession_loggedin"])) && ($_SESSION["loginsession_loggedin"])) {
    $loginsession_evaluated = true;
  }
}

// select user dependent on loginsession
if (($loginsession_evaluated) || (defined("DOING_CRON"))) {
  define("DB_USER",     "<ADMIN USER>");
  define("DB_PASSWORD", "<ADMIN PASSWORD>");
} else {
  define("DB_USER",     "<GUEST USER>");
  define("DB_PASSWORD", "<GUEST PASSWORD>");
}

Da das von der Theorie her vielleicht alles ein bisschen trocken ist, habe ich den Ablauf eines vollständigen Logins als Screencapture aufgenommen und bei Youtube hochgeladenen. Vielleicht wird dadurch klarer, wie die einzelnen Schritte ineinander greifen.

Ich hoffe, dieser kleiner Exkurs bietet dem ein oder anderen eine neue Lösungsmöglichkeit für die Absicherung von Nutzerzugängen. Die gezeigte Implementation habe ich, soweit wie möglich, generisch gehalten. Durch das Challenge-Reponse-Verfahren ist sie beispielsweise auch als Grundlage für Inter-Domain-Single-Sign-On geeignet und durch die Verwendung von einfachen Sessions sollte sie zudem softwareunabhängig einsetzbar sein.


Behandlung depublizierter Artikel in WordPress

03.09.2014 yahe administration code legacy wordpress

Vor kurzen wurde ich von einem Auftraggeber vor eine spannende Aufgabe gestellt: Eine Seite musste eine ganze Menge alter Artikel depublizieren. Die Besucher dieser Artikel sollten jedoch nicht einfach von einer 404-Fehlerseite abgefangen werden, sondern ihnen sollte mitgeteilt werden, weshalb der Fehler aufgetreten ist und sie sollten möglichst automatisch auf die Startseite umgelenkt werden.

Dabei gab es mehrere Hindernisse. Zum einen musste eine Datenbasis geschaffen werden, welche URLs nicht mehr verfügbar sind und es musste bei deren Aufruf der entsprechend angepasste Inhalt angezeigt werden. Anstatt jedoch anzufangen, die gesamte Seite abzugrasen, um an die URLs zu gelangen, hatte ich eine bessere Idee. Die Artikeldatenbank selbst würde diese Datenbasis darstellen.

Um die Artikel offline zu nehmen, sollten diese nicht gelöscht, sondern lediglich deren Sichtbarkeit auf "privat" gestellt werden. Damit könnte man eine generalisierte Lösung erstellen, die für alle privaten Artikelseiten funktioniert. Das gute an privaten Artikeln ist, dass sie bei externen Besuchern zur 404-Seite führen, so, als würde sie nicht existieren. Der Inhalt der 404-Seite wird durch die Datei "404.php" des Themes repräsentiert. Dort kann man sich einklinken und vor dem Anzeigen des Fehlers prüfen, ob der Fehler aufgetreten ist, weil die angeforderte Seite privat ist. Je nach Ergebnis der Prüfung kann man dann den einen oder den anderen Inhalt anzeigen.

<?php
  $private_404_args    = wp_parse_args($wp->matched_query);
  $private_404_content = "";
  $private_404_done    = false;
  $private_404_result  = 0;

  if (isset($private_404_args["p"]) || isset($private_404_args["name"])) {
    if ((!$private_404_done) && isset($private_404_args["p"])) {
      $private_404_result = $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM $wpdb->posts WHERE id = %d AND LOWER(post_status) = 'private' AND LOWER(post_type) = 'post';"), intval($private_404_args["p"]));

      $private_404_done = (intval($private_404_result) === 1);
    }

    if ((!$private_404_done) && isset($private_404_args["name"])) {
      $private_404_result = $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM $wpdb->posts WHERE LOWER(post_name) = LOWER(%s) AND LOWER(post_status) = 'private' AND LOWER(post_type) = 'post';", $private_404_args["name"]));

      $private_404_done = (intval($private_404_result) === 1);
    }
  }

  if ($private_404_done) {
    $private_404_content = $wpdb->get_var("SELECT post_content FROM $wpdb->posts WHERE LOWER(post_title) = '[410]' AND LOWER(post_status) = 'private' AND LOWER(post_type) = 'page';");

    if ($private_404_content != null) {
      print(apply_filters('the_content', $private_404_content));
    }
  } else {
    $private_404_content = $wpdb->get_var("SELECT post_content FROM $wpdb->posts WHERE LOWER(post_title) = '[404]' AND LOWER(post_status) = 'private' AND LOWER(post_type) = 'page';");

    if ($private_404_content != null) {
      print(apply_filters('the_content', $private_404_content));
    }
  }
?>

Die schlussendliche Lösung ist etwas komplizierter geworden. Eigentlich hatte ich vor, einfach per "url_to_postid()" herauszufinden, ob unter der URL, die aufgerufen wurde, ein Artikel existiert. Leider funktioniert die Funktion offenbar nicht bei privaten Artikeln, wenn sie von einem Besucher aufgerufen wurden.

Im Feld "$wp->matched_query" stehen jedoch glücklicherweise die Teile der erfolgreich erkannten URL-Teile, wie sie die Bildungsregeln in der WordPress-Konfiguration vorgeben. In meinen Fall wäre das z.B. "/%year%/%postname%/". Der Inhalt von "matched_query" wäre demnach für den aktuellen Artikel hier "year=2014&name=behandlung-depublizierter-artikel-in-wordpress". Dieser Wert kann mithilfe der Funktion "wp_parse_args()" dann weiter aufgedröselt werden. Anhand des Namens, der in WordPress eindeutig ist, kann wiederum der zugehörige Artikel in der "wp_posts"-Tabelle gefunden werden.

Da ich sowieso gerade dabei war, habe ich auch noch eine weitere Vereinfachung vorgenommen, nämlich die, den angezeigten Inhalt der entsprechenden Fehlerseiten über das WordPress-Backend bestimmen zu können. Dazu müssen zwei Seiten (nicht Artikel!) erstellt werden, deren Sichtbarkeit ebenfalls auf "privat" gesetzt wird. Die Seite, die für generelle 404-Fehlermeldungen genutzt werden soll, muss den Titel "[404]" erhalten (ohne Anführungszeichen). Die Seite, die für depublizierte Fehlermeldungen genutzt werden soll, muss hingegen den Titel "[410]" erhalten (ebenfalls ohne Anführungszeichen).


Monat der WordPress-Sicherheit ist beendet

01.09.2014 yahe legacy security wordpress

In den letzten Wochen habe ich einige Artikel zur Sicherheit ausgewählter WordPress-Plugins geschrieben. Nun wollte ich abschließend noch einmal ein Fazit ziehen.

Begonnen hatte alles damit, dass auf Golem.de im Abstand von wenigen Wochen über zwei weit verbreitete Plugins mit leicht auffindbaren und dennoch schwerwiegenden Fehlern berichtet wurde. Es hatte mich daher interessiert, wie schlecht es wirklich um die Codequalität bei weit verbreiteten Plugins bestellt ist. Also begab ich mich zur Liste der beliebtesten Plugins und begann, mir Plugins anzusehen. Dabei habe ich mich auf halbwegs aktuelle Plugins konzentriert, die mindestens 25.000 Downloads hatten. Außerdem sollte die Analyse schnell gehen. Es ging mir nicht darum, Fehler in der Ablauflogik der Plugins zu finden, sondern ich wollte die groben Schnitzer finden, also die Fehler, die einem sofort ins Auge springen. Glücklicherweise verfügt jedes Plugin über ein eigenes Trac-Repository, sodass man direkt online in den Quelltext gucken kann. Damit entfällt das langwierige downloaden, entpacken und im Editor öffnen. So kann man relativ schnell einzelne Plugins abarbeiten.

Ich habe mir etwa 100 Plugins angesehen, die sich vom Namen oder von der versprochenen Funktionalität spannend anhörten. Dabei sind die 12 unten aufgelisteten Plugins aufgefallen, die solche nennenswerten Löcher enthielten, wobei 2 davon Beifang sind. Hatte ich ein Plugin mit einer Lücke gefunden, habe ich mir auch die anderen Plugins des gleichen Autors angesehen.

Plugin Angriff Anzahl
WP RSS Aggregator XSS 141.960*
WP CSV Information Leakage 31.675*
WP Advanced Importer Unprivileged File Upload 12.041*
WP Ultimate CSV Importer Unprivileged File Upload 112.793*
Simplr Registration Form Plus+ Information Leakage 105.325*
Form Builder Unprivileged File Upload 88.218*
WordPress File Upload XSS 25.060*
WP Modal Login Privilege Escalation 34.763*
Quick Chat SQL Injection 207,488**
Quick Count SQL Injection 14,268**
Login With Ajax Privilege Escalation 273.635**
FormGet Contact Form XSS 159.545**
Total: 1.206.771
(*Stand vom 16.08.2014)
(**Stand vom 26.08.2014)

Bei der Durchsicht der Plugins sind mir mehrere Dinge aufgefallen:

  1. Viele Plugins sind offenbar eher zufällig "sicher" und nicht etwa, weil der Programmierer das geplant hätte. Oft scheitert ein Angriff nur daran, dass vor einem anfälligen Code eine WordPress-eigene Funktion aufgerufen wird, was zu einer Exception führt, weil sie zu dem Zeitpunkt noch nicht definiert ist. Dadurch wird häufig die Programmausführung vorzeitig beendet. Würden solche Aufrufe in eine eigene Funktion gekapselt werden, wären viel mehr Angriffe möglich.
  2. Nur wenige Plugins schützen sich gegen Cross-Site-Request-Forgeries. Sie enthalten derzeit eventuell keine Lücken, sollten diese jedoch durch eine Codeänderung eingeführt werden, ließen sie sich dadurch wesentlich leichter ausnutzen.
  3. Die Punkte 1.) und 2.) rühren oftmals daher, dass die Entwickler sich nicht darum sorgen, ob ihre PHP-Scripte direkt aufgerufen werden dürfen. Vielen Entwicklern ist offenbar nicht bewusst, dass sie ihre Entrypoints und Codepfade kennen und absichern sollten. Dies war die häufigste Fehlerkorrektur, die Plugin-Entwickler umgesetzt haben, wenn sie von mir angeschrieben wurden.
  4. Viele Plugin-Entwickler verlassen sich darauf, dass eingeloggte Nutzer gutartig sind. Während sie den Eingaben von nicht-eingeloggten Besuchern meist gänzlich misstrauen, werden Eingaben von eingeloggten Nutzern häufig weniger oder gar nicht geprüft. Daher sollte man sich beim Einsatz von Plugins gut überlegen, ob man "Subscriber" in seiner WordPress-Installation zulässt.
  5. WordPress bietet Entwicklern viele fertige Funktionen an, mit denen sie ihre Plugins sicherer machen können. Das reicht bis zu fertigen Referer- und Nonce-Prüfungen gegen XSRF-Angriffe. Mir ist jedoch auch aufgefallen, dass WordPress-Nonces relativ lange gültig sind, sich nur an der Zeit orientieren statt an nutzergebundenen Werten und damit potentiell mehrfach und von verschiedenen Nutzern verwendet werden könnten. Das führt dazu, dass ein Angreifer sich regelmäßig einen neuen Nonce holen und diesen für länger laufende Attacken verwenden könnte.

Die Kommunikation mit den einzelnen Plugin-Entwicklern war bisher durchweg positiv. Viele haben sich gefreut, dass sich jemand tiefer mit ihren Plugins auseinandergesetzt und sie über Probleme informiert hat. Die meisten haben in weniger als einer Woche das Problem behoben. Bei anderen Plugins warte ich darauf hingegen immernoch. Dort wurde ein Fix lediglich in Aussicht gestellt. Überrascht hat mich die Reaktion eines Entwicklers, der sein Plugin aufgrund von Zeitmangel aus dem Plugin-Repository entfernt hat. In meinen Augen war das ein mutiger Schritt.

Für Leute und Firmen, die WordPress professionell einsetzen, kann ich aufgrund der Ergebnisse eigentlich nur einen Rat geben. Wenn Fremdplugins eingesetzt werden, empfiehlt es sich, wenigstens kurz in deren Quelltext zu blicken. Guckt euch an, ob sichergestellt wird, dass die PHP-Scripte nicht direkt aufgerufen werden können. Damit ist bereits viel gewonnen, da der Plugin-Entwickler so viel mehr Kontrolle über den Programmablauf hat.


Bug in FormGet Contact Form Plugin erlaubt Persistive XSS

29.08.2014 yahe legacy security wordpress

Heute habe ich noch etwas spannendes für euch: eine Persistive-XSS-Lücke. Das bedeutet, dass man nicht jedem Benutzer den manipulierten Seiteninhalt unterschieben muss, sondern, dass diese permanent gespeichert wird und so bei jedem Aufruf der Originalwebseite aktiv wird. Gefunden habe ich diesen Fehler im FormGet Contact Form Plugin.

Das Plugin besteht nur aus der Datei "index.php" und bietet einen AJAX-Endpunkt in der Funktion "cf_text_ajax_process_request()". Dort werden einfach ungeprüft POST-Parameter in die Plugin-Konfiguration übernommen. Besonders spannend ist die Option "fg_embed_code", die über den POST-Parameter "value" gesetzt wird. Dabei handelt es sich um Code, der später über die Funktion "embeded_code()" in jeder Seite des Wordpress-Blogs eingebunden wird: Jackpot! Die Einbringung von eigenem Code funktioniert relativ einfach über ein HTML-Formular, das ein Angreifer einmalig benutzen muss:

<form action="http://example.com/wp-admin/admin-ajax.php" method="POST">
  <input type="hidden" name="action" value="request_response" />
  <input type="hidden" name="value" value="<script>alert('XSS.');//sideBar</script>" />
  <input type="hidden" name="value_hide" value="" />
  <input type="hidden" name="page_id" value="" />
  <input type="submit" />
</form>

Ich stand mit den Entwicklern in Kontakt, die mir auch mitgeteilt hatten, etwas auf der Serverseite gefixt zu haben. Auf meine Anmerkung hin, dass der Fehler im Plugin sei und nicht in ihrer Serveranwendung, erhielt ich jedoch keine Antwort mehr. Solange kein Patch verfügbar ist, kann der Rat deshalb nur lauten, das Plugin umgehend zu deaktivieren.


Search

Links

RSS feed

Categories

administration (40)
arduino (12)
calcpw (2)
code (33)
hardware (16)
java (2)
legacy (113)
linux (27)
publicity (6)
review (2)
security (58)
thoughts (21)
windows (17)
wordpress (19)