├── .gitignore ├── Gemfile ├── README.md └── webshot.rb /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | 8 | # Ignore MACs DS_store file 9 | .DS_Store 10 | 11 | # Removing pesky Gemfile.lock 12 | Gemfile.lock 13 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'pry' 4 | gem 'nokogiri' 5 | gem 'imgkit' 6 | gem 'wkhtmltoimage-binary' 7 | gem 'thread' 8 | gem 'ruby-nmap', github: 'sophsec/ruby-nmap' 9 | gem 'nmap-parser' 10 | gem 'phantomjs' 11 | gem 'screencap' 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Webshot 2 | Just another tool to screenshot web servers. 3 | 4 | ## Installation 5 | $ bundle install 6 | 7 | For some reason the thread gem isn't installing via bundle install so install it manually. 8 | 9 | $ gem install thread 10 | 11 | Download the correct wkhtmltopdf package for your distribution from [https://wkhtmltopdf.org/downloads.html](https://wkhtmltopdf.org/downloads.html) and install with ```dpkg -i```. The current version tested and working with Kali from capsulecorp is ```wkhtmltox_0.12.6.1-2.bullseye_amd64.deb``` 12 | 13 | $ sudo dpkg -i wkhtmltox_0.12.6.1-2.bullseye_amd64.deb 14 | 15 | You will also need to install libssl for wkhtmltopdf 16 | 17 | $ sudo apt install libssl1.1 18 | 19 | IF you are still having issues, Webshot utilized the wkhtmltoimage binary which is stuck on libpng12. Download the .deb package from [https://packages.ubuntu.com/xenial/amd64/libpng12-0/download](https://packages.ubuntu.com/xenial/amd64/libpng12-0/download) and install it with `dpkg -i` 20 | 21 | $ sudo dpkg -i libpng12-0_1.2.54-1ubuntu1.1_amd64.deb 22 | 23 | ## Help 24 | $ ./webshot.rb -h 25 | Webshot.rb VERSION: 1.1 - UPDATED: 7/16/2019 26 | 27 | References: 28 | https://github.com/R3dy/webshot 29 | 30 | Usage: ./webshot.rb [options] [target list] 31 | 32 | -t, --targets [Nmap XML File] XML Output From Nmap Scan 33 | -c, --css [CSS File] File containing css to apply to all screenshtos 34 | -u, --url [Single URL] Single URL to take a screenshot 35 | -U, --url-file [URL File] Text file containing URLs, one on each line 36 | -o, --output [Output Directory] Path to file where screenshots will be stored 37 | -T, --threads [Thread Count] Integer value between 1-20 (Default is 10) 38 | -v, --verbose Enables verbose output 39 | 40 | 41 | -------------------------------------------------------------------------------- /webshot.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | require 'imgkit' 4 | require 'pry' 5 | require 'thread/pool' 6 | require 'optparse' 7 | require 'nmap/parser' 8 | require 'nokogiri' 9 | require 'net/https' 10 | require 'openssl' 11 | rescue LoadError => msg 12 | puts msg.message.chomp + " try running \'bundle install\' first" 13 | exit! 14 | end 15 | 16 | unless ARGV.length > 0 17 | puts "Try ./webshot.rb -h\r\n\r\n" 18 | exit! 19 | end 20 | 21 | 22 | @options = {} 23 | args = OptionParser.new do |opts| 24 | opts.banner = "Webshot.rb VERSION: 1.1 - UPDATED: 7/16/2019\r\n\r\n" 25 | opts.banner += "References:\r\n" 26 | opts.banner += "\thttps://github.com/R3dy/webshot\r\n\r\n" 27 | opts.banner += "Usage: ./webshot.rb [options] [target list]\r\n\r\n" 28 | opts.on("-t", "--targets [Nmap XML File]", "XML Output From Nmap Scan") { |targets| @options[:targets] = File.open(targets, "r").read } 29 | opts.on("-c", "--css [CSS File]", "File containing css to apply to all screenshtos") { |css| @options[:css] = File.open(css, "r") } 30 | opts.on("-u", "--url [Single URL]", "Single URL to take a screenshot") { |url| @options[:url] = url.chomp } 31 | opts.on("-U", "--url-file [URL File]", "Text file containing URLs, one on each line") { |url_file| @options[:url_file] = File.open(url_file, "r").read } 32 | opts.on("-o", "--output [Output Directory]", "Path to file where screenshots will be stored") { |output| @options[:output] = output.chomp } 33 | opts.on("-T", "--threads [Thread Count]", "Integer value between 1-20 (Default is 10)") { |threads| @options[:threads] = threads.to_i } 34 | opts.on("-v", "--verbose", "Enables verbose output\r\n\r\n") { |v| @options[:verbose] = true } 35 | end 36 | args.parse!(ARGV) 37 | 38 | 39 | @options[:urls] = Array.new 40 | if @options[:targets] 41 | puts "Extracting URLs from Nmap scan" 42 | xml = Nokogiri::XML(@options[:targets]) 43 | xml.xpath("//host")[1..-1].each do |host| 44 | ip = host.css('address').attr('addr').text 45 | host.css("port").each do |p| 46 | if p.css('state').attr('state').value == "open" 47 | next if !p.css('service').attr('name') 48 | port = p.attr('portid') 49 | proto = p.css('service').attr('name').text.split('-')[0] 50 | if proto == 'http' || proto == 'https' 51 | @options[:urls] << "https://#{ip}:#{port}" 52 | @options[:urls] << "http://#{ip}:#{port}" 53 | end 54 | end 55 | end 56 | end 57 | end 58 | 59 | 60 | if @options[:url] 61 | @options[:urls] << @options[:url] if @options[:url] 62 | end 63 | 64 | 65 | if @options[:url_file] 66 | @options[:urls] = @options[:url_file].split 67 | end 68 | 69 | 70 | if @options[:threads] 71 | if @options[:threads] > 20 || @options[:threads] < 1 72 | puts "Error: Thread count must be an integer value between 1 & 20" 73 | exit! 74 | else 75 | @threads = Thread.pool(@options[:threads]) 76 | end 77 | else 78 | @threads = Thread.pool(10) 79 | end 80 | 81 | 82 | puts "Configuring IMGKit options" 83 | IMGKit.configure do |config| 84 | config.default_options = { 85 | quality: 25, 86 | height: 600, 87 | width: 800, 88 | # If the website happens to offer up an auth prompt. 89 | # No harm in trying admin/admin while we're here... 90 | username: 'admin', 91 | password: 'admin', 92 | 'load-error-handling' => 'ignore' 93 | } 94 | end 95 | 96 | 97 | # Method not used but keeping the code cause YOLO 98 | def setup_http(url, use_ssl) 99 | uri = URI.parse(url) 100 | http = Net::HTTP.new(uri.host, uri.port) 101 | http.use_ssl = use_ssl 102 | http.verify_mode = OpenSSL::SSL::VERIFY_NONE if use_ssl 103 | return http 104 | end 105 | 106 | 107 | def get_screenshot(url) 108 | begin 109 | puts "Taking screenshot: #{url}" if @options[:verbose] 110 | #headers = {} 111 | #use_ssl = url.include? 'https' 112 | #http = setup_http(url, use_ssl) 113 | #response = http.get('/', {}) 114 | #if response.code == "302" 115 | #path = '/' + response.header["location"].split('/')[-1] 116 | # response = http.get(path, {}) 117 | #end 118 | screenshot = IMGKit.new(url, quality: 25, height: 600, width: 800) 119 | screenshot.stylesheets << @options[:css].path if @options[:css] 120 | rescue Errno::EHOSTUNREACH => error 121 | puts error if @options[:verbose] 122 | return nil 123 | rescue => error 124 | puts error if @options[:verbose] 125 | return nil 126 | end 127 | return screenshot 128 | end 129 | 130 | 131 | puts "Capturing #{@options[:urls].size.to_s} screenshots using #{@threads.max.to_s} threads" 132 | @options[:urls].each do |url| 133 | @threads.process { 134 | screenshot = get_screenshot(url) 135 | if screenshot 136 | begin 137 | ip = url.split('//')[1] 138 | file = screenshot.to_file("#{@options[:output]}/#{url.split(':')[0]}_#{ip.gsub(/[:\/]/,'_')}.png") 139 | FileUtils.rm(file.path) unless file.size > 3273 140 | rescue => error 141 | if error.message.include? "No such file or directory" 142 | puts "Directory \"#{@options[:output]}\" does not exist." if @options[:verbose] 143 | return 144 | end 145 | next 146 | end 147 | end 148 | } 149 | end 150 | @threads.shutdown 151 | --------------------------------------------------------------------------------