7

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 user@1.1.1.1 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?

Błażej Michalik
  • 4,474
  • 40
  • 55

2 Answers2

10

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.

Błażej Michalik
  • 4,474
  • 40
  • 55
  • This is absolutely beautiful code, and a very ingenuitive solution! Would it be possible to extend Paramiko's actual `SSHClient` or `Channel` classes to implement this as a one step solution, so that the user doesn't need to know how to manually instantiate `mid_cli` and pass `proxy` to `end_cli.connect`? – Vinny Jan 23 '23 at 15:24
-1

Better to post this as a proposed answer, you can do the following:

Code is not tested nor will work as it is very incomplete. I would recommend to check this amazing tut for reference http://www.revsys.com/writings/quicktips/ssh-tunnel.html

From the middle machine

"ssh -f user@anothermachine -L 2000:localhost:22 -N"

From localmachine:

paramiko.connect(middlemachine, 2000)
patito
  • 530
  • 2
  • 13
  • 1
    I can't use port forwarding, because there is a huge firewall between `MyLocalMachine` and `MiddleMachine`, which will block opening any port that tinkering with is not administratively prohibited anyway. Should've mention it. If your solution would work, I'd use paramiko with `direct-tcpip` channel. – Błażej Michalik Feb 13 '17 at 17:03