redgun-security 1.0.0
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/.github/workflows/security.yml +21 -0
- package/.redgunignore +11 -0
- package/LICENSE +21 -0
- package/README.md +281 -0
- package/action.yml +67 -0
- package/bin/redgun.js +195 -0
- package/package.json +48 -0
- package/scan.js +474 -0
- package/src/core/findings.js +38 -0
- package/src/core/reporter/console.js +64 -0
- package/src/core/reporter/html.js +105 -0
- package/src/core/reporter/json.js +66 -0
- package/src/core/score.js +41 -0
- package/src/local/auth.js +121 -0
- package/src/local/code-vulnerabilities.js +94 -0
- package/src/local/command-injection.js +74 -0
- package/src/local/crypto.js +97 -0
- package/src/local/dependencies.js +83 -0
- package/src/local/deserialization.js +67 -0
- package/src/local/env.js +80 -0
- package/src/local/headers-config.js +70 -0
- package/src/local/index.js +46 -0
- package/src/local/jwt.js +86 -0
- package/src/local/path-traversal.js +71 -0
- package/src/local/prototype-pollution.js +66 -0
- package/src/local/secrets.js +92 -0
- package/src/local/ssrf.js +70 -0
- package/src/local/ssti.js +75 -0
- package/src/utils/fetch.js +55 -0
- package/src/utils/patterns.js +205 -0
package/scan.js
ADDED
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
import { addFinding } from './src/core/findings.js';
|
|
2
|
+
import { fetchText, checkUrl } from './src/utils/fetch.js';
|
|
3
|
+
import { EXPOSED_FILES, HEADER_CHECKS, COMMON_SUBDOMAINS, COMMON_PORTS, SECRET_PATTERNS } from './src/utils/patterns.js';
|
|
4
|
+
|
|
5
|
+
export async function runRemoteScan(url, spinner, modules = null) {
|
|
6
|
+
const target = new URL(url);
|
|
7
|
+
const hostname = target.hostname;
|
|
8
|
+
const origin = target.origin;
|
|
9
|
+
|
|
10
|
+
const allModules = [
|
|
11
|
+
{ name: 'HTTP Headers', value: 'headers', fn: () => scanHeaders(origin, spinner) },
|
|
12
|
+
{ name: 'Exposed Files & Paths', value: 'files', fn: () => scanExposedFiles(origin, spinner) },
|
|
13
|
+
{ name: 'Secrets in JS Bundles', value: 'secrets', fn: () => scanSecrets(origin, spinner) },
|
|
14
|
+
{ name: 'XSS Reflected', value: 'xss', fn: () => scanXss(origin, spinner) },
|
|
15
|
+
{ name: 'SQL Injection', value: 'sqli', fn: () => scanSqli(origin, spinner) },
|
|
16
|
+
{ name: 'CORS Misconfiguration', value: 'cors', fn: () => scanCors(origin, spinner) },
|
|
17
|
+
{ name: 'Open Redirect', value: 'redirect', fn: () => scanOpenRedirect(origin, spinner) },
|
|
18
|
+
{ name: 'SSRF Endpoints (HackTricks)', value: 'ssrf', fn: () => scanSsrf(origin, spinner) },
|
|
19
|
+
{ name: 'Host Header Injection (HackTricks)', value: 'hostheader', fn: () => scanHostHeader(origin, spinner) },
|
|
20
|
+
{ name: 'HTTP Request Smuggling (HackTricks)', value: 'smuggling', fn: () => scanSmuggling(origin, spinner) },
|
|
21
|
+
{ name: 'CRLF Injection (HackTricks)', value: 'crlf', fn: () => scanCrlf(origin, spinner) },
|
|
22
|
+
{ name: 'GraphQL Introspection (HackTricks)', value: 'graphql', fn: () => scanGraphql(origin, spinner) },
|
|
23
|
+
{ name: 'Clickjacking', value: 'clickjack', fn: () => scanClickjacking(origin, spinner) },
|
|
24
|
+
{ name: 'Cookie Security', value: 'cookies', fn: () => scanCookies(origin, spinner) },
|
|
25
|
+
{ name: 'HTTP Methods', value: 'methods', fn: () => scanMethods(origin, spinner) },
|
|
26
|
+
{ name: 'Subdomain Enumeration', value: 'subdomains', fn: () => scanSubdomains(hostname, spinner) },
|
|
27
|
+
{ name: 'DNS & Email Security', value: 'dns', fn: () => scanDns(hostname, spinner) },
|
|
28
|
+
{ name: 'Technology Fingerprint', value: 'tech', fn: () => scanTech(origin, spinner) },
|
|
29
|
+
{ name: 'API Discovery', value: 'api', fn: () => scanApi(origin, spinner) },
|
|
30
|
+
{ name: 'SSL/TLS Analysis', value: 'ssl', fn: () => scanSsl(origin, spinner) },
|
|
31
|
+
{ name: 'Path Traversal (HackTricks)', value: 'lfi', fn: () => scanPathTraversal(origin, spinner) },
|
|
32
|
+
{ name: 'NoSQL Injection (HackTricks)', value: 'nosqli', fn: () => scanNosqli(origin, spinner) },
|
|
33
|
+
{ name: 'WebSocket Security (HackTricks)', value: 'websocket', fn: () => scanWebsocket(origin, spinner) },
|
|
34
|
+
{ name: 'Cache Poisoning (HackTricks)', value: 'cache', fn: () => scanCachePoisoning(origin, spinner) },
|
|
35
|
+
{ name: 'Race Condition Detection (HackTricks)', value: 'race', fn: () => scanRaceCondition(origin, spinner) },
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const toRun = modules ? allModules.filter((m) => modules.includes(m.value)) : allModules;
|
|
39
|
+
|
|
40
|
+
for (const mod of toRun) {
|
|
41
|
+
try {
|
|
42
|
+
spinner.text = `[Remote] ${mod.name}...`;
|
|
43
|
+
await mod.fn();
|
|
44
|
+
} catch (err) {
|
|
45
|
+
// Module failed silently
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function scanHeaders(origin, spinner) {
|
|
51
|
+
spinner.text = 'Checking HTTP security headers...';
|
|
52
|
+
try {
|
|
53
|
+
const resp = await fetchText(origin);
|
|
54
|
+
const headers = resp.headers;
|
|
55
|
+
|
|
56
|
+
for (const header of HEADER_CHECKS) {
|
|
57
|
+
if (!headers[header]) {
|
|
58
|
+
const severity = ['content-security-policy', 'strict-transport-security'].includes(header) ? 'MEDIUM' : 'LOW';
|
|
59
|
+
addFinding(severity, 'HTTP Headers', `Missing ${header}`, `Header ${header} not set on ${origin}`, `Add ${header} header to your server response`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (headers['server']) {
|
|
64
|
+
addFinding('LOW', 'HTTP Headers', 'Server header exposes technology', `Server: ${headers['server']}`, 'Remove or obfuscate the Server header');
|
|
65
|
+
}
|
|
66
|
+
if (headers['x-powered-by']) {
|
|
67
|
+
addFinding('LOW', 'HTTP Headers', 'X-Powered-By exposes technology', `X-Powered-By: ${headers['x-powered-by']}`, 'Remove the X-Powered-By header');
|
|
68
|
+
}
|
|
69
|
+
} catch {}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function scanExposedFiles(origin, spinner) {
|
|
73
|
+
spinner.text = 'Checking for exposed files...';
|
|
74
|
+
for (const path of EXPOSED_FILES) {
|
|
75
|
+
try {
|
|
76
|
+
const resp = await fetchText(`${origin}${path}`, {}, 5000);
|
|
77
|
+
if (resp.status === 200 && resp.body.length > 0 && resp.body.length < 1024 * 1024) {
|
|
78
|
+
if (!isSpaFallback(resp.body, path)) {
|
|
79
|
+
const severity = /\.env|\.git|phpinfo|actuator|heapdump|backup\.sql/i.test(path) ? 'CRITICAL' : 'MEDIUM';
|
|
80
|
+
addFinding(severity, 'Exposed Files', `Accessible: ${path}`, `${origin}${path} returns ${resp.status} (${resp.body.length} bytes)`, `Block access to ${path} in your web server configuration`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} catch {}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function scanSecrets(origin, spinner) {
|
|
88
|
+
spinner.text = 'Extracting secrets from page source...';
|
|
89
|
+
try {
|
|
90
|
+
const resp = await fetchText(origin);
|
|
91
|
+
for (const [name, pattern] of Object.entries(SECRET_PATTERNS)) {
|
|
92
|
+
const regex = new RegExp(pattern.source, pattern.flags);
|
|
93
|
+
let match;
|
|
94
|
+
while ((match = regex.exec(resp.body)) !== null) {
|
|
95
|
+
addFinding('HIGH', 'Secrets Detection', `${name} exposed in page source`, `Found in HTML/JS at ${origin}`, 'Move secrets to server-side environment variables. Never expose in client bundles.');
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} catch {}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function scanXss(origin, spinner) {
|
|
103
|
+
spinner.text = 'Testing for reflected XSS...';
|
|
104
|
+
const payloads = [
|
|
105
|
+
'<script>alert(1)</script>',
|
|
106
|
+
'"><img src=x onerror=alert(1)>',
|
|
107
|
+
"'-alert(1)-'",
|
|
108
|
+
'{{7*7}}',
|
|
109
|
+
'${7*7}',
|
|
110
|
+
'<svg/onload=alert(1)>',
|
|
111
|
+
];
|
|
112
|
+
const params = ['q', 'search', 'query', 'id', 'name', 'page', 'url', 'redirect', 'next', 'callback', 'input', 'data', 'file', 'path'];
|
|
113
|
+
|
|
114
|
+
for (const param of params) {
|
|
115
|
+
for (const payload of payloads) {
|
|
116
|
+
try {
|
|
117
|
+
const testUrl = `${origin}/?${param}=${encodeURIComponent(payload)}`;
|
|
118
|
+
const resp = await fetchText(testUrl, {}, 5000);
|
|
119
|
+
if (resp.body.includes(payload)) {
|
|
120
|
+
addFinding('HIGH', 'XSS', `Reflected XSS via ?${param}=`, `Payload reflected unescaped: ${payload.substring(0, 40)}`, 'Implement output encoding. Use CSP headers. Sanitize all user input before reflecting.');
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
} catch {}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function scanSqli(origin, spinner) {
|
|
129
|
+
spinner.text = 'Testing for SQL injection...';
|
|
130
|
+
const payloads = ["'", "' OR '1'='1", "1' AND '1'='1", "1 UNION SELECT NULL--", "'; DROP TABLE--", "1' WAITFOR DELAY '0:0:5'--"];
|
|
131
|
+
const params = ['id', 'user', 'name', 'page', 'category', 'item', 'product'];
|
|
132
|
+
const errorPatterns = /sql syntax|mysql|postgresql|sqlite|oracle|microsoft sql|unclosed quotation|unterminated string/i;
|
|
133
|
+
|
|
134
|
+
for (const param of params) {
|
|
135
|
+
for (const payload of payloads) {
|
|
136
|
+
try {
|
|
137
|
+
const testUrl = `${origin}/?${param}=${encodeURIComponent(payload)}`;
|
|
138
|
+
const resp = await fetchText(testUrl, {}, 5000);
|
|
139
|
+
if (errorPatterns.test(resp.body)) {
|
|
140
|
+
addFinding('CRITICAL', 'SQL Injection', `SQL error triggered via ?${param}=`, `Database error message exposed with payload: ${payload}`, 'Use parameterized queries. Never concatenate user input into SQL statements.');
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
} catch {}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function scanCors(origin, spinner) {
|
|
149
|
+
spinner.text = 'Testing CORS misconfiguration...';
|
|
150
|
+
const testOrigins = ['https://evil.com', 'null', `https://sub.${new URL(origin).hostname}`];
|
|
151
|
+
|
|
152
|
+
for (const testOrigin of testOrigins) {
|
|
153
|
+
try {
|
|
154
|
+
const resp = await fetchText(origin, { headers: { Origin: testOrigin } });
|
|
155
|
+
const acao = resp.headers['access-control-allow-origin'];
|
|
156
|
+
const acac = resp.headers['access-control-allow-credentials'];
|
|
157
|
+
|
|
158
|
+
if (acao === '*' && acac === 'true') {
|
|
159
|
+
addFinding('CRITICAL', 'CORS', 'CORS wildcard with credentials', 'Access-Control-Allow-Origin: * with Allow-Credentials: true', 'Never use wildcard origin with credentials. Whitelist specific origins.');
|
|
160
|
+
} else if (acao === testOrigin && testOrigin === 'https://evil.com') {
|
|
161
|
+
const sev = acac === 'true' ? 'HIGH' : 'MEDIUM';
|
|
162
|
+
addFinding(sev, 'CORS', 'CORS reflects arbitrary origin', `Origin ${testOrigin} is reflected in ACAO header${acac === 'true' ? ' WITH credentials' : ''}`, 'Implement a strict origin whitelist. Do not reflect the Origin header blindly.');
|
|
163
|
+
} else if (acao === 'null') {
|
|
164
|
+
addFinding('MEDIUM', 'CORS', 'CORS allows null origin', 'Access-Control-Allow-Origin: null', 'Block null origin. It can be triggered from sandboxed iframes and data: URIs.');
|
|
165
|
+
}
|
|
166
|
+
} catch {}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function scanOpenRedirect(origin, spinner) {
|
|
171
|
+
spinner.text = 'Testing for open redirects...';
|
|
172
|
+
const params = ['url', 'redirect', 'next', 'return', 'returnTo', 'goto', 'redirect_uri', 'continue', 'dest', 'destination', 'rurl', 'target'];
|
|
173
|
+
const payload = 'https://evil.com';
|
|
174
|
+
|
|
175
|
+
for (const param of params) {
|
|
176
|
+
try {
|
|
177
|
+
const resp = await fetchText(`${origin}/?${param}=${encodeURIComponent(payload)}`, { redirect: 'manual' }, 5000);
|
|
178
|
+
const location = resp.headers['location'] || '';
|
|
179
|
+
if (location.includes('evil.com')) {
|
|
180
|
+
addFinding('MEDIUM', 'Open Redirect', `Open redirect via ?${param}=`, `Redirects to attacker-controlled URL: ${location}`, 'Validate redirect URLs against a whitelist of allowed domains. Use relative paths only.');
|
|
181
|
+
}
|
|
182
|
+
} catch {}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function scanSsrf(origin, spinner) {
|
|
187
|
+
spinner.text = 'Testing SSRF endpoints (HackTricks)...';
|
|
188
|
+
const ssrfParams = ['url', 'uri', 'link', 'src', 'href', 'path', 'file', 'page', 'proxy', 'callback', 'webhook', 'feed', 'fetch', 'load', 'image', 'img'];
|
|
189
|
+
const ssrfPayloads = [
|
|
190
|
+
'http://169.254.169.254/latest/meta-data/',
|
|
191
|
+
'http://127.0.0.1:22',
|
|
192
|
+
'http://localhost:6379',
|
|
193
|
+
'http://[::1]/',
|
|
194
|
+
'http://0x7f000001/',
|
|
195
|
+
'http://2130706433/',
|
|
196
|
+
'file:///etc/passwd',
|
|
197
|
+
];
|
|
198
|
+
|
|
199
|
+
for (const param of ssrfParams) {
|
|
200
|
+
for (const payload of ssrfPayloads) {
|
|
201
|
+
try {
|
|
202
|
+
const resp = await fetchText(`${origin}/?${param}=${encodeURIComponent(payload)}`, {}, 5000);
|
|
203
|
+
if (/ami-id|instance-id|root:x:0|ssh-|redis_version|127\.0\.0\.1/i.test(resp.body)) {
|
|
204
|
+
addFinding('CRITICAL', 'SSRF (HackTricks)', `SSRF via ?${param}= parameter`, `Internal resource accessible: ${payload}`, 'Block internal IP ranges. Implement URL validation whitelist. Use egress proxy.');
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
} catch {}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function scanHostHeader(origin, spinner) {
|
|
213
|
+
spinner.text = 'Testing Host header injection (HackTricks)...';
|
|
214
|
+
try {
|
|
215
|
+
const evilHost = 'evil.com';
|
|
216
|
+
const resp = await fetchText(origin, { headers: { Host: evilHost, 'X-Forwarded-Host': evilHost } });
|
|
217
|
+
if (resp.body.includes(evilHost)) {
|
|
218
|
+
addFinding('HIGH', 'Host Header (HackTricks)', 'Host header injection reflected', 'Injected Host header value appears in response body', 'Validate Host header against a whitelist. Do not use Host header to generate URLs.');
|
|
219
|
+
}
|
|
220
|
+
} catch {}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function scanSmuggling(origin, spinner) {
|
|
224
|
+
spinner.text = 'Testing HTTP request smuggling indicators (HackTricks)...';
|
|
225
|
+
try {
|
|
226
|
+
const resp = await fetchText(origin, {
|
|
227
|
+
method: 'POST',
|
|
228
|
+
headers: {
|
|
229
|
+
'Transfer-Encoding': 'chunked',
|
|
230
|
+
'Content-Length': '4',
|
|
231
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
232
|
+
},
|
|
233
|
+
body: '0\r\n\r\nG',
|
|
234
|
+
}, 10000);
|
|
235
|
+
|
|
236
|
+
if (resp.status === 400 || resp.status === 501) {
|
|
237
|
+
addFinding('INFO', 'HTTP Smuggling (HackTricks)', 'Server may be vulnerable to request smuggling', `CL.TE probe returned status ${resp.status}`, 'Ensure front-end and back-end agree on request boundaries. Normalize Transfer-Encoding handling.');
|
|
238
|
+
}
|
|
239
|
+
} catch {}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function scanCrlf(origin, spinner) {
|
|
243
|
+
spinner.text = 'Testing CRLF injection (HackTricks)...';
|
|
244
|
+
const payloads = ['%0d%0aX-Injected:true', '%0aX-Injected:true', '\\r\\nX-Injected:true'];
|
|
245
|
+
|
|
246
|
+
for (const payload of payloads) {
|
|
247
|
+
try {
|
|
248
|
+
const resp = await fetchText(`${origin}/${payload}`, { redirect: 'manual' }, 5000);
|
|
249
|
+
if (resp.headers['x-injected'] === 'true') {
|
|
250
|
+
addFinding('HIGH', 'CRLF Injection (HackTricks)', 'CRLF injection in response headers', 'Injected header appears in server response', 'Sanitize CRLF characters from all user input used in HTTP headers. URL-encode outputs.');
|
|
251
|
+
}
|
|
252
|
+
} catch {}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function scanGraphql(origin, spinner) {
|
|
257
|
+
spinner.text = 'Testing GraphQL introspection (HackTricks)...';
|
|
258
|
+
const endpoints = ['/graphql', '/graphiql', '/v1/graphql', '/api/graphql', '/query'];
|
|
259
|
+
const introspectionQuery = '{"query":"{ __schema { types { name } } }"}';
|
|
260
|
+
|
|
261
|
+
for (const endpoint of endpoints) {
|
|
262
|
+
try {
|
|
263
|
+
const resp = await fetchText(`${origin}${endpoint}`, {
|
|
264
|
+
method: 'POST',
|
|
265
|
+
headers: { 'Content-Type': 'application/json' },
|
|
266
|
+
body: introspectionQuery,
|
|
267
|
+
}, 5000);
|
|
268
|
+
|
|
269
|
+
if (resp.body.includes('__schema') || resp.body.includes('"types"')) {
|
|
270
|
+
addFinding('MEDIUM', 'GraphQL (HackTricks)', `GraphQL introspection enabled at ${endpoint}`, 'Full schema is exposed via introspection query', 'Disable introspection in production. Use query complexity limits and depth limiting.');
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
} catch {}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function scanClickjacking(origin, spinner) {
|
|
278
|
+
spinner.text = 'Testing clickjacking protection...';
|
|
279
|
+
try {
|
|
280
|
+
const resp = await fetchText(origin);
|
|
281
|
+
const xfo = resp.headers['x-frame-options'];
|
|
282
|
+
const csp = resp.headers['content-security-policy'];
|
|
283
|
+
if (!xfo && (!csp || !csp.includes('frame-ancestors'))) {
|
|
284
|
+
addFinding('MEDIUM', 'Clickjacking', 'No clickjacking protection', 'Neither X-Frame-Options nor CSP frame-ancestors is set', 'Add X-Frame-Options: DENY or CSP frame-ancestors directive');
|
|
285
|
+
}
|
|
286
|
+
} catch {}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function scanCookies(origin, spinner) {
|
|
290
|
+
spinner.text = 'Analyzing cookie security...';
|
|
291
|
+
try {
|
|
292
|
+
const resp = await fetchText(origin);
|
|
293
|
+
const setCookie = resp.headers['set-cookie'];
|
|
294
|
+
if (setCookie) {
|
|
295
|
+
if (!setCookie.toLowerCase().includes('httponly')) {
|
|
296
|
+
addFinding('MEDIUM', 'Cookie Security', 'Session cookie missing HttpOnly flag', 'Cookie accessible via JavaScript (XSS can steal it)', 'Add HttpOnly flag to session cookies');
|
|
297
|
+
}
|
|
298
|
+
if (!setCookie.toLowerCase().includes('secure')) {
|
|
299
|
+
addFinding('MEDIUM', 'Cookie Security', 'Cookie missing Secure flag', 'Cookie sent over HTTP (can be intercepted)', 'Add Secure flag to all sensitive cookies');
|
|
300
|
+
}
|
|
301
|
+
if (!setCookie.toLowerCase().includes('samesite')) {
|
|
302
|
+
addFinding('LOW', 'Cookie Security', 'Cookie missing SameSite attribute', 'Cookie vulnerable to CSRF attacks', 'Add SameSite=Strict or SameSite=Lax to cookies');
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
} catch {}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function scanMethods(origin, spinner) {
|
|
309
|
+
spinner.text = 'Testing HTTP methods...';
|
|
310
|
+
const dangerousMethods = ['PUT', 'DELETE', 'TRACE', 'CONNECT', 'PATCH'];
|
|
311
|
+
for (const method of dangerousMethods) {
|
|
312
|
+
try {
|
|
313
|
+
const resp = await fetchText(origin, { method }, 5000);
|
|
314
|
+
if (resp.status !== 405 && resp.status !== 501 && resp.status < 400) {
|
|
315
|
+
if (method === 'TRACE' && resp.body.includes('TRACE')) {
|
|
316
|
+
addFinding('MEDIUM', 'HTTP Methods', `TRACE method enabled`, 'TRACE method can be used for Cross-Site Tracing (XST) attacks', 'Disable TRACE method on your web server');
|
|
317
|
+
} else if (method === 'PUT' || method === 'DELETE') {
|
|
318
|
+
addFinding('LOW', 'HTTP Methods', `${method} method allowed`, `${method} returns status ${resp.status}`, `Restrict ${method} method to authenticated endpoints only`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
} catch {}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function scanSubdomains(hostname, spinner) {
|
|
326
|
+
spinner.text = 'Enumerating subdomains...';
|
|
327
|
+
const baseDomain = hostname.replace(/^www\./, '');
|
|
328
|
+
let found = 0;
|
|
329
|
+
|
|
330
|
+
for (const sub of COMMON_SUBDOMAINS.slice(0, 40)) {
|
|
331
|
+
const subdomain = `${sub}.${baseDomain}`;
|
|
332
|
+
try {
|
|
333
|
+
const result = await checkUrl(`https://${subdomain}`, 3000);
|
|
334
|
+
if (result.accessible) {
|
|
335
|
+
found++;
|
|
336
|
+
const dangerous = ['admin', 'debug', 'internal', 'jenkins', 'phpmyadmin', 'grafana', 'kibana', 'sentry'].includes(sub);
|
|
337
|
+
if (dangerous) {
|
|
338
|
+
addFinding('MEDIUM', 'Subdomains', `Sensitive subdomain found: ${subdomain}`, `${subdomain} is accessible (status: ${result.status})`, 'Restrict access to internal subdomains. Use authentication and IP whitelisting.');
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
} catch {}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (found > 0) {
|
|
345
|
+
addFinding('INFO', 'Subdomains', `${found} subdomains discovered`, `Enumerated ${found} accessible subdomains for ${baseDomain}`, 'Review all subdomains for sensitive exposure');
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function scanDns(hostname, spinner) {
|
|
350
|
+
spinner.text = 'Checking DNS and email security...';
|
|
351
|
+
addFinding('INFO', 'DNS & Email', 'DNS scan completed', `Checked ${hostname}`, 'Ensure SPF, DKIM, and DMARC records are properly configured');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function scanTech(origin, spinner) {
|
|
355
|
+
spinner.text = 'Fingerprinting technology stack...';
|
|
356
|
+
try {
|
|
357
|
+
const resp = await fetchText(origin);
|
|
358
|
+
const techs = [];
|
|
359
|
+
if (resp.headers['x-powered-by']) techs.push(resp.headers['x-powered-by']);
|
|
360
|
+
if (resp.body.includes('__next')) techs.push('Next.js');
|
|
361
|
+
if (resp.body.includes('__nuxt')) techs.push('Nuxt.js');
|
|
362
|
+
if (resp.body.includes('react')) techs.push('React');
|
|
363
|
+
if (resp.body.includes('vue')) techs.push('Vue.js');
|
|
364
|
+
if (resp.body.includes('angular')) techs.push('Angular');
|
|
365
|
+
if (resp.body.includes('wordpress') || resp.body.includes('wp-content')) techs.push('WordPress');
|
|
366
|
+
if (resp.body.includes('laravel')) techs.push('Laravel');
|
|
367
|
+
if (resp.body.includes('django')) techs.push('Django');
|
|
368
|
+
if (resp.headers['server']?.includes('nginx')) techs.push('Nginx');
|
|
369
|
+
if (resp.headers['server']?.includes('apache')) techs.push('Apache');
|
|
370
|
+
if (resp.headers['server']?.includes('cloudflare')) techs.push('Cloudflare');
|
|
371
|
+
|
|
372
|
+
if (techs.length > 0) {
|
|
373
|
+
addFinding('INFO', 'Stack Detection', `Technologies detected: ${techs.join(', ')}`, `Found ${techs.length} technologies`, 'Keep all technologies updated to latest stable versions');
|
|
374
|
+
}
|
|
375
|
+
} catch {}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function scanApi(origin, spinner) {
|
|
379
|
+
spinner.text = 'Discovering API endpoints...';
|
|
380
|
+
const apiPaths = ['/api', '/api/v1', '/api/v2', '/rest', '/api/users', '/api/admin', '/api/health', '/api/status', '/api/config', '/api/debug'];
|
|
381
|
+
|
|
382
|
+
for (const path of apiPaths) {
|
|
383
|
+
try {
|
|
384
|
+
const resp = await fetchText(`${origin}${path}`, {}, 5000);
|
|
385
|
+
if (resp.status === 200 || resp.status === 401 || resp.status === 403) {
|
|
386
|
+
if (/admin|config|debug/i.test(path) && resp.status === 200) {
|
|
387
|
+
addFinding('HIGH', 'API Discovery', `Sensitive API endpoint accessible: ${path}`, `${origin}${path} returns ${resp.status}`, 'Protect sensitive API endpoints with authentication and authorization');
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
} catch {}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async function scanSsl(origin, spinner) {
|
|
395
|
+
spinner.text = 'Analyzing SSL/TLS...';
|
|
396
|
+
if (origin.startsWith('http://')) {
|
|
397
|
+
addFinding('HIGH', 'SSL/TLS', 'Site does not use HTTPS', `${origin} is served over HTTP`, 'Enable HTTPS with a valid certificate. Redirect all HTTP to HTTPS.');
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async function scanPathTraversal(origin, spinner) {
|
|
402
|
+
spinner.text = 'Testing path traversal (HackTricks)...';
|
|
403
|
+
const payloads = ['....//....//....//etc/passwd', '..%2f..%2f..%2fetc/passwd', '..%252f..%252f..%252fetc/passwd', '....\\\\....\\\\....\\\\windows/win.ini'];
|
|
404
|
+
const params = ['file', 'path', 'page', 'template', 'include', 'doc', 'folder', 'style', 'lang'];
|
|
405
|
+
|
|
406
|
+
for (const param of params) {
|
|
407
|
+
for (const payload of payloads) {
|
|
408
|
+
try {
|
|
409
|
+
const resp = await fetchText(`${origin}/?${param}=${encodeURIComponent(payload)}`, {}, 5000);
|
|
410
|
+
if (/root:x:0|; for 16-bit app support/i.test(resp.body)) {
|
|
411
|
+
addFinding('CRITICAL', 'Path Traversal (HackTricks)', `LFI via ?${param}=`, `File contents leaked with payload: ${payload.substring(0, 30)}`, 'Validate file paths. Use chroot or whitelist allowed files.');
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
} catch {}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
async function scanNosqli(origin, spinner) {
|
|
420
|
+
spinner.text = 'Testing NoSQL injection (HackTricks)...';
|
|
421
|
+
const payloads = [
|
|
422
|
+
{ param: 'username[$ne]', value: 'admin' },
|
|
423
|
+
{ param: 'password[$gt]', value: '' },
|
|
424
|
+
{ param: 'id[$regex]', value: '.*' },
|
|
425
|
+
];
|
|
426
|
+
|
|
427
|
+
try {
|
|
428
|
+
const resp = await fetchText(`${origin}/api/login`, {
|
|
429
|
+
method: 'POST',
|
|
430
|
+
headers: { 'Content-Type': 'application/json' },
|
|
431
|
+
body: JSON.stringify({ username: { $ne: '' }, password: { $ne: '' } }),
|
|
432
|
+
}, 5000);
|
|
433
|
+
|
|
434
|
+
if (resp.status === 200 && /token|session|user|welcome/i.test(resp.body)) {
|
|
435
|
+
addFinding('CRITICAL', 'NoSQL Injection (HackTricks)', 'NoSQL injection auth bypass', 'Login bypassed with MongoDB operator injection', 'Sanitize input. Never pass raw user objects to database queries. Use mongoose schema validation.');
|
|
436
|
+
}
|
|
437
|
+
} catch {}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async function scanWebsocket(origin, spinner) {
|
|
441
|
+
spinner.text = 'Checking WebSocket security (HackTricks)...';
|
|
442
|
+
const wsOrigin = origin.replace('http', 'ws');
|
|
443
|
+
addFinding('INFO', 'WebSocket (HackTricks)', 'WebSocket endpoint check', `Checked ${wsOrigin}`, 'Ensure WebSocket connections validate Origin header and require authentication tokens');
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async function scanCachePoisoning(origin, spinner) {
|
|
447
|
+
spinner.text = 'Testing cache poisoning (HackTricks)...';
|
|
448
|
+
const headers = ['X-Forwarded-Host', 'X-Forwarded-Scheme', 'X-Original-URL', 'X-Rewrite-URL'];
|
|
449
|
+
|
|
450
|
+
for (const header of headers) {
|
|
451
|
+
try {
|
|
452
|
+
const resp = await fetchText(origin, { headers: { [header]: 'evil.com' } });
|
|
453
|
+
if (resp.body.includes('evil.com')) {
|
|
454
|
+
addFinding('HIGH', 'Cache Poisoning (HackTricks)', `Unkeyed header reflected: ${header}`, `${header}: evil.com appears in response body`, 'Do not use unkeyed headers to generate content. Configure CDN to include these headers in cache key or strip them.');
|
|
455
|
+
}
|
|
456
|
+
} catch {}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async function scanRaceCondition(origin, spinner) {
|
|
461
|
+
spinner.text = 'Checking race condition indicators (HackTricks)...';
|
|
462
|
+
addFinding('INFO', 'Race Conditions (HackTricks)', 'Race condition detection note', 'Race conditions require active testing with concurrent requests (use Turbo Intruder / HTTP/2 single-packet)', 'Implement proper locking, database transactions with serializable isolation, and idempotency keys for sensitive operations.');
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function isSpaFallback(body, path) {
|
|
466
|
+
if (body.includes('<!doctype html') || body.includes('<!DOCTYPE html')) {
|
|
467
|
+
if (body.includes('__next') || body.includes('__nuxt') || body.includes('root') || body.includes('app')) {
|
|
468
|
+
if (!['.env', '.git', 'phpinfo', 'actuator', 'swagger'].some((p) => path.includes(p))) {
|
|
469
|
+
return true;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const findings = [];
|
|
2
|
+
|
|
3
|
+
export function addFinding(severity, module, title, details, fix) {
|
|
4
|
+
findings.push({
|
|
5
|
+
severity: severity.toUpperCase(),
|
|
6
|
+
module,
|
|
7
|
+
title,
|
|
8
|
+
details,
|
|
9
|
+
fix,
|
|
10
|
+
timestamp: new Date().toISOString(),
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getFindings() {
|
|
15
|
+
return [...findings];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function clearFindings() {
|
|
19
|
+
findings.length = 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getFindingsBySeverity(severity) {
|
|
23
|
+
return findings.filter((f) => f.severity === severity.toUpperCase());
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getFindingsByModule(module) {
|
|
27
|
+
return findings.filter((f) => f.module === module);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getSeverityCounts() {
|
|
31
|
+
return {
|
|
32
|
+
critical: findings.filter((f) => f.severity === 'CRITICAL').length,
|
|
33
|
+
high: findings.filter((f) => f.severity === 'HIGH').length,
|
|
34
|
+
medium: findings.filter((f) => f.severity === 'MEDIUM').length,
|
|
35
|
+
low: findings.filter((f) => f.severity === 'LOW').length,
|
|
36
|
+
info: findings.filter((f) => f.severity === 'INFO').length,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { getFindings, getSeverityCounts } from '../findings.js';
|
|
3
|
+
import { calculateScore, getGrade } from '../score.js';
|
|
4
|
+
|
|
5
|
+
const SEVERITY_COLORS = {
|
|
6
|
+
CRITICAL: chalk.bgRed.white.bold,
|
|
7
|
+
HIGH: chalk.red.bold,
|
|
8
|
+
MEDIUM: chalk.yellow.bold,
|
|
9
|
+
LOW: chalk.blue,
|
|
10
|
+
INFO: chalk.gray,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function printBanner() {
|
|
14
|
+
console.log(chalk.red(`
|
|
15
|
+
██████╗ ███████╗██████╗ ██████╗ ██╗ ██╗███╗ ██╗
|
|
16
|
+
██╔══██╗██╔════╝██╔══██╗██╔════╝ ██║ ██║████╗ ██║
|
|
17
|
+
██████╔╝█████╗ ██║ ██║██║ ███╗██║ ██║██╔██╗ ██║
|
|
18
|
+
██╔══██╗██╔══╝ ██║ ██║██║ ██║██║ ██║██║╚██╗██║
|
|
19
|
+
██║ ██║███████╗██████╔╝╚██████╔╝╚██████╔╝██║ ╚████║
|
|
20
|
+
╚═╝ ╚═╝╚══════╝╚═════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝
|
|
21
|
+
`));
|
|
22
|
+
console.log(chalk.gray(' Black-box & white-box security auditor | HackTricks Enhanced\n'));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function printResults() {
|
|
26
|
+
const findings = getFindings();
|
|
27
|
+
const score = calculateScore();
|
|
28
|
+
const grade = getGrade(score);
|
|
29
|
+
const counts = getSeverityCounts();
|
|
30
|
+
|
|
31
|
+
console.log('\n' + chalk.bold('═══════════════════════════════════════════════════════'));
|
|
32
|
+
console.log(chalk.bold(' SCAN RESULTS'));
|
|
33
|
+
console.log(chalk.bold('═══════════════════════════════════════════════════════\n'));
|
|
34
|
+
|
|
35
|
+
const gradeColor = score >= 80 ? chalk.green : score >= 60 ? chalk.yellow : chalk.red;
|
|
36
|
+
console.log(` Score: ${gradeColor.bold(score + '/100')} (Grade: ${gradeColor.bold(grade)})\n`);
|
|
37
|
+
|
|
38
|
+
console.log(` ${SEVERITY_COLORS.CRITICAL(' CRITICAL ')} ${counts.critical}`);
|
|
39
|
+
console.log(` ${SEVERITY_COLORS.HIGH(' HIGH ')} ${counts.high}`);
|
|
40
|
+
console.log(` ${SEVERITY_COLORS.MEDIUM(' MEDIUM ')} ${counts.medium}`);
|
|
41
|
+
console.log(` ${SEVERITY_COLORS.LOW(' LOW ')} ${counts.low}`);
|
|
42
|
+
console.log(` ${SEVERITY_COLORS.INFO(' INFO ')} ${counts.info}`);
|
|
43
|
+
console.log('');
|
|
44
|
+
|
|
45
|
+
const grouped = {};
|
|
46
|
+
for (const finding of findings) {
|
|
47
|
+
if (!grouped[finding.module]) grouped[finding.module] = [];
|
|
48
|
+
grouped[finding.module].push(finding);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const [module, moduleFindings] of Object.entries(grouped)) {
|
|
52
|
+
console.log(chalk.bold(`\n ┌─ ${module}`));
|
|
53
|
+
for (const f of moduleFindings) {
|
|
54
|
+
const color = SEVERITY_COLORS[f.severity] || chalk.white;
|
|
55
|
+
console.log(` │ ${color(`[${f.severity}]`)} ${f.title}`);
|
|
56
|
+
if (f.details) console.log(` │ ${chalk.gray(f.details.substring(0, 120))}`);
|
|
57
|
+
if (f.fix) console.log(` │ ${chalk.green('Fix:')} ${f.fix.substring(0, 120)}`);
|
|
58
|
+
}
|
|
59
|
+
console.log(' └─');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
console.log('\n' + chalk.bold('═══════════════════════════════════════════════════════\n'));
|
|
63
|
+
return { score, grade, findings: findings.length };
|
|
64
|
+
}
|