module WWMD # WWMD::Page is an extension of a Curl::Easy object which provides methods to # enhance and ease the performance of web application penetration testing. class Page attr_accessor :curl_object attr_accessor :body_data attr_accessor :post_data attr_accessor :header_data attr_accessor :use_referer attr_reader :forms attr_reader :last_error attr_reader :links # array of links (urls) attr_reader :jlinks # array of included javascript files attr_reader :spider # spider object attr_reader :scrape # scrape object attr_reader :urlparse # urlparse object attr_reader :comments attr_reader :header_file attr_accessor :base_url # needed to properly munge relative urls into fq urls attr_accessor :logged_in # are we logged in? attr_accessor :opts attr_accessor :inputs include WWMDUtils def inspect # hack return "Page" end def initialize(opts={}, &block) @opts = opts.clone DEFAULTS.each { |k,v| @opts[k] = v unless @opts.has_key?(k) } @spider = Spider.new(@opts) @scrape = Scrape.new @base_url ||= opts[:base_url] @scrape.warn = opts[:scrape_warn] if !opts[:scrape_warn].nil? # yeah yeah... bool false @urlparse = URLParse.new() @inputs = Inputs.new(self) @logged_in = false @body_data = "" @post_data = "" @comments = [] @header_data = FormArray.new @header_file = nil @curl_object = Curl::Easy.new @opts.each do |k,v| next if k == :proxy_url if (@curl_object.respond_to?("#{k}=".intern)) @curl_object.send("#{k}=".intern,v) else self.instance_variable_set("@#{k.to_s}".intern,v) end end @curl_object.on_body { |data| self._body_cb(data) } @curl_object.on_header { |data| self._header_cb(data) } # cookies? @curl_object.enable_cookies = @opts[:enable_cookies] if @curl_object.enable_cookies? @curl_object.cookiejar = @opts[:cookiejar] || "./__cookiejar" end #proxy? @curl_object.proxy_url = @opts[:proxy_url] if @opts[:use_proxy] instance_eval(&block) if block_given? if opts.empty? && @scrape.warn putw "Page initialized without opts" @scrape.warn = false end if @header_file begin headers_from_file(@header_file) @curl_object.enable_cookies = false rescue => e puts "ERROR: #{e}" end end end #:section: Heavy Lifting # set reporting data for the page # # Scan for comments, anchors, links and javascript includes and # set page flags. The heavy lifting for parsing is done in the # scrape class. # # returns: array [ code, page_status, body_data.size ] def set_data # reset scrape and inputs object # transparently gunzip begin io = StringIO.new(self.body_data) gz = Zlib::GzipReader.new(io) self.body_data.replace(gz.read) rescue => e end @scrape.reset(self.body_data) @inputs.set # remove comments that are css selectors for IE silliness @comments = @scrape.for_comments.reject do |c| c =~ /\[if IE\]/ || c =~ /\[if IE \d/ || c =~ /\[if lt IE \d/ end @links = @scrape.for_links.map do |url| l = @urlparse.parse(self.last_effective_url,url).to_s end @jlinks = @scrape.for_javascript_links @forms = @scrape.for_forms @spider.add(self.last_effective_url,@links) return [self.code,self.body_data.size] end # clear self.body_data and self.header_data def clear_data return false if self.opts[:parse] = false @body_data = "" @post_data = nil @header_data.clear @last_error = nil end # override Curl::Easy.perform to perform page actions, # call self.set_data # # returns: array [ code, page_status, body_data.size ] # # don't call this directly if we are in console mode # use get and submit respectively for GET and POST def perform self.clear_data self.headers["Referer"] = self.cur if self.use_referer begin @curl_object.perform rescue => e @last_error = e putw "WARN: #{e.class}" if e.class =~ /Curl::Err/ end self.set_data end # replacement for Curl::Easy.http_post # # post the form attempting to remove curl supplied headers (Expect, X-Forwarded-For # call self.set_data # # if passed a regexp, escape values in the form using regexp before submitting # if passed nil for the regexp arg, the form will not be escaped # default: WWMD::ESCAPE[:url] # # returns: array [ code, body_data.size ] def submit(iform=nil,reg=WWMD::ESCAPE[:default]) ## this is just getting worse and worse if iform.class == "Symbol" reg = iform iform = nil end reg = WWMD::ESCAPE[reg] if reg.class == Symbol self.clear_data ["Expect","X-Forwarded-For","Content-length"].each { |s| self.clear_header(s) } self.headers["Referer"] = self.cur if self.use_referer unless iform unless self.form.empty? sform = self.form.clone else return "no form provided" end else sform = iform.clone # clone the form so that we don't change the original end sform.escape_all!(reg) self.url = sform.action if sform.action if sform.empty? self.http_post('') else self.http_post(self.post_data = sform.to_post) end self.set_data end # submit a form using POST string def submit_string(post_string) self.clear_data self.http_post(post_string) putw "WARN: authentication headers in response" if self.auth? self.set_data end # override for Curl::Easy.perform # # if the passed url string doesn't contain an fully qualified # path, we'll guess and prepend opts[:base_url] # # returns: array [ code, body_data.size ] def get(url=nil,parse=true) self.clear_data self.headers["Referer"] = self.cur if self.use_referer if !(url =~ /[a-z]+:\/\//) && parse self.url = @urlparse.parse(self.base_url,url).to_s if url elsif url self.url = url end self.http_get putw "WARN: authentication headers in response" if self.auth? self.set_data end # GET with params and POST it as a form def post(url=nil) ep = url.clip self.url = @urlparse.parse(self.opts[:base_url],ep).to_s if ep form = url.clop.to_form self.submit(form) end # send arbitrary verb (only works with patch to taf2-curb) def verb(verb,url=nil) return false if !@curl_object.respond_to?(:http_verb) self.url = url if url self.clear_data self.headers["Referer"] = self.cur if self.use_referer self.http_verb(verb) self.set_data end #:section: Data callbacks and method_missing # callback for self.on_body def _body_cb(data) @body_data << data if data return data.length.to_i end # callback for self.on_header def _header_cb(data) myArr = Array.new(data.split(":",2)) @header_data.add(myArr[0].to_s.strip,myArr[1].to_s.strip) # @header_data[myArr[0].to_s.strip] = myArr[1].to_s.strip return data.length.to_i end # send methods not defined here to @curl_object def method_missing(methodname, *args) @curl_object.send(methodname, *args) end end end