bin/ssrf-proxy
#!/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