ssrf_proxy 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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'