Accueil How I Automated Wapiti to Find Vulnerable WordPress Plugins
Post
Annuler

How I Automated Wapiti to Find Vulnerable WordPress Plugins

Introduction

This is not the first time I have automated Wapiti in order to find vulnerabilities : In September 2019, I conducted some mass vulnerability scanning on the Internet to figure out how effective the XSS capabilities of Wapiti were and to discover bugs in my code.

With 18650 websites scanned each day over a period of one month, I found numerous vulnerabilities. I submitted my discoveries to the OpenBugBounty website which quickly made me reach the 1st place on the website.

During this research I found several kinds of vulnerabilities :

  • unknown vulnerabilities in custom web applications
  • known vulnerabilities in popular applications
  • unknown vulnerabilities in (not so) popular applications

As it was obvious that Wapiti was capable enough to find vulnerabilities in closed or OSS software, I asked myself

What if I scanned all WordPress plugins to find vulnerabilities ?

Here we go again

Ok, let’s do it. But where to start ?

First, the list of WordPress plugins: fortunately WordPress keeps an SVN with all existing plugins, almost 100k last time I checked :

https://plugins.svn.wordpress.org/

Second, I need the stack to operate the WordPress. It means Apache / PHP / MySQL. A bonus point if I can automate the plugin installation in an easy way.

It turns out there is a CLI tool called WP-CLI that does what I need.

It is also easy to find a Docker Compose file that will set up containers with the CLI tool and the required stack.

And finally… How will I scan each plugin ?

Life is about making choices

Let’s be honest : most WordPress plugins are only designed with the administrator in mind.

Stuff like rich editors or user management expose URLs that only the administrator can access, not visitors.

Do I really want to connect as administrator to the WordPress using an automated scanner and taking the risks to accidentally delete stuff, break the database or more ? No.

Also, some plugins require human interaction. A plugin to include a poll within a post doesn’t have any effect before you actually fill the poll in the administrator dashboard.

Can I create a script that will guess what to write in the database to fill such data ? No.

So I decided to scan only the visible part of the WordPress. I will of course miss some vulnerabilities, but it would make things a lot easier.

Problems

With my initial setup I quickly noticed several issues.

The most annoying issue was that some WordPress plugins will actually break your WordPress installation :

  • broken database structure that cause a PHP error and prevent WordPress from running
  • script trying to fetch some data on some server that doesn’t exist anymore
  • htaccess designed to block access to the WordPress in different ways
  • script that eats all your RAM before eating your swap and kill your computer

Therefore, I could not rely on WP-CLI for uninstalling plugins. I had to do the cleanup in a more basic way : restoring files and database.

Docker doesn’t provide a snapshot mechanism meaning I had to write a script to do that for me, working on a mounted volume.

One secondary problem was tied to permissions. With the volume mounted on my disk, files inside the volume were tied to non-existent UID/GID (because for example, Apache used UID 33 in the container).

It was annoying because if I wanted to read/edit some files I had to use the sudo command for that. The less I use sudo / sudoers the safer I feel, so I had to edit the Dockerfiles of containers to change the default UID.

Docker

Here is the docker-compose file that specifies how containers work together :

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
version: '3'
services:
  wordpress:
    build:
      dockerfile: Dockerfile.wordpress
      context: .
    depends_on:
      - mysql
    links:
      - mysql
    ports:
      - 8000:80
    restart: always
    environment:
      WORDPRESS_DB_HOST: mysql:3306
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD: wordpress
      WORDPRESS_DB_NAME: wp-app
      WORDPRESS_DEBUG: 1
      WORDPRESS_TABLE_PREFIX: wp_
      WORDPRESS_CONFIG_EXTRA: |
        define('FS_METHOD', 'direct');
        define('SCRIPT_DEBUG', true);
    volumes:
      - ./.srv/wordpress/:/var/www/html
      - ./.srv/log/:/var/log
      - ./custom.ini:/usr/local/etc/php/conf.d/custom.ini

  wpcli:
    depends_on:
      - mysql
      - wordpress
    build:
      dockerfile: Dockerfile.wpcli
      context: .
    links:
      - mysql:mysql
    entrypoint: wp
    command: "--info"
    environment:
      WORDPRESS_DB_HOST: mysql:3306
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD: wordpress
      WORDPRESS_DB_NAME: wp-app
      WORDPRESS_DEBUG: 1
      WORDPRESS_TABLE_PREFIX: wp_
      WORDPRESS_CONFIG_EXTRA: |
        define('FS_METHOD', 'direct');
    volumes:
      - ./.srv/wordpress/:/var/www/html
      - ./.srv/log/:/var/log
      - ./custom.ini:/usr/local/etc/php/conf.d/custom.ini

  mysql:
    build:
      dockerfile: Dockerfile.mysql
      context: .
    restart: always
    ports:
      - 3306:3306
    volumes:
      - "./.srv/database:/var/lib/mysql"
    environment:
      MYSQL_ROOT_PASSWORD: wordpress
      MYSQL_DATABASE: wp-app
      MYSQL_USER: wordpress
      MYSQL_PASSWORD: wordpress

The wordpress container needs a custom.ini file which is the PHP configuration file.

Here it is :

1
2
3
4
5
6
7
file_uploads = On
memory_limit = 3072M
upload_max_filesize = 3072M
post_max_size = 3072M
max_execution_time = 1200
max_input_vars = 2000
default_socket_timeout = 7

I fixed the problem of scripts fetching external resources by setting a socket timeout.

Note also that the docker-compose sets WORDPRESS_DEBUG to true meaning that PHP warnings are displayed, which makes it handy for error-based SQL injections.

Still, this is not the default behavior so once the vulnerability is found you must set it to false and check if the vulnerability is exploitable with blind techniques.

Here is the Dockerfile for WordPress :

1
2
3
4
5
6
7
8
9
# Use the official WordPress image as the base image
FROM wordpress:latest

# Set the desired UID and GID for the www-data user
ARG WWW_DATA_UID=1000
ARG WWW_DATA_GID=1000

# Create a new user with the specified UID and GID
RUN usermod -u $WWW_DATA_UID www-data && groupmod -g $WWW_DATA_GID www-data

The one for WP-CLI :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Use the official WordPress image as the base image
FROM wordpress:cli

# Set the desired UID and GID for the www-data user
# ARG WWW_DATA_UID=1000
# ARG WWW_DATA_GID=1000

# Set the desired UID and GID for the custom user
ARG CUSTOM_UID=1000
ARG CUSTOM_GID=1000

USER root
# Create the custom user
RUN addgroup -g $CUSTOM_GID custom-group && \
    adduser -D -u $CUSTOM_UID -G custom-group -s /bin/sh -h /var/www custom-user

    # Change ownership of the application directory to the existing www-data user and group
RUN chown -R custom-user:custom-group /var/www/html
USER custom-user

And finally, the one for MySQL :

1
2
3
4
5
6
7
8
FROM mysql:5.7

# Set the desired UID and GID for the www-data user
ARG MYSQL_UID=1000
ARG MYSQL_GID=1000

# Create a new user with the specified UID and GID
RUN usermod -u $MYSQL_UID mysql && groupmod -g $MYSQL_GID mysql

A problem that I didn’t fix is the logs : they are written by the root user so cleaning them requires sudo.

They also grow quickly, so I had to regularly empty them.

One improvement would be to clean them after uninstalling each plugin. We could also rely on logs to spot vulnerabilities.

Setting up the full thing

Here is the Bash script which was launching the containers, configuring the WordPress and creating a backup of the initial state.

1
2
3
4
5
6
7
8
9
10
11
12
13
docker compose up -d
sleep 20
docker-compose run --rm wpcli core install --title="My Site" --admin_user=admin --admin_password=changeme --admin_email=me@example.com --url=http://localhost:8000/ --allow-root
docker-compose run --rm wpcli term create category fun --description="This is gonna be fun"
docker-compose run --rm wpcli user create bob bob@example.com --role=author
docker-compose run --rm wpcli post create --post_title='Bob post' --post_content='Hello world' --meta_input='{"key1":"hi","key2":"there"}' --post_status=publish --post_category=fun --post_author=2 --tags_input=hello,world
docker-compose run --rm wpcli plugin delete akismet
docker-compose run --rm wpcli plugin delete hello
docker compose run --rm wpcli option update comment_previously_approved 0
mysqldump -u root -pwordpress -h 127.0.0.1 wp-app > dump.sql
find .srv/wordpress -type d -exec chmod 777 {} \;
rm -rf wordpress_backup 2> /dev/null
cp -r .srv/wordpress wordpress_backup

Wapiti

Here is the script responsible for installing plugins, launching Wapiti and restoring the initial state :

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
import os
from os.path import isfile
import subprocess

with open("plugins.txt") as fd:
    for plugin in fd:
        plugin = plugin.strip()

        if isfile(f"scans/{plugin}.json"):
            continue

        try:
            subprocess.check_output(
                    [
                        "docker-compose", "run", "--rm", "wpcli", "plugin", "install", plugin, "--activate", "--allow-root"
                    ],
                    stderr=subprocess.STDOUT,
            )
        except subprocess.CalledProcessError as exception:
            if "plugin could not be found" in exception.output.decode(encoding="utf-8", errors="ignore"):
                # Allows to skip non-existent plugins if the script crash
                with open(f"scans/{plugin}.json", "w") as fd_out:
                    fd_out.write("{}")
                continue

        print(f"Installing {plugin}")
        subprocess.run(
                [
                    "wapiti",
                    "-u", "http://localhost:8000/",
                    "--color",
                    "--flush-session",
                    "-m", "exec,file,permanentxss,redirect,sql,ssrf,upload,xss,xxe",
                    "-f", "json",
                    "-o", f"scans/{plugin}.json",
                ]
        )

        os.system("/usr/bin/bash ./reinit.sh")

Restore state

Here is the script that restored the initial state :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/bash
# remove content of wordpress directory
find .srv/wordpress/ -type f -name '.*' -exec rm -f {} \;
find .srv/wordpress/ -type d -name '.*' -exec rm -rf {} \;
rm -rf .srv/wordpress/*

# restore original files
cp -r wordpress_backup/* .srv/wordpress/

# get rid of permission issues
find .srv/wordpress/ -type d -exec chmod 777 {} \;

# drop the db
mysqladmin -u wordpress -pwordpress -f -h 127.0.0.1 drop wp-app
# recreate it
mysqladmin -u wordpress -pwordpress -f -h 127.0.0.1 create wp-app
# restore original data
mysql -u wordpress -pwordpress -h 127.0.0.1 wp-app < dump.sql

I had to remove everything under the web root but not the web root itself as Apache seems to keep the inode to that folder.

Removing the web root directly made Apache go crazy.

Waiting

I started the scans in August 2023. If WordPress plugins weren’t causing any troubles I could have installed them in bulks of 10 before scanning the WordPress with Wapiti.

Unfortunately if a single plugin breaks the WordPress it would have hidden potential vulnerabilities for other plugins.

I finally split the list of plugins over 3 computers and I let the scans run till January 2024.

Vulnerabilities

I found a total of 36 vulnerabilities. No crazy stuff like a vulnerability that would impact 50% of existing Wordpress websites : most plugins here are old and not popular enough.

None of those vulnerabilities were previously found.

Note that while I may be the first to use a vulnerability scanner to scan WordPress plugins, some people are using code analysis tools to find vulnerabilities. That method may have missed vulnerabilities that Wapiti was able to find.

Still it shows that Wapiti is effective when it comes to vulnerability scanning.

Here is the list of vulnerabilities, each one has been documented on my website :

SQL injection in ActivityTime

SQL injection in Article Analytics

Cross-Site Scripting in AMP Plus

Cross-Site Scripting in Bee Classifieds

Cross-Site Scripting in Charjing For Subscription Billing

Open-Redirect in Clik Stats

Cross-Site Scripting in Clickbank WordPress Storefront

Cross-Site Scripting in Comments Link Optimization

Cross-Site Scripting in easy AMP

Cross-Site Scripting in EchBay Admin Security

SQL injection in FS Product Inquiry

Cross-Site Scripting in FS Product Inquiry

Cross-Site Scripting in JP Theme Switcher Bar

File inclusion in JS Job Manager

SQL injection in LeaderBoard Lite

Cross-Site Scripting in LH Login Page

Cross-Site Scripting in Like DisLike Voting

SQL injection in LogDash Activity Log

SQL injection in Loginplus

Open-Redirect in Multipurpose CSS3 Animated Buttons

Cross-Site Scripting in NanoSupport Support Ticketing Knowledgebase for WordPress

Cross-Site Scripting in NS Simple Intro Loader

SSRF in OpenID

Cross-Site Scripting in Peters Custom Anti-Spam Image

Cross-Site Scripting in Post Like Dislike

SQL injection in Posts Logs And Tracking

File inclusion in PS PHPCaptcha WP

Cross-Site Scripting in qrLogin

Cross-Site Scripting in Real Simple Contact Form

Cross-Site Scripting in Reservation

Cross-Site Scripting in TM Wordpress Redirection

Cross-Site Scripting in Valz Display Query Filters

Cross-Site Scripting in Verifyne

SSRF in Webmention

File inclusion in WP-Game

Cross-Site Scripting in XYZZY Basic SEO and-Analytics

Cet article est sous licence CC BY 4.0 par l'auteur.