PRTG-Wordpress

Wenn eine Webseite mit Wordpress läuft, dann sollte man diese immer schön aktuell halten. Natürlich kann das ein "AutoUpdater" machen aber es hindert mich auch niemand daran, die Version zu ermitteln und der aktuellen Version zu vergleichen. Damit das immer wieder passiert, ist ein Skript hilfreich und für die regelmäßige Ausführung und Alarmierung nutze ich z.B. PRTG.

Diese Seite ist viel länger geworden, weil ich alle Zwischenschritte meiner Analyse mit dokumentiert und nicht wieder gelöscht habe. Wer also direkt nur ein Skript zur Prüfung der Version sucht, kann auch nach unten zu PRTG-Integration springen.

WP Version ermitteln

Zuerst will ich natürlich wissen, welche Version läuft. Als Administrator kann ich das im Portal gleich an mehreren Stellen sehen aber ich möchte ja nicht, dass sich ein Monitoring-Skript auch noch anmeldet. Also habe ich nach anderen Quellen gesucht, die aktuell installierte Version zu ermitteln. Die meisten Webseiten haben ja irgendwo doch ihre Versionsnummer im Pfad oder Dateien hinterlegt. Eine Suchmaschine liefert hier sehr schnell gute Werte, z.B.

So enthält der Links zum Stylesheet meist die Version.

Die Version steht auch noch an der den oder anderen Stelle. Die Frage ist einfach wie beständig das ist.

Da zumindest ich keine fremde Seiten sondern eigene Wordpress-Instanzen überwachen möchte, kann ich das ja aber steuern. Leider enthält PowerShell 7 keinen HTML-Parser mehr und auch kein ConvertFrom-Html. Hier muss ich dann entweder selbst nach Strings suchen, 3rd Party-Parser verwenden oder auf Powershell 5 zurückgehen. Wenn ich aber mal davon ausgehe, dass der HTML-Code halbwegs "pretty" formatiert ist, dann sollte eine String-Suche nach "<link rel="stylesheet" *>"  oder "type="text/css" funktionieren.

Es geht aber noch einfacher, denn die Seite enthält auch noch einen HTML-Header "Generator":

Diese Information "kann" ein Administrator natürlich auch verbergen

Damit versteckt man einen Weg, die Version einer Wordpress-Installation zu ermitteln aber es gibt ja noch viele andere Optionen. Das Problem einer unsicheren Version einer Wordpress Installation ist ja nicht, dass ein Angreifer die Version kennt sondern dass das Updates noch nicht ausgeführt wurde. Bandbreite ist günstig und wenn es eine bekannte Lücke in einem Wordpress gibt, dann wird ein Angreifer diese unbeachtet der gemeldeten Version ausprobieren. Ich habe noch keinen Dieb gesehen, der vorher die Stellung des Schloss prüft, ehe er die Klinke nieder drückt.

$htmlanswer = Invoke-WebRequest "https://www.netatwork.de" -Method GET
$generatorvalid = $htmlanswer.Content -match "<meta name=""generator"".* content=""WordPress (.*)""?.*""/>"
if ($generatorvalid) {
   "WPVersion: $($matches[1])"
}
else {
   "No WPVersion found"
}

Sofern es nicht durch den Administrator abgeschaltet ist, kann Wordpress die Seiten auch als "Feed" liefern, indem Sie hinter die URL noch ein "/feed" anhängen und damit die "computerlesbare" Version bekommen.

Beides sind XML-Dateien und können daher recht einfach geparsed werden. Leider liefert mit ein "Invoke-Restmethod" nur die Informationen unter "item" aber ich kann ja auch direkt den HTML-Content parsen.

PS C:\> ([xml](Invoke-WebRequest http://carius.info/feed).content).rss.channel.generator
https://wordpress.org/?v=5.8.1

Ich werde für meine weiteren Schritte erst einmal nur den Generator der übergeben URL nutzen. Sollte das mal nicht gehen, können Sie ja jederzeit einen der anderen Weg nutzen.

Neueste Version

Der zweite Teil ist die Ermittlung der aktuellen Version. Die Wordpress Software kann jeder einfach auf https://wordpress.org/download/ herunterladen. Die Seite enthält aber auch den Hinweis auf die aktuelle Version:

Der "Download-Button" samt Text könnte ich per PowerShell laden und parsen aber es geht noch einfacher. Wenn ich die URL https://wordpress.org/latest.zip aufrufe, dann zeigt mir der "Speichern unter..."-Dialog den richtigen Namen an:

"latest.zip" ist also einfach ein Redirect, den ich auch auswerten kann. Fiddler zeigt es sehr anschaulich.

Entsprechend ist auch hier ein PowerShell-Script schnell gebaut und über die Methode "HEAD" erspare ich mir sogar den Download und entlaste Wordpress.

PS C:\> (Invoke-WebRequest "https://wordpress.org/latest.zip" -Method head).headers."content-disposition"
attachment; filename=wordpress-5.8.1.zip

Ich gehe davon aus, dass der Dateiname auf die aktuelle "public"-Version verweist und sich nur die Nummer ändert. Ansonsten muss ich das Skript wieder anpassen.

Es gibt aber noch eine zweite Version. Im WP Admin Dashboard gibt es unter Updates die Anzeige der eigenen Version und die Information über eine eventuell neue Version:

Bein Druck auf "Check again" wird die URL http://<hostname>/wordpress/wp-admin/update-core.php?force-check=1 aufgerufen und in der PHP-Datei wird dann der folgende Core gestartet:

Die Funktion "wp_version_check" befindet sich dann in der Datei update.php die folgenden Zeilen ausführt

Die PHP-Seite holt sich von https://apiwordpress.org die Informationen über die aktuellen Versionen. Das können wir auch per PowerShell.

$wpversioncheck=Invoke-RestMethod http://api.wordpress.org/core/version-check/1.7/
$wpversioncheck.offers

response : upgrade
download : http://downloads.wordpress.org/release/wordpress-5.8.1.zip
locale : en_US
packages : @{full=http://downloads.wordpress.org/release/wordpress-5.8.1.zip;
no_content=http://downloads.wordpress.org/release/wordpress-5.8.1-no-content.zip;
new_bundled=http://downloads.wordpress.org/release/wordpress-5.8.1-new-bundled.zip; partial=False;
rollback=False}
current : 5.8.1
version : 5.8.1
php_version : 5.6.20
mysql_version : 5.0
new_bundled : 5.6
partial_version : False

Damit bekomme ich auch die aktuelle Version von Wordpress heraus. Der Text in "Generator" ist übrigens nicht hilfreich, denn wordpress.org scheint durchaus das "Eat your own Dogfood"-Modell, wie ein Abruf am 10. Sep 2021 zeigt. Offiziell war noch Version 5.8.1 aktuell aber wordpress.org selbst lief schon auf 5.9-alpha-51780.

Wordpress führt natürlich auch eine eigene Release-Seite und History.

Aus der "Releases"-Seite lässt sich die Versionsnummer auch "parsen", indem das Skript einfach die erste Spalte der ersten Tabelle, die zudem mit "class="releases latest">" gekennzeichnet ist.

Der Vorteil ist hier, dass in der zweiten Spalte auch noch das Release-Datum enthalten ist, so dass eine Auswertung sogar ein "Alter" ermitteln kann, seit dem ein Updates noch nicht installiert ist. Allerdings ist diese Seite zumindest am 10. Sep 2021 mit 726kByte schon deutlich größer, denn Wordpress führt alle je veröffentlichten Versionen mit auf.

Leider ist mit PowerShell 7 kein HTML-Parser mehr mit an Bord. Einfache Suchen lassen sich vielleicht per REGEX noch umsetzen aber HTML ist leider nicht immer per XML zu parsen.

Install-Module PowerHTML -Scope CurrentUser -ErrorAction Stop

Das Modul hat nur genau einen Befehl "ConvertFrom-Html" und mit dem passenden XPATH-Ausdruck kann ich direkt die Tabellenspalten auswerten.

$htmlanswer = Invoke-WebRequest "https://wordpress.org/download/releases/" -Method GET
$htmlpage = convertfrom-html $htmlanswer.content
$publiversion = $htmlpage.SelectNodes('//table[@class="releases latest"]//td')[0].innertext
$releasedate = $htmlpage.SelectNodes('//table[@class="releases latest"]//td')[1].innertext
$releaseage = ((get-date) - (get-date $releasedate)).days

Eine RSS/JSON/XML-Schnittstelle zur Versionsermittlung habe ich leider nicht gefunden.

Release History

Ich hatte schon gedacht, dass ich nun fertig bin und nur noch die Bausteine zusammenführen muss. Aber dann habe ich gemerkt, dass es nicht ausreichend ist, das Datum des letzten Release zu ermitteln. Das hilft zwar schon, um einen Webseitenbetreiber aufzuschrecken, wenn er mehrere Tage vergessen hat ein Update zu installieren.

Aber wichtiger finde ich eigentlich noch, wie alt schon die installierte Version ist. Dazu muss ich mir aber die komplette Historie aus der Webseite einsammeln, denn eine API mit einer XML/JSON-Datei habe ich nicht gefunden. Aber auch das ist per PowerHTML einfach möglich.

Achtung: Dieses "Screeenscraping" funktioniert nur, solange sich das Layout der Webseite nicht ändert.

Ich habe mir daher aus der HTML-Seite alle Tabellenzeilen extrahiert und dann die Spalten einzeln verarbeitet

[hashtable]$versionlist = @{}
[long]$versioncount=0
$tablerows=$htmlpage.SelectNodes('//table[@class="releases"]//tr')
foreach ($tablerow in $tablerows) {
   $versioncount++
   $versionlist[$tablerow.Childnodes[1].innertext] = @($versioncount=0,$tablerow.Childnodes[3].innertext)
}

Nun habe ich zu jeder Version als "Key" das Releasedatum als "Value" für die weitere Berechnung und die verschiedenen Versionen.

RSS als Release History

Nun habe ich weiter oben schon geschrieben dass Wordpress zu jeder Seite auch eine RSS-Version bereitstellt. Die lässt sich vielleicht noch besser parsen als eine HTML-Tabelle. Hier meine Tests vom 20. Sep 2021

#Die History-Page bekomme ich wie folgt
$historyhtml=Invoke-WebRequest https://wordpress.org/news/
$historyhtml.RawContentLength
161250

#Die RSS-Version ist leider etwas größer
$historyrss=Invoke-WebRequest https://wordpress.org/news/feed/
$historyrss.RawContentLength
258257

Wenn ich sie aber per Invoke-Restmethod lese, dann ist sie einfache zu parsen
$historyrest=Invoke-RestMethod https://wordpress.org/news/feed/
$historyrest.Count
20

$historyrest[0]

title : WordPress 5.8.1 Security and Maintenance Release
link : https://wordpress.org/news/2021/09/wordpress-5-8-1-security-and-maintenance-release/
creator : creator
pubDate : Thu, 09 Sep 2021 03:11:37 +0000
category : {category, category}
guid : guid
description : description
encoded : encoded
post-id : post-id

Hier sehe ich das Wordpress 5.8.1 Updates samt dem Datum in "pubDate", so dass ich einfach das Alter ausrechnen. Um Updates von Marketing-Meldungen zu unterscheiden, kann ich das Feld "Category" weiter auswerten. Es enthält Werte wie:

wp-briefing 
Events
Updates
polyglots
translation
wptranslationday
Releases
Security
Development
Month in WordPress
...

Neue Versionen und Sicherheitsupdates haben wohl immer ein "Releases", bzw. "Security". Daher bietet es sich an, darauf zu filtern und den aktuellsten Eintrag zu nutzen. Ganz so einfach ist es aber nicht, denn die Kategorie "Releases" tragen auch Beta-Versionen und man muss schon "Development" noch rausnehmen.

$historyrest `
| where {     ($_.category."#cdata-section" -contains "releases") `
         -and ($_.category."#cdata-section" -notcontains "development")}

title       : WordPress 5.8.1 Security and Maintenance Release
link        : https://wordpress.org/news/2021/09/wordpress-5-8-1-security-and-maintenance-release/
creator     : creator
pubDate     : Thu, 09 Sep 2021 03:11:37 +0000
category    : {category, category}
guid        : guid
description : description
encoded     : encoded
post-id     : post-id

title       : WordPress 5.8 Tatum
link        : https://wordpress.org/news/2021/07/tatum/
creator     : creator
pubDate     : Tue, 20 Jul 2021 17:43:25 +0000
category    : {category, category}
guid        : guid
description : description
encoded     : encoded
post-id     : post-id

Das Alter der aktuellen Version erhalten ich also mit:

$historyrest=Invoke-RestMethod https://wordpress.org/news/feed/
$currentversion = $historyrest `
| where {     ($_.category."#cdata-section" -contains "releases") `
         -and ($_.category."#cdata-section" -notcontains "development")} `
| select -first 1

$Age=[int]((get-date) - (get-date $currentversion.pubDate)).totaldays

Leider sehe ich hier nicht die Version selbst. Dazu muss ich doch wieder den Code von weiter oben verwenden

$wpversioncheck=Invoke-RestMethod http://api.wordpress.org/core/version-check/1.7/
$wpversioncheck.offers.current

Auswertung

Mit den Daten aus der Vorarbeiten kann ich mir nun überlegen, welche Situationen ein Grün, Gelb oder Rot als Status ergeben. Ich gehe davon aus, dass man immer die aktuellste WordPress-Version installieren sollte. Neben Funktionsupdates werden doch häufiger Sicherheitsupdates mit verteilt. Da WordPress als PHP-Datei ja "offener Sourcecode" ist, können auch Angreifer schnell die Änderungen vergleichen und auf die Lücke schließen.

  • InstalledVersion = PublicVersion gleich dann...
    gibt es nicht zu tun. Alles ist ok
  • InstalledVersion < PublicVersion dann...
    wenn seit <$maxage Tage, dann Gelb
    dann Rot, weil installierte Version älter als $maxage Tage.

Ich beschränke mich aber immer auf die aktuelle Version und berücksichtige auch keine Branches oder Beta-Versionen.

Bei der Berechnung des Alters gibt es noch eine kleine Lücke. Wenn z.B. zwei neue Versionen kurz hintereinander kommen, dann bewerte ich nur das Alter der "neuesten" Version. Es kann dann aber schon sein, dass die installierte Version schon älter als MaxAge ist.

01.9.2021  Die Version 1.0 wird installiert und ist aktuell
20.9.2021  Die neue Version 1.1 wird veröffentlicht. Das Skript meldet 0 Tage
23.9.2021  Das Skript meldet nun, dass die neue Version schon 3 Tage verfügbar ist
25.9.2021  Die neue Version 1.2 wird veröffentlicht. Das Skript meldet wieder 0 Tage "Alter"

Das Problem könnte man nur lösen, indem man von allen Versionen das Releasedatum mit in die Bewertung mit einbezieht. Ein Skript könnte dann auch noch die Zwischenversionen und den Grade der Wichtigkeit bei Security Updates mit einbeziehen.

Beispielskript

Mit all der Vorarbeit ist es nun natürlich trivial, ein PowerShell-Script zu bauen, welche diese Informationen sammelt und als Ausgabe neben einer Information einfach das Alter einer neueren Version auszugeben. Das Skript können sie dann z.B. regelmäßig durch ihr Monitoring aufrufen lassen, um das Alter in Tagen als numerischen Wert zu erfassen und mit selbst vorgegeben Grenzwerten den Status des Sensors auf Warnung oder Alarm zu stellen. Auch die Benachrichtigung kann ihr Monitoring übernehmen.

prtg-wordpress.20220415.ps1.txt

Die Steuerung des Skripts erfolgt per Parameter, die das aufrufende Programm übergeben kann oder sie passen das Skript einfach für ihre Umgebung an.

Sie sollten natürlich zumindest die URL auf ihre eigene Wordpress-Seite anpassen.

Die Ausgabe ist dann wieder recht unspektakulär:

Interessant wird es nun, wenn Sie den Status und das Alter in ihre Monitoring-Lösung übertragen.

PRTG-Integration

Das PowerShell-Script können Sie auch auf ihrem PRTG-Server einspielen.

Denken Sie daran, dass viele PRTG-Installationen immer noch 32bit sind und PRTG die klassische 32bit PowerShell 3.0 und nicht die neue PowerShell Core (6+) oder 64bit startet.

Kopieren sie die PS1- Datei einfach nach "C:\Program Files (x86)\PRTG Network Monitor\Custom Sensors\EXEXML" und stellen Sie sicher, dass Sie nicht noch die Zoneneinstellung "Internet" vom Download trägt. Die URL ihrer Webseite können Sie im Skript selbst anpassen oder als Parameter angeben.

Ich habe das Skript mit einer kleinen "PRTG-Erkennung" ausgestattet.

Sie können das Skript daher auch einfach in einer PowerShell 3-Shell im PRTG-Verzeichnis starten und das Verhalten und mögliche Fehler sehen:

Kopieren Sie es einfach nach EXEXML auf einer Probe und addieren Sie einen Sensor und binden:

Bitte achten Sie drauf, dass Sie den Check nicht jede 60 Sekunden machen. Das würde nur viel Last bei Wordpress generieren.

Weiterentwicklung

Wordpress besteht nicht nur aus dem eigentlichen Wordpress-Programm sondern erlaubt die Erweiterung durch Plug-ins. Dadurch hat sich ein sehr großer Markt an Third-Party-Code gebildet, mit dem Wordpress Administratoren ihre Umgebung flexibel erweitern können. Allerdings sind diese Plug-ins natürlich "PHP-Code", der auf dem Server ausgeführt wird. Die Qualität dieser Plug-ins ist nicht immer hoch, da es keine Kontrolle oder Zertifizierung gibt. Auch ist die Versuchung groß, ein ähnliches kostenfreies Plug-ins einem kommerziellen Plug-ins vorzuziehen.

Weitere Links