# frozen_string_literal: true # # ronin-post_ex - a Ruby API for Post-Exploitation. # # Copyright (c) 2007-2023 Hal Brodigan (postmodern.mod3 at gmail.com) # # ronin-post_ex is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published # by the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # ronin-post_ex is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with ronin-post_ex. If not, see . # require 'ronin/post_ex/sessions/session' require 'shellwords' require 'base64' module Ronin module PostEx module Sessions # # Base class for all interactive shell based post-exploitation sessions. # # ## Features # # * Supports Bash, Zsh, and all POSIX shells. # * Emulates most of the post-exploitation API via shell commands. # class ShellSession < Session # The IO object used to communicate with the shell. # # @return [Socket, IO] # # @api private attr_reader :io # # Initializes the shell session. # # @param [Socet, IO] io # The IO object used to communicate with the shell. # def initialize(io) @io = io end # # @group Shell Methods # # # Writes a line to the shell. # # @param [String] line # The line to write. # # @api private # def shell_puts(line) @io.write("#{line}\n") end # # Reads a line from the shell. # # @return [String, nil] # # @api private # def shell_gets @io.gets end # Deliminator line to indicate the beginning and end of output DELIMINATOR = '---' # # Executes a shell command and returns it's output. # # @param [String] command # The shell command to execute. # # @return [String] # The output of the shell command. # def shell_exec(command) shell_puts("echo #{DELIMINATOR}; #{command} 2>/dev/null | base64; echo #{DELIMINATOR}") # consume any leading output before the command output while (line = shell_gets) if line.chomp == DELIMINATOR break end end output = String.new while (line = shell_gets) if line.chomp == DELIMINATOR break end output << line end return Base64.decode64(output) end # # Joins a command with arguments into a single command String. # # @param [String] command # The command name to execute. # # @param [Array] arguments # Additional arguments for the command. # # @return [String] # The command string. # # @api private # def command_join(command,*arguments) Shellwords.join([command,*arguments]) end # # Invokes a specific command with arguments. # # @param [String] command # The command name to execute. # # @param [Array] arguments # Additional arguments for the command. # # @return [String, nil] # The command's output or `nil` if there was no output. # # @api private # def command_exec(command,*arguments) output = shell_exec(command_join(command,*arguments)) if output.empty? then nil else output end end # # @group System Methods # # # Gets the current time and returns the UNIX timestamp. # # @return [Integer] # The current time as a UNIX timestamp. # # @note executes the `date +%s` command. # def sys_time shell_exec('date +%s').to_i end # # Gets the system's hostname. # # @return [String] # # @note executes the `echo $HOSTNAME` command. # def sys_hostname shell_exec("echo $HOSTNAME").chomp end # # @group File-System methods # # # Gets the current working directory and returns the directory path. # # @return [String] # The remote current working directory. # # @note executes the `pwd` command. # def fs_getcwd shell_exec('pwd').chomp end # # Changes the current working directory. # # @param [String] path # The new remote current working directory. # # @note executes the `cd ` command. # def fs_chdir(path) shell_puts("cd #{Shellwords.escape(path)} 2>/dev/null") end # # Reads the entire file at the given path and returns the full file's # contents. # # @param [String] path # The remote path to read. # # @return [String, nil] # The contents of the remote file or `nil` if the file could not be # read. # # @note executes the `cat ` command. # def fs_readfile(path) command_exec('cat',path) end # # Reads the destination path of a remote symbolic link. # # @param [String] path # The remote path to read. # # @return [String, nil] # The destination of the remote symbolic link or `nil` if the symbolic # link could not be read. # # @note executes the `readlink -f ` command. # def fs_readlink(path) command_exec('readlink','-f',path).chomp end # # Reads the contents of a remote directory and returns an Array of # directory entry names. # # @param [String] path # The path of the remote directory to read. # # @return [Array] # The entities within the remote directory. # # @note executes the `ls ` command. # def fs_readdir(path) command_exec('ls',path).lines(chomp: true) end # # Evaluates a directory glob pattern and returns all matching paths. # # @param [String] pattern # The glob pattern to search for remotely. # # @return [Array] # The matching paths. # # @note executes the `ls ` command. # def fs_glob(pattern,&block) shell_exec("ls #{pattern}").lines(chomp: true) end # # Creates a remote temporary file with the given file basename. # # @param [String] basename # The basename for the new temporary file. # # @return [String] # The path of the newly created temporary file. # # @note executes the `mktemp ` command. # def fs_mktemp(basename) command_exec('mktemp',basename).chomp end # # Creates a new remote directory at the given path. # # @param [String] new_path # The new remote directory to create. # # @note executes the `mkdir ` command. # def fs_mkdir(new_path) command_exec('mkdir',new_path) end # # Copies a source file to the destination path. # # @param [String] path # The source file. # # @param [String] new_path # The destination path. # # @note executes the `cp -r ` command. # def fs_copy(path,new_path) command_exec('cp','-r',path,new_path) end # # Removes a file at the given path. # # @param [String] path # The remote path to remove. # # @note executes the `rm ` command. # def fs_unlink(path) command_exec('rm',path) end # # Removes an empty directory at the given path. # # @param [String] path # The remote directory path to remove. # # @note executes the `rmdir ` command. # def fs_rmdir(path) command_exec('rmdir',path) end # # Moves or renames a remote source file to a new destination path. # # @param [String] path # The source file path. # # @param [String] new_path # The destination file path. # # @note executes the `mv ` command. # def fs_move(path,new_path) command_exec('mv',path,new_path) end # # Creates a remote symbolic link at the destination path pointing to the # source path. # # @param [String] src # The source file path for the new symbolic link. # # @param [String] dest # The remote path of the new symbolic link. # # @note executes the `ln -s ` command. # def fs_link(src,dest) command_exec('ln','-s',src,dest) end # # Changes the group ownership of a remote file or directory. # # @param [String] group # The new group name for the remote file or directory. # # @param [String] path # The path of the remote file or directory. # # @note executes the `chgrp ` command. # def fs_chgrp(group,path) command_exec('chgrp',group,path) end # # Changes the user ownership of remote a file or directory. # # @param [String] user # The new user for the remote file or directory. # # @param [String] path # The path of the remote file or directory. # # @note executes the `chown ` command. # def fs_chown(user,path) command_exec('chown',user,path) end # # Changes the permissions on a remote file or directory. # # @param [Integer] mode # The permissions mode for the remote file or directory. # # @param [String] path # The path of the remote file or directory. # # @note executes the `chmod ` command. # def fs_chmod(mode,path) umask = "%.4o" % mode command_exec('chmod',umask,path) end # # Queries file information for the given remote path and returns a Hash # of file metadata. # # @param [String] path # The path to the remote file or directory. # # @return [Hash{Symbol => Object}, nil] # The metadata for the remote file. # # @note executes the `stat -t ` command. # def fs_stat(path) fields = command_exec('stat','-t',path).strip.split(' ') return { path: path, size: fields[1].to_i, blocks: fields[2].to_i, uid: fields[4].to_i, gid: fields[5].to_i, inode: fields[7].to_i, links: fields[8].to_i, atime: Time.at(fields[11].to_i), mtime: Time.at(fields[12].to_i), ctime: Time.at(fields[13].to_i), blocksize: fields[14].to_i } end # # @group Process methods # # # Gets the current process's Process ID (PID). # # @return [Integer] # The current process's PID. # # @note executes the `echo $$` command. # def process_getpid shell_exec('echo $$').to_i end # # Gets the current process's parent Process ID (PPID). # # @return [Integer] # The current process's PPID. # # @note executes the `echo $PPID` command. # def process_getppid shell_exec('echo $PPID').to_i end # # Gets the current process's user ID (UID). # # @return [Integer] # The current process's UID. # # @note executes the `id -u` command. # def process_getuid command_exec('id','-u').to_i end # # Gets the current process's group ID (GID). # # @return [Integer] # The group ID (GID) for the current process. # # @note executes the `id -g` command. # def process_getgid command_exec('id','-g').to_i end # # Queries all environment variables of the current process. Returns a # Hash of the env variable names and values. # # @return [Hash{String => String}] # The Hash of environment variables. # # @note executes the `env` command. # def process_environ Hash[command_exec('env').each_line(chomp: true).map { |line| line.split('=',2) }] end # # Gets an individual environment variable. If the environment variable # has not been set, `nil` will be returned. # # @param [String] name # The environment variable name to get. # # @return [String] # The environment variable value. # # @note executes the `echo $` command. # def process_getenv(name) shell_exec("echo $#{name}").chomp end # # Sets an environment variable to the given value. # # @param [String] name # The environment variable name to set. # # @param [String] value # The new value for the environment variable. # # @note executes the `export =` command. # def process_setenv(name,value) shell_puts("export #{name}=#{value}") end # # Un-sets an environment variable. # # @param [String] name # The environment variable to unset. # # @note executes the `unset ` command. # def process_unsetenv(name) shell_puts("unset #{name}") end # # Kills another process using the given Process ID (POD) and the signal # number. # # @param [Integer] pid # The process ID (PID) to kill. # # @param [Integer] signal # The signal to send the process ID (PID). # # @note executes the `kill -s ` command. # def process_kill(pid,signal) command_exec('kill','-s',signal,pid) end # # Spawns a new process using the given program and additional arguments. # # @param [String] command # The command name to spawn. # # @param [Array] arguments # Additional arguments for the program. # # @return [Integer] # The process ID (PID) of the spawned process. # # @note # executes the command with additional arguments as a background # process. # def process_spawn(command,*arguments) command = command_join(command,*arguments) shell_exec("#{command} 2>&1 >/dev/null &; echo $!").to_i end # # Exits the current process. # # @note executes the `exit` command. # def process_exit shell_puts('exit') end # # Closes the remote shell. # def close @io.close end end end end end