How I capture and monitor Wordpress attacks

Like probably everyone who maintains Wordpress installations I had to deal with massive (automated) attacks. Due to its significant market share (nearly a quarter of all websites) and its mostly inexperienced users Wordpress is one of the top targets for malicious attackers.

To get some insights in these attacks I decided to monitor and capture them. Of course I didn’t want to let others hack my existing Wordpress installations so I setup an extra honeypot system. I had three basic requirements for the honeypot:

  • It should be possible to use the standard Wordpress (without any modification)
  • It should capture and save attack attempts and successful attacks
  • An attacker should not be able to cause any harm via the compromised Wordpress (e.g. sending SPAM)

With these requirements in mind I designed the following setup:

Docker, Docker, Docker, …

My honeypot is entirely based on docker. The benefit of this is less overhead and a lot of flexibility. If you’re not familiar with docker I suggest reading this: I used three separate containers for my setup:

Before starting the different containers I created the following directory structure to store the data:

  • wordpress/db: mounted to the MySQL container, contains the database dump for rapid honeypot provisioning
  • wordpress/html: mounted to the Apache container, contains the webroot
  • wordpress/logs: mounted to the NGINX, contains the access and error logs
  • wordpress/nginx.conf: contains the NGINX configuration

The database container

Wordpress needs a SQL database so lets start with a MySQL docker container:

docker run -d --name "wordpress_db" \
-v "/home/honeypot/wordpress/db:/docker-entrypoint-initdb.d" \
-e MYSQL_DATABASE="wordpress_db" \
-e MYSQL_USER="db_user" \
-e MYSQL_PASSWORD="$db_pass" \
-e MYSQL_ROOT_PASSWORD="$db_root_pass" mysql:latest

The database is started with a unique user and root password. In addition I mounted a folder to “/docker-entrypoint-initdb.d”. This will come in handy later, when erasing and restarting the honeypot. The folder “/docker-entrypoint-initdb.d” is used to initialize a fresh MySQL instance. The docker instance will import all “*.sql” files inside this folder into the database instance.

The apache container

As already mentioned I almost used the default configuration, but I had to modify the standard Apache (php:apache) to include some necessary PHP extension. I created an extra Dockerfile:

FROM php:apache
# install the PHP extensions we need
RUN apt-get update && apt-get upgrade -y && apt-get install -y libpng12-dev libjpeg-dev && rm -rf /var/lib/apt/lists/* \
&& docker-php-ext-configure gd --with-png-dir=/usr --with-jpeg-dir=/usr \
&& docker-php-ext-install gd mysqli opcache
# set recommended PHP.ini settings
# see
RUN { \
echo 'opcache.memory_consumption=128'; \
echo 'opcache.interned_strings_buffer=8'; \
echo 'opcache.max_accelerated_files=4000'; \
echo 'opcache.revalidate_freq=2'; \
echo 'opcache.fast_shutdown=1'; \
echo 'opcache.enable_cli=1'; \
} > /usr/local/etc/php/conf.d/opcache-recommended.ini
RUN a2enmod rewrite expires
CMD /usr/sbin/apache2ctl -D FOREGROUND

After I built the container image with docker build -t apache-php . I could start the Apache. To make it possible to monitor any changes in the webroot I mounted it into the container:

docker run -d --name "wordpress_php" \
--link "wordpress_db:mysql" \
-v "/home/honeypot/wordpress/html":/var/www/html apache-php

The NGINX container

Last but not least the NGINX. The NGINX is used as a proxy in front of the Apache to isolate it. I also mounted the log directory and the NGINX configuration file.

The NGINX configuration is pretty basic and looks like this:

server {
listen 80;
    access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
    location / {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://apache:80/;

With this I am able to start the NGINX and access the Apache.

docker run -d --name "wordpress_nginx" \
--link "wordpress_php:apache" \
-v "/home/honeypot/wordpress/nginx.conf:/etc/nginx/conf.d/default.conf" \
-v "/home/honeypot/wordpress/logs:/var/log/nginx" \
-p 8080:80 nginx:latest

Okay, I now have three different containers. Let’s configure each so they can play together.

Setting up Wordpress

After every container is configured and started it is time to setup the Wordpress. This step is pretty just following the install wizard and choosing the super secure administration password “admin123”. After I convinced Wordpress that I really wanted to use such an insecure password I filled the blog with some dummy data. I used the data from (big thanks to Post Status for maintaining this) which provides good test data for my purpose.

Before making the honeypot world accessible I created a snapshot of the current setup to easily restore the honeypot after a compromise. I first created a dump of the database:

docker exec wordpress_db \
sh -c 'exec mysqldump --all-databases -uroot -p"$MYSQL_ROOT_PASSWORD"' > "/home/honeypot/wordpress/db/wordpress.sql"

The database dump is stored in the db directory of the honeypot, so it will be provisioned when I restart the database container. The next step is to backup the whole honeypot directory (ensure that this is not already compromised ;)). I simply used tar for this purpose:

tar -cpzf "/home/honeypot/wordpress.tar.gz" -C "/home/honeypot/wordpress" .

That’s all at this point.

Isolating the Apache

There is one more thing to do. As I mentioned above I don’t want my Wordpress to do any harm like sending spam or ddos’ing some sites when it is compromised.

I decided to disallow any incoming and outgoing network connections (except for those which are necessary). My servers are running a RHEL based Linux, so I used firewall-cmd to establish some firewall rules (okay, I admit these rules could probably a lot more elegant but I don’t speak iptables fluently):

# Allow communication between the NGINX proxy and the Apache
firewall-cmd --direct --add-rule ipv4 filter DOCKER-ISOLATION 0 -s <wordpress_php_ip> -d <wordpress_nginx_ip> -j ACCEPT
firewall-cmd --direct --add-rule ipv4 filter DOCKER-ISOLATION 0 -s <wordpress_nginx_ip> -d <wordpress_php_ip> -j ACCEPT
# Allow communication between the Apache and the MySQL
firewall-cmd --direct --add-rule ipv4 filter DOCKER-ISOLATION 0 -s <wordpress_php_ip> -d <wordpress_db_ip> -j ACCEPT
firewall-cmd --direct --add-rule ipv4 filter DOCKER-ISOLATION 0 -s <wordpress_db_ip> -d <wordpress_php_ip> -j ACCEPT
# Drop all outgoing communication from the Apache and the MySQL
firewall-cmd --direct --add-rule ipv4 filter DOCKER-ISOLATION 2 -s <wordpress_db_ip> -j DROP
firewall-cmd --direct --add-rule ipv4 filter DOCKER-ISOLATION 2 -s <honeypot_php_ip> -j DROP

The Apache and the MySQL are now completely isolated and only accessible via the NGINX proxy. Let’s go out and catch some attacks :)

Capturing attacks

This is probably the most interesting part. Before I can capture an attack I need to know when an attack was successful and the honeypot is compromised. Wordpress attacks mostly result in modification of existing files or the installation of malicious plugins or themes. So it is easy to check if some files have changed to recognize a successful attack.

I setup a cronjob on the host system that constantly checks the webroot for changes. It uses a little handy tool checksumdir. This tool calculates checksums for entire directories. With this I can calculate the current hash of the wordpress directory and compare it to the reference hash of the newly setup directory. If a change is recognized the cronjob performs the following:

  • Create a database dump and a snapshot of the current Wordpress instance:
docker exec wordpress_db \
sh -c 'exec mysqldump --all-databases -uroot -p"$MYSQL_ROOT_PASSWORD"' > "/home/honeypot/wordpress/db/wordpress.sql"
tar -cpzf "/home/honeypot/wordpress_$(date +%s).tar.gz" -C "/home/honeypot/wordpress" .
  • Stop and delete all honeypot containers
  • Delete the Wordpress directory and restore the initial snapshot
  • Start new honeypot containers

This is basically everything and this setup works pretty well. I currently check every 30 minutes if an attack occurred which is maybe to fast because I sometimes miss the following attacks. But in general this setup runs quite well and I am currently experimenting with some other CMS using the same setup.

An example of an captured attack is published here: