├── .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 | 
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 = />.*?
9 | @tokens = str.split(@regex).map { |m| m }.zip(str.scan(@regex).map { |m| m }).flatten.compact
10 | end
11 |
12 | def fuzz_each_token(testcase, &block)
13 | @tokens.each_with_index do |t,i|
14 | next unless t =~ @regex
15 | testcase.rewind(t)
16 | while(testcase.next?)
17 | f = testcase.next
18 | fuzz_positions(t, i, f, &block)
19 | end
20 | end
21 | end
22 |
23 | def to_s
24 | @tokens.map { |t| t }.join('')
25 | end
26 |
27 | def fuzz_positions(tok, i, fuzz)
28 | [">#{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 = "%s>" % 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}#{arg}>"
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!
--------------------------------------------------------------------------------