class SSRFProxy::Server

SSRFProxy::Server takes a SSRFProxy::HTTP object, interface and port, and starts a HTTP proxy server on the specified interface and port. All client HTTP requests are sent via the specified SSRFProxy::HTTP object.

Attributes

logger[R]

@return [Logger] logger

Public Class Methods

new(ssrf, interface = '127.0.0.1', port = 8081) click to toggle source

Start the local server and listen for connections

@param [SSRFProxy::HTTP] ssrf A configured SSRFProxy::HTTP object @param [String] interface Listen interface (Default: 127.0.0.1) @param [Integer] port Listen port (Default: 8081)

@raise [SSRFProxy::Server::Error::InvalidSsrf]

Invalid SSRFProxy::SSRF object provided.

@raise [SSRFProxy::Server::Error::ProxyRecursion]

Proxy recursion error. SSRF Proxy cannot use itself as an
upstream proxy.

@raise [SSRFProxy::Server::Error::RemoteProxyUnresponsive]

Could not connect to remote proxy.

@raise [SSRFProxy::Server::Error::AddressInUse]

Could not bind to the port on the specified interface as
address already in use.

@example Start SSRF Proxy server with the default options

ssrf_proxy = SSRFProxy::Server.new(
  SSRFProxy::HTTP.new('http://example.local/?url=xxURLxx'),
  '127.0.0.1',
  8081)
ssrf_proxy.serve
    # File lib/ssrf_proxy/server.rb
 60 def initialize(ssrf, interface = '127.0.0.1', port = 8081)
 61   @banner = 'SSRF Proxy'
 62   @server = nil
 63   @logger = ::Logger.new(STDOUT).tap do |log|
 64     log.progname = 'ssrf-proxy-server'
 65     log.level = ::Logger::WARN
 66     log.datetime_format = '%Y-%m-%d %H:%M:%S '
 67   end
 68   # set ssrf
 69   unless ssrf.class == SSRFProxy::HTTP
 70     raise SSRFProxy::Server::Error::InvalidSsrf.new,
 71           'Invalid SSRF provided'
 72   end
 73   @ssrf = ssrf
 74 
 75   # check if the remote proxy server is responsive
 76   unless @ssrf.proxy.nil?
 77     if @ssrf.proxy.host == interface && @ssrf.proxy.port == port
 78       raise SSRFProxy::Server::Error::ProxyRecursion.new,
 79             "Proxy recursion error: #{@ssrf.proxy}"
 80     end
 81     if port_open?(@ssrf.proxy.host, @ssrf.proxy.port)
 82       print_good('Connected to remote proxy ' \
 83                 "#{@ssrf.proxy.host}:#{@ssrf.proxy.port} successfully")
 84     else
 85       raise SSRFProxy::Server::Error::RemoteProxyUnresponsive.new,
 86             'Could not connect to remote proxy ' \
 87             "#{@ssrf.proxy.host}:#{@ssrf.proxy.port}"
 88     end
 89   end
 90 
 91   # if no upstream proxy is set, check if the remote server is responsive
 92   if @ssrf.proxy.nil?
 93     if port_open?(@ssrf.url.host, @ssrf.url.port)
 94       print_good('Connected to remote host ' \
 95                  "#{@ssrf.url.host}:#{@ssrf.url.port} successfully")
 96     else
 97       raise SSRFProxy::Server::Error::RemoteHostUnresponsive.new,
 98             'Could not connect to remote host ' \
 99             "#{@ssrf.url.host}:#{@ssrf.url.port}"
100     end
101   end
102 
103   # start server
104   logger.info "Starting HTTP proxy on #{interface}:#{port}"
105   begin
106     print_status "Listening on #{interface}:#{port}"
107     @server = TCPServer.new(interface, port.to_i)
108   rescue Errno::EADDRINUSE
109     raise SSRFProxy::Server::Error::AddressInUse.new,
110           "Could not bind to #{interface}:#{port}" \
111           ' - address already in use'
112   end
113 end

Public Instance Methods

serve() click to toggle source

Run proxy server asynchronously

    # File lib/ssrf_proxy/server.rb
162 def serve
163   loop { async.handle_connection(@server.accept) }
164 end

Private Instance Methods

handle_connection(socket) click to toggle source

Handle client socket connection

@param [Celluloid::IO::TCPSocket] socket client socket

    # File lib/ssrf_proxy/server.rb
180 def handle_connection(socket)
181   start_time = Time.now
182   _, port, host = socket.peeraddr
183   logger.debug("Client #{host}:#{port} connected")
184   request = socket.read
185   logger.debug("Received client request (#{request.length} bytes):\n" \
186                "#{request}")
187 
188   response = nil
189   if request.to_s =~ /\ACONNECT ([_a-zA-Z0-9\.\-]+:[\d]+) .*$/
190     host = $1.to_s
191     logger.info("Negotiating connection to #{host}")
192     response = send_request("GET http://#{host}/ HTTP/1.0\n\n")
193 
194     if response['code'].to_i == 502 || response['code'].to_i == 504
195       logger.info("Connection to #{host} failed")
196       socket.write("#{response['status_line']}\n" \
197                    "#{response['headers']}\n" \
198                    "#{response['body']}")
199       raise Errno::ECONNRESET
200     end
201 
202     logger.info("Connected to #{host} successfully")
203     socket.write("HTTP/1.0 200 Connection established\r\n\r\n")
204     request = socket.read
205     logger.debug("Received client request (#{request.length} bytes):\n" \
206                  "#{request}")
207 
208     # CHANGE_CIPHER_SPEC  20   0x14
209     # ALERT               21   0x15
210     # HANDSHAKE           22   0x16
211     # APPLICATION_DATA    23   0x17
212     if request.to_s.start_with?("\x14", "\x15", "\x16", "\x17")
213       logger.warn("Received SSL/TLS client request. SSL/TLS tunneling is not supported. Aborted.")
214       raise Errno::ECONNRESET
215     end
216   end
217 
218   response = send_request(request.to_s)
219   socket.write("#{response['status_line']}\n" \
220                "#{response['headers']}\n" \
221                "#{response['body']}")
222   raise Errno::ECONNRESET
223 rescue EOFError, Errno::ECONNRESET, Errno::EPIPE
224   socket.close
225   logger.debug("Client #{host}:#{port} disconnected")
226   end_time = Time.now
227   duration = ((end_time - start_time) * 1000).round(3)
228   if response.nil?
229     logger.info("Served 0 bytes in #{duration} ms")
230   else
231     logger.info("Served #{response['body'].length} bytes in #{duration} ms")
232   end
233 end
port_open?(ip, port, seconds = 10) click to toggle source

Checks if a port is open or not on a remote host From: gist.github.com/ashrithr/5305786

@param [String] ip connect to IP @param [Integer] port connect to port @param [Integer] seconds connection timeout

    # File lib/ssrf_proxy/server.rb
123 def port_open?(ip, port, seconds = 10)
124   Timeout.timeout(seconds) do
125     TCPSocket.new(ip, port).close
126     true
127   end
128 rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, SocketError, Timeout::Error
129   false
130 end
print_error(msg = '') click to toggle source

Print error message

@param [String] msg message to print

print_good(msg = '') click to toggle source

Print progress messages

@param [String] msg message to print

print_status(msg = '') click to toggle source

Print status message

@param [String] msg message to print

send_request(request) click to toggle source

Send client HTTP request

@param [String] request client HTTP request

@return [Hash] HTTP response

    # File lib/ssrf_proxy/server.rb
242 def send_request(request)
243   response_error = { 'uri' => '',
244                      'duration' => '0',
245                      'http_version' => '1.0',
246                      'headers' => "Server: #{@banner}\n",
247                      'body' => '' }
248 
249   # parse client request
250   begin
251     if request.to_s !~ %r{\A(CONNECT|GET|HEAD|DELETE|POST|PUT|OPTIONS) https?://}
252       if request.to_s !~ /^Host: ([^\s]+)\r?\n/
253         logger.warn('No host specified')
254         raise SSRFProxy::HTTP::Error::InvalidClientRequest,
255               'No host specified'
256       end
257     end
258     req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
259     req.parse(StringIO.new(request))
260   rescue => e
261     logger.info('Received malformed client HTTP request.')
262     error_msg = 'Error -- Invalid request: ' \
263                 "Received malformed client HTTP request: #{e.message}"
264     print_error(error_msg)
265     response_error['code'] = '502'
266     response_error['message'] = 'Bad Gateway'
267     response_error['status_line'] = "HTTP/#{response_error['http_version']}"
268     response_error['status_line'] << " #{response_error['code']}"
269     response_error['status_line'] << " #{response_error['message']}"
270     return response_error
271   end
272   uri = req.request_uri
273 
274   # send request
275   response = nil
276   logger.info("Requesting URL: #{uri}")
277   status_msg = "Request  -> #{req.request_method}"
278   status_msg << " -> PROXY[#{@ssrf.proxy.host}:#{@ssrf.proxy.port}]" unless @ssrf.proxy.nil?
279   status_msg << " -> SSRF[#{@ssrf.url.host}:#{@ssrf.url.port}] -> URI[#{uri}]"
280   print_status(status_msg)
281 
282   begin
283     response = @ssrf.send_request(request.to_s)
284   rescue SSRFProxy::HTTP::Error::InvalidClientRequest => e
285     logger.info(e.message)
286     error_msg = "Error -- Invalid request: #{e.message}"
287     print_error(error_msg)
288     response_error['code'] = '502'
289     response_error['message'] = 'Bad Gateway'
290     response_error['status_line'] = "HTTP/#{response_error['http_version']}"
291     response_error['status_line'] << " #{response_error['code']}"
292     response_error['status_line'] << " #{response_error['message']}"
293     return response_error
294   rescue SSRFProxy::HTTP::Error::InvalidResponse => e
295     logger.info(e.message)
296     error_msg = 'Response <- 503'
297     error_msg << " <- PROXY[#{@ssrf.proxy.host}:#{@ssrf.proxy.port}]" unless @ssrf.proxy.nil?
298     error_msg << " <- SSRF[#{@ssrf.url.host}:#{@ssrf.url.port}] <- URI[#{uri}]"
299     error_msg << " -- Error: #{e.message}"
300     print_error(error_msg)
301     response_error['code'] = '503'
302     response_error['message'] = 'Service Unavailable'
303     response_error['status_line'] = "HTTP/#{response_error['http_version']}"
304     response_error['status_line'] << " #{response_error['code']}"
305     response_error['status_line'] << " #{response_error['message']}"
306     return response_error
307   rescue SSRFProxy::HTTP::Error::ConnectionFailed => e
308     logger.info(e.message)
309     error_msg = 'Response <- 503'
310     error_msg << " <- PROXY[#{@ssrf.proxy.host}:#{@ssrf.proxy.port}]" unless @ssrf.proxy.nil?
311     error_msg << " <- SSRF[#{@ssrf.url.host}:#{@ssrf.url.port}] <- URI[#{uri}]"
312     error_msg << " -- Error: #{e.message}"
313     print_error(error_msg)
314     response_error['code'] = '503'
315     response_error['message'] = 'Service Unavailable'
316     response_error['status_line'] = "HTTP/#{response_error['http_version']}"
317     response_error['status_line'] << " #{response_error['code']}"
318     response_error['status_line'] << " #{response_error['message']}"
319     return response_error
320   rescue SSRFProxy::HTTP::Error::ConnectionTimeout => e
321     logger.info(e.message)
322     error_msg = 'Response <- 504'
323     error_msg << " <- PROXY[#{@ssrf.proxy.host}:#{@ssrf.proxy.port}]" unless @ssrf.proxy.nil?
324     error_msg << " <- SSRF[#{@ssrf.url.host}:#{@ssrf.url.port}] <- URI[#{uri}]"
325     error_msg << " -- Error: #{e.message}"
326     print_error(error_msg)
327     response_error['code'] = '504'
328     response_error['message'] = 'Timeout'
329     response_error['status_line'] = "HTTP/#{response_error['http_version']}"
330     response_error['status_line'] << " #{response_error['code']}"
331     response_error['status_line'] << " #{response_error['message']}"
332     return response_error
333   rescue => e
334     logger.warn(e.message)
335     error_msg = "Error -- Unexpected error: #{e.backtrace.join("\n")}"
336     print_error(error_msg)
337     response_error['code'] = '502'
338     response_error['message'] = 'Bad Gateway'
339     response_error['status_line'] = "HTTP/#{response_error['http_version']}"
340     response_error['status_line'] << " #{response_error['code']} "
341     response_error['status_line'] << " #{response_error['message']}"
342     return response_error
343   end
344 
345   # return response
346   status_msg = "Response <- #{response['code']}"
347   status_msg << " <- PROXY[#{@ssrf.proxy.host}:#{@ssrf.proxy.port}]" unless @ssrf.proxy.nil?
348   status_msg << " <- SSRF[#{@ssrf.url.host}:#{@ssrf.url.port}] <- URI[#{uri}]"
349   status_msg << " -- Title[#{response['title']}]" unless response['title'].eql?('')
350   status_msg << " -- [#{response['body'].size} bytes]"
351   print_good(status_msg)
352   response
353 end
shutdown() click to toggle source

Handle shutdown of server socket

    # File lib/ssrf_proxy/server.rb
169 def shutdown
170   logger.info 'Shutting down'
171   @server.close if @server
172   logger.debug 'Shutdown complete'
173 end