Anatomy
WordPress has a small core, a handful of always-present PHP entry points, and one big directory where everything interesting lives: wp-content/. Plugins are PHP files in wp-content/plugins/<name>/, themes are PHP files in wp-content/themes/<name>/, and uploads land in wp-content/uploads/. Every operator-relevant path follows this structure.
/var/www/html/├── index.php # all dynamic requests route through here├── wp-config.php # DB creds, salts - the looting prize├── wp-login.php # login page (form-based auth)├── xmlrpc.php # XML-RPC API - brute-force/SSRF target├── wp-admin/ # admin dashboard├── wp-includes/ # core libraries└── wp-content/ ├── plugins/<plugin>/ # plugin code - usually contains vulnerabilities ├── themes/<theme>/ # theme code - RCE target via theme editor └── uploads/<year>/<month>/ # media uploads, often directory-indexedSuccess indicator: you can mentally map any WordPress URL to a filesystem path in seconds. target.com/wp-content/plugins/foo/bar.php → /var/www/html/wp-content/plugins/foo/bar.php.
Default file structure
Section titled “Default file structure”After a standard install on Linux/Apache/MySQL/PHP, the webroot (usually /var/www/html/) contains:
.├── index.php├── license.txt├── readme.html├── wp-activate.php├── wp-admin/├── wp-blog-header.php├── wp-comments-post.php├── wp-config.php├── wp-config-sample.php├── wp-content/├── wp-cron.php├── wp-includes/├── wp-links-opml.php├── wp-load.php├── wp-login.php├── wp-mail.php├── wp-settings.php├── wp-signup.php├── wp-trackback.php└── xmlrpc.phpKey files
Section titled “Key files”Entry points (PHP files at the webroot)
Section titled “Entry points (PHP files at the webroot)”| File | What it does | Operator interest |
|---|---|---|
index.php | Front controller - all dynamic page requests route through here | Implicit - every URL like /?p=42 ultimately runs through this |
wp-login.php | Login form (and password reset flow) | Primary auth surface; brute-force target |
xmlrpc.php | XML-RPC API endpoint (legacy; partly replaced by REST) | User enum, brute-force without rate limit, SSRF via pingback.ping |
wp-admin/ | Dashboard, admin functions, AJAX handlers | Post-auth attack surface; theme/plugin editors live here |
wp-cron.php | Time-based job runner triggered on requests | Sometimes exploitable when scheduled tasks process untrusted data |
wp-trackback.php | Trackback handler (mostly defunct, occasional CVE) | Rarely useful but check if present |
wp-signup.php | New blog signup on multisite installs | If 200 OK and the form is enabled, multisite - different config surface |
readme.html | Version banner | Discloses WP version on installs that didn’t delete it |
license.txt | License text | Sometimes contains version info |
wp-config-sample.php | Template config | Confirms WP install but contains no real secrets |
Documents and dotfiles you’ll look for after RCE
Section titled “Documents and dotfiles you’ll look for after RCE”| File | Why |
|---|---|
wp-config.php | The looting prize - DB host/user/pass/name + secret keys |
.htaccess | Rewrite rules, sometimes credentials in RewriteCond for HTTP basic |
wp-admin/.htaccess | Per-directory access rules - sometimes IP allowlists or HTTP basic creds |
wp-content/uploads/ | Recent uploads - sometimes accidentally uploaded sensitive files |
wp-content/debug.log | Created if WP_DEBUG_LOG=true; contains stack traces with sensitive paths |
wp-content/backup*/, wp-content/backups/, wp-content/uploads/backup*/ | Various backup plugins put dumps here, often world-readable |
Login-page renaming
Section titled “Login-page renaming”Several security plugins rename the login page to make brute-force harder by obscurity. Common patterns:
/wp-admin/login.php/wp-admin/wp-login.php/login.php/wp-login.php/admin//admin-login//secret-login-<random>/When wp-login.php returns 404 but the rest of the site is clearly WordPress, the page has been moved. Try common renames; check the WPS Hide Login plugin’s known patterns; if all else fails, fall back to xmlrpc.php (which is harder to “hide” because plugins depend on it).
The configuration file
Section titled “The configuration file”wp-config.php is the single highest-value file on a WordPress server. It contains:
<?php/** Database settings */define( 'DB_NAME', 'wp_production' );define( 'DB_USER', 'wp_user' );define( 'DB_PASSWORD', 'P4ssw0rd_db' );define( 'DB_HOST', 'localhost' );define( 'DB_CHARSET', 'utf8mb4' );define( 'DB_COLLATE', '' );
/** Authentication keys and salts - used for cookie signing */define( 'AUTH_KEY', '<random 64-char string>' );define( 'SECURE_AUTH_KEY', '<random 64-char string>' );define( 'LOGGED_IN_KEY', '<random 64-char string>' );define( 'NONCE_KEY', '<random 64-char string>' );define( 'AUTH_SALT', '<random 64-char string>' );define( 'SECURE_AUTH_SALT', '<random 64-char string>' );define( 'LOGGED_IN_SALT', '<random 64-char string>' );define( 'NONCE_SALT', '<random 64-char string>' );
/** Table prefix - relevant if you're SQL injecting */$table_prefix = 'wp_';
/** Debug - can leak verbose info if true */define( 'WP_DEBUG', false );define( 'WP_DEBUG_LOG', false );define( 'WP_DEBUG_DISPLAY', false );
/** Absolute filesystem path */if ( ! defined( 'ABSPATH' ) ) { define( 'ABSPATH', __DIR__ . '/' );}
require_once ABSPATH . 'wp-settings.php';What this gives you when you can read it:
- DB credentials - direct database access. Read the
wp_userstable for password hashes; modify it to insert your own admin account. - DB host - sometimes points to an internal database server reachable for further lateral movement.
- Auth keys / salts - used to sign auth cookies. Knowing them means you can forge
wordpress_logged_in_*cookies for any user. - Table prefix - needed for SQL injection payloads against the WP database; the default
wp_is overridden in some hardened installs (wp_xy12_) and SQLi payloads need to match.
wp-config.php is read-protected at the filesystem level for the web user, but it’s typically readable by it (since PHP needs to include it). An arbitrary file read primitive (e.g., LFI in a vulnerable plugin) usually gets it.
The wp-content directory
Section titled “The wp-content directory”wp-content/├── index.php # blank placeholder, prevents directory indexing if Apache obeys it├── plugins/│ ├── index.php # same - placeholder│ ├── akismet/ # bundled plugin│ ├── hello.php # the legendary "Hello Dolly" stub plugin│ └── <plugin>/ # one directory per installed plugin└── themes/ ├── index.php └── <theme>/ # one directory per installed themewp-content/plugins/<plugin>/
Section titled “wp-content/plugins/<plugin>/”Each plugin is a self-contained directory. Standard layout:
plugins/<plugin>/├── <plugin>.php # main plugin file with the standard header├── readme.txt # plugin description, version, requires/tested wp versions├── includes/, inc/, lib/ # supporting PHP files├── assets/, public/, css/, js/ # static assets└── languages/ # i18n .po/.mo filesThe plugin’s main file has a header like:
/* * Plugin Name: Mail Masta * Plugin URI: https://mail-masta.com/ * Description: Email subscription and newsletter management * Version: 1.0 * Author: ... * License: GPL2 */The Version: field is the canonical source for plugin version. Combine this with the version-fingerprinting techniques in the next page.
wp-content/themes/<theme>/
Section titled “wp-content/themes/<theme>/”Themes are like plugins but for visual presentation. Standard layout:
themes/<theme>/├── style.css # theme header lives in CSS comments├── functions.php # arbitrary PHP that runs on every page load├── index.php # template fallback├── 404.php, header.php, footer.php, sidebar.php # template parts├── single.php, page.php, archive.php # post-type templates└── assets/, css/, js/, images/ # staticfunctions.php is full-power PHP that loads on every page. The theme editor (admin-only) lets you write to it directly - that’s the canonical admin-to-RCE path.
style.css opens with a header:
/*Theme Name: Twenty Twenty-ThreeTheme URI: https://wordpress.org/themes/twentytwentythree/Author: the WordPress teamDescription: ...Version: 1.2*/Same pattern as plugins - Version: is the truth.
wp-content/uploads/
Section titled “wp-content/uploads/”uploads/└── YYYY/ └── MM/ └── <filename>Year/month subdirectories are auto-created on upload. Three things to check here:
- Directory indexing. If Apache
Options +Indexesis set, you can browse the entire upload tree without authentication. Common finding. - Backup plugin droppings. Many backup plugins write ZIP/SQL/TAR files into
uploads/oruploads/backups/. Database dumps with hashed passwords have leaked this way for years. - Accidentally uploaded sensitive files. Editors sometimes attach a customer-data spreadsheet to a draft post; the upload is web-accessible immediately regardless of whether the post is published.
User roles
Section titled “User roles”WordPress has five built-in user roles, with the role determining what the user can do post-login:
| Role | Can do |
|---|---|
| Administrator | Everything: install/edit plugins and themes, edit any post, manage users, modify site settings. This is the role you want. |
| Editor | Publish and edit any post (including others’), manage comments, manage categories/tags |
| Author | Publish and edit their own posts, upload media |
| Contributor | Write and edit their own posts but cannot publish them |
| Subscriber | Read content, edit their profile. The default for anyone who self-registers when users_can_register is on. |
Multisite installs add a sixth role, Super Admin, which has authority across all sites in the network.
Role implications for attackers
Section titled “Role implications for attackers”| With | You can |
|---|---|
| Subscriber | Profile editing - that’s about it. Sometimes XSS in profile fields propagates. |
| Author | Upload media (the file-upload attack surface in File Upload applies). |
| Editor | Edit any post; if any post template runs PHP-style shortcodes that themes register, you may have shortcode-based code execution. |
| Administrator | Theme editor, plugin upload, plugin editor, install new plugin from a ZIP - multiple direct paths to RCE. |
This means an Author-level compromise is sometimes enough to chain to RCE via an upload-bypass technique in the uploads cluster. Administrator is the cleanest path, but lower roles aren’t useless.
Filesystem locations under Linux
Section titled “Filesystem locations under Linux”For reference when you have shell access:
Webroot: /var/www/html/ (Apache default) /usr/share/nginx/html/ (nginx default) /srv/www/wordpress/ (some distros) /home/<user>/public_html/ (shared hosting)
Apache config: /etc/apache2/sites-enabled/*.conf /etc/apache2/conf-enabled/*.conf
nginx config: /etc/nginx/sites-enabled/* /etc/nginx/conf.d/*.conf
PHP config: /etc/php/<version>/apache2/php.ini /etc/php/<version>/fpm/php.ini
MySQL data: /var/lib/mysql/ /var/lib/mysql/<wp_db_name>/wp_users.MYD (MyISAM table data)
WP logs: wp-content/debug.log (if WP_DEBUG_LOG)Apache logs: /var/log/apache2/access.log, error.logPHP logs: /var/log/php_errors.log, /var/log/apache2/error.logMany of these are read-protected; an arbitrary file read primitive will most reliably get you wp-config.php, .htaccess, wp-content/debug.log (sometimes), and /etc/passwd if the daemon’s chroot doesn’t block it.
Quick reference
Section titled “Quick reference”| URL path | Maps to |
|---|---|
/ | index.php |
/wp-login.php | wp-login.php (login form) |
/wp-admin/ | wp-admin/ (dashboard, post-auth) |
/xmlrpc.php | xmlrpc.php (XML-RPC API) |
/wp-json/wp/v2/... | REST API (handled by index.php with rewrites) |
/wp-content/plugins/<x>/ | wp-content/plugins/<x>/ |
/wp-content/themes/<x>/ | wp-content/themes/<x>/ |
/wp-content/uploads/YYYY/MM/... | wp-content/uploads/YYYY/MM/... |
/?author=1 | index.php, redirects to that user’s archive (user-enum primitive) |
/readme.html | readme.html (version banner on old installs) |
| Goal | First file to inspect after RCE |
|---|---|
| DB credentials | wp-config.php |
| Auth cookie forgery keys | wp-config.php (AUTH_KEY, SECURE_AUTH_KEY, etc.) |
| Hashed passwords | DB: wp_users.user_pass |
| Recent admin activity | DB: wp_users.user_activation_key, wp_usermeta |
| Installed plugins | wp-content/plugins/ directory listing |
| Site URL / multisite check | DB: wp_options.siteurl, wp_options.home |