wraith-sec 0.3.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- wraith/__init__.py +3 -0
- wraith/__main__.py +4 -0
- wraith/art/wraith.txt +38 -0
- wraith/cli.py +406 -0
- wraith/core/__init__.py +0 -0
- wraith/core/console.py +266 -0
- wraith/core/context.py +102 -0
- wraith/core/engine.py +100 -0
- wraith/core/http.py +95 -0
- wraith/core/models.py +73 -0
- wraith/core/phase.py +32 -0
- wraith/core/report.py +150 -0
- wraith/core/showdown.py +161 -0
- wraith/core/web.py +122 -0
- wraith/phases/__init__.py +14 -0
- wraith/phases/access_control.py +263 -0
- wraith/phases/content_discovery.py +119 -0
- wraith/phases/http_probe.py +107 -0
- wraith/phases/injection.py +124 -0
- wraith/phases/resolve.py +44 -0
- wraith/phases/security_headers.py +133 -0
- wraith/phases/tcp_scan.py +69 -0
- wraith/phases/tech_detect.py +137 -0
- wraith/phases/template_checks.py +151 -0
- wraith/phases/vhost.py +116 -0
- wraith/shell/__init__.py +0 -0
- wraith/shell/handler.py +189 -0
- wraith/shell/payloads.py +49 -0
- wraith/shell/session.py +101 -0
- wraith/templates/apache-server-status.json +19 -0
- wraith/templates/directory-listing.json +19 -0
- wraith/templates/dotenv-exposure.json +20 -0
- wraith/templates/git-config-exposure.json +19 -0
- wraith/templates/phpinfo-disclosure.json +28 -0
- wraith/templates/swagger-ui.json +26 -0
- wraith_sec-0.3.3.dist-info/METADATA +217 -0
- wraith_sec-0.3.3.dist-info/RECORD +40 -0
- wraith_sec-0.3.3.dist-info/WHEEL +4 -0
- wraith_sec-0.3.3.dist-info/entry_points.txt +2 -0
- wraith_sec-0.3.3.dist-info/licenses/LICENSE +21 -0
wraith/__init__.py
ADDED
wraith/__main__.py
ADDED
wraith/art/wraith.txt
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
:========:
|
|
2
|
+
-++-.. ..-++:
|
|
3
|
+
-**: :*+:
|
|
4
|
+
:*+: :**.
|
|
5
|
+
=#- :-. :*+
|
|
6
|
+
#%-=-: .::. ..: =#
|
|
7
|
+
*@@*. :+=*--*=+: .-: *+
|
|
8
|
+
-@*: .=**- -**- --.%:
|
|
9
|
+
:%+ .=##+: :+*- -%.
|
|
10
|
+
.+* =%#= :::: .+*=. *=.
|
|
11
|
+
=# *#- :-*#%@@%#*-: .=#+ #-
|
|
12
|
+
-#- .#+. :*#@@@@%%%@@@@@%+: .+#. -*-
|
|
13
|
+
=+ ## .+=====#%%%%%@#====== ## *-
|
|
14
|
+
:*=.+* =- .-%%-: -- .*+ :=:
|
|
15
|
+
++ -# :#= .=%##%=. *+ #= ++
|
|
16
|
+
++ -# -%@%*++*%@* *@%++++#@#. #=.*=
|
|
17
|
+
-=.:*- .++--+%@%.-:.%@%+-=+=. :*:-*-
|
|
18
|
+
-#- *= +#%@#%%#@%#= += -*-
|
|
19
|
+
-%- %= =:*=%+**==+: += -%-
|
|
20
|
+
:+%+ -#: . :--.*==-+-:-- =+. =%+-
|
|
21
|
+
.-*#*--*- :+- :---=+--==++=+.=.-=:=+-=-- = .+*:*#*=.
|
|
22
|
+
:##:. += == *+-:-:::-:::%.--------.::# .** ..+#:
|
|
23
|
+
:#+ :: :=: .- -+.:-::::- *+.=------=-.#. .=- +#-
|
|
24
|
+
+* .*+. :+- *=:=---:.-%.--=-::::= ++ .- *+
|
|
25
|
+
++ -+: - ..-=.----- #= =:::--:-.:% -: -*. .*+
|
|
26
|
+
.#= . :+: :+=***:**++- -%.---::--=-=*+. .. :+ =#.
|
|
27
|
+
#=. .-+- .. :*#:::-=-=:=+.*= -= :*---::--*+*. .:-:.-+*
|
|
28
|
+
+#=. :**. -+:**=--=:==*=.*--=- :+-+:*===.:=*. -#+. . :*+
|
|
29
|
+
.=#. :-. . -=+=.++=-+-==+- =+----:=-+*:-+.. -+ .+=.
|
|
30
|
+
.#- .::+*==-==*=-+-:=--=- *+:::++++.-+--=**-. :#.
|
|
31
|
+
#+=- .-==+=+**-*%::====+--+*=::.. ==-+-:=:=====.-@***+:-=+=. --+#
|
|
32
|
+
-%= .. :%#-: =%=.-=:=*+:: .::==*-+-::.+# :+%+ .:+= :**
|
|
33
|
+
#: -#= .*+ .:+*-. .:=%-. :%= .*= -- .%
|
|
34
|
+
: =*. -@-.--:. :=+:.#: +- :
|
|
35
|
+
:=: +%-.. := :- :@* =:
|
|
36
|
+
.: += :=: .+: .+*
|
|
37
|
+
+: += .- :*
|
|
38
|
+
== :*
|
wraith/cli.py
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
"""Command-line entrypoint for wraith."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import asyncio
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from urllib.parse import urlencode, urljoin, urlsplit
|
|
12
|
+
|
|
13
|
+
import wraith.phases # noqa: F401 (importing populates PHASE_REGISTRY)
|
|
14
|
+
from wraith import __version__
|
|
15
|
+
from wraith.core import report
|
|
16
|
+
from wraith.core.console import THEMES, Console
|
|
17
|
+
from wraith.core.context import Workspace
|
|
18
|
+
from wraith.core.engine import Engine
|
|
19
|
+
from wraith.core.models import Severity
|
|
20
|
+
from wraith.core.phase import PHASE_REGISTRY
|
|
21
|
+
from wraith.core.showdown import Showdown
|
|
22
|
+
|
|
23
|
+
_SEVERITY_BY_NAME = {s.label.lower(): s for s in Severity}
|
|
24
|
+
|
|
25
|
+
# Subcommands. Anything else on the command line is treated as a target for the
|
|
26
|
+
# default `run` command, so `wraith example.com` works without typing `run`.
|
|
27
|
+
_COMMANDS = {"run", "showdown", "phases", "shell", "login", "aces"}
|
|
28
|
+
|
|
29
|
+
EXAMPLES = """\
|
|
30
|
+
examples:
|
|
31
|
+
wraith example.com full scan — `run` is the default command
|
|
32
|
+
wraith example.com -p tcp-scan,http-probe only these phases
|
|
33
|
+
wraith example.com -s sessions.json add Broken Access Control / IDOR
|
|
34
|
+
wraith example.com -x high exit 2 if a High+ finding turns up
|
|
35
|
+
wraith showdown toggle showdown mode (reveal on a find; sticks)
|
|
36
|
+
wraith login http://host/login -u alice -p secret -o sessions.json
|
|
37
|
+
wraith shell -l 9001 catch a reverse shell
|
|
38
|
+
|
|
39
|
+
run `wraith phases` to see the pipeline; phases run concurrently by dependency.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class _Help(argparse.RawDescriptionHelpFormatter):
|
|
44
|
+
"""Keep the examples block verbatim and give options room to breathe."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, prog):
|
|
47
|
+
super().__init__(prog, max_help_position=30, width=86)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _with_default_command(argv):
|
|
51
|
+
"""Insert `run` when the first non-option token isn't a known subcommand,
|
|
52
|
+
so the common case (`wraith TARGET ...`) needs no subcommand at all."""
|
|
53
|
+
out = list(argv)
|
|
54
|
+
i = 0
|
|
55
|
+
while i < len(out):
|
|
56
|
+
tok = out[i]
|
|
57
|
+
if tok in ("-h", "--help", "--version"):
|
|
58
|
+
return out # let argparse handle these as-is
|
|
59
|
+
if tok == "--theme": # the one global option that takes a value
|
|
60
|
+
i += 2
|
|
61
|
+
continue
|
|
62
|
+
if tok.startswith("-"):
|
|
63
|
+
i += 1
|
|
64
|
+
continue
|
|
65
|
+
if tok not in _COMMANDS: # first bare word is a target -> default to run
|
|
66
|
+
out.insert(i, "run")
|
|
67
|
+
return out
|
|
68
|
+
return out
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _console(args) -> Console:
|
|
72
|
+
c = Console(
|
|
73
|
+
theme=getattr(args, "theme", None),
|
|
74
|
+
color=False if getattr(args, "no_color", False) else None,
|
|
75
|
+
banner=not getattr(args, "no_banner", False),
|
|
76
|
+
)
|
|
77
|
+
if _load_config().get("showdown"): # mode on -> wire it to the console
|
|
78
|
+
c.showdown = Showdown(c)
|
|
79
|
+
return c
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# `wraith showdown` flips a mode that sticks between runs, so it's persisted here.
|
|
83
|
+
_CONFIG_PATH = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "wraith" / "config.json"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _load_config() -> dict:
|
|
87
|
+
try:
|
|
88
|
+
return json.loads(_CONFIG_PATH.read_text(encoding="utf-8"))
|
|
89
|
+
except (OSError, ValueError):
|
|
90
|
+
return {}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _save_config(cfg: dict) -> None:
|
|
94
|
+
try:
|
|
95
|
+
_CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
96
|
+
_CONFIG_PATH.write_text(json.dumps(cfg, indent=2), encoding="utf-8")
|
|
97
|
+
except OSError:
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _select(names):
|
|
102
|
+
if not names:
|
|
103
|
+
return [cls() for cls in PHASE_REGISTRY.values()]
|
|
104
|
+
chosen = []
|
|
105
|
+
for n in names:
|
|
106
|
+
cls = PHASE_REGISTRY.get(n)
|
|
107
|
+
if not cls:
|
|
108
|
+
raise SystemExit(f"unknown phase: {n} (see `wraith phases`)")
|
|
109
|
+
chosen.append(cls())
|
|
110
|
+
return chosen
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def cmd_phases(args) -> None:
|
|
114
|
+
c = _console(args)
|
|
115
|
+
c.banner()
|
|
116
|
+
for name, cls in PHASE_REGISTRY.items():
|
|
117
|
+
deps = ", ".join(sorted(cls.requires)) or "—"
|
|
118
|
+
c.plain(f" {name:<14} requires: {deps}")
|
|
119
|
+
if cls.description:
|
|
120
|
+
c.plain(f" {'':<14} {cls.description}")
|
|
121
|
+
c.plain("")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _load_sessions(ws, path, console) -> None:
|
|
125
|
+
data = json.loads(Path(path).read_text(encoding="utf-8"))
|
|
126
|
+
if data.get("base_url"):
|
|
127
|
+
ws.meta["base_url"] = data["base_url"]
|
|
128
|
+
if data.get("seeds"):
|
|
129
|
+
ws.meta["seeds"] = data["seeds"]
|
|
130
|
+
for s in data.get("sessions", []):
|
|
131
|
+
ws.add_session(
|
|
132
|
+
name=s["name"],
|
|
133
|
+
role=s.get("role", "low"),
|
|
134
|
+
headers=s.get("headers", {}),
|
|
135
|
+
cookies=s.get("cookies", {}),
|
|
136
|
+
)
|
|
137
|
+
console.info(f"loaded {len(ws.sessions)} session(s) from {path}")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def cmd_run(args) -> None:
|
|
141
|
+
c = _console(args)
|
|
142
|
+
c.banner()
|
|
143
|
+
|
|
144
|
+
phases = _select(args.phases.split(",") if args.phases else None)
|
|
145
|
+
ws = Workspace.create(args.target, base_dir=args.workdir)
|
|
146
|
+
if args.sessions:
|
|
147
|
+
_load_sessions(ws, args.sessions, c)
|
|
148
|
+
if args.wordlist:
|
|
149
|
+
ws.meta["wordlist"] = args.wordlist
|
|
150
|
+
if args.templates:
|
|
151
|
+
ws.meta["templates"] = args.templates
|
|
152
|
+
c.info(f"target {ws.target}")
|
|
153
|
+
c.info(f"workdir {ws.workdir}")
|
|
154
|
+
c.info(f"phases {', '.join(p.name for p in phases)}")
|
|
155
|
+
|
|
156
|
+
engine = Engine(ws, phases, c, concurrency=args.concurrency)
|
|
157
|
+
results = asyncio.run(engine.run())
|
|
158
|
+
|
|
159
|
+
report_md = report.write_markdown(ws, results)
|
|
160
|
+
report_html = report.write_html(ws, results)
|
|
161
|
+
report_json = report.write_json(ws)
|
|
162
|
+
ws.save()
|
|
163
|
+
|
|
164
|
+
c.rule("summary")
|
|
165
|
+
c.good(
|
|
166
|
+
f"hosts {len(ws.hosts)} · services {len(ws.services)} · "
|
|
167
|
+
f"endpoints {len(ws.endpoints)} · findings {len(ws.findings)}"
|
|
168
|
+
)
|
|
169
|
+
c.info(f"workspace {ws.workdir / 'workspace.json'}")
|
|
170
|
+
c.info(f"report {report_md}")
|
|
171
|
+
c.info(f"report {report_html}")
|
|
172
|
+
c.info(f"findings {report_json}")
|
|
173
|
+
|
|
174
|
+
worst = max((f.severity for f in ws.findings), default=Severity.INFO)
|
|
175
|
+
fail = bool(args.fail_on) and bool(ws.findings) and worst >= _SEVERITY_BY_NAME[args.fail_on]
|
|
176
|
+
if fail:
|
|
177
|
+
c.warn(f"findings at/above '{args.fail_on}' (worst: {worst.label}) — exit 2")
|
|
178
|
+
|
|
179
|
+
if c.showdown is not None:
|
|
180
|
+
# Showdown mode owns the ending: reveal, kill-chain, receipts, verdict.
|
|
181
|
+
c.showdown.close(ws)
|
|
182
|
+
else:
|
|
183
|
+
# Plain run: the findings, then the tally.
|
|
184
|
+
c.findings_report(ws.findings)
|
|
185
|
+
counts = {}
|
|
186
|
+
for f in ws.findings:
|
|
187
|
+
counts[f.severity.label] = counts.get(f.severity.label, 0) + 1
|
|
188
|
+
c.severity_summary(counts)
|
|
189
|
+
|
|
190
|
+
if fail:
|
|
191
|
+
sys.exit(2)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _login_form(html: str, page_url: str):
|
|
195
|
+
"""Find the login form on the page and return (action_url, hidden_fields).
|
|
196
|
+
|
|
197
|
+
The login form is the one with a password input. We grab its action (where
|
|
198
|
+
the POST really goes — often /auth/login, not the page URL) and every hidden
|
|
199
|
+
field, so anti-CSRF tokens (ASP.NET __RequestVerificationToken, Django
|
|
200
|
+
csrfmiddlewaretoken, Rails authenticity_token...) ride along automatically.
|
|
201
|
+
Returns (None, {}) when no such form is found.
|
|
202
|
+
"""
|
|
203
|
+
import re
|
|
204
|
+
|
|
205
|
+
for fm in re.finditer(r"<form\b([^>]*)>(.*?)</form>", html or "", re.I | re.S):
|
|
206
|
+
attrs, inner = fm.group(1), fm.group(2)
|
|
207
|
+
if not re.search(r'type\s*=\s*["\']password', inner, re.I):
|
|
208
|
+
continue # not the login form
|
|
209
|
+
action_m = re.search(r'action\s*=\s*["\']([^"\']*)', attrs, re.I)
|
|
210
|
+
action = urljoin(page_url, action_m.group(1)) if action_m and action_m.group(1) else page_url
|
|
211
|
+
hidden = {}
|
|
212
|
+
for inp in re.finditer(r"<input\b([^>]*)>", inner, re.I):
|
|
213
|
+
a = inp.group(1)
|
|
214
|
+
if not re.search(r'type\s*=\s*["\']hidden', a, re.I):
|
|
215
|
+
continue
|
|
216
|
+
name = re.search(r'name\s*=\s*["\']([^"\']+)', a, re.I)
|
|
217
|
+
value = re.search(r'value\s*=\s*["\']([^"\']*)', a, re.I)
|
|
218
|
+
if name:
|
|
219
|
+
hidden[name.group(1)] = value.group(1) if value else ""
|
|
220
|
+
return action, hidden
|
|
221
|
+
return None, {}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def cmd_login(args) -> None:
|
|
225
|
+
"""Authenticate against a form login and emit a sessions.json snippet."""
|
|
226
|
+
import http.cookiejar
|
|
227
|
+
import ssl
|
|
228
|
+
import urllib.request
|
|
229
|
+
|
|
230
|
+
c = _console(args)
|
|
231
|
+
ctx = ssl.create_default_context()
|
|
232
|
+
ctx.check_hostname = False
|
|
233
|
+
ctx.verify_mode = ssl.CERT_NONE
|
|
234
|
+
jar = http.cookiejar.CookieJar()
|
|
235
|
+
opener = urllib.request.build_opener(
|
|
236
|
+
urllib.request.HTTPSHandler(context=ctx),
|
|
237
|
+
urllib.request.HTTPCookieProcessor(jar),
|
|
238
|
+
)
|
|
239
|
+
ua = {"User-Agent": "wraith/0.1"}
|
|
240
|
+
|
|
241
|
+
# GET the login page first: seeds the session cookie *and* lets us read the
|
|
242
|
+
# form's real action and any hidden token it expects on submit.
|
|
243
|
+
action, hidden = args.url, {}
|
|
244
|
+
try:
|
|
245
|
+
with opener.open(urllib.request.Request(args.url, headers=ua), timeout=10) as resp:
|
|
246
|
+
html = resp.read(200000).decode("utf-8", "ignore")
|
|
247
|
+
found_action, hidden = _login_form(html, args.url)
|
|
248
|
+
if found_action:
|
|
249
|
+
action = found_action
|
|
250
|
+
if hidden:
|
|
251
|
+
c.info(f"form action {action} · carried {len(hidden)} hidden field(s): {', '.join(hidden)}")
|
|
252
|
+
except Exception as exc:
|
|
253
|
+
raise SystemExit(f"could not load login page: {exc}")
|
|
254
|
+
|
|
255
|
+
# hidden fields first, then our credentials and any --data overrides on top.
|
|
256
|
+
fields = dict(hidden)
|
|
257
|
+
fields[args.user_field] = args.username
|
|
258
|
+
fields[args.pass_field] = args.password
|
|
259
|
+
for pair in args.data or []:
|
|
260
|
+
if "=" in pair:
|
|
261
|
+
k, v = pair.split("=", 1)
|
|
262
|
+
fields[k] = v
|
|
263
|
+
body = urlencode(fields).encode()
|
|
264
|
+
|
|
265
|
+
try:
|
|
266
|
+
post = urllib.request.Request(action, data=body, headers={**ua, "Referer": args.url})
|
|
267
|
+
opener.open(post, timeout=10)
|
|
268
|
+
except Exception as exc:
|
|
269
|
+
raise SystemExit(f"login request failed: {exc}")
|
|
270
|
+
|
|
271
|
+
cookies = {ck.name: ck.value for ck in jar}
|
|
272
|
+
if not cookies:
|
|
273
|
+
c.warn("no cookies captured — check the field names/URL")
|
|
274
|
+
|
|
275
|
+
parts = urlsplit(args.url)
|
|
276
|
+
snippet = {
|
|
277
|
+
"base_url": f"{parts.scheme}://{parts.netloc}",
|
|
278
|
+
"seeds": ["/"],
|
|
279
|
+
"sessions": [{"name": args.name, "role": args.role, "cookies": cookies}],
|
|
280
|
+
}
|
|
281
|
+
text = json.dumps(snippet, indent=2)
|
|
282
|
+
if args.output:
|
|
283
|
+
Path(args.output).write_text(text, encoding="utf-8")
|
|
284
|
+
c.good(f"captured {len(cookies)} cookie(s) -> {args.output}")
|
|
285
|
+
else:
|
|
286
|
+
print(text)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def cmd_showdown(args) -> None:
|
|
290
|
+
"""Toggle showdown mode on/off — it sticks between runs. While on, every run
|
|
291
|
+
that finds a vulnerability reveals the wraith (more mode behaviour to come)."""
|
|
292
|
+
c = _console(args)
|
|
293
|
+
cfg = _load_config()
|
|
294
|
+
cfg["showdown"] = not cfg.get("showdown", False)
|
|
295
|
+
_save_config(cfg)
|
|
296
|
+
if cfg["showdown"]:
|
|
297
|
+
if c.show_banner:
|
|
298
|
+
c.aces()
|
|
299
|
+
c.good("showdown mode ON — runs now play the catch out (run `wraith showdown` again to turn off)")
|
|
300
|
+
else:
|
|
301
|
+
c.info("showdown mode OFF — wraith runs plain again")
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def cmd_aces(args) -> None:
|
|
305
|
+
_console(args).aces()
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def cmd_shell(args) -> None:
|
|
309
|
+
from wraith.shell import payloads
|
|
310
|
+
from wraith.shell.handler import ShellServer
|
|
311
|
+
|
|
312
|
+
c = _console(args)
|
|
313
|
+
c.banner()
|
|
314
|
+
try:
|
|
315
|
+
ports = [int(p) for p in args.listen.split(",")]
|
|
316
|
+
except ValueError:
|
|
317
|
+
raise SystemExit("--listen expects comma-separated port numbers")
|
|
318
|
+
lhost = args.lhost or payloads.guess_lhost()
|
|
319
|
+
server = ShellServer(ports, lhost, c)
|
|
320
|
+
try:
|
|
321
|
+
asyncio.run(server.run())
|
|
322
|
+
except KeyboardInterrupt:
|
|
323
|
+
pass
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
327
|
+
p = argparse.ArgumentParser(
|
|
328
|
+
prog="wraith",
|
|
329
|
+
description="Offensive recon & exploitation pipeline. Run is the default: `wraith TARGET`.",
|
|
330
|
+
epilog=EXAMPLES,
|
|
331
|
+
formatter_class=_Help,
|
|
332
|
+
)
|
|
333
|
+
p.add_argument("--version", action="version", version=f"wraith {__version__}")
|
|
334
|
+
p.add_argument("--theme", metavar="NAME", choices=list(THEMES),
|
|
335
|
+
help="colour theme: " + " | ".join(THEMES) + " (default: crimson)")
|
|
336
|
+
p.add_argument("--no-color", action="store_true", help="disable coloured output")
|
|
337
|
+
p.add_argument("--no-banner", action="store_true", help="suppress the ASCII banner")
|
|
338
|
+
sub = p.add_subparsers(dest="command", metavar="<command>")
|
|
339
|
+
|
|
340
|
+
def _add_scan_args(sp):
|
|
341
|
+
sp.add_argument("target", help="hostname, IP or URL")
|
|
342
|
+
sp.add_argument("-p", "--phases", metavar="LIST", help="comma-separated subset of phases (default: all)")
|
|
343
|
+
sp.add_argument("-s", "--sessions", metavar="FILE", help="sessions JSON — enables access-control / IDOR")
|
|
344
|
+
sp.add_argument("-w", "--wordlist", metavar="FILE", help="wordlist for content-discovery")
|
|
345
|
+
sp.add_argument("-t", "--templates", metavar="DIR", help="extra template-checks directory")
|
|
346
|
+
sp.add_argument("-x", "--fail-on", metavar="SEV", choices=list(_SEVERITY_BY_NAME),
|
|
347
|
+
help="exit 2 on a finding at/above SEV (info|low|medium|high|critical)")
|
|
348
|
+
sp.add_argument("-c", "--concurrency", metavar="N", type=int, default=8,
|
|
349
|
+
help="max phases running in parallel (default: 8)")
|
|
350
|
+
sp.add_argument("--workdir", metavar="DIR", default="wraith-runs",
|
|
351
|
+
help="output directory (default: wraith-runs)")
|
|
352
|
+
|
|
353
|
+
run = sub.add_parser("run", help="scan a target (default command)", epilog=EXAMPLES,
|
|
354
|
+
formatter_class=_Help, description="Run the phase pipeline against a target.")
|
|
355
|
+
_add_scan_args(run)
|
|
356
|
+
run.set_defaults(func=cmd_run)
|
|
357
|
+
|
|
358
|
+
sd = sub.add_parser("showdown", help="toggle showdown mode on/off (sticks between runs)",
|
|
359
|
+
formatter_class=_Help,
|
|
360
|
+
description="Toggle showdown mode. While it's on, every run reveals the "
|
|
361
|
+
"wraith when it catches a vulnerability. Run it again to turn off.")
|
|
362
|
+
sd.set_defaults(func=cmd_showdown)
|
|
363
|
+
|
|
364
|
+
ph = sub.add_parser("phases", help="list available phases", formatter_class=_Help)
|
|
365
|
+
ph.set_defaults(func=cmd_phases)
|
|
366
|
+
|
|
367
|
+
sh = sub.add_parser("shell", help="reverse-shell handler / post-exploitation console", formatter_class=_Help)
|
|
368
|
+
sh.add_argument("-l", "--listen", metavar="PORTS", default="9001",
|
|
369
|
+
help="comma-separated ports to listen on (default: 9001)")
|
|
370
|
+
sh.add_argument("--lhost", metavar="IP", help="LHOST embedded in generated payloads (auto-detected)")
|
|
371
|
+
sh.set_defaults(func=cmd_shell)
|
|
372
|
+
|
|
373
|
+
lg = sub.add_parser("login", help="grab a session from a form login -> sessions.json", formatter_class=_Help)
|
|
374
|
+
lg.add_argument("url", help="login form URL (GET to seed cookies, POST to submit)")
|
|
375
|
+
lg.add_argument("-u", "--username", required=True)
|
|
376
|
+
lg.add_argument("-p", "--password", required=True)
|
|
377
|
+
lg.add_argument("-o", "--output", metavar="FILE", help="write the sessions.json here (default: stdout)")
|
|
378
|
+
lg.add_argument("--user-field", metavar="NAME", default="username", help="username form field name")
|
|
379
|
+
lg.add_argument("--pass-field", metavar="NAME", default="password", help="password form field name")
|
|
380
|
+
lg.add_argument("--data", metavar="K=V", action="append", help="extra form field (repeatable)")
|
|
381
|
+
lg.add_argument("--name", metavar="NAME", default="user", help="session name for the output")
|
|
382
|
+
lg.add_argument("--role", metavar="ROLE", default="low", help="session role (none/low/med/high)")
|
|
383
|
+
lg.set_defaults(func=cmd_login)
|
|
384
|
+
|
|
385
|
+
egg = sub.add_parser("aces") # easter egg: no help= keeps it out of the listing
|
|
386
|
+
egg.set_defaults(func=cmd_aces)
|
|
387
|
+
|
|
388
|
+
return p
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def main(argv=None) -> None:
|
|
392
|
+
argv = sys.argv[1:] if argv is None else list(argv)
|
|
393
|
+
parser = build_parser()
|
|
394
|
+
if not argv: # bare `wraith` -> banner + help, not an error
|
|
395
|
+
Console().banner()
|
|
396
|
+
parser.print_help()
|
|
397
|
+
return
|
|
398
|
+
args = parser.parse_args(_with_default_command(argv))
|
|
399
|
+
if not hasattr(args, "func"): # options but no command
|
|
400
|
+
parser.print_help()
|
|
401
|
+
return
|
|
402
|
+
args.func(args)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
if __name__ == "__main__":
|
|
406
|
+
main()
|
wraith/core/__init__.py
ADDED
|
File without changes
|