ssrf_proxy 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,147 +1,327 @@
1
- #!/usr/bin/env ruby
1
+ # coding: utf-8
2
2
  #
3
- # Copyright (c) 2015 Brendan Coles <bcoles@gmail.com>
3
+ # Copyright (c) 2015-2016 Brendan Coles <bcoles@gmail.com>
4
4
  # SSRF Proxy - https://github.com/bcoles/ssrf_proxy
5
- # See the file 'LICENSE' for copying permission
5
+ # See the file 'LICENSE.md' for copying permission
6
6
  #
7
7
 
8
- require "ssrf_proxy"
9
-
10
8
  module SSRFProxy
11
- #
12
- # @note SSRFProxy::Server
13
- #
14
- class Server
9
+ #
10
+ # SSRFProxy::Server takes a SSRFProxy::HTTP object, interface
11
+ # and port, and starts a HTTP proxy server on the specified
12
+ # interface and port. All client HTTP requests are sent via
13
+ # the specified SSRFProxy::HTTP object.
14
+ #
15
+ class Server
16
+ include Celluloid::IO
17
+ finalizer :shutdown
15
18
 
16
- # @note output status messages
17
- def print_status(msg='')
18
- puts '[*] '.blue + msg
19
- end
20
- # @note output progress messages
21
- def print_good(msg='')
22
- puts '[+] '.green + msg
23
- end
19
+ #
20
+ # SSRFProxy::Server errors
21
+ #
22
+ module Error
23
+ # SSRFProxy::Server custom errors
24
+ class Error < StandardError; end
25
+ exceptions = %w(
26
+ InvalidSsrf
27
+ ProxyRecursion
28
+ AddressInUse
29
+ RemoteProxyUnresponsive
30
+ RemoteHostUnresponsive )
31
+ exceptions.each { |e| const_set(e, Class.new(Error)) }
32
+ end
24
33
 
25
- require 'socket'
26
- require 'celluloid/current'
27
- require 'celluloid/io'
34
+ #
35
+ # Start the local server and listen for connections
36
+ #
37
+ # @param [SSRFProxy::HTTP] ssrf A configured SSRFProxy::HTTP object
38
+ # @param [String] interface Listen interface (Default: 127.0.0.1)
39
+ # @param [Integer] port Listen port (Default: 8081)
40
+ #
41
+ # @raise [SSRFProxy::Server::Error::InvalidSsrf]
42
+ # Invalid SSRFProxy::SSRF object provided.
43
+ # @raise [SSRFProxy::Server::Error::ProxyRecursion]
44
+ # Proxy recursion error. SSRF Proxy cannot use itself as an
45
+ # upstream proxy.
46
+ # @raise [SSRFProxy::Server::Error::RemoteProxyUnresponsive]
47
+ # Could not connect to remote proxy.
48
+ # @raise [SSRFProxy::Server::Error::AddressInUse]
49
+ # Could not bind to the port on the specified interface as
50
+ # address already in use.
51
+ #
52
+ # @example Start SSRF Proxy server with the default options
53
+ # ssrf_proxy = SSRFProxy::Server.new(
54
+ # SSRFProxy::HTTP.new('http://example.local/index.php?url=xxURLxx'),
55
+ # '127.0.0.1',
56
+ # 8081)
57
+ # ssrf_proxy.serve
58
+ #
59
+ def initialize(ssrf, interface = '127.0.0.1', port = 8081)
60
+ @banner = 'SSRF Proxy'
61
+ @server = nil
62
+ @max_request_len = 8192
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
28
74
 
29
- include Celluloid::IO
30
- finalizer :shutdown
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 #{@ssrf.proxy.host}:#{@ssrf.proxy.port} successfully")
83
+ else
84
+ raise SSRFProxy::Server::Error::RemoteProxyUnresponsive.new,
85
+ "Could not connect to remote proxy #{@ssrf.proxy.host}:#{@ssrf.proxy.port}"
86
+ end
87
+ end
31
88
 
32
- attr_accessor :logger
89
+ # if no upstream proxy is set, check if the remote server is responsive
90
+ if @ssrf.proxy.nil?
91
+ if port_open?(@ssrf.host, @ssrf.port)
92
+ print_good("Connected to remote host #{@ssrf.host}:#{@ssrf.port} successfully")
93
+ else
94
+ raise SSRFProxy::Server::Error::RemoteHostUnresponsive.new,
95
+ "Could not connect to remote host #{@ssrf.host}:#{@ssrf.port}"
96
+ end
97
+ end
33
98
 
34
- # @note logger
35
- def logger
36
- @logger || ::Logger.new(STDOUT).tap do |log|
37
- log.progname = 'ssrf-proxy-server'
38
- log.level = ::Logger::WARN
39
- log.datetime_format = '%Y-%m-%d %H:%M:%S '
99
+ # start server
100
+ logger.info "Starting HTTP proxy on #{interface}:#{port}"
101
+ begin
102
+ print_status "Listening on #{interface}:#{port}"
103
+ @server = TCPServer.new(interface, port.to_i)
104
+ rescue Errno::EADDRINUSE
105
+ raise SSRFProxy::Server::Error::AddressInUse.new,
106
+ "Could not bind to #{interface}:#{port} - address already in use"
107
+ end
40
108
  end
41
- end
42
109
 
43
- #
44
- # @note SSRFProxy::Server errors
45
- #
46
- module Error
47
- # custom errors
48
- class Error < StandardError; end
49
- exceptions = %w( InvalidSsrf )
50
- exceptions.each { |e| const_set(e, Class.new(Error)) }
51
- end
110
+ #
111
+ # Checks if a port is open or not on a remote host
112
+ # From: https://gist.github.com/ashrithr/5305786
113
+ #
114
+ # @param [String] ip connect to IP
115
+ # @param [Integer] port connect to port
116
+ # @param [Integer] seconds connection timeout
117
+ #
118
+ def port_open?(ip, port, seconds = 10)
119
+ Timeout.timeout(seconds) do
120
+ begin
121
+ TCPSocket.new(ip, port).close
122
+ true
123
+ rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, SocketError
124
+ false
125
+ end
126
+ end
127
+ rescue Timeout::Error
128
+ false
129
+ end
52
130
 
53
- #
54
- # @note Start the local server and listen for connections
55
- #
56
- # @options
57
- # - interface - String - Listen interface (Default: 127.0.0.1)
58
- # - port - Integer - Listen port (Default: 8081)
59
- # - ssrf - SSRFProxy::HTTP - SSRF
60
- #
61
- def initialize(interface='127.0.0.1', port=8081, ssrf)
62
- @logger = ::Logger.new(STDOUT).tap do |log|
63
- log.progname = 'ssrf-proxy-server'
64
- log.level = ::Logger::WARN
65
- log.datetime_format = '%Y-%m-%d %H:%M:%S '
131
+ #
132
+ # Print status message
133
+ #
134
+ # @param [String] msg message to print
135
+ #
136
+ def print_status(msg = '')
137
+ puts '[*] '.blue + msg
66
138
  end
67
- # set ssrf
68
- unless ssrf.class == SSRFProxy::HTTP
69
- raise SSRFProxy::Server::Error::InvalidSsrf.new,
70
- "Invalid SSRF provided"
139
+
140
+ #
141
+ # Print progress messages
142
+ #
143
+ # @param [String] msg message to print
144
+ #
145
+ def print_good(msg = '')
146
+ puts '[+] '.green + msg
71
147
  end
72
- @ssrf = ssrf
73
- # start server
74
- logger.info "Starting HTTP proxy on #{interface}:#{port}"
75
- print_status "Listening on #{interface}:#{port}"
76
- @server = TCPServer.new(interface, port.to_i)
77
- end
78
148
 
79
- #
80
- # @note Run proxy server
81
- #
82
- def serve
83
- loop { async.handle_connection(@server.accept) }
84
- end
149
+ #
150
+ # Print error message
151
+ #
152
+ # @param [String] msg message to print
153
+ #
154
+ def print_error(msg = '')
155
+ puts '[-] '.red + msg
156
+ end
85
157
 
86
- private
158
+ #
159
+ # Logger accessor
160
+ #
161
+ # @return [Logger] class logger object
162
+ #
163
+ def logger
164
+ @logger
165
+ end
87
166
 
88
- #
89
- # @note Handle shutdown of client socket
90
- #
91
- def shutdown
92
- logger.info 'Shutting down'
93
- @server.close if @server
94
- logger.debug 'Shutdown complete'
95
- end
167
+ #
168
+ # Run proxy server asynchronously
169
+ #
170
+ def serve
171
+ loop { async.handle_connection(@server.accept) }
172
+ end
96
173
 
97
- #
98
- # @note Handle client request
99
- #
100
- # @options
101
- # - socket - String - client socket
102
- #
103
- def handle_connection(socket)
104
- _, port, host = socket.peeraddr
105
- max_len = 4096
106
- logger.debug "Client #{host}:#{port} connected"
107
- request = socket.readpartial(max_len)
108
- logger.debug("Received client request (#{request.length} bytes):\n#{request}")
109
- if request.length >= max_len
110
- logger.warn("Client request too long (truncated at #{request.length} bytes)")
174
+ #
175
+ # Handle shutdown of client socket
176
+ #
177
+ def shutdown
178
+ logger.info 'Shutting down'
179
+ @server.close if @server
180
+ logger.debug 'Shutdown complete'
111
181
  end
112
- if request.to_s !~ /\A[A-Z]{1,20} /
113
- logger.warn("Malformed client HTTP request")
114
- response = "HTTP/1.0 501 Error\r\n\r\n"
115
- elsif request.to_s =~ /\ACONNECT ([a-zA-Z0-9\.\-]+:[\d]+) .*$/
116
- host = "#{$1}"
117
- logger.info("Negotiating connection to #{host}")
118
- response = @ssrf.send_request("GET http://#{host}/ HTTP/1.0\n\n")
119
- if response =~ /^Server: SSRF Proxy$/i && response =~ /^Content-Length: 0$/i
120
- logger.warn("Connection to #{host} failed")
121
- response = "HTTP/1.0 502 Bad Gateway\r\n\r\n"
122
- else
182
+
183
+ #
184
+ # Handle client socket connection
185
+ #
186
+ # @param [Celluloid::IO::TCPSocket] socket client socket
187
+ #
188
+ def handle_connection(socket)
189
+ start_time = Time.now
190
+ _, port, host = socket.peeraddr
191
+ logger.debug("Client #{host}:#{port} connected")
192
+ request = socket.readpartial(@max_request_len)
193
+ logger.debug("Received client request (#{request.length} bytes):\n#{request}")
194
+
195
+ response = nil
196
+ if request.to_s =~ /\ACONNECT ([_a-zA-Z0-9\.\-]+:[\d]+) .*$/
197
+ host = $1.to_s
198
+ logger.info("Negotiating connection to #{host}")
199
+ response = send_request("GET http://#{host}/ HTTP/1.0\n\n")
200
+
201
+ if response['code'].to_i == 502 || response['code'].to_i == 504
202
+ logger.info("Connection to #{host} failed")
203
+ socket.write("#{response['status_line']}\n#{response['headers']}\n#{response['body']}")
204
+ raise Errno::ECONNRESET
205
+ end
206
+
123
207
  logger.info("Connected to #{host} successfully")
124
208
  socket.write("HTTP/1.0 200 Connection established\r\n\r\n")
125
- request = socket.readpartial(max_len)
209
+ request = socket.readpartial(@max_request_len)
126
210
  logger.debug("Received client request (#{request.length} bytes):\n#{request}")
127
- if request.length >= max_len
128
- logger.warn("Client request too long (truncated at #{request.length} bytes)")
129
- end
130
- response = @ssrf.send_request(request)
131
211
  end
132
- else
133
- response = @ssrf.send_request(request)
212
+
213
+ response = send_request(request.to_s)
214
+ socket.write("#{response['status_line']}\n#{response['headers']}\n#{response['body']}")
215
+ raise Errno::ECONNRESET
216
+ rescue EOFError, Errno::ECONNRESET
217
+ socket.close
218
+ logger.debug("Client #{host}:#{port} disconnected")
219
+ end_time = Time.now
220
+ duration = end_time - start_time
221
+ logger.info("Served #{response['body'].length} bytes in #{(duration * 1000).round(3)} ms")
134
222
  end
135
- socket.write(response)
136
- socket.close
137
- rescue EOFError, Errno::ECONNRESET
138
- logger.debug "Client #{host}:#{port} disconnected"
139
- socket.close
140
- end
141
223
 
142
- private :shutdown,:handle_connection
224
+ #
225
+ # Send client HTTP request
226
+ #
227
+ # @param [String] client HTTP request
228
+ #
229
+ # @return [Hash] HTTP response
230
+ #
231
+ def send_request(request)
232
+ response_error = {
233
+ 'uri' => '',
234
+ 'duration' => '0',
235
+ 'http_version' => '1.0',
236
+ 'headers' => "Server: #{@banner}\n",
237
+ 'body' => '' }
143
238
 
144
- end
239
+ # parse client request
240
+ begin
241
+ if request.to_s !~ %r{\A(CONNECT|GET|HEAD|DELETE|POST|PUT) https?://}
242
+ if request.to_s !~ /^Host: ([^\s]+)\r?\n/
243
+ logger.warn('No host specified')
244
+ raise SSRFProxy::HTTP::Error::InvalidClientRequest,
245
+ 'No host specified'
246
+ end
247
+ end
248
+ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
249
+ req.parse(StringIO.new(request))
250
+ rescue => e
251
+ logger.info('Received malformed client HTTP request.')
252
+ error_msg = "Error -- Invalid request: Received malformed client HTTP request: #{e.message}"
253
+ print_error(error_msg)
254
+ response_error['code'] = '502'
255
+ response_error['message'] = 'Bad Gateway'
256
+ response_error['status_line'] = "HTTP/#{response_error['http_version']}"
257
+ response_error['status_line'] << " #{response_error['code']}"
258
+ response_error['status_line'] << " #{response_error['message']}"
259
+ return response_error
260
+ end
261
+ uri = req.request_uri
145
262
 
146
- end
263
+ # send request
264
+ response = nil
265
+ logger.info("Sending request: #{uri}")
266
+ status_msg = "Request -> #{req.request_method}"
267
+ status_msg << " -> PROXY[#{@ssrf.proxy.host}:#{@ssrf.proxy.port}]" unless @ssrf.proxy.nil?
268
+ status_msg << " -> SSRF[#{@ssrf.host}:#{@ssrf.port}] -> URI[#{uri}]"
269
+ print_status(status_msg)
270
+
271
+ begin
272
+ response = @ssrf.send_request(request.to_s)
273
+ rescue SSRFProxy::HTTP::Error::InvalidClientRequest => e
274
+ logger.info(e.message)
275
+ error_msg = "Error -- Invalid request: #{e.message}"
276
+ print_error(error_msg)
277
+ response_error['code'] = '502'
278
+ response_error['message'] = 'Bad Gateway'
279
+ response_error['status_line'] = "HTTP/#{response_error['http_version']}"
280
+ response_error['status_line'] << " #{response_error['code']}"
281
+ response_error['status_line'] << " #{response_error['message']}"
282
+ return response_error
283
+ rescue SSRFProxy::HTTP::Error::ConnectionTimeout => e
284
+ logger.info(e.message)
285
+ error_msg = 'Response <- 504'
286
+ error_msg << " <- PROXY[#{@ssrf.proxy.host}:#{@ssrf.proxy.port}]" unless @ssrf.proxy.nil?
287
+ error_msg << " <- SSRF[#{@ssrf.host}:#{@ssrf.port}] <- URI[#{uri}]"
288
+ error_msg << " -- Error: #{e.message}"
289
+ print_error(error_msg)
290
+ response_error['code'] = '504'
291
+ response_error['message'] = 'Timeout'
292
+ response_error['status_line'] = "HTTP/#{response_error['http_version']}"
293
+ response_error['status_line'] << " #{response_error['code']}"
294
+ response_error['status_line'] << " #{response_error['message']}"
295
+ return response_error
296
+ rescue => e
297
+ logger.warn(e.message)
298
+ error_msg = "Error -- Unexpected error: #{e.message}"
299
+ print_error(error_msg)
300
+ response_error['code'] = '502'
301
+ response_error['message'] = 'Bad Gateway'
302
+ response_error['status_line'] = "HTTP/#{response_error['http_version']}"
303
+ response_error['status_line'] << " #{response_error['code']} "
304
+ response_error['status_line'] << " #{response_error['message']}"
305
+ return response_error
306
+ end
147
307
 
308
+ # return response
309
+ status_msg = "Response <- #{response['code']}"
310
+ status_msg << " <- PROXY[#{@ssrf.proxy.host}:#{@ssrf.proxy.port}]" unless @ssrf.proxy.nil?
311
+ status_msg << " <- SSRF[#{@ssrf.host}:#{@ssrf.port}] <- URI[#{uri}]"
312
+ status_msg << " -- Title[#{response['title']}]" unless response['title'].eql?('')
313
+ status_msg << " -- [#{response['body'].size} bytes]"
314
+ print_good(status_msg)
315
+ response
316
+ end
317
+
318
+ # private methods
319
+ private :print_status,
320
+ :print_good,
321
+ :print_error,
322
+ :shutdown,
323
+ :handle_connection,
324
+ :send_request,
325
+ :port_open?
326
+ end
327
+ end
@@ -1,10 +1,18 @@
1
- #!/usr/bin/env ruby
1
+ # coding: utf-8
2
2
  #
3
- # Copyright (c) 2015 Brendan Coles <bcoles@gmail.com>
3
+ # Copyright (c) 2015-2016 Brendan Coles <bcoles@gmail.com>
4
4
  # SSRF Proxy - https://github.com/bcoles/ssrf_proxy
5
- # See the file 'LICENSE' for copying permission
5
+ # See the file 'LICENSE.md' for copying permission
6
6
  #
7
7
 
8
8
  module SSRFProxy
9
- VERSION = "0.0.2"
9
+ # Gem version
10
+ VERSION = '0.0.3'.freeze
11
+ # Elite font ASCII art from: http://patorjk.com/software/taag/
12
+ BANNER = " \n" \
13
+ " .▄▄ · .▄▄ · ▄▄▄ ·▄▄▄ ▄▄▄·▄▄▄ ▐▄• ▄ ▄· ▄▌ \n" \
14
+ " ▐█ ▀. ▐█ ▀. ▀▄ █·▐▄▄· ▐█ ▄█▀▄ █·▪ █▌█▌▪▐█▪██▌ \n" \
15
+ " ▄▀▀▀█▄▄▀▀▀█▄▐▀▀▄ ██▪ ██▀·▐▀▀▄ ▄█▀▄ ·██· ▐█▌▐█▪ \n" \
16
+ " ▐█▄▪▐█▐█▄▪▐█▐█•█▌██▌. ▐█▪·•▐█•█▌▐█▌.▐▌▪▐█·█▌ ▐█▀·. \n" \
17
+ " ▀▀▀▀ ▀▀▀▀ .▀ ▀▀▀▀ .▀ .▀ ▀ ▀█▄▀▪•▀▀ ▀▀ ▀ • \n".freeze
10
18
  end
data/lib/ssrf_proxy.rb CHANGED
@@ -1,18 +1,45 @@
1
- #!/usr/bin/env ruby
1
+ # coding: utf-8
2
2
  #
3
- # Copyright (c) 2015 Brendan Coles <bcoles@gmail.com>
3
+ # Copyright (c) 2015-2016 Brendan Coles <bcoles@gmail.com>
4
4
  # SSRF Proxy - https://github.com/bcoles/ssrf_proxy
5
- # See the file 'LICENSE' for copying permission
5
+ # See the file 'LICENSE.md' for copying permission
6
6
  #
7
7
 
8
- require "ssrf_proxy/version"
9
- require "ssrf_proxy/http"
10
- require "ssrf_proxy/server"
8
+ # ouput
9
+ require 'logger'
10
+ require 'colorize'
11
+ String.disable_colorization = false
11
12
 
12
- module SSRFProxy
13
+ # proxy server
14
+ require 'socket'
13
15
 
14
- require 'logger'
15
- require 'colorize'
16
+ # threading
17
+ require 'celluloid/current'
18
+ require 'celluloid/io'
16
19
 
17
- end
20
+ # command line option parsing
21
+ require 'getoptlong'
18
22
 
23
+ # http requests
24
+ require 'net/http'
25
+ require 'socksify/http'
26
+
27
+ # http parsing
28
+ require 'uri'
29
+ require 'cgi'
30
+ require 'webrick'
31
+ require 'stringio'
32
+ require 'base64'
33
+ require 'htmlentities'
34
+
35
+ # client request url rules
36
+ require 'digest'
37
+ require 'base32'
38
+
39
+ # ip encoding
40
+ require 'ipaddress'
41
+
42
+ # SSRF Proxy gem libs
43
+ require 'ssrf_proxy/version'
44
+ require 'ssrf_proxy/http'
45
+ require 'ssrf_proxy/server'