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
@return [Logger] logger
Public Class Methods
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
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 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
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 message
@param [String] msg message to print
# File lib/ssrf_proxy/server.rb 155 def print_error(msg = '') 156 puts '[-] '.red + msg 157 end
Print progress messages
@param [String] msg message to print
# File lib/ssrf_proxy/server.rb 146 def print_good(msg = '') 147 puts '[+] '.green + msg 148 end
Print status message
@param [String] msg message to print
# File lib/ssrf_proxy/server.rb 137 def print_status(msg = '') 138 puts '[*] '.blue + msg 139 end
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
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