使用 Paramiko 在 Python 中使用 2FA 进行 SSH

2024-04-26

我正在尝试编写一个 Python 3 脚本,该脚本将通过 ssh 连接到远程服务器并使用 paramiko 模块运行命令。

远程服务器使用 Duo 2 因素身份验证,并在使用 ssh 连接时提示您选择身份验证模式:

$ ssh [email protected] /cdn-cgi/l/email-protection
Duo two-factor login for myuser

Enter a passcode or select one of the following options:

 1. Duo Push to +XXX XX-XXX-1111
 2. Phone call to +XXX XX-XXX-1111
 3. SMS passcodes to +XXX XX-XXX-1111

Passcode or option (1-3): 1
Success. Logging you in...

当我在终端中使用 ssh 时,我只需按 1,然后按 Enter,将推送到我的手机,在其中确认连接,然后登录。

不幸的是,我无法在 Python 中做到这一点。这是我尝试使用的代码:

import paramiko

ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())

ssh.connect('remoteserver.com', port=22, username='myuser', password='mypassword')
stdin, stdout, stderr = ssh.exec_command('ls -l')
output = stdout.readlines()
print(output)

如果我在没有 2FA 的远程服务器上尝试相同的代码,它会按预期工作,但使用此服务器时,我会收到身份验证错误:

paramiko.ssh_exception.AuthenticationException: Authentication failed.

任何帮助将不胜感激。


刚刚在我的一个项目中最终解决了这个问题,我将留下我使用的允许这项工作的代码。

主要的收获是 paramiko 确实允许在传输和提示列表中完成此操作,就我而言,我缺少我的 publickey 方法two_factor_types

def _auth(self, username, password, pkey, *args):
    self.password = password
    saved_exception = None
    two_factor = False
    allowed_types = set()
    two_factor_types = {'keyboard-interactive', 'password', 'publickey'}

参考:
Paramiko/Python:键盘交互式身份验证 https://stackoverflow.com/questions/55498370/paramiko-python-keyboard-interactive-authentication
https://github.com/paramiko/paramiko/pull/467 https://github.com/paramiko/paramiko/pull/467
https://github.com/paramiko/paramiko/pull/467/commits/dae916f7bd6723cee95891778baff51ef45532ee https://github.com/paramiko/paramiko/pull/467/commits/dae916f7bd6723cee95891778baff51ef45532ee
http://docs.paramiko.org/en/stable/api/transport.html http://docs.paramiko.org/en/stable/api/transport.html

我建议尝试一些类似的事情 auth_interactive_dumb

auth_interactive_dumb(username, handler=None, submethods='')

以交互方式向服务器进行身份验证,但比较笨。只需将提示和/或指令打印到标准输出并发回响应。这对于通过密钥实现部分身份验证然后用户必须输入 2fac 令牌的情况很有用。


有关更完整的示例,请参阅下面的摘录和链接

完整的 SSH 客户端类供参考 https://gitlab.com/mikeramsey/wizardwebssh/-/blob/master/wizardwebssh/handler.py:

class SSHClient(paramiko.SSHClient):
    duo_auth = False

    def handler(self, title, instructions, prompt_list):
        answers = []
        global duo_auth

        if title.startswith('Duo two-factor login'):
            duo_auth = True
            raise SSHException("Expecting one field only.")

        for prompt_, _ in prompt_list:
            prompt = prompt_.strip().lower()
            if prompt.startswith('password'):
                answers.append(self.password)
            elif prompt.startswith('verification'):
                answers.append(self.totp)
            elif prompt.startswith('passcode'):
                answers.append(self.totp)
            else:
                raise ValueError('Unknown prompt: {}'.format(prompt_))
        return answers

    def auth_interactive(self, username, handler):
        if not self.totp:
            raise ValueError('Need a verification code for 2fa.')
        self._transport.auth_interactive(username, handler)

    def _auth(self, username, password, pkey, *args):
        self.password = password
        saved_exception = None
        two_factor = False
        allowed_types = set()
        two_factor_types = {'keyboard-interactive', 'password', 'publickey'}

        agent = paramiko.Agent()
        try:
            agent_keys = agent.get_keys()
            # if len(agent_keys) == 0:
            # return
        except:
            pass

        for key in agent_keys:
            logging.info("Trying ssh-agent key %s" % hexlify(key.get_fingerprint()))
            try:
                self._transport.auth_publickey(username, key)
                logging.info("... success!")
                return
            except paramiko.SSHException as e:
                logging.info("... nope.")
                saved_exception = e

        if pkey is not None:
            logging.info('Trying publickey authentication')
            try:
                allowed_types = set(
                    self._transport.auth_publickey(username, pkey)
                )
                two_factor = allowed_types & two_factor_types
                if not two_factor:
                    return
            except paramiko.SSHException as e:
                saved_exception = e

        if duo_auth or two_factor:
            logging.info('Trying 2fa interactive auth')
            return self.auth_interactive(username, self.handler)

        if password is not None:
            logging.info('Trying password authentication')
            try:
                self._transport.auth_password(username, password)
                return
            except paramiko.SSHException as e:
                saved_exception = e
                allowed_types = set(getattr(e, 'allowed_types', []))
                two_factor = allowed_types & two_factor_types

        assert saved_exception is not None
        raise saved_exception
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

使用 Paramiko 在 Python 中使用 2FA 进行 SSH 的相关文章

随机推荐