From 28ecd52f56d2bb50683c094e2518be7c5320f913 Mon Sep 17 00:00:00 2001 From: Hamza Avvan Date: Sun, 12 Apr 2026 17:42:31 +0200 Subject: [PATCH] feat: add threading support with -t/--threads option - Added multi-threading using a worker pool and Queue for concurrent scanning - Defaults to 10 threads, adjustable via -t or --threads - Real-time output with thread-safe Mutex - Fixed uninitialized constant StringIO by adding require 'stringio' for Ruby 3.3.8 - Updated README.md to document the new -t/--threads option --- README.md | 1 + scan.rb | 94 ++++++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 70 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 7740d5a..8de0059 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Here's a list of all available options: - **--wordlist**: specify an alternative location for the wordlist. - **--ssl**: `on` or `off` depending on whether you want to connect with SSL. - **--output**: optionally specify an alternative file to write the output to. Defaults to `output.txt` in the current directory. + - **-t, --threads**: number of concurrent threads to use for scanning. Defaults to `10`. Increase for faster scanning (e.g., `-t=100`) but be mindful of server load and rate limits. ## Wordlist There's a default, small, wordlist in this repository. To use your own wordlist, use the **--wordlist** option. **%s** will be replaced with the given **--host** header in every line of the wordlist file. diff --git a/scan.rb b/scan.rb index 92286b8..ed7e1a0 100644 --- a/scan.rb +++ b/scan.rb @@ -1,5 +1,7 @@ require 'net/http' require 'openssl' +require 'stringio' +require 'thread' # for Queue and Mutex def get_option(key) selected = ARGV.select do |argument| @@ -14,14 +16,10 @@ def get_option(key) def write_results(result, file_path) begin puts " Start writing final results" - file = File.open(file_path, "ab+") - file.write(result) + File.open(file_path, "wb") { |f| f.write(result) } puts " Finish writing final results" rescue IOError => e - #some error occur, dir not writable etc. - puts e; - ensure - file.close unless file.nil? + puts e end end @@ -33,6 +31,7 @@ def write_results(result, file_path) ssl = get_option('ssl') || 'off' port = get_option('port') || (ssl == 'on' ? 443 : 80) output = get_option('output') || 'output.txt' +threads = (get_option('t') || get_option('threads') || 10).to_i if ip_address.nil? || host.nil? puts 'Usage: ruby scan.rb --ip= --host=' @@ -45,36 +44,81 @@ def write_results(result, file_path) puts ' --ignore-content-length=' puts ' --wordlist=' puts ' --ssl=' - puts ' --output=' + puts ' --output=' + puts ' -t, --threads= (default 10)' exit end port = port.to_i -ignore_http_codes = ignore_http_codes.split(',').map { |code| code.to_i } +ignore_http_codes = ignore_http_codes.split(',').map(&:to_i) ignore_content_length = ignore_content_length.to_i -result = StringIO.new -IO.read(File.expand_path(wordlist_file)).split("\n").each do |virtual_host| - hostname = virtual_host.gsub('%s', host) - Net::HTTP.start(ip_address, port, use_ssl: ssl == 'on', verify_mode: OpenSSL::SSL::VERIFY_NONE) do |http| - request = Net::HTTP::Head.new('/') - request['Host'] = port == 80 ? hostname : format('%s:%d', hostname, port) - request['Accept'] = '*/*' +# Load wordlist +wordlist = IO.read(File.expand_path(wordlist_file)).split("\n") - response = http.request(request) +# Thread-safe output collection +output_mutex = Mutex.new +results = StringIO.new +progress_counter = 0 +total = wordlist.size - next if ignore_http_codes.include?(response.code.to_i) - next if ignore_content_length > 0 && ignore_content_length == response['content-length'].to_i +puts "Starting scan with #{threads} threads on #{total} hosts..." - result << "Found: #{hostname} (#{response.code})\n" - response.to_hash.each do |header, values| - result << " #{header}:\n" - values.each do |value| - result << " #{value}\n" +# Work queue +queue = Queue.new +wordlist.each { |vhost| queue << vhost } + +# Worker threads +workers = (1..threads).map do + Thread.new do + # Each thread gets its own HTTP connection for efficiency + Net::HTTP.start(ip_address, port, + use_ssl: ssl == 'on', + verify_mode: OpenSSL::SSL::VERIFY_NONE) do |http| + while (virtual_host = queue.pop(true) rescue nil) + hostname = virtual_host.gsub('%s', host) + request = Net::HTTP::Head.new('/') + request['Host'] = port == 80 ? hostname : format('%s:%d', hostname, port) + request['Accept'] = '*/*' + + begin + response = http.request(request) + rescue StandardError => e + output_mutex.synchronize do + results << "Error: #{hostname} - #{e.message}\n" + $stderr.puts "Error: #{hostname} - #{e.message}" + end + next + end + + next if ignore_http_codes.include?(response.code.to_i) + next if ignore_content_length > 0 && ignore_content_length == response['content-length'].to_i + + # Build output block + block = "Found: #{hostname} (#{response.code})\n" + response.to_hash.each do |header, values| + block << " #{header}:\n" + values.each { |v| block << " #{v}\n" } + end + block << "\n" + + # Thread-safe console printing and result collection + output_mutex.synchronize do + print block + results << block + progress_counter += 1 + # Optional progress indicator (overwrites same line) + # print "\rProgress: #{progress_counter}/#{total}" if progress_counter % 10 == 0 + end end end end end -puts result.string -write_results(result.string, output) +# Wait for all threads to finish +workers.each(&:join) + +puts "\nScan completed." + +# Write final results to file +write_results(results.string, output) \ No newline at end of file