rav-xss 1.0.28
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.
- package/LICENSE +21 -0
- package/README.md +449 -0
- package/package.json +41 -0
- package/payloads/Basic/basic.txt +35 -0
- package/payloads/FilterEvasion/evasion.txt +20 -0
- package/payloads/Polyglots/polyglots.txt +11 -0
- package/payloads/PureReflex/reflex.txt +222 -0
- package/payloads/WAFBypass/wafbypass.txt +12 -0
- package/src/cli/args.js +57 -0
- package/src/cli/help.js +31 -0
- package/src/cli/wizard.js +196 -0
- package/src/config/colors.js +83 -0
- package/src/config/manager.js +112 -0
- package/src/core/browser.js +430 -0
- package/src/core/scanner.js +778 -0
- package/src/index.js +113 -0
- package/src/media/ravxss.png +0 -0
- package/src/utils/box.js +266 -0
- package/src/utils/helpers.js +22 -0
- package/src/utils/logger.js +44 -0
- package/src/utils/packageInfo.js +192 -0
- package/src/utils/reporter.js +56 -0
|
@@ -0,0 +1,778 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const axios = require("axios");
|
|
6
|
+
const inquirer = require("inquirer");
|
|
7
|
+
const ora = require("ora");
|
|
8
|
+
const chalk = require("chalk");
|
|
9
|
+
|
|
10
|
+
const { sleep, ensureDir } = require("../utils/helpers");
|
|
11
|
+
const { colors } = require("../config/colors");
|
|
12
|
+
const { Logger } = require("../utils/logger");
|
|
13
|
+
const boxManager = require("../utils/box");
|
|
14
|
+
const { Reporter } = require("../utils/reporter");
|
|
15
|
+
const { getDefaultConfig } = require("../config/manager");
|
|
16
|
+
|
|
17
|
+
const CATEGORY_MAP = {
|
|
18
|
+
"basic": ["🔰 Basic Payloads", "Standard HTML tags & events"],
|
|
19
|
+
"filterevasion": ["🛡️ Filter Evasion", "Encoding, null bytes, obfuscation"],
|
|
20
|
+
"polyglots": ["🎭 Polyglots", "Multi-context payloads"],
|
|
21
|
+
"wafbypass": ["🔥 WAF Bypass", "Cloudflare, ModSecurity evasion"],
|
|
22
|
+
"purereflex": ["💎 Pure Reflex", "Reflected-only payloads, no template injection"]
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
class XSSScanner {
|
|
26
|
+
constructor(config, args) {
|
|
27
|
+
this.config = config;
|
|
28
|
+
this.args = args;
|
|
29
|
+
this.payloads = [];
|
|
30
|
+
this.category = args.category || null;
|
|
31
|
+
this.mode = args.mode || config.mode || null;
|
|
32
|
+
|
|
33
|
+
const defaultConfig = getDefaultConfig();
|
|
34
|
+
const defaultUrl = defaultConfig.targets[0].url;
|
|
35
|
+
const defaultName = defaultConfig.targets[0].name;
|
|
36
|
+
|
|
37
|
+
this.targetUrl = args.url || (config.targets?.length > 0 ? config.targets[0].url : defaultUrl);
|
|
38
|
+
this.targetName = config.targets?.length > 0 ? config.targets[0].name : defaultName;
|
|
39
|
+
|
|
40
|
+
this.results = {
|
|
41
|
+
scan_start: null,
|
|
42
|
+
scan_end: null,
|
|
43
|
+
total_tests: 0,
|
|
44
|
+
vulns_found: 0,
|
|
45
|
+
findings: []
|
|
46
|
+
};
|
|
47
|
+
this.reporter = null;
|
|
48
|
+
|
|
49
|
+
this.payloadsDir = path.join(__dirname, "..", "..", "payloads");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 🚀 Inicializa o scanner com menu interativo e configuração
|
|
54
|
+
* @returns {Promise<void>}
|
|
55
|
+
*/
|
|
56
|
+
async initialize() {
|
|
57
|
+
while (!this.category || !this.mode) {
|
|
58
|
+
if (!this.category) {
|
|
59
|
+
await this.showMainMenu();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (this.category && !this.mode) {
|
|
63
|
+
await this.showModeMenu();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!this.targetUrl || !this.targetUrl.includes("[XSS]")) {
|
|
68
|
+
await this.promptForUrl();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!this.targetUrl || !this.targetUrl.includes("[XSS]")) {
|
|
72
|
+
const defaultConfig = getDefaultConfig();
|
|
73
|
+
this.targetUrl = defaultConfig.targets[0].url;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
await this.loadPayloads();
|
|
77
|
+
ensureDir(this.config.scanner.report_dir);
|
|
78
|
+
this.reporter = new Reporter(this.config.scanner.report_dir);
|
|
79
|
+
|
|
80
|
+
if (this.mode === "playwright") {
|
|
81
|
+
const { BrowserManager } = require("./browser");
|
|
82
|
+
this.browserManager = new BrowserManager(this.config, { ...this.args, mode: "playwright" });
|
|
83
|
+
await this.browserManager.launch();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 🔄 Exibe menu de seleção de modo de execução
|
|
89
|
+
* Pula automaticamente para axios se estiver no Termux
|
|
90
|
+
* Verifica e instala dependências do Playwright quando necessário
|
|
91
|
+
* @returns {Promise<void>}
|
|
92
|
+
*/
|
|
93
|
+
async showModeMenu() {
|
|
94
|
+
console.clear();
|
|
95
|
+
|
|
96
|
+
const { isTermux } = require("./browser");
|
|
97
|
+
|
|
98
|
+
if (isTermux()) {
|
|
99
|
+
this.mode = "axios";
|
|
100
|
+
console.log(colors.warning("\n 📱 Termux detected - Using Axios mode automatically\n"));
|
|
101
|
+
await sleep(2000);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const defaultConfig = getDefaultConfig();
|
|
106
|
+
const displayUrl = this.targetUrl || defaultConfig.targets[0].url;
|
|
107
|
+
|
|
108
|
+
Logger.showBanner(this.config, 0, this.category, displayUrl);
|
|
109
|
+
|
|
110
|
+
const modeChoices = [
|
|
111
|
+
{
|
|
112
|
+
name: ` ${colors.action("⚡")} ${colors.action.bold("Axios Mode")} ${colors.dim("—")} ${colors.menuDescription("Fast HTTP requests (recommended)")}`,
|
|
113
|
+
value: "axios",
|
|
114
|
+
short: "Axios Mode"
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: ` ${colors.highlight("🌐")} ${colors.highlight.bold("Playwright Mode")} ${colors.dim("—")} ${colors.menuDescription("Real browser automation (slower)")}`,
|
|
118
|
+
value: "playwright",
|
|
119
|
+
short: "Playwright Mode"
|
|
120
|
+
},
|
|
121
|
+
new inquirer.Separator(colors.dim("─".repeat(55))),
|
|
122
|
+
{
|
|
123
|
+
name: ` ${colors.down("⮘")} ${colors.down.bold("Back to Categories")}`,
|
|
124
|
+
value: "back",
|
|
125
|
+
short: "Back"
|
|
126
|
+
}
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
const { mode } = await inquirer.prompt([
|
|
130
|
+
{
|
|
131
|
+
type: "list",
|
|
132
|
+
name: "mode",
|
|
133
|
+
prefix: colors.highlight2.bold("✸"),
|
|
134
|
+
message: colors.highlight2.bold("SELECT MODE"),
|
|
135
|
+
choices: modeChoices,
|
|
136
|
+
pageSize: 5,
|
|
137
|
+
loop: false
|
|
138
|
+
}
|
|
139
|
+
]);
|
|
140
|
+
|
|
141
|
+
if (mode === "back") {
|
|
142
|
+
this.category = null;
|
|
143
|
+
this.mode = null;
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (mode === "playwright") {
|
|
148
|
+
try {
|
|
149
|
+
const { execSync } = require("child_process");
|
|
150
|
+
const os = require("os");
|
|
151
|
+
const fs = require("fs");
|
|
152
|
+
const path = require("path");
|
|
153
|
+
|
|
154
|
+
let browserPath;
|
|
155
|
+
const home = os.homedir();
|
|
156
|
+
|
|
157
|
+
if (process.platform === "win32") {
|
|
158
|
+
const localAppData = process.env.LOCALAPPDATA || path.join(home, "AppData", "Local");
|
|
159
|
+
browserPath = path.join(localAppData, "ms-playwright");
|
|
160
|
+
} else if (process.platform === "darwin") {
|
|
161
|
+
browserPath = path.join(home, "Library", "Caches", "ms-playwright");
|
|
162
|
+
} else {
|
|
163
|
+
const cacheDir = process.env.XDG_CACHE_HOME || path.join(home, ".cache");
|
|
164
|
+
browserPath = path.join(cacheDir, "ms-playwright");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const browsersInstalled = fs.existsSync(browserPath);
|
|
168
|
+
|
|
169
|
+
if (!browsersInstalled) {
|
|
170
|
+
console.log(colors.warning("\n ⚠️ Playwright browsers not found!"));
|
|
171
|
+
console.log(colors.muted(" Installing Chromium automatically...\n"));
|
|
172
|
+
execSync("npx playwright install chromium", {
|
|
173
|
+
stdio: "inherit",
|
|
174
|
+
timeout: 120000
|
|
175
|
+
});
|
|
176
|
+
console.log(colors.success("\n ✅ Chromium installed successfully!\n"));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
console.log(colors.success(" ✅ Playwright mode activated with Chromium\n"));
|
|
180
|
+
await sleep(1500);
|
|
181
|
+
} catch (installError) {
|
|
182
|
+
console.log(colors.error(`\n ❌ Failed to install Chromium: ${installError.message}\n`));
|
|
183
|
+
console.log(colors.muted(" Switching to Axios mode...\n"));
|
|
184
|
+
await sleep(2000);
|
|
185
|
+
this.mode = "axios";
|
|
186
|
+
console.log(colors.success(" ✅ Axios mode selected - fast and lightweight\n"));
|
|
187
|
+
await sleep(1500);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
console.log(colors.success("\n ✅ Axios mode selected - fast and lightweight\n"));
|
|
192
|
+
await sleep(1500);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
this.mode = mode;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* 🌐 Solicita URL alvo com placeholder [XSS]
|
|
200
|
+
* @returns {Promise<void>}
|
|
201
|
+
*/
|
|
202
|
+
async promptForUrl() {
|
|
203
|
+
console.clear();
|
|
204
|
+
|
|
205
|
+
const defaultConfig = getDefaultConfig();
|
|
206
|
+
const demoUrl = defaultConfig.targets[0].url;
|
|
207
|
+
const demoName = defaultConfig.targets[0].name;
|
|
208
|
+
|
|
209
|
+
Logger.showBanner(this.config, 0, this.category, this.targetUrl || demoUrl);
|
|
210
|
+
|
|
211
|
+
console.log(colors.link2.bold("\n ⚙ CONFIGURE TARGET URL\n"));
|
|
212
|
+
console.log(colors.muted(" Enter the URL with [XSS] as placeholder for the payload\n"));
|
|
213
|
+
console.log(colors.text(" 💡 Demo target: ") + colors.link(demoUrl));
|
|
214
|
+
console.log(colors.muted(` (${demoName} - public XSS testing page)\n`));
|
|
215
|
+
console.log(colors.highlight(` ✸ Mode: ${this.mode === "playwright" ? "🌐 Playwright" : "⚡ Axios"}\n`));
|
|
216
|
+
|
|
217
|
+
const { url } = await inquirer.prompt([
|
|
218
|
+
{
|
|
219
|
+
type: "input",
|
|
220
|
+
name: "url",
|
|
221
|
+
prefix: colors.action(">"),
|
|
222
|
+
message: colors.text("Target URL:"),
|
|
223
|
+
default: this.targetUrl || demoUrl,
|
|
224
|
+
validate: (input) => {
|
|
225
|
+
if (!input.includes("[XSS]")) {
|
|
226
|
+
return colors.error("URL must contain [XSS] placeholder!");
|
|
227
|
+
}
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
]);
|
|
232
|
+
|
|
233
|
+
this.targetUrl = url;
|
|
234
|
+
console.log(colors.success(`\n✅ Target configured: ${this.truncateUrl(url)}`));
|
|
235
|
+
|
|
236
|
+
if (url === demoUrl) {
|
|
237
|
+
console.log(colors.highlight2("\n ℹ️ Using demo target - perfect for testing!"));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
await sleep(1500);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* 🧹 Limpa arquivos de configuração e pasta de relatórios
|
|
245
|
+
* @returns {Promise<void>}
|
|
246
|
+
*/
|
|
247
|
+
async cleanExit() {
|
|
248
|
+
console.clear();
|
|
249
|
+
|
|
250
|
+
const { confirm } = await inquirer.prompt([
|
|
251
|
+
{
|
|
252
|
+
type: "confirm",
|
|
253
|
+
name: "confirm",
|
|
254
|
+
message: chalk.hex("#F72585").bold("⚠️ This will DELETE all config files and reports. Continue?"),
|
|
255
|
+
default: false,
|
|
256
|
+
}
|
|
257
|
+
]);
|
|
258
|
+
|
|
259
|
+
if (!confirm) {
|
|
260
|
+
console.log(colors.success("\n✅ Operation cancelled. Returning to menu..."));
|
|
261
|
+
await sleep(1500);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
console.log(colors.warning("\n🧹 Cleaning up...\n"));
|
|
266
|
+
|
|
267
|
+
const configFiles = [
|
|
268
|
+
path.join(process.cwd(), "config.json"),
|
|
269
|
+
path.join(process.cwd(), "config.txt"),
|
|
270
|
+
path.join(__dirname, "..", "..", "config.json"),
|
|
271
|
+
path.join(__dirname, "..", "..", "config.txt")
|
|
272
|
+
];
|
|
273
|
+
|
|
274
|
+
let cleaned = 0;
|
|
275
|
+
for (const file of configFiles) {
|
|
276
|
+
try {
|
|
277
|
+
if (fs.existsSync(file)) {
|
|
278
|
+
fs.unlinkSync(file);
|
|
279
|
+
console.log(colors.success(` ✓ Deleted: ${path.basename(file)}`));
|
|
280
|
+
cleaned++;
|
|
281
|
+
}
|
|
282
|
+
} catch (e) {
|
|
283
|
+
console.log(colors.error(` ✗ Error deleting ${path.basename(file)}: ${e.message}`));
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const reportsDir = path.join(__dirname, "..", "..", "reports");
|
|
288
|
+
try {
|
|
289
|
+
if (fs.existsSync(reportsDir)) {
|
|
290
|
+
fs.rmSync(reportsDir, { recursive: true, force: true });
|
|
291
|
+
console.log(colors.success(" ✓ Reports folder deleted"));
|
|
292
|
+
cleaned++;
|
|
293
|
+
} else {
|
|
294
|
+
console.log(colors.muted(" ℹ Reports folder not found"));
|
|
295
|
+
}
|
|
296
|
+
} catch (e) {
|
|
297
|
+
console.log(colors.error(` ✗ Error deleting reports: ${e.message}`));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (cleaned > 0) {
|
|
301
|
+
console.log(colors.success(`\n✅ Cleanup completed! ${cleaned} item(s) removed.`));
|
|
302
|
+
} else {
|
|
303
|
+
console.log(colors.muted("\nℹ Nothing to clean."));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
Logger.showExit();
|
|
307
|
+
await sleep(2000);
|
|
308
|
+
process.exit(0);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* 📋 Exibe o menu principal interativo
|
|
313
|
+
* @returns {Promise<void>}
|
|
314
|
+
*/
|
|
315
|
+
async showMainMenu() {
|
|
316
|
+
while (true) {
|
|
317
|
+
try {
|
|
318
|
+
console.clear();
|
|
319
|
+
|
|
320
|
+
const defaultConfig = getDefaultConfig();
|
|
321
|
+
const displayUrl = this.targetUrl || defaultConfig.targets[0].url;
|
|
322
|
+
|
|
323
|
+
Logger.showBanner(this.config, 0, "Select category...", displayUrl);
|
|
324
|
+
|
|
325
|
+
if (!fs.existsSync(this.payloadsDir)) {
|
|
326
|
+
console.log(colors.error(`\n Payloads directory not found!`));
|
|
327
|
+
console.log(colors.muted(` Expected: ${this.payloadsDir}\n`));
|
|
328
|
+
console.log(colors.text(` Make sure the package was installed correctly.`));
|
|
329
|
+
console.log(colors.text(` Try: npm uninstall -g rav-xss && npm install -g rav-xss\n`));
|
|
330
|
+
process.exit(1);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const folders = fs.readdirSync(this.payloadsDir, { withFileTypes: true })
|
|
334
|
+
.filter(dirent => dirent.isDirectory())
|
|
335
|
+
.map(dirent => dirent.name);
|
|
336
|
+
|
|
337
|
+
if (folders.length === 0) {
|
|
338
|
+
console.log(colors.error(`\n No payload categories found in:`));
|
|
339
|
+
console.log(colors.muted(` ${this.payloadsDir}\n`));
|
|
340
|
+
process.exit(1);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const menuChoices = [
|
|
344
|
+
new inquirer.Separator(colors.dim("─".repeat(55))),
|
|
345
|
+
];
|
|
346
|
+
|
|
347
|
+
for (const folderName of folders) {
|
|
348
|
+
const key = folderName.toLowerCase();
|
|
349
|
+
const info = CATEGORY_MAP[key] || [`📂 ${folderName}`, "Custom payloads"];
|
|
350
|
+
const displayName = info[0];
|
|
351
|
+
const description = info[1];
|
|
352
|
+
|
|
353
|
+
menuChoices.push({
|
|
354
|
+
name: ` ${displayName} ${colors.dim("—")} ${colors.menuDescription(description)}`,
|
|
355
|
+
value: folderName,
|
|
356
|
+
short: displayName.replace(/[🔰🛡️🎭👁️🔥💎]/g, "").trim()
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
menuChoices.push(
|
|
361
|
+
new inquirer.Separator(colors.dim("─".repeat(55))),
|
|
362
|
+
{
|
|
363
|
+
name: ` ${colors.menuConfig("🎯")} ${colors.menuConfig.bold("Configure Target URL")}`,
|
|
364
|
+
value: "config_url",
|
|
365
|
+
short: "Configure URL"
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
name: ` ${colors.danger("🧹")} ${colors.danger.bold("Clean and Exit")}`,
|
|
369
|
+
value: "clean_exit",
|
|
370
|
+
short: "Clean"
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
name: ` ${colors.menuExit("❌")} ${colors.menuExit.bold("Exit")}`,
|
|
374
|
+
value: "exit",
|
|
375
|
+
short: "Exit"
|
|
376
|
+
}
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
const { action } = await inquirer.prompt([
|
|
380
|
+
{
|
|
381
|
+
type: "list",
|
|
382
|
+
name: "action",
|
|
383
|
+
prefix: colors.highlight2.bold("✸"),
|
|
384
|
+
message: colors.highlight2.bold("SELECT PAYLOAD CATEGORY"),
|
|
385
|
+
choices: menuChoices,
|
|
386
|
+
pageSize: 12,
|
|
387
|
+
loop: false
|
|
388
|
+
}
|
|
389
|
+
]);
|
|
390
|
+
|
|
391
|
+
if (action === "exit") {
|
|
392
|
+
Logger.showExit();
|
|
393
|
+
await sleep(1500);
|
|
394
|
+
process.exit(0);
|
|
395
|
+
} else if (action === "clean_exit") {
|
|
396
|
+
await this.cleanExit();
|
|
397
|
+
continue;
|
|
398
|
+
} else if (action === "config_url") {
|
|
399
|
+
await this.promptForUrl();
|
|
400
|
+
continue;
|
|
401
|
+
} else {
|
|
402
|
+
this.category = action;
|
|
403
|
+
this.mode = null;
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
} catch (error) {
|
|
408
|
+
if (error.message === "User force closed the prompt") {
|
|
409
|
+
Logger.showExit();
|
|
410
|
+
await sleep(1500);
|
|
411
|
+
process.exit(0);
|
|
412
|
+
}
|
|
413
|
+
console.log(colors.error(`Menu error: ${error.message}`));
|
|
414
|
+
await sleep(2000);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* 📂 Encontra o arquivo de payload na categoria
|
|
421
|
+
* @param {string} categoryDir - Diretório da categoria
|
|
422
|
+
* @returns {string|null} Caminho do arquivo de payload ou null
|
|
423
|
+
*/
|
|
424
|
+
findPayloadFile(categoryDir) {
|
|
425
|
+
if (!fs.existsSync(categoryDir)) return null;
|
|
426
|
+
|
|
427
|
+
const files = fs.readdirSync(categoryDir);
|
|
428
|
+
|
|
429
|
+
const txtFile = files.find(f => f.endsWith(".txt"));
|
|
430
|
+
if (txtFile) return path.join(categoryDir, txtFile);
|
|
431
|
+
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* 📥 Carrega payloads do diretório da categoria
|
|
437
|
+
* @returns {Promise<void>}
|
|
438
|
+
*/
|
|
439
|
+
async loadPayloads() {
|
|
440
|
+
const categoryDir = path.join(this.payloadsDir, this.category);
|
|
441
|
+
|
|
442
|
+
if (!fs.existsSync(categoryDir)) {
|
|
443
|
+
const availableCats = fs.existsSync(this.payloadsDir)
|
|
444
|
+
? fs.readdirSync(this.payloadsDir).filter(d =>
|
|
445
|
+
fs.statSync(path.join(this.payloadsDir, d)).isDirectory()
|
|
446
|
+
).join(", ")
|
|
447
|
+
: "NONE";
|
|
448
|
+
|
|
449
|
+
throw new Error(
|
|
450
|
+
`Category directory not found: ${categoryDir}\n` +
|
|
451
|
+
`Available categories: ${availableCats}`
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const payloadPath = this.findPayloadFile(categoryDir);
|
|
456
|
+
|
|
457
|
+
if (!payloadPath) {
|
|
458
|
+
const files = fs.readdirSync(categoryDir);
|
|
459
|
+
throw new Error(
|
|
460
|
+
`No .txt file found in ${categoryDir}\n` +
|
|
461
|
+
`Files found: ${files.join(", ") || "NONE"}`
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
this.payloads = fs.readFileSync(payloadPath, "utf8")
|
|
466
|
+
.split("\n")
|
|
467
|
+
.map(l => l.trim())
|
|
468
|
+
.filter(Boolean);
|
|
469
|
+
|
|
470
|
+
if (this.payloads.length === 0) {
|
|
471
|
+
throw new Error("No payloads loaded. File is empty.");
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* 🛡️ Verifica se a resposta HTTP indica um bloqueio por WAF ou segurança
|
|
477
|
+
* @param {Object} response - Resposta do axios
|
|
478
|
+
* @returns {boolean} true se for um bloqueio
|
|
479
|
+
*/
|
|
480
|
+
isSecurityBlock(response) {
|
|
481
|
+
if (response.status === 403 || response.status === 429) {
|
|
482
|
+
return true;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (response.status >= 500 && response.status < 600) {
|
|
486
|
+
return true;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const bodyStr = typeof response.data === 'string' ? response.data.toLowerCase() : '';
|
|
490
|
+
|
|
491
|
+
if (bodyStr.includes('cloudflare') &&
|
|
492
|
+
(bodyStr.includes('ray id') ||
|
|
493
|
+
bodyStr.includes('just a moment') ||
|
|
494
|
+
bodyStr.includes('attention required') ||
|
|
495
|
+
bodyStr.includes('sorry, you have been blocked') ||
|
|
496
|
+
bodyStr.includes('you are unable to access'))) {
|
|
497
|
+
return true;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (bodyStr.includes('access denied') &&
|
|
501
|
+
(bodyStr.includes('waf') || bodyStr.includes('firewall') || bodyStr.includes('security'))) {
|
|
502
|
+
return true;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* 🔍 Verifica se o payload foi refletido na resposta de forma potencialmente perigosa
|
|
510
|
+
* @param {string} responseData - Corpo da resposta HTML/texto
|
|
511
|
+
* @param {string} payload - Payload injetado
|
|
512
|
+
* @returns {boolean} true se o payload foi refletido
|
|
513
|
+
*/
|
|
514
|
+
isPayloadReflected(responseData, payload) {
|
|
515
|
+
if (!responseData || typeof responseData !== 'string') {
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const htmlWithoutComments = responseData.replace(/<!--[\s\S]*?-->/g, '');
|
|
520
|
+
|
|
521
|
+
const htmlEntities = {
|
|
522
|
+
'<': '<',
|
|
523
|
+
'>': '>',
|
|
524
|
+
'"': '"',
|
|
525
|
+
''': "'",
|
|
526
|
+
'/': '/',
|
|
527
|
+
'&': '&'
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
let fullyEscapedPayload = payload;
|
|
531
|
+
for (const [entity, char] of Object.entries(htmlEntities)) {
|
|
532
|
+
const escapedChar = entity;
|
|
533
|
+
fullyEscapedPayload = fullyEscapedPayload.split(char).join(escapedChar);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (fullyEscapedPayload !== payload &&
|
|
537
|
+
htmlWithoutComments.includes(fullyEscapedPayload) &&
|
|
538
|
+
!htmlWithoutComments.includes(payload)) {
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const searchPayloads = [payload];
|
|
543
|
+
|
|
544
|
+
if (payload.includes('<') && payload.includes('>')) {
|
|
545
|
+
const encodedLt = payload.replace(/</g, '<');
|
|
546
|
+
if (encodedLt !== payload) {
|
|
547
|
+
searchPayloads.push(encodedLt);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
for (const searchPayload of searchPayloads) {
|
|
552
|
+
if (htmlWithoutComments.includes(searchPayload)) {
|
|
553
|
+
return true;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* 🎯 Testa um payload individual contra a URL alvo
|
|
562
|
+
* @param {string} payload - Payload a ser testado
|
|
563
|
+
* @param {number} index - Índice do payload na lista
|
|
564
|
+
* @returns {Object} Resultado do teste
|
|
565
|
+
*/
|
|
566
|
+
async testPayload(payload, index) {
|
|
567
|
+
const url = this.targetUrl.replace("[XSS]", encodeURIComponent(payload));
|
|
568
|
+
let vulnerable = false;
|
|
569
|
+
let dialogMessage = null;
|
|
570
|
+
|
|
571
|
+
try {
|
|
572
|
+
let response;
|
|
573
|
+
|
|
574
|
+
if (this.browserManager && this.mode === "playwright") {
|
|
575
|
+
response = await this.browserManager.request(url);
|
|
576
|
+
|
|
577
|
+
if (response && response.dialogMessage) {
|
|
578
|
+
vulnerable = true;
|
|
579
|
+
dialogMessage = response.dialogMessage;
|
|
580
|
+
}
|
|
581
|
+
} else {
|
|
582
|
+
response = await axios.get(url, {
|
|
583
|
+
timeout: this.config.scanner.timeout_ms,
|
|
584
|
+
headers: {
|
|
585
|
+
"User-Agent": this.config.scanner.user_agent,
|
|
586
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
587
|
+
"Accept-Language": "en-US,en;q=0.5"
|
|
588
|
+
},
|
|
589
|
+
validateStatus: () => true,
|
|
590
|
+
maxRedirects: 5
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
if (!this.isSecurityBlock(response)) {
|
|
594
|
+
const responseData = typeof response.data === 'string' ? response.data : '';
|
|
595
|
+
vulnerable = this.isPayloadReflected(responseData, payload);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
} catch (err) {
|
|
599
|
+
// Request failed silently
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
return { payload, url, vulnerable, index: index + 1, dialogMessage };
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* ⏱️ Obtém o delay configurado em milissegundos
|
|
607
|
+
* @returns {number} Delay em milissegundos
|
|
608
|
+
*/
|
|
609
|
+
getConfiguredDelay() {
|
|
610
|
+
if (this.config?.scanner?.delay_between_requests_ms !== undefined &&
|
|
611
|
+
this.config?.scanner?.delay_between_requests_ms !== null) {
|
|
612
|
+
return parseInt(this.config.scanner.delay_between_requests_ms) || 0;
|
|
613
|
+
}
|
|
614
|
+
return 0;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* 🚀 Executa o scan completo
|
|
619
|
+
* @returns {Promise<void>}
|
|
620
|
+
*/
|
|
621
|
+
async run() {
|
|
622
|
+
await this.initialize();
|
|
623
|
+
|
|
624
|
+
if (this.args.headed && this.mode === "playwright") {
|
|
625
|
+
if (!this.config.scanner) this.config.scanner = {};
|
|
626
|
+
this.config.scanner.headless = false;
|
|
627
|
+
this.browserManager.config.scanner.headless = false;
|
|
628
|
+
await this.browserManager.close();
|
|
629
|
+
await this.browserManager.launch();
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
this.results.scan_start = new Date().toISOString();
|
|
633
|
+
|
|
634
|
+
console.clear();
|
|
635
|
+
|
|
636
|
+
const displayMode = this.mode === "playwright" ? "🌐 Playwright" : "⚡ Axios";
|
|
637
|
+
Logger.showBanner(this.config, this.payloads.length, this.category, this.targetUrl);
|
|
638
|
+
|
|
639
|
+
console.log(colors.highlight(`\n ✸ Mode: ${displayMode}`));
|
|
640
|
+
|
|
641
|
+
if (this.mode === "playwright") {
|
|
642
|
+
if (this.config.scanner.headless === false) {
|
|
643
|
+
console.log(colors.warning(" 🖥️ Headed mode - browser window will be visible\n"));
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
Logger.log("info",
|
|
648
|
+
`Testing ${colors.primary.bold(String(this.payloads.length))} payloads from ${colors.highlight.bold(this.category)} category\n`
|
|
649
|
+
);
|
|
650
|
+
|
|
651
|
+
const spinner = ora({
|
|
652
|
+
text: colors.action("Initializing scan..."),
|
|
653
|
+
spinner: { interval: 80, frames: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] },
|
|
654
|
+
color: "cyan"
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
if (!this.args.verbose) spinner.start();
|
|
658
|
+
|
|
659
|
+
const concurrentLimit = this.mode === "playwright" ? 1 : 5;
|
|
660
|
+
let cursor = 0;
|
|
661
|
+
|
|
662
|
+
const configuredDelay = this.getConfiguredDelay();
|
|
663
|
+
const effectiveDelay = configuredDelay > 0 ? configuredDelay : 500;
|
|
664
|
+
|
|
665
|
+
while (cursor < this.payloads.length) {
|
|
666
|
+
const batch = [];
|
|
667
|
+
for (let i = cursor; i < cursor + concurrentLimit && i < this.payloads.length; i++) {
|
|
668
|
+
batch.push(this.testPayload(this.payloads[i], i));
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const batchResults = await Promise.all(batch);
|
|
672
|
+
|
|
673
|
+
for (const result of batchResults) {
|
|
674
|
+
this.results.total_tests++;
|
|
675
|
+
|
|
676
|
+
if (!this.args.verbose && spinner.isSpinning) {
|
|
677
|
+
spinner.text = colors.action(
|
|
678
|
+
`${result.index}/${this.payloads.length} ${colors.primary("▸")} ${colors.text(result.payload.substring(0, 30))}`
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
if (result.vulnerable) {
|
|
683
|
+
this.results.vulns_found++;
|
|
684
|
+
this.results.findings.push(result);
|
|
685
|
+
|
|
686
|
+
if (spinner.isSpinning) spinner.stop();
|
|
687
|
+
|
|
688
|
+
const shortPayload = result.payload.length > 50
|
|
689
|
+
? result.payload.substring(0, 47) + "..."
|
|
690
|
+
: result.payload;
|
|
691
|
+
|
|
692
|
+
Logger.log("vuln",
|
|
693
|
+
`#${String(result.index).padStart(4, "0")} ${colors.icon.arrow} ${colors.highlight(shortPayload)}`
|
|
694
|
+
);
|
|
695
|
+
|
|
696
|
+
if (!this.args.verbose) spinner.start();
|
|
697
|
+
|
|
698
|
+
} else if (this.args.verbose || this.config.output?.show_safe) {
|
|
699
|
+
if (spinner.isSpinning) spinner.stop();
|
|
700
|
+
Logger.log("safe", `#${String(result.index).padStart(4, "0")} ${colors.icon.success} ${colors.muted(result.payload.substring(0, 40))}`);
|
|
701
|
+
if (!this.args.verbose) spinner.start();
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
cursor += concurrentLimit;
|
|
706
|
+
|
|
707
|
+
if (cursor < this.payloads.length) {
|
|
708
|
+
await sleep(effectiveDelay);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
if (spinner.isSpinning) {
|
|
713
|
+
spinner.succeed(colors.success("Scan completed"));
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
if (this.browserManager) {
|
|
717
|
+
await this.browserManager.close();
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
this.results.scan_end = new Date().toISOString();
|
|
721
|
+
|
|
722
|
+
const { textPath } = this.reporter.saveReport(this.results, this.targetUrl);
|
|
723
|
+
const displayPath = this.formatReportPath(textPath);
|
|
724
|
+
const duration = ((new Date(this.results.scan_end) - new Date(this.results.scan_start)) / 1000).toFixed(1);
|
|
725
|
+
|
|
726
|
+
Logger.showResults(
|
|
727
|
+
this.results,
|
|
728
|
+
this.targetUrl,
|
|
729
|
+
this.category,
|
|
730
|
+
duration,
|
|
731
|
+
displayPath,
|
|
732
|
+
this.reporter.reportDir
|
|
733
|
+
);
|
|
734
|
+
|
|
735
|
+
process.exit(this.results.vulns_found > 0 ? 1 : 0);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* 📄 Formata o caminho do relatório para exibição
|
|
740
|
+
* Exibe caminho completo no Termux
|
|
741
|
+
* @param {string} absolutePath - Caminho absoluto do relatório
|
|
742
|
+
* @returns {string} Caminho formatado
|
|
743
|
+
*/
|
|
744
|
+
formatReportPath(absolutePath) {
|
|
745
|
+
const normalized = absolutePath.replace(/\\/g, "/");
|
|
746
|
+
|
|
747
|
+
const packageBase = path.join(__dirname, "..", "..").replace(/\\/g, "/");
|
|
748
|
+
|
|
749
|
+
if (normalized.startsWith(packageBase)) {
|
|
750
|
+
let relative = normalized.substring(packageBase.length);
|
|
751
|
+
if (relative.startsWith("/")) relative = relative.substring(1);
|
|
752
|
+
return `[package]/${relative}`;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const cwd = process.cwd().replace(/\\/g, "/");
|
|
756
|
+
if (normalized.startsWith(cwd)) {
|
|
757
|
+
let relative = normalized.substring(cwd.length);
|
|
758
|
+
if (relative.startsWith("/")) relative = relative.substring(1);
|
|
759
|
+
return `./${relative}`;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
return normalized;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* 🔗 Trunca a URL para exibição compacta
|
|
767
|
+
* @param {string} url - URL completa
|
|
768
|
+
* @param {number} maxLength - Comprimento máximo
|
|
769
|
+
* @returns {string} URL truncada
|
|
770
|
+
*/
|
|
771
|
+
truncateUrl(url, maxLength = 55) {
|
|
772
|
+
if (!url) return "N/A";
|
|
773
|
+
if (url.length <= maxLength) return url;
|
|
774
|
+
return url.substring(0, maxLength - 3) + "...";
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
module.exports = { XSSScanner };
|