bcoles/ssrf_proxy

View on GitHub
bin/ssrf-proxy

Summary

Maintainability
Test Coverage
#!/usr/bin/env ruby
#
# Copyright (c) 2015-2017 Brendan Coles <bcoles@gmail.com>
# SSRF Proxy - https://github.com/bcoles/ssrf_proxy
# See the file 'LICENSE.md' for copying permission
#

require 'ssrf_proxy'

#
# @note output banner
#
def banner
  puts "\n_________________________________________________________".blue
  puts SSRFProxy::BANNER.blue
  puts "\n                    SSRF Proxy v#{SSRFProxy::VERSION}"
  puts "          https://github.com/bcoles/ssrf_proxy\n"
  puts "\n_________________________________________________________\n".blue
end

#
# @note output usage
#
def usage
  banner
  puts 'Usage:   ssrf-proxy [options] -u <SSRF URL>'
  puts 'Example: ssrf-proxy -u http://target/?url=xxURLxx'
  puts 'Options:'
  puts "
   -h, --help             Help
       --version          Display version

  Output options:
   -v, --verbose          Verbose output
   -d, --debug            Debugging output
       --no-color         Disable colored output

  Server options:
   -p, --port=PORT        Listen port (Default: 8081)
       --interface=IP     Listen interface (Default: 127.0.0.1)

  SSRF request options:
   -u, --url=URL          Target URL vulnerable to SSRF.
   -f, --file=FILE        Load HTTP request from a file.
       --placeholder=STR  Placeholder indicating SSRF insertion point.
                          (Default: xxURLxx)
       --method=METHOD    HTTP method (GET/HEAD/DELETE/POST/PUT/OPTIONS)
                          (Default: GET)
       --post-data=DATA   HTTP post data
       --cookie=COOKIE    HTTP cookies (separated by ';')
       --user=USER[:PASS] HTTP basic authentication credentials.
       --user-agent=AGENT HTTP user-agent (Default: none)
       --rules=RULES      Rules for parsing client request
                          (separated by ',') (Default: none)
       --no-urlencode     Do not URL encode client request

  SSRF connection options:
       --ssl              Connect using SSL/TLS.
       --proxy=PROXY      Use a proxy to connect to the server.
                          (Supported proxies: http, https, socks)
       --insecure         Skip server SSL certificate validation.
       --timeout=SECONDS  Connection timeout in seconds (Default: 10)

  HTTP response modification:
       --match=REGEX      Regex to match response body content.
                          (Default: \\A(.*)\\z)
       --strip=HEADERS    Headers to remove from the response.
                          (separated by ',') (Default: none)
       --decode-html      Decode HTML entities in response body.
       --unescape         Unescape special characters in response body.
       --guess-status     Replaces response status code and message
                          headers (determined by common strings in the
                          response body, such as 404 Not Found.)
       --guess-mime       Replaces response content-type header with the
                          appropriate mime type (determined by the file
                          extension of the requested resource.)
       --sniff-mime       Replaces response content-type header with the
                          appropriate mime type (determined by magic bytes
                          in the response body.)
       --timeout-ok       Replaces timeout HTTP status code 504 with 200.
       --detect-headers   Replaces response headers if response headers
                          are identified in the response body.
       --fail-no-content  Return HTTP status 502 if the response body
                          is empty.
       --cors             Adds a 'Access-Control-Allow-Origin: *' header.

  Client request modification:
       --forward-method   Forward client request method.
       --forward-headers  Forward all client request headers.
       --forward-body     Forward client request body.
       --forward-cookies  Forward client request cookies.
       --cookies-to-uri   Add client request cookies to URI query string.
       --body-to-uri      Add client request body to URI query string.
       --auth-to-uri      Use client request basic authentication
                          credentials in request URI.
       --ip-encoding=MODE Encode client request host IP address.
                          (Modes: int, ipv6, oct, hex, dotted_hex)
       --cache-buster     Append a random value to the client request
                          query string.

"
  exit 1
end

#
# @note parse options and start server
#
def start_server
  # get args
  opts = GetoptLong.new(
    ['-h', '--help',      GetoptLong::NO_ARGUMENT],
    ['--version',         GetoptLong::NO_ARGUMENT],

    ['-v', '--verbose',   GetoptLong::NO_ARGUMENT],
    ['-d', '--debug',     GetoptLong::NO_ARGUMENT],
    ['--no-color',        GetoptLong::NO_ARGUMENT],

    ['-p', '--port',      GetoptLong::REQUIRED_ARGUMENT],
    ['--interface',       GetoptLong::REQUIRED_ARGUMENT],

    ['-u', '--url',       GetoptLong::REQUIRED_ARGUMENT],
    ['-f', '--file',      GetoptLong::REQUIRED_ARGUMENT],
    ['--placeholder',     GetoptLong::REQUIRED_ARGUMENT],
    ['--method',          GetoptLong::REQUIRED_ARGUMENT],
    ['--post-data',       GetoptLong::REQUIRED_ARGUMENT],
    ['--cookie',          GetoptLong::REQUIRED_ARGUMENT],
    ['--user',            GetoptLong::REQUIRED_ARGUMENT],
    ['--user-agent',      GetoptLong::REQUIRED_ARGUMENT],

    ['--ssl',             GetoptLong::NO_ARGUMENT],
    ['--proxy',           GetoptLong::REQUIRED_ARGUMENT],
    ['--insecure',        GetoptLong::NO_ARGUMENT],
    ['--timeout',         GetoptLong::REQUIRED_ARGUMENT],

    ['--rules',           GetoptLong::REQUIRED_ARGUMENT],
    ['--no-urlencode',    GetoptLong::NO_ARGUMENT],
    ['--forward-method',  GetoptLong::NO_ARGUMENT],
    ['--forward-headers', GetoptLong::NO_ARGUMENT],
    ['--forward-body',    GetoptLong::NO_ARGUMENT],
    ['--forward-cookies', GetoptLong::NO_ARGUMENT],
    ['--cookies-to-uri',  GetoptLong::NO_ARGUMENT],
    ['--body-to-uri',     GetoptLong::NO_ARGUMENT],
    ['--auth-to-uri',     GetoptLong::NO_ARGUMENT],
    ['--cache-buster',    GetoptLong::NO_ARGUMENT],
    ['--ip-encoding',     GetoptLong::REQUIRED_ARGUMENT],

    ['--match',           GetoptLong::REQUIRED_ARGUMENT],
    ['--strip',           GetoptLong::REQUIRED_ARGUMENT],
    ['--decode-html',     GetoptLong::NO_ARGUMENT],
    ['--unescape',        GetoptLong::NO_ARGUMENT],
    ['--guess-status',    GetoptLong::NO_ARGUMENT],
    ['--guess-mime',      GetoptLong::NO_ARGUMENT],
    ['--sniff-mime',      GetoptLong::NO_ARGUMENT],
    ['--cors',            GetoptLong::NO_ARGUMENT],
    ['--detect-headers',  GetoptLong::NO_ARGUMENT],
    ['--fail-no-content', GetoptLong::NO_ARGUMENT],
    ['--timeout-ok',      GetoptLong::NO_ARGUMENT]
  )

  # local proxy server defaults
  interface = '127.0.0.1'
  port = 8081

  # ssrf defaults
  url = nil
  file = nil
  rules = nil
  ip_encoding = nil
  placeholder = 'xxURLxx'
  method = 'GET'
  post_data = nil
  match = '\\A(.*)\\z'
  strip = nil
  no_urlencode = false
  decode_html = false
  unescape = false
  guess_status = false
  guess_mime = false
  sniff_mime = false
  cors = false
  timeout_ok = false
  detect_headers = false
  fail_no_content = false
  forward_method = false
  forward_headers = false
  forward_body = false
  forward_cookies = false
  body_to_uri = false
  auth_to_uri = false
  cookies_to_uri = false
  cache_buster = false

  # http connection defaults
  cookie = nil
  user = nil
  timeout = 10
  ssl = false
  upstream_proxy = nil
  user_agent = nil
  insecure = false

  # logging
  log_level = ::Logger::WARN

  # handle args
  begin
    opts.each do |opt, arg|
      case opt
      when '-p', '--port'
        port = arg
      when '--interface'
        interface = arg
      when '-u', '--url'
        url = arg
      when '-f', '--file'
        file = arg
      when '--ip-encoding'
        ip_encoding = arg
      when '--rules'
        rules = arg
      when '--no-urlencode'
        no_urlencode = true
      when '--ssl'
        ssl = true
      when '--proxy'
        upstream_proxy = URI.parse(arg)
      when '--cookie'
        cookie = arg
      when '--user'
        user = arg
      when '--timeout'
        timeout = arg.to_i
      when '--user-agent'
        user_agent = arg
      when '--insecure'
        insecure = true
      when '--placeholder'
        placeholder = arg
      when '--method'
        method = arg
      when '--post-data'
        post_data = arg
      when '--match'
        match = arg
      when '--strip'
        strip = arg
      when '--decode-html'
        decode_html = true
      when '--unescape'
        unescape = true
      when '--guess-status'
        guess_status = true
      when '--guess-mime'
        guess_mime = true
      when '--sniff-mime'
        sniff_mime = true
      when '--cors'
        cors = true
      when '--timeout-ok'
        timeout_ok = true
      when '--detect-headers'
        detect_headers = true
      when '--fail-no-content'
        fail_no_content = true
      when '--forward-method'
        forward_method = true
      when '--forward-headers'
        forward_headers = true
      when '--forward-body'
        forward_body = true
      when '--forward-cookies'
        forward_cookies = true
      when '--body-to-uri'
        body_to_uri = true
      when '--auth-to-uri'
        auth_to_uri = true
      when '--cookies-to-uri'
        cookies_to_uri = true
      when '--cache-buster'
        cache_buster = true
      when '-h', '--help'
        usage
      when '-v', '--verbose'
        log_level = ::Logger::INFO unless log_level == ::Logger::DEBUG
      when '-d', '--debug'
        log_level = ::Logger::DEBUG
      when '--no-color'
        String.disable_colorization = true
      when '--version'
        puts "SSRF Proxy v#{SSRFProxy::VERSION}"
        exit
      end
    end
  rescue GetoptLong::InvalidOption, GetoptLong::MissingArgument
    puts "Error: Invalid usage. Try '#{$PROGRAM_NAME} --help' for usage information."
    exit 1
  end

  opts = {
    url:              url,
    file:             file,
    proxy:            upstream_proxy,
    ssl:              ssl,
    method:           method,
    placeholder:      placeholder,
    post_data:        post_data,
    rules:            rules,
    no_urlencode:     no_urlencode,
    ip_encoding:      ip_encoding,
    match:            match,
    strip:            strip,
    decode_html:      decode_html,
    unescape:         unescape,
    guess_mime:       guess_mime,
    sniff_mime:       sniff_mime,
    guess_status:     guess_status,
    cors:             cors,
    timeout_ok:       timeout_ok,
    detect_headers:   detect_headers,
    fail_no_content:  fail_no_content,
    forward_method:   forward_method,
    forward_headers:  forward_headers,
    forward_body:     forward_body,
    forward_cookies:  forward_cookies,
    body_to_uri:      body_to_uri,
    auth_to_uri:      auth_to_uri,
    cookies_to_uri:   cookies_to_uri,
    cache_buster:     cache_buster,
    cookie:           cookie,
    user:             user,
    timeout:          timeout,
    user_agent:       user_agent,
    insecure:         insecure
  }

  begin
    banner

    %w[INT QUIT TERM].each do |s|
      Signal.trap(s) { exit }
    end

    # setup ssrf
    ssrf = SSRFProxy::HTTP.new(opts)
    ssrf.logger.level = log_level

    # start server
    ssrf_proxy = SSRFProxy::Server.new(ssrf, interface, port)
    ssrf_proxy.logger.level = log_level
    ssrf_proxy.serve
  rescue => e
    puts "Error: #{e.message}"
  end
end

usage if ARGV.empty?
start_server