New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Server implementation does not check for auth before serving later requests #1175
Comments
A later note for testing: in addition to responding to Those are just responded to with |
Also also, I am finding that there are some Travis-level issues outstanding for most recent branches, orthogonal to this ticket (python 3.7-dev is exploding for no good reason) so this will not appear to go green there until I tackle that afterwards. |
More notes: in addition to writing the about-to-be-pushed tests, I also executed a standalone set of scripts provided by Matthijs, with the same net effect & result ("succeeds", as in the channel opens, without the fix; "fails", with the correct exception, with the fix.) |
At least, insofar as the new tests pass...!
At least, insofar as the new tests pass...!
Gotta figure out when I will stop backporting anything to 1.x, it is such a huge logistical hassle at this point despite overall code compatibility |
This is now on PyPI as Paramiko versions 1.17.6, 1.18.5, 2.0.8, 2.1.5, 2.2.3, 2.3.2, and 2.4.1. Side note: I cannot bloody imagine doing this level of back and forth branch-wankery on Subversion. Very glad git has been my friend the last decade or so. |
Nice fix, thanks. Should the server side in _ensure_authed() or in run() be doing any logging (possibly at DEBUG level) if it sees these pre-auth attempts to open channels? |
Here's the fix backported to paramiko 1.10 that we will likely use in Ubuntu 14.04 LTS in case it's useful to anyone else. |
At least, insofar as the new tests pass...!
Where can I find Matthijs's client-test.py? I'd like to see if our specific implementation of Paramiko server is affected. We will upgrade to the latest version, but we have some old installations that will not be patched quickly. |
@six8 I probably didn't share it inline before due to sensitivity reasons but at this point with the patch out, I don't see why not. It's pretty innocuous either way: #!/usr/bin/env python
import paramiko
import logging
hostname = "localhost"
port = 2200
skip_auth = True
try:
logging.basicConfig(level=logging.DEBUG)
client = paramiko.SSHClient()
client.load_system_host_keys()
client.set_missing_host_key_policy(paramiko.WarningPolicy())
# Override the _auth method to let Paramiko skip the auth step
if skip_auth:
client._auth = lambda *args, **kwargs: None
client.connect(hostname, port=port)
client.exec_command('ls')
finally:
client.close() |
@bitprophet thanks. Easy enough. |
What follows is a direct paste from a private gist used to workshop the issue a bit late last week; have completed tests/impl (from 1.17+) and will be pushing those shortly. (wanted the issue number set in stone first...:D)
We have a CVE for this issue: CVE-2018-7750.
Intro
Email from one Matthijs Kooijman (@matthijskooijman) dated 2018.03.02 notes that Paramiko's server implementation may be connected to by clients that do not implement the auth step, and happily serves up commands/etc to such un-authed clients. He found that AsyncSSH (another Python lib that does not use Paramiko) has the same issue. Finally, he states the RFC is unclear as to whether this is purposeful.
Let's double check both the RFCs and then our favorite reference implementation, OpenSSH.
Should neither provide a useful clue, my gut says the server implementation should track whether we've sent
SSH_MSG_USERAUTH_SUCCESS
(99% sure we already do track this) and default to rejecting any connection-level messages (likeSSH_MSG_CHANNEL_OPEN
orSSH_MSG_GLOBAL_REQUEST
) unless that flag is True.RFC scan
tl;dr it is indeed kinda vague, there are two kinda-disagreeing undercurrents, neither of which are ironclad:
Specifics:
RFC 4251 (protocol arch)
1: pretty clear that the intent is that a user auth step always occurs, followed by a service request:
4.3: third bullet point re: policy issues that 'SHOULD' be addressed, highlights that auth specifics are up to the site/operator:
9.4.3: this whole section arguably applies, but it's very vague. However it seems to back up my hunch that at core, this is up to the server implementer and/or operator, e.g:
RFC 4252 (auth protocol)
4: more vague implications that the server can do whatever it wants, e.g. the below quote about
none
auth implies the authors at least partly considered servers that intentionally don't care about auth at all (though the specific discussion is about the actual, explicit use of thenone
auth type message, which is distinct from "did not submit auth at all"):5.3: this (like 4251.1) states that the server should start up the requested service after sending auth-success. One could read this to imply that services SHOULD NOT start UNLESS auth has occurred, but it's not explicit...
7: notes that implementations MUST implement public-key auth, though I note this is distinct from requiring that it is enabled (clearly, many real servers only offer password auth, for example.)
RFC 4253 (transport protocol)
RFC 4254 (connection protocol)
1: once again implies that connection is "designed to" occur after/on top of auth.
11: again, it's 'assumed':
OpenSSH's implementation
My old friend and the only C codebase I have any familiarity with whatsoever, openssh-portable...
Synopsis
After all the below, the tl;dr seems to be:
NotImplementedError
style situation - no auth step, no idea how to handle anything beyond auth.Deep dive
serverloop.c
->server_loop2()
MSG_CHANNEL_OPEN
it callsserver_input_channel_open()
: https://github.com/openssh/openssh-portable/blob/71e48bc7945f867029e50e06c665c66aed6d3c64/serverloop.c#L897session
: https://github.com/openssh/openssh-portable/blob/71e48bc7945f867029e50e06c665c66aed6d3c64/serverloop.c#L630server_request_session
: https://github.com/openssh/openssh-portable/blob/71e48bc7945f867029e50e06c665c66aed6d3c64/serverloop.c#L582session_open
with a handle onthe_authctxt
: https://github.com/openssh/openssh-portable/blob/71e48bc7945f867029e50e06c665c66aed6d3c64/serverloop.c#L603ssh
object used to get the actual channel in play on the call prior: https://github.com/openssh/openssh-portable/blob/71e48bc7945f867029e50e06c665c66aed6d3c64/serverloop.c#L600extern
'd at top of file: https://github.com/openssh/openssh-portable/blob/71e48bc7945f867029e50e06c665c66aed6d3c64/serverloop.c#L84session_open
is, bizarrely, defined insession.c
: https://github.com/openssh/openssh-portable/blob/de1920d743d295f50e6905e5957c4172c038e8eb/session.c#L1757authctxt->valid
(or a null password entry) and gets mad if not true: https://github.com/openssh/openssh-portable/blob/de1920d743d295f50e6905e5957c4172c038e8eb/session.c#L1767->valid
member actually maps to.Authctxt
struct is defined inauth.h
here: https://github.com/openssh/openssh-portable/blob/71e48bc7945f867029e50e06c665c66aed6d3c64/auth.h#L55-L98success
,authenticated
, andvalid
all seem relevant.success
is not documented;authenticated
sounds like it maps to, well, authentication (user is who they claim to be) withvalid
mapping to (a generic level of) authorization (the user is actually allowed to login.)valid
) aren't set in too many places; the most useful and in retrospect most obvious place is in handling of userauth requests: https://github.com/openssh/openssh-portable/blob/71e48bc7945f867029e50e06c665c66aed6d3c64/auth2.c#L215Authmethod
structs (format defined here) created by the variousauth2-*
modules (one for each implemented auth backend - kerberos, password, publickey, hostbased, etc)name
,userauth
,enabled
structures, withuserauth
being a pointer to an implementation function (so e.g. the one for password auth is referencinguserauth_passwd
inauth2-passwd.c
(here.)userauth
func is called and the result stored asauthenticated
var: https://github.com/openssh/openssh-portable/blob/71e48bc7945f867029e50e06c665c66aed6d3c64/auth2.c#L287authctxt->success = 1
, etc: https://github.com/openssh/openssh-portable/blob/71e48bc7945f867029e50e06c665c66aed6d3c64/auth2.c#L352-L360->valid
flag seems to actually just be "is the requested username a valid local system user": https://github.com/openssh/openssh-portable/blob/71e48bc7945f867029e50e06c665c66aed6d3c64/auth2.c#L236-L239getpwnamallow
, which (basically) wraps the syscallgetpwnam
aka "get password entry for user" (soauthctxt->pw
is specifically that structure and not just a password)success
andauthenticated
.Initial distillation
->valid
flag is set byinput_userauth_request
, meaning the client has to actually submit auth in order to set it. (Ditto->pw
.)authctxt->pw
andauthctxt->valid
will be null, and thussession_open
should call line 1767 and thus result infatal("no user for session")
.Testing with live OpenSSH server
Proving this with a live install is interesting:
Ran local docker container executing Ubuntu + OpenSSH 7.2 on port 2222 with nothing but root password auth by default
Executed Matthijs's
client-test.py
with nothing but the port number changedDid not get expected
no user for session
but instead seem to have just confused the poor thing:Second dive
dispatch_protocol_error()
do_authentication2
here: https://github.com/openssh/openssh-portable/blob/de1920d743d295f50e6905e5957c4172c038e8eb/auth2.c#L172SSH2_MSG_UNIMPLEMENTED
, then the table is filled in with what is intended to be responded to (e.g. indo_authentication2
, the very next line is to say "ok and now respond to service requests")SSH_MSG_CHANNEL_OPEN
, aka what our test client was requesting: https://tools.ietf.org/html/rfc4250#section-4.1.2server_init_dispatch
: https://github.com/openssh/openssh-portable/blob/de1920d743d295f50e6905e5957c4172c038e8eb/serverloop.c#L393End result
SHOULD
or aMUST
.SSH_MSG_UNIMPLEMENTED
.paramiko/paramiko/transport.py
Lines 1891 to 1917 in 27a8ed1
Doing nothing certainly seems like a bad idea: this is clearly a massive security flaw, and the only reason I did all the above investigation is because software has an irritating history of "but I was relying on that bug / looseness in the spec / whatever!". Given the main reference implementation disallows it, I'm inclined to assume nobody could possibly rely on this.
So there's two obvious fixes for Paramiko:
self.is_authenticated
(which, impressively, appears to only ever be used in__repr__
...!!!).auth_handler
and/or.is_authenticated
in specific spots such asServer.check_channel_request
(here)__init__
onServer
subclasses for the passing-in of a reference to one of the other objects, since right nowServer
doesn't even define one!) though I would like to examine it sometime. Now probably not the best time though.Transport
though, such as around thecheck_channel_request
calls here:paramiko/paramiko/transport.py
Lines 2531 to 2537 in 27a8ed1
My gut says to take a quick stab at the 1st approach but to fall back to the
2nd if the 1st cannot be done relatively painlessly.
Either way, re: the actual action to take seems poorly defined, but esp given OpenSSH simply spits out a bunch of question marks and not a "useful" error; the RFCs (4250, 4254) list only 4 default 'error' types, of which
OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED
seems the closest fit. And indeed Paramiko uses it for eg bogus channel types, in some legacy tests.The text was updated successfully, but these errors were encountered: