#!/usr/bin/env python
# -*- coding: utf-8 -*-
# File: transferagent.py
#
# Copyright 2021 Vincent Schouten
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
#
"""
Main code for transferagent.
.. _Google Python Style Guide:
http://google.github.io/styleguide/pyguide.html
NOTE: The TransferAgent class is responsible to purge the stream (ie. index in stream is at COMMAND_PROMPT)
"""
import inspect
import os.path
import pexpect
from .logging import LoggerMixin
__author__ = '''Vincent Schouten <powermole@protonmail.com>'''
__docformat__ = '''google'''
__date__ = '''10-05-2019'''
__copyright__ = '''Copyright 2021, Vincent Schouten'''
__credits__ = ["Vincent Schouten"]
__license__ = '''MIT'''
__maintainer__ = '''Vincent Schouten'''
__email__ = '''<powermole@protonmail.com>'''
__status__ = '''Development''' # "Prototype", "Development", "Production".
# Constant for Pexpect. This prompt is default for Fedora and CentOS.
COMMAND_PROMPT = '[#$] '
[docs]class TransferAgent(LoggerMixin):
"""Establishes a connection to the target destination host via one or more intermediaries."""
def __init__(self, path_ssh_cfg, all_host_addr, deploy_path='/tmp'):
"""Initializes the TransferAgent object.
Args:
path_ssh_cfg (str): Path to the SSH config file that is generated by write_ssh_config_file().
all_host_addr (list): IP addresses of all hosts (eg. gateway/intermediary and destination hosts).
deploy_path (str): Path where the agent.py module (Agent) will reside.
"""
super().__init__()
self.path_ssh_cfg = path_ssh_cfg
self.all_host_addr = all_host_addr
self.last_host_addr = all_host_addr[-1]
self.deploy_path = deploy_path
self.child = None
self.authenticated_hosts = []
def __str__(self):
return 'TransferAgent'
@property
def _path_to_agent_module(self):
"""Determines the path to the Agent-module."""
running_script = inspect.getframeinfo(inspect.currentframe()).filename
running_script_dir = os.path.dirname(os.path.abspath(running_script))
path_file = os.path.join(running_script_dir, 'payload', 'agent.py')
self._logger.debug('agent.py resides in: %s', running_script_dir)
return path_file
def _generate_ssh_runtime_param(self):
"""Composes a SSH runtime param command."""
if len(self.all_host_addr) == 2: # only 1 gateway + destination host
# the result will be something in this format:
# scp -F {} -o 'ProxyJump 10.10.1.72' /home/vincent/Pictures/andy_apollo_imdb.jpg 10.10.2.92:/tmp
order_of_hosts = f'{self.all_host_addr[0]}'
else: # more than 1 gateway + destination host
# the result will be something in this format:
# scp -F {} -o 'ProxyJump 10.10.1.72,10.10.2.92' /home/vincent/Pictures/andy_apollo_imdb.jpg 10.10.3.82:/tmp
order_of_hosts = ''
for i, host in enumerate(self.all_host_addr):
if i == 0:
order_of_hosts += f'{host}'
elif i < len(self.all_host_addr) - 1:
order_of_hosts += f',{host}'
else:
order_of_hosts += f' {host}' # determine if this branch is still necessary.
runtime_param = f"scp -v -F {self.path_ssh_cfg} -o 'ProxyJump {order_of_hosts}' " \
f"{self._path_to_agent_module} "
runtime_param += f'{self.last_host_addr}:{self.deploy_path}'
self._logger.debug(runtime_param)
return runtime_param
[docs] def start(self):
"""Transfers the Agent module.
It determines along the way if the authentication process is successful.
"""
result = False
try:
self.child = pexpect.spawn(self._generate_ssh_runtime_param(), env={"TERM": "dumb"}, timeout=10)
# self.process.setecho(False) # doesn't seem to have effect
# self.process.waitnoecho() # doesn't seem to have effect
self._logger.debug('going through the stream to match patterns: %s', self.all_host_addr)
for hostname in self.all_host_addr:
# according to the documentation, "If you wish to read up to the end of the child's output -
# without generating an EOF exception then use the expect(pexpect.EOF) method."
# but apparently this doesn't work in a shell within a shell (SSH spawns a new shell)
index = self.child.expect(
[f'Authenticated to {hostname}', 'Last failed login:', 'Last login:', 'socket error',
'not accessible', 'fingerprint', 'open failed: connect failed:', pexpect.TIMEOUT])
result = False # reset var as this var could be set True in a previous iteration, we want fresh start
if index == 0:
self._logger.info('authenticated to %s', hostname) # logger level is "info" to inform user
self.authenticated_hosts.append(hostname)
result = True
elif index == 1:
self._logger.debug('there were failed login attempts')
result = True
elif index == 2:
self._logger.debug('there were no failed login attempts')
result = True
elif index == 3:
self._logger.error('socket error. probable cause: SCP service on proxy or target machine disabled')
break
elif index == 4:
self._logger.error('the identity file is not accessible')
break
elif index == 5:
self._logger.warning('warning: hostname automatically added to list of known hosts')
self.child.sendline('yes') # security issue
elif index == 6:
self._logger.error('SCP could not connect to %s', hostname)
break
elif index == 7:
self._logger.error('TIMEOUT exception was thrown. SCP could probably not connect to %s', hostname)
break
else:
self._logger.error('unknown state reached')
self.child.expect(pexpect.EOF) # the buffer has to be 'read' continuously, otherwise Pexpect deteriorates
except pexpect.exceptions.ExceptionPexpect:
self._logger.error('EOF is read; SCP has exited abnormally.')
self.child.terminate()
if not result:
self._logger.error('debug information: %s', str(self.child))
self.child.terminate()
return result