Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
94 changes: 69 additions & 25 deletions scan.rb
Original file line number Diff line number Diff line change
@@ -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|
Expand All @@ -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

Expand All @@ -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=<ip-address> --host=<host>'
Expand All @@ -45,36 +44,81 @@ def write_results(result, file_path)
puts ' --ignore-content-length=<value>'
puts ' --wordlist=<file location>'
puts ' --ssl=<on|off>'
puts ' --output=<output - default current directory, output.txt>'
puts ' --output=<output - default output.txt>'
puts ' -t, --threads=<number> (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)