liferay_scan 0.0.2

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.
data/data/users.txt ADDED
@@ -0,0 +1,9 @@
1
+ guest
2
+ user
3
+ admin
4
+ administrator
5
+ bruno
6
+ test
7
+ demo
8
+ liferay
9
+ liferayadmin
@@ -0,0 +1,489 @@
1
+ #
2
+ # This file is part of LiferayScan
3
+ # https://github.com/bcoles/liferay_scan
4
+ #
5
+
6
+ require 'uri'
7
+ require 'cgi'
8
+ require 'logger'
9
+ require 'net/http'
10
+ require 'openssl'
11
+ require 'stringio'
12
+
13
+ class LiferayScan
14
+ VERSION = '0.0.2'.freeze
15
+ @resource_path = File.join(File.dirname(File.expand_path(__FILE__)), '../data')
16
+
17
+ class << self
18
+ attr_reader :logger
19
+ end
20
+
21
+ class << self
22
+ attr_writer :logger
23
+ end
24
+
25
+ def self.insecure
26
+ @insecure ||= false
27
+ end
28
+
29
+ class << self
30
+ attr_writer :insecure
31
+ end
32
+
33
+ #
34
+ # Check if URL is running Liferay
35
+ #
36
+ # @param [String] URL
37
+ #
38
+ # @return [Boolean]
39
+ #
40
+ def self.detectLiferay(url)
41
+ return true if detectLiferayFromLogin(url)
42
+ return true if detectLiferayFromHome(url)
43
+
44
+ false
45
+ end
46
+
47
+ #
48
+ # Check if URL is running Liferay using login page
49
+ #
50
+ # @param [String] URL
51
+ #
52
+ # @return [Boolean]
53
+ #
54
+ def self.detectLiferayFromLogin(url)
55
+ url += '/' unless url.to_s.end_with?('/')
56
+
57
+ res = sendHttpRequest("#{url}c/portal/login")
58
+
59
+ return false unless res
60
+ return false unless res.code.to_i == 302
61
+
62
+ return true if res['liferay-portal'].to_s.start_with?('Liferay')
63
+
64
+ # old Liferay <= 6.x
65
+ return true if res['location'] =~ /p_p_id=58/
66
+
67
+ # new Liferay >= 7.x
68
+ return true if res['location'] =~ /p_p_id=com_liferay_login_web_portlet_LoginPortlet/
69
+
70
+ false
71
+ end
72
+
73
+ #
74
+ # Check if URL is running Liferay using home page
75
+ #
76
+ # @param [String] URL
77
+ #
78
+ # @return [Boolean]
79
+ #
80
+ def self.detectLiferayFromHome(url)
81
+ url += '/' unless url.to_s.end_with?('/')
82
+
83
+ res = sendHttpRequest("#{url}home")
84
+
85
+ return false unless res
86
+ return false unless res.code.to_i == 200
87
+
88
+ return true if res['liferay-portal'].to_s.start_with?('Liferay')
89
+ return true if res.body.to_s.include?('var Liferay = Liferay || {};')
90
+ return true if res.body.to_s.include?('var Liferay = {')
91
+
92
+ false
93
+ end
94
+
95
+ # Get Liferay version
96
+ #
97
+ # @param [String] URL
98
+ #
99
+ # @return [String] Liferay version
100
+ #
101
+ def self.getVersion(url)
102
+ version = getVersionFromLogin(url)
103
+ return version if version
104
+
105
+ version = getVersionFromGuestHome(url)
106
+ return version if version
107
+
108
+ nil
109
+ end
110
+
111
+ #
112
+ # Get Liferay version from login page
113
+ #
114
+ # @param [String] URL
115
+ #
116
+ # @return [String] Liferay version
117
+ #
118
+ def self.getVersionFromLogin(url)
119
+ url += '/' unless url.to_s.end_with?('/')
120
+
121
+ res = sendHttpRequest("#{url}")
122
+
123
+ return unless res
124
+
125
+ if res['liferay-portal'].to_s.start_with?('Liferay') && res['liferay-portal'].to_s.include?('.')
126
+ return res['liferay-portal'].to_s
127
+ end
128
+
129
+ res.body.to_s.scan(/<div class="clearfix component-paragraph text-break" data-lfr-editable-id="element-text" data-lfr-editable-type="rich-text">\s*(Welcome to )?(Liferay [^<]+)\s*</).flatten[1]
130
+ end
131
+
132
+ #
133
+ # Get Liferay version from guest home page
134
+ #
135
+ # @param [String] URL
136
+ #
137
+ # @return [String] Liferay version
138
+ #
139
+ def self.getVersionFromGuestHome(url)
140
+ url += '/' unless url.to_s.end_with?('/')
141
+
142
+ res = sendHttpRequest("#{url}web/guest/home")
143
+
144
+ return unless res
145
+
146
+ if res['liferay-portal'].to_s.start_with?('Liferay') && res['liferay-portal'].to_s.include?('.')
147
+ return res['liferay-portal'].to_s
148
+ end
149
+
150
+ # Hello World default post
151
+ res.body.to_s.scan(%r{<div class="portlet-body">\s*Welcome to (Liferay Portal [^<]+)\.\s*</div>}).flatten.first
152
+ end
153
+
154
+ # Get server from server error page
155
+ #
156
+ # @param [String] URL
157
+ #
158
+ # @return [String] Server version
159
+ #
160
+ def self.getServerVersion(url)
161
+ url += '/' unless url.to_s.end_with?('/')
162
+
163
+ res = sendHttpRequest("#{url}api/liferay")
164
+
165
+ return unless res
166
+
167
+ tomcat = res.body.scan(%r{>(Apache Tomcat/[0-9.]+)}).flatten.first
168
+ return tomcat if tomcat
169
+
170
+ glassfish = res.body.scan(/>(GlassFish Server Open Source Edition [0-9.]+)/).flatten.first
171
+ return glassfish if glassfish
172
+
173
+ nil
174
+ end
175
+
176
+ # Get client IP address from server error page
177
+ # This may disclose the IP address of intermediary proxy servers
178
+ #
179
+ # @param [String] URL
180
+ #
181
+ # @return [String] Client IP address
182
+ #
183
+ def self.getClientIpAddress(url)
184
+ url += '/' unless url.to_s.end_with?('/')
185
+
186
+ res = sendHttpRequest("#{url}api/liferay")
187
+
188
+ return unless res
189
+
190
+ res.body.scan(/>Access denied for ([\d.]+)</).flatten.first
191
+ end
192
+
193
+ #
194
+ # Get Liferay default language
195
+ #
196
+ # @param [String] URL
197
+ #
198
+ # @return [String] Liferay language
199
+ #
200
+ def self.getLanguage(url)
201
+ url += '/' unless url.to_s.end_with?('/')
202
+
203
+ res = sendHttpRequest(url)
204
+
205
+ return unless res
206
+
207
+ res['set-cookie'].to_s.scan(/GUEST_LANGUAGE_ID=([a-z]{2,3}_[A-Z]{2,3})/).flatten.first
208
+ end
209
+
210
+ #
211
+ # Retrieve organisation email address domain
212
+ #
213
+ # @param [String] URL
214
+ #
215
+ # @return [String] Organisation email address domain
216
+ #
217
+ def self.getOrganisationEmail(url)
218
+ url += '/' unless url.to_s.end_with?('/')
219
+
220
+ # old Liferay <= 6.x
221
+ res = sendHttpRequest("#{url}web/guest/home?p_p_id=58&p_p_lifecycle=0&p_p_state=maximized&p_p_mode=view&saveLastPath=false")
222
+ if res && res.body =~ (/name="_58_login"[^>]+type="text"\s*value="&#x40;([^"]+)"/)
223
+ return CGI.unescapeHTML(::Regexp.last_match(1))
224
+ end
225
+
226
+ # new Liferay >= 7.x
227
+ res = sendHttpRequest("#{url}home?p_p_id=com_liferay_login_web_portlet_LoginPortlet&p_p_lifecycle=0&p_p_state=maximized&p_p_mode=view&saveLastPath=false")
228
+ if res
229
+ return res.body.scan(/name="_com_liferay_login_web_portlet_LoginPortlet_login"\s*type="text"\s*value="@([^"]+)"/).flatten.first
230
+ end
231
+
232
+ nil
233
+ end
234
+
235
+ #
236
+ # Retrieve names from open search
237
+ #
238
+ # @param [String] URL
239
+ #
240
+ # @return [Array] list of users
241
+ #
242
+ def self.getUsersFromSearch(url)
243
+ url += '/' unless url.to_s.end_with?('/')
244
+
245
+ res = sendHttpRequest("#{url}c/search/open_search")
246
+
247
+ return [] unless res
248
+ return [] unless res.body
249
+
250
+ valid_users = []
251
+ res.body.encode('UTF-8', invalid: :replace, undef: :replace).scan(/\[Users &raquo; ([^\]]+)\]/).each do |full_name|
252
+ next if full_name.empty?
253
+
254
+ valid_users << [nil, full_name.flatten.first]
255
+ end
256
+
257
+ valid_users
258
+ rescue StandardError
259
+ @logger.error("#{e.message}")
260
+ []
261
+ end
262
+
263
+ #
264
+ # Check if account registration is enabled
265
+ #
266
+ # @param [String] URL
267
+ #
268
+ # @return [Boolean]
269
+ #
270
+ def self.userRegistration(url)
271
+ url += '/' unless url.to_s.end_with?('/')
272
+
273
+ # new Liferay >= 7.x
274
+ res = sendHttpRequest("#{url}web/guest/home?p_p_id=com_liferay_login_web_portlet_LoginPortlet&p_p_lifecycle=0&p_p_state=maximized&p_p_mode=view&_com_liferay_login_web_portlet_LoginPortlet_mvcRenderCommandName=%2Flogin%2Fcreate_account&saveLastPath=false")
275
+ if res && res.body.include?('_com_liferay_login_web_portlet_LoginPortlet_firstName') && res.body.include?('_com_liferay_login_web_portlet_LoginPortlet_lastName')
276
+ return true
277
+ end
278
+
279
+ # old Liferay <= 6.x
280
+ res = sendHttpRequest("#{url}web/guest/home?p_p_id=58&p_p_lifecycle=0&p_p_state=maximized&p_p_mode=view&saveLastPath=false&_58_struts_action=%2Flogin%2Fcreate_account")
281
+ return true if res && res.body =~ /_58_firstName/ && res.body =~ /_58_lastName/
282
+
283
+ false
284
+ end
285
+
286
+ #
287
+ # Check if Single SignOn (SSO) authentication is enabled
288
+ #
289
+ # @param [String] URL
290
+ #
291
+ # @return [Boolean] SSO authentication is enabled
292
+ #
293
+ def self.ssoAuthEnabled(url)
294
+ url += '/' unless url.to_s.end_with?('/')
295
+
296
+ res = sendHttpRequest("#{url}c/portal/login")
297
+
298
+ return false unless res
299
+
300
+ return true if res.body.to_s.include?('name="SAMLRequest"')
301
+ return true if res.body =~ /id="idpEntityId"\s+name="idpEntityId"/
302
+
303
+ false
304
+ end
305
+
306
+ #
307
+ # Check if remote access to the SOAP API is allowed
308
+ # https://help.liferay.com/hc/en-us/articles/360018161151-SOAP-Web-Services
309
+ #
310
+ # @param [String] URL
311
+ #
312
+ # @return [Boolean]
313
+ #
314
+ def self.remoteSoapApi(url)
315
+ url += '/' unless url.to_s.end_with?('/')
316
+
317
+ res = sendHttpRequest("#{url}api/axis")
318
+
319
+ return false unless res
320
+ return false unless res.code.to_i == 200
321
+
322
+ return true if res.body.to_s.include?('<h2>And now... Some Services</h2>')
323
+
324
+ false
325
+ end
326
+
327
+ #
328
+ # Check if remote access to the JSON API is allowed
329
+ # https://liferay.atlassian.net/browse/LPSA-86672
330
+ # https://help.liferay.com/hc/en-us/articles/360018179011-Portal-Configuration-of-JSON-Web-Services
331
+ # https://help.liferay.com/hc/en-us/articles/360017882172-Configuring-JSON-Web-Services-#controlling-public-access
332
+ #
333
+ # @param [String] URL
334
+ #
335
+ # @return [Boolean]
336
+ #
337
+ def self.remoteJsonApi(url)
338
+ url += '/' unless url.to_s.end_with?('/')
339
+
340
+ res = sendHttpRequest("#{url}api/jsonws")
341
+
342
+ return false unless res
343
+ return false unless res.code.to_i == 200
344
+
345
+ return true if res.body.to_s.include?('<title>json-web-services-api</title>')
346
+ return true if res.body.to_s.include?('"JSONWS API"')
347
+
348
+ false
349
+ end
350
+
351
+ #
352
+ # Check if Forgot Password is enabled
353
+ #
354
+ # @param [String] URL
355
+ #
356
+ # @return [Boolean]
357
+ #
358
+ def self.passwordResetEnabled(url)
359
+ url += '/' unless url.to_s.end_with?('/')
360
+
361
+ # new Liferay >= 7.x
362
+ res = sendHttpRequest("#{url}home?p_p_id=com_liferay_login_web_portlet_LoginPortlet&p_p_lifecycle=0&p_p_state=maximized&p_p_mode=view&_com_liferay_login_web_portlet_LoginPortlet_mvcRenderCommandName=%2Flogin%2Fforgot_password&saveLastPath=false")
363
+ return true if res && res.body.to_s.include?('Forgot Password')
364
+
365
+ # old Liferay <= 6.x
366
+ res = sendHttpRequest("#{url}web/guest/home?p_p_id=58&p_p_lifecycle=0&p_p_state=exclusive&p_p_mode=view&_58_struts_action=%2Flogin%2Fforgot_password")
367
+ return true if res && res.body.to_s.include?('Forgot Password')
368
+
369
+ false
370
+ end
371
+
372
+ #
373
+ # Check if Forgot Password requires CAPTCHA
374
+ #
375
+ # @param [String] URL
376
+ #
377
+ # @return [Boolean]
378
+ #
379
+ def self.passwordResetUsesCaptcha(url)
380
+ url += '/' unless url.to_s.end_with?('/')
381
+
382
+ # new Liferay >= 7.x
383
+ res = sendHttpRequest("#{url}home?p_p_id=com_liferay_login_web_portlet_LoginPortlet&p_p_lifecycle=0&p_p_state=maximized&p_p_mode=view&_com_liferay_login_web_portlet_LoginPortlet_mvcRenderCommandName=%2Flogin%2Fforgot_password&saveLastPath=false")
384
+ if res
385
+ return true if res.body.to_s.include?('id="_58_captcha"')
386
+ return true if res.body.to_s.include?('id="_com_liferay_login_web_portlet_LoginPortlet_captchaText"')
387
+ return true if res.body.to_s.include?('RecaptchaOptions')
388
+ end
389
+
390
+ # old Liferay <= 6.x
391
+ res = sendHttpRequest("#{url}web/guest/home?p_p_id=58&p_p_lifecycle=0&p_p_state=exclusive&p_p_mode=view&_58_struts_action=%2Flogin%2Fforgot_password")
392
+ if res
393
+ return true if res.body.to_s.include?('id="_58_captcha"')
394
+ return true if res.body.to_s.include?('id="_com_liferay_login_web_portlet_LoginPortlet_captchaText"')
395
+ return true if res.body.to_s.include?('RecaptchaOptions')
396
+ end
397
+
398
+ false
399
+ end
400
+
401
+ #
402
+ # Enumerate users
403
+ #
404
+ # @param [String] URL
405
+ #
406
+ # @return [Array] list of users
407
+ #
408
+ def self.enumerateUsersFromBlogRss(url)
409
+ url += '/' unless url.to_s.end_with?('/')
410
+
411
+ # load potential usernames from gem data files
412
+ users = File.readlines("#{@resource_path}/users.txt")
413
+ users.concat(File.readlines("#{@resource_path}/names.txt"))
414
+
415
+ # enumerate common user names from blog RSS feed
416
+ valid_users = []
417
+ users.sort.uniq.each do |screen_name|
418
+ next if screen_name.start_with?('#')
419
+
420
+ screen_name.chomp!
421
+
422
+ next if screen_name.empty?
423
+
424
+ res = sendHttpRequest("#{url}web/#{URI::Parser.new.escape(screen_name)}/home/-/blogs/rss")
425
+
426
+ next if res.nil?
427
+ next if res.code.to_i != 200
428
+
429
+ full_name = res.body.to_s.scan(%r{<subtitle>(.+?)</subtitle>}).flatten.first
430
+
431
+ next unless full_name
432
+
433
+ valid_users << [screen_name, full_name]
434
+ end
435
+
436
+ valid_users
437
+ rescue StandardError => e
438
+ @logger.error("#{e.message}")
439
+ []
440
+ end
441
+
442
+ #
443
+ # Fetch URL
444
+ #
445
+ # @param [String] URL
446
+ #
447
+ # @return [Net::HTTPResponse] HTTP response
448
+ #
449
+ def self.sendHttpRequest(url)
450
+ target = URI.parse(url)
451
+ @logger.info("Fetching #{target}")
452
+
453
+ http = Net::HTTP.new(target.host, target.port)
454
+ if target.scheme.to_s.eql?('https')
455
+ http.use_ssl = true
456
+ http.verify_mode = @insecure ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER
457
+ end
458
+ http.open_timeout = 20
459
+ http.read_timeout = 20
460
+ headers = {}
461
+ headers['User-Agent'] = "LiferayScan/#{VERSION}"
462
+ headers['Accept-Encoding'] = 'gzip,deflate'
463
+
464
+ begin
465
+ res = http.request(Net::HTTP::Get.new(target, headers.to_hash))
466
+ rescue Timeout::Error, Errno::ETIMEDOUT
467
+ @logger.error("Could not retrieve URL #{target}: Timeout")
468
+ return nil
469
+ rescue StandardError => e
470
+ @logger.error("Could not retrieve URL #{target}: #{e}")
471
+ return nil
472
+ end
473
+
474
+ @logger.info("Received reply (#{res.body.length} bytes)")
475
+
476
+ begin
477
+ if res.body && res['Content-Encoding'].eql?('gzip')
478
+ sio = StringIO.new(res.body)
479
+ gz = Zlib::GzipReader.new(sio)
480
+ res.body = gz.read
481
+ end
482
+ rescue Zlib::GzipFile::Error => e
483
+ # Not compressed? Return raw response.
484
+ @logger.info("Gzip decompression failed: #{e.message}")
485
+ end
486
+
487
+ res
488
+ end
489
+ end
metadata ADDED
@@ -0,0 +1,75 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: liferay_scan
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Brendan Coles
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-04-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: terminal-table
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '4.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '4.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: logger
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.6'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.6'
41
+ description: A simple remote scanner for Liferay Portal
42
+ email: bcoles@gmail.com
43
+ executables:
44
+ - liferay-scan
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - bin/liferay-scan
49
+ - data/names.txt
50
+ - data/users.txt
51
+ - lib/liferay_scan.rb
52
+ homepage: https://github.com/bcoles/liferay_scan
53
+ licenses:
54
+ - MIT
55
+ metadata: {}
56
+ post_install_message:
57
+ rdoc_options: []
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: 3.0.0
65
+ required_rubygems_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ requirements: []
71
+ rubygems_version: 3.3.27
72
+ signing_key:
73
+ specification_version: 4
74
+ summary: Liferay scanner
75
+ test_files: []