PHPIDS für WordPress

Wie schon geschildert: Bloggen ist eine tolle Sache – und gleichzeitig übernimmt man mit seinem Blog einen kleinen Ausschnitt der Verantwortung für die Sicherheit des Internet. Jeder Blogger sollte sich um die Sichrheit seines Blogs Gedanken machen.

Im Kielwasser dieser Basteleien bin ich über einen c’t-Artikel auf PHP-IDS gestoßen. Das scheint mir – gerade im Kielwasser des WordPress-Wurmes – ein interessanter Ansatz zu sein. Es gab sogar einmal ein Plug-In, um PHP-IDS und WordPress zu verbinden, doch das Plug-In ist veraltet. Wenn’s um Sicherheit geht, ist hier Endstation.
Und weil die PHP-IDS-Entwickler richtig fleißig sind und ständig neue Versionen fertig werden, läuft dieser Artikel lieber als „Hilfe zur Selbsthilfe“.

Erster Schritt: Dateien auf den Server und Depperltest

Zuerst einmal die Dateien auf den Server spielen. Wer WordPress installieren kann, schafft das auch. Ich habe mir für die ersten Experimente ein „Sandkasten-Verzeichnis“ angelegt, d.h. ein Verzeichnis mit einem Zufallsnamen. Wenn ich (hoffentlich) heute abend mit der Installation fertig bin, wird es einfach wieder gelöscht. Dort gibt es jetzt ein Unterverzeichnis „phpids-xx“ (xx steht für die Versionsnummer – die werde ich später noch einmal verwenden :-)), und darin ein Verzeichnis „docs“ und eines „lib“. Aus der ganzen Hierarchie „docs“ brauche ich tatsächlich nur „docs/examples/example.php“, um zu überprüfen, ob alles seine Ordnung hat.
Wenn man diese Datei zunächst mit dem Browser aufruft, müsste man schon die erste Meldung von PHP-IDS bekommen. Tatsächlich ist das Ganze nicht sooo einfach: Möglicherweise müssen die Schreibberechtigungen an ein paar Dateien und Verzeichnissen geradegebogen werden. Aber auch hier gilt: Wer WordPress installieren kann, schafft das auch.
Also: Depperltest bestanden.

Beispiel-Konfiguration

In der „examples.php“ sind einige Konfigurations-Optionen „fest verdrahtet“. Ich vermute, daß weniger Server-Ressourcen gefressen werden, wenn die Konfiguration direkt aus der lib/IDS/Config/Config.ini gelesen wird, also werden erst einmal (testhalber) die beiden Dateien miteinander abgeglichen. Dabei stellt sich heraus, daß die Konfigurationsdatei den absoluten Pfad zu PHP-IDS kennen sollte. Pfade in PHP-Installationen sind immer so eine Sache, gerade bei reinen Webspace-Angeboten, bei denen sich mehrere Kunden einen Web-Server teilen. Also zunächst der nächste Test: Der Pfad in der Konfigurationsdatei fest eingestellt und die folgenden Zeilen aus der „example.php“ auskommentiert:

// $init->config[‚General‘][‚base_path‘] = dirname(__FILE__) . ‚/../../lib/IDS/‘;
// $init->config[‚General‘][‚use_base_path‘] = true;

Ein kleiner Test:

An error occured: XML data could not be loaded. Make sure you specified the correct path.

Aha… Nochmal. –> Tippfehler. Der vollständige Pfad in der Config.ini muß natürlich mit abschließendem Schrägstrich angegeben werden: „(Pfad)/phpids-xx/lib/IDS/„.
Schon läuft’s. Hier wird auch klar, warum die Versionsnummer im Pfad steht: Damit kann ich später einfach Upgraden: Neue Version „danebenlegen“ und an zwei Stellen die Versionsnummer korrigieren. Dazu später mehr.

Als nächstes möchte ich die examples.php ausdünnen, damit sie nicht versehentlich die Konfiguration überschreibt.
Damit das klappt, muß die Option zum Abschalten der Caches in die Config.ini:

; caching: session|file|database|memcached|none
caching = none

Jetzt kommt ein spannender Teil: Bevor die erste Konfiguration eingestellt werden kann, muß man sich überlegen, was man will.

Der dezente Hinweis

; !!!DO NOT PLACE THIS FILE INSIDE THE WEB-ROOT IF DATABASE CONNECTION DATA WAS ADDED!!!

wird jedenfalls beherzigt: Solange die Welt mich nicht vom Gegenteil überzeugt, gehe ich davon aus, daß logging in eine einfache Datei ausreicht. Außerdem möchte ich auch zu jedem Einbruchsversuch eine Email erhalten. Also: Keine DB-Daten in der Datei.
Übrigens empfiehlt das PHPIDS-Forum, die Datei nach „Config.ini.php“ umzutaufen und „; <?php die(); ?>“ vornanzustellen. Dies ist ein weiterer Schutz dagegen, die Datei von außen aufzurufen.

Wieder ein Test, und ich bekomme sogar eine Email. Nicht besonders leserlich, aber immerhin wäre im Ernstfall klar, daß hier irgendjemand seltsame Dinge versucht hat.

Das ist auch der richtige Zeitpunkt, um die aktuellsten Rules / Converter-Dateien von dern PHP-IDS-Webseite einzubauen.

Damit bin ich mit der Konfiguration so weit zufrieden und kann mich um die Integration mit WordPress kümmern.

Integration mit WordPress

Wenn alles glatt läuft, ist die Integration mit WordPress von hier aus extrem simpel: Eigentlich brauche ich nur die „example.php“ in den Kopf des WordPress-Templates kopiert und fertig ist die Laube. Naja… ein paar Anpassungen hier und da braucht’s schon noch, und das Ganze packt man sinnvollerweise nicht nur ins FrontEnd-Template, sondern auch ins Admin-Template. Also los:

Zunächst das FrontEnd: Im WordPress-Admin-Panel den Theme-Editor aufrufen. In einem „anständigen“ Theme kann man das Ganze genüsslich in die „header.php“ einbauen (vor „<!DOCTYPE…“)., und es funktioniert auf Anhieb. Allerdings ist es im Moment noch etwas hyper-sensibel: Bei jedem Zugriff wird ins Log geschrieben bzw. gemailt :-/

Die ersten Experimente zeigen, daß der PHPIDS-Impact der typischen WordPress-Cookies 7 beträgt. Entsprechende Ausnahme-Einträge in der Config.ini bereinigen die Scan-Ergebnisse. Die WordPress-Cookies heißen von Site zu Site anders. Die Parameter aus der Email zeigen die Namen, die in die entsprechenden Exception-Listen in der Config.ini einzutragen sind, etwa so:

exceptions[] = COOKIE.wordpress_logged_in_XYZ ; Vorsicht: der Name dieses Cookies ist von Site zu Site verschieden!

Ich gebe zu, daß mir der Gedanke an die Ausnahmen etwas Unwohl ist…

Hier sind die verbleibenden Gesamt-Kunstwerke:

Config.ini.php

; <?php die(); ?>
[General]

filter_type = xml

base_path = (pfad)/phpids-xx/lib/IDS/
use_base_path = true

filter_path = default_filter.xml
tmp_path = tmp
scan_keys = false

; in case you want to use a different HTMLPurifier source, specify it here
; By default, those files are used that are being shipped with PHPIDS
HTML_Purifier_Path = vendors/htmlpurifier/HTMLPurifier.auto.php
HTML_Purifier_Cache = vendors/htmlpurifier/HTMLPurifier/DefinitionCache/Serializer

; define which fields contain html and need preparation before
; hitting the PHPIDS rules (new in PHPIDS 0.5)
html[] = POST.__wysiwyg

; define which fields contain JSON data and should be treated as such
; for fewer false positives (new in PHPIDS 0.5.3)
json[] = POST.__jsondata

; define which fields shouldn’t be monitored (a[b]=c should be referenced via a.b)
exceptions[] = GET.__utmz
exceptions[] = GET.__utmc
exceptions[] = REQUEST.wordpress_logged_in_XZY ; Vorsicht: der Name dieses Cookies ist von Site zu Site verschieden!
exceptions[] = REQUEST.wp-settings-2
exceptions[] = COOKIE.wordpress_logged_in_XYZ ; Vorsicht: der Name dieses Cookies ist von Site zu Site verschieden!
exceptions[] = COOKIE.wp-settings-2

; PHPIDS should run with PHP 5.1.2 but this is untested – set
; this value to force compatibilty with minor versions
min_php_version = 5.1.6

; If you use the PHPIDS logger you can define specific configuration here

[Logging]

; file logging
path = tmp/phpids_log.txt

; email logging

; note that enabling safemode you can prevent spam attempts,
; see documentation
recipients[] = test@test.com.invalid
subject = „PHPIDS detected an intrusion attempt!“
header = „From: <PHPIDS> info@php-ids.org“
envelope = „“
safemode = true
urlencode = true
allowed_rate = 15

; database logging

wrapper = „mysql:host=localhost;port=3306;dbname=phpids“
user = phpids_user
password = 123456
table = intrusions

; If you would like to use other methods than file caching you can configure them here

[Caching]

; caching: session|file|database|memcached|none
caching = none
; expiration_time = 600

; file cache
; path = tmp/default_filter.cache

; database cache
; wrapper = „mysql:host=localhost;port=3306;dbname=phpids“
; user = phpids_user
; password = 123456
; table = cache

; memcached
;host = localhost
;port = 11211
;key_prefix = PHPIDS

PHP-Anweisungen zur Integration in WordPress

<?php
set_include_path(get_include_path() . PATH_SEPARATOR . ‚(pfad)/phpids-xx/lib/‘);

if (!session_id()) { session_start(); }

require_once ‚IDS/Init.php‘;

try {$request = array(‚REQUEST‘ => $_REQUEST,’GET‘ => $_GET,’POST‘ => $_POST, ‚COOKIE‘ => $_COOKIE);$init = IDS_Init::init(‚(pfad)/phpids-xx/lib/IDS/Config/Config.ini.php‘);

$ids = new IDS_Monitor($request, $init);
$result = $ids->run();

if (!$result->isEmpty()) {
require_once ‚IDS/Log/Composite.php‘;
require_once ‚IDS/Log/File.php‘;
require_once ‚IDS/Log/Email.php‘;

$compositeLog = new IDS_Log_Composite();
$compositeLog->addLogger(IDS_Log_File::getInstance($init));
$compositeLog->addLogger(IDS_Log_Email::getInstance($init));
$compositeLog->execute($result);
}
} catch (Exception $e) {
printf(
‚An error occured: %s‘,
$e->getMessage()
);
}
?>

Upgrade

Upgrade-Gelegenheiten rund um PHP-IDS gibt es zweierlei:

  • Man kann eine neue Version einspielen
  • Man kann die Converter- und Filter-Dateien austauschen

Das Leben rund um neue PHPIDS-Versionen haben wir uns durch die Versionierung der Verzeichnisse leicht gemacht: In drei bis vier Schritten ist alles geschafft:

  • Verstand einschalten: Passen die folgenden Schritte zu „meiner“ Installation? Fehlt etwas, ist etwas zu viel?
  • Die neuen Dateien müssen auf den Server. Einfach „neben“ die alten legen, so daß alle PHPIDS-Verzeichnisse „untereinander stehen“. Momentan wird nur das Verzeichnis „lib“ verwendet – alles andere kann man wohl löschen.
  • Die Config.ini (bzw. Config.ini.php) aus dem alten Verzeichnis ins neue kopieren.
  • An zwei Stellen die Konfiguration auf die neuen Verzeichnisse umstellen: Im WordPress-Adapter und in der Config.ini[.php]

Zweitens kann man die Dateien „default_filter.xml“ und „Converter.php“ einfach austauschen. Etwa im Wochentakt werden neue Versionen auf der PHP-IDS-Seite veröffentlicht.

Und jetzt? – oder: Warnende Worte zum Abschluß

Zunächst eine Bitte in eigener Sache: Bitte probiert das nicht an MEINER SEITE AUS! Ich bin kein Security-Experte, nur ein kleiner digitaler Heimwerker. Meine Einschätzung ist, daß die Site in den oberen 5-10% der sichereren WordPress Blogs rangiert – aber bei zig-tausenden WordPress Blogs heißt das nicht viel. Mein Ehrgeiz ist nur, besser zu sein als die Masse, damit meine Site nicht durch automatische Skripte zum Zombie gemacht wird und den Rest des Internet gefährdet. Das geht bei anderen Sites sicher leichter. Ein menschlicher Hacker findet überall einen Weg „rein“ , da hoffe ich, daß mein kleines „Reich“ hier einfach nicht interessant genug ist. Damit bin ich sicher auf dem gleichen Level wie tausende andere Hobbyblogger um mich herum. (Übrigens: nein, ich habe nicht genau die Konfiguration oben in Betrieb.)

Zum „Ausprobieren“ haben die PHP-IDS-Leute eine kleine Demosite aufgebaut – dort könnt ihr euch austoben.

Ansonsten: PHP-IDS ist nur eine Alarmanlage. Ob die Site dadurch tatsächlich sicherer wird oder nicht hängt davon ab, wie sorgfältig man die Aktionen beobachtet. Ein cooles Negativ-Beispiel ist im PHP-IDS Forum zu finden: „Now that you mentioned it, I think that is a real atack.“

Oh.

2 Kommentare

  1. Hi,
    interessanter Artikel,
    jedoch wird der Teil

    exceptions[] = REQUEST.wordpress_logged_in_XZY ; Vorsicht: der Name dieses Cookies ist von Site zu Site verschieden!
    exceptions[] = REQUEST.wp-settings-2
    exceptions[] = COOKIE.wordpress_logged_in_XYZ ; Vorsicht: der Name dieses Cookies ist von Site zu Site verschieden!
    exceptions[] = COOKIE.wp-settings-2

    so nicht funktionieren.
    Wordpress generiert nämlich Cookies mit dem Schlüssel wp-settings-x, wobei x eine fortlaufende Nummer ist.
    Wenn du deinen Cookie-Cache leerst und dich erneut einloggst, wirst du feststellen, dass x sich geändert hat und damit dein Zugriff blockiert wird.
    Dies gilt auch für Alle, die auf deine Seite kommen. Die haben stets andere x.
    Ich habe einen Workaround auf dem phpids forum gepostet.

    Weiterhin schreibst du:

    Ansonsten: PHP-IDS ist nur eine Alarmanlage. Ob die Site dadurch tatsächlich sicherer wird oder nicht hängt davon ab, wie sorgfältig man die Aktionen beobachtet.

    Ich sollte dich darauf hinweisen, dass man mit PHPIDS tatsächlich sein Blog auch schützen kann, indem man nämlich definiert, was bei einer Detection passiert.
    Nach dem Code

    if (!$result->isEmpty()) {

    hat man die Möglichkeit, bei sehr hohen Werten den Angreifer zu blocken, wobei man genauere Werte erst durch Beobachten ermitteln sollte / muss.
    Mit einem

    if ($result->getImpact()>20){
    die(‚Nicht so, Freundchen!‘);
    }

    hat man eine erste Maßnahme, kann aber mit Sessions einzelne Angreifer verfolgen und den Impakt kummulieren und damit noch genauer reagieren.

    Übrigens solltest du dir zum Thema WordPress-Sicherheit auf jeden Fall auch das WordPress securitywhitepaper zu Herzen nehmen.

  2. Danke!

    Die genannten Exceptions funktionieren jedenfalls, es gibt keinen „falschen“ Alarm mehr. Auf der anderen Seite: Völlig „tot“ kann der Mechanismus auch nicht sein, gelegentlich geht die Alarmanlage tatsächlich los. Hast Du Ideen, warum es „trotzdem“ funktioniert?

    Nochmal Danke!

Wie denkst Du darüber?