├── .gitignore ├── Gemfile ├── README.md ├── lib ├── ufuzz.rb └── ufuzz │ ├── command_line.rb │ ├── config.rb │ ├── constants.rb │ ├── errors.rb │ ├── extensions.rb │ ├── fault.rb │ ├── fuzzer.rb │ ├── helpers │ ├── syslog.rb │ └── telnet.rb │ ├── http │ ├── burp.rb │ ├── connection.rb │ ├── cookies.rb │ ├── fuzzer.rb │ ├── request.rb │ ├── response.rb │ └── session.rb │ ├── logger.rb │ ├── monitor.rb │ ├── net │ ├── socket.rb │ └── ssl_socket.rb │ ├── tag_splitter.rb │ ├── testcase │ ├── buffer_test.rb │ ├── cmd_test.rb │ ├── fmt_test.rb │ ├── integer_test.rb │ ├── path_test.rb │ ├── sqli_test.rb │ ├── testcase.rb │ └── xxe_test.rb │ ├── tokenizer.rb │ ├── upnp │ ├── fuzzer.rb │ └── request.rb │ ├── validator.rb │ └── wordlist │ ├── cmd_injection.txt │ ├── format_string.txt │ ├── path_traversal.txt │ ├── sql_injection.txt │ └── xxe.txt ├── log └── .gitignore ├── modules ├── generic │ ├── config.rb │ └── monitor.rb ├── serial │ ├── config.rb │ └── monitor.rb └── telnet │ ├── config.rb │ ├── http_session.rb │ └── monitor.rb └── ufuzz /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.xml 3 | .DS_Store 4 | 5 | *.gem 6 | *.rbc 7 | /.config 8 | /coverage/ 9 | /InstalledFiles 10 | /pkg/ 11 | /spec/reports/ 12 | /test/tmp/ 13 | /test/version_tmp/ 14 | /tmp/ 15 | 16 | ## Specific to RubyMotion: 17 | .dat* 18 | .repl_history 19 | build/ 20 | 21 | ## Documentation cache and generated files: 22 | /.yardoc/ 23 | /_yardoc/ 24 | /doc/ 25 | /rdoc/ 26 | 27 | ## Environment normalisation: 28 | /.bundle/ 29 | /lib/bundler/man/ 30 | 31 | # for a library or gem, you might want to ignore these files since the code is 32 | # intended to run in multiple environments; otherwise, check them in: 33 | # Gemfile.lock 34 | # .ruby-version 35 | # .ruby-gemset 36 | 37 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 38 | .rvmrc -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'colorize' 4 | gem 'rbkb' 5 | gem 'nokogiri' 6 | gem 'escape_utils' 7 | gem 'eventmachine' 8 | gem 'serialport' 9 | gem 'activesupport' -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![UFuzz](http://pulpphikshun.files.wordpress.com/2014/04/ufuzzlogo.png?w=300&h=150) 2 | == 3 | 4 | UFuzz, or Universal Plug and Fuzz, is an automatic UPnP fuzzing tool. It will enumerate all UPnP endpoints on the network, find the available services and fuzz them. It also has the capability to fuzz HTTP using Burp proxy logs. 5 | 6 | It is designed to fuzz embedded systems, and as such, is only single threaded. It also has a very limited payload set since fuzzing these systems can be slow. Certain payloads such as blind SQLi and command injection rely on delays to indicate whether the injection was successful, and may have false positives. Other payloads such as format strings and buffer overflows are designed to use a custom monitor to detect crashes. 7 | 8 | Example configuration modules and monitor modules are included. Custom monitors allow the use of target system telemetry to detect crashes. Example modules have been provide for telnet-based and serial console based crash detection. 9 | 10 | Note that the code is very rough around the edges. "Hacky" would be the best way to describe it. Unfortunately this project was written quickly and really never properly architected. I will be working to resolve this in the coming months. 11 | 12 | Finally, some of the code was borrowed from other projects: 13 | 14 | * The UPnP code is based largely on Craig Heffner's miranda code. Craig has been an inspiration to me and I highly recommend you read his blog [/dev/ttys0](http://www.devttys0.com). 15 | 16 | * Some of the test set generation code is based on Ben Nagy's Metafuzz project. 17 | 18 | * The socket and http parsing code is based on [Excon](https://github.com/geemus/excon). 19 | 20 | Installation 21 | ---- 22 | 23 | UFuzz has been tested with Ruby 1.9.3 and Ruby 2.1.1. You can install all the required gems by running `bundle install` in the UFuzz directory. 24 | 25 | Usage 26 | ---- 27 | 28 | Run ufuzz with the -h option to see all command line options. When the tool runs, logs are written into the log directory. 29 | 30 | For basic fuzzing of all UPnP devices on the network, just run `ufuzz --upnp`. You will probably also want to use the `-v 4` option to see the requests and response summaries. -------------------------------------------------------------------------------- /lib/ufuzz.rb: -------------------------------------------------------------------------------- 1 | $:.unshift(File.expand_path(File.dirname(__FILE__))) 2 | 3 | require 'colorize' 4 | require 'rbkb/extends' 5 | require 'uri' 6 | require 'ostruct' 7 | require 'socket' 8 | require 'ipaddr' 9 | require 'net/http' 10 | require 'thread' 11 | require 'openssl' 12 | require 'timeout' 13 | require 'net/telnet' 14 | require 'eventmachine' 15 | require 'serialport' 16 | require 'nokogiri' 17 | require 'escape_utils' 18 | require 'forwardable' 19 | require 'active_support/inflector' 20 | 21 | require 'ufuzz/extensions' 22 | require 'ufuzz/fault' 23 | require 'ufuzz/testcase/testcase' 24 | 25 | # include all test case generators 26 | Dir["#{File.expand_path(File.dirname(__FILE__))}/ufuzz/testcase/*.rb"].each {|file| require file } 27 | 28 | require 'ufuzz/constants' 29 | require 'ufuzz/errors' 30 | require 'ufuzz/net/socket' 31 | require 'ufuzz/net/ssl_socket' 32 | require 'ufuzz/logger' 33 | require 'ufuzz/validator' 34 | require 'ufuzz/command_line' 35 | require 'ufuzz/config' 36 | require 'ufuzz/tokenizer' 37 | require 'ufuzz/tag_splitter' 38 | require 'ufuzz/monitor' 39 | require 'ufuzz/fuzzer' 40 | 41 | require 'ufuzz/http/request' 42 | require 'ufuzz/http/response' 43 | require 'ufuzz/http/cookies' 44 | require 'ufuzz/http/connection' 45 | require 'ufuzz/http/session' 46 | require 'ufuzz/http/fuzzer' 47 | require 'ufuzz/http/burp' 48 | 49 | require 'ufuzz/upnp/request' 50 | require 'ufuzz/upnp/fuzzer' 51 | 52 | require 'ufuzz/helpers/telnet' 53 | require 'ufuzz/helpers/syslog' 54 | 55 | # include all modules 56 | Dir["#{File.expand_path(File.dirname(__FILE__))}/../modules/*/*.rb"].each {|file| require file } 57 | 58 | trap("INT") { puts ''; log "shutting down", WARN; exit } 59 | -------------------------------------------------------------------------------- /lib/ufuzz/command_line.rb: -------------------------------------------------------------------------------- 1 | require 'optparse' 2 | 3 | module UFuzz 4 | module Options 5 | def parse_options(cmd_line) 6 | options = {} 7 | 8 | optparse = OptionParser.new do |opts| 9 | opts.on('-m', '--module STR', 'Load configuration/monitor module') do |m| 10 | options[:module] = m 11 | end 12 | 13 | opts.on('-t', '--target STR', 'Hostname or IP of target') do |h| 14 | options[:host] = h 15 | end 16 | 17 | opts.on('-p NUM', '--port NUM', 'TCP/UDP port number of target service (default 80)') do |p| 18 | options[:port] = p 19 | end 20 | 21 | opts.on('--ssl', 'Use SSL to connect to service') do 22 | options[:ssl] = true 23 | end 24 | 25 | opts.on('--soap', 'Assume request has XML payload') do 26 | options[:soap] = true 27 | end 28 | 29 | opts.on('-i', '--import FILE', 'Fuzz from requests in Burp XML format') do |i| 30 | options[:import] = i 31 | end 32 | 33 | opts.on('-u', '--upnp', 'Build list of fuzz requests from UPnP') do |i| 34 | options[:upnp] = true 35 | options[:soap] = true 36 | options[:fuzzers] = ['post'] 37 | end 38 | 39 | opts.on('-f', '--fuzzers STR', 'Comma separated list of fuzzing engines to utilize (param,post,token,...)') do |f| 40 | options[:fuzzers] = f.split(',').map do |d| 41 | d.downcase.strip 42 | end 43 | end 44 | 45 | opts.on('-s', '--test-set STR', 'Comma separated list of test sets to utilize (buffer,integer,sqli,xxe,...)') do |s| 46 | options[:tests] = s.split(',').map do |d| 47 | d.downcase.strip 48 | end 49 | end 50 | 51 | opts.on('--delay NUM', 'Add a delay after each request') do |d| 52 | options[:delay] = d.to_f 53 | end 54 | 55 | opts.on('--skip-dedup', 'Skip proxy log de-duplication') do 56 | options[:skip_dedup] = true 57 | end 58 | 59 | opts.on('--reverse-log', 'Fuzz proxy log in reverse order') do 60 | options[:reverse_log] = true 61 | end 62 | 63 | opts.on('-v', '--verbose NUM', 'Enabled verbose output, from 0 (fail) to 4 (trace), default 2 (info)') do |v| 64 | options[:verbose] = v.to_i 65 | end 66 | 67 | opts.on( '-h', '--help', 'Display this screen' ) do 68 | puts opts 69 | exit 70 | end 71 | end 72 | 73 | optparse.parse!(cmd_line) 74 | options[:module] ||= 'generic' 75 | options 76 | end 77 | end 78 | end -------------------------------------------------------------------------------- /lib/ufuzz/config.rb: -------------------------------------------------------------------------------- 1 | module UFuzz 2 | class Config 3 | extend Options 4 | include Validator 5 | attr_accessor :store, :monitor, :session, :logger 6 | 7 | def default_options 8 | { 9 | :request => nil, 10 | :logging => { :text => true }, 11 | :verbose => 2, 12 | :connect_timeout => 2, 13 | :read_timeout => 10, 14 | :write_timeout => 10, 15 | :msearch_timeout => 15, 16 | :non_block => true, 17 | :nonblock => true, 18 | :chunk_size => 1048576, 19 | :retry_limit => 2, 20 | :traversal_match => [ /root:/ ], 21 | :csrf_token_regex => nil, 22 | :detect_delay => 5, 23 | :thread_count => 1, 24 | :encoders => [ proc { |f| f.to_s }, proc { |f| f.to_s.urlenc } ], 25 | :extra_param => { 't' => '1' }, 26 | :fuzzable_headers => { 27 | 'Host' => 'localhost', 28 | 'Cookie' => '0', 29 | 'User-Agent' => 'Mozilla', 30 | 'Referer' => 'localhost' 31 | }, 32 | :tests => [ 'buffer', 'integer', 'fmt', 'path', 'cmd', 'sqli', 'xxe' ], 33 | } 34 | end 35 | 36 | def options 37 | { } 38 | end 39 | 40 | def initialize(opts) 41 | @store = OpenStruct.new(default_options.merge(options.merge(opts))) 42 | validate(self) 43 | end 44 | 45 | def method_missing(meth, *args, &block) 46 | @store.send(meth, *args) 47 | rescue 48 | nil 49 | end 50 | 51 | def create_request 52 | "UFuzz::#{@store.app.camelize}::Request".safe_constantize.new(@store.request) 53 | rescue 54 | nil 55 | end 56 | 57 | def create_connection 58 | "UFuzz::#{@store.app.camelize}::Connection".safe_constantize.new(self) 59 | rescue 60 | nil 61 | end 62 | 63 | def create_monitor 64 | @monitor ||= "#{@store.module.camelize}Monitor".safe_constantize.new(self) 65 | rescue => e 66 | UFuzz::Monitor.new(self) 67 | end 68 | 69 | def create_session 70 | @session ||= "#{@store.module.camelize}Session".safe_constantize 71 | @session.new(self) 72 | rescue => e 73 | UFuzz::Http::Session.new(self) 74 | end 75 | 76 | def create_logger 77 | @logger ||= Logger.instance 78 | end 79 | 80 | def create_testcase 81 | test_set = @store.tests 82 | 83 | tests = test_set.map do |t| 84 | "UFuzz::#{t.camelize}Test".safe_constantize.new(:monitor => create_monitor) 85 | end 86 | 87 | TestCaseChain.new(*tests) 88 | end 89 | 90 | # class methods 91 | 92 | def self.create(command_line) 93 | opts = parse_options(command_line) 94 | @@instance = "#{opts[:module].camelize}Config".safe_constantize.new(opts) 95 | end 96 | 97 | def self.instance 98 | @@instance 99 | end 100 | end 101 | end -------------------------------------------------------------------------------- /lib/ufuzz/constants.rb: -------------------------------------------------------------------------------- 1 | module UFuzz 2 | HTTP_1_1 = " HTTP/1.1\r\n" 3 | CR_NL = "\r\n" 4 | 5 | DEFAULT_NONBLOCK = OpenSSL::SSL::SSLSocket.public_method_defined?(:connect_nonblock) && 6 | OpenSSL::SSL::SSLSocket.public_method_defined?(:read_nonblock) && 7 | OpenSSL::SSL::SSLSocket.public_method_defined?(:write_nonblock) 8 | 9 | NO_ENTITY = [204, 205, 304].freeze 10 | 11 | HTTP_ERRORS = { 12 | 100 => 'Continue', 13 | 101 => 'Switching Protocols', 14 | 200 => 'OK', 15 | 201 => 'Created', 16 | 202 => 'Accepted', 17 | 203 => 'Non-Authoritative Information', 18 | 204 => 'No Content', 19 | 205 => 'Reset Content', 20 | 206 => 'Partial Content', 21 | 300 => 'Multiple Choices', 22 | 301 => 'Moved Permanently', 23 | 302 => 'Found', 24 | 303 => 'See Other', 25 | 304 => 'Not Modified', 26 | 305 => 'Use Proxy', 27 | 307 => 'Temporary Redirect', 28 | 400 => 'Bad Request', 29 | 401 => 'Unauthorized', 30 | 402 => 'Payment Required', 31 | 403 => 'Forbidden', 32 | 404 => 'Not Found', 33 | 405 => 'Method Not Allowed', 34 | 406 => 'Not Acceptable', 35 | 407 => 'Proxy Authentication Required', 36 | 408 => 'Request Timeout', 37 | 409 => 'Conflict', 38 | 410 => 'Gone', 39 | 411 => 'Length Required', 40 | 412 => 'Precondition Failed', 41 | 413 => 'Request Entity Too Large', 42 | 414 => 'Request-URI Too Long', 43 | 415 => 'Unsupported Media Type', 44 | 416 => 'Request Range Not Satisfiable', 45 | 417 => 'Expectation Failed', 46 | 422 => 'Unprocessable Entity', 47 | 500 => 'InternalServerError', 48 | 501 => 'Not Implemented', 49 | 502 => 'Bad Gateway', 50 | 503 => 'Service Unavailable', 51 | 504 => 'Gateway Timeout' 52 | } 53 | 54 | unless ::IO.const_defined?(:WaitReadable) 55 | class ::IO 56 | module WaitReadable; end 57 | end 58 | end 59 | 60 | unless ::IO.const_defined?(:WaitWritable) 61 | class ::IO 62 | module WaitWritable; end 63 | end 64 | end 65 | end -------------------------------------------------------------------------------- /lib/ufuzz/errors.rb: -------------------------------------------------------------------------------- 1 | module UFuzz 2 | module Errors 3 | class Error < StandardError; end 4 | 5 | class SocketError < Error 6 | attr_reader :socket_error 7 | 8 | def initialize(socket_error=nil) 9 | if socket_error.message =~ /certificate verify failed/ 10 | super("Unable to verify certificate") 11 | else 12 | super("#{socket_error.message} (#{socket_error.class})") 13 | end 14 | set_backtrace(socket_error.backtrace) 15 | @socket_error = socket_error 16 | end 17 | end 18 | 19 | class Timeout < Error; end 20 | end 21 | end -------------------------------------------------------------------------------- /lib/ufuzz/extensions.rb: -------------------------------------------------------------------------------- 1 | class String 2 | def limit(len) 3 | if self.length > len 4 | self[0,len] 5 | else 6 | self 7 | end 8 | end 9 | end -------------------------------------------------------------------------------- /lib/ufuzz/fault.rb: -------------------------------------------------------------------------------- 1 | module UFuzz 2 | 3 | class Fault 4 | attr_accessor :reason, :desc, :crash_dump, :tx, :rx 5 | 6 | def initialize(reason, desc, crash_dump = nil) 7 | @reason = reason 8 | @desc = desc 9 | @crash_dump = crash_dump 10 | @tx = nil 11 | @rx = nil 12 | end 13 | 14 | def pretty_print(opts = {}) 15 | "#{reason} - #{desc}" 16 | end 17 | end 18 | 19 | end -------------------------------------------------------------------------------- /lib/ufuzz/fuzzer.rb: -------------------------------------------------------------------------------- 1 | module UFuzz 2 | 3 | class Fuzzer 4 | attr_accessor :testcase, :config, :request, :mutex, :connection, :session 5 | 6 | def initialize(config) 7 | @config = config 8 | @connection = config.create_connection 9 | @testcase = config.create_testcase 10 | @request = config.create_request 11 | @session = config.create_session 12 | @monitor = @config.create_monitor 13 | @count = 0 14 | @config.req_summary = @config.req_summary || 'default' 15 | end 16 | 17 | def new_session 18 | end 19 | 20 | def parser 21 | if @request.is_a?(Array) 22 | @request.each do |req| 23 | standard_fuzz(req) 24 | end 25 | else 26 | standard_fuzz(req) 27 | end 28 | end 29 | 30 | def standard_fuzz(req) 31 | t = Tokenizer.new(req.dup) 32 | t.fuzz_each_token(@testcase) do |fuzz_case, i, fuzz| 33 | do_fuzz_case(fuzz_case, i, fuzz) 34 | end 35 | end 36 | 37 | def do_fuzz(req, position, fuzz, opts) 38 | @count += 1 39 | conn = nil 40 | resp = nil 41 | 42 | Timeout::timeout(15) do 43 | log "tx >>>\n#{req}", TRACE 44 | conn = config.create_connection 45 | resp = conn.send(req) 46 | if resp.respond_to?(:summary) 47 | log "rx <<< #{resp.summary}", TRACE 48 | else 49 | log "rx <<< #{resp}", TRACE 50 | end 51 | end 52 | 53 | check_fault(fuzz, req, resp) 54 | 55 | sleep @config.delay if @config.delay 56 | 57 | rescue Timeout::Error 58 | if conn.connection_success? 59 | check_fault(fuzz, req) 60 | else 61 | log "connect fail", WARN 62 | end 63 | end 64 | 65 | def do_fuzz_case(req, position, fuzz, opts = {}) 66 | #if fuzz.threadable? 67 | # if Thread.list.count >= @config.thread_count 68 | # Thread.list[1].join 69 | # end 70 | # Thread.new { do_fuzz(req, position, fuzz, opts) } 71 | #else 72 | do_fuzz(req, position, fuzz, opts) 73 | #end 74 | end 75 | 76 | def check_fault(fuzz, req, resp = nil) 77 | fault = fuzz.test(resp) 78 | 79 | if fault 80 | fault.tx = req 81 | fault.rx = resp 82 | log fault 83 | new_session 84 | end 85 | end 86 | 87 | def run 88 | log "begin #{self.class}", INFO 89 | start_time = Time.now.to_f 90 | @count = 0 91 | 92 | parser 93 | 94 | log "finished #{self.class}, #{(@count / (Time.now.to_f - start_time)).to_i} req/sec, #{@count} total", INFO 95 | cleanup 96 | end 97 | 98 | def cleanup 99 | @session.clear if @config.use_session 100 | end 101 | 102 | def start! 103 | run 104 | end 105 | 106 | # class methods 107 | 108 | def self.load(opts) 109 | config = UFuzz::Config.create(opts) 110 | 111 | if config.upnp 112 | fuzzer = UFuzz::UPnP::Fuzzer.new(config) 113 | else 114 | fuzzer = UFuzz::Http::Fuzzer.new(config) 115 | end 116 | 117 | fuzzer 118 | end 119 | end 120 | 121 | end -------------------------------------------------------------------------------- /lib/ufuzz/helpers/syslog.rb: -------------------------------------------------------------------------------- 1 | module UFuzz 2 | module MonitorHelpers 3 | 4 | module SyslogServer 5 | def messages 6 | @messages 7 | end 8 | 9 | def clear_messages 10 | @messages = nil 11 | end 12 | 13 | def receive_data(data) 14 | if @messages 15 | @messages += data + "\n" 16 | else 17 | @messages = data + "\n" 18 | end 19 | end 20 | end 21 | 22 | module SyslogHelper 23 | def start(opts = {}) 24 | listen_host = (opts[:syslog_listen_host] || '172.16.8.1') 25 | listen_port = (opts[:syslog_listen_port] || 9514).to_i 26 | @syslog_em = nil 27 | @syslog_thread = Thread.new { 28 | EM::run do 29 | @syslog_em = EM.open_datagram_socket listen_host, listen_port, UFuzz::Helpers::SyslogServer 30 | end 31 | } 32 | end 33 | 34 | def read 35 | msgs = @syslog_em.messages 36 | @syslog_em.clear_messages 37 | msgs 38 | end 39 | 40 | def stop 41 | @syslog_thread.kill 42 | end 43 | end 44 | 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/ufuzz/helpers/telnet.rb: -------------------------------------------------------------------------------- 1 | module UFuzz 2 | module MonitorHelpers 3 | 4 | module Telnet 5 | def login 6 | @session = ::Net::Telnet::new("Host" => (config.monitor_host || config.host), "Prompt" => (config.prompt || /[a-zA-Z0-9]+ # \z/in)) 7 | @session.login(config.admin_user, config.admin_pass) 8 | rescue Errno::ETIMEDOUT, Timeout::Error => e 9 | log "could not connect to target via telnet", FAIL 10 | log e.backtrace, DEBUG 11 | exit 12 | end 13 | 14 | def telnet_cmd(cmd, &block) 15 | if block_given? 16 | @session.cmd(cmd, &block) 17 | else 18 | @session.cmd(cmd) 19 | end 20 | rescue Errno::ETIMEDOUT, Timeout::Error => e 21 | log "telnet logged out, retrying", FAIL 22 | log e.backtrace, DEBUG 23 | login 24 | end 25 | end 26 | 27 | end 28 | end -------------------------------------------------------------------------------- /lib/ufuzz/http/burp.rb: -------------------------------------------------------------------------------- 1 | module UFuzz 2 | module Http 3 | 4 | class Burp 5 | attr_accessor :doc, :entries, :config 6 | 7 | def initialize(config) 8 | @config = config 9 | 10 | load_doc config.import 11 | process_doc 12 | end 13 | 14 | def load_doc(xml_file) 15 | @doc = Nokogiri::XML(File.read xml_file) 16 | end 17 | 18 | def process_doc 19 | @entries = [] 20 | doc.root.xpath('/items/item').each do |i| 21 | entry = [] 22 | entry << i.xpath('request').children.first.content.d64 23 | entry << (i.xpath('response').children.first.content.d64 rescue '') 24 | @entries << entry 25 | end 26 | 27 | @entries.reverse! if @config.reverse_log 28 | log "processing the following requests:", INFO 29 | @entries.each do |e| 30 | log Request.new(e[0]).first_line, INFO 31 | end 32 | end 33 | 34 | def each_entry 35 | if block_given? 36 | @entries.each do |e| 37 | yield(e[0], e[1]) 38 | end 39 | else 40 | raise SyntaxError 41 | end 42 | end 43 | end 44 | 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/ufuzz/http/connection.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: binary -*- 2 | module UFuzz 3 | module Http 4 | 5 | class Connection 6 | attr_accessor :host, :port, :ssl, :socket, :config, :connected 7 | 8 | def initialize(c) 9 | @config = c 10 | @host = c.host 11 | @port = c.port.to_i 12 | @ssl = c.ssl 13 | @socket = nil 14 | @connected = false 15 | end 16 | 17 | def send(req, opts = {}) 18 | resp = nil 19 | @socket = nil 20 | retr = 0 21 | @connected = false 22 | 23 | begin 24 | if @ssl 25 | @socket = UFuzz::Net::SSLSocket.new(@config) 26 | else 27 | @socket = UFuzz::Net::Socket.new(@config) 28 | end 29 | 30 | @connected = true 31 | @socket.write(req.to_s) 32 | resp = Response.parse(@socket, req.to_s) 33 | 34 | rescue => e 35 | log e.message, DEBUG 36 | @socket.close if @socket 37 | if e.message == 'Connection refused - connect(2)' 38 | sleep(0.5) 39 | retr += 1 40 | if retr <= @config.retry_limit 41 | retry 42 | end 43 | end 44 | end 45 | 46 | @socket.close if @socket 47 | resp 48 | end 49 | 50 | def connection_success? 51 | @connected 52 | end 53 | end 54 | 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/ufuzz/http/cookies.rb: -------------------------------------------------------------------------------- 1 | module UFuzz 2 | module Http 3 | 4 | class CookieJar 5 | attr_accessor :cookies 6 | 7 | def initialize(data = nil) 8 | @cookies = {} 9 | if data.is_a? Hash 10 | @cookies = data 11 | else 12 | parse(data) 13 | end 14 | end 15 | 16 | def to_s 17 | @cookies.each_pair.map { |k,v| "#{k}=#{v}" }.join('; ') 18 | end 19 | 20 | def inspect 21 | "<#{"%x" % self.object_id} #{self.class}> #{cookies.inspect}" 22 | end 23 | 24 | def to_h 25 | @cookies 26 | end 27 | 28 | def to_hash 29 | @cookies 30 | end 31 | 32 | def merge(h) 33 | @cookies.merge!(h) 34 | end 35 | 36 | def find(c) 37 | if @cookies[c] 38 | return { c => @cookies[c] } 39 | else 40 | return { } 41 | end 42 | end 43 | 44 | def empty? 45 | @cookies.empty? 46 | end 47 | 48 | def parse(data) 49 | if data.is_a? Response 50 | parse_from_resp(data) 51 | else 52 | parse_from_req(data.to_s) 53 | end 54 | end 55 | 56 | def parse_from_resp(resp) 57 | set_cookie = [] 58 | 59 | set_cookie << resp.headers['Set-Cookie'] if resp.headers['Set-Cookie'] 60 | set_cookie << resp.headers['Set-cookie'] if resp.headers['Set-cookie'] 61 | 62 | set_cookie.each do |sc| 63 | sc.split(/[;,]\s*/).each do |c| 64 | pair = c.split('=', 2) 65 | next if pair.count != 2 || ['expires', 'path'].include?(pair[0]) 66 | update_cookie_raw(c) 67 | end 68 | end 69 | end 70 | 71 | def parse_from_req(req) 72 | req.match(/Cookie: ([^\r\n]+)/) do |m| 73 | cookie_header = m[1] 74 | cookie_header.split(/[;,]\s?/).each do |c| 75 | update_cookie_raw(c) 76 | end 77 | end 78 | end 79 | 80 | def update_cookie_raw(c) 81 | pair = c.split("=", 2) 82 | return nil unless pair.count == 2 83 | @cookies.merge!({pair[0] => pair[1]}) 84 | end 85 | 86 | end 87 | 88 | end 89 | end -------------------------------------------------------------------------------- /lib/ufuzz/http/fuzzer.rb: -------------------------------------------------------------------------------- 1 | module UFuzz 2 | module Http 3 | 4 | class Fuzzer < UFuzz::Fuzzer 5 | def run 6 | log "begin #{self.class}", INFO 7 | start_time = Time.now.to_f 8 | @count = 0 9 | 10 | if config.request 11 | @request = Request.new(@config.request) 12 | @config.req_summary = @request.verb_path 13 | log "fuzzing request #{@request.summary}", INFO 14 | 15 | (config.fuzzers || default_fuzzers).each do |f| 16 | new_session 17 | begin 18 | self.send("#{f}_fuzzer".to_sym) 19 | rescue 20 | log "error running #{f}_fuzzer", FAIL 21 | log e.message, DEBUG 22 | log e.backtrace, TRACE 23 | end 24 | end 25 | 26 | elsif config.import 27 | @transactions = UFuzz::Http::Burp.new(config) 28 | @transactions.each_entry do |req, resp| 29 | @request = Request.new(req) 30 | @config.req_summary = @request.verb_path 31 | @response = resp 32 | 33 | if config.skip_urls && @request.summary =~ config.skip_urls 34 | log "skipping request #{@request.summary}", INFO 35 | next 36 | else 37 | log "fuzzing request #{@request.summary}", INFO 38 | end 39 | 40 | (config.fuzzers || default_fuzzers).each do |f| 41 | new_session 42 | begin 43 | self.send("#{f}_fuzzer".to_sym) 44 | rescue => e 45 | log "error running #{f}_fuzzer", FAIL 46 | log e.message, DEBUG 47 | log e.backtrace, TRACE 48 | end 49 | end 50 | end 51 | end 52 | 53 | log "finished #{self.class}, #{(@count / (Time.now.to_f - start_time)).to_i} req/sec, #{@count} total", INFO 54 | cleanup 55 | end 56 | 57 | def default_fuzzers 58 | ['param', 'post', 'header', 'rest', 'verb'] 59 | end 60 | 61 | def new_session 62 | 10.times do 63 | if @config.use_session && !@session.valid? 64 | @session.clear 65 | @session.create 66 | @request.update_cookies(@session) 67 | else 68 | return @session 69 | end 70 | sleep(1) 71 | end 72 | raise "new_session: could not establish new session" 73 | end 74 | 75 | def header_fuzzer 76 | headers = @config.fuzzable_headers.merge(@request.headers) 77 | headers = headers.merge({ 'User-Agent' => 'Mozilla/5.0' }) # speed fix 78 | 79 | @config.fuzzable_headers.each_key do |header| 80 | value = headers[header] 81 | t = Tokenizer.new(value) 82 | t.fuzz_each_token(testcase) do |fuzz_header, i, fuzz| 83 | req = Request.new(@request.to_s) 84 | req.set_header(header, fuzz_header) 85 | do_fuzz_case(req, i, fuzz) 86 | end 87 | end 88 | 89 | testcase.rewind 90 | while(testcase.next?) 91 | req = Request.new(@request.to_s) 92 | fuzz = testcase.next 93 | req.set_header(fuzz.to_s, '1') 94 | do_fuzz_case(req, req.to_s.index(fuzz.to_s), fuzz) 95 | end 96 | end 97 | 98 | def param_fuzzer 99 | @request.url_variables.each_pair do |k,v| 100 | t = Tokenizer.new(v) 101 | t.fuzz_each_token(testcase) do |fuzz_param, i, fuzz| 102 | req = Request.new(@request.to_s) 103 | req.query_string = @request.url_variables.merge({k => fuzz_param}) 104 | do_fuzz_case(req, i, fuzz) 105 | end 106 | end 107 | 108 | @request.url_variables.each_pair do |k,v| 109 | testcase.rewind 110 | while(testcase.next?) 111 | fuzz = testcase.next 112 | @config.encoders.each do |encoder| 113 | encoded_fuzz = encoder.call(fuzz) 114 | req = Request.new(@request.to_s) 115 | req.query_string = @request.url_variables.merge({k => encoded_fuzz}) 116 | do_fuzz_case(req, req.first_line.index(encoded_fuzz), fuzz) 117 | end 118 | end 119 | end 120 | 121 | if @config.extra_param 122 | @config.extra_param.each_pair do |k,v| 123 | t = Tokenizer.new(v) 124 | t.fuzz_each_token(testcase) do |fuzz_param, i, fuzz| 125 | req = Request.new(@request.to_s) 126 | req.query_string = @request.url_variables.merge({k => fuzz_param}) 127 | do_fuzz_case(req, i, fuzz) 128 | end 129 | end 130 | end 131 | 132 | testcase.rewind 133 | while(testcase.next?) 134 | fuzz = testcase.next 135 | @config.encoders.each do |encoder| 136 | encoded_fuzz = encoder.call(fuzz) 137 | req = Request.new(@request.to_s) 138 | req.query_string = @request.url_variables.merge({encoded_fuzz => '1'}) 139 | do_fuzz_case(req, req.first_line.index(encoded_fuzz), fuzz) 140 | end 141 | end 142 | end 143 | 144 | def post_fuzzer 145 | if @request.post? 146 | if @config.soap 147 | t = Tokenizer.new(@request.body) 148 | t.fuzz_each_token(testcase) do |fuzz_var, i, fuzz| 149 | req = Request.new(@request.to_s) 150 | req.body = fuzz_var 151 | do_fuzz_case(req, i, fuzz) 152 | end 153 | else 154 | @request.body_variables.each_pair do |k,v| 155 | next if @config.csrf_token_regex && k =~ @config.csrf_token_regex 156 | t = Tokenizer.new(v) 157 | t.fuzz_each_token(testcase) do |fuzz_var, i, fuzz| 158 | req = Request.new(@request.to_s) 159 | req.body = req.body_variables.merge({k => fuzz_var}) 160 | do_fuzz_case(req, i, fuzz) 161 | end 162 | end 163 | 164 | @request.body_variables.each_pair do |k,v| 165 | testcase.rewind 166 | while(testcase.next?) 167 | fuzz = testcase.next 168 | @config.encoders.each do |encoder| 169 | encoded_fuzz = encoder.call(fuzz) 170 | req = Request.new(@request.to_s) 171 | req.body = @request.body_variables.merge({k => encoded_fuzz}) 172 | do_fuzz_case(req, req.first_line.index(encoded_fuzz), fuzz) 173 | end 174 | end 175 | end 176 | 177 | testcase.rewind 178 | while(testcase.next?) 179 | fuzz = testcase.next 180 | @config.encoders.each do |encoder| 181 | encoded_fuzz = encoder.call(fuzz) 182 | req = Request.new(@request.to_s) 183 | req.body = req.body_variables.merge({encoded_fuzz => '1'}) 184 | do_fuzz_case(req, req.body.index(encoded_fuzz), fuzz) 185 | end 186 | end 187 | end 188 | end 189 | end 190 | 191 | def rest_fuzzer 192 | t = Tokenizer.new(@request.path) 193 | t.fuzz_each_token(testcase) do |fuzz_path, i, fuzz| 194 | req = Request.new(@request.to_s) 195 | req.path = fuzz_path 196 | do_fuzz_case(req, i, fuzz) 197 | end 198 | end 199 | 200 | def token_fuzzer 201 | t = Tokenizer.new(request.to_s) 202 | t.fuzz_each_token(testcase) do |r, i, f| 203 | do_fuzz_case(Request.new(r).update_content_length, i, f) 204 | end 205 | end 206 | 207 | def verb_fuzzer 208 | testcase.rewind(@request.verb) 209 | while testcase.next? 210 | req = Request.new(@request.to_s) 211 | fuzz = testcase.next 212 | req.verb = fuzz.to_s 213 | do_fuzz_case(req, req.first_line.index(fuzz.to_s), fuzz) 214 | end 215 | end 216 | end 217 | 218 | end 219 | end -------------------------------------------------------------------------------- /lib/ufuzz/http/request.rb: -------------------------------------------------------------------------------- 1 | module UFuzz 2 | module Http 3 | 4 | class Request 5 | attr_accessor :request 6 | 7 | def initialize(req) 8 | @request = req.dup 9 | @config = Config.instance 10 | if @config.fixhost 11 | @request.gsub!(/^[hH]ost: (.+)$/) do |g| 12 | "Host: #{@config.host}" 13 | end 14 | end 15 | end 16 | 17 | def to_s 18 | request 19 | end 20 | 21 | def inspect 22 | "<#{"%x" % self.object_id} #{self.class}> #{request.inspect}" 23 | end 24 | 25 | def post? 26 | first_line =~ /^POST/ 27 | end 28 | 29 | def get? 30 | first_line =~ /^GET/ 31 | end 32 | 33 | def body 34 | request.scan(/(\r\n\r\n|\n\n)([^\r^\n]+)/).flatten.last 35 | end 36 | 37 | def body=(params) 38 | if params.is_a? Hash 39 | params = params.each_pair.map { |k,v| "#{k}=#{v}" }.join('&') 40 | end 41 | @request.gsub!(/(\r\n\r\n|\n\n)([^\r^\n]+)/, "\r\n\r\n" + params) 42 | update_content_length 43 | end 44 | 45 | def first_line 46 | request.split(/\r\n|\n/)[0] 47 | end 48 | 49 | def path 50 | first_line.match(/^[\w]+ ([^ \?]+)/) { |m| m[1] } 51 | end 52 | 53 | def path=(new_path) 54 | @request.gsub!(/\A.*(\r\n|\n)/) do |g| 55 | crlf = $1.dup 56 | if query_string 57 | "#{verb} #{new_path}?#{query_string} HTTP/1.1#{crlf}" 58 | else 59 | "#{verb} #{new_path} HTTP/1.1#{crlf}" 60 | end 61 | end 62 | end 63 | 64 | def uri 65 | first_line.match(/^[\w]+ ([^ ]+)/) { |m| m[1] } 66 | end 67 | 68 | def verb 69 | first_line.match(/^([\w]+) /) { |m| m[1] } 70 | end 71 | 72 | def body_variables 73 | body.split('&').inject({}) do |r, v| 74 | h = v.split('=') 75 | h << '' if h.count < 2 76 | r.merge!({h[0] => h[1]}) if h.count > 1 77 | r 78 | end 79 | end 80 | 81 | def query_string 82 | first_line.match(/.*\?(.*) /) { |m| m[1] } 83 | end 84 | 85 | def headers 86 | req = @request.split(/\r\n\r\n|\n\n/)[0] 87 | req.split(/\r\n|\n/)[1..-1].inject({}) do |r, h| 88 | pair = h.split(": ", 2) 89 | r.merge!({pair[0] => pair[1]}); r 90 | end 91 | end 92 | 93 | def set_header(header, value) 94 | if @request.include? "#{header}: " 95 | @request.gsub!(/^#{header}: .*(\r\n|\n)/) do |g| 96 | "#{header}: #{value}\r\n" 97 | end 98 | else 99 | index = (@request =~ /\r\n\r\n|\n\n/) 100 | @request.insert(index, "\r\n#{header}: #{value}") 101 | end 102 | end 103 | 104 | def query_string=(params) 105 | if params.is_a? Hash 106 | params = params.each_pair.map { |k,v| "#{k}=#{v}" }.join('&') 107 | end 108 | @request.gsub!(/\A.*(\r\n|\n)/) do |g| 109 | crlf = $1.dup 110 | "#{verb} #{path}?#{params} HTTP/1.1#{crlf}" 111 | end 112 | end 113 | 114 | def url_variables 115 | return {} if query_string.nil? 116 | query_string.split('&').inject({}) do |r, v| 117 | h = v.split('=') 118 | h << '' if h.count < 2 119 | r.merge!({h[0] => h[1]}) if h.count > 1 120 | r 121 | end 122 | end 123 | 124 | def verb=(new_verb) 125 | @request.gsub!(/\A[A-Z]+/, new_verb) 126 | end 127 | 128 | def verb_path 129 | "#{verb} #{path}" 130 | end 131 | 132 | def verb_uri 133 | "#{verb} #{uri}" 134 | end 135 | 136 | def summary 137 | "#{first_line} #{@request.length} bytes" 138 | end 139 | 140 | def sanitized_path 141 | verb_uri.gsub(/(.){20,}/, '\1....\1').gsub(/(..){10,}/, '\1....\1') 142 | end 143 | 144 | def update_content_length 145 | if post? 146 | length = @request.split("\r\n\r\n")[1].length rescue nil 147 | length = @request.split("\n\n")[1].length if length.nil? 148 | @request.gsub!(/Content-Length: [0-9]+/i, "Content-Length: #{length}") 149 | end 150 | self 151 | end 152 | 153 | def update_cookies(sess) 154 | @cookies = CookieJar.new(self) 155 | @cookies.merge(sess.session_cookies) 156 | 157 | unless @cookies.empty? 158 | log "cookies: #{@cookies}", TRACE 159 | if @request =~ /Cookie: /i 160 | @request.gsub!(/^Cookie: ([^\r^\n]*)/i, "Cookie: #{@cookies}") 161 | else 162 | apos = (@request =~ /Accept: /i) 163 | @request.insert(apos, "Cookie: #{@cookies}\r\n") if apos && apos > 0 164 | end 165 | end 166 | self 167 | end 168 | 169 | end 170 | 171 | end 172 | end 173 | 174 | 175 | -------------------------------------------------------------------------------- /lib/ufuzz/http/response.rb: -------------------------------------------------------------------------------- 1 | module UFuzz 2 | module Http 3 | 4 | class Response 5 | attr_accessor :headers, :body, :status, :remote_ip, :data 6 | 7 | def initialize(resp) 8 | @headers = resp[:headers] 9 | @body = resp[:body] 10 | @status = resp[:status] 11 | @remote_ip = resp[:remote_ip] 12 | @data = nil 13 | end 14 | 15 | def to_s 16 | return @data if @data 17 | 18 | @data = "HTTP/1.1 #{@status} #{UFuzz::HTTP_ERRORS[@status]}" + UFuzz::CR_NL # FIXME 19 | @data += @headers.each_pair.map do |k,v| 20 | "#{k}: #{v}" 21 | end.join(UFuzz::CR_NL) 22 | @data += UFuzz::CR_NL * 2 23 | @data += @body 24 | end 25 | 26 | def inspect 27 | "<#{"%x" % self.object_id} #{self.class}> @status => #{@status} " + 28 | "@headers => #{@headers.inspect} @body => #{@body.inspect}" 29 | end 30 | 31 | def first_line 32 | "HTTP/1.1 #{@status} #{UFuzz::HTTP_ERRORS[@status]}" 33 | end 34 | 35 | def summary 36 | "#{first_line} #{@body.length} bytes" 37 | end 38 | 39 | def code 40 | @status 41 | end 42 | 43 | def contains(*arr) 44 | arr.each do |m| 45 | if @body && @body =~ m 46 | return $0 47 | end 48 | end 49 | false 50 | end 51 | 52 | def self.parse(socket, req) 53 | config = UFuzz::Config.instance 54 | resp = { 55 | :body => '', 56 | :headers => {}, 57 | :status => -1, 58 | :remote_ip => socket.respond_to?(:remote_ip) && socket.remote_ip 59 | } 60 | 61 | begin 62 | status_line = socket.read(12) 63 | raise SocketError, "no data" unless status_line 64 | 65 | resp[:status] = status_line[9, 11].to_i 66 | socket.readline # read the rest of the status line and CRLF 67 | 68 | until ((data = socket.readline).chop!).empty? 69 | key, value = data.split(/:\s*/, 2) 70 | resp[:headers][key] = ([*resp[:headers][key]] << value).compact.join(', ') 71 | if key.casecmp('Content-Length') == 0 72 | content_length = value.to_i 73 | elsif (key.casecmp('Transfer-Encoding') == 0) && (value.casecmp('chunked') == 0) 74 | transfer_encoding_chunked = true 75 | end 76 | end 77 | 78 | unless req =~ /^CONNECT/ || req =~ /^HEAD/ || UFuzz::NO_ENTITY.include?(resp[:status]) 79 | if transfer_encoding_chunked 80 | while (chunk_size = socket.readline.chop!.to_i(16)) > 0 81 | resp[:body] << socket.read(chunk_size + 2).chop! # 2 == "/r/n".length 82 | end 83 | socket.read(2) # 2 == "/r/n".length 84 | elsif remaining = content_length 85 | while remaining > 0 86 | resp[:body] << socket.read([config.chunk_size, remaining].min) 87 | remaining -= config.chunk_size 88 | end 89 | else 90 | resp[:body] << socket.read 91 | end 92 | end 93 | rescue => e 94 | log "error parsing response - #{e.message}", DEBUG 95 | log e.backtrace, TRACE 96 | end 97 | 98 | self.new(resp) 99 | end 100 | 101 | end 102 | 103 | end 104 | end 105 | 106 | 107 | -------------------------------------------------------------------------------- /lib/ufuzz/http/session.rb: -------------------------------------------------------------------------------- 1 | module UFuzz 2 | module Http 3 | 4 | class Session 5 | attr_accessor :cookies, :config, :connection, :request, :response, :config 6 | 7 | def initialize(c) 8 | @config = c 9 | @connection = c.create_connection 10 | @request = c.create_request 11 | @response = nil 12 | @cookies = CookieJar.new(@request) 13 | end 14 | 15 | def session_cookie 16 | raise NotImplementedError 17 | end 18 | 19 | def login 20 | raise NotImplementedError 21 | end 22 | 23 | def logout 24 | raise NotImplementedError 25 | end 26 | 27 | def check 28 | raise NotImplementedError 29 | end 30 | 31 | def logged_out 32 | raise NotImplementedError 33 | end 34 | 35 | def create 36 | log "#{caller.first.match(/`(.+)'/) { |m| m[1] }} -> creating new session", DEBUG 37 | @response = @connection.send(Request.new(login)) 38 | @cookies.parse @response 39 | end 40 | 41 | def clear 42 | log "#{caller.first.match(/`(.+)'/) { |m| m[1] }} -> invalidating session", DEBUG 43 | @response = @connection.send(Request.new(logout).update_cookies(self)) 44 | @cookies.parse @response 45 | end 46 | 47 | def session_cookies 48 | @cookies.find(session_cookie) 49 | end 50 | 51 | def valid? 52 | req = Request.new(check).update_cookies(self) 53 | resp = @connection.send(req) 54 | 55 | if logged_out.is_a? Integer 56 | return !(resp.status == logged_out) 57 | elsif logged_out.is_a? Regexp 58 | return !(resp.body =~ logged_out) 59 | else 60 | return !(resp.body =~ /#{logged_out}/) 61 | end 62 | true 63 | end 64 | end 65 | 66 | end 67 | end -------------------------------------------------------------------------------- /lib/ufuzz/logger.rb: -------------------------------------------------------------------------------- 1 | require 'time' 2 | require 'singleton' 3 | 4 | CRASH = -1 5 | FAIL = 0 6 | WARN = 1 7 | INFO = 2 8 | DEBUG = 3 9 | TRACE = 4 10 | 11 | def log(message, level = INFO, opts = {}) 12 | UFuzz::Logger.instance.log(message, level, opts) 13 | end 14 | 15 | module UFuzz 16 | 17 | class Logger 18 | include Singleton 19 | 20 | def initialize 21 | @count = 0 22 | @config = nil 23 | @log_level = 2 24 | end 25 | 26 | def lazy_init 27 | @config = Config.instance 28 | @log_level = @config.verbose 29 | 30 | @logdir ||= File.expand_path(File.dirname(__FILE__) + "/../../log") 31 | unless File.directory?(@logdir) 32 | Dir::mkdir(@logdir) 33 | end 34 | 35 | @directory ||= File.expand_path(@logdir + "/#{Process.ppid}#{Process.pid}_#{@config.module}-#{@config.app}_#{timestamp}") 36 | unless File.directory?(@directory) 37 | Dir::mkdir(@directory) 38 | end 39 | end 40 | 41 | def counter 42 | @count += 1 43 | @count = 1 if @@count > 9999 44 | "%04d" % @count 45 | end 46 | 47 | def timestamp 48 | @ts ||= Time.now.inspect.gsub(/ [\-+][0-9]+$/,'').gsub(/[ \/:\-_]/,'') 49 | end 50 | 51 | def filename 52 | @filename ||= @directory + "/fuzz.log" 53 | end 54 | 55 | def log_array(arr, level, opts={}) 56 | arr.each do |msg| 57 | log(msg, level, opts) 58 | end 59 | end 60 | 61 | def log(message, level=INFO, opts={}) 62 | if message.is_a? Array 63 | log_array(message, level, opts) 64 | return nil 65 | elsif message.is_a? Fault 66 | log_crash(message) 67 | return nil 68 | end 69 | 70 | if level <= @log_level 71 | ts = Time.now.iso8601 72 | case level 73 | when CRASH # for target crashes 74 | puts "[#{ts} EVENT DETECTED]".underline.colorize(:light_red) + " #{message}" 75 | log_to_file "[#{ts} EVENT] #{message}" 76 | when FAIL 77 | puts "[#{ts} FAIL]".colorize(:light_red) + " #{message}" 78 | log_to_file "[#{ts} FAIL] #{message}" 79 | when WARN 80 | puts "[#{ts} WARN]".colorize(:light_yellow) + " #{message}" 81 | log_to_file "[#{ts} WARN] #{message}" 82 | when INFO 83 | puts "[#{ts} INFO]".colorize(:light_white) + " #{message}" 84 | log_to_file "[#{ts} INFO] #{message}" 85 | when DEBUG 86 | puts "[#{ts} DEBUG]".colorize(:cyan) + " #{message}" 87 | #log_to_file "[#{ts} DEBUG] #{message}" 88 | when TRACE 89 | puts "[#{ts} TRACE]".colorize(:magenta) + " #{message}" 90 | #log_to_file "[#{ts} TRACE] #{message}" 91 | end 92 | end 93 | end 94 | 95 | def log_to_file(message) 96 | unless @config 97 | begin 98 | lazy_init 99 | rescue => e 100 | #puts e.message 101 | #puts e.backtrace 102 | return 103 | end 104 | end 105 | 106 | if @config.logging[:text] 107 | begin 108 | @logfile ||= File.open(filename, 'a') 109 | rescue Errno::ENOENT => e 110 | unless File.directory?(@directory) 111 | Dir::mkdir(@directory) 112 | retry 113 | end 114 | raise e 115 | end 116 | @logfile.puts message 117 | @logfile.flush 118 | end 119 | end 120 | 121 | def log_crash(fault) 122 | reason = fault.reason || 'unknown' 123 | crash_dir = @directory + "/" + "#{reason.downcase.gsub(/[\/ ]/, '_')}" 124 | unless File.directory?(crash_dir) 125 | Dir::mkdir(crash_dir) 126 | end 127 | 128 | File.open("#{crash_dir}/#{@config.req_summary.to_s.limit(40).downcase.gsub(/[\/ ]/, '_')}.log", 'a') do |f| 129 | f.puts "[#{Time.now.iso8601} EVENT DETECTED] *************************************" + "\n" + 130 | '-' * 80 + "\n#{fault.tx}\n" + '-' * 80 + "\n#{fault.rx.to_s.limit(5000)}\n" + '-' * 80 + 131 | "\n#{fault.crash_dump}\n\n" 132 | end 133 | log fault.pretty_print, CRASH 134 | end 135 | 136 | def close 137 | @logfile.close if @logfile 138 | end 139 | end 140 | 141 | end -------------------------------------------------------------------------------- /lib/ufuzz/monitor.rb: -------------------------------------------------------------------------------- 1 | module UFuzz 2 | 3 | class Monitor 4 | attr_accessor :config, :crash_dump, :session 5 | 6 | def initialize(config) 7 | @config = config 8 | start 9 | end 10 | 11 | def start 12 | end 13 | 14 | def close 15 | end 16 | 17 | def check 18 | end 19 | end 20 | 21 | end -------------------------------------------------------------------------------- /lib/ufuzz/net/socket.rb: -------------------------------------------------------------------------------- 1 | module UFuzz 2 | module Net 3 | 4 | class Socket 5 | 6 | extend Forwardable 7 | 8 | attr_accessor :config 9 | attr_reader :remote_ip 10 | 11 | def_delegators(:@socket, :readline, :readline) 12 | 13 | def initialize(config) 14 | @config = config 15 | @read_buffer = '' 16 | @eof = false 17 | 18 | @config.family ||= ::Socket::Constants::AF_UNSPEC 19 | if @config.proxy_host 20 | @config.proxy_family ||= ::Socket::Constants::AF_UNSPEC 21 | end 22 | 23 | connect 24 | end 25 | 26 | def read(max_length=nil) 27 | if @eof 28 | return nil 29 | elsif @config.nonblock 30 | begin 31 | if max_length 32 | until @read_buffer.length >= max_length 33 | @read_buffer << @socket.read_nonblock(max_length - @read_buffer.length) 34 | end 35 | else 36 | while true 37 | @read_buffer << @socket.read_nonblock(@config.chunk_size) 38 | end 39 | end 40 | rescue OpenSSL::SSL::SSLError => error 41 | if error.message == 'read would block' 42 | if IO.select([@socket], nil, nil, @config.read_timeout) 43 | retry 44 | else 45 | raise(UFuzz::Errors::Timeout.new("read timeout reached")) 46 | end 47 | else 48 | raise(error) 49 | end 50 | rescue Errno::EAGAIN, Errno::EWOULDBLOCK, IO::WaitReadable 51 | if IO.select([@socket], nil, nil, @config.read_timeout) 52 | retry 53 | else 54 | raise(UFuzz::Errors::Timeout.new("read timeout reached")) 55 | end 56 | rescue EOFError 57 | @eof = true 58 | end 59 | if max_length 60 | @read_buffer.slice!(0, max_length) 61 | else 62 | # read until EOFError, so return everything 63 | @read_buffer.slice!(0, @read_buffer.length) 64 | end 65 | else 66 | begin 67 | Timeout.timeout(@config.read_timeout) do 68 | @socket.read(max_length) 69 | end 70 | rescue Timeout::Error 71 | raise UFuzz::Errors::Timeout.new('read timeout reached') 72 | end 73 | end 74 | end 75 | 76 | def write(data) 77 | if @config.nonblock 78 | # We normally return from the return in the else block below, but 79 | # we guard that data is still something in case we get weird 80 | # values and String#[] returns nil. (This behavior has been observed 81 | # in the wild, so this is a simple defensive mechanism) 82 | while data 83 | begin 84 | # I wish that this API accepted a start position, then we wouldn't 85 | # have to slice data when there is a short write. 86 | written = @socket.write_nonblock(data) 87 | rescue OpenSSL::SSL::SSLError => error 88 | if error.message == 'write would block' 89 | if IO.select(nil, [@socket], nil, @config.write_timeout) 90 | retry 91 | else 92 | raise(UFuzz::Errors::Timeout.new("write timeout reached")) 93 | end 94 | else 95 | raise(error) 96 | end 97 | rescue Errno::EAGAIN, Errno::EWOULDBLOCK, IO::WaitWritable 98 | if IO.select(nil, [@socket], nil, @config.write_timeout) 99 | retry 100 | else 101 | raise(UFuzz::Errors::Timeout.new("write timeout reached")) 102 | end 103 | else 104 | # Fast, common case. 105 | # The >= seems weird, why would it have written MORE than we 106 | # requested. But we're getting some weird behavior when @socket 107 | # is an OpenSSL socket, where it seems like it's saying it wrote 108 | # more (perhaps due to SSL packet overhead?). 109 | # 110 | # Pretty weird, but this is a simple defensive mechanism. 111 | return if written >= data.size 112 | 113 | # This takes advantage of the fact that most ruby implementations 114 | # have Copy-On-Write strings. Thusly why requesting a subrange 115 | # of data, we actually don't copy data because the new string 116 | # simply references a subrange of the original. 117 | data = data[written, data.size] 118 | end 119 | end 120 | else 121 | begin 122 | Timeout.timeout(@config.write_timeout) do 123 | @socket.write(data) 124 | end 125 | rescue Timeout::Error 126 | UFuzz::Errors::Timeout.new('write timeout reached') 127 | end 128 | end 129 | end 130 | 131 | def close 132 | @socket.close unless @socket.closed? 133 | rescue => e 134 | puts e.message 135 | puts e.backtrace 136 | end 137 | 138 | private 139 | 140 | def connect 141 | @socket = nil 142 | exception = nil 143 | 144 | addrinfo = if @config.proxy_host 145 | ::Socket.getaddrinfo(@config.proxy_host, @config.proxy_port, @config.proxy_family, ::Socket::Constants::SOCK_STREAM) 146 | else 147 | ::Socket.getaddrinfo(@config.host, @config.port, @config.family, ::Socket::Constants::SOCK_STREAM) 148 | end 149 | 150 | addrinfo.each do |_, port, _, ip, a_family, s_type| 151 | @remote_ip = ip 152 | 153 | # nonblocking connect 154 | begin 155 | sockaddr = ::Socket.sockaddr_in(port, ip) 156 | 157 | socket = ::Socket.new(a_family, s_type, 0) 158 | 159 | if @config.nonblock 160 | socket.connect_nonblock(sockaddr) 161 | else 162 | begin 163 | Timeout.timeout(@config.connect_timeout) do 164 | socket.connect(sockaddr) 165 | end 166 | rescue Timeout::Error 167 | raise UFuzz::Errors::Timeout.new('connect timeout reached') 168 | end 169 | end 170 | 171 | @socket = socket 172 | break 173 | rescue Errno::EINPROGRESS 174 | unless IO.select(nil, [socket], nil, @config.connect_timeout) 175 | raise(UFuzz::Errors::Timeout.new("connect timeout reached")) 176 | end 177 | begin 178 | socket.connect_nonblock(sockaddr) 179 | 180 | @socket = socket 181 | break 182 | rescue Errno::EISCONN 183 | @socket = socket 184 | break 185 | rescue SystemCallError => exception 186 | socket.close 187 | next 188 | end 189 | rescue SystemCallError => exception 190 | socket.close if socket 191 | next 192 | end 193 | end 194 | 195 | unless @socket 196 | # this will be our last encountered exception 197 | raise exception 198 | end 199 | 200 | if @config.tcp_nodelay 201 | @socket.setsockopt(::Socket::IPPROTO_TCP, 202 | ::Socket::TCP_NODELAY, 203 | true) 204 | end 205 | end 206 | end 207 | 208 | end 209 | end -------------------------------------------------------------------------------- /lib/ufuzz/net/ssl_socket.rb: -------------------------------------------------------------------------------- 1 | module UFuzz 2 | module Net 3 | 4 | class SSLSocket < Socket 5 | 6 | def initialize(config = {}) 7 | @config = config 8 | check_nonblock_support 9 | 10 | super 11 | 12 | # create ssl context 13 | if config.ssl_version 14 | ssl_context = OpenSSL::SSL::SSLContext.new(config.ssl_version) 15 | else 16 | ssl_context = OpenSSL::SSL::SSLContext.new 17 | end 18 | 19 | if @config.ssl_verify_peer 20 | # turn verification on 21 | ssl_context.verify_mode = OpenSSL::SSL::VERIFY_PEER 22 | 23 | if ca_path = ENV['SSL_CERT_DIR'] || @config.ssl_ca_path 24 | ssl_context.ca_path = ca_path 25 | elsif ca_file = ENV['SSL_CERT_FILE'] || @config.ssl_ca_file 26 | ssl_context.ca_file = ca_file 27 | else # attempt default, fallback to bundled 28 | ssl_context.cert_store = OpenSSL::X509::Store.new 29 | ssl_context.cert_store.set_default_paths 30 | ssl_context.cert_store.add_file(DEFAULT_CA_FILE) 31 | end 32 | else 33 | # turn verification off 34 | ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE 35 | end 36 | 37 | # maintain existing API 38 | certificate_path = @config.client_cert || @config.certificate_path 39 | private_key_path = @config.client_key || @config.private_key_path 40 | 41 | if certificate_path && private_key_path 42 | ssl_context.cert = OpenSSL::X509::Certificate.new(File.read(certificate_path)) 43 | ssl_context.key = OpenSSL::PKey::RSA.new(File.read(private_key_path)) 44 | elsif @config.certificate && @config.private_key 45 | ssl_context.cert = OpenSSL::X509::Certificate.new(@config.certificate) 46 | ssl_context.key = OpenSSL::PKey::RSA.new(@config.private_key) 47 | end 48 | 49 | if @config.proxy_host 50 | request = 'CONNECT ' << @config.host << ':' << @config.port << UFuzz::HTTP_1_1 51 | request << 'Host: ' << @config.host << ':' << @config.port << UFuzz::CR_NL 52 | 53 | if @config.proxy_password || @config.proxy_user 54 | auth = ['' << @config.proxy_user.to_s << ':' << @config.proxy_password.to_s].pack('m').delete(UFuzz::CR_NL) 55 | request << "Proxy-Authorization: Basic " << auth << UFuzz::CR_NL 56 | end 57 | 58 | request << 'Proxy-Connection: Keep-Alive' << UFuzz::CR_NL 59 | 60 | request << UFuzz::CR_NL 61 | 62 | # write out the proxy setup request 63 | @socket.write(request) 64 | 65 | # eat the proxy's connection response 66 | UFuzz::Http::Response.parse(@socket, request) 67 | end 68 | 69 | # convert Socket to OpenSSL::SSL::SSLSocket 70 | @socket = OpenSSL::SSL::SSLSocket.new(@socket, ssl_context) 71 | @socket.sync_close = true 72 | @socket.connect 73 | 74 | # Server Name Indication (SNI) RFC 3546 75 | if @socket.respond_to?(:hostname=) 76 | @socket.hostname = @config.host 77 | end 78 | 79 | # verify connection 80 | if @config.ssl_verify_peer 81 | @socket.post_connection_check(@config.host) 82 | end 83 | 84 | @socket 85 | end 86 | 87 | def read(max_length=nil) 88 | check_nonblock_support 89 | super 90 | end 91 | 92 | def write(data) 93 | check_nonblock_support 94 | super 95 | end 96 | 97 | private 98 | 99 | def check_nonblock_support 100 | # backwards compatability for things lacking nonblock 101 | if !UFuzz::DEFAULT_NONBLOCK && @config.nonblock 102 | log "socket nonblock is not supported by your OpenSSL::SSL::SSLSocket", WARN 103 | @config.nonblock = false 104 | elsif UFuzz::DEFAULT_NONBLOCK 105 | @config.nonblock = true 106 | end 107 | end 108 | 109 | def connect 110 | check_nonblock_support 111 | super 112 | end 113 | 114 | end 115 | 116 | end 117 | end -------------------------------------------------------------------------------- /lib/ufuzz/tag_splitter.rb: -------------------------------------------------------------------------------- 1 | module UFuzz 2 | 3 | class TagSplitter 4 | attr_accessor :tokens 5 | 6 | def initialize(str) 7 | @tokens = [] 8 | @regex = />.*?#{fuzz}<"].each do |f| 29 | t = @tokens.dup 30 | t[i] = f 31 | yield Tokenizer.tok_to_string(t), i, fuzz 32 | end 33 | end 34 | 35 | def self.tok_to_string(tok) 36 | tok.map { |t| t }.join('') 37 | end 38 | end 39 | 40 | end 41 | -------------------------------------------------------------------------------- /lib/ufuzz/testcase/buffer_test.rb: -------------------------------------------------------------------------------- 1 | module UFuzz 2 | 3 | class BufferTest < TestCase 4 | def initialize(opts = {}) 5 | @series = opts[:series] || 'A' 6 | @start = opts[:start] || 32 7 | @step = opts[:step] || :exponential 8 | @limit = opts[:limit] || 18000 9 | super 10 | end 11 | 12 | def threadable? 13 | false 14 | end 15 | 16 | def test(content) 17 | @monitor.check ? Fault.new('buffer overflow', 'possible buffer overflow', @monitor.crash_dump) : nil 18 | end 19 | 20 | def update_transforms 21 | @transforms = [ proc {|a| a.join.to_s } ] 22 | end 23 | 24 | private 25 | 26 | def init_block 27 | if @series.respond_to? :each 28 | @repeatables = @series 29 | else 30 | @repeatables = Array(@series) 31 | end 32 | 33 | @block = Fiber.new do 34 | @repeatables.each do |r| 35 | if @step == :exponential 36 | n = 0 37 | loop do 38 | i = @start + (2 ** n + 1) 39 | i = @limit if i > @limit 40 | Fiber.yield( @transforms.inject(Array.new(i,r)) { |v,proc| v = proc.call(v) } ) 41 | break if i == @limit 42 | n += 1 43 | end 44 | else 45 | (@start..@limit).step(@step) do |i| 46 | next if i == 0 47 | Fiber.yield( @transforms.inject(Array.new(i,r)) { |v,proc| v = proc.call(v) } ) 48 | end 49 | end 50 | end 51 | nil 52 | end 53 | end 54 | 55 | end 56 | 57 | end -------------------------------------------------------------------------------- /lib/ufuzz/testcase/cmd_test.rb: -------------------------------------------------------------------------------- 1 | module UFuzz 2 | 3 | class CmdTest < TestCase 4 | def initialize(opts = {}) 5 | @limit = opts[:limit] || -1 6 | @vals = File.read(File.expand_path(File.join( 7 | File.dirname(__FILE__), '..', 'wordlist', 'cmd_injection.txt'))) 8 | 9 | super 10 | end 11 | 12 | def threadable? 13 | true 14 | end 15 | 16 | def test(content) 17 | delay = Time.now.to_f - @time 18 | if content && content.to_s.length > 20 && delay > Config.instance.detect_delay 19 | Fault.new('cmd injection', "possible cmd injection - #{@current.inspect}: delay #{delay}") 20 | else 21 | nil 22 | end 23 | end 24 | 25 | def update_transforms 26 | @transforms = [ proc {|a| a.to_s } ] 27 | add_context_transforms 28 | end 29 | 30 | private 31 | 32 | def init_block 33 | @block = Fiber.new do 34 | @vals.each_line do |val| 35 | begin 36 | val = eval('"' + val + '"') 37 | rescue SyntaxError 38 | log "Error in wordlist: #{val.inspect}", WARN 39 | end 40 | val.chomp! 41 | @transforms.each do |t| 42 | Fiber.yield( t.call(val) ) 43 | end 44 | end 45 | nil 46 | end 47 | end 48 | 49 | end 50 | 51 | end -------------------------------------------------------------------------------- /lib/ufuzz/testcase/fmt_test.rb: -------------------------------------------------------------------------------- 1 | module UFuzz 2 | 3 | class FmtTest < TestCase 4 | def initialize(opts = {}) 5 | @limit = opts[:limit] || -1 6 | @vals = File.read(File.expand_path(File.join( 7 | File.dirname(__FILE__), '..', 'wordlist', 'format_string.txt'))) 8 | 9 | super 10 | end 11 | 12 | def threadable? 13 | false 14 | end 15 | 16 | def test(content) 17 | @monitor.check ? Fault.new('format string', "possible fmt string vuln - #{@current.inspect}", @monitor.crash_dump) : nil 18 | end 19 | 20 | def update_transforms 21 | @transforms = [ proc {|a| a.to_s } ] 22 | add_context_transforms 23 | end 24 | 25 | private 26 | 27 | def init_block 28 | @block = Fiber.new do 29 | @vals.each_line do |val| 30 | begin 31 | val = eval('"' + val + '"') 32 | rescue SyntaxError 33 | log "Error in wordlist: #{val.inspect}", WARN 34 | end 35 | val.chomp! 36 | @transforms.each do |t| 37 | Fiber.yield( t.call(val) ) 38 | end 39 | end 40 | nil 41 | end 42 | end 43 | 44 | end 45 | 46 | end -------------------------------------------------------------------------------- /lib/ufuzz/testcase/integer_test.rb: -------------------------------------------------------------------------------- 1 | module UFuzz 2 | 3 | class IntegerTest < TestCase 4 | def initialize(opts = {}) 5 | @series = opts[:series] || 'A' 6 | @start = opts[:start] || 16 7 | @step = opts[:step] || :exponential 8 | @limit = opts[:limit] || 32768 9 | super 10 | end 11 | 12 | def threadable? 13 | false 14 | end 15 | 16 | def test(content) 17 | @monitor.check ? Fault.new('integer overflow', 'possible integer overflow', @monitor.crash_dump) : nil 18 | end 19 | 20 | def update_transforms 21 | @transforms = [ proc {|a| a.to_s } ] 22 | if @data.match(/^[A-Z0-9]+$/) 23 | @transforms << proc {|a| ("%x" % a.to_i).upcase } 24 | elsif @data.match(/^[a-z0-9]+$/) 25 | @transforms << proc {|a| "%x" % a.to_i } 26 | end 27 | end 28 | 29 | private 30 | 31 | def init_block 32 | cases = [] 33 | 34 | [ 32 ].each do |bitlength| 35 | # full and empty 36 | cases << ('1' * bitlength).to_i(2) 37 | cases << ('0' * bitlength).to_i(2) 38 | 39 | # flip up to 4 bits at each end 40 | # depending on bitlength 41 | case 42 | when bitlength > 32 43 | lim = 4 44 | when (16..32) === bitlength 45 | lim = 3 46 | when (8..15) === bitlength 47 | lim = 2 48 | else 49 | lim = 1 50 | end 51 | 52 | for i in (1..lim) do 53 | cases << (('1' * i) + ('0' * (bitlength - i))).to_i(2) 54 | cases << (('0' * (bitlength - i)) + ('1' * i)).to_i(2) 55 | cases << (('0' * i) + ('1' * (bitlength - i))).to_i(2) 56 | cases << (('1' * (bitlength - i)) + ('0' * i)).to_i(2) 57 | end 58 | 59 | # alternating 60 | cases << ('1'*bitlength).gsub(/11/,"10").to_i(2) 61 | cases << ('0'*bitlength).gsub(/00/,"01").to_i(2) 62 | end 63 | 64 | @block = Fiber.new do 65 | # The call to uniq avoids repeated elements 66 | # when bitlength < 4 67 | cases.uniq.each { |c| Fiber.yield c } 68 | nil 69 | end 70 | end 71 | 72 | end 73 | 74 | end -------------------------------------------------------------------------------- /lib/ufuzz/testcase/path_test.rb: -------------------------------------------------------------------------------- 1 | module UFuzz 2 | 3 | class PathTest < TestCase 4 | def initialize(opts = {}) 5 | @limit = opts[:limit] || -1 6 | @vals = File.read(File.expand_path(File.join( 7 | File.dirname(__FILE__), '..', 'wordlist', 'path_traversal.txt'))) 8 | 9 | super 10 | end 11 | 12 | def threadable? 13 | true 14 | end 15 | 16 | def test(content) 17 | Config.instance.traversal_match.each do |regex| 18 | if content.to_s =~ regex 19 | return Fault.new('path traversal', "possible path traversal - #{@current.inspect}: found #{regex.inspect}") 20 | end 21 | end 22 | nil 23 | end 24 | 25 | def update_transforms 26 | @transforms = [ proc {|a| a.to_s } ] 27 | add_context_transforms 28 | end 29 | 30 | private 31 | 32 | def init_block 33 | @block = Fiber.new do 34 | @vals.each_line do |val| 35 | begin 36 | val = eval('"' + val + '"') 37 | rescue SyntaxError 38 | log "Error in wordlist: #{val.inspect}", WARN 39 | end 40 | val.chomp! 41 | @transforms.each do |t| 42 | Fiber.yield( t.call(val) ) 43 | end 44 | end 45 | nil 46 | end 47 | end 48 | end 49 | 50 | end -------------------------------------------------------------------------------- /lib/ufuzz/testcase/sqli_test.rb: -------------------------------------------------------------------------------- 1 | module UFuzz 2 | 3 | class SqliTest < TestCase 4 | def initialize(opts = {}) 5 | @limit = opts[:limit] || -1 6 | @vals = File.read(File.expand_path(File.join( 7 | File.dirname(__FILE__), '..', 'wordlist', 'sql_injection.txt'))) 8 | 9 | super 10 | end 11 | 12 | def threadable? 13 | true 14 | end 15 | 16 | def test(content) 17 | delay = Time.now.to_f - @time 18 | if delay >= Config.instance.detect_delay 19 | Fault.new('sql injection', "possible sql injection - #{@current.inspect}: delay #{delay}") 20 | else 21 | nil 22 | end 23 | end 24 | 25 | def update_transforms 26 | @transforms = [ proc {|a| a.to_s } ] 27 | add_context_transforms 28 | end 29 | 30 | private 31 | 32 | def init_block 33 | @block = Fiber.new do 34 | @vals.each_line do |val| 35 | val.chomp! 36 | @transforms.each do |t| 37 | Fiber.yield( t.call(val) ) 38 | end 39 | end 40 | nil 41 | end 42 | end 43 | end 44 | 45 | end -------------------------------------------------------------------------------- /lib/ufuzz/testcase/testcase.rb: -------------------------------------------------------------------------------- 1 | module UFuzz 2 | 3 | class TestCase 4 | def initialize(opts = {}) 5 | @opts = opts 6 | @monitor = opts[:monitor] 7 | @time = Time.now.to_f 8 | @data = opts[:data].to_s 9 | 10 | raise(SyntaxError, "A monitor must be supplied") if @monitor.nil? 11 | 12 | update_transforms 13 | init_block 14 | @alive = true 15 | @cache = @block.resume 16 | end 17 | 18 | def next? 19 | @alive 20 | end 21 | 22 | def finished? 23 | !@alive 24 | end 25 | 26 | def next 27 | @current = @cache 28 | begin 29 | @cache = @block.resume 30 | rescue FiberError 31 | @cache = false 32 | end 33 | @alive = false unless @cache 34 | @time = Time.now.to_f 35 | self 36 | end 37 | 38 | def to_s 39 | @current.to_s 40 | end 41 | 42 | def inspect 43 | self.class 44 | end 45 | 46 | def to_a 47 | a = [] 48 | a << self.next while self.next? 49 | self.rewind 50 | a 51 | end 52 | 53 | def each(&blk) 54 | blk.yield self.next while self.next? 55 | self.rewind 56 | end 57 | 58 | def rewind(data = nil) 59 | initialize(@opts.merge(:data => data)) 60 | true 61 | end 62 | 63 | def threadable? 64 | true 65 | end 66 | 67 | def test(content) 68 | raise NotImplementedError 69 | end 70 | 71 | def update_transforms 72 | end 73 | 74 | def init_block 75 | end 76 | 77 | def add_context_transforms 78 | if @data.match(/^[0-9\.\-]+$/) 79 | @transforms 80 | elsif @data.match(/^[A-Z0-9]+$/) && @data.length > 7 81 | @transforms << proc {|a| a.to_s.hexify.upcase } 82 | elsif @data.match(/^[a-z0-9]+$/) && @data.length > 7 83 | @transforms << proc {|a| a.to_s.hexify } 84 | elsif @data.match(/^[a-zA-Z0-9+\/]+={0,2}$/) && @data.length > 11 85 | @transforms << proc {|a| a.to_s.b64 } 86 | end 87 | end 88 | end 89 | 90 | class TestCaseChain < TestCase 91 | def initialize(*test_cases) 92 | @opts = test_cases 93 | @test_cases = test_cases 94 | 95 | init_block 96 | 97 | @alive = true 98 | @cache = @block.resume 99 | end 100 | 101 | def rewind(data=nil) 102 | @test_cases.each { |t| t.rewind(data) } 103 | initialize(*@opts) 104 | true 105 | end 106 | 107 | def next 108 | @current = @cache 109 | begin 110 | @cache = @block.resume 111 | rescue FiberError 112 | @cache = false 113 | end 114 | @alive = false unless @cache 115 | @current 116 | end 117 | 118 | def init_block 119 | @block = Fiber.new do 120 | @test_cases.each do |t| 121 | while t.next? 122 | Fiber.yield t 123 | t.next 124 | end 125 | end 126 | false 127 | end 128 | end 129 | end 130 | 131 | end -------------------------------------------------------------------------------- /lib/ufuzz/testcase/xxe_test.rb: -------------------------------------------------------------------------------- 1 | module UFuzz 2 | 3 | class XxeTest < TestCase 4 | def initialize(opts = {}) 5 | @limit = opts[:limit] || -1 6 | @vals = File.read(File.expand_path(File.join( 7 | File.dirname(__FILE__), '..', 'wordlist', 'xxe.txt'))) 8 | 9 | super 10 | end 11 | 12 | def threadable? 13 | true 14 | end 15 | 16 | def test(content) 17 | Config.instance.traversal_match.each do |regex| 18 | if content.to_s =~ regex 19 | return Fault.new('xml external entity injection', "possible xxe injection - #{@current.inspect}: found #{regex.inspect}") 20 | end 21 | end 22 | @monitor.check # has a nasty habit of crashing embedded xml parsers 23 | nil 24 | end 25 | 26 | def update_transforms 27 | @transforms = [ proc { |a| a.to_s } ] 28 | add_context_transforms 29 | end 30 | 31 | private 32 | 33 | def init_block 34 | @block = Fiber.new do 35 | @vals.each_line do |val| 36 | begin 37 | val = eval('"' + val + '"') 38 | rescue SyntaxError 39 | log "Error in wordlist: #{val.inspect}", WARN 40 | end 41 | val.chomp! 42 | @transforms.each do |t| 43 | Fiber.yield( t.call(val) ) 44 | end 45 | end 46 | nil 47 | end 48 | end 49 | end 50 | 51 | end -------------------------------------------------------------------------------- /lib/ufuzz/tokenizer.rb: -------------------------------------------------------------------------------- 1 | module UFuzz 2 | class Tokenizer 3 | attr_accessor :tokens 4 | 5 | def initialize(str) 6 | @tokens = [] 7 | @regex = /[@"\/\:=\[\]&,\r\n\|;\*\?\)\(\{\}\\ ]/ 8 | 9 | prev = '' 10 | (0).upto(str.length-1) do |n| 11 | if str[n] =~ @regex 12 | @tokens << prev unless prev.empty? 13 | @tokens << str[n] 14 | prev = '' 15 | else 16 | prev += str[n] 17 | end 18 | end 19 | @tokens << prev if prev 20 | 21 | if @tokens =~ /(%[0-9a-f][0-9a-f])/i 22 | @tokens.map! { |t| t.split(/(%[0-9a-f][0-9a-f])/i) } 23 | @tokens.flatten! 24 | end 25 | 26 | @tokens 27 | end 28 | 29 | def fuzz_each_token(testcase, opts = {}, &block) 30 | @tokens.each_with_index do |t,i| 31 | next if t =~ @regex || t =~ /%[0-9a-f][0-9a-f]/i #|| i < 20 32 | testcase.rewind(t) 33 | while(testcase.next?) 34 | f = testcase.next 35 | fuzz_positions(t, i, f, opts, &block) 36 | end 37 | end 38 | end 39 | 40 | def to_s 41 | @tokens.map { |t| t }.join('') 42 | end 43 | 44 | def fuzz_positions(tok, i, fuzz, opts) 45 | Config.instance.encoders.each do |encode| 46 | fuzz_val = encode.call(fuzz) 47 | ["#{fuzz_val}", "#{tok}#{fuzz_val}", "#{fuzz_val}#{tok}"].each do |f| 48 | t = @tokens.dup 49 | t[i] = f 50 | yield Tokenizer.tok_to_string(t), i, fuzz 51 | end 52 | end 53 | end 54 | 55 | def self.tok_to_string(tok) 56 | tok.map { |t| t }.join('') 57 | end 58 | end 59 | end -------------------------------------------------------------------------------- /lib/ufuzz/upnp/fuzzer.rb: -------------------------------------------------------------------------------- 1 | require 'pp' 2 | 3 | module UFuzz 4 | module UPnP 5 | 6 | class Fuzzer < UFuzz::Http::Fuzzer 7 | def run 8 | @upnp = Request.new 9 | @upnp.msearch(@config.msearch_timeout) 10 | @upnp.enumerate 11 | 12 | request_count = @upnp.generate_requests.count 13 | 14 | @upnp.generate_requests.each_with_index do |req, index| 15 | next if @config.skip_urls && req =~ @config.skip_urls 16 | host_header = req.match(/^Host: (.+)$/i)[1].strip 17 | host, port = host_header.split(':', 2) 18 | @config.host = host 19 | @config.port = port.to_i 20 | 21 | log "fuzzing host #{host_header} with request:\n#{req.gsub(/(\$PARAM_[0-9]+_\$)/, 'PARAM')}", INFO 22 | @request = UFuzz::Http::Request.new(req) 23 | soap_fuzzer 24 | 25 | @request = UFuzz::Http::Request.new(req.gsub(/(\$PARAM_[0-9]+_\$)/, '1')) 26 | if @config.fuzzers.include?('token') && index == (request_count-1) 27 | log "running token fuzzer on last request only", INFO 28 | token_fuzzer 29 | end 30 | end 31 | end 32 | 33 | def soap_fuzzer 34 | @request.to_s.scan(/(\$PARAM_[0-9]+_\$)/) do |params| 35 | params.each do |param_id| 36 | @testcase.rewind('') 37 | while(@testcase.next?) 38 | fuzz = @testcase.next 39 | @config.encoders.each do |encoder| 40 | encoded_fuzz = encoder.call(fuzz) 41 | temp_request = @request.to_s.gsub(param_id, encoded_fuzz).gsub(/(\$PARAM_[0-9]+_\$)/, '1') 42 | req = UFuzz::Http::Request.new(temp_request) 43 | req.update_content_length 44 | do_fuzz_case(req, 0, fuzz) 45 | end 46 | end 47 | end 48 | end 49 | end 50 | end 51 | 52 | end 53 | end -------------------------------------------------------------------------------- /lib/ufuzz/upnp/request.rb: -------------------------------------------------------------------------------- 1 | module UFuzz 2 | module UPnP 3 | 4 | class Request 5 | attr_accessor :ip, :port, :csock, :ssock, :enum_hosts 6 | 7 | DEFAULT_IP = "239.255.255.250" 8 | DEFAULT_PORT = 1900 9 | UPNP_VERSION = '1.0' 10 | MAX_RECV = 8192 11 | VERBOSE = nil 12 | UNIQ = nil 13 | DEBUG = nil 14 | LOG_FILE = nil 15 | IFACE = nil 16 | 17 | def initialize(ip = nil, port = nil, iface = nil, app_cmds = []) 18 | @msearch_headers = { 19 | 'MAN' => '"ssdp:discover"', 20 | 'MX' => '2' 21 | } 22 | 23 | init_sockets(ip, port, iface) 24 | @http_headers = [] 25 | @enum_hosts = {} 26 | @soap_end = /<\/.*:envelope>/ 27 | end 28 | 29 | def init_sockets(ip, port, iface) 30 | @csock.close if @csock && !@csock.closed? 31 | @ssock.close if @ssock && !@ssock.closed? 32 | 33 | @iface = iface 34 | @ip = (ip || DEFAULT_IP) 35 | @port = (port || DEFAULT_PORT) 36 | 37 | @csock = UDPSocket.open 38 | @csock.setsockopt Socket::IPPROTO_IP, Socket::IP_TTL, 2 39 | 40 | @ssock = UDPSocket.open 41 | @ssock.setsockopt Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1 42 | @ssock.setsockopt Socket::IPPROTO_IP, Socket::IP_ADD_MEMBERSHIP, 43 | IPAddr.new(@ip).hton + Socket.gethostbyname(Socket.gethostname)[3] 44 | @ssock.bind Socket::INADDR_ANY, @port 45 | end 46 | 47 | def cleanup 48 | @csock.close 49 | @ssock.close 50 | end 51 | 52 | def send_udp(data, socket = nil) 53 | socket = @csock unless socket 54 | socket.send(data, 0, @ip, @port) 55 | end 56 | 57 | def listen_udp(size, socket = nil) 58 | socket = @ssock unless socket 59 | socket.recv(size) 60 | end 61 | 62 | def local_ip 63 | IPAddr.new_ntoh(Socket.gethostbyname(Socket.gethostname)[3]).to_s 64 | end 65 | 66 | def create_new_listener(ip = local_ip, port = DEFAULT_PORT) 67 | newsock = UDPSocket.new(Socket::AF_INET) 68 | newsock.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1) 69 | newsock.bind(ip, port) 70 | newsock 71 | end 72 | 73 | def listener 74 | @ssock 75 | end 76 | 77 | def sender 78 | @csock 79 | end 80 | 81 | def parse_url(url) 82 | delim = '://' 83 | host = nil 84 | page = nil 85 | 86 | begin 87 | (host,page) = url.split(delim)[1].split('/', 2) 88 | page = '/' + page 89 | rescue 90 | #If '://' is not in the url, then it's not a full URL, so assume that it's just a relative path 91 | page = url 92 | end 93 | 94 | [host, page] 95 | end 96 | 97 | def parse_device_type_name(str) 98 | delim1 = 'device:' 99 | delim2 = ':' 100 | 101 | if str.include?(delim1) && !str.end_with?(delim2) 102 | str.split(delim1)[1].split(delim2, 2)[0] 103 | else 104 | nil 105 | end 106 | end 107 | 108 | def parse_service_type_name(str) 109 | delim1 = 'service:' 110 | delim2 = ':' 111 | 112 | if str.include?(delim1) && !str.end_with?(delim2) 113 | str.split(delim1)[1].split(delim2, 2)[0] 114 | else 115 | nil 116 | end 117 | end 118 | 119 | def parse_header(data, header) 120 | delimiter = "#{header}:" 121 | 122 | lower_delim = delimiter.downcase 123 | data_array = data.split("\r\n") 124 | 125 | #Loop through each line of the headers 126 | data_array.each do |line| 127 | lower_line = line.downcase 128 | #Does this line start with the header we're looking for? 129 | if lower_line.start_with?(lower_delim) 130 | return line.split(':', 2)[1].strip 131 | end 132 | end 133 | nil 134 | end 135 | 136 | def extract_single_tag(data, tag) 137 | start_tag = "<%s" % tag 138 | end_tag = "" % tag 139 | 140 | tmp = data.split(start_tag)[1] 141 | index = tmp.index('>') 142 | if index 143 | index += 1 144 | return tmp[index..-1].split(endTag)[0].strip 145 | end 146 | rescue Exception => e 147 | puts e.message 148 | puts e.backtrace 149 | nil 150 | end 151 | 152 | def parse_ssdp_info(data) 153 | host_found = nil 154 | message_type = nil 155 | xml_file = nil 156 | host = nil 157 | upnp_type = nil 158 | known_headers = { 159 | "NOTIFY" => 'notification', 160 | "HTTP/1.1 200 OK" => 'reply' 161 | } 162 | 163 | known_headers.each_pair do |text, msg_type| 164 | if data.upcase.start_with? text 165 | message_type = msg_type 166 | break 167 | end 168 | end 169 | 170 | return nil unless message_type 171 | 172 | #Get the host name and location of it's main UPNP XML file 173 | xml_file = parse_header(data, 'LOCATION') 174 | upnp_type = parse_header(data, 'SERVER') 175 | host, page = parse_url(xml_file) 176 | 177 | #Sanity check to make sure we got all the info we need 178 | unless xml_file && host && page 179 | puts "Error parsing header:" 180 | puts data 181 | return nil 182 | end 183 | 184 | #Get the protocol in use (i.e., http, https, etc) 185 | protocol = xml_file.split('://')[0] + '://' 186 | 187 | #Check if we've seen this host before; add to the list of hosts if: 188 | # 1. This is a new host 189 | # 2. We've already seen this host, but the uniq hosts setting is disabled 190 | @enum_hosts.each_pair do |host_id, host_info| 191 | if host_info[:name] == host 192 | host_found = true 193 | return nil 194 | end 195 | end 196 | 197 | index = @enum_hosts.length 198 | @enum_hosts[index] = { 199 | name: host, 200 | data_complete: nil, 201 | proto: protocol, 202 | xml_file: xml_file, 203 | server_type: nil, 204 | upnp_server: upnp_type, 205 | device_list: {} 206 | } 207 | 208 | log "SSDP #{message_type} message from #{host}", INFO 209 | log("XML file is located at #{xml_file}", INFO) if xml_file 210 | log("Device is running #{upnp_type}", INFO) if upnp_type 211 | end 212 | 213 | def get_xml(url) 214 | headers = { 215 | 'USER-AGENT' => "uPNP/#{UPNP_VERSION}", 216 | 'CONTENT-TYPE' => 'text/xml; charset="utf-8"' 217 | } 218 | 219 | resp = ::Net::HTTP.get_response(URI(url)) 220 | output = resp.body 221 | headers = {} 222 | resp.to_hash.each_pair { |k,v| headers[k] = (v.first rescue '') } 223 | [headers, output] 224 | end 225 | 226 | def build_soap_request(hostname, service_type, control_url, action_name, action_args = {}) 227 | arg_list = '' 228 | 229 | if control_url.include? '://' 230 | url_array = control_url.split('/', 4) 231 | if url_array.length < 4 232 | control_url = '/' 233 | else 234 | control_url = '/' + url_array[3] 235 | end 236 | end 237 | 238 | soap_request = "POST #{control_url} HTTP/1.1\r\n" 239 | 240 | if hostname.include? ':' 241 | hostname_array = hostname.split(':') 242 | host = hostname_array[0] 243 | port = hostname_array[1].to_i 244 | else 245 | host = hostname 246 | port = 80 247 | end 248 | 249 | action_args.each_pair do |arg, value| 250 | arg_list += "<#{arg}>#{value}" 251 | end 252 | 253 | soap_body = <<-EOS 254 | 255 | 256 | 257 | 258 | #{arg_list} 259 | 260 | 261 | 262 | EOS 263 | 264 | headers = { 265 | 'Content-Type' => 'text/xml; charset="utf-8"', 266 | 'SOAPACTION' => "\"#{service_type}##{action_name}\"", 267 | 'Content-Length' => soap_body.length, 268 | 'HOST' => hostname, 269 | 'User-Agent' => 'CyberGarage-HTTP/1.0', 270 | } 271 | 272 | headers.each_pair do |head, value| 273 | soap_request += "#{head}: #{value}\r\n" 274 | end 275 | soap_request += "\r\n#{soap_body}" 276 | 277 | soap_request 278 | end 279 | 280 | def send_soap(hostname, service_type, control_url, action_name, action_args) 281 | soap_response = '' 282 | 283 | soap_request = build_soap_request(hostname, service_type, control_url, action_name, action_args) 284 | 285 | sock = TCPSocket.new host, port 286 | sock.write(soap_request) 287 | 288 | data = '' 289 | begin 290 | Timeout.timeout(10) do 291 | loop { 292 | data = (sock.readpartial(1024) rescue nil) 293 | break if data.nil? 294 | soap_response += data 295 | break if soap_response =~ @soap_end 296 | } 297 | end 298 | sock.close 299 | 300 | header, body = soap_response.split("\r\n\r\n", 2) 301 | if !header.upcase.start_with?('HTTP/1.1 200') 302 | puts "SOAP request failed with error code: #{header.split("\r\n")[0].split(' ', 2)[1]}" 303 | error_msg = extract_single_tag(body, 'errorDescription') 304 | puts "SOAP error message: #{error_msg}" if error_msg 305 | return nil 306 | else 307 | return body 308 | end 309 | rescue Exception => e 310 | log "caught exception in send_soap:", FAIL 311 | puts e.message 312 | puts e.backtrace 313 | sock.close 314 | return nil 315 | end 316 | end 317 | 318 | def get_host_info(xml_data, xml_headers, index) 319 | if index >= 0 && index < @enum_hosts.length 320 | xml_root = Nokogiri::XML.parse(xml_data).root 321 | parse_device_info(xml_root, index) 322 | @enum_hosts[index][:server_type] = xml_headers['server'] 323 | @enum_hosts[index][:data_complete] = true 324 | true 325 | else 326 | nil 327 | end 328 | end 329 | 330 | def parse_device_info(xml_root, index) 331 | device_entry_pointer = {} 332 | dev_tag = 'device' 333 | device_type = 'deviceType' 334 | device_list_entries = 'deviceList' 335 | device_tags = [ 336 | "friendlyName", 337 | "modelDescription", 338 | "modelName", 339 | "modelNumber", 340 | "modelURL", 341 | "presentationURL", 342 | "UDN", 343 | "UPC", 344 | "manufacturer", 345 | "manufacturerURL" 346 | ] 347 | 348 | xml_root.css(dev_tag).each do |device| 349 | device_type_name = (device.css(device_type).children.first.content rescue nil) 350 | next unless device_type_name 351 | 352 | device_display_name = parse_device_type_name(device_type_name) 353 | next unless device_display_name 354 | 355 | device_entry_pointer = @enum_hosts[index][:device_list][device_display_name] = {} 356 | device_entry_pointer['fullName'] = device_type_name 357 | 358 | device_tags.each do |tag| 359 | device_entry_pointer[tag] = (device.css(tag).children.first.content rescue '') 360 | end 361 | 362 | parse_service_list(device, device_entry_pointer, index) 363 | end 364 | end 365 | 366 | def parse_service_list(xml_root, device, index) 367 | service_entry_pointer = nil 368 | dict_name = :services 369 | service_list_tag = "serviceList" 370 | service_tag = "service" 371 | service_name_tag = "serviceType" 372 | service_tags = [ 373 | "serviceId", 374 | "controlURL", 375 | "eventSubURL", 376 | "SCPDURL" 377 | ] 378 | 379 | device[dict_name] = {} 380 | # Get a list of all services offered by this device 381 | 382 | xml_root.css(service_tag).each do |service| 383 | service_name = (service.css(service_name_tag).children.first.content rescue nil) 384 | next unless service_name 385 | 386 | service_display_name = parse_service_type_name(service_name) 387 | next unless service_display_name 388 | 389 | service_entry_pointer = device[dict_name][service_display_name] = {} 390 | service_entry_pointer['fullName'] = service_name 391 | 392 | service_tags.each do |tag| 393 | service_entry_pointer[tag] = service.css(tag).children.first.content 394 | end 395 | 396 | parse_service_info(service_entry_pointer, index) 397 | end 398 | end 399 | 400 | def parse_service_info(service, index) 401 | arg_index = 0 402 | arg_tags = ['direction', 'relatedStateVariable'] 403 | action_list = 'actionList' 404 | action_tag = 'action' 405 | name_tag = 'name' 406 | argument_list = 'argumentList' 407 | argument_tag = 'argument' 408 | 409 | xml_file = @enum_hosts[index][:proto] + @enum_hosts[index][:name] 410 | if !xml_file.end_with?('/') && !service['SCPDURL'].start_with?('/') 411 | xml_file += '/' 412 | end 413 | 414 | if service['SCPDURL'].include? @enum_hosts[index][:proto] 415 | xml_file = service['SCPDURL'] 416 | else 417 | xml_file += service['SCPDURL'] 418 | end 419 | 420 | service['actions'] = {} 421 | 422 | xml_headers, xml_data = get_xml(xml_file) 423 | 424 | unless xml_data 425 | log "could not get service descriptor at: #{xml_file}", WARN 426 | return nil 427 | end 428 | 429 | xml_root = Nokogiri::XML.parse(xml_data).root 430 | 431 | action_list = xml_root.css(action_list) 432 | actions = action_list.css(action_tag) 433 | 434 | actions.each do |action| 435 | action_name = action.css(name_tag).children.first.content.strip 436 | 437 | service['actions'][action_name] = {} 438 | service['actions'][action_name]['arguments'] = {} 439 | 440 | arg_list = action.css(argument_list) 441 | arguments = arg_list.css(argument_tag) 442 | 443 | arguments.each do |argument| 444 | arg_name = (argument.css(name_tag).children.first.content rescue nil) 445 | next unless arg_name 446 | service['actions'][action_name]['arguments'][arg_name] = {} 447 | 448 | arg_tags.each do |tag| 449 | service['actions'][action_name]['arguments'][arg_name][tag] = argument.css(tag).children.first.content 450 | end 451 | end 452 | end 453 | 454 | parse_service_state_vars(xml_root, service) 455 | end 456 | 457 | def parse_service_state_vars(xml_root, service_pointer) 458 | na = 'N/A' 459 | var_vals = ['sendEvents','dataType','defaultValue','allowedValues'] 460 | service_state_table = 'serviceStateTable' 461 | state_variable = 'stateVariable' 462 | name_tag = 'name' 463 | data_type = 'dataType' 464 | send_events = 'sendEvents' 465 | allowed_value_list = 'allowedValueList' 466 | allowed_value = 'allowedValue' 467 | allowed_value_range = 'allowedValueRange' 468 | minimum = 'minimum' 469 | maximum = 'maximum' 470 | 471 | service_pointer['serviceStateVariables'] = {} 472 | 473 | begin 474 | state_vars = xml_root.css(service_state_table).first.css(state_variable) 475 | rescue 476 | return false 477 | end 478 | 479 | state_vars.each do |var| 480 | var_vals.each do |tag| 481 | begin 482 | var_name = var.css(name_tag).children.first.content 483 | rescue 484 | log "failed to get state variable name for service #{service_pointer['fullName']}", WARN 485 | next 486 | end 487 | 488 | service_pointer['serviceStateVariables'][var_name] = {} 489 | 490 | service_pointer['serviceStateVariables'][var_name]['dataType'] = (var.css(data_type).children.first.content rescue na) 491 | service_pointer['serviceStateVariables'][var_name]['sendEvents'] = (var.css(send_events).children.first.content rescue na) 492 | service_pointer['serviceStateVariables'][var_name][allowed_value_list] = [] 493 | 494 | begin 495 | vals = var.css(allowed_value_list).first.css(allowed_value) 496 | 497 | vals.each do |val| 498 | service_pointer['serviceStateVariables'][var_name][allowed_value_list] << val.children.first.content 499 | end 500 | rescue 501 | end 502 | 503 | begin 504 | val_list = var.css(allowed_value_range) 505 | rescue 506 | next 507 | end 508 | 509 | service_pointer['serviceStateVariables'][var_name][allowed_value_range] = [] 510 | begin 511 | service_pointer['serviceStateVariables'][var_name][allowed_value_range] << val_list.css(minimum).first.children.content 512 | service_pointer['serviceStateVariables'][var_name][allowed_value_range] << val_list.css(maximum).first.children.content 513 | rescue 514 | end 515 | end 516 | end 517 | 518 | true 519 | end 520 | 521 | def msearch(timeout = 10) 522 | default_st = 'upnp:rootdevice' 523 | st = 'schemas-upnp-org' 524 | myip = local_ip 525 | lport = @port 526 | 527 | st = default_st 528 | 529 | request = "M-SEARCH * HTTP/1.1\r\n" + 530 | "HOST:#{@ip}:#{@port}\r\n" + 531 | "ST:#{st}\r\n" 532 | 533 | @msearch_headers.each_pair do |header, value| 534 | request += "#{header}:#{value}\r\n" 535 | end 536 | request += "\r\n" 537 | 538 | log "discovering UPnP devices", INFO 539 | 540 | server = create_new_listener(myip, lport) 541 | unless server 542 | puts "failed to bind to port #{lport}" 543 | return nil 544 | end 545 | 546 | send_udp(request, server) 547 | begin 548 | Timeout.timeout(timeout) do 549 | loop { 550 | data = listen_udp(1024, server) 551 | parse_ssdp_info(data) 552 | } 553 | end 554 | rescue Timeout::Error 555 | log "finished discovery mode", INFO 556 | server.close 557 | end 558 | end 559 | 560 | def enumerate(index = nil) 561 | if index 562 | enum_host(@enum_hosts[index]) 563 | else 564 | @enum_hosts.each_pair do |index, host| 565 | enum_host(index, host) 566 | end 567 | end 568 | end 569 | 570 | def enum_host(index, host_info) 571 | log "Requesting device and service info for #{host_info[:name]} (this could take a few seconds)", INFO 572 | xml_headers, xml_data = get_xml host_info[:xml_file] 573 | 574 | unless xml_data 575 | log "Failed to request host XML file: #{host_info[:xml_file]}", WARN 576 | return nil 577 | end 578 | 579 | unless get_host_info(xml_data, xml_headers, index) 580 | log "Failed to get device/service info for #{host_info[:name]}" 581 | return nil 582 | end 583 | 584 | log "Host data enumeration complete for #{host_info[:name]}", INFO 585 | end 586 | 587 | def generate_requests(index = nil) 588 | requests = [] 589 | 590 | if index 591 | gen_host_requests(@enum_hosts[index]) 592 | else 593 | @enum_hosts.each_pair do |index, host| 594 | requests += gen_host_requests(index, host) 595 | end 596 | end 597 | 598 | requests 599 | end 600 | 601 | def gen_host_requests(index, host) 602 | requests = [] 603 | hostname = host[:name] 604 | 605 | host[:device_list].each_pair do |device_name, device| 606 | device[:services].each_pair do |service_name, service| 607 | service['actions'].each_pair do |action_name, action| 608 | arg_list = {} 609 | index = 0 610 | action['arguments'].each_pair do |arg_name, argument| 611 | if argument['direction'] && argument['direction'].include?('in') 612 | arg_list[arg_name] = "$PARAM_#{index}_$" 613 | index += 1 614 | end 615 | end 616 | 617 | requests << build_soap_request(hostname, service['fullName'], service['controlURL'], action_name, arg_list) 618 | end 619 | end 620 | end 621 | 622 | requests 623 | end 624 | end 625 | 626 | end 627 | end 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | -------------------------------------------------------------------------------- /lib/ufuzz/validator.rb: -------------------------------------------------------------------------------- 1 | module UFuzz 2 | module Validator 3 | def validate(config) 4 | unless config.module 5 | log "no module specified, assuming generic", WARN 6 | config.module = 'generic' 7 | end 8 | 9 | unless config.host || config.upnp 10 | log "No target host specified, use -t to define target IP", FAIL 11 | exit 12 | end 13 | 14 | unless config.import || config.upnp 15 | log "No request or input file specified, use -i or --upnp", FAIL 16 | exit 17 | end 18 | 19 | unless config.app 20 | config.app = 'http' 21 | end 22 | 23 | unless config.port || config.upnp 24 | if config.app == 'http' 25 | log "no port specified, assuming tcp port 80", WARN 26 | config.port = 80 27 | end 28 | end 29 | end 30 | end 31 | end -------------------------------------------------------------------------------- /lib/ufuzz/wordlist/cmd_injection.txt: -------------------------------------------------------------------------------- 1 | ;ping -c 10 127.0.0.1; 2 | |ping -c 10 127.0.0.1 3 | && ping -c 10 127.0.0.1 && 4 | && ping -c 10 127.0.0.1 5 | `ping -c 10 127.0.0.1` 6 | ;`ping -c 10 127.0.0.1`; 7 | $(ping -c 10 127.0.0.1) -------------------------------------------------------------------------------- /lib/ufuzz/wordlist/format_string.txt: -------------------------------------------------------------------------------- 1 | %99999999999s 2 | %s%n%s%n 3 | %s%n%s%n%s%n%s%n%s%n%s%n%s%n%s%n%s%n%s%n%s%n%s%n%s%n%s%n%s%n%s%n%s%n%s%n -------------------------------------------------------------------------------- /lib/ufuzz/wordlist/path_traversal.txt: -------------------------------------------------------------------------------- 1 | ../../../../../../../../../../../../etc/passwd%00 2 | ../../../../../../../../../../../../etc/passwd 3 | /../../../../../../../../../../etc/passwd -------------------------------------------------------------------------------- /lib/ufuzz/wordlist/sql_injection.txt: -------------------------------------------------------------------------------- 1 | ; SELECT LIKE('ABCDEFG',UPPER(HEX(RANDOMBLOB(500000000/2)))) -- 2 | '||(SELECT LIKE('ABCDEFG',UPPER(HEX(RANDOMBLOB(500000000/2)))))||' 3 | ' AND 999999=LIKE('ABCDEFG',UPPER(HEX(RANDOMBLOB(500000000/2)))) -- 4 | ' OR 999999=LIKE('ABCDEFG',UPPER(HEX(RANDOMBLOB(500000000/2)))) -- 5 | (SELECT LIKE('ABCDEFG',UPPER(HEX(RANDOMBLOB(500000000/2))))) -------------------------------------------------------------------------------- /lib/ufuzz/wordlist/xxe.txt: -------------------------------------------------------------------------------- 1 | ]>&xxe; -------------------------------------------------------------------------------- /log/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore -------------------------------------------------------------------------------- /modules/generic/config.rb: -------------------------------------------------------------------------------- 1 | class GenericConfig < UFuzz::Config 2 | def options 3 | { 4 | platform: 'Generic', 5 | use_ssl: false, 6 | use_session: false, 7 | encoders: [ proc { |f| f.to_s } ], 8 | #skip_urls: /firmwareupdate1|UpdateWeeklyCalendar|ChangeFriendlyName/, 9 | #delay: 1, 10 | } 11 | end 12 | end -------------------------------------------------------------------------------- /modules/generic/monitor.rb: -------------------------------------------------------------------------------- 1 | class GenericMonitor < UFuzz::Monitor 2 | def start 3 | end 4 | 5 | def check 6 | end 7 | 8 | def close 9 | end 10 | end -------------------------------------------------------------------------------- /modules/serial/config.rb: -------------------------------------------------------------------------------- 1 | class SerialConfig < UFuzz::Config 2 | def options 3 | { 4 | platform: 'D-Link Router', 5 | use_ssl: false, 6 | use_session: false, 7 | #delay: 1, 8 | skip_urls: /AddPortMapping/ 9 | } 10 | end 11 | end -------------------------------------------------------------------------------- /modules/serial/monitor.rb: -------------------------------------------------------------------------------- 1 | class SerialMonitor < UFuzz::Monitor 2 | def start 3 | @index = 0 4 | @log = '' 5 | @sp = SerialPort.new("/dev/tty.usbserial-A600e1dU", 38400) 6 | 7 | t = Thread.new { 8 | loop do 9 | c = @sp.getc 10 | print c 11 | @log += c 12 | end 13 | } 14 | end 15 | 16 | def check 17 | curr_length = @log.length 18 | if @log[@index..-1] =~ /SIGSEGV/ 19 | sleep(10) 20 | @log.length > 1000 ? @crash_dump = @log[-1000..-1].dup : @crash_dump = @log[@index..-1].dup 21 | @index = curr_length 22 | log "Reboot Detected, pausing...", WARN 23 | sleep(30) 24 | log "Resuming...", WARN 25 | return @crash_dump 26 | else 27 | @crash_dump = nil 28 | return nil 29 | end 30 | end 31 | 32 | def close 33 | @sp.close 34 | end 35 | end -------------------------------------------------------------------------------- /modules/telnet/config.rb: -------------------------------------------------------------------------------- 1 | class TelnetConfig < UFuzz::Config 2 | def options 3 | { 4 | platform: 'Embedded Firewall', 5 | username: 'admin', 6 | password: '', 7 | admin_user: 'admin', 8 | admin_pass: '', 9 | use_ssl: false, 10 | use_session: true, 11 | skip_urls: /logincheck|logout/, 12 | } 13 | end 14 | end -------------------------------------------------------------------------------- /modules/telnet/http_session.rb: -------------------------------------------------------------------------------- 1 | class TelnetSession < UFuzz::Http::Session 2 | def login 3 | post_string = "ajax=1&username=#{config.username}&secretkey=#{config.password}" 4 | "POST /logincheck HTTP/1.1\r\n" + 5 | "Host: #{connection.host}\r\n" + 6 | "Content-Type: text/plain; charset=UTF-8\r\n" + 7 | "Content-Length: #{post_string.length}\r\n\r\n" + 8 | post_string 9 | end 10 | 11 | def logout 12 | "GET /logout HTTP/1.1\r\n" + 13 | "Host: #{connection.host}\r\n" + 14 | "Accept: text/html\r\n\r\n" 15 | end 16 | 17 | def check 18 | "GET /index HTTP/1.1\r\n" + 19 | "Host: #{connection.host}\r\n" + 20 | "Accept: text/html\r\n\r\n" 21 | end 22 | 23 | def logged_out 24 | /login_panel/ 25 | end 26 | 27 | def session_cookie 28 | @@session_cookie ||= "#{@response}".match(/APSCOOKIE_[0-9]+/) {|m| m[0]} 29 | end 30 | end -------------------------------------------------------------------------------- /modules/telnet/monitor.rb: -------------------------------------------------------------------------------- 1 | class TelnetMonitor < UFuzz::Monitor 2 | include UFuzz::MonitorHelpers::Telnet 3 | 4 | def start 5 | login 6 | telnet_cmd('diag debug crash clear') 7 | end 8 | 9 | def check 10 | telnet_cmd('diag debug crash read') do |c| 11 | if c && c.length > 300 12 | @crash_dump = c.dup 13 | telnet_cmd('diag debug crash clear') 14 | return c 15 | elsif c && c.length > 80 16 | telnet_cmd('diag debug crash clear') 17 | end 18 | end 19 | @crash_dump = nil 20 | return nil 21 | end 22 | 23 | def close 24 | telnet_cmd('diag debug crash clear') 25 | telnet_cmd('exit') 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /ufuzz: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $:.unshift(File.expand_path(File.dirname(__FILE__))) 3 | require 'lib/ufuzz' 4 | 5 | fuzzer = UFuzz::Fuzzer.load(ARGV) 6 | fuzzer.start! --------------------------------------------------------------------------------