##
# $Id: wardial.rb 9929 2010-07-25 21:37:54Z jduck $
##

##
# This file is part of the Metasploit Framework and may be subject to
# redistribution and commercial restrictions. Please see the Metasploit
# Framework web site for more information on licensing and terms of use.
# http://metasploit.com/framework/
##

require 'msf/core'
require 'zlib'


# Extend Object class to include save_to_file and load_from_file methods
class Object
	def self.save_to_file obj, filename, options={}
		#obj = self
		marshal_dump = Marshal.dump(obj)
		file = File.new(filename,'w')
		file = Zlib::GzipWriter.new(file) unless options[:gzip] == false
		file.write marshal_dump
		file.close
		return obj
	end

	def self.load_from_file filename
		begin
			file = Zlib::GzipReader.open(filename)
		rescue Zlib::GzipFile::Error
			file = File.open(filename, 'rb')
		ensure
			return nil if ! file
			#obj = Marshal.load file.read
			obj = Marshal.load file.read
			file.close
			return obj
		end
	end
end

class Metasploit3 < Msf::Auxiliary

	include Msf::Auxiliary::Report
	include Msf::Auxiliary::Scanner

	def initialize
		super(
			'Name'        => 'Wardialer',
			'Version'     => '$Revision: 9929 $',
			'Description' => 'Scan for dial-up systems that are connected to modems and answer telephony indials.',
			'Author'      => [ 'I)ruid' ],
			'License'     => MSF_LICENSE
		)

		register_options(
		[
			OptString.new('DIALMASK',     [true,  'Dial Mask (e.g. 1.800.95X.99XX, (202) 358-XXXX, 358.####, etc.)', '202.358.XXXX']),
			OptString.new('DIALPREFIX',   [true,  'Dial Prefix', 'ATDT']),
			OptString.new('INITSTRING',   [true,  'Initialization String', 'AT X6 S11=80']),
			OptString.new('SERIALPORT',   [true,  'Serial Port (e.g. 0 (COM1), 1 (COM2), /dev/ttyS0, etc.)', '/dev/ttyS0']),
		], self.class)

		register_advanced_options(
		[
			OptInt.new(   'BaudRate',     [true,  'Baud Rate', 19200]),
			OptEnum.new(  'DataBits',     [true,  'Data Bits (4 is Windows Only)', '8', ['4', '5', '6', '7', '8'], '8']),
			OptInt.new(   'ConnTimeout',  [true,  'Timeout per data connection in seconds', 45]),
			OptInt.new(   'DialDelay',    [true,  'Time to wait between dials in seconds (rec. min. 1)', 1]),
			OptString.new('DialSuffix',   [false, 'Dial Suffix', nil]),
			OptInt.new(   'DialTimeout',  [true,  'Timeout per dialed number in seconds', 40]),
			OptBool.new(  'DisplayModem', [false,  'Displays modem commands and responses on the console', false]),
			OptEnum.new(  'FlowControl',  [true,  'Flow Control', 'None', ['None', 'Hardware', 'Software', 'Both'], 'None']),
			OptInt.new(   'InitInterval', [true,  'Number of dials before reinitializing modem', 30]),
			#OptEnum.new(  'LogMethod',    [true,  'Log Method', 'File', ['File', 'DataBase', 'TIDBITS'], 'File']),
			OptEnum.new(  'LogMethod',    [true,  'Log Method', 'File', ['File'], 'File']),
			OptString.new('NudgeString',  [false, 'Nudge String', '\x1b\x1b\r\n\r\n']),
			OptEnum.new(  'Parity',       [false,  'Parity (Mark & Space are Windows Only)', 'None', ['None', 'Even', 'Odd', 'Mark', 'Space'], 'None']),
			OptBool.new(  'RedialBusy',   [false,  'Redials numbers found to be busy', false]),
			OptEnum.new(  'StopBits',     [true,  'Stop Bits', '1', ['1', '2'], '1']),
		], self.class)

		deregister_options('NUMBER')
		deregister_options('RPORT')
		deregister_options('RHOSTS')
		deregister_options('PAYLOAD')

		@logmethod = :file
		@commandstate = true

		begin
			require 'telephony'
			@telephony_loaded = true
		rescue ::Exception => e
			@telephony_loaded = false
			@telephony_error  = e
		end
	end

	def run
		if ! @telephony_loaded
			print_error("The Telephony module is not available: #{@telephony_error}")
			raise RuntimeError, "Telephony not available"
		end

		@confdir      = File.join(Msf::Config.get_config_root, 'wardial')
		@datadir      = File.join(Msf::Config.get_config_root, 'logs', 'wardial')

		# make sure working dirs exist
		FileUtils.mkdir_p(@confdir)
		FileUtils.mkdir_p(@datadir)

		@logmethod   = case datastore['LogMethod']
			when 'DataBase' ; :database
			when 'TIDBITS'  ; :tidbits
			else              :file
		end
		serialport   = datastore['SERIALPORT']
		baud         = datastore['BaudRate'].to_i
		data_bits    = datastore['DataBits'].to_i
		stop_bits    = datastore['StopBits'].to_i
		parity       = case datastore['Parity']
			when 'Even' ; Telephony::Modem::EVEN
			when 'Odd'  ; Telephony::Modem::ODD
			when 'Mark' ; Telephony::Modem::MARK
			when 'Space'; Telephony::Modem::SPACE
			else          Telephony::Modem::NONE
		end
		flowcontrol  = case datastore['FlowControl']
			when 'Hardware' ; Telephony::Modem::HARD
			when 'Software' ; Telephony::Modem::SOFT
			when 'Both'     ; Telephony::Modem::HARD | Telephony::Modem::SOFT
			else            ; Telephony::Modem::NONE
		end
		initstring   = datastore['INITSTRING']
		initinterval = datastore['InitInterval']
		dialprefix   = datastore['DIALPREFIX']
		dialsuffix   = datastore['DialSuffix']
		nudgestring  = datastore['NudgeString'] ? eval('"'+datastore['NudgeString']+'"') : "\r\n\r\n"
		dialtimeout  = datastore['DialTimeout'].to_i
		conntimeout  = datastore['ConnTimeout'].to_i
		dialmask     = datastore['DIALMASK'].tr(' ', '')
		dialdelay    = datastore['DialDelay'].to_i
		redialbusy   = datastore['RedialBusy']
		@displaymodem = datastore['DisplayModem']

		# Connect to and init modem
		modem = Telephony::Modem.new(serialport)
		modem.params = {
			'baud'      => baud,
			'data_bits' => data_bits,
			'parity'    => parity,
			'stop_bits' => stop_bits
		}
		modem.flow_control = flowcontrol
		modem.display = @displaymodem

		modem.flush

		# reload data from previous scan
		datfile = @datadir + '/' + dialmask.gsub(/[( ]/, '').gsub(/[).]/, '-').gsub(/[#]/, 'X').upcase + '.dat'
		dialrange = Object.load_from_file(datfile)
		if dialrange
			print_status( "Previous scan data loaded from #{datfile}" )
			select = dialrange.select {|key, value|
				case @target
					when :carrier ; value[:carrier] == true
					when :fax     ; value[:fax]     == true
				end
			}
			num_identified = select.size
			select = dialrange.select {|key, value|
				value[:carrier] == true
			}
			num_carriers = select.size
			select = dialrange.select {|key, value|
				value[:fax] == true
			}
			num_faxes = select.size
			select = dialrange.select {|key, value|
				value[:busy] == true
			}
			num_busy = select.size
		else
			print_status( "No previous scan data found (#{datfile})" )
			dialrange = build_dialrange(dialmask)
			num_identified = 0
			num_carriers   = 0
			num_faxes      = 0
			num_busy       = 0
		end

		# Dial loop
		begin
			done = false
			nextnum = true
			dialcount = 0
			while true

				if dialcount % initinterval == 0
					return if initmodem(modem, initstring) == false
				end

				if nextnum == true
					unidentified = dialrange.select {|key, value|
						value[:identified] == false
					}
					if redialbusy
						unidentified += unidentified.select {|key, value|
							value[:busy] == true
						}
					end
					break if unidentified.size == 0

					chosen  = rand(unidentified.size)
					dialnum = unidentified[chosen][0]
					dialval = unidentified[chosen][1]
				end
				print_status("#{unidentified.size} of #{dialrange.size} numbers unidentified, #{num_carriers} carriers found, #{num_faxes} faxes found, #{num_busy} busy")
				if dialval[:busy] == true
					print_status("Dialing: #{dialnum} (#{dialtimeout} sec. timeout, previously busy)")
				else
					print_status("Dialing: #{dialnum} (#{dialtimeout} sec. timeout, previously undialed)")
				end

				dialstring = dialprefix + ' ' + dialnum
				dialstring += (' ' + dialsuffix) if dialsuffix

				modem.flush
				time = Time.now
				result = modem.put_command(dialstring, dialtimeout)
				while result =~ /RINGING/i
					result = modem.get_response(dialtimeout-(Time.now-time))
				end
				dialcount += 1
				dialrange[dialnum][:dialed] = dialnum

				case result
					when /TIMEOUT/i
						print_status( 'Timeout' )
						dialrange[dialnum][:identified] = true
						dialrange[dialnum][:result] = result
						dialrange[dialnum][:timeout] = true
						dialrange[dialnum][:timestamp] = Time.now
						modem.puts "\r\n" # force the modem to respond to last command (hangup/abort)
						result = modem.get_response(3)
					when /CONNECT/i
						print_status( "Carrier: #{result}" )
						@commandstate = false
						dialrange[dialnum][:identified] = true
						dialrange[dialnum][:result] = result
						dialrange[dialnum][:carrier] = true
						dialrange[dialnum][:timestamp] = Time.now
						dialrange[dialnum][:banner] = get_banner(modem, conntimeout, nudgestring)
						modem.hangup
						initmodem(modem, initstring)
						num_carriers += 1
						note = dialrange[dialnum][:result] + "\n" + dialrange[dialnum][:banner]
						report_note(:host => dialnum, :type => "wardial_result", :data => note)
						log_result(dialrange[dialnum])
					when /HK_CARRIER/i
						print_status( "Carrier: #{result}" )
						dialrange[dialnum][:identified] = true
						dialrange[dialnum][:result] = result
						dialrange[dialnum][:carrier] = true
						dialrange[dialnum][:timestamp] = Time.now
						modem.hangup
						initmodem(modem, initstring)
						num_carriers += 1
						note = dialrange[dialnum][:result] + "\n" + dialrange[dialnum][:banner]
						report_note(:host => dialnum, :type => "wardial_result", :data => note)
						log_result(dialrange[dialnum])
					when /\+FCO/i
						print_status( "Fax: #{result}" )
						dialrange[dialnum][:identified] = true
						dialrange[dialnum][:result] = result
						dialrange[dialnum][:fax] = true
						dialrange[dialnum][:timestamp] = Time.now
						modem.hangup
						initmodem(modem, initstring)
						num_faxes += 1
						note = dialrange[dialnum][:result] + "\n" + dialrange[dialnum][:banner]
						report_note(:host => dialnum, :type => "wardial_result", :data => note)
						log_result(dialrange[dialnum])
					when /VOICE/i
						print_status( "Voice" )
						dialrange[dialnum][:identified] = true
						dialrange[dialnum][:result] = result
						dialrange[dialnum][:voice] = true
						dialrange[dialnum][:timestamp] = Time.now
						modem.hangup
					when /HK_VMB/i
						dialrange[dialnum][:identified] = true
						dialrange[dialnum][:result] = result
						dialrange[dialnum][:voicemail] = true
						dialrange[dialnum][:timestamp] = Time.now
						modem.hangup
					when /HK_AVS/i
						dialrange[dialnum][:identified] = true
						dialrange[dialnum][:result] = result
						dialrange[dialnum][:avs] = true
						dialrange[dialnum][:timestamp] = Time.now
						modem.hangup
					when /HK_NOTED/i
						dialrange[dialnum][:identified] = true
						dialrange[dialnum][:result] = result
						dialrange[dialnum][:noted] = true
						dialrange[dialnum][:timestamp] = Time.now
						modem.hangup
					when /HK_GIRL/i
						dialrange[dialnum][:identified] = true
						dialrange[dialnum][:result] = result
						dialrange[dialnum][:girl] = true
						dialrange[dialnum][:timestamp] = Time.now
						modem.hangup
					when /NO CARRIER/i
						print_status( "No Carrier" )
						dialrange[dialnum][:identified] = true #TODO: should this be false?
						dialrange[dialnum][:result] = result
						dialrange[dialnum][:timestamp] = Time.now
					when /BUSY/i
						print_status( "Busy" )
						dialrange[dialnum][:identified] = false
						dialrange[dialnum][:result] = result
						dialrange[dialnum][:busy] = true
						dialrange[dialnum][:timestamp] = Time.now
						num_busy += 1
					when /OK/i
						print_status( "Unexpected OK response..." )
					when /NO DIAL *TONE/i
						nextnum = false
						modem.hangup
						select(nil,nil,nil,1)
						next
					when nil
						modem.hangup
					else
						print_status( "Unrecognized Response String" )
				end

				Object.save_to_file(dialrange, datfile)
				#dialrange.save_to_file(datfile)
				nextnum = true
				select(nil,nil,nil,1) # we need at least a little buffer for the modem to hangup/reset
				select(nil,nil,nil,dialdelay-1) if dialdelay >= 1
			end

		rescue ::Interrupt
			modem.hangup
			Object.save_to_file(dialrange, datfile)
			#dialrange.save_to_file(datfile)
			raise $!
		rescue ::Exception => e
			print_error("Error during dial process: #{e.class} #{e} #{e.backtrace}")
			return
		end

		print_status("Dialing Complete")
		modem.close
	end

	def initmodem(modem, initstring)
		print_status("Initializing Modem")
		result = modem.put_command('ATZ', 3)
		if result != 'OK'
			print_error("Error resetting modem")
			return false
		end
		result = modem.put_command(initstring, 3)
		if result != 'OK'
			print_error("Error initializing modem")
			return false
		end

		return true
	end

	def build_dialrange(dialmask)
		dialrange = {}

		incdigits = 0
		dialmask.each_char {|c|
			incdigits += 1 if c =~ /^[X#]$/i
		}
		max = (10**incdigits)-1
		print_status("Detected #{incdigits} masked digits in DIALMASK (#{dialmask})")
		print_status("Generating storage for #{max+1} numbers to dial")

		(0..max).each {|num|
			number = dialmask.dup # copy the mask
			numstr = sprintf("%0#{incdigits}d", num) # stringify our incrementing number
			j = 0 # index for numstr
			for i in 0..number.length-1 do # step through the number (mask)
				if number[i].chr =~ /^[X#]$/i
					number[i] = numstr[j] # replaced masked indexes with digits from incrementing number
					j += 1
				end
			end
			dialrange[number] = {}
			dialrange[number][:identified] = false
		}
		#print_status("Storage for #{dialrange.size} numbers generated")

		return dialrange
	end

	def log_result(dialnum)
		case @logmethod
			when :file
				logfile = File.join(@datadir, 'found.log')
				file = File.new(logfile, 'a')
				file.puts( "#####( NEW LOG ENTRY )#####\n")
				file.puts( "#{Time.now}\n")
				file.puts( "#{dialnum[:dialed]} : #{dialnum[:result]}\n")
				file.puts( "#{dialnum[:banner]}\n") if dialnum[:banner]
				file.puts( "#####( END LOG ENTRY )#####\n")
				file.close
			when :database
			when :tidbits
		end
	end

	def get_banner(modem, timeout, nudgestring)
		print_status("Grabbing banner...")
		banner = ''

		time = Time.now
		gotchar = Time.now
		while Time.now < time + timeout
			if Time.now >= gotchar + 8 # nudges after 8 seconds of receiving nothing
				if nudgestring
					print_status( "Nudging..." )
					modem.puts nudgestring
				end
				gotchar = Time.now # resets timer so we don't nudge too often
			end

			c = modem.getc
			next if ! c

			gotchar = Time.now
			print( c.chr ) if @displaymodem

			# stop if carrier dropped
			break if modem.dcd == 0

			banner += c.chr
		end

		print("\n") if @displaymodem
		return banner
	end

end

