Due to recurring security issues with WordPress I wanted a some kind of wordpress vulnerability monitoring for our Wordpress implementations. The current monitoring setup is implemented with good old fossil Nagios. Despite of many better alternatives, Nagios still does a great job in alerting me through the aNag app. In this post I’ll describe a simple Nagios setup for continuously monitoring WordPress vulnerabilities. It’s pretty straightforward if you already know Nagios. Nevertheless the scripts I wrote to wrap things around should be re-usable (more or less) to be used in any other setup (ELK?).
Scanning WordPress is done with the wpscan tool, which can be downloaded from http://wpscan.org . Output of this tool is stored, transformed and displayed in the Nagios way of doing this. As a prerequisite you need to install this tool on your server (hint; use the RVM method) and have PHP/MySQL installed for different subcommands that will be called.
Nagios configuration
First we’re going to write the command, service and service template in Nagios. Check interval is set to once every 24 hours. And by using a parent service (template), you can easily create more checks for other WordPress instances. I copied the configuration from my generated config, which is created by Nconf, but shouldn’t make any difference in directly using this config.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
# The actual command define command { command_name check_wordpress_security command_line /opt/wpscan/wpscan.rb --update -u $ARG1$ | $USER1$/wpsecurity_store $HOSTNAME$ '$SERVICEDESC$' | $USER1$/wpsecurity_filter | $USER1$/wpsecurity_message } # Service check that will pop up in your nagios dashboard define service { service_description Check Wordpress security MySite check_command check_wordpress_security!http://www.mysite.com host_name myserver.virtual.hostingcompany.tld check_period 24x7 notification_period 24x7 event_handler_enabled 0 use security-service contact_groups +admins } # Following is a template for this specific check, so multiple sites are easily configured without repeating config define service { name security-service register 0 notes_url /nagios/security/show.php?hostname=$HOSTNAME$&desc=$SERVICEDESC$ max_check_attempts 2 check_interval 1440 retry_interval 720 notification_options w,u,c,r active_checks_enabled 1 passive_checks_enabled 1 notifications_enabled 1 check_freshness 0 check_period 24x7 notification_period 24x7 } |
The actual command(s)
When you look closely at the configured command you will see a lot of piping. I could have put all into one command, but that wouldn’t be re-usable in a future possible ELK setup. It’s also a bit easier to change these scripts to your needs, as its purpose is more obvious.
The actual scanning is done with /opt/wpscan/wpscan.rb –update -u $ARG1$. The output of wpscan is intended to be read by humans, so we need to convert this into something that Nagios understands. Note that this feature should be released in the future as it is announced in the 3.0 release of wpscan (https://github.com/wpscanteam/wpscan/issues/198). Until then we use our own filter for transforming which is defined in $USER1$/wpsecurity_filter . This filter will process the wpscan output to a JSON string which is passed to $USER1$/wpsecurity_message that will exit for your with the right exit code and message for Nagios. See gists below for this filtering and messaging to Nagios:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
#!/bin/env php <?php $data = stream_get_contents(STDIN); preg_match_all('/\[(\+|\!|i)][^\[]*/', $data, $sections); $lines = $sections[0]; $types = $sections[1]; $struct = array(); if (empty($lines)) { echo json_encode(array(array('text'=>'No output received from wpscan'))); exit; } foreach ($lines as $key => $line) { $text = preg_replace('/\[.\]/ ', '', $line); $type = $types[$key]; if ($type == '+') { continue; } if (preg_match('/We could not determine a version so all vulnerabilities are printed out/', $text)) { continue; } if (preg_match('/vulnerabilities identified from the version number/', $text)) { continue; } switch(true) { case preg_match('/The version is out of date, the latest version/', $text): $falsePositiveLine = $lines[$key - 1]; $info = $text; $text = preg_replace('/\[.\]/ ', '', $falsePositiveLine); $struct[] = array( 'text' => $text . $info, ); break; case $type == 'i'; // only if previous line contains a ! add info to that item $previousType = $types[$key -1]; if ($previousType != '!') { continue; } $pos = count($struct) -1; $previousStructItem = $struct[$pos]; $previousStructItem['text'].=$info; // Reassign to struct $struct[$pos] = $previousStructItem; break; default: $struct[] = array( 'text' => $text ); break; } } echo json_encode($struct); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#!/bin/env php <?php $data = stream_get_contents(STDIN); $lines = json_decode($data, true); if (count($lines) == 0) { echo 'OK - No vulnerabilities found'; exit(0); } echo sprintf('WARNING - (%s) Wordpress vulnerabilities found', count($lines)); exit(1); |
If you’ve read closely, you might already noticed a missing command, namely $USER1$/wpsecurity_store $HOSTNAME$ ‘$SERVICEDESC$‘. Reason to name this separately is because it is optional. From the outside it just moves STDIN to STDOUT, seemingly doing nothing. In its internals, it stores the output in a MySQL database, which can be accessed with a custom PHP script from the Nagios dashboard (by using the “notes_url” configuration directive). See gist below for this storage:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#!/bin/env php <?php $pass = '<secret>'; $user = 'nagios'; $db = 'wpsecurity'; $data = stream_get_contents(STDIN); $hostname = $argv[1]; $desc = $argv[2]; $mysqli = new mysqli('localhost', $user, $pass, $db); $stmt = $mysqli->prepare("INSERT INTO log (`datetime`, `output`, `server`, `desc`) VALUES (NOW(), ?, ?, ?)"); $stmt->bind_param('sss', $data, $hostname, $desc); $stmt->execute(); $stmt->close(); $mysqli->close(); echo $data; |
By using different subcommands we can easily replace the filter later on when wpscan 3.0 is released, also the wpsecurity_store is optional as it outputs exactly what it receives (so you can leave that out if you don’t want storage; also remove the notes_url from Nagios config if you do so).
Vulnerabilities overview
In the Nagios dashboard there is only room for just one sentence for the state of that service. Just naming the number of found vulnerabilities isn’t enough. That’s why we stored the output in MySQL. With a notes_url config we can redirect to a separate PHP file which will display the output generated by wpscan. Please adjust to your needs 🙂 as it is just bare minimum. In the example below are 2 PHP includes which can be downloaded from the SensioLabs repo https://github.com/sensiolabs/ansi-to-html.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
<?php include('AnsiToHtmlConverter.php'); include('Theme.php'); $pass = '<secret>'; $user = 'nagios'; $db = 'wpsecurity'; $host = $_GET['hostname']; $desc = $_GET['desc']; $mysqli = new mysqli('localhost', $user, $pass, $db); $stmt = $mysqli->prepare('SELECT `datetime`, `output` FROM log WHERE `server` = ? AND `desc` = ? ORDER BY `datetime` DESC limit 1'); $stmt->bind_param('ss', $host, $desc); $stmt->execute(); $stmt->bind_result($datetime, $output); $stmt->fetch(); $stmt->close(); use SensioLabs\AnsiConverter\AnsiToHtmlConverter; $converter = new AnsiToHtmlConverter(); ?> <html> <body bgcolor="#000000"> <?php echo nl2br($converter->convert($output)); ?> </body> </html> |
Nagios dashboard
If all went well (see /var/log/nagios/nagios.log for errors), you should be seeing something like screenshot below. Watch for the the document icon right from the service description. This link will lead you to the next screenshot.
One thought to “WordPress vulnerability monitoring”