Author:Knownsec 404 Security Research Team
Date:2018/10/25
Chinese version:https://paper.seebug.org/720/
A libSSH authentication bypass vulnerability was discovered recently. At first, I felt that this vulnerability might be quite powerful. Then the PoC appeared on github immediately and exp was added on msf. But I found it couldn’t getshell during the process. So I conducted an in-depth analysis of it.
Source code for both versions of 0.7.5 and 0.7.6:
360 released an analysis article with a graph of getshell:
The Python version of PoC can be found on Github:
libSSH-0.7.5 source code download address:
PS: Please install the missing dependencies yourself. There is no original compilation record, and I am too lazy to do it again.
$ tar -xf libssh-0.7.5.tar.xz
$ cd libssh-0.7.5
$ mkdir build
$ cd build
$ cmake -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Debug ..
$ make
Mainly using two. One is the SSH server Demo: examples/ssh_server_fork
, and the other is the SSH client Demo: ./examples/samplessh
.
Server startup command: sudo examples/ssh_server_fork -p 22221 127.0.0.1 -v
Client using command: ./examples/samplessh -p 22221 [email protected]
PS: The user just fills in. I used myuser just to compare the normal authentication request with the bypass request.
Modify the ssh_userauth_xxxx
function of ../src/auth.c
. I modified
ssh_userauth_password
:
According to 360’s analysis article and my own research results, I modified the three places shown in the arrow above, so that /examples/samplessh
become the PoC to verify this vulnerability.
PS: Don’t forget to execute make again after modifying the source code.
According to the debug information output by the server, you can find the ssh_packet_process
function, see line 1211:
1121 r=cb->callbacks[type - cb->start](session,type,session->in_buffer,cb->user);
Then trace to the callbacks
array equal to default_packet_handlers
Normally, send SSH2_MSG_USERAUTH_REQUEST
request can get into ssh_packet_userauth_request
function. The exploited point of this vulnerability is that send SSH2_MSG_USERAUTH_SUCCESS
request can get into the ssh_packet_userauth_success
function.
PS: We can get into any function of this array. But after reading other functions, there is no way to getshell.
The execution path under normal conditions is:
ssh_packet_userauth_request ->
ssh_message_queue ->
ssh_execute_server_callbacks ->
ssh_execute_server_request ->
rc = session->server_callbacks->auth_password_function(session,
msg->auth_request.username, msg->auth_request.password,
session->server_callbacks->userdata);
You can see this function. I found that it was set in the server Demo:
// examples/ssh_server_fork.c
......
514 struct ssh_server_callbacks_struct server_cb = {
515 .userdata = &sdata,
516 .auth_password_function = auth_password,
517 .channel_open_request_session_function = channel_open,
518 };
519 ssh_callbacks_init(&server_cb);
520 ssh_callbacks_init(&channel_cb);
521 ssh_set_server_callbacks(session, &server_cb);
......
Found the auth_password
function, set by the server's author:
// examples/ssh_server_fork.c
static int auth_password(ssh_session session, const char *user,
const char *pass, void *userdata) {
struct session_data_struct *sdata = (struct session_data_struct *) userdata;
(void) session;
if (strcmp(user, USER) == 0 && strcmp(pass, PASS) == 0) {
sdata->authenticated = 1;
return SSH_AUTH_SUCCESS;
}
sdata->auth_attempts++;
return SSH_AUTH_DENIED;
}
Path after successful authentication:
ssh_message_auth_reply_success ->
ssh_auth_reply_success:
994 session->session_state = SSH_SESSION_STATE_AUTHENTICATED;
995 session->flags |= SSH_SESSION_FLAG_AUTHENTICATED;
Under normal condition, after SSH login succeeds, libSSH sets the session to the authentication status. The person who wrote the SSH server sets the flag defined by himself to 1: sdata->authenticated = 1;
I utilized this vulnerability to bypass verification. The process of the server:
ssh_packet_userauth_success:
SSH_LOG(SSH_LOG_DEBUG, "Authentication successful");
SSH_LOG(SSH_LOG_TRACE, "Received SSH_USERAUTH_SUCCESS");
session->auth_state=SSH_AUTH_STATE_SUCCESS;
session->session_state=SSH_SESSION_STATE_AUTHENTICATED;
session->flags |= SSH_SESSION_FLAG_AUTHENTICATED;
The libSSH session can be successfully set to the state of successful authentication, but it will not enter the auth_password
function, so the user-defined flag sdata->authenticated
is still equal to 0.
The graph that we can see verified PoC successfully by other people on the internet is the Authenticated successful
that output by the ssh_packet_userauth_success
function.
When many people recur the vulnerability, they can find that the server debugging information has output authenticated successfully, but it has not been successful in getshell. According to the above code, I find that the session has been set for the successful authentication, but why can’t I get shell permission? In this regard, I continue to explore in depth.
According to the debugging information of the server, I found that the channel
can be successfully opened, but in the next step pty-req channel_request
the information displayed on my server is rejected:
So I continued to track the flow of code execution and traced the ssh_execute_server_request
function:
166 case SSH_REQUEST_CHANNEL:
channel = msg->channel_request.channel;
if (msg->channel_request.type == SSH_CHANNEL_REQUEST_PTY &&
ssh_callbacks_exists(channel->callbacks, channel_pty_request_function)) {
rc = channel->callbacks->channel_pty_request_function(session, channel,
msg->channel_request.TERM,
msg->channel_request.width, msg->channel_request.height,
msg->channel_request.pxwidth, msg->channel_request.pxheight,
channel->callbacks->userdata);
if (rc == 0) {
ssh_message_channel_request_reply_success(msg);
} else {
ssh_message_reply_default(msg);
}
return SSH_OK;
Next, we found the ssh_callbacks_exists(channel->callbacks, channel_pty_request_function)
check failed, so not entered the branch and caused the request to be rejected.
Then backtrack channel->callbacks
, back to the SSH server ssh_server_fork.c
:
530 ssh_set_auth_methods(session, SSH_AUTH_METHOD_PASSWORD);
531 ssh_event_add_session(event, session);
532 n = 0;
533 while (sdata.authenticated == 0 || sdata.channel == NULL) {
534 /* If the user has used up all attempts, or if he hasn't been able to
535 * authenticate in 10 seconds (n * 100ms), disconnect. */
536 if (sdata.auth_attempts >= 3 || n >= 100) {
537 return;
538 }
539 if (ssh_event_dopoll(event, 100) == SSH_ERROR) {
540 fprintf(stderr, "%s\n", ssh_get_error(session));
541 return;
542 }
543 n++;
544 }
545 ssh_set_channel_callbacks(sdata.channel, &channel_cb);
There is no code in libSSH to set the callback function of the channel, as long as it is set manually by the developer in the server, such as the above code of 545 lines.
Then we could see the variable sdata.authenticated
again. The vulnerability bypassed authentication can only set the session to the authentication state, but could not modify the sdata.authenticated
variable defined by the SSH server developer. So the loop will not jump out until n = 100
, return ends the function. This led us to fail to getshell.
If you want to getshell, there are two ways to modify it:
1.Delete the sdata.authenticated
variable.
533 while (sdata.channel == NULL) {
......
544 }
2.Move this code which adds the channel to the callback function before the loop.
530 ssh_set_auth_methods(session, SSH_AUTH_METHOD_PASSWORD);
531 ssh_event_add_session(event, session);
532 ssh_set_channel_callbacks(sdata.channel, &channel_cb);
533 n = 0;
534 while (sdata.authenticated == 0 || sdata.channel == NULL) {
......
I can getshell successfully after modifying the server code.
After that, I audited the other branches of the ssh_execute_server_request
function and found all the branches under the SSH_REQUEST_CHANNEL
:
SSH_CHANNEL_REQUEST_PTY
SSH_CHANNEL_REQUEST_SHELL
SSH_CHANNEL_REQUEST_X11
SSH_CHANNEL_REQUEST_WINDOW_CHANGE
SSH_CHANNEL_REQUEST_EXEC
SSH_CHANNEL_REQUEST_ENV
SSH_CHANNEL_REQUEST_SUBSYSTEM
It is a callback function which calls the channel
. So if the callback function is not registered, the getshell will fail.
Finally, it is concluded that CVE-2018-10933 is not as harmful as expected. And according to the banner, I think thousands of ssh targets that use libssh on the Internet are the ssh server in the official libssh demo. A vulnerable version can indeed bypass authentication, but it cannot getshell.