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.
- checksums.yaml +15 -0
- data/LICENSE.md +22 -0
- data/README.md +222 -0
- data/bin/console +24 -0
- data/bin/setup +6 -0
- data/bin/ssrf-proxy +170 -153
- data/lib/ssrf_proxy/http.rb +911 -1227
- data/lib/ssrf_proxy/server.rb +298 -118
- data/lib/ssrf_proxy/version.rb +12 -4
- data/lib/ssrf_proxy.rb +37 -10
- metadata +162 -39
- data/bin/ssrf-scan +0 -452
data/bin/ssrf-scan
DELETED
@@ -1,452 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
#
|
3
|
-
# Copyright (c) 2015 Brendan Coles <bcoles@gmail.com>
|
4
|
-
# SSRF Proxy - https://github.com/bcoles/ssrf_proxy
|
5
|
-
# See the file 'LICENSE' for copying permission
|
6
|
-
#
|
7
|
-
|
8
|
-
require 'getoptlong'
|
9
|
-
require 'ssrf_proxy'
|
10
|
-
|
11
|
-
def banner
|
12
|
-
puts "
|
13
|
-
_______________________________________________
|
14
|
-
___
|
15
|
-
___ ___ ___| _| ___ ___ ___ _ _ _ _
|
16
|
-
|_ -|_ -| _| _| | . | _| . |_'_| | |
|
17
|
-
|___|___|_| |_| | _|_| |___|_,_|_ |
|
18
|
-
|_| |___| ".blue
|
19
|
-
puts "
|
20
|
-
SSRF Proxy v0.0.2
|
21
|
-
https://github.com/bcoles/ssrf_proxy"
|
22
|
-
puts "
|
23
|
-
_______________________________________________
|
24
|
-
".blue
|
25
|
-
end
|
26
|
-
|
27
|
-
def usage
|
28
|
-
|
29
|
-
puts "Usage: ssrf-scan [options] -u <SSRF URL> --host <HOST>"
|
30
|
-
puts "Example: ssrf-scan -u http://target/?url=xxURLxx"
|
31
|
-
puts "Options:"
|
32
|
-
puts "
|
33
|
-
-h, --help Help
|
34
|
-
-v, --verbose Verbose output
|
35
|
-
-d, --debug Debugging output
|
36
|
-
|
37
|
-
Scan options:
|
38
|
-
--host=HOST IP address or hostname to scan
|
39
|
-
(Default: 127.0.0.1)
|
40
|
-
--port=PORT Port to scan (Default: 80)
|
41
|
-
|
42
|
-
Connection options:
|
43
|
-
--proxy=PROXY Upstream HTTP proxy
|
44
|
-
|
45
|
-
SSRF options:
|
46
|
-
-u, --url=URL SSRF URL with 'xxURLxx' placeholder
|
47
|
-
--method=METHOD HTTP method (GET/POST/HEAD) (Default: GET)
|
48
|
-
--post-data=DATA HTTP post data
|
49
|
-
--cookie=COOKIE HTTP cookies (separated by ';')
|
50
|
-
--user-agent=AGENT HTTP user-agent (Default: Mozilla/5.0)
|
51
|
-
--timeout=SECONDS Connection timeout in seconds (Default: 10)
|
52
|
-
--ip-encoding=MODE Encode IP address for blacklist evasion.
|
53
|
-
(Modes: int, ipv6, oct, hex) (Default: none)
|
54
|
-
--rules=RULES Rules for parsing client request for xxURLxx
|
55
|
-
(separated by ',') (Default: none)
|
56
|
-
|
57
|
-
Response modification:
|
58
|
-
--match=REGEX Regex to match response body content.
|
59
|
-
(Default: \\A(.+)\\z)
|
60
|
-
--strip=HEADERS Headers to remove from the response.
|
61
|
-
(separated by ',') (Default: none)
|
62
|
-
--guess-status Replaces response status code and message
|
63
|
-
headers (determined by common strings in the
|
64
|
-
response body, such as 404 Not Found.)
|
65
|
-
--guess-mime Replaces response content-type header with the
|
66
|
-
appropriate mime type (determined by the file
|
67
|
-
extension of the requested resource.)
|
68
|
-
|
69
|
-
"
|
70
|
-
exit 1
|
71
|
-
end
|
72
|
-
|
73
|
-
#
|
74
|
-
# @note parse options and start scan
|
75
|
-
#
|
76
|
-
def start_scan
|
77
|
-
|
78
|
-
# get args
|
79
|
-
opts = GetoptLong.new(
|
80
|
-
[ '-h', '--help', GetoptLong::NO_ARGUMENT ],
|
81
|
-
[ '-v', '--verbose', GetoptLong::NO_ARGUMENT ],
|
82
|
-
[ '-d', '--debug', GetoptLong::NO_ARGUMENT ],
|
83
|
-
|
84
|
-
[ '--host', GetoptLong::REQUIRED_ARGUMENT ],
|
85
|
-
[ '--port', GetoptLong::REQUIRED_ARGUMENT ],
|
86
|
-
|
87
|
-
[ '--proxy', GetoptLong::REQUIRED_ARGUMENT ],
|
88
|
-
|
89
|
-
[ '-u', '--url', GetoptLong::REQUIRED_ARGUMENT ],
|
90
|
-
[ '--method', GetoptLong::REQUIRED_ARGUMENT ],
|
91
|
-
[ '--post-data', GetoptLong::REQUIRED_ARGUMENT ],
|
92
|
-
[ '--cookie', GetoptLong::REQUIRED_ARGUMENT ],
|
93
|
-
[ '--user-agent', GetoptLong::REQUIRED_ARGUMENT ],
|
94
|
-
[ '--timeout', GetoptLong::REQUIRED_ARGUMENT ],
|
95
|
-
|
96
|
-
[ '--ip-encoding', GetoptLong::REQUIRED_ARGUMENT ],
|
97
|
-
[ '--rules', GetoptLong::REQUIRED_ARGUMENT ],
|
98
|
-
[ '--forward-cookies', GetoptLong::NO_ARGUMENT ],
|
99
|
-
[ '--body-to-uri', GetoptLong::NO_ARGUMENT ],
|
100
|
-
[ '--auth-to-uri', GetoptLong::NO_ARGUMENT ],
|
101
|
-
|
102
|
-
[ '--match', GetoptLong::REQUIRED_ARGUMENT ],
|
103
|
-
[ '--strip', GetoptLong::REQUIRED_ARGUMENT ],
|
104
|
-
[ '--guess-status', GetoptLong::NO_ARGUMENT ],
|
105
|
-
[ '--guess-mime', GetoptLong::NO_ARGUMENT ]
|
106
|
-
)
|
107
|
-
|
108
|
-
# scan option
|
109
|
-
host = '127.0.0.1'
|
110
|
-
port = 80
|
111
|
-
|
112
|
-
# ssrf details
|
113
|
-
url = nil
|
114
|
-
rules = ''
|
115
|
-
ip_encoding = ''
|
116
|
-
method = 'GET'
|
117
|
-
post_data = ''
|
118
|
-
match = "\\A(.+)\\z"
|
119
|
-
strip = ''
|
120
|
-
guess_mime = false
|
121
|
-
guess_status = false
|
122
|
-
forward_cookies = false
|
123
|
-
body_to_uri = false
|
124
|
-
auth_to_uri = false
|
125
|
-
|
126
|
-
# http connection defaults
|
127
|
-
cookie = ''
|
128
|
-
timeout = 10
|
129
|
-
upstream_proxy = nil
|
130
|
-
user_agent = 'Mozilla/5.0'
|
131
|
-
|
132
|
-
# logging
|
133
|
-
log_level = ::Logger::WARN
|
134
|
-
|
135
|
-
# handle args
|
136
|
-
opts.each do |opt, arg|
|
137
|
-
case opt
|
138
|
-
when '-u','--url'
|
139
|
-
url = arg
|
140
|
-
when '--host'
|
141
|
-
host = arg
|
142
|
-
when '--port'
|
143
|
-
port = arg
|
144
|
-
when '--rules'
|
145
|
-
rules=arg
|
146
|
-
when '--proxy'
|
147
|
-
upstream_proxy = URI::parse(arg)
|
148
|
-
when '--cookie'
|
149
|
-
cookie=arg
|
150
|
-
when '--timeout'
|
151
|
-
timeout=arg
|
152
|
-
when '--user-agent'
|
153
|
-
user_agent=arg
|
154
|
-
when '--method'
|
155
|
-
method=arg
|
156
|
-
when '--post-data'
|
157
|
-
post_data=arg
|
158
|
-
when '--match'
|
159
|
-
match=arg
|
160
|
-
when '--strip'
|
161
|
-
strip=arg
|
162
|
-
when '--guess-status'
|
163
|
-
guess_status = true
|
164
|
-
when '--guess-mime'
|
165
|
-
guess_mime = true
|
166
|
-
when '--forward-cookies'
|
167
|
-
forward_cookies = true
|
168
|
-
when '--body-to-uri'
|
169
|
-
body_to_uri = true
|
170
|
-
when '--auth-to-uri'
|
171
|
-
auth_to_uri = true
|
172
|
-
when '-h','--help'
|
173
|
-
usage
|
174
|
-
when '-v','--verbose'
|
175
|
-
log_level = ::Logger::INFO unless log_level == ::Logger::DEBUG
|
176
|
-
when '-d','--debug'
|
177
|
-
log_level = ::Logger::DEBUG
|
178
|
-
end
|
179
|
-
end
|
180
|
-
|
181
|
-
opts = {
|
182
|
-
'proxy' => "#{upstream_proxy}",
|
183
|
-
'method' => "#{method}",
|
184
|
-
'post_data' => "#{post_data}",
|
185
|
-
'rules' => "#{rules}",
|
186
|
-
'match' => "#{match}",
|
187
|
-
'strip' => "#{strip}",
|
188
|
-
'guess_status' => "#{guess_status}",
|
189
|
-
'guess_mime' => "#{guess_mime}",
|
190
|
-
'forward_cookies'=> "#{forward_cookies}",
|
191
|
-
'body_to_uri' => "#{body_to_uri}",
|
192
|
-
'auth_to_uri' => "#{auth_to_uri}",
|
193
|
-
'cookie' => "#{cookie}",
|
194
|
-
'timeout' => "#{timeout}",
|
195
|
-
'user_agent' => "#{user_agent}"
|
196
|
-
}
|
197
|
-
|
198
|
-
# setup ssrf
|
199
|
-
ssrf = SSRFProxy::HTTP.new(url, opts)
|
200
|
-
ssrf.logger.level = log_level
|
201
|
-
|
202
|
-
# start scanner
|
203
|
-
scan = SSRFProxyScanner.new(ssrf, opts, host)
|
204
|
-
scan.logger.level = log_level
|
205
|
-
|
206
|
-
end
|
207
|
-
|
208
|
-
#
|
209
|
-
# @note SSRFProxyScanner
|
210
|
-
# - version: 0.0.2
|
211
|
-
#
|
212
|
-
class SSRFProxyScanner
|
213
|
-
|
214
|
-
# @note output status messages
|
215
|
-
def print_status(msg='')
|
216
|
-
puts '[*] '.blue + msg
|
217
|
-
end
|
218
|
-
# @note output progress messages
|
219
|
-
def print_good(msg='')
|
220
|
-
puts '[+] '.green + msg
|
221
|
-
end
|
222
|
-
|
223
|
-
attr_accessor :logger
|
224
|
-
|
225
|
-
# @note logging
|
226
|
-
def logger
|
227
|
-
@logger || ::Logger.new(STDOUT).tap do |log|
|
228
|
-
log.progname = 'ssrf-proxy-scanner'
|
229
|
-
log.level = ::Logger::WARN
|
230
|
-
log.datetime_format = '%Y-%m-%d %H:%M:%S '
|
231
|
-
end
|
232
|
-
end
|
233
|
-
|
234
|
-
# @note SSRFProxyScanner errors
|
235
|
-
module Error
|
236
|
-
# custom errors
|
237
|
-
class Error < StandardError; end
|
238
|
-
exceptions = %w( InvalidSsrf )
|
239
|
-
exceptions.each { |e| const_set(e, Class.new(Error)) }
|
240
|
-
end
|
241
|
-
|
242
|
-
#
|
243
|
-
# @note Start the scanner
|
244
|
-
#
|
245
|
-
# @options
|
246
|
-
# - ssrf - SSRFProxy::HTTP - SSRF
|
247
|
-
# - opts - Hash - SSRF and HTTP connection options
|
248
|
-
# - host - String - target host to scan
|
249
|
-
# - port - Integer - target port to scan
|
250
|
-
#
|
251
|
-
def initialize(ssrf, opts = {}, host='127.0.0.1', port=80)
|
252
|
-
return unless ssrf.class == SSRFProxy::HTTP
|
253
|
-
|
254
|
-
@report = []
|
255
|
-
@ssrf = ssrf
|
256
|
-
|
257
|
-
scan_host host, port
|
258
|
-
|
259
|
-
# output report
|
260
|
-
puts "#{'-'*60}\n"
|
261
|
-
@report.sort.each do |r|
|
262
|
-
print_good "#{r.join(' :: ')}\n"
|
263
|
-
end
|
264
|
-
|
265
|
-
end
|
266
|
-
|
267
|
-
#
|
268
|
-
# @note Report accessor
|
269
|
-
#
|
270
|
-
# @returns Array - findings
|
271
|
-
#
|
272
|
-
def report
|
273
|
-
@report
|
274
|
-
end
|
275
|
-
|
276
|
-
private
|
277
|
-
|
278
|
-
# @note request the specified url via SSRF
|
279
|
-
def request url
|
280
|
-
begin
|
281
|
-
response = @ssrf.send_uri(url)
|
282
|
-
rescue => e
|
283
|
-
logger.error "Error: #{e.message}"
|
284
|
-
end
|
285
|
-
return response.to_s
|
286
|
-
end
|
287
|
-
|
288
|
-
# @note port scan common ports for the specified host
|
289
|
-
def scan_common_ports host
|
290
|
-
[21,22,23,80,81,443,631,3128,3306,3389,4444,5432,6666,6789,8080,8081,8082,8443,10000,10040,10443,11211].each do |port|
|
291
|
-
url = "http://#{host}:#{port}/#{rand(10)}"
|
292
|
-
res = request "#{url}".unpack("C*").pack("U*")
|
293
|
-
#next if res =~ /failed to open stream: Connection refused/ # php: port is closed
|
294
|
-
@report << [url, 'SSH'] if port == 22 && res =~ /SSH/
|
295
|
-
@report << [url, 'CUPS'] if port == 631 && res =~ /CUPS/
|
296
|
-
@report << [url, 'MySQL'] if port == 3306 && res =~ /Got packets out of order/
|
297
|
-
@report << [url, 'Groovy Shell'] if port == 6789 && res =~ /Groovy Shell/
|
298
|
-
@report << [url, 'Apache Felix Shell'] if port == 6666 && res =~ /Felix/i
|
299
|
-
@report << [url, 'memcached'] if port == 11211 && res =~ /memcached/
|
300
|
-
end
|
301
|
-
end
|
302
|
-
|
303
|
-
# @note check the first ip in all /24s
|
304
|
-
def ping_sweep_network(network)
|
305
|
-
print_status "Scanning #{network}.x.x.1"
|
306
|
-
255.times do |i|
|
307
|
-
255.times do |j|
|
308
|
-
request "http://#{network}.#{i}.#{j}.1/"
|
309
|
-
end
|
310
|
-
end
|
311
|
-
end
|
312
|
-
|
313
|
-
# @note scan for vendor docs directory
|
314
|
-
def scan_doc host, port=80
|
315
|
-
url = "http://#{host}/doc/"
|
316
|
-
res = request url
|
317
|
-
@report << [url, 'Documentation'] if res =~ /Parent Directory/
|
318
|
-
end
|
319
|
-
|
320
|
-
# @note scan for vendor icons directory
|
321
|
-
def scan_icons host, port=80
|
322
|
-
url = "http://#{host}/icons/"
|
323
|
-
res = request url
|
324
|
-
@report << [url, 'Icons'] if res =~ /Parent Directory/
|
325
|
-
end
|
326
|
-
|
327
|
-
# @note scan for CUPS
|
328
|
-
def scan_cups host, port=631
|
329
|
-
url = "http://#{host}:#{port}/"
|
330
|
-
res = request url
|
331
|
-
@report << [url, 'CUPS'] if res =~ /CUPS/
|
332
|
-
end
|
333
|
-
|
334
|
-
# @note scan for mod_status (/server-status)
|
335
|
-
def scan_mod_status host, port=80
|
336
|
-
url = "http://#{host}:#{port}/server-status"
|
337
|
-
res = request url
|
338
|
-
@report << [url, 'mod_status'] if res =~ /Server Status/
|
339
|
-
end
|
340
|
-
|
341
|
-
# @note scan for Tomcat management interface
|
342
|
-
def scan_tomcat host, port=8080
|
343
|
-
url = "http://#{host}:#{port}/manager/html"
|
344
|
-
res = request url
|
345
|
-
@report << [url, 'Tomcat Manager'] if res =~ /Tomcat Manager/
|
346
|
-
end
|
347
|
-
|
348
|
-
# @note scan for Jenkins script console
|
349
|
-
def scan_jenkins_script host, port=8080
|
350
|
-
url = "http://#{host}:#{port}/jenkins/script"
|
351
|
-
res = request url
|
352
|
-
@report << [url, 'Jenkins Script Console'] if res =~ /Script Console/
|
353
|
-
end
|
354
|
-
|
355
|
-
# @note scan for WordPress
|
356
|
-
def scan_wordpress host, port=80
|
357
|
-
url = "http://#{host}:#{port}/wp-admin/"
|
358
|
-
res = request url
|
359
|
-
@report << [url, 'WordPress'] if res =~ /WordPress/i
|
360
|
-
url = "http://#{host}:#{port}/wordpress/"
|
361
|
-
res = request url
|
362
|
-
@report << [url, 'WordPress'] if res =~ /wp-content/i
|
363
|
-
end
|
364
|
-
|
365
|
-
# @note scan for phpMyAdmin
|
366
|
-
def scan_phpmyadmin host, port=80
|
367
|
-
url = "http://#{host}:#{port}/phpmyadmin/"
|
368
|
-
res = request url
|
369
|
-
@report << [url, 'phpMyAdmin'] if res =~ /phpMyAdmin/
|
370
|
-
end
|
371
|
-
|
372
|
-
# @note scan for Groovy Shell
|
373
|
-
def scan_groovy_shell host, port=6789
|
374
|
-
url = "http://#{host}:#{port}/"
|
375
|
-
res = request url
|
376
|
-
@report << [url, 'Groovy Shell'] if res =~ /Groovy Shell/
|
377
|
-
end
|
378
|
-
|
379
|
-
# @note scan for redis
|
380
|
-
def scan_redis host, port=6379
|
381
|
-
url = "http://#{host}:#{port}/1"
|
382
|
-
res = request url
|
383
|
-
@report << [url, 'Redis'] if res =~ /redis/i
|
384
|
-
end
|
385
|
-
|
386
|
-
# @note scan for couchdb
|
387
|
-
def scan_couchdb host, port=5984
|
388
|
-
url = "http://#{host}:#{port}/_users/_all_docs"
|
389
|
-
res = request url
|
390
|
-
@report << [url, 'CouchDB'] if res =~ /"total_rows"/
|
391
|
-
end
|
392
|
-
|
393
|
-
# @note scan for /Trace.axd
|
394
|
-
def trace_axd
|
395
|
-
url = "http://#{host}:#{port}/trace.axd"
|
396
|
-
res = request url
|
397
|
-
@report << [url, 'Trace.axd'] if res =~ /Trace/
|
398
|
-
end
|
399
|
-
|
400
|
-
# @note scan for Ruby Gem Server
|
401
|
-
def scan_gems host, port=8808
|
402
|
-
url = "http://#{host}:#{port}/"
|
403
|
-
res = request url
|
404
|
-
@report << [url, 'Ruby Gems Server'] if res =~ /RubyGems Documentation Index/
|
405
|
-
end
|
406
|
-
|
407
|
-
# @note scan XAMPP
|
408
|
-
def scan_xampp host, port=80
|
409
|
-
url = "http://#{host}:#{port}/xampp/"
|
410
|
-
res = request url
|
411
|
-
@report << [url, 'XAMPP'] if res =~ /XAMPP/
|
412
|
-
url = "http://#{host}:#{port}/xampp/phpinfo.php"
|
413
|
-
res = request url
|
414
|
-
@report << [url, 'XAMPP'] if res =~ /XAMPP/
|
415
|
-
end
|
416
|
-
|
417
|
-
# @note scan a host
|
418
|
-
def scan_host host, port
|
419
|
-
#print_status "Beginning port scan (#{host})"
|
420
|
-
#scan_common_ports host
|
421
|
-
|
422
|
-
print_status "Beginning app enumeration (#{host}:#{port})"
|
423
|
-
scan_port host, port
|
424
|
-
|
425
|
-
print_status "Beginning service enumeration (#{host})"
|
426
|
-
scan_cups host
|
427
|
-
scan_groovy_shell host
|
428
|
-
scan_couchdb host
|
429
|
-
scan_jenkins_script host
|
430
|
-
scan_redis host
|
431
|
-
scan_gems host
|
432
|
-
|
433
|
-
print_good "Scan complete (#{host})"
|
434
|
-
end
|
435
|
-
|
436
|
-
# @note scan a port for web apps
|
437
|
-
def scan_port host, port=80
|
438
|
-
scan_mod_status host, port
|
439
|
-
scan_doc host, port
|
440
|
-
scan_icons host, port
|
441
|
-
scan_xampp host, port
|
442
|
-
scan_wordpress host, port
|
443
|
-
scan_tomcat host, port
|
444
|
-
scan_phpmyadmin host, port
|
445
|
-
end
|
446
|
-
|
447
|
-
end
|
448
|
-
|
449
|
-
banner
|
450
|
-
usage if ARGV.length == 0
|
451
|
-
start_scan
|
452
|
-
|