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.

Introduction

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:

Condition

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.

Vulnerability analysis

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.

The mystery of getshell failure

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.

Summary

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.

Reference

  1. https://0x48.pw/libssh/
  2. https://www.anquanke.com/post/id/162225
  3. https://github.com/search?utf8=%E2%9C%93&q=CVE-2018-10933&type=
  4. https://www.libssh.org/files/0.7/libssh-0.7.5.tar.xz
  5. https://0x48.pw/libssh/libssh_0.7.6/src/packet.c.html#ssh_packet_process
  6. https://0x48.pw/libssh/libssh_0.7.5/src/packet.c.html#default_packet_handlers

源链接

Hacking more

...