# frozen_string_literal: true
require 'htmlentities'
require 'nokogiri'
module PWN
module Plugins
# This plugin is used for interacting w/ baresip over a screen session.
module BareSIP
@session_data = []
# Supported Method Parameters::
# baresip_http_call(
# http_method: 'optional HTTP method (defaults to GET)
# cmd: 'required rest call to make per the schema',
# )
private_class_method def self.baresip_http_call(opts = {})
baresip_obj = opts[:baresip_obj]
cmd = opts[:cmd]
http_listen_ip_port = opts[:http_listen_ip_port]
baresip_url = "http://#{http_listen_ip_port}"
max_conn_attempts = 30
conn_attempt = 0
begin
conn_attempt += 1
browser_obj = PWN::Plugins::TransparentBrowser.open(
browser_type: :rest
)
rest_client = browser_obj[:browser]::Request
response = rest_client.execute(
method: :get,
url: "#{baresip_url}/?#{cmd}",
verify_ssl: false
)
Nokogiri::HTML.parse(response)
rescue Errno::ECONNREFUSED => e
raise e if conn_attempt > max_conn_attempts
sleep 1
retry
end
rescue StandardError => e
case e.message
when '400 Bad Request', '404 Resource Not Found'
"#{e.message}: #{e.response}"
else
raise e
end
end
# Supported Method Parameters::
# baresip_obj = PWN::Plugins::BareSIP.start(
# src_num: 'Optional source phone number displayed',
# baresip_bin: 'Optional path of baresip binary (Defaults to /usr/bin/baresip)',
# config_root: 'Optional dir of baresip config (Defaults to ~/.baresip)',
# session_root: 'Optional dir of baresip session (Defaults to Dir.pwd)',
# screenlog_path: 'Optional path of screenlog file (Defaults to screenlog.txt)',
# screen_session: 'Optional name of screen session (Defaults baresip)'
# )
public_class_method def self.start(opts = {})
src_num = opts[:src_num]
baresip_bin = opts[:baresip_bin] if File.exist?(
opts[:baresip_bin].to_s
)
baresip_bin ||= '/usr/bin/baresip'
session_root = opts[:session_root] if Dir.exist?(opts[:session_root].to_s)
config_root = opts[:config_root] if Dir.exist?(
opts[:config_root].to_s
)
config_root ||= "#{Dir.home}/.baresip"
config = "#{config_root}/config"
config_lines = File.readlines(config)
http_list_entry = config_lines.grep(/^http_listen\s.+$/)
raise "no http_listen value found in #{config}." if http_list_entry.empty?
# Update http_listen value in respective config with random available port
# random_port = -1
# port_in_use = true
# while port_in_use
# random_port = Random.rand(1024..65_535)
# port_in_use = PWN::Plugins::Sock.check_port_in_use(port: random_port)
# end
random_port = PWN::Plugins::Sock.get_random_unused_port
http_listen_ip_port = "127.0.0.1:#{random_port}"
updated_config_content = ''
File.read(config).each_line do |line|
this_config_line = line
# Only allow one call per thread.
this_config_line = "call_max_calls 1\n" if line.match?(/^call_max_calls\s.+$/)
# One random HTTP listener / thread
if line.match?(/^http_listen\s.+$/)
this_config_line = "http_listen #{http_listen_ip_port}\n"
end
updated_config_content = "#{updated_config_content}#{this_config_line}"
end
File.write(config, updated_config_content)
baresip_obj = {}
screenlog_path = opts[:screenlog_path]
screenlog_path ||= "#{session_root}/screenlog.txt"
baresip_obj[:screenlog_path] = screenlog_path
screen_session = opts[:screen_session]
screen_session ||= 'baresip'
baresip_obj[:config_root] = config_root
baresip_obj[:http_listen_ip_port] = http_listen_ip_port
baresip_obj[:session_root] = session_root
baresip_obj[:screen_session] = screen_session
# Prefer running baresip in detached screen vs --daemon mode
# Since sndfile doesn't produce .wav files in --daemon mode
system(
'screen',
'-T',
'xterm',
'-L',
'-Logfile',
screenlog_path,
'-S',
screen_session,
'-d',
'-m',
baresip_bin,
'-f',
config_root,
'-e',
'/insmod httpd',
'-e',
'/insmod sndfile',
'-v'
)
ok = 'registered successfully'
gone = 'account: No SIP accounts found'
forb = '403 Forbidden'
# TODO: Make this faster.
print 'Starting baresip...'
loop do
next unless File.exist?(screenlog_path)
dump_session_data = File.readlines(screenlog_path)
dump_session_data.delete_if do |line|
line.include?('ua: using best effort AF: af=AF_INET')
end
break if dump_session_data.select { |s| s.include?(ok) }.length.positive?
next unless dump_session_data.select { |s| s.include?(gone) }.length.positive?
next unless dump_session_data.select { |s| s.include?(forb) }.length.positive?
error = gone if dump_session_data.select { |s| s.include?(gone) }.length.positive?
error = forbid if dump_session_data.select { |s| s.include?(forb) }.length.positive?
raise "Something happened when attempting to start baresip: #{error}"
end
puts 'ready.'
baresip_obj
rescue StandardError => e
raise e
end
# Supported Method Parameters::
# cmd_resp = PWN::Plugins::BareSIP.baresip_exec(
# baresip_obj: 'Required - baresip obj returned from #start method',
# cmd: 'Required - command to send to baresip HTTP daemon'
# )
public_class_method def self.baresip_exec(opts = {})
baresip_obj = opts[:baresip_obj]
http_listen_ip_port = baresip_obj[:http_listen_ip_port]
cmd = opts[:cmd]
baresip_http_call(
baresip_obj: baresip_obj,
http_listen_ip_port: http_listen_ip_port,
cmd: cmd
)
rescue StandardError => e
raise e
end
# Supported Method Parameters::
# PWN::Plugins::BareSIP.stop(
# screen_session: 'Required - screen session to stop'
# )
public_class_method def self.stop(opts = {})
baresip_obj = opts[:baresip_obj]
screen_session = baresip_obj[:screen_session]
puts "STOPPING #{baresip_obj[:screen_session]}"
cmd_resp = baresip_exec(
baresip_obj: baresip_obj,
cmd: "/quit\r\n"
)
rescue StandardError => e
raise e
end
# Supported Method Parameters::
# PWN::Plugins::BareSIP.parse_target_file(
# target_file: 'Required - txt file containing phone numbers to dial',
# randomize: 'Optional - randomize list of phone numbers to dial (Defaults to false)'
# )
public_class_method def self.parse_target_file(opts = {})
target_file = opts[:target_file]
randomize = opts[:randomize]
# Parse entries from target_file and build out our target range
target_lines = File.readlines(target_file)
target_range = []
print 'Initializing targets...'
target_lines.each do |target_line|
next if target_line.match?(/^#.*$/)
target_line.scrub.strip.chomp.delete('(').delete(')').delete('.').delete('+')
if target_line.include?('-')
split_range = target_line.split('-')
if split_range.length == 2
from_num = split_range.first.to_i
to_num = split_range.last.to_i
(from_num..to_num).each do |number_in_range|
target_range.push(number_in_range)
end
else
target_line.scrub.strip.chomp.delete('-')
end
else
target_range.push(target_line.to_i)
end
end
puts 'complete.'
# Randomize targets if applicable
target_range.shuffle! if randomize
target_range
rescue StandardError => e
raise e
end
# Supported Method Parameters::
# PWN::Plugins::BareSIP.apply_src_num_rules(
# target_num: 'Required - destination number to derive source number',
# src_num_rules: 'Optional - Comma-delimited list of rules for src_num format (i.e. XXXXXXXXXX, self, same_country, same_area, and/or same_prefix [Defaults to random src_num w/ same length as target_num])'
# )
public_class_method def self.apply_src_num_rules(opts = {})
config_root = opts[:config_root] if Dir.exist?(
opts[:config_root].to_s
)
config_root ||= "#{Dir.home}/.baresip"
src_num_rules = opts[:src_num_rules]
target_num = opts[:target_num]
src_num_rules_arr = []
if src_num_rules
src_num_rules_arr = src_num_rules.delete("\s").split(',').map(
&:to_sym
)
end
case target_num.to_s.length
when 10
# area+prefix+suffix
country = ''
when 11
# 1 digit country+area+prefix+suffix
country = format('%0.1d', Random.rand(1..9))
country = target_num.to_s.chars.first if src_num_rules_arr.include?(
:same_country
)
when 12
# 2 digit country+area+prefix+suffix
country = format('%0.2d', Random.rand(1..99))
country = target_num.to_s.chars[0..1].join if src_num_rules_arr.include?(
:same_country
)
when 13
# 3 digit country+area+prefix+suffix
country = format('%0.3d', Random.rand(1..999))
country = target_num.to_s.chars[0..2].join if src_num_rules_arr.include?(
:same_country
)
when 14
# 4 digit country+area+prefix+suffix
country = format('%0.4d', Random.rand(1..9999))
country = target_num.to_s.chars[0..3].join if src_num_rules_arr.include?(
:same_country
)
else
raise "Target # should be 10-14 digits. Length is: #{target_num.to_s.length}"
end
src_num_rules_arr.delete(:same_country)
# > 799 for prefix leads to call issues when calling 800 numbers.
# area = format('%0.3s', Random.rand(200..999))
area = format('%0.3d', Random.rand(200..999))
area = target_num.to_s.chars[-10..-8].join if src_num_rules_arr.include?(
:same_area
)
src_num_rules_arr.delete(:same_area)
prefix = format('%0.3d', Random.rand(200..999))
prefix = target_num.to_s.chars[-7..-5].join if src_num_rules_arr.include?(
:same_prefix
)
src_num_rules_arr.delete(:same_prefix)
suffix = format('%0.4d', Random.rand(0..9999))
# Defaults to Random Source Number
src_num = "#{country}#{area}#{prefix}#{suffix}"
# Change to same as dest if :self is passed
src_num = target_num if src_num_rules_arr.include?(:self)
src_num_rules_arr.delete(:self)
# Assume a custom number is passed if an element
# still exists in src_num_rules_arr
# Cast symbol to string, string to integer to massage input,
# and cast back to string.
src_num = src_num_rules_arr.first.to_s.to_i.to_s if src_num_rules_arr.any?
# TODO: Update ~/.baresip/accounts to apply source number
sip_accounts_path = "#{config_root}/accounts"
updated_account_content = ''
File.read(sip_accounts_path).each_line do |line|
this_account_line = line
if line.match?(/^/)
sip_account_to_keep = this_account_line.split('@').last
this_account_line = " e
raise e
end
# Supported Method Parameters::
# PWN::Plugins::BareSIP.dial_target_in_list(
# )
public_class_method def self.dial_target_in_list(opts = {})
baresip_bin = opts[:baresip_bin]
target_num = opts[:target_num]
config_root = opts[:config_root] if Dir.exist?(
opts[:config_root].to_s
)
config_root ||= "#{Dir.home}/.baresip"
session_root = opts[:session_root]
session_root ||= Dir.pwd
randomize = opts[:randomize]
src_num_rules = opts[:src_num_rules]
seconds_to_record = opts[:seconds_to_record].to_i
seconds_to_record = 60 if seconds_to_record.zero?
sox_bin = opts[:sox_bin] if File.exist?(opts[:sox_bin].to_s)
sox_bin ||= '/usr/bin/sox'
speech_to_text = true if opts[:speech_to_text]
speech_to_text ||= false
waveform_bin = 'waveform'
# Intialize empty baresip obj for ensure block below
baresip_obj = {}
# Colors!
red = "\e[31m"
green = "\e[32m"
yellow = "\e[33m"
cayan = "\e[36m"
end_of_color = "\e[0m"
config_root_for_target_num = "#{config_root}-#{target_num}"
FileUtils.cp_r(config_root, config_root_for_target_num)
src_num = apply_src_num_rules(
config_root: config_root_for_target_num,
target_num: target_num,
src_num_rules: src_num_rules
)
call_resp_hash = {}
call_started = Time.now.strftime('%Y-%m-%d_%H.%M.%S')
call_resp_hash[:call_started] = call_started
call_resp_hash[:src_num] = src_num
call_resp_hash[:src_num_rules] = src_num_rules
call_resp_hash[:target_num] = target_num
target_num_root = "#{session_root}/#{target_num}-#{call_started}"
Dir.mkdir(target_num_root)
screenlog_path = "#{target_num_root}/screenlog-#{target_num}.txt"
screen_session = "#{File.basename($PROGRAM_NAME)}-#{target_num}"
# Start baresip in detached screen to support commands over HTTP
# and call recording to wav files
baresip_obj = start(
src_num: src_num,
baresip_bin: baresip_bin,
config_root: config_root_for_target_num,
session_root: session_root,
screenlog_path: screenlog_path,
screen_session: screen_session
)
# session_root = baresip_obj[:session_root]
config_root = baresip_obj[:config_root]
config = "#{config_root}/config"
puts "#{green}#{call_started} >>>#{end_of_color}"
puts "#{yellow}dialing #{target_num}#{end_of_color}"
cmd_resp = baresip_exec(
baresip_obj: baresip_obj,
cmd: "/dial #{target_num}\r\n"
)
puts "/dial #{target_num} RESP:"
puts cmd_resp.xpath('//pre').text
cmd_resp = baresip_exec(
baresip_obj: baresip_obj,
cmd: "/listcalls\r\n"
)
puts '/listcalls RESP:'
puts cmd_resp.xpath('//pre').text
puts red
# Conditions to hangup when less than seconds_to_record
terminated = 'terminated (duration:'
unavail = '503 Service Unavailable'
not_found = 'session closed: 404 Not Found'
reason = 'recording limit reached'
seconds_recorded = 0
seconds_to_record.downto(1).each do |countdown|
seconds_recorded += 1
print "#{seconds_to_record}s to record - remaining: #{format('%-9.9s', countdown)}"
print "\r"
dump_session_data = File.readlines(screenlog_path)
dump_session_data.delete_if do |line|
line.include?('ua: using best effort AF: af=AF_INET')
end
if dump_session_data.select { |s| s.include?(terminated) }.length.positive?
reason = 'call terminated by other party'
break
end
if dump_session_data.select { |s| s.include?(unavail) }.length.positive?
reason = 'SIP 503 (service unavailable)'
break
end
if dump_session_data.select { |s| s.include?(not_found) }.length.positive?
reason = 'SIP 404 (not found)'
break
end
sleep 1
end
call_resp_hash[:seconds_recorded] = seconds_recorded
puts end_of_color
# Move to ensure block?
# call_stopped = Time.now.strftime('%Y-%m-%d_%H.%M.%S')
# puts "\n#{green}#{call_stopped} >>> #{reason} #{target_num}#{end_of_color}"
# call_resp_hash[:call_stopped] = call_stopped
# call_resp_hash[:reason] = reason
# puts "call termination reason: #{reason}"
# stop(baresip_obj: baresip_obj)
# FileUtils.rm_rf(config_root_for_target_num)
# End of ensure block
absolute_recording = ''
relative_recording = ''
Dir.glob("#{session_root}/dump-*=>*#{target_num}*.wav").each do |path|
wav = File.basename(path)
File.delete(path) if wav.match(/^dump-.+#{target_num}.+-enc\.wav$/)
next unless wav.match(/^dump-.+#{target_num}.+-dec.wav/)
FileUtils.mv(path, target_num_root)
absolute_recording = "#{target_num_root}/#{wav}"
relative_recording = "#{target_num}-#{call_started}/#{wav}"
end
screenlog_file = File.basename(screenlog_path)
relative_screenlog = "#{target_num}-#{call_started}/#{screenlog_file}"
call_resp_hash[:screenlog] = relative_screenlog
call_resp_hash[:recording] = '--'
call_resp_hash[:waveform] = '--'
call_resp_hash[:spectrogram] = '--'
unless absolute_recording.empty?
puts cayan
call_resp_hash[:recording] = relative_recording
absolute_spectrogram = "#{absolute_recording}-spectrogram.png"
relative_spectrogram = "#{relative_recording}-spectrogram.png"
print "Generating Audio Spectrogram for #{absolute_recording}..."
system(
sox_bin,
'--show-progress',
'--type',
'sndfile',
'--encoding',
'signed-integer',
'--bits',
'16',
'--endian',
'little',
'--channels',
'1',
'--rate',
'8000',
absolute_recording,
'-n',
'spectrogram',
'-o',
absolute_spectrogram,
'-d',
seconds_to_record.to_s
)
puts 'complete.'
call_resp_hash[:spectrogram] = relative_spectrogram
absolute_waveform = "#{absolute_recording}-waveform.png"
relative_waveform = "#{relative_recording}-waveform.png"
print "Generating Audio Waveform for #{absolute_recording}..."
system(
waveform_bin,
'--method',
'peak',
'--color',
'#FF0000',
'--background',
'#000000',
'--force',
absolute_recording,
absolute_waveform
)
puts 'complete.'
call_resp_hash[:waveform] = relative_waveform
call_resp_hash[:speech_to_text] = 'N/A'
if speech_to_text
abs_rec_dir = File.dirname(absolute_recording)
abs_rec_file = File.basename(absolute_recording, '.*')
absolute_speech_to_text = "#{abs_rec_dir}/#{abs_rec_file}.txt"
# absolute_speech_to_text = "#{absolute_recording}.txt"
rel_rec_dir = File.dirname(relative_recording)
rel_rec_file = File.basename(relative_recording, '.*')
relative_speech_to_text = "#{rel_rec_dir}/#{rel_rec_file}.txt"
# relative_speech_to_text = "#{relative_recording}.txt"
PWN::Plugins::Voice.speech_to_text(
audio_file_path: absolute_recording,
output_dir: target_num_root
)
print "Generating Speech-to-Text for #{absolute_recording}..."
puts 'complete.'
call_resp_hash[:speech_to_text] = relative_speech_to_text
end
puts end_of_color
end
call_resp_hash
rescue StandardError => e
raise e
ensure
# Ensure baresip session is stopped
call_stopped = Time.now.strftime('%Y-%m-%d_%H.%M.%S')
puts "\n#{green}#{call_stopped} >>> #{reason} #{target_num}#{end_of_color}"
call_resp_hash[:call_stopped] = call_stopped
call_resp_hash[:reason] = reason
puts "call termination reason: #{reason}"
stop(baresip_obj: baresip_obj)
FileUtils.rm_rf(config_root_for_target_num)
end
# Supported Method Parameters::
# PWN::Plugins::BareSIP.recon(
# )
public_class_method def self.recon(opts = {})
baresip_bin = opts[:baresip_bin]
config_root = opts[:config_root] if Dir.exist?(
opts[:config_root].to_s
)
config_root ||= "#{Dir.home}/.baresip"
session_root = opts[:session_root]
session_root ||= Dir.pwd
target_file = opts[:target_file]
randomize = opts[:randomize]
src_num_rules = opts[:src_num_rules]
max_threads = opts[:max_threads].to_i
max_threads = 3 if max_threads.zero?
seconds_to_record = opts[:seconds_to_record].to_i
seconds_to_record = 60 if seconds_to_record.zero?
sox_bin = opts[:sox_bin] if File.exist?(opts[:sox_bin].to_s)
sox_bin ||= '/usr/bin/sox'
speech_to_text = opts[:speech_to_text]
report_name = opts[:report_name]
target_range = parse_target_file(
target_file: target_file,
randomize: randomize
)
results_hash = {
report_name: HTMLEntities.new.encode(
report_name.to_s.scrub.strip.chomp
),
session_started: Time.now.strftime('%Y-%m-%d_%H.%M.%S'),
data: []
}
# Change to session_root _before_ executing threads
Dir.chdir(session_root)
# Multi-thread calls!
mutex = Mutex.new
PWN::Plugins::ThreadPool.fill(
enumerable_array: target_range,
max_threads: max_threads
) do |target_num|
call_resp_hash = dial_target_in_list(
baresip_bin: baresip_bin,
target_num: target_num,
config_root: config_root,
session_root: session_root,
randomize: randomize,
src_num_rules: src_num_rules,
seconds_to_record: seconds_to_record,
sox_bin: sox_bin,
speech_to_text: speech_to_text
)
# Push Call Results to results_hash[:data]
mutex.synchronize do
results_hash[:data].push(call_resp_hash)
end
end
results_hash[:session_ended] = Time.now.strftime('%Y-%m-%d_%H.%M.%S')
results_hash
rescue StandardError => e
raise e
end
# Author(s):: 0day Inc.
public_class_method def self.authors
"AUTHOR(S):
0day Inc.
"
end
# Display Usage for this Module
public_class_method def self.help
puts "USAGE:
baresip_obj = #{self}.start(
src_num: 'Optional source phone number displayed',
baresip_bin: 'Optional path of baresip binary (Defaults to /usr/bin/baresip)',
config_root: 'Optional dir of baresip config (Defaults to ~/.baresip)',
session_root: 'Optional dir of baresip session (Defaults to Dir.pwd)',
screenlog_path: 'Optional path of screenlog file (Defaults to screenlog.txt)',
screen_session: 'Optional name of screen session (Defaults baresip)'
)
cmd_resp = #{self}.baresip_exec(
baresip_obj: 'Required - baresip obj returned from #start method',
cmd: 'Required - command to send to baresip HTTP daemon'
)
stopped_bool = #{self}.stop(
screen_session: 'Required - screen session to stop'
)
#{self}.authors
"
end
end
end
end