Skip to main content
Glama

MCP Pentest

directory-scanner.ts•30.4 kB
import { exec } from 'child_process'; import { promisify } from 'util'; import { ScanResult } from './recon.js'; const execAsync = promisify(exec); export interface DirectoryResult { url: string; status_code: number; content_length: number; content_type?: string; redirect_location?: string; response_time: number; is_directory: boolean; is_sensitive: boolean; risk_level: 'info' | 'low' | 'medium' | 'high' | 'critical'; description: string; recommendations: string[]; } export interface DirectoryScanConfiguration { tool: 'dirb' | 'dirsearch' | 'gobuster' | 'feroxbuster'; wordlist?: string; extensions?: string[]; threads: number; timeout: number; recursive: boolean; max_depth: number; status_codes: number[]; user_agent?: string; headers?: Record<string, string>; proxy?: string; delay?: number; } export class DirectoryScannerEngine { async scanDirectories(target: string, config: Partial<DirectoryScanConfiguration> = {}): Promise<ScanResult> { try { const defaultConfig: DirectoryScanConfiguration = { tool: 'dirsearch', wordlist: this.getDefaultWordlist(), extensions: ['php', 'asp', 'aspx', 'jsp', 'html', 'htm', 'txt', 'xml', 'json', 'js', 'css'], threads: 30, timeout: 10, recursive: true, max_depth: 3, status_codes: [200, 201, 204, 301, 302, 307, 308, 401, 403, 405, 500], user_agent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', delay: 0, ...config }; console.error(`šŸ” Directory scanning ${target} with ${defaultConfig.tool}`); let results: DirectoryResult[] = []; switch (defaultConfig.tool) { case 'dirb': results = await this.runDirb(target, defaultConfig); break; case 'dirsearch': results = await this.runDirsearch(target, defaultConfig); break; case 'gobuster': results = await this.runGobuster(target, defaultConfig); break; case 'feroxbuster': results = await this.runFeroxbuster(target, defaultConfig); break; default: throw new Error(`Unsupported tool: ${defaultConfig.tool}`); } // Analyze and categorize results const analyzedResults = this.analyzeResults(results); const sensitiveFiles = analyzedResults.filter(r => r.is_sensitive); const accessibleDirs = analyzedResults.filter(r => r.is_directory && r.status_code === 200); const highRiskFindings = analyzedResults.filter(r => r.risk_level === 'high' || r.risk_level === 'critical'); return { target, timestamp: new Date().toISOString(), tool: 'directory_scanner', results: { scan_tool: defaultConfig.tool, total_discovered: results.length, accessible_directories: accessibleDirs.length, sensitive_files: sensitiveFiles.length, high_risk_findings: highRiskFindings.length, status_code_breakdown: this.groupByStatusCode(results), risk_level_breakdown: this.groupByRiskLevel(analyzedResults), discovered_paths: analyzedResults, security_recommendations: this.generateSecurityRecommendations(analyzedResults) }, status: 'success' }; } catch (error) { return { target, timestamp: new Date().toISOString(), tool: 'directory_scanner', results: {}, status: 'error', error: error instanceof Error ? error.message : String(error) }; } } async advancedDirectoryScan(target: string): Promise<ScanResult> { try { console.error(`šŸ” Advanced multi-tool directory scanning for ${target}`); const allResults: DirectoryResult[] = []; // Phase 1: Quick discovery with common wordlist console.error(' Phase 1: Quick discovery scan'); const quickResults = await this.scanDirectories(target, { tool: 'dirsearch', extensions: ['php', 'html', 'asp', 'jsp'], threads: 50, recursive: false }); if (quickResults.status === 'success') { allResults.push(...(quickResults.results.discovered_paths || [])); } // Phase 2: Technology-specific scanning console.error(' Phase 2: Technology-specific scanning'); const techResults = await this.technologySpecificScan(target); allResults.push(...techResults); // Phase 3: Backup and sensitive file discovery console.error(' Phase 3: Sensitive file discovery'); const sensitiveResults = await this.sensitiveFileScan(target); allResults.push(...sensitiveResults); // Phase 4: API endpoint discovery console.error(' Phase 4: API endpoint discovery'); const apiResults = await this.apiEndpointScan(target); allResults.push(...apiResults); // Deduplicate and analyze final results const uniqueResults = this.deduplicateResults(allResults); const finalAnalyzed = this.analyzeResults(uniqueResults); return { target, timestamp: new Date().toISOString(), tool: 'advanced_directory_scanner', results: { total_phases: 4, total_discovered: finalAnalyzed.length, unique_paths: finalAnalyzed.length, critical_findings: finalAnalyzed.filter(r => r.risk_level === 'critical').length, high_risk_findings: finalAnalyzed.filter(r => r.risk_level === 'high').length, sensitive_files: finalAnalyzed.filter(r => r.is_sensitive).length, discovered_paths: finalAnalyzed, attack_surface_summary: this.generateAttackSurfaceSummary(finalAnalyzed), prioritized_recommendations: this.generatePrioritizedRecommendations(finalAnalyzed) }, status: 'success' }; } catch (error) { return { target, timestamp: new Date().toISOString(), tool: 'advanced_directory_scanner', results: {}, status: 'error', error: error instanceof Error ? error.message : String(error) }; } } private async runDirb(target: string, config: DirectoryScanConfiguration): Promise<DirectoryResult[]> { try { // Check if dirb is installed try { await execAsync('dirb -h', { timeout: 5000 }); } catch { console.warn('dirb not found, skipping dirb scan'); return []; } const extensions = config.extensions?.map(ext => `.${ext}`).join(',') || ''; const command = `dirb ${target} ${config.wordlist} -X ${extensions} -w -r`; console.error(`Executing: ${command}`); const { stdout } = await execAsync(command, { timeout: 300000, // 5 minutes maxBuffer: 1024 * 1024 * 10 // 10MB }); return this.parseDirbOutput(stdout, target); } catch (error) { console.error('Dirb execution error:', error); return []; } } private async runDirsearch(target: string, config: DirectoryScanConfiguration): Promise<DirectoryResult[]> { try { // Check if dirsearch is installed try { await execAsync('dirsearch -h', { timeout: 5000 }); } catch { console.warn('dirsearch not found, skipping dirsearch scan'); return []; } const extensions = config.extensions?.join(',') || 'php,asp,aspx,jsp,html'; const statusCodes = config.status_codes.join(','); let command = `dirsearch -u ${target} -e ${extensions} -t ${config.threads} --timeout ${config.timeout}`; command += ` --include-status ${statusCodes}`; if (config.recursive) { command += ` -r --max-recursion-depth ${config.max_depth}`; } if (config.wordlist) { command += ` -w ${config.wordlist}`; } if (config.user_agent) { command += ` --user-agent "${config.user_agent}"`; } command += ' --format simple --quiet-mode'; console.error(`Executing: ${command}`); const { stdout } = await execAsync(command, { timeout: 600000, // 10 minutes maxBuffer: 1024 * 1024 * 20 // 20MB }); return this.parseDirsearchOutput(stdout, target); } catch (error) { console.error('Dirsearch execution error:', error); return []; } } private async runGobuster(target: string, config: DirectoryScanConfiguration): Promise<DirectoryResult[]> { try { // Check if gobuster is installed try { await execAsync('gobuster version', { timeout: 5000 }); } catch { console.warn('gobuster not found, skipping gobuster scan'); return []; } const extensions = config.extensions?.join(',') || 'php,asp,aspx,jsp,html'; const statusCodes = config.status_codes.join(','); let command = `gobuster dir -u ${target} -w ${config.wordlist} -x ${extensions}`; command += ` -t ${config.threads} --timeout ${config.timeout}s -s ${statusCodes}`; command += ' --no-error --quiet'; if (config.user_agent) { command += ` -a "${config.user_agent}"`; } console.error(`Executing: ${command}`); const { stdout } = await execAsync(command, { timeout: 600000, // 10 minutes maxBuffer: 1024 * 1024 * 10 // 10MB }); return this.parseGobusterOutput(stdout, target); } catch (error) { console.error('Gobuster execution error:', error); return []; } } private async runFeroxbuster(target: string, config: DirectoryScanConfiguration): Promise<DirectoryResult[]> { try { // Check if feroxbuster is installed try { await execAsync('feroxbuster --version', { timeout: 5000 }); } catch { console.warn('feroxbuster not found, skipping feroxbuster scan'); return []; } const extensions = config.extensions?.join(',') || 'php,asp,aspx,jsp,html'; const statusCodes = config.status_codes.join(','); let command = `feroxbuster -u ${target} -w ${config.wordlist} -x ${extensions}`; command += ` -t ${config.threads} -T ${config.timeout} -s ${statusCodes}`; command += ' --silent --no-state'; if (config.recursive) { command += ` -d ${config.max_depth}`; } else { command += ' --no-recursion'; } if (config.user_agent) { command += ` -a "${config.user_agent}"`; } console.error(`Executing: ${command}`); const { stdout } = await execAsync(command, { timeout: 600000, // 10 minutes maxBuffer: 1024 * 1024 * 20 // 20MB }); return this.parseFeroxbusterOutput(stdout, target); } catch (error) { console.error('Feroxbuster execution error:', error); return []; } } private async technologySpecificScan(target: string): Promise<DirectoryResult[]> { const results: DirectoryResult[] = []; // Technology-specific wordlists and paths const techPaths = { wordpress: [ 'wp-admin', 'wp-content', 'wp-includes', 'wp-config.php', 'wp-content/uploads', 'wp-content/themes', 'wp-content/plugins', 'xmlrpc.php', 'readme.html', 'license.txt' ], drupal: [ 'sites/default', 'modules', 'themes', 'core', 'sites/default/settings.php', 'update.php', 'install.php' ], joomla: [ 'administrator', 'components', 'modules', 'plugins', 'configuration.php', 'htaccess.txt' ], php: [ 'phpinfo.php', 'info.php', 'test.php', 'config.php', 'database.php', 'db.php', 'connect.php' ], asp: [ 'web.config', 'global.asax', 'bin', 'App_Data', 'default.aspx', 'login.aspx', 'admin.aspx' ], java: [ 'WEB-INF', 'META-INF', 'classes', 'lib', 'web.xml', 'struts.xml', 'spring' ] }; // Test each technology's common paths for (const [tech, paths] of Object.entries(techPaths)) { for (const path of paths.slice(0, 10)) { // Limit paths per technology try { const testUrl = `${target.replace(/\/$/, '')}/${path}`; const result = await this.testSinglePath(testUrl); if (result && (result.status_code === 200 || result.status_code === 403)) { results.push({ ...result, description: `${tech.toUpperCase()} technology file/directory`, is_sensitive: this.isSensitivePath(path), risk_level: this.calculateRiskLevel(path, result.status_code) }); } // Rate limiting await this.sleep(50); } catch { // Continue with next path } } } return results; } private async sensitiveFileScan(target: string): Promise<DirectoryResult[]> { const results: DirectoryResult[] = []; const sensitivePaths = [ // Configuration files '.env', '.env.local', '.env.production', 'config.php', 'config.ini', 'configuration.php', 'settings.php', 'config.json', 'config.yml', // Database files 'database.sql', 'backup.sql', 'dump.sql', 'db.sqlite', 'database.db', 'app.db', // Backup files 'backup.zip', 'backup.tar.gz', 'backup.7z', 'site.zip', 'www.zip', 'web.zip', 'backup.rar', 'files.zip', // Version control '.git/config', '.git/HEAD', '.git/logs/HEAD', '.svn/entries', '.hg/hgrc', // Log files 'error.log', 'access.log', 'app.log', 'debug.log', 'application.log', // Development files 'test.php', 'debug.php', 'dev.php', 'phpinfo.php', 'info.php', // API keys and secrets 'api_keys.txt', 'secrets.txt', 'credentials.txt', 'keys.txt', 'passwords.txt', // Server files '.htaccess', '.htpasswd', 'web.config', 'robots.txt', 'sitemap.xml', // Admin panels 'admin.php', 'administrator.php', 'manage.php', 'control.php', 'panel.php' ]; for (const path of sensitivePaths) { try { const testUrl = `${target.replace(/\/$/, '')}/${path}`; const result = await this.testSinglePath(testUrl); if (result && result.status_code !== 404) { results.push({ ...result, description: `Sensitive file: ${path}`, is_sensitive: true, risk_level: this.calculateSensitiveFileRisk(path, result.status_code) }); } // Rate limiting await this.sleep(100); } catch { // Continue with next path } } return results; } private async apiEndpointScan(target: string): Promise<DirectoryResult[]> { const results: DirectoryResult[] = []; const apiPaths = [ // Common API paths 'api', 'api/v1', 'api/v2', 'api/v3', 'rest', 'rest/v1', 'rest/v2', 'graphql', 'graphiql', // Documentation 'swagger', 'swagger-ui', 'swagger.json', 'openapi.json', 'api-docs', 'docs', // Admin APIs 'admin/api', 'admin/rest', 'management/api', 'internal/api', // Mobile APIs 'mobile/api', 'app/api', 'm/api', 'mobile/rest', // Versioned endpoints 'v1', 'v2', 'v3', 'latest', 'beta', 'dev', 'test' ]; for (const path of apiPaths) { try { const testUrl = `${target.replace(/\/$/, '')}/${path}`; const result = await this.testSinglePath(testUrl); if (result && (result.status_code === 200 || result.status_code === 401 || result.status_code === 403)) { results.push({ ...result, description: `API endpoint: ${path}`, is_sensitive: path.includes('admin') || path.includes('internal'), risk_level: this.calculateAPIRisk(path, result.status_code) }); } // Rate limiting await this.sleep(75); } catch { // Continue with next path } } return results; } private async testSinglePath(url: string): Promise<DirectoryResult | null> { const axios = require('axios'); try { const startTime = Date.now(); const response = await axios.get(url, { timeout: 10000, maxRedirects: 5, validateStatus: () => true, // Accept all status codes headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' } }); const responseTime = Date.now() - startTime; return { url, status_code: response.status, content_length: response.data?.length || response.headers['content-length'] || 0, content_type: response.headers['content-type'], redirect_location: response.headers['location'], response_time: responseTime, is_directory: this.isDirectory(response), is_sensitive: false, risk_level: 'info', description: '', recommendations: [] }; } catch (error: any) { if (error.response) { return { url, status_code: error.response.status, content_length: 0, response_time: 0, is_directory: false, is_sensitive: false, risk_level: 'info', description: '', recommendations: [] }; } return null; } } private parseDirbOutput(output: string, target: string): DirectoryResult[] { const results: DirectoryResult[] = []; const lines = output.split('\n'); for (const line of lines) { if (line.includes('==> DIRECTORY:') || line.includes('+ ')) { const match = line.match(/\+ ([^\s]+) \(CODE:(\d+)\|SIZE:(\d+)\)/); if (match) { const url = match[1]; const statusCode = parseInt(match[2]); const size = parseInt(match[3]); results.push({ url, status_code: statusCode, content_length: size, response_time: 0, is_directory: line.includes('==> DIRECTORY:'), is_sensitive: false, risk_level: 'info', description: '', recommendations: [] }); } } } return results; } private parseDirsearchOutput(output: string, target: string): DirectoryResult[] { const results: DirectoryResult[] = []; const lines = output.split('\n'); for (const line of lines) { // Parse dirsearch output format const match = line.match(/(\d+)\s+(\d+)B?\s+([^\s]+)/); if (match) { const statusCode = parseInt(match[1]); const size = parseInt(match[2]); const path = match[3]; results.push({ url: `${target.replace(/\/$/, '')}${path}`, status_code: statusCode, content_length: size, response_time: 0, is_directory: path.endsWith('/'), is_sensitive: false, risk_level: 'info', description: '', recommendations: [] }); } } return results; } private parseGobusterOutput(output: string, target: string): DirectoryResult[] { const results: DirectoryResult[] = []; const lines = output.split('\n'); for (const line of lines) { // Parse gobuster output format const match = line.match(/([^\s]+)\s+\(Status:\s*(\d+)\)\s*\[Size:\s*(\d+)\]/); if (match) { const path = match[1]; const statusCode = parseInt(match[2]); const size = parseInt(match[3]); results.push({ url: `${target.replace(/\/$/, '')}${path}`, status_code: statusCode, content_length: size, response_time: 0, is_directory: path.endsWith('/'), is_sensitive: false, risk_level: 'info', description: '', recommendations: [] }); } } return results; } private parseFeroxbusterOutput(output: string, target: string): DirectoryResult[] { const results: DirectoryResult[] = []; const lines = output.split('\n'); for (const line of lines) { // Parse feroxbuster output format const match = line.match(/(\d+)\s+(\d+)c\s+([^\s]+)/); if (match) { const statusCode = parseInt(match[1]); const size = parseInt(match[2]); const url = match[3]; results.push({ url, status_code: statusCode, content_length: size, response_time: 0, is_directory: url.endsWith('/'), is_sensitive: false, risk_level: 'info', description: '', recommendations: [] }); } } return results; } private analyzeResults(results: DirectoryResult[]): DirectoryResult[] { return results.map(result => { // Analyze each result for security implications result.is_sensitive = this.isSensitivePath(result.url); result.risk_level = this.calculateRiskLevel(result.url, result.status_code); result.description = this.generateDescription(result); result.recommendations = this.generateRecommendations(result); return result; }); } private isSensitivePath(path: string): boolean { const sensitivePatterns = [ '.env', '.git', '.svn', 'config', 'backup', 'admin', 'database', 'log', 'debug', 'test', 'phpinfo', 'api', 'swagger', 'internal' ]; return sensitivePatterns.some(pattern => path.toLowerCase().includes(pattern) ); } private calculateRiskLevel(path: string, statusCode: number): 'info' | 'low' | 'medium' | 'high' | 'critical' { if (statusCode === 404) return 'info'; const pathLower = path.toLowerCase(); // Critical risks if (pathLower.includes('.env') || pathLower.includes('.git') || pathLower.includes('database') || pathLower.includes('backup')) { return statusCode === 200 ? 'critical' : 'high'; } // High risks if (pathLower.includes('admin') || pathLower.includes('config') || pathLower.includes('phpinfo') || pathLower.includes('debug')) { return statusCode === 200 ? 'high' : 'medium'; } // Medium risks if (pathLower.includes('api') || pathLower.includes('test') || pathLower.includes('dev') || pathLower.includes('swagger')) { return statusCode === 200 ? 'medium' : 'low'; } return statusCode === 200 ? 'low' : 'info'; } private calculateSensitiveFileRisk(path: string, statusCode: number): 'info' | 'low' | 'medium' | 'high' | 'critical' { if (statusCode === 404) return 'info'; const pathLower = path.toLowerCase(); if (pathLower.includes('.env') || pathLower.includes('.git/config') || pathLower.includes('database.sql') || pathLower.includes('backup')) { return 'critical'; } if (pathLower.includes('config') || pathLower.includes('phpinfo') || pathLower.includes('.htpasswd') || pathLower.includes('web.config')) { return 'high'; } if (pathLower.includes('log') || pathLower.includes('debug') || pathLower.includes('test') || pathLower.includes('robots.txt')) { return 'medium'; } return 'low'; } private calculateAPIRisk(path: string, statusCode: number): 'info' | 'low' | 'medium' | 'high' | 'critical' { if (statusCode === 404) return 'info'; const pathLower = path.toLowerCase(); if (pathLower.includes('admin') || pathLower.includes('internal')) { return statusCode === 200 ? 'high' : 'medium'; } if (pathLower.includes('swagger') || pathLower.includes('graphql')) { return statusCode === 200 ? 'medium' : 'low'; } return statusCode === 200 ? 'low' : 'info'; } private isDirectory(response: any): boolean { const contentType = response.headers['content-type'] || ''; return contentType.includes('text/html') && (response.data?.includes('<title>Index of') || response.data?.includes('Directory listing')); } private generateDescription(result: DirectoryResult): string { if (result.status_code === 200) { return `Accessible ${result.is_directory ? 'directory' : 'file'}: ${result.url}`; } else if (result.status_code === 403) { return `Forbidden ${result.is_directory ? 'directory' : 'file'}: ${result.url}`; } else if (result.status_code === 401) { return `Authentication required for: ${result.url}`; } else { return `Found ${result.url} (Status: ${result.status_code})`; } } private generateRecommendations(result: DirectoryResult): string[] { const recommendations: string[] = []; if (result.is_sensitive && result.status_code === 200) { recommendations.push('Restrict access to this sensitive resource'); recommendations.push('Consider moving sensitive files outside web root'); } if (result.url.toLowerCase().includes('admin') && result.status_code === 200) { recommendations.push('Implement strong authentication for admin areas'); recommendations.push('Consider IP whitelisting for admin access'); } if (result.url.toLowerCase().includes('backup')) { recommendations.push('Remove backup files from public access'); recommendations.push('Store backups in secure, non-web accessible location'); } if (result.url.toLowerCase().includes('api') && result.status_code === 200) { recommendations.push('Implement proper API authentication'); recommendations.push('Consider rate limiting for API endpoints'); } return recommendations; } private deduplicateResults(results: DirectoryResult[]): DirectoryResult[] { const seen = new Set<string>(); return results.filter(result => { if (seen.has(result.url)) { return false; } seen.add(result.url); return true; }); } private groupByStatusCode(results: DirectoryResult[]): Record<string, number> { const groups: Record<string, number> = {}; for (const result of results) { const code = result.status_code.toString(); groups[code] = (groups[code] || 0) + 1; } return groups; } private groupByRiskLevel(results: DirectoryResult[]): 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.risk_level]++; } return groups; } private generateSecurityRecommendations(results: DirectoryResult[]): string[] { const recommendations = new Set<string>(); const criticalFindings = results.filter(r => r.risk_level === 'critical'); const sensitiveFiles = results.filter(r => r.is_sensitive); if (criticalFindings.length > 0) { recommendations.add('Immediately secure or remove critical security exposures'); } if (sensitiveFiles.length > 0) { recommendations.add('Review and restrict access to sensitive files and directories'); } if (results.some(r => r.url.includes('admin'))) { recommendations.add('Implement strong authentication for administrative interfaces'); } if (results.some(r => r.url.includes('backup'))) { recommendations.add('Remove backup files from web-accessible locations'); } if (results.some(r => r.url.includes('api'))) { recommendations.add('Secure API endpoints with proper authentication and rate limiting'); } recommendations.add('Implement proper access controls and directory browsing restrictions'); recommendations.add('Regular security assessments to identify new exposures'); return Array.from(recommendations); } private generateAttackSurfaceSummary(results: DirectoryResult[]): any { return { total_discovered_paths: results.length, accessible_resources: results.filter(r => r.status_code === 200).length, administrative_interfaces: results.filter(r => r.url.includes('admin')).length, api_endpoints: results.filter(r => r.url.includes('api')).length, configuration_files: results.filter(r => r.url.includes('config')).length, backup_files: results.filter(r => r.url.includes('backup')).length, development_files: results.filter(r => r.url.includes('test') || r.url.includes('dev')).length, information_disclosure: results.filter(r => r.url.includes('phpinfo') || r.url.includes('debug')).length }; } private generatePrioritizedRecommendations(results: DirectoryResult[]): string[] { const recommendations: string[] = []; const criticalCount = results.filter(r => r.risk_level === 'critical').length; const highCount = results.filter(r => r.risk_level === 'high').length; if (criticalCount > 0) { recommendations.push(`Priority 1: Address ${criticalCount} critical security exposures immediately`); } if (highCount > 0) { recommendations.push(`Priority 2: Secure ${highCount} high-risk findings within 24 hours`); } recommendations.push('Priority 3: Implement comprehensive access controls'); recommendations.push('Priority 4: Regular security monitoring and assessment'); return recommendations; } private getDefaultWordlist(): string { // Common wordlist locations const wordlists = [ '/usr/share/wordlists/dirb/common.txt', '/usr/share/wordlists/SecLists/Discovery/Web-Content/common.txt', '/opt/wordlists/common.txt', '/usr/share/dirb/wordlists/common.txt' ]; return wordlists[0]; // Default to dirb common wordlist } private async sleep(ms: number): Promise<void> { return new Promise(resolve => setTimeout(resolve, ms)); } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/adriyansyah-mf/mcp-pentest'

If you have feedback or need assistance with the MCP directory API, please join our Discord server