pwn 0.4.514 → 0.4.515

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.
@@ -0,0 +1,632 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'nokogiri'
4
+
5
+ module PWN
6
+ module Plugins
7
+ # This plugin is used for interacting w/ baresip over a screen session.
8
+ module BareSIP
9
+ @@logger = PWN::Plugins::PWNLogger.create
10
+ @session_data = []
11
+ # Supported Method Parameters::
12
+ # baresip_http_call(
13
+ # http_method: 'optional HTTP method (defaults to GET)
14
+ # cmd: 'required rest call to make per the schema',
15
+ # )
16
+
17
+ private_class_method def self.baresip_http_call(opts = {})
18
+ baresip_obj = opts[:baresip_obj]
19
+ cmd = opts[:cmd]
20
+ http_listen_ip_port = baresip_obj[:http_listen_ip_port]
21
+ baresip_url = "http://#{http_listen_ip_port}"
22
+
23
+ max_conn_attempts = 30
24
+ conn_attempt = 0
25
+
26
+ begin
27
+ conn_attempt += 1
28
+
29
+ rest_client = PWN::Plugins::TransparentBrowser.open(
30
+ browser_type: :rest
31
+ )::Request
32
+
33
+ response = rest_client.execute(
34
+ method: :get,
35
+ url: "#{baresip_url}/?#{cmd}",
36
+ verify_ssl: false
37
+ )
38
+
39
+ Nokogiri::HTML.parse(response)
40
+ rescue Errno::ECONNREFUSED
41
+ raise e if conn_attempt > max_conn_attempts
42
+
43
+ sleep 1
44
+ retry
45
+ end
46
+ rescue StandardError => e
47
+ case e.message
48
+ when '400 Bad Request', '404 Resource Not Found'
49
+ "#{e.message}: #{e.response}"
50
+ else
51
+ raise e
52
+ end
53
+ end
54
+
55
+ # Supported Method Parameters::
56
+ # baresip_obj = PWN::Plugins::BareSIP.start(
57
+ # src_num: 'Optional source phone number displayed',
58
+ # baresip_bin: 'Optional path of baresip binary (Defaults to /usr/bin/baresip)',
59
+ # config_root: 'Optional dir of baresip config (Defaults to ~/.baresip)',
60
+ # session_root: 'Optional dir of baresip session (Defaults to Dir.pwd)',
61
+ # screen_session: 'Optional name of screen session (Defaults baresip)'
62
+ # )
63
+
64
+ public_class_method def self.start(opts = {})
65
+ src_num = opts[:src_num]
66
+
67
+ baresip_bin = opts[:baresip_bin] if File.exist?(
68
+ opts[:baresip_bin].to_s
69
+ )
70
+ baresip_bin ||= '/usr/bin/baresip'
71
+
72
+ baresip_obj = {}
73
+
74
+ session_root = opts[:session_root] if Dir.exist?(opts[:session_root].to_s)
75
+
76
+ config_root = opts[:config_root] if Dir.exist?(
77
+ opts[:config_root].to_s
78
+ )
79
+ config_root ||= "#{Dir.home}/.baresip"
80
+
81
+ config = "#{config_root}/config"
82
+ config_lines = File.readlines(config)
83
+ http_list_entry = config_lines.grep(/^http_listen\s.+$/)
84
+
85
+ raise "no http_listen value found in #{config}." if http_list_entry.empty?
86
+
87
+ http_listen_ip_port = http_list_entry.last.split.grep(/:/).last
88
+
89
+ screen_session = opts[:screen_session]
90
+ screen_session ||= 'baresip'
91
+
92
+ baresip_obj[:config_root] = config_root
93
+ baresip_obj[:http_listen_ip_port] = http_listen_ip_port
94
+ baresip_obj[:session_root] = session_root
95
+ baresip_obj[:screen_session] = screen_session
96
+
97
+ screenlog_file = 'screenlog.txt'
98
+ baresip_obj[:screenlog_file] = screenlog_file
99
+
100
+ # Prefer running baresip in detached screen vs --daemon mode
101
+ # Since sndfile doesn't produce .wav files in --daemon mode
102
+ system(
103
+ 'screen',
104
+ '-T',
105
+ 'xterm',
106
+ '-L',
107
+ '-Logfile',
108
+ screenlog_file,
109
+ '-S',
110
+ screen_session,
111
+ '-d',
112
+ '-m',
113
+ baresip_bin,
114
+ '-f',
115
+ config_root,
116
+ '-e',
117
+ '/insmod httpd',
118
+ '-e',
119
+ '/insmod sndfile'
120
+ )
121
+
122
+ baresip_obj[:session_thread] = init_session_thread(
123
+ baresip_obj: baresip_obj
124
+ )
125
+
126
+ ok = 'registered successfully'
127
+ gone = 'account: No SIP accounts found'
128
+ forb = '403 Forbidden'
129
+
130
+ # TODO: Make this faster.
131
+ print 'Starting baresip...'
132
+ loop do
133
+ break if @session_data.select { |s| s.include?(ok) }.length.positive?
134
+
135
+ next unless dump_session_data.select { |s| s.include?(gone) }.length.positive?
136
+ next unless dump_session_data.select { |s| s.include?(forb) }.length.positive?
137
+
138
+ error = gone if dump_session_data.select { |s| s.include?(gone) }.length.positive?
139
+ error = forbid if dump_session_data.select { |s| s.include?(forb) }.length.positive?
140
+ raise "Something happened when attempting to start baresip: #{error}"
141
+ end
142
+ puts 'ready.'
143
+
144
+ baresip_obj
145
+ rescue StandardError => e
146
+ raise e
147
+ end
148
+
149
+ # Supported Method Parameters::
150
+ # session_thread = init_session_thread(
151
+ # serial_conn: 'required - SerialPort.new object'
152
+ # )
153
+
154
+ private_class_method def self.init_session_thread(opts = {})
155
+ baresip_obj = opts[:baresip_obj]
156
+
157
+ session_root = baresip_obj[:session_root]
158
+ screenlog_file = baresip_obj[:screenlog_file]
159
+
160
+ screenlog = "#{session_root}/#{screenlog_file}"
161
+
162
+ # Spin up a baresip_obj session_thread
163
+ Thread.new do
164
+ loop do
165
+ next unless File.exist?(screenlog)
166
+
167
+ # Continuously consume contents of screenlog.0
168
+ @session_data = File.readlines(screenlog)
169
+ @session_data.delete_if do |line|
170
+ line.include?('ua: using best effort AF: af=AF_INET')
171
+ end
172
+ end
173
+ end
174
+ rescue StandardError => e
175
+ session_thread&.terminate
176
+
177
+ raise e
178
+ end
179
+
180
+ # Supported Method Parameters::
181
+ # session_data = PWN::Plugins::BareSIP.dump_session_data
182
+
183
+ public_class_method def self.dump_session_data
184
+ @session_data
185
+ rescue StandardError => e
186
+ raise e
187
+ end
188
+
189
+ # Supported Method Parameters::
190
+ # session_data = PWN::Plugins::BareSIP.flush_session_data
191
+
192
+ public_class_method def self.flush_session_data
193
+ @session_data.clear
194
+ rescue StandardError => e
195
+ raise e
196
+ end
197
+
198
+ # Supported Method Parameters::
199
+ # cmd_resp = PWN::Plugins::BareSIP.baresip_exec(
200
+ # baresip_obj: 'Required - baresip obj returned from #start method',
201
+ # cmd: 'Required - command to send to baresip HTTP daemon'
202
+ # )
203
+
204
+ public_class_method def self.baresip_exec(opts = {})
205
+ baresip_obj = opts[:baresip_obj]
206
+ cmd = opts[:cmd]
207
+
208
+ baresip_http_call(
209
+ baresip_obj: baresip_obj,
210
+ cmd: cmd
211
+ )
212
+ rescue StandardError => e
213
+ raise e
214
+ end
215
+
216
+ # Supported Method Parameters::
217
+ # PWN::Plugins::BareSIP.stop(
218
+ # screen_session: 'Required - screen session to stop'
219
+ # )
220
+
221
+ public_class_method def self.stop(opts = {})
222
+ baresip_obj = opts[:baresip_obj]
223
+ session_thread = baresip_obj[:session_thread]
224
+ screen_session = baresip_obj[:screen_session]
225
+
226
+ flush_session_data
227
+
228
+ session_thread.terminate
229
+
230
+ system(
231
+ 'screen',
232
+ '-X',
233
+ '-S',
234
+ screen_session,
235
+ 'quit'
236
+ )
237
+
238
+ baresip_obj = nil
239
+ rescue StandardError => e
240
+ raise e
241
+ end
242
+
243
+ # Supported Method Parameters::
244
+ # PWN::Plugins::BareSIP.parse_target_file(
245
+ # target_file: 'Required - txt file containing phone numbers to dial',
246
+ # randomize: 'Optional - randomize list of phone numbers to dial (Defaults to false)'
247
+ # )
248
+
249
+ public_class_method def self.parse_target_file(opts = {})
250
+ target_file = opts[:target_file]
251
+ randomize = opts[:randomize]
252
+
253
+ # Parse entries from target_file and build out our target range
254
+ target_lines = File.readlines(target_file)
255
+ target_range = []
256
+ print 'Initializing targets...'
257
+ target_lines.each do |target_line|
258
+ next if target_line.match?(/^#.*$/)
259
+
260
+ target_line.scrub.strip.chomp.delete('(').delete(')').delete('.').delete('+')
261
+ if target_line.include?('-')
262
+ split_range = target_line.split('-')
263
+ if split_range.length == 2
264
+ from_num = split_range.first.to_i
265
+ to_num = split_range.last.to_i
266
+
267
+ (from_num..to_num).each do |number_in_range|
268
+ target_range.push(number_in_range)
269
+ end
270
+ else
271
+ target_line.scrub.strip.chomp.delete('-')
272
+ end
273
+ else
274
+ target_range.push(target_line.to_i)
275
+ end
276
+ end
277
+ puts 'complete.'
278
+
279
+ # Randomize targets if applicable
280
+ target_range.shuffle! if randomize
281
+
282
+ target_range
283
+ rescue StandardError => e
284
+ raise e
285
+ end
286
+
287
+ # Supported Method Parameters::
288
+ # PWN::Plugins::BareSIP.apply_src_num_rules(
289
+ # target_num: 'Required - destination number to derive source number',
290
+ # src_num_rules: 'Optional - Comma-delimited list of rules for src_num format (i.e. self, same_country, same_area, and/or same_prefix [Defaults to random src_num w/ same length as target_num])'
291
+ # )
292
+
293
+ public_class_method def self.apply_src_num_rules(opts = {})
294
+ src_num_rules = opts[:src_num_rules]
295
+ target_num = opts[:target_num]
296
+
297
+ src_num_rules_arr = []
298
+ if src_num_rules
299
+ src_num_rules_arr = src_num_rules.delete("\s").split(',').map(
300
+ &:to_sym
301
+ )
302
+ end
303
+
304
+ case target_num.to_s.length
305
+ when 10
306
+ # area+prefix+suffix
307
+ country = ''
308
+ when 11
309
+ # 1 digit country+area+prefix+suffix
310
+ country = format('%0.1s', Random.rand(1..9))
311
+ country = target_num.to_s.chars.first if src_num_rules_arr.include?(
312
+ :same_country
313
+ )
314
+ when 12
315
+ # 2 digit country+area+prefix+suffix
316
+ country = format('%0.2s', Random.rand(1..99))
317
+ country = target_num.to_s.chars[0..1].join if src_num_rules_arr.include?(
318
+ :same_country
319
+ )
320
+ when 13
321
+ # 3 digit country+area+prefix+suffix
322
+ country = format('%0.3s', Random.rand(1..999))
323
+ country = target_num.to_s.chars[0..2].join if src_num_rules_arr.include?(
324
+ :same_country
325
+ )
326
+ when 14
327
+ # 4 digit country+area+prefix+suffix
328
+ country = format('%0.4s', Random.rand(1..9999))
329
+ country = target_num.to_s.chars[0..3].join if src_num_rules_arr.include?(
330
+ :same_country
331
+ )
332
+ else
333
+ raise "Target # should be 10-14 digits. Length is: #{target_num.to_s.length}"
334
+ end
335
+
336
+ area = format('%0.3s', Random.rand(200..999))
337
+ area = target_num.to_s.chars[-10..-8].join if src_num_rules_arr.include?(
338
+ :same_area
339
+ )
340
+
341
+ prefix = format('%0.3s', Random.rand(200..999))
342
+ prefix = target_num.to_s.chars[-7..-5].join if src_num_rules_arr.include?(
343
+ :same_prefix
344
+ )
345
+ suffix = format('%0.4s', Random.rand(0..9999))
346
+ src_num = "#{country}#{area}#{prefix}#{suffix}"
347
+ src_num = target_num if src_num_rules_arr.include?(:self)
348
+
349
+ # TODO: Update ~/.baresip/accounts to apply source number
350
+ # config_root = baresip_obj[:config_root]
351
+ # config = "#{config_root}/config"
352
+
353
+ src_num
354
+ rescue StandardError => e
355
+ raise e
356
+ end
357
+
358
+ # Supported Method Parameters::
359
+ # PWN::Plugins::BareSIP.recon(
360
+ # baresip_obj: 'Required - baresip_obj returned from #start method',
361
+ # target_num: 'Required - target destination to recon (i.e. phone number)',
362
+ # src_num_rules: 'Optional - Comma-delimited list of rules for src_num format (i.e. self, same_country, same_area, and/or same_prefix [Defaults to random src_num w/ same length as target_num])',
363
+ # seconds_to_record: 'Optional - Seconds to Record (Defaults to 60)',
364
+ # sox_bin: 'Optional - Path to SoX Binary, the Swiss Army knife of Audio (Defaults to /usr/bin/sox)'
365
+ # )
366
+
367
+ public_class_method def self.recon(opts = {})
368
+ baresip_bin = opts[:baresip_bin]
369
+ session_root = opts[:session_root]
370
+ session_root ||= Dir.pwd
371
+ target_file = opts[:target_file]
372
+ randomize = opts[:randomize]
373
+ src_num_rules = opts[:src_num_rules]
374
+ max_threads = opts[:max_threads].to_i
375
+ max_threads = 3 if max_threads.zero?
376
+ seconds_to_record = opts[:seconds_to_record].to_i
377
+ seconds_to_record = 60 if seconds_to_record.zero?
378
+ sox_bin = opts[:sox_bin] if File.exist?(opts[:sox_bin].to_s)
379
+ sox_bin ||= '/usr/bin/sox'
380
+ waveform_bin = 'waveform'
381
+
382
+ # Intialize empty baresip obj for ensure block below
383
+ baresip_obj = {}
384
+
385
+ # Colors!
386
+ red = "\e[31m"
387
+ green = "\e[32m"
388
+ yellow = "\e[33m"
389
+ cayan = "\e[36m"
390
+ end_of_color = "\e[0m"
391
+
392
+ target_range = parse_target_file(
393
+ target_file: target_file,
394
+ randomize: randomize
395
+ )
396
+
397
+ results_hash = {
398
+ session_started: Time.now.strftime('%Y-%m-%d_%H.%M.%S'),
399
+ data: []
400
+ }
401
+
402
+ # TODO: Multi-thread!
403
+ # mutex = Mutex.new
404
+ # PWN::Plugins::ThreadPool.fill(
405
+ # enumerable_array: target_range,
406
+ # max_threads: max_threads
407
+ # ) do |target_num|
408
+ target_range.each_with_index do |target_num, index|
409
+ call_count = index + 1
410
+ puts "Call #{call_count} of #{target_range.length}..."
411
+
412
+ # Change to session_root _before_ starting to ensure
413
+ # wav files are stored in the proper location
414
+ Dir.chdir(session_root)
415
+
416
+ src_num = apply_src_num_rules(
417
+ target_num: target_num,
418
+ src_num_rules: src_num_rules
419
+ )
420
+
421
+ call_info_hash = {}
422
+
423
+ call_started = Time.now.strftime('%Y-%m-%d_%H.%M.%S')
424
+
425
+ call_info_hash[:call_started] = call_started
426
+ call_info_hash[:src_num] = src_num
427
+ call_info_hash[:src_num_rules] = src_num_rules
428
+ call_info_hash[:target_num] = target_num
429
+
430
+ target_num_root = "#{session_root}/#{target_num}-#{call_started}"
431
+
432
+ # TODO: Determine what to do w/ existing Directory
433
+ Dir.mkdir(target_num_root)
434
+ Dir.chdir(target_num_root)
435
+
436
+ # Start baresip in detached screen to support commands over HTTP
437
+ # and call recording to wav files
438
+ baresip_obj = start(
439
+ src_num: src_num,
440
+ baresip_bin: baresip_bin,
441
+ session_root: target_num_root,
442
+ screen_session: "#{File.basename($PROGRAM_NAME)}-#{target_num}"
443
+ )
444
+
445
+ # session_root = baresip_obj[:session_root]
446
+ config_root = baresip_obj[:config_root]
447
+ config = "#{config_root}/config"
448
+
449
+ puts "#{green}#{call_started} >>>#{end_of_color}"
450
+ puts "#{yellow}dialing #{target_num}#{end_of_color}"
451
+
452
+ cmd_resp = baresip_exec(
453
+ baresip_obj: baresip_obj,
454
+ cmd: "/dial #{target_num}\r\n"
455
+ )
456
+ puts "/dial #{target_num} RESP:"
457
+ puts cmd_resp.xpath('//pre').text
458
+
459
+ cmd_resp = baresip_exec(
460
+ baresip_obj: baresip_obj,
461
+ cmd: "/listcalls\r\n"
462
+ )
463
+ puts '/listcalls RESP:'
464
+ puts cmd_resp.xpath('//pre').text
465
+
466
+ puts red
467
+ # Conditions to proceed less than seconds_to_record
468
+ terminated = 'terminated (duration:'
469
+ unavail = '503 Service Unavailable'
470
+ not_found = 'session closed: 404 Not Found'
471
+
472
+ reason = 'recording limit reached'
473
+ seconds_recorded = 0
474
+ seconds_to_record.downto(1).each do |countdown|
475
+ seconds_recorded += 1
476
+ print "#{seconds_to_record}s to record - remaining: #{format('%-9.9s', countdown)}"
477
+ print "\r"
478
+
479
+ if dump_session_data.select { |s| s.include?(terminated) }.length.positive?
480
+ reason = 'call terminated by other party'
481
+ break
482
+ end
483
+
484
+ if dump_session_data.select { |s| s.include?(unavail) }.length.positive?
485
+ reason = 'SIP 503 (service unavailable)'
486
+ break
487
+ end
488
+
489
+ if dump_session_data.select { |s| s.include?(not_found) }.length.positive?
490
+ reason = 'SIP 404 (not found)'
491
+ break
492
+ end
493
+
494
+ sleep 1
495
+ end
496
+ call_info_hash[:seconds_recorded] = seconds_recorded
497
+ puts end_of_color
498
+
499
+ call_stopped = Time.now.strftime('%Y-%m-%d_%H.%M.%S')
500
+ puts "\n#{green}#{call_stopped} >>> #{reason} #{target_num}#{end_of_color}"
501
+ call_info_hash[:call_stopped] = call_stopped
502
+ call_info_hash[:reason] = reason
503
+
504
+ recording = ''
505
+ # BUGFIX: Sometimes recordings aren't generated
506
+ # Perhaps because previous session wasn't stopped properly?
507
+ Dir.entries(target_num_root).each do |entry|
508
+ recording = entry if entry.match(/^dump-.+-dec.wav/)
509
+ File.delete(entry) if entry.match(/^dump-.+-enc\.wav$/) && File.exist?(entry)
510
+ end
511
+
512
+ call_info_hash[:recording] = '--'
513
+ call_info_hash[:waveform] = '--'
514
+ call_info_hash[:spectrogram] = '--'
515
+
516
+ unless recording.empty?
517
+ puts cayan
518
+
519
+ call_info_hash[:recording] = "#{target_num}-#{call_started}/#{recording}"
520
+
521
+ spectrogram = "#{recording}-spectrogram.png"
522
+ print "Generating Audio Spectrogram for #{recording}..."
523
+ system(
524
+ sox_bin,
525
+ recording,
526
+ '-n',
527
+ 'spectrogram',
528
+ '-o',
529
+ spectrogram,
530
+ '-d',
531
+ seconds_to_record.to_s
532
+ )
533
+ puts 'complete.'
534
+ call_info_hash[:spectrogram] = "#{target_num}-#{call_started}/#{spectrogram}"
535
+
536
+ waveform = "#{recording}-waveform.png"
537
+ print "Generating Audio Waveform for #{recording}..."
538
+ system(
539
+ waveform_bin,
540
+ '--method',
541
+ 'peak',
542
+ '--color',
543
+ '#FF0000',
544
+ '--background',
545
+ '#000000',
546
+ '--force',
547
+ recording,
548
+ waveform
549
+ )
550
+ puts 'complete.'
551
+ call_info_hash[:waveform] = "#{target_num}-#{call_started}/#{waveform}"
552
+ puts end_of_color
553
+ end
554
+
555
+ # Push Call Results to results_hash[:data]
556
+ # mutex.synchronize do
557
+ # results_hash[:data].push(call_info_hash)
558
+ # end
559
+ results_hash[:data].push(call_info_hash)
560
+
561
+ puts "call termination reason: #{reason}"
562
+ # Stop call to cd into next session root
563
+ stop(baresip_obj: baresip_obj)
564
+
565
+ seconds_to_delay_between_pool_of_calls = Random.rand(1..9)
566
+ seconds_to_delay_between_pool_of_calls.downto(1).each do |countdown|
567
+ print "#{red}ZZZ #{seconds_to_delay_between_pool_of_calls}s until next pool of calls: #{format('%-9.9s', countdown)}#{end_of_color}"
568
+ print "\r"
569
+ sleep 1
570
+ end
571
+ puts "\n#{green}waking up for next call \\o/#{end_of_color}"
572
+ puts "\n\n\n"
573
+ end
574
+ results_hash[:session_ended] = Time.now.strftime('%Y-%m-%d_%H.%M.%S')
575
+
576
+ results_hash
577
+ rescue StandardError => e
578
+ raise e
579
+ ensure
580
+ stop(baresip_obj: baresip_obj) unless baresip_obj.empty?
581
+ end
582
+
583
+ # Supported Method Parameters::
584
+ # PWN::Plugins::BareSIP.killall
585
+
586
+ public_class_method def self.killall
587
+ system(
588
+ 'killall',
589
+ '-15',
590
+ 'baresip'
591
+ )
592
+ rescue StandardError => e
593
+ raise e
594
+ end
595
+
596
+ # Author(s):: 0day Inc. <request.pentest@0dayinc.com>
597
+
598
+ public_class_method def self.authors
599
+ "AUTHOR(S):
600
+ 0day Inc. <request.pentest@0dayinc.com>
601
+ "
602
+ end
603
+
604
+ # Display Usage for this Module
605
+
606
+ public_class_method def self.help
607
+ puts "USAGE:
608
+ baresip_obj = #{self}.start(
609
+ baresip_bin: 'Optional path of baresip binary (Defaults to /usr/bin/baresip)'
610
+ config_root: 'Optional dir of baresip config (Defaults to ~/.baresip)',
611
+ screen_session: 'Optional name of screen session (Defaults baresip)'
612
+ )
613
+
614
+ session_data_arr = #{self}.dump_session_data
615
+
616
+ cmd_resp = #{self}.baresip_exec(
617
+ baresip_obj: 'Required - baresip obj returned from #start method',
618
+ cmd: 'Required - command to send to baresip HTTP daemon'
619
+ )
620
+
621
+ stopped_bool = #{self}.stop(
622
+ screen_session: 'Required - screen session to stop'
623
+ )
624
+
625
+ #{self}.killall
626
+
627
+ #{self}.authors
628
+ "
629
+ end
630
+ end
631
+ end
632
+ end
@@ -213,7 +213,7 @@ module PWN
213
213
  cmd_response_arr.map(&:strip)
214
214
  rescue StandardError => e
215
215
  # Flush Responses for Next Request
216
- flush_session_data(serial_obj: serial_obj)
216
+ flush_session_data
217
217
 
218
218
  raise e
219
219
  end
@@ -47,6 +47,32 @@ module PWN
47
47
  raise e
48
48
  end
49
49
 
50
+ # Supported Method Parameters::
51
+ # PWN::Plugins::Sock.check_port_availability(
52
+ # port: 'required - target port',
53
+ # server_ip: 'optional - target host or ip to check (Defaults to 127.0.0.1)',
54
+ # protocol: 'optional - :tcp || :udp (defaults to tcp)'
55
+ # )
56
+
57
+ public_class_method def self.check_port_availability(opts = {})
58
+ server_ip = opts[:server_ip]
59
+ server_ip ||= '127.0.0.1'
60
+ port = opts[:port]
61
+ protocol = opts[:protocol]
62
+ protocol ||= :tcp
63
+
64
+ ct = 0.1
65
+ s = Socket.tcp(server_ip, port, connect_timeout: ct) if protocol == :tcp
66
+ s = Socket.udp(server_ip, port, connect_timeout: ct) if protocol == :udp
67
+ s.close
68
+
69
+ true
70
+ rescue Errno::ECONNREFUSED,
71
+ Errno::EHOSTUNREACH,
72
+ Errno::ETIMEDOUT
73
+ false
74
+ end
75
+
50
76
  # Supported Method Parameters::
51
77
  # PWN::Plugins::Sock.listen(
52
78
  # server_ip: 'required - target host or ip to listen',
@@ -137,6 +163,12 @@ module PWN
137
163
  tls: 'optional - boolean connect to target socket using TLS (defaults to false)'
138
164
  )
139
165
 
166
+ #{self}.check_port_availability(
167
+ port: 'required - target port',
168
+ server_ip: 'optional - target host or ip to check (Defaults to 127.0.0.1)',
169
+ protocol: 'optional - :tcp || :udp (defaults to tcp)'
170
+ )
171
+
140
172
  #{self}.listen(
141
173
  server_ip: 'required - target host or ip to listen',
142
174
  port: 'required - target port',