CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/auxiliary/gather/cloud_lookup.rb
Views: 1904
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
require 'public_suffix'
7
8
class MetasploitModule < Msf::Auxiliary
9
include Msf::Exploit::Remote::DNS::Enumeration
10
include Msf::Auxiliary::Report
11
12
def initialize(info = {})
13
super(
14
update_info(
15
info,
16
'Name' => 'Cloud Lookup (and Bypass)',
17
'Description' => %q{
18
This module can be useful if you need to test the security of your server and your
19
website behind a solution Cloud based. By discovering the origin IP address of the
20
targeted host.
21
22
More precisely, this module uses multiple data sources (in order ViewDNS.info, DNS enumeration
23
and Censys) to collect assigned (or have been assigned) IP addresses from the targeted site or domain
24
that uses the following:
25
* Cloudflare, Amazon CloudFront, ArvanCloud, Envoy Proxy, Fastly, Stackpath Fireblade,
26
Stackpath MaxCDN, Imperva Incapsula, InGen Security (BinarySec EasyWAF), KeyCDN, Microsoft AzureCDN,
27
Netlify and Sucuri.
28
},
29
'Author' => [
30
'mekhalleh (RAMELLA Sébastien)', # https://www.pirates.re/
31
'Yvain'
32
],
33
'References' => [
34
['URL', 'https://citadelo.com/en/blog/cloudflare-how-to-do-it-right-and-do-not-reveal-your-real-ip/']
35
],
36
'DefaultOptions' => { 'DnsNote' => false },
37
'License' => MSF_LICENSE,
38
'Actions' => [
39
['Automatic', {}],
40
[
41
'CloudFlare', {
42
'Description' => 'Cloudflare provides SaaS based CDN, WAF, DNS and DDoS mitigation services.',
43
'Signatures' => ['server: cloudflare']
44
}
45
],
46
[
47
'Amazon CloudFront', {
48
'Description' => 'Content Delivery Network services of Amazon',
49
'Signatures' => ['x-amz-cf-id:']
50
}
51
],
52
[
53
'ArvanCloud CDN', {
54
'Description' => 'ArvanCloud CDN comprises tens of PoP sites in important locations all around the world to deliver online content to the users',
55
'Signatures' => ['server: ArvanCloud']
56
}
57
],
58
[
59
'AzureCDN', {
60
'Description' => 'Microsoft Azure Content Delivery Network (CDN) is a global content distribution network solution for delivering high bandwidth content',
61
'Signatures' => []
62
}
63
],
64
[
65
'Envoy Proxy', {
66
'Description' => 'An open source edge and service proxy, designed for Cloud-Native applications',
67
'Signatures' => ['server: envoy']
68
}
69
],
70
[
71
'Fastly', {
72
'Description' => 'Another widely used CDN/WAF solution',
73
'Signatures' => ['Fastly-SSL']
74
}
75
],
76
[
77
'Imperva Incapsula', {
78
'Description' => 'Cloud based Web application firewall of Imperva',
79
'Signatures' => ['X-CDN: Incapsula', '_incap_']
80
}
81
],
82
[
83
'InGen Security (BinarySec EasyWAF)', { # Reunion island powa!
84
'Description' => 'Cloud based Web application firewall of InGen Security and BinarySec',
85
'Signatures' => ['binarysec', 'server: gatejs']
86
}
87
],
88
[
89
'KeyCDN', {
90
'Description' => 'KeyCDN is a high performance content delivery network that has been built for the future', # lol
91
'Signatures' => ['Server: keycdn-engine']
92
}
93
],
94
[
95
'Netlifi', {
96
'Description' => 'One workflow, from local development to global deployment',
97
'Signatures' => ['x-nf-request-id:']
98
}
99
],
100
[
101
'NoWAFBypass', {
102
'Description' => 'Do NOT check any bypass method',
103
'Signatures' => []
104
}
105
],
106
[
107
'Stackpath Fireblade', {
108
'Description' => 'Enterprise Website Security & DDoS Protection',
109
'Signatures' => ['Server: fbs']
110
}
111
],
112
[
113
'Stackpath MaxCDN', {
114
'Description' => 'Speed Up your Content Delivery',
115
'Signatures' => ['Server: NetDNA-cache']
116
}
117
],
118
[
119
'Sucuri', {
120
'Description' => 'Cloud based Web application firewall of Sucuri',
121
'Signatures' => ['x-sucuri-id:']
122
}
123
],
124
],
125
'DefaultAction' => 'Automatic',
126
'Notes' => {
127
'Stability' => [],
128
'Reliability' => [],
129
'SideEffects' => [IOC_IN_LOGS]
130
}
131
)
132
)
133
134
register_options([
135
OptString.new('CENSYS_SECRET', [false, 'The Censys API SECRET']),
136
OptString.new('CENSYS_UID', [false, 'The Censys API UID']),
137
OptString.new('COMPSTR', [false, 'You can use a custom string to perform the comparison (read documentation)']),
138
OptString.new('HOSTNAME', [true, 'The hostname or domain name where we want to find the real IP address']),
139
OptPath.new('IPBLACKLIST_FILE', [false, 'Files containing IP addresses to blacklist during the analysis process, one per line', nil]),
140
OptString.new('Proxies', [false, 'A proxy chain of format type:host:port[,type:host:port][...]']),
141
OptInt.new('RPORT', [true, 'The target TCP port on which the protected website responds', 443]),
142
OptBool.new('SSL', [true, 'Negotiate SSL/TLS for outgoing connections', true]),
143
OptInt.new('THREADS', [true, 'Threads for DNS enumeration', 8]),
144
OptString.new('URIPATH', [true, 'The URI path on which to perform the page comparison', '/']),
145
OptPath.new('WORDLIST', [false, 'Wordlist of subdomains', ::File.join(Msf::Config.data_directory, 'wordlists', 'namelist.txt')])
146
])
147
148
register_advanced_options([
149
OptBool.new('ALLOW_NOWAF', [true, 'Automatically switch to NoWAFBypass when detection fails with the Automatic action', false]),
150
OptBool.new('ENUM_BRT', [true, 'Set DNS bruteforce as optional', true]),
151
OptBool.new('REPORT_LEAKS', [true, 'Set to write leaked ip addresses in notes', false]),
152
OptString.new('USERAGENT', [true, 'Specify a personalized User-Agent header in HTTP requests', Rex::UserAgent.session_agent]),
153
OptEnum.new('TAG', [true, 'Specify the HTML tag in which you want to find the fingerprint', 'title', ['title', 'html']]),
154
OptInt.new('HTTP_TIMEOUT', [true, 'HTTP(s) request timeout', 8]),
155
])
156
end
157
158
def setup_resolver
159
dns_resolver = super
160
# set the DNS port explicitly so it does not conflict with the datastore
161
# RPORT value which is for HTTP
162
dns_resolver.port = 53
163
@dns_resolver = dns_resolver
164
end
165
166
# ------------------------------------------------------------------------- #
167
168
# auxiliary/gather/censys_search.rb
169
def censys_search(keyword, uid, secret)
170
begin
171
cli = Rex::Proto::Http::Client.new('search.censys.io', 443, {}, true, nil, datastore['Proxies'])
172
cli.connect
173
174
response = cli.request_cgi(
175
'method' => 'GET',
176
'uri' => "/api/v2/hosts/search?q=#{keyword}",
177
'agent' => datastore['USERAGENT'],
178
'headers' => {
179
'Authorization' => "Basic #{Rex::Text.encode_base64("#{uid}:#{secret}")}"
180
}
181
)
182
results = cli.send_recv(response)
183
rescue ::Rex::ConnectionError, Errno::ECONNREFUSED, Errno::ETIMEDOUT
184
print_error('HTTP connection failed to Censys.IO website.')
185
end
186
187
unless results
188
print_error('Unable to retrieve any data from Censys.IO website.')
189
return []
190
end
191
192
records = ActiveSupport::JSON.decode(results.body)
193
194
results = records['result']
195
parse_ipv4(results)
196
end
197
198
def check_tcp_port(ip, port)
199
begin
200
sock = Rex::Socket::Tcp.create(
201
'PeerHost' => ip,
202
'PeerPort' => port,
203
'Proxies' => datastore['Proxies']
204
)
205
rescue ::Rex::ConnectionRefused, Rex::ConnectionError
206
vprint_status(" * Closed: tcp://#{ip}:#{port}/")
207
return false
208
end
209
210
sock.close
211
return true
212
end
213
214
def wrap_mx(records)
215
ar_ips = []
216
records.each.map do |r|
217
a = /(\d*\.\d*\.\d*\.\d*)/.match(dns_get_a(r.exchange.to_s).to_s)
218
ar_ips.push(a) if a
219
end
220
ar_ips
221
end
222
223
def grab_domain_ip_history(domain)
224
begin
225
cli = Rex::Proto::Http::Client.new('viewdns.info', 443, {}, true, nil, datastore['Proxies'])
226
cli.connect
227
228
request = cli.request_cgi({
229
'method' => 'GET',
230
'uri' => "/iphistory/?domain=#{domain}",
231
'agent' => datastore['USERAGENT']
232
})
233
response = cli.send_recv(request)
234
cli.close
235
rescue ::Rex::ConnectionError, Errno::ECONNREFUSED, Errno::ETIMEDOUT
236
print_error('HTTP connection failed to ViewDNS.info website.')
237
return []
238
end
239
240
unless response
241
print_error('Unable to retrieve any data from ViewDNS.info website.')
242
return []
243
end
244
245
html = response.get_html_document
246
table = html.css('table')[3]
247
248
unless table.nil?
249
rows = table.css('tr')
250
251
ar_ips = []
252
rows.each.map do |row|
253
row = /(\d*\.\d*\.\d*\.\d*)/.match(row.css('td').map(&:text).to_s)
254
unless row.nil?
255
ar_ips.push(row)
256
end
257
end
258
end
259
260
if ar_ips.nil?
261
print_bad('No domain IP(s) history founds.')
262
return []
263
end
264
265
ar_ips
266
end
267
268
def http_get_request_raw(host, port, ssl, uri, vhost = nil)
269
begin
270
http = Rex::Proto::Http::Client.new(host, port, {}, ssl, nil, datastore['Proxies'])
271
http.connect(datastore['HTTP_TIMEOUT'])
272
273
unless vhost.nil?
274
http.set_config({ 'vhost' => vhost })
275
end
276
277
request = http.request_raw({
278
'method' => 'GET',
279
'uri' => uri,
280
'agent' => datastore['USERAGENT']
281
})
282
response = http.send_recv(request)
283
http.close
284
rescue ::Rex::ConnectionError, Errno::ECONNREFUSED, Errno::ETIMEDOUT
285
# noop
286
rescue ::StandardError => e
287
print_error(e.message)
288
end
289
return false if response.nil?
290
291
response
292
end
293
294
# auxiliary/gather/censys_search.rb
295
def parse_ipv4(records)
296
ip_list = []
297
records['hits'].each do |ipv4|
298
ip_list.push(ipv4['ip'])
299
end
300
ip_list
301
end
302
303
def save_note(hostname, ip, port, proto, ebypass)
304
data = { 'vhost' => hostname, 'detected_ip' => ip, 'action' => @my_action.name, 'effective_bypass' => ebypass }
305
report_note(
306
host: hostname,
307
service: proto,
308
port: port,
309
type: 'auxiliary.gather.cloud_lookup',
310
data: data,
311
update: :unique_data
312
)
313
end
314
315
# ------------------------------------------------------------------------- #
316
317
def check_bypass(fingerprint, ip)
318
# Check for "misconfigured" web server on TCP/80.
319
if check_tcp_port(ip, 80)
320
ret_value ||= check_request(fingerprint, ip, 80, false)
321
end
322
323
# Check for "misconfigured" web server on TCP/443.
324
if check_tcp_port(ip, 443)
325
ret_value ||= check_request(fingerprint, ip, 443, true)
326
end
327
328
ret_value
329
end
330
331
def check_request(fingerprint, ip, port, ssl)
332
proto = (ssl ? 'https' : 'http')
333
334
vprint_status(" * Trying: #{proto}://#{ip}:#{port}/")
335
response = http_get_request_raw(ip, port, ssl, datastore['URIPATH'], datastore['HOSTNAME'])
336
337
if response
338
return false if detect_signature(response)
339
340
if response.code == 200
341
found = false
342
343
html = response.get_html_document
344
begin
345
# Searches for the chain to compare in the defined tag.
346
found = true if html.at(datastore['TAG']).to_s.include? fingerprint.to_s.encode('utf-8')
347
rescue NoMethodError, ::Encoding::CompatibilityError
348
return false
349
end
350
351
if found
352
print_good("A direct-connect IP address was found: #{proto}://#{ip}:#{port}/")
353
if @my_action.name == 'NoWAFBypass'
354
save_note(datastore['HOSTNAME'], ip, port, proto, 'manual check claimed')
355
else
356
save_note(datastore['HOSTNAME'], ip, port, proto, true)
357
end
358
return true
359
end
360
361
elsif response.redirect?
362
found = false
363
364
vprint_line(" --> responded with HTTP status code: #{response.code} to #{response.headers['location']}")
365
begin
366
found = true if response.headers['location'].include?(datastore['hostname'])
367
rescue NoMethodError, ::Encoding::CompatibilityError
368
return false
369
end
370
371
if found
372
print_warning("A leaked IP address was found: #{proto}://#{ip}:#{port}/")
373
save_note(datastore['HOSTNAME'], ip, port, proto, false) if datastore['REPORT_LEAKS']
374
end
375
376
else
377
vprint_line(" --> responded with an unhandled HTTP status code: #{response.code}")
378
end
379
end
380
381
return false
382
end
383
384
def detect_signature(data)
385
@my_action['Signatures'].each do |signature|
386
return true if data.headers.to_s.downcase.include?(signature.downcase)
387
end
388
return false
389
end
390
391
def arvancloud_ips
392
response = http_get_request_raw(
393
'www.arvancloud.com',
394
443,
395
true,
396
'/en/ips.txt'
397
)
398
return [] if response.nil?
399
400
response.get_html_document.text.split("\n")
401
end
402
403
# https://docs.microsoft.com/fr-fr/azure/cdn/cdn-pop-list-api
404
def azurecdn_ips
405
regions = {
406
'region' => [
407
'asiaeast', 'asiasoutheast', 'australiaeast', 'australiasoutheast', 'canadacentral',
408
'canadaeast', 'chinaeast', 'chinanorth', 'europenorth', 'europewest',
409
'germanycentral', 'germanyn', 'germanynortheast', 'indiacentral', 'indiasouth',
410
'indiawest', 'japaneast', 'japanwest', 'brazilsouth', 'koreasouth',
411
'koreacentral', 'ukwest', 'uksouth', 'uscentral', 'useast',
412
'useast2', 'usnorth', 'ussouth', 'uswestcentral', 'uswest',
413
'uswest2'
414
]
415
}
416
params = regions.merge({
417
'complement' => 'on',
418
'outputformat' => 'list-cidr'
419
})
420
421
begin
422
cli = Rex::Proto::Http::Client.new('azurerange.azurewebsites.net', 443, {}, true, nil, datastore['Proxies'])
423
cli.connect
424
425
response = cli.request_cgi(
426
'method' => 'GET',
427
'uri' => '/Download/',
428
'agent' => datastore['USERAGENT'],
429
'vars_get' => params
430
)
431
results = cli.send_recv(response)
432
rescue ::Rex::ConnectionError, Errno::ECONNREFUSED, Errno::ETIMEDOUT
433
print_error('HTTP connection failed to Azurerange website.')
434
end
435
436
unless results
437
print_error('Unable to retrieve any data from Azurerange website.')
438
return []
439
end
440
441
results.get_html_document.css('p').text.split("\r\n")
442
end
443
444
def cloudflare_ips
445
response = http_get_request_raw(
446
'www.cloudflare.com',
447
443,
448
true,
449
'/ips-v4'
450
)
451
return [] if response.nil?
452
453
response.get_html_document.css('p').text.split("\n")
454
end
455
456
def cloudfront_ips
457
response = http_get_request_raw(
458
'd7uri8nf7uskq.cloudfront.net',
459
443,
460
true,
461
'/tools/list-cloudfront-ips'
462
)
463
return [] if response.nil?
464
465
ip_list = response.get_json_document['CLOUDFRONT_GLOBAL_IP_LIST']
466
ip_list += response.get_json_document['CLOUDFRONT_REGIONAL_EDGE_IP_LIST']
467
468
ip_list.map { |ip| ip.gsub('"', '') }
469
end
470
471
def fastly_ips
472
response = http_get_request_raw(
473
'api.fastly.com',
474
443,
475
true,
476
'/public-ip-list'
477
)
478
return [] if response.nil?
479
480
response.get_json_document['addresses'].map { |ip| ip.gsub('"', '') }
481
end
482
483
def incapsula_ips
484
begin
485
cli = Rex::Proto::Http::Client.new('my.incapsula.com', 443, {}, true, nil, datastore['Proxies'])
486
cli.connect
487
488
response = cli.request_cgi(
489
'method' => 'POST',
490
'uri' => '/api/integration/v1/ips',
491
'agent' => datastore['USERAGENT'],
492
'vars_post' => { 'resp_format' => 'json' }
493
)
494
results = cli.send_recv(response)
495
rescue ::Rex::ConnectionError, Errno::ECONNREFUSED, Errno::ETIMEDOUT
496
print_error('HTTP connection failed to Incapsula website.')
497
end
498
499
unless results
500
print_error('Unable to retrieve any data from Incapsula website.')
501
return []
502
end
503
504
results.get_json_document['ipRanges'].map { |ip| ip.gsub('"', '') }
505
end
506
507
def pick_action
508
return action if action.name != 'Automatic'
509
510
response = http_get_request_raw(
511
datastore['HOSTNAME'],
512
datastore['RPORT'],
513
datastore['SSL'],
514
datastore['URIPATH']
515
)
516
return nil unless response
517
518
actions.each do |my_action|
519
next if my_action.name == 'Automatic'
520
521
my_action['Signatures'].each do |signature|
522
return my_action if response.headers.to_s.downcase.include?(signature.downcase)
523
end
524
end
525
526
nil
527
end
528
529
# ------------------------------------------------------------------------- #
530
531
def run
532
# If the action can be detected automatically. (Action: Automatic)
533
@my_action = pick_action
534
if @my_action.nil?
535
# If the automatic search fails, bye bye.
536
unless datastore['ALLOW_NOWAF']
537
print_error('Couldn\'t determine the action automatically because no target signatures matched')
538
return
539
end
540
# If allowed, and the automatic action fails, searches for all website occurrences without regard to filtering systems.
541
actions.each do |my_action|
542
@my_action = my_action if my_action.name == 'NoWAFBypass'
543
end
544
end
545
vprint_status("Selected action: #{@my_action.name}")
546
547
print_status('Passive gathering information...')
548
549
domain_name = PublicSuffix.parse(datastore['HOSTNAME']).domain
550
ip_list = []
551
552
# Start collecting information for grabbing all IP address(es).
553
554
# ViewDNS.info
555
ip_records = grab_domain_ip_history(domain_name)
556
if ip_records && !ip_records.empty?
557
ip_list |= ip_records
558
end
559
print_status(" * ViewDNS.info: #{ip_records.count} IP address(es) found.")
560
561
# Mail eXchanger
562
ip_records = dns_get_mx(domain_name)
563
if ip_records && !ip_records.empty?
564
ip_records = wrap_mx(ip_records)
565
ip_list |= ip_records
566
print_status(" * Received MX records: #{ip_records.count} IP address(es) found.")
567
end
568
569
# DNS Enumeration
570
if datastore['ENUM_BRT'] && !dns_wildcard_enabled?(domain_name)
571
ip_records = dns_bruteforce(domain_name, datastore['WORDLIST'], datastore['THREADS'])
572
if ip_records && !ip_records.empty?
573
ip_list |= ip_records
574
end
575
print_status(" * DNS Enumeration: #{ip_records.count} IP address(es) found.")
576
end
577
578
# Censys search
579
if [datastore['CENSYS_UID'], datastore['CENSYS_SECRET']].none?(&:nil?)
580
ip_records = censys_search(domain_name, datastore['CENSYS_UID'], datastore['CENSYS_SECRET'])
581
if ip_records && !ip_records.empty?
582
ip_list |= ip_records
583
end
584
print_status(" * Censys IPv4: #{ip_records.count} IP address(es) found.")
585
end
586
print_status
587
588
# Exit if no IP address(es) has been found.
589
if ip_list.empty?
590
print_bad('No IP address found :-(')
591
return
592
end
593
594
# Comparison to remove address(es) that match the security solution to be tested.
595
# except:
596
# - the selected action is set to NoWAFBypass (except if blacklist ip file is set)
597
# - addresses are not provided
598
599
ip_blacklist = []
600
# Cleaning IP addresses if necessary.
601
case @my_action.name
602
when /ArvanCloud/
603
ip_blacklist = arvancloud_ips
604
when /AzureCDN/
605
ip_blacklist = azurecdn_ips
606
when /CloudFlare/
607
ip_blacklist = cloudflare_ips
608
when /CloudFront/
609
ip_blacklist = cloudfront_ips
610
when /Fastly/
611
ip_blacklist = fastly_ips
612
when /Incapsula/
613
ip_blacklist = incapsula_ips
614
when /InGen Security/
615
# Public address(es) not available, check for known provider DNS responses match :-)
616
ip_list.uniq.each do |ip|
617
a = dns_get_a(ip.to_s)
618
['binarysec', 'easywaf', 'ingensec', '127.0.0.1'].each do |signature|
619
ip_blacklist << ip.to_s if a.to_s.downcase.include? signature.downcase
620
end
621
end
622
end
623
624
# Add. Blacklisted IP address(es) from file.
625
unless datastore['IPBLACKLIST_FILE'].nil?
626
if File.readable? datastore['IPBLACKLIST_FILE']
627
ips = File.readlines(datastore['IPBLACKLIST_FILE'], chomp: true)
628
ips.each do |ip|
629
ip_blacklist << ip
630
end
631
else
632
raise ArgumentError, "Cannot read file #{datastore['IPBLACKLIST_FILE']}"
633
end
634
end
635
636
# Time to clean, removing bad address(es).
637
records = []
638
if !ip_blacklist.empty?
639
print_status("Clean #{@my_action.name} server(s)...")
640
ip_list.uniq.each do |ip|
641
is_listed = false
642
643
ip_blacklist.each do |ip_range|
644
if IPAddr.new(ip_range).include? ip.to_s
645
is_listed = true
646
break
647
end
648
end
649
650
records << ip.to_s unless is_listed
651
end
652
else
653
records.concat(ip_list.uniq.map(&:to_s))
654
end
655
656
# Exit if no IP address(es) has been found after cleaning.
657
if records.empty?
658
print_bad('No IP address found after cleaning.')
659
return
660
end
661
662
print_status(" * Total: #{records.uniq.count} IP address(es) found after cleaning.")
663
print_status
664
665
# Processing bypass steps.
666
667
print_status("Bypass #{action.name} is in progress...")
668
if datastore['COMPSTR'].nil?
669
# If no customized comparison string is entered by the user, search automatically into the user defined TAG (default: <title>).
670
print_status(" * Initial request to the original server for <#{datastore['TAG']}> comparison")
671
response = http_get_request_raw(
672
datastore['HOSTNAME'],
673
datastore['RPORT'],
674
datastore['SSL'],
675
datastore['URIPATH']
676
)
677
html = response.get_html_document
678
begin
679
fingerprint = html.at(datastore['TAG'])
680
unless fingerprint
681
print_bad('Auto-fingerprinting value is empty. Please consider the COMPSTR option')
682
return
683
end
684
rescue NoMethodError
685
print_bad('Please consider the COMPSTR option')
686
return
687
end
688
689
vprint_status(" * Fingerprint: #{fingerprint.to_s.gsub("\n", '')}")
690
vprint_status
691
else
692
# The user-defined comparison string does not require a request to initiate a connection to the target server.
693
# The comparison is made by the check_bypass function in the user-defined TAG (default: <title>).
694
fingerprint = datastore['COMPSTR']
695
end
696
697
# Loop for each unique IP:PORT candidate to check bypass.
698
ret_value = false
699
records.uniq.each do |ip|
700
found = check_bypass(
701
fingerprint,
702
ip
703
)
704
ret_value = true if found
705
end
706
707
# message indicating that nothing was found.
708
unless ret_value
709
print_bad('No direct-connect IP address found :-(')
710
end
711
end
712
713
end
714
715