Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Paramiko: nest ssh session to another machine while preserving paramiko functionality (ProxyJump)

I'm trying to use paramiko to bounce an SSH session via netcat:

 MyLocalMachine ----||----> MiddleMachine --(netcat)--> AnotherMachine
 ('localhost')  (firewall)   ('1.1.1.1')                 ('2.2.2.2')
  • There is no direct connection from MyLocalMachine to AnotherMachine
  • The SSH server on MiddleMachine will not accept any attempts to open a direct-tcpip channel connected to AnotherMachine
  • I can't use SSH keys. I can only connect via given username and password.
  • I can't use sshpass
  • I can't use PExpect
  • I want to connect automatically
  • I want to preserve all of paramiko functionality

I can achieve this partially using the following code:

cli = paramiko.SSHClient()
cli.set_missing_host_key_policy(paramiko.AutoAddPolicy())
proxy = paramiko.ProxyCommand('ssh [email protected] nc 2.2.2.2 22')
cli.connect(hostname='2.2.2.2', username='user', password='pass', sock=proxy)

The thing is, that because ProxyCommand is using subprocess.Popen to run the given command, it is asking me to give the password "ad-hoc", from user input (also, it requires the OS on MyLocalMachine to have ssh installed - which isn't always the case).

Since ProxyCommand's methods (recv, send) are a simple bindings to apropriate POpen methods, I was wondering if it would be possible to trick paramiko client into using another client's session as the proxy?

like image 448
Błażej Michalik Avatar asked Feb 13 '17 16:02

Błażej Michalik


People also ask

How do I use paramiko to connect to a user using SSH?

This example shows how to use paramiko to connect to [email protected] using the SSH key stored in ~/.ssh/id_ed25519, execute the command ls and print its output. Since paramiko is a pure Python implementation of SSH, this does not require SSH clients to be installed.

Does paramiko support 2FA / MFA with SSH?

This should support most 2FA / MFA infrastructure approaches to SSH authentication. The keyboard-interactive authentication handler is injected, permitting easy integration with more advanced use cases. In this example, we use keyboard-interactive authentication on the Jump Host, and we tell Paramiko to 'auto add' (and accept) unknown Host Keys.

What is the use of paramiko in Python?

Paramiko is a Python module that implements the SSHv2 protocol. Paramiko is not part of Python’s standard library, although it’s widely used. This guide shows you how to use Paramiko in your Python scripts to authenticate to a server using a password and SSH keys. If you have not already done so, create a Linode account and Compute Instance.

How do I install paramiko on Anaconda?

If your system is configured to use Anaconda, you can use the following command to install Paramiko: This section shows you how to authenticate to a remote server with a username and password. To begin, create a new file named first_experiment.py and add the contents of the example file.


1 Answers

Update 15.05.18: added the missing code (copy-paste gods haven't been favorable to me).

TL;DR: I managed to do it using simple exec_command call and a class that pretends to be a sock.

To summarize:

  • This solution does not use any other port than 22. If you can manually connect to the machine by nesting ssh clients - it will work. It doesn't require any port forwarding nor configuration changes.
  • It works without prompting for password (everything is automatic)
  • It nests ssh sessions while preserving paramiko functionality.
  • You can nest sessions as many times as you want
  • It requires netcat (nc) installed on the proxy host - although anything that can provide basic netcat functionality (moving data between a socket and stdin/stdout) will work.

So, here be the solution:

The masquerader

The following code defines a class that can be used in place of paramiko.ProxyCommand. It supplies all the methods that a standard socket object does. The init method of this class takes the 3-tupple that exec_command() normally returns:

Note: It was tested extensively by me, but you shouldn't take anything for granted. It is a hack.

import paramiko
import time
import socket     
from select import select                                                       


class ParaProxy(paramiko.proxy.ProxyCommand):                      
    def __init__(self, stdin, stdout, stderr):                             
        self.stdin = stdin                                                 
        self.stdout = stdout                                               
        self.stderr = stderr
        self.timeout = None
        self.channel = stdin.channel                                               

    def send(self, content):                                               
        try:                                                               
            self.stdin.write(content)                                      
        except IOError as exc:                                             
            raise socket.error("Error: {}".format(exc))                                                    
        return len(content)                                                

    def recv(self, size):                                                  
        try:
            buffer = b''
            start = time.time()

            while len(buffer) < size:
                select_timeout = self._calculate_remaining_time(start)
                ready, _, _ = select([self.stdout.channel], [], [],
                                     select_timeout)
                if ready and self.stdout.channel is ready[0]:
                      buffer += self.stdout.read(size - len(buffer))

        except socket.timeout:
            if not buffer:
                raise

        except IOError as e:
            return ""

        return buffer

    def _calculate_remaining_time(self, start):
        if self.timeout is not None:
            elapsed = time.time() - start
            if elapsed >= self.timeout:
                raise socket.timeout()
            return self.timeout - elapsed
        return None                                   

    def close(self):                                                       
        self.stdin.close()                                                 
        self.stdout.close()                                                
        self.stderr.close()
        self.channel.close()                                                                                                                            

The usage

The following shows how I used the above class to solve my problem:

# Connecting to MiddleMachine and executing netcat
mid_cli = paramiko.SSHClient()
mid_cli.set_missing_host_key_policy(paramiko.AutoAddPolicy())
mid_cli.connect(hostname='1.1.1.1', username='user', password='pass')
io_tupple = mid_cli.exec_command('nc 2.2.2.2 22')

# Instantiate the 'masquerader' class
proxy = ParaProxy(*io_tupple)

# Connecting to AnotherMachine and executing... anything...
end_cli = paramiko.SSHClient()
end_cli.set_missing_host_key_policy(paramiko.AutoAddPolicy())
end_cli.connect(hostname='2.2.2.2', username='user', password='pass', sock=proxy)
end_cli.exec_command('echo THANK GOD FINALLY')

Et voila.

like image 111
Błażej Michalik Avatar answered Oct 11 '22 08:10

Błażej Michalik