fuzzing.tsā¢27.2 kB
import { exec } from 'child_process';
import { promisify } from 'util';
import { ScanResult } from './recon.js';
import { ExtractedParameter } from './parameter-extraction.js';
const execAsync = promisify(exec);
export interface FuzzingResult {
parameter: string;
payload: string;
url: string;
method: string;
response_code: number;
response_size: number;
response_time: number;
vulnerability_detected: boolean;
vulnerability_type?: string;
evidence?: string;
severity: 'info' | 'low' | 'medium' | 'high' | 'critical';
}
export interface FuzzingConfiguration {
tool: 'ffuf' | 'wfuzz';
threads: number;
timeout: number;
delay: number;
wordlist: string;
extensions?: string[];
filter_codes?: number[];
filter_size?: number[];
match_codes?: number[];
custom_headers?: Record<string, string>;
}
export class FuzzingEngine {
async fuzzParameters(parameters: ExtractedParameter[], config: Partial<FuzzingConfiguration> = {}): Promise<ScanResult> {
try {
const defaultConfig: FuzzingConfiguration = {
tool: 'ffuf',
threads: 10,
timeout: 10,
delay: 100,
wordlist: this.getDefaultWordlist('parameters'),
filter_codes: [404, 403],
...config
};
console.error(`š Fuzzing ${parameters.length} parameters with ${defaultConfig.tool}`);
const allResults: FuzzingResult[] = [];
// Group parameters by URL and method for efficient fuzzing
const paramGroups = this.groupParametersByEndpoint(parameters);
for (const [endpoint, params] of paramGroups.entries()) {
console.error(` Fuzzing endpoint: ${endpoint}`);
if (defaultConfig.tool === 'ffuf') {
const ffufResults = await this.runFFUF(endpoint, params, defaultConfig);
allResults.push(...ffufResults);
} else if (defaultConfig.tool === 'wfuzz') {
const wfuzzResults = await this.runWfuzz(endpoint, params, defaultConfig);
allResults.push(...wfuzzResults);
}
// Rate limiting between endpoints
await this.sleep(defaultConfig.delay);
}
// Analyze results for vulnerabilities
const analyzedResults = this.analyzeResults(allResults);
const vulnerabilities = analyzedResults.filter(r => r.vulnerability_detected);
return {
target: 'parameter_fuzzing',
timestamp: new Date().toISOString(),
tool: 'fuzzing_engine',
results: {
total_tests: allResults.length,
vulnerabilities_found: vulnerabilities.length,
critical_findings: vulnerabilities.filter(v => v.severity === 'critical').length,
high_findings: vulnerabilities.filter(v => v.severity === 'high').length,
fuzzing_results: allResults,
vulnerability_summary: this.summarizeVulnerabilities(vulnerabilities),
recommendations: this.generateRecommendations(vulnerabilities)
},
status: 'success'
};
} catch (error) {
return {
target: 'parameter_fuzzing',
timestamp: new Date().toISOString(),
tool: 'fuzzing_engine',
results: {},
status: 'error',
error: error instanceof Error ? error.message : String(error)
};
}
}
async fuzzDirectories(baseUrl: string, config: Partial<FuzzingConfiguration> = {}): Promise<ScanResult> {
try {
const defaultConfig: FuzzingConfiguration = {
tool: 'ffuf',
threads: 20,
timeout: 10,
delay: 50,
wordlist: this.getDefaultWordlist('directories'),
extensions: ['php', 'asp', 'aspx', 'jsp', 'html', 'txt', 'xml', 'json'],
filter_codes: [404],
...config
};
console.error(`š Directory fuzzing on ${baseUrl}`);
let results: FuzzingResult[] = [];
if (defaultConfig.tool === 'ffuf') {
results = await this.runFFUFDirectories(baseUrl, defaultConfig);
} else if (defaultConfig.tool === 'wfuzz') {
results = await this.runWfuzzDirectories(baseUrl, defaultConfig);
}
const analyzedResults = this.analyzeDirectoryResults(results);
const interestingFindings = analyzedResults.filter(r => r.vulnerability_detected || r.response_code === 200);
return {
target: baseUrl,
timestamp: new Date().toISOString(),
tool: 'directory_fuzzing',
results: {
total_requests: results.length,
found_directories: interestingFindings.length,
accessible_paths: interestingFindings.filter(r => r.response_code === 200).length,
sensitive_files: interestingFindings.filter(r => this.isSensitiveFile(r.url)).length,
fuzzing_results: results,
interesting_findings: interestingFindings
},
status: 'success'
};
} catch (error) {
return {
target: baseUrl,
timestamp: new Date().toISOString(),
tool: 'directory_fuzzing',
results: {},
status: 'error',
error: error instanceof Error ? error.message : String(error)
};
}
}
async customPayloadFuzzing(target: string, parameter: string, payloads: string[], method: string = 'GET'): Promise<ScanResult> {
try {
console.error(`š Custom payload fuzzing: ${parameter} with ${payloads.length} payloads`);
const results: FuzzingResult[] = [];
for (const payload of payloads) {
try {
const result = await this.testSinglePayload(target, parameter, payload, method);
results.push(result);
// Rate limiting
await this.sleep(100);
} catch (error) {
console.error(`Error testing payload ${payload}:`, error);
}
}
const vulnerabilities = results.filter(r => r.vulnerability_detected);
return {
target,
timestamp: new Date().toISOString(),
tool: 'custom_payload_fuzzing',
results: {
parameter_tested: parameter,
total_payloads: payloads.length,
vulnerabilities_found: vulnerabilities.length,
test_results: results,
vulnerability_types: [...new Set(vulnerabilities.map(v => v.vulnerability_type).filter(Boolean))],
severity_breakdown: this.groupBySeverity(vulnerabilities)
},
status: 'success'
};
} catch (error) {
return {
target,
timestamp: new Date().toISOString(),
tool: 'custom_payload_fuzzing',
results: {},
status: 'error',
error: error instanceof Error ? error.message : String(error)
};
}
}
private async runFFUF(endpoint: string, parameters: ExtractedParameter[], config: FuzzingConfiguration): Promise<FuzzingResult[]> {
try {
// Check if ffuf is installed
try {
await execAsync('ffuf -h', { timeout: 5000 });
} catch {
console.warn('ffuf not found, skipping ffuf fuzzing');
return [];
}
const results: FuzzingResult[] = [];
for (const param of parameters.slice(0, 5)) { // Limit to 5 params per endpoint
const payloads = this.getPayloadsForParameter(param);
for (const payload of payloads.slice(0, 20)) { // Limit payloads
try {
const testUrl = this.buildTestUrl(endpoint, param, payload);
const command = `ffuf -u "${testUrl}" -w ${config.wordlist} -t ${config.threads} -timeout ${config.timeout} -p ${config.delay / 1000} -o /dev/null -s`;
const { stdout } = await execAsync(command, {
timeout: 30000,
maxBuffer: 1024 * 1024
});
// Parse ffuf output (simplified)
const lines = stdout.split('\n').filter(line => line.trim());
for (const line of lines) {
if (line.includes('Status:') && line.includes('Size:')) {
const statusMatch = line.match(/Status:\s*(\d+)/);
const sizeMatch = line.match(/Size:\s*(\d+)/);
const timeMatch = line.match(/Time:\s*(\d+)/);
if (statusMatch && sizeMatch) {
const result: FuzzingResult = {
parameter: param.name,
payload,
url: testUrl,
method: param.method,
response_code: parseInt(statusMatch[1]),
response_size: parseInt(sizeMatch[1]),
response_time: timeMatch ? parseInt(timeMatch[1]) : 0,
vulnerability_detected: false,
severity: 'info'
};
// Analyze for vulnerabilities
this.analyzeResponseForVulnerabilities(result, param);
results.push(result);
}
}
}
} catch (error) {
console.error(`FFuf error for ${param.name}:`, error);
}
}
}
return results;
} catch (error) {
console.error('FFuf execution error:', error);
return [];
}
}
private async runWfuzz(endpoint: string, parameters: ExtractedParameter[], config: FuzzingConfiguration): Promise<FuzzingResult[]> {
try {
// Check if wfuzz is installed
try {
await execAsync('wfuzz --help', { timeout: 5000 });
} catch {
console.warn('wfuzz not found, skipping wfuzz fuzzing');
return [];
}
const results: FuzzingResult[] = [];
for (const param of parameters.slice(0, 5)) { // Limit parameters
const payloads = this.getPayloadsForParameter(param);
// Create temporary payload file
const fs = require('fs').promises;
const tempFile = `/tmp/wfuzz_payloads_${Date.now()}.txt`;
await fs.writeFile(tempFile, payloads.slice(0, 20).join('\n'));
try {
const testUrl = this.buildTestUrl(endpoint, param, 'FUZZ');
const command = `wfuzz -c -z file,${tempFile} --hc 404,403 -t ${config.threads} "${testUrl}"`;
const { stdout } = await execAsync(command, {
timeout: 60000,
maxBuffer: 1024 * 1024
});
// Parse wfuzz output
const lines = stdout.split('\n').filter(line => line.includes('C='));
for (const line of lines) {
const match = line.match(/(\d+)\s+C=(\d+)\s+L=(\d+)\s+W=(\d+)\s+Ch=(\d+)\s+"([^"]+)"/);
if (match) {
const result: FuzzingResult = {
parameter: param.name,
payload: match[6],
url: testUrl.replace('FUZZ', match[6]),
method: param.method,
response_code: parseInt(match[2]),
response_size: parseInt(match[5]),
response_time: 0,
vulnerability_detected: false,
severity: 'info'
};
this.analyzeResponseForVulnerabilities(result, param);
results.push(result);
}
}
} finally {
// Clean up temp file
try {
await fs.unlink(tempFile);
} catch {
// Ignore cleanup errors
}
}
}
return results;
} catch (error) {
console.error('Wfuzz execution error:', error);
return [];
}
}
private async runFFUFDirectories(baseUrl: string, config: FuzzingConfiguration): Promise<FuzzingResult[]> {
try {
const results: FuzzingResult[] = [];
// Directory fuzzing
const dirCommand = `ffuf -u ${baseUrl}/FUZZ -w ${config.wordlist} -t ${config.threads} -timeout ${config.timeout} -fc ${config.filter_codes?.join(',')} -o /dev/null -s`;
const { stdout: dirOutput } = await execAsync(dirCommand, {
timeout: 120000,
maxBuffer: 1024 * 1024 * 5
});
// Parse directory results
this.parseFFUFOutput(dirOutput, baseUrl, results);
// File extension fuzzing
if (config.extensions) {
for (const ext of config.extensions) {
const extCommand = `ffuf -u ${baseUrl}/FUZZ.${ext} -w ${config.wordlist} -t ${config.threads} -timeout ${config.timeout} -fc ${config.filter_codes?.join(',')} -o /dev/null -s`;
try {
const { stdout: extOutput } = await execAsync(extCommand, {
timeout: 60000,
maxBuffer: 1024 * 1024
});
this.parseFFUFOutput(extOutput, baseUrl, results, ext);
} catch {
// Continue with other extensions
}
}
}
return results;
} catch (error) {
console.error('FFuf directory fuzzing error:', error);
return [];
}
}
private async runWfuzzDirectories(baseUrl: string, config: FuzzingConfiguration): Promise<FuzzingResult[]> {
try {
const results: FuzzingResult[] = [];
const command = `wfuzz -c -z file,${config.wordlist} --hc ${config.filter_codes?.join(',')} -t ${config.threads} "${baseUrl}/FUZZ"`;
const { stdout } = await execAsync(command, {
timeout: 120000,
maxBuffer: 1024 * 1024 * 5
});
// Parse wfuzz output
const lines = stdout.split('\n').filter(line => line.includes('C='));
for (const line of lines) {
const match = line.match(/(\d+)\s+C=(\d+)\s+L=(\d+)\s+W=(\d+)\s+Ch=(\d+)\s+"([^"]+)"/);
if (match) {
results.push({
parameter: 'directory',
payload: match[6],
url: `${baseUrl}/${match[6]}`,
method: 'GET',
response_code: parseInt(match[2]),
response_size: parseInt(match[5]),
response_time: 0,
vulnerability_detected: this.isInterestingDirectory(match[6]),
severity: this.calculateDirectorySeverity(match[6], parseInt(match[2]))
});
}
}
return results;
} catch (error) {
console.error('Wfuzz directory fuzzing error:', error);
return [];
}
}
private async testSinglePayload(target: string, parameter: string, payload: string, method: string): Promise<FuzzingResult> {
const axios = require('axios');
try {
const startTime = Date.now();
let response;
if (method.toUpperCase() === 'GET') {
const url = new URL(target);
url.searchParams.set(parameter, payload);
response = await axios.get(url.toString(), { timeout: 10000 });
} else {
const data = { [parameter]: payload };
response = await axios.post(target, data, { timeout: 10000 });
}
const responseTime = Date.now() - startTime;
const result: FuzzingResult = {
parameter,
payload,
url: target,
method,
response_code: response.status,
response_size: response.data?.length || 0,
response_time: responseTime,
vulnerability_detected: false,
severity: 'info'
};
// Analyze response for vulnerabilities
this.analyzeResponseContent(result, response.data);
return result;
} catch (error: any) {
return {
parameter,
payload,
url: target,
method,
response_code: error.response?.status || 0,
response_size: 0,
response_time: 0,
vulnerability_detected: false,
severity: 'info'
};
}
}
private groupParametersByEndpoint(parameters: ExtractedParameter[]): Map<string, ExtractedParameter[]> {
const groups = new Map<string, ExtractedParameter[]>();
for (const param of parameters) {
const endpoint = `${param.method}:${param.url}`;
if (!groups.has(endpoint)) {
groups.set(endpoint, []);
}
groups.get(endpoint)!.push(param);
}
return groups;
}
private getPayloadsForParameter(param: ExtractedParameter): string[] {
const basePayloads = [
// XSS payloads
'<script>alert(1)</script>',
'<img src=x onerror=alert(1)>',
'javascript:alert(1)',
// SQL injection payloads
"' OR '1'='1",
"'; DROP TABLE users; --",
"' UNION SELECT null, version() --",
// Command injection payloads
'; cat /etc/passwd',
'| whoami',
'`id`',
// Path traversal payloads
'../../../etc/passwd',
'..\\..\\..\\windows\\system32\\drivers\\etc\\hosts',
// LDAP injection payloads
'*)(uid=*',
'*)(|(password=*))',
// XXE payloads
'<?xml version="1.0"?><!DOCTYPE root [<!ENTITY test SYSTEM "file:///etc/passwd">]><root>&test;</root>',
// SSTI payloads
'{{7*7}}',
'${7*7}',
'#{7*7}',
// NoSQL injection payloads
'{"$ne": null}',
'{"$regex": ".*"}',
// SSRF payloads
'http://127.0.0.1:80',
'http://localhost:22',
'file:///etc/passwd'
];
// Filter payloads based on parameter vulnerability types
const relevantPayloads = basePayloads.filter(payload => {
for (const vulnType of param.potential_vuln_types) {
if (vulnType.includes('XSS') && payload.includes('<script>')) return true;
if (vulnType.includes('SQL') && payload.includes("'")) return true;
if (vulnType.includes('Command') && payload.includes(';')) return true;
if (vulnType.includes('Path') && payload.includes('../')) return true;
if (vulnType.includes('SSRF') && payload.includes('http://')) return true;
}
return false;
});
return relevantPayloads.length > 0 ? relevantPayloads : basePayloads.slice(0, 10);
}
private buildTestUrl(endpoint: string, param: ExtractedParameter, payload: string): string {
try {
const url = new URL(endpoint);
if (param.method === 'GET') {
url.searchParams.set(param.name, payload);
}
return url.toString();
} catch {
return `${endpoint}?${param.name}=${encodeURIComponent(payload)}`;
}
}
private analyzeResponseForVulnerabilities(result: FuzzingResult, param: ExtractedParameter): void {
// Analyze based on response code
if (result.response_code === 500) {
result.vulnerability_detected = true;
result.vulnerability_type = 'Application Error';
result.severity = 'medium';
result.evidence = `HTTP 500 error triggered by payload: ${result.payload}`;
}
// Analyze based on response size anomalies
if (result.response_size > 10000) {
result.vulnerability_detected = true;
result.vulnerability_type = 'Information Disclosure';
result.severity = 'low';
result.evidence = `Unusually large response size: ${result.response_size} bytes`;
}
// Analyze based on response time
if (result.response_time > 5000) {
result.vulnerability_detected = true;
result.vulnerability_type = 'Potential Blind Injection';
result.severity = 'medium';
result.evidence = `Response time anomaly: ${result.response_time}ms`;
}
// Payload-specific analysis
if (result.payload.includes('<script>') && result.response_code === 200) {
result.vulnerability_detected = true;
result.vulnerability_type = 'Potential XSS';
result.severity = 'high';
result.evidence = `XSS payload reflected in response`;
}
if (result.payload.includes("'") && result.response_code === 500) {
result.vulnerability_detected = true;
result.vulnerability_type = 'Potential SQL Injection';
result.severity = 'high';
result.evidence = `SQL error triggered by injection payload`;
}
}
private analyzeResponseContent(result: FuzzingResult, responseContent: string): void {
if (!responseContent) return;
const content = responseContent.toLowerCase();
// Check for reflected payload
if (content.includes(result.payload.toLowerCase())) {
result.vulnerability_detected = true;
result.vulnerability_type = 'Reflected Input';
result.severity = 'medium';
result.evidence = 'Payload reflected in response content';
}
// Check for error messages
const errorPatterns = [
'sql syntax error',
'mysql error',
'postgresql error',
'oracle error',
'syntax error near',
'unexpected token',
'stack trace',
'exception',
'error message'
];
for (const pattern of errorPatterns) {
if (content.includes(pattern)) {
result.vulnerability_detected = true;
result.vulnerability_type = 'Application Error';
result.severity = 'medium';
result.evidence = `Error pattern detected: ${pattern}`;
break;
}
}
// Check for sensitive file content
if (content.includes('root:') || content.includes('[users]') || content.includes('password')) {
result.vulnerability_detected = true;
result.vulnerability_type = 'Information Disclosure';
result.severity = 'high';
result.evidence = 'Sensitive file content detected';
}
}
private analyzeResults(results: FuzzingResult[]): FuzzingResult[] {
// Additional analysis and correlation
return results;
}
private analyzeDirectoryResults(results: FuzzingResult[]): FuzzingResult[] {
return results.map(result => {
if (this.isSensitiveFile(result.url)) {
result.vulnerability_detected = true;
result.vulnerability_type = 'Sensitive File Exposure';
result.severity = this.calculateFileSeverity(result.url);
result.evidence = `Sensitive file accessible: ${result.url}`;
}
return result;
});
}
private isSensitiveFile(url: string): boolean {
const sensitivePatterns = [
'.env', '.git', '.svn', 'config.php', 'database.sql',
'backup.', '.bak', '.old', '.tmp', 'admin.php',
'phpinfo.php', 'test.php', '.htaccess', 'web.config'
];
return sensitivePatterns.some(pattern => url.toLowerCase().includes(pattern));
}
private isInterestingDirectory(directory: string): boolean {
const interestingDirs = [
'admin', 'administrator', 'backup', 'config', 'db',
'database', 'test', 'dev', 'staging', 'api', 'v1', 'v2'
];
return interestingDirs.some(dir => directory.toLowerCase().includes(dir));
}
private calculateDirectorySeverity(directory: string, responseCode: number): 'info' | 'low' | 'medium' | 'high' | 'critical' {
if (responseCode !== 200) return 'info';
const highRisk = ['admin', 'administrator', 'backup', 'database', 'config'];
const mediumRisk = ['test', 'dev', 'staging', 'api'];
const dirLower = directory.toLowerCase();
if (highRisk.some(risk => dirLower.includes(risk))) return 'high';
if (mediumRisk.some(risk => dirLower.includes(risk))) return 'medium';
return 'low';
}
private calculateFileSeverity(url: string): 'info' | 'low' | 'medium' | 'high' | 'critical' {
const urlLower = url.toLowerCase();
if (urlLower.includes('.env') || urlLower.includes('database') || urlLower.includes('.git')) {
return 'critical';
}
if (urlLower.includes('config') || urlLower.includes('backup') || urlLower.includes('admin')) {
return 'high';
}
if (urlLower.includes('.bak') || urlLower.includes('.old') || urlLower.includes('test')) {
return 'medium';
}
return 'low';
}
private summarizeVulnerabilities(vulnerabilities: FuzzingResult[]): Record<string, number> {
const summary: Record<string, number> = {};
for (const vuln of vulnerabilities) {
const type = vuln.vulnerability_type || 'Unknown';
summary[type] = (summary[type] || 0) + 1;
}
return summary;
}
private generateRecommendations(vulnerabilities: FuzzingResult[]): string[] {
const recommendations: string[] = [];
const vulnTypes = new Set(vulnerabilities.map(v => v.vulnerability_type).filter(Boolean));
if (vulnTypes.has('Potential XSS')) {
recommendations.push('Implement proper input validation and output encoding to prevent XSS');
}
if (vulnTypes.has('Potential SQL Injection')) {
recommendations.push('Use parameterized queries and input validation to prevent SQL injection');
}
if (vulnTypes.has('Sensitive File Exposure')) {
recommendations.push('Remove or protect sensitive files from public access');
}
if (vulnTypes.has('Application Error')) {
recommendations.push('Implement proper error handling to prevent information disclosure');
}
if (recommendations.length === 0) {
recommendations.push('Continue regular security testing and monitoring');
}
return recommendations;
}
private groupBySeverity(results: FuzzingResult[]): Record<string, number> {
const groups: Record<string, number> = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
for (const result of results) {
groups[result.severity]++;
}
return groups;
}
private parseFFUFOutput(output: string, baseUrl: string, results: FuzzingResult[], extension?: string): void {
const lines = output.split('\n').filter(line => line.trim() && !line.startsWith('['));
for (const line of lines) {
// Simple parsing - in real implementation, use proper JSON output from ffuf
const parts = line.trim().split(/\s+/);
if (parts.length >= 3) {
const payload = parts[0];
const status = parseInt(parts[1]) || 200;
const size = parseInt(parts[2]) || 0;
const url = extension ? `${baseUrl}/${payload}.${extension}` : `${baseUrl}/${payload}`;
results.push({
parameter: 'directory',
payload,
url,
method: 'GET',
response_code: status,
response_size: size,
response_time: 0,
vulnerability_detected: this.isInterestingDirectory(payload) || this.isSensitiveFile(url),
severity: this.calculateDirectorySeverity(payload, status)
});
}
}
}
private getDefaultWordlist(type: 'parameters' | 'directories'): string {
// Return default wordlist paths
const wordlists = {
parameters: '/usr/share/wordlists/SecLists/Discovery/Web-Content/burp-parameter-names.txt',
directories: '/usr/share/wordlists/SecLists/Discovery/Web-Content/common.txt'
};
return wordlists[type] || wordlists.directories;
}
private async sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}